From 1f04117bbf41580f3d20c33d5c71b7fb71899998 Mon Sep 17 00:00:00 2001 From: DongHyeonka Date: Mon, 7 Sep 2026 15:02:25 +0900 Subject: [PATCH] =?UTF-8?q?docs(clean-architecture-backend-template):=20?= =?UTF-8?q?=EC=A0=9C1=EB=B6=80=EA=B0=80=20=EC=B1=84=ED=83=9D=ED=95=9C=20?= =?UTF-8?q?=EA=B2=83=EB=A7=8C=20=EA=B8=80=EA=B0=90=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=82=A8=EA=B8=B0=EA=B3=A0=20=EB=8B=A4=EC=8B=9C=20=EA=B3=A0?= =?UTF-8?q?=EB=A5=B8=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 글감 1,001개 중 제1부(§3~§11) 앵커를 하나라도 가진 것은 112개뿐이었다. 나머지 889개는 제2부 모듈 분석 65편의 절 제목에서 나온 것이고, 그것이 재판정이 필요했던 이유다. 주제 44 → 16 (43개가 독자 질문 없이 있었다. 지금은 전부 있다) 글감 1,001 → 123 (제1부 앵커 112 + 제1부가 채택했는데 비어 있던 자리 11) 후보 965 → 1,088 · PENDING 905 → 0 error 3,042 → 0 내려온 889개는 후보 대장에 KEEP_IN_SSOT 로 남는다 — 버린 것이 아니라 분석에 남기고 독립 기록으로 만들지 않기로 한 것이다. 그 글감을 받치던 기록 파일 828개는 지웠다. 계약이 정본이고, 파일이 남아 있다는 이유로 계약에서 뺀 주제가 되살아나면 안 된다. 이력에는 그대로 있다 — git checkout a0ca2bb -- <경로>. 제1부가 채택했는데 글감이 없던 자리 열하나를 채웠다: mongo high-water mark 가 재전달 이벤트를 삼킨 P1, admin plane 이 가드만 켜고 서비스는 켜지 않은 것과 그 짝인 결정, 실패 어휘 세 층과 SQLState 매트릭스 병합 규칙, 부하 아래에서만 새는 admission 경계, 발행 증거와 완료 판정의 분리, keyset·JSONB 결정 둘. Concept 17개에 basis-version 을 채우고, 계약 제목과 기록 제목이 갈라져 있던 23건을 기록 쪽에 맞췄다. candidateScope 에 excludedAnchorPattern 을 적어 제2부 앵커만 가진 글감이 다시 올라올 수 없게 한다. Co-Authored-By: Claude Opus 5 --- .../skills/writing-tech-log-records/SKILL.md | 16 +- .../references/from-ssot-to-records.md | 33 + .../references/review-checklist.md | 2 + .../references/tech-log-tree-contract.md | 50 +- .../concept-adapter-inbound-graphql-c04.md | 63 - .../concept-adapter-inbound-graphql-c05.md | 40 - .../concept-adapter-inbound-web-c05.md | 46 - .../concept-adapter-inbound-web-c14.md | 48 - .../concept-adapter-inbound-web-c18.md | 46 - ...oncept-adapter-outbound-cache-redis-c04.md | 45 - ...oncept-adapter-outbound-cache-redis-c05.md | 44 - ...oncept-adapter-outbound-cache-redis-c12.md | 58 - ...concept-adapter-outbound-fileserver-c06.md | 48 - ...concept-adapter-outbound-httpclient-c01.md | 56 - ...concept-adapter-outbound-httpclient-c05.md | 56 - ...concept-adapter-outbound-httpclient-c08.md | 48 - ...cept-adapter-outbound-objectstorage-c02.md | 50 - .../concept/concept-application-core-c08.md | 53 - .../concept-grpc-advanced-bootstrap-c01.md | 51 - .../concept-grpc-advanced-streaming-c01.md | 44 - .../concept-messaging-admin-api-c04.md | 44 - .../concept/concept-messaging-policy-c05.md | 58 - .../concept-messaging-schema-avro-c07.md | 55 - ...essaging-spring-cloud-stream-bridge-c03.md | 76 - .../concept/concept-shared-contract-c02.md | 40 - .../case/case-a06-f018-changestreams-false.md | 204 - .../case-autoconfiguration-in-name-only.md | 90 - ...nditionalonbean-evaluated-at-parse-time.md | 90 - ...wing-the-scan-orphaned-eight-components.md | 104 - .../concept-when-conditions-are-evaluated.md | 114 - ...conditional-evaluation-order-unverified.md | 2 +- ...ence-a-bean-is-not-composition-evidence.md | 2 +- ...e-conditionalonbean-must-be-satisfiable.md | 2 +- ...t-the-frameworks-own-autoconfigurations.md | 53 - .../reference-off-must-be-structural.md | 2 +- ...a-report-that-cannot-carry-a-datasource.md | 125 - .../case-a05-f004-retrydecision-reason.md | 133 - .../case/case-cursor-verification-order.md | 101 - .../reference-reject-rather-than-sanitize.md | 55 - .../case/case-a10-f001-readme.md | 224 - .../case/case-a10-f004-pub-sub.md | 219 - .../case/case-a10-f005-hyperloglog-merge.md | 215 - .../case/case-a10-f006-requireidentifier.md | 216 - .../case/case-analysis-finding-a10-f003.md | 108 - .../case/case-analysis-finding-a10-f007.md | 123 - .../case/case-analysis-finding-a10-f008.md | 110 - .../concept-adapter-inbound-web-c01.md | 56 - .../concept-adapter-inbound-web-c17.md | 48 - ...cept-adapter-outbound-objectstorage-c01.md | 57 - ...cept-adapter-outbound-objectstorage-c08.md | 46 - ...pt-adapter-outbound-persistence-jpa-c24.md | 49 - .../concept/concept-app-bootstrap-c01.md | 46 - .../concept/concept-app-bootstrap-c02.md | 46 - .../concept/concept-app-bootstrap-c03.md | 40 - .../concept-grpc-advanced-diagnostics-c02.md | 51 - ...-messaging-kafka-share-experimental-c07.md | 57 - .../concept/concept-messaging-testkit-c05.md | 46 - ...g-without-the-topology-that-delivers-it.md | 103 - ...bility-true-and-its-validator-never-run.md | 133 - .../case/case-a05-f006-stable.md | 149 - .../case/case-a05-f022-stable.md | 185 - ...ys-nothing-is-deployed-and-eighteen-are.md | 87 - ...rt-and-the-validator-answer-differently.md | 95 - ...pt-three-sources-of-a-capability-answer.md | 97 - ...y-constant-must-derive-from-the-profile.md | 59 - ...a-flag-is-set-by-the-code-that-reads-it.md | 57 - .../concept-adapter-inbound-graphql-c03.md | 44 - .../concept-adapter-inbound-graphql-c06.md | 53 - .../concept-adapter-inbound-graphql-c09.md | 61 - .../concept-adapter-inbound-web-c02.md | 54 - .../concept-adapter-inbound-web-c12.md | 40 - .../concept-adapter-inbound-web-c13.md | 50 - .../concept-adapter-inbound-websocket-c01.md | 55 - ...oncept-adapter-outbound-cache-redis-c01.md | 51 - ...oncept-adapter-outbound-cache-redis-c02.md | 44 - ...concept-adapter-outbound-fileserver-c01.md | 44 - ...ncept-adapter-outbound-notification-c01.md | 63 - ...ncept-adapter-outbound-notification-c02.md | 44 - ...pt-adapter-outbound-persistence-jpa-c56.md | 53 - ...-adapter-outbound-persistence-mongo-c01.md | 55 - ...-adapter-outbound-persistence-mongo-c08.md | 49 - .../concept-adapter-outbound-support-c01.md | 51 - .../concept-grpc-advanced-bootstrap-c02.md | 55 - .../concept/concept-grpc-observability-c02.md | 40 - .../concept-messaging-admin-runtime-c02.md | 49 - ...-messaging-kafka-share-experimental-c01.md | 70 - ...-messaging-kafka-share-experimental-c02.md | 55 - ...concept-messaging-nats-experimental-c02.md | 48 - .../concept-messaging-runtime-core-c07.md | 59 - .../concept/concept-messaging-security-c02.md | 62 - ...ncept-messaging-spring-boot-starter-c01.md | 63 - ...essaging-spring-cloud-stream-bridge-c01.md | 65 - .../case/case-analysis-finding-a18-f001.md | 110 - .../case/case-analysis-finding-a18-f002.md | 124 - .../case-grpc-advanced-diagnostics-f03.md | 95 - .../case/case-grpc-advanced-resilience-f02.md | 76 - .../case/case-grpc-advanced-streaming-f01.md | 84 - .../case/case-grpc-advanced-streaming-f02.md | 78 - .../case/case-grpc-core-api-f05.md | 88 - .../case/case-grpc-discovery-f01.md | 80 - .../case-grpc-operation-ledger-jpa-f01.md | 82 - .../case/case-messaging-core-api-f05.md | 74 - .../case/case-messaging-observability-f07.md | 75 - .../case-messaging-reliability-api-f05.md | 77 - .../case-messaging-reliability-api-f08.md | 77 - .../openquestion-messaging-claim-check-f05.md | 50 - ...nce-messaging-inbox-jdbc-postgresql-f05.md | 52 - ...nce-messaging-inbox-jdbc-postgresql-f06.md | 52 - ...check-has-no-wiring-line-in-the-starter.md | 66 - ...has-no-applier-and-collides-on-adoption.md | 82 - ...-turns-on-the-guard-and-not-the-service.md | 75 - ...-contract-test-lives-outside-the-family.md | 70 - ...wenty-five-main-files-and-one-test-file.md | 66 - .../case/case-a05-f019-ssot.md | 141 - .../case/case-a06-f016-flamingock.md | 186 - .../case/case-grpc-advanced-bootstrap-f02.md | 86 - .../case/case-grpc-advanced-edition-f02.md | 78 - .../case/case-grpc-advanced-resilience-f01.md | 88 - .../case/case-grpc-client-f04.md | 76 - .../case/case-grpc-discovery-f02.md | 74 - .../case/case-grpc-observability-f02.md | 90 - .../case/case-grpc-policy-f06.md | 92 - .../case/case-grpc-proto-contract-f02.md | 68 - .../case/case-grpc-server-f02.md | 78 - .../case/case-messaging-kafka-f01.md | 87 - .../case/case-messaging-observability-f01.md | 94 - ...se-messaging-outbox-jdbc-postgresql-f06.md | 76 - ...se-messaging-outbox-jdbc-postgresql-f07.md | 72 - .../case/case-messaging-schema-avro-f01.md | 86 - .../case-messaging-spring-boot-starter-f02.md | 102 - .../case-messaging-spring-boot-starter-f04.md | 85 - .../case/case-messaging-testkit-f02.md | 93 - .../case/case-messaging-testkit-f08.md | 70 - ...ion-messaging-inbox-jdbc-postgresql-f04.md | 50 - .../reference-messaging-cloudevents-f05.md | 50 - .../reference-messaging-observability-f06.md | 56 - .../reference-messaging-policy-f06.md | 52 - .../reference-messaging-policy-f07.md | 52 - ...reference-messaging-reliability-api-f06.md | 54 - .../reference-messaging-schema-api-f02.md | 48 - .../reference-messaging-schema-api-f03.md | 48 - ...essaging-spring-cloud-stream-bridge-f03.md | 43 - ...n-order-contract-with-no-implementation.md | 136 - .../concept-adapter-inbound-graphql-c08.md | 40 - ...oncept-adapter-outbound-cache-redis-c06.md | 50 - ...concept-adapter-outbound-fileserver-c05.md | 44 - ...concept-adapter-outbound-fileserver-c07.md | 44 - .../concept-adapter-outbound-messaging-c04.md | 51 - ...ncept-adapter-outbound-notification-c03.md | 48 - ...pt-adapter-outbound-persistence-jpa-c04.md | 55 - ...pt-adapter-outbound-persistence-jpa-c31.md | 44 - ...pt-adapter-outbound-persistence-jpa-c32.md | 41 - ...pt-adapter-outbound-persistence-jpa-c33.md | 44 - ...-adapter-outbound-persistence-mongo-c07.md | 67 - .../concept/concept-application-core-c06.md | 49 - .../concept/concept-application-core-c07.md | 49 - .../concept/concept-application-core-c11.md | 49 - .../concept/concept-domain-core-c02.md | 48 - .../concept/concept-grpc-admin-c01.md | 53 - .../concept/concept-grpc-admin-c02.md | 54 - .../concept-grpc-advanced-resilience-c02.md | 50 - .../concept-messaging-admin-api-c01.md | 53 - .../concept-messaging-admin-runtime-c01.md | 55 - .../concept-messaging-admin-runtime-c04.md | 44 - .../concept-messaging-cloudevents-c04.md | 60 - .../concept/concept-messaging-core-api-c04.md | 57 - .../concept/concept-messaging-core-api-c06.md | 44 - .../concept/concept-messaging-kafka-c01.md | 51 - ...-messaging-kafka-share-experimental-c04.md | 53 - ...-messaging-kafka-share-experimental-c05.md | 59 - ...concept-messaging-nats-experimental-c01.md | 48 - .../concept-messaging-observability-c03.md | 72 - .../concept-messaging-observability-c04.md | 53 - .../concept-messaging-observability-c05.md | 49 - ...pt-messaging-outbox-jdbc-postgresql-c03.md | 60 - .../concept/concept-messaging-policy-c02.md | 53 - .../concept/concept-messaging-policy-c04.md | 53 - .../concept/concept-messaging-policy-c07.md | 52 - .../concept/concept-messaging-policy-c08.md | 52 - .../concept/concept-messaging-rabbit-c01.md | 49 - .../concept-messaging-runtime-core-c01.md | 55 - .../concept-messaging-runtime-core-c02.md | 51 - .../concept-messaging-runtime-core-c03.md | 74 - .../concept-messaging-runtime-core-c05.md | 55 - .../concept-messaging-schema-json-c01.md | 54 - .../concept-messaging-schema-json-c05.md | 49 - .../concept-messaging-schema-protobuf-c03.md | 64 - .../concept/concept-messaging-security-c05.md | 65 - ...ncept-messaging-spring-boot-starter-c02.md | 49 - ...ncept-messaging-spring-boot-starter-c03.md | 54 - ...essaging-spring-cloud-stream-bridge-c02.md | 57 - ...essaging-spring-cloud-stream-bridge-c04.md | 55 - ...essaging-spring-cloud-stream-bridge-c05.md | 53 - .../concept/concept-messaging-testkit-c01.md | 55 - .../concept/concept-messaging-testkit-c02.md | 44 - .../concept-messaging-transport-spi-c02.md | 47 - .../concept/concept-shared-contract-c01.md | 57 - ...ject-new-admission-that-rejects-nothing.md | 85 - ...ast-step-of-secret-erasure-is-not-wired.md | 89 - ...ncept-the-eight-phase-shutdown-contract.md | 95 - ...gate-only-if-something-reads-that-order.md | 55 - ...rence-numbers-in-docs-should-be-derived.md | 57 - ...-a-decision-once-and-not-the-other-time.md | 96 - ...decision-one-audit-mechanism-per-entity.md | 54 - ...erence-two-vocabularies-for-one-concept.md | 55 - ...eported-as-a-permanent-business-failure.md | 113 - ...enial-recorded-as-a-configuration-error.md | 142 - ...ontract-between-retry-dlq-and-dashboard.md | 55 - ...circumstance-is-not-a-permanent-failure.md | 55 - ...005-transferbufferpool-maxborrowedbytes.md | 190 - .../case/case-analysis-finding-a08-f002.md | 97 - .../case/case-analysis-finding-a08-f003.md | 96 - .../case/case-analysis-finding-a08-f004.md | 113 - .../case/case-analysis-finding-a09-f001.md | 117 - .../case/case-analysis-finding-a09-f004.md | 112 - .../case/case-analysis-finding-a09-f006.md | 98 - .../case-a-cleanup-claim-without-fencing.md | 111 - ...ad-then-delete-race-on-the-upload-lease.md | 114 - .../decision-no-physical-paths-in-metadata.md | 55 - ...le-state-must-be-complete-by-constraint.md | 55 - ...im-with-a-conditional-update-not-a-read.md | 60 - ...x-matching-fits-signatures-not-sniffing.md | 56 - .../case/case-a16-f002-oneof.md | 132 - ...ase-a16-f004-graphqloperationnamepolicy.md | 167 - .../case/case-analysis-finding-a16-f001.md | 128 - .../case/case-analysis-finding-a16-f003.md | 128 - .../case/case-analysis-finding-a16-f005.md | 122 - .../case/case-analysis-finding-a16-f006.md | 116 - .../case/case-analysis-finding-a16-f008.md | 90 - .../case/case-analysis-finding-a16-f011.md | 114 - .../case/case-analysis-finding-a16-f012.md | 112 - .../case/case-a20-f003-claude.md | 147 - ...0-f006-grpcadmissioncontroller-tryadmit.md | 147 - .../case/case-a20-f007-grpcstreamadmission.md | 148 - ...-grpcserializedstreamwriter-drop-oldest.md | 153 - .../case/case-a20-f010-grpcoutcomereplay.md | 152 - ...f011-grpccompletionreconciler-arraylist.md | 153 - .../case/case-analysis-finding-a15-f001.md | 129 - .../case/case-analysis-finding-a15-f002.md | 115 - .../case/case-analysis-finding-a20-f004.md | 112 - .../case/case-analysis-finding-a20-f005.md | 110 - .../case/case-a11-f001-close.md | 241 - .../case-a11-f002-pool-route-exceeds-total.md | 226 - .../case/case-a11-f004-number.md | 214 - .../case-a11-f006-boundeddatabufferflux.md | 216 - .../case/case-a11-f007-dns.md | 223 - .../case/case-analysis-finding-a11-f003.md | 103 - ...er-permit-that-leaks-on-local-rejection.md | 118 - ...assifier-sees-only-what-the-engine-kept.md | 55 - ...ity-needs-both-idempotency-and-category.md | 58 - ...ference-verify-runtime-shape-at-runtime.md | 62 - ...walk-the-cause-chain-most-specific-wins.md | 61 - .../case/case-a07-f001-uuidcodec.md | 176 - .../case/case-a07-f002-normalize.md | 208 - .../case/case-a07-f004-claude.md | 148 - .../case/case-a07-f006-claude.md | 157 - .../case/case-analysis-finding-a07-f005.md | 99 - .../case/case-a01-f002-idfactory-newid.md | 128 - .../concept-adapter-inbound-web-c19.md | 46 - ...oncept-adapter-outbound-cache-redis-c03.md | 51 - ...ncept-adapter-outbound-notification-c05.md | 47 - ...ncept-adapter-outbound-notification-c06.md | 52 - ...cept-adapter-outbound-objectstorage-c09.md | 46 - .../concept/concept-app-bootstrap-c04.md | 40 - .../concept/concept-grpc-core-api-c01.md | 42 - .../concept/concept-messaging-testkit-c04.md | 65 - ...f001-check-verifyjsonschemaruntimegraph.md | 253 - .../case/case-a12-f002-jackson-databind.md | 223 - ...case-a19-f003-messaging-reliability-api.md | 134 - .../case-a19-f006-messaging-cloudevents.md | 127 - .../case/case-a19-f008-acl.md | 133 - .../case/case-a19-f014-kafka-msg.md | 160 - ...-a19-f015-compatibilitymatrix-extension.md | 130 - .../case/case-a19-f020-messaging-admin-api.md | 143 - ...f022-messagingpublicsurfacecontracttest.md | 122 - .../case/case-analysis-finding-a19-f001.md | 121 - .../case/case-analysis-finding-a19-f002.md | 122 - .../case/case-analysis-finding-a19-f004.md | 124 - .../case/case-analysis-finding-a19-f005.md | 114 - .../case/case-analysis-finding-a19-f009.md | 128 - .../case/case-analysis-finding-a19-f013.md | 100 - .../case/case-analysis-finding-a19-f018.md | 110 - .../case/case-analysis-finding-a19-f019.md | 135 - .../case/case-analysis-finding-a03-f001.md | 155 - .../case/case-analysis-finding-a04-f002.md | 165 - .../case/case-analysis-finding-a04-f004.md | 142 - .../case/case-analysis-finding-a04-f005.md | 146 - .../case/case-analysis-finding-a05-f001.md | 163 - .../case/case-analysis-finding-a05-f010.md | 132 - .../case/case-analysis-finding-a05-f023.md | 130 - .../case/case-analysis-finding-a05-f024.md | 139 - .../case/case-analysis-finding-a05-f028.md | 151 - .../case/case-analysis-finding-a05-f030.md | 146 - .../case/case-analysis-finding-a05-f031.md | 155 - .../case/case-analysis-finding-a05-f032.md | 156 - .../case/case-analysis-finding-a05-f034.md | 136 - .../case/case-analysis-finding-a06-f002.md | 148 - .../case/case-analysis-finding-a06-f004.md | 176 - .../case/case-analysis-finding-a06-f005.md | 170 - .../case/case-analysis-finding-a06-f007.md | 157 - .../case/case-analysis-finding-a06-f008.md | 154 - .../case/case-analysis-finding-a06-f009.md | 130 - .../case/case-analysis-finding-a06-f010.md | 156 - .../case/case-analysis-finding-a06-f014.md | 148 - .../case/case-analysis-finding-a06-f015.md | 148 - .../case/case-analysis-finding-a06-f019.md | 145 - .../case/case-analysis-finding-a06-f021.md | 138 - .../case/case-analysis-finding-a06-f022.md | 146 - .../case/case-analysis-finding-a06-f023.md | 148 - .../case/case-analysis-finding-a06-f024.md | 94 - .../case/case-analysis-finding-a06-f025.md | 95 - .../case/case-analysis-finding-a06-f026.md | 112 - .../case/case-analysis-finding-a06-f027.md | 101 - .../case/case-analysis-finding-a06-f028.md | 96 - ...ch-path-survived-the-return-to-the-pool.md | 89 - ...nt-pools-summed-past-the-server-ceiling.md | 87 - .../concept-four-multitenancy-strategies.md | 102 - .../question/openquestion-a02-f001-lro.md | 60 - .../openquestion-a02-f002-domaincontextkey.md | 60 - ...-a05-f003-capabilitysupport-constraints.md | 64 - .../openquestion-analysis-finding-a02-f003.md | 62 - .../openquestion-analysis-finding-a02-f004.md | 58 - .../openquestion-analysis-finding-a02-f005.md | 58 - .../openquestion-analysis-finding-a03-f002.md | 62 - .../openquestion-analysis-finding-a03-f004.md | 64 - .../openquestion-analysis-finding-a04-f006.md | 60 - ...ion-performance-and-capacity-unmeasured.md | 72 - .../reference-analysis-finding-a03-f003.md | 56 - ...rnate-filter-is-not-a-security-boundary.md | 57 - ...tion-settings-must-be-transaction-local.md | 62 - ...lumn-belongs-in-every-unique-constraint.md | 55 - ...d-a-generation-that-cannot-be-reclaimed.md | 143 - .../case/case-a05-f026-enqueue.md | 201 - ...-leaks-and-a-counter-that-cannot-return.md | 150 - ...g-began-and-a-service-came-back-serving.md | 89 - .../concept-check-then-act-on-atomic-types.md | 93 - ...and-the-set-it-covers-are-one-operation.md | 57 - ...002-authentication-failed-resumehealthy.md | 272 - .../case/case-a13-f003-accesscontext.md | 205 - .../case/case-a13-f004-thymeleaf.md | 208 - ...05-retry-after-illegalargumentexception.md | 304 - .../case/case-a13-f008-host.md | 184 - .../case/case-a13-f009-sigv4-string.md | 206 - .../case/case-analysis-finding-a13-f007.md | 122 - .../case/case-analysis-finding-a13-f010.md | 126 - .../case/case-analysis-finding-a13-f011.md | 93 - .../case-endpoint-check-only-on-upload.md | 82 - .../case-the-last-part-cannot-get-a-grant.md | 82 - ...-legacy-adoption-requires-two-approvers.md | 61 - ...a-binary-approval-codec-must-round-trip.md | 60 - ...erful-half-must-not-be-one-setting-away.md | 58 - .../concept-adapter-inbound-web-c15.md | 46 - ...pt-adapter-outbound-persistence-jpa-c44.md | 40 - .../concept-adapter-outbound-support-c02.md | 57 - .../concept-adapter-outbound-support-c06.md | 43 - .../concept/concept-grpc-observability-c03.md | 66 - .../concept-messaging-admin-runtime-c05.md | 47 - .../concept-messaging-observability-c02.md | 52 - ...ed-redrive-skips-what-it-could-not-move.md | 142 - ...means-startup-fails-and-nothing-runs-it.md | 107 - ...roval-survived-on-the-irreversible-half.md | 92 - ...concept-approval-verification-execution.md | 95 - ...ference-resume-by-identity-not-by-index.md | 55 - ...ng-and-verifying-do-not-share-an-object.md | 59 - ...-a-digest-that-covered-who-but-not-what.md | 116 - ...-expired-claim-versus-expired-execution.md | 99 - ...sion-state-machines-carry-no-stereotype.md | 56 - ...est-must-be-length-framed-and-versioned.md | 58 - ...ired-claim-and-expired-execution-differ.md | 58 - .../concept-adapter-inbound-graphql-c10.md | 45 - .../concept-adapter-inbound-web-c06.md | 40 - ...pt-adapter-outbound-persistence-jpa-c27.md | 47 - ...pt-adapter-outbound-persistence-jpa-c28.md | 52 - ...pt-adapter-outbound-persistence-jpa-c29.md | 52 - .../concept/concept-application-core-c01.md | 68 - ...e-a-build-gate-that-is-not-in-the-build.md | 2 +- ...ision-unclassified-commands-are-refused.md | 60 - ...admission-point-must-count-its-bypasses.md | 60 - ...nce-server-metadata-defines-the-command.md | 58 - .../concept-adapter-inbound-grpc-c01.md | 49 - ...oncept-adapter-outbound-cache-redis-c09.md | 40 - .../concept/concept-domain-core-c03.md | 50 - .../concept-messaging-cloudevents-c08.md | 51 - ...ept-messaging-inbox-jdbc-postgresql-c07.md | 47 - ...ncept-messaging-pulsar-experimental-c01.md | 51 - .../concept-messaging-schema-api-c07.md | 50 - .../concept/concept-messaging-testkit-c06.md | 48 - .../concept-messaging-transport-spi-c07.md | 54 - .../concept/concept-shared-contract-c03.md | 44 - ...se-a-replay-store-with-no-eviction-path.md | 117 - ...anup-that-causes-the-outage-it-prevents.md | 98 - ...cept-bounded-and-unbounded-side-by-side.md | 91 - ...nnot-show-the-property-is-not-a-witness.md | 57 - ...rs-both-forms-has-chosen-the-unsafe-one.md | 55 - ...d-one-enforcement-point-per-safety-rule.md | 55 - .../case/case-a05-f005-jparetrypolicy.md | 132 - .../case/case-grpc-core-api-f04.md | 84 - .../case/case-grpc-observability-f03.md | 84 - .../case/case-grpc-spring-boot-starter-f04.md | 86 - .../case/case-messaging-claim-check-f03.md | 75 - .../case/case-messaging-core-api-f04.md | 68 - ...se-messaging-outbox-jdbc-postgresql-f05.md | 74 - ...se-messaging-outbox-jdbc-postgresql-f08.md | 66 - .../case-messaging-spring-boot-starter-f03.md | 81 - ...-messaging-kafka-share-experimental-f04.md | 48 - .../reference-messaging-policy-f05.md | 52 - .../reference-messaging-runtime-core-f05.md | 52 - ...ase-a05-f007-transactionprofileregistry.md | 163 - .../case/case-a05-f018-sql.md | 138 - .../case/case-a06-f003-change-streams-true.md | 157 - .../case/case-grpc-advanced-compat-f01.md | 80 - .../case/case-grpc-core-api-f01.md | 91 - .../case/case-grpc-observability-f01.md | 90 - .../case/case-grpc-policy-f07.md | 82 - .../case/case-grpc-server-f03.md | 78 - .../case/case-grpc-spring-boot-starter-f01.md | 97 - .../case/case-grpc-spring-boot-starter-f03.md | 74 - .../case/case-grpc-testkit-f05.md | 82 - .../case/case-messaging-admin-api-f03.md | 68 - .../case/case-messaging-admin-api-f04.md | 74 - .../case/case-messaging-admin-api-f06.md | 66 - .../case/case-messaging-admin-runtime-f03.md | 77 - .../case/case-messaging-admin-runtime-f04.md | 68 - .../case/case-messaging-admin-runtime-f09.md | 66 - .../case/case-messaging-claim-check-f01.md | 90 - .../case/case-messaging-cloudevents-f02.md | 94 - ...ase-messaging-inbox-jdbc-postgresql-f01.md | 100 - .../case/case-messaging-observability-f02.md | 84 - ...se-messaging-outbox-jdbc-postgresql-f01.md | 95 - ...se-messaging-outbox-jdbc-postgresql-f04.md | 81 - .../case/case-messaging-rabbit-f04.md | 82 - .../case-messaging-reliability-api-f02.md | 92 - .../case/case-messaging-runtime-core-f01.md | 104 - .../case/case-messaging-runtime-core-f02.md | 84 - .../case/case-messaging-schema-api-f01.md | 90 - .../case/case-messaging-security-f04.md | 81 - .../case-messaging-spring-boot-starter-f01.md | 96 - .../case/case-messaging-testkit-f01.md | 95 - .../case/case-messaging-testkit-f04.md | 72 - .../case/case-messaging-testkit-f05.md | 68 - .../case/case-messaging-testkit-f06.md | 66 - ...lidator-is-the-only-reader-of-four-keys.md | 103 - ...e-validator-declared-and-never-injected.md | 101 - .../openquestion-messaging-core-api-f01.md | 46 - .../openquestion-messaging-core-api-f03.md | 46 - ...-messaging-kafka-share-experimental-f03.md | 54 - .../openquestion-messaging-policy-f02.md | 54 - ...nquestion-messaging-reliability-api-f03.md | 52 - .../openquestion-messaging-schema-json-f03.md | 48 - ...ce-a-validator-is-enforced-by-injection.md | 60 - .../reference-messaging-claim-check-f04.md | 50 - ...-messaging-kafka-share-experimental-f02.md | 48 - .../reference-messaging-runtime-core-f07.md | 52 - .../reference-messaging-security-f07.md | 52 - ...essaging-spring-cloud-stream-bridge-f01.md | 43 - ...essaging-spring-cloud-stream-bridge-f05.md | 43 - .../case/case-grpc-advanced-bootstrap-f05.md | 78 - .../case/case-grpc-advanced-edition-f01.md | 97 - .../case/case-grpc-codegen-f03.md | 84 - .../case/case-grpc-core-api-f06.md | 86 - .../case/case-grpc-policy-f03.md | 95 - .../case/case-grpc-proto-contract-f01.md | 78 - .../case/case-grpc-proto-contract-f03.md | 78 - .../case/case-grpc-proto-contract-f04.md | 80 - .../case/case-messaging-observability-f04.md | 73 - .../case/case-messaging-runtime-core-f03.md | 87 - .../case/case-messaging-schema-avro-f02.md | 86 - .../case/case-messaging-schema-json-f01.md | 88 - .../case-messaging-schema-protobuf-f04.md | 77 - .../openquestion-messaging-cloudevents-f04.md | 50 - ...nquestion-messaging-schema-protobuf-f03.md | 52 - .../reference-messaging-schema-avro-f03.md | 50 - .../reference-messaging-schema-avro-f05.md | 50 - .../concept-adapter-inbound-graphql-c02.md | 42 - .../concept-adapter-inbound-graphql-c07.md | 40 - .../concept-adapter-inbound-graphql-c11.md | 48 - .../concept-adapter-inbound-web-c08.md | 40 - .../concept-adapter-inbound-web-c10.md | 40 - .../concept-adapter-inbound-web-c11.md | 44 - .../concept-adapter-inbound-websocket-c03.md | 40 - .../concept-adapter-inbound-websocket-c04.md | 40 - ...oncept-adapter-outbound-cache-redis-c07.md | 57 - ...oncept-adapter-outbound-cache-redis-c11.md | 44 - ...concept-adapter-outbound-fileserver-c02.md | 50 - ...concept-adapter-outbound-httpclient-c03.md | 44 - ...concept-adapter-outbound-httpclient-c06.md | 51 - .../concept-adapter-outbound-messaging-c01.md | 58 - .../concept-adapter-outbound-messaging-c02.md | 44 - ...cept-adapter-outbound-objectstorage-c04.md | 44 - ...cept-adapter-outbound-objectstorage-c06.md | 52 - ...pt-adapter-outbound-persistence-jpa-c05.md | 67 - ...pt-adapter-outbound-persistence-jpa-c25.md | 51 - ...pt-adapter-outbound-persistence-jpa-c45.md | 44 - ...pt-adapter-outbound-persistence-jpa-c49.md | 40 - ...-adapter-outbound-persistence-mongo-c02.md | 60 - ...-adapter-outbound-persistence-mongo-c05.md | 48 - ...-adapter-outbound-persistence-mongo-c09.md | 48 - .../concept/concept-application-core-c10.md | 56 - .../concept/concept-grpc-policy-c01.md | 44 - .../concept-messaging-admin-api-c03.md | 69 - .../concept-messaging-cloudevents-c01.md | 56 - .../concept-messaging-cloudevents-c02.md | 51 - .../concept-messaging-cloudevents-c03.md | 62 - .../concept-messaging-cloudevents-c05.md | 45 - .../concept-messaging-cloudevents-c06.md | 51 - .../concept/concept-messaging-core-api-c02.md | 48 - .../concept/concept-messaging-core-api-c05.md | 47 - .../concept-messaging-schema-api-c02.md | 47 - .../concept-messaging-schema-api-c03.md | 66 - .../concept-messaging-schema-api-c04.md | 51 - .../concept-messaging-schema-avro-c01.md | 53 - .../concept-messaging-schema-avro-c02.md | 53 - .../concept-messaging-schema-avro-c03.md | 64 - .../concept-messaging-schema-avro-c04.md | 53 - .../concept-messaging-schema-avro-c05.md | 49 - .../concept-messaging-schema-json-c02.md | 53 - .../concept-messaging-schema-json-c03.md | 61 - .../concept-messaging-schema-json-c04.md | 43 - .../concept-messaging-schema-protobuf-c01.md | 51 - .../concept-messaging-schema-protobuf-c04.md | 49 - .../concept-messaging-schema-protobuf-c05.md | 51 - .../concept-messaging-schema-protobuf-c07.md | 47 - .../concept/concept-messaging-testkit-c03.md | 40 - .../concept/concept-shared-contract-c04.md | 44 - ...mizer-that-discarded-the-bound-property.md | 110 - ...istry-column-too-short-for-its-own-path.md | 87 - .../decision/decision-repair-is-not-a-mode.md | 56 - ...erence-an-applied-checksum-is-a-promise.md | 60 - ...ence-each-stream-owns-its-history-table.md | 62 - .../case/case-a06-f020-tls-stable.md | 199 - .../concept-adapter-inbound-graphql-c01.md | 49 - .../concept-adapter-inbound-web-c03.md | 58 - .../concept-adapter-inbound-web-c04.md | 60 - .../concept-adapter-inbound-web-c07.md | 40 - ...concept-adapter-outbound-httpclient-c07.md | 44 - ...concept-adapter-outbound-httpclient-c09.md | 59 - ...ncept-adapter-outbound-notification-c04.md | 51 - ...cept-adapter-outbound-objectstorage-c03.md | 42 - ...pt-adapter-outbound-persistence-jpa-c53.md | 40 - .../concept-adapter-outbound-support-c03.md | 44 - .../concept-adapter-outbound-support-c05.md | 48 - .../concept/concept-app-bootstrap-c05.md | 40 - .../concept/concept-application-core-c02.md | 58 - .../concept-grpc-advanced-bootstrap-c03.md | 53 - .../concept-grpc-advanced-diagnostics-c01.md | 49 - .../concept/concept-grpc-observability-c01.md | 54 - .../concept-grpc-spring-boot-starter-c01.md | 55 - .../concept-messaging-admin-api-c02.md | 49 - .../concept-messaging-admin-api-c05.md | 59 - .../concept/concept-messaging-rabbit-c02.md | 42 - ...ase-a06-f011-mongoregexpolicy-forbidden.md | 169 - .../case/case-grpc-admin-f02.md | 93 - .../case-grpc-advanced-diagnostics-f01.md | 97 - .../case-grpc-advanced-diagnostics-f02.md | 76 - .../case/case-grpc-advanced-edition-f03.md | 88 - .../case/case-grpc-core-api-f03.md | 88 - .../case/case-grpc-policy-f01.md | 89 - .../case/case-grpc-policy-f02.md | 97 - .../case/case-grpc-server-f04.md | 111 - .../case/case-grpc-testkit-f06.md | 86 - ...-ipv4-only-mask-passes-every-other-form.md | 103 - .../case/case-messaging-admin-api-f02.md | 76 - .../case/case-messaging-admin-api-f05.md | 78 - .../case/case-messaging-admin-api-f07.md | 68 - .../case/case-messaging-admin-runtime-f01.md | 87 - .../case/case-messaging-admin-runtime-f05.md | 74 - .../case/case-messaging-admin-runtime-f08.md | 71 - .../case/case-messaging-observability-f05.md | 77 - .../case/case-messaging-security-f01.md | 92 - .../case/case-messaging-security-f02.md | 92 - .../openquestion-messaging-security-f03.md | 54 - .../reference-messaging-security-f06.md | 52 - ...written-for-a-grade-that-does-not-exist.md | 120 - .../case/case-a05-f020-inspect-claim.md | 174 - .../case/case-a05-f021-complete-replayttl.md | 197 - .../case-a05-f025-filequotaservice-commit.md | 181 - .../case/case-a05-f027-maximum-attempts.md | 171 - .../concept-adapter-inbound-graphql-c12.md | 40 - .../concept-adapter-inbound-graphql-c13.md | 40 - .../concept-adapter-inbound-web-c09.md | 40 - .../concept-adapter-inbound-web-c16.md | 40 - .../concept-adapter-inbound-websocket-c05.md | 36 - ...oncept-adapter-outbound-cache-redis-c08.md | 49 - ...oncept-adapter-outbound-cache-redis-c10.md | 60 - ...concept-adapter-outbound-fileserver-c03.md | 53 - ...concept-adapter-outbound-fileserver-c04.md | 44 - ...concept-adapter-outbound-fileserver-c08.md | 49 - ...concept-adapter-outbound-httpclient-c02.md | 44 - ...concept-adapter-outbound-httpclient-c04.md | 53 - .../concept-adapter-outbound-messaging-c03.md | 48 - ...cept-adapter-outbound-objectstorage-c05.md | 49 - ...cept-adapter-outbound-objectstorage-c07.md | 46 - ...pt-adapter-outbound-persistence-jpa-c01.md | 48 - ...pt-adapter-outbound-persistence-jpa-c03.md | 51 - ...pt-adapter-outbound-persistence-jpa-c06.md | 53 - ...pt-adapter-outbound-persistence-jpa-c07.md | 40 - ...pt-adapter-outbound-persistence-jpa-c08.md | 40 - ...pt-adapter-outbound-persistence-jpa-c09.md | 53 - ...pt-adapter-outbound-persistence-jpa-c12.md | 52 - ...pt-adapter-outbound-persistence-jpa-c13.md | 55 - ...pt-adapter-outbound-persistence-jpa-c17.md | 40 - ...pt-adapter-outbound-persistence-jpa-c19.md | 44 - ...pt-adapter-outbound-persistence-jpa-c30.md | 44 - ...pt-adapter-outbound-persistence-jpa-c34.md | 40 - ...pt-adapter-outbound-persistence-jpa-c35.md | 54 - ...pt-adapter-outbound-persistence-jpa-c36.md | 59 - ...pt-adapter-outbound-persistence-jpa-c38.md | 44 - ...pt-adapter-outbound-persistence-jpa-c39.md | 47 - ...pt-adapter-outbound-persistence-jpa-c40.md | 41 - ...pt-adapter-outbound-persistence-jpa-c41.md | 46 - ...pt-adapter-outbound-persistence-jpa-c43.md | 42 - ...pt-adapter-outbound-persistence-jpa-c46.md | 40 - ...pt-adapter-outbound-persistence-jpa-c47.md | 40 - ...pt-adapter-outbound-persistence-jpa-c48.md | 44 - ...pt-adapter-outbound-persistence-jpa-c50.md | 40 - ...pt-adapter-outbound-persistence-jpa-c52.md | 40 - ...pt-adapter-outbound-persistence-jpa-c54.md | 40 - ...pt-adapter-outbound-persistence-jpa-c55.md | 40 - ...pt-adapter-outbound-persistence-jpa-c57.md | 44 - ...pt-adapter-outbound-persistence-jpa-c58.md | 48 - ...-adapter-outbound-persistence-mongo-c04.md | 49 - ...-adapter-outbound-persistence-mongo-c06.md | 52 - .../concept-adapter-outbound-support-c04.md | 44 - .../concept/concept-application-core-c04.md | 64 - .../concept/concept-application-core-c05.md | 57 - .../concept/concept-application-core-c09.md | 57 - .../concept/concept-domain-core-c01.md | 49 - .../concept-grpc-advanced-resilience-c01.md | 55 - .../concept/concept-grpc-codegen-c01.md | 55 - .../concept/concept-grpc-discovery-c01.md | 49 - .../concept-grpc-operation-ledger-jpa-c01.md | 40 - .../concept-grpc-operation-ledger-jpa-c02.md | 46 - .../concept-grpc-operation-ledger-jpa-c03.md | 47 - .../concept-grpc-spring-boot-starter-c02.md | 46 - .../concept/concept-grpc-testkit-c01.md | 46 - .../concept-messaging-admin-api-c06.md | 49 - .../concept-messaging-admin-api-c07.md | 48 - .../concept-messaging-admin-runtime-c03.md | 53 - .../concept-messaging-admin-runtime-c06.md | 53 - .../concept-messaging-admin-runtime-c07.md | 40 - .../concept-messaging-claim-check-c01.md | 54 - .../concept-messaging-claim-check-c02.md | 47 - .../concept-messaging-claim-check-c03.md | 64 - .../concept-messaging-claim-check-c04.md | 49 - .../concept-messaging-claim-check-c05.md | 47 - .../concept-messaging-claim-check-c06.md | 51 - .../concept-messaging-claim-check-c07.md | 56 - .../concept-messaging-cloudevents-c07.md | 47 - .../concept/concept-messaging-core-api-c01.md | 53 - .../concept/concept-messaging-core-api-c03.md | 46 - .../concept/concept-messaging-core-api-c07.md | 48 - .../concept/concept-messaging-core-api-c08.md | 40 - ...ept-messaging-inbox-jdbc-postgresql-c04.md | 47 - ...ept-messaging-inbox-jdbc-postgresql-c05.md | 47 - ...ept-messaging-inbox-jdbc-postgresql-c06.md | 60 - .../concept/concept-messaging-kafka-c02.md | 51 - ...-messaging-kafka-share-experimental-c03.md | 64 - ...-messaging-kafka-share-experimental-c06.md | 55 - .../concept-messaging-observability-c01.md | 54 - .../concept-messaging-observability-c06.md | 45 - .../concept-messaging-observability-c07.md | 45 - ...pt-messaging-outbox-jdbc-postgresql-c05.md | 53 - .../concept/concept-messaging-policy-c01.md | 58 - .../concept/concept-messaging-policy-c03.md | 67 - .../concept/concept-messaging-policy-c06.md | 57 - .../concept/concept-messaging-policy-c09.md | 49 - .../concept-messaging-reliability-api-c01.md | 58 - .../concept-messaging-reliability-api-c02.md | 49 - .../concept-messaging-reliability-api-c03.md | 67 - .../concept-messaging-reliability-api-c04.md | 49 - .../concept-messaging-reliability-api-c05.md | 49 - .../concept-messaging-reliability-api-c06.md | 58 - .../concept-messaging-reliability-api-c07.md | 49 - .../concept-messaging-runtime-core-c04.md | 53 - .../concept-messaging-runtime-core-c06.md | 51 - .../concept-messaging-schema-api-c01.md | 56 - .../concept-messaging-schema-api-c05.md | 47 - .../concept-messaging-schema-api-c06.md | 51 - .../concept-messaging-schema-avro-c06.md | 49 - .../concept-messaging-schema-json-c06.md | 45 - .../concept-messaging-schema-json-c07.md | 51 - .../concept-messaging-schema-protobuf-c02.md | 47 - .../concept-messaging-schema-protobuf-c06.md | 47 - .../concept/concept-messaging-security-c01.md | 66 - .../concept/concept-messaging-security-c03.md | 72 - .../concept/concept-messaging-security-c04.md | 53 - .../concept/concept-messaging-security-c06.md | 51 - .../concept/concept-messaging-security-c07.md | 51 - ...essaging-spring-cloud-stream-bridge-c06.md | 53 - .../concept/concept-messaging-testkit-c07.md | 50 - .../concept/concept-messaging-testkit-c08.md | 44 - .../concept-messaging-transport-spi-c01.md | 56 - .../concept-messaging-transport-spi-c03.md | 66 - .../concept-messaging-transport-spi-c04.md | 51 - .../concept-messaging-transport-spi-c05.md | 51 - .../concept-messaging-transport-spi-c06.md | 51 - .../case-a05-f029-for-update-skip-locked.md | 184 - .../case/case-a06-f013-recordapplied.md | 228 - ...ase-assigned-id-turns-claim-into-upsert.md | 110 - ...se-complete-drain-rolls-back-a-rotation.md | 107 - .../case/case-grpc-admin-f03.md | 78 - .../case/case-grpc-advanced-bootstrap-f03.md | 84 - .../case/case-grpc-advanced-resilience-f03.md | 101 - .../case/case-grpc-advanced-streaming-f03.md | 92 - .../case/case-grpc-client-f01.md | 89 - .../case/case-grpc-client-f02.md | 87 - .../case/case-grpc-client-f03.md | 80 - .../case/case-grpc-codegen-f02.md | 82 - .../case/case-grpc-discovery-f03.md | 97 - .../case-grpc-operation-ledger-jpa-f02.md | 79 - .../case/case-grpc-policy-f04.md | 77 - .../case/case-grpc-policy-f05.md | 91 - .../case/case-grpc-testkit-f02.md | 78 - .../case/case-messaging-admin-runtime-f06.md | 76 - .../case/case-messaging-admin-runtime-f07.md | 74 - .../case/case-messaging-rabbit-f02.md | 93 - .../reference-messaging-claim-check-f02.md | 50 - .../reference-messaging-security-f08.md | 52 - ...essaging-spring-cloud-stream-bridge-f04.md | 43 - ...essaging-spring-cloud-stream-bridge-f06.md | 43 - .../tech-log-studio/tech-log-tree.json | 30402 +++------------- .../concept-adapter-inbound-websocket-c02.md | 40 - ...oncept-adapter-outbound-cache-redis-c13.md | 53 - ...pt-adapter-outbound-persistence-jpa-c02.md | 55 - ...pt-adapter-outbound-persistence-jpa-c10.md | 44 - ...pt-adapter-outbound-persistence-jpa-c11.md | 44 - ...pt-adapter-outbound-persistence-jpa-c14.md | 40 - ...pt-adapter-outbound-persistence-jpa-c15.md | 51 - ...pt-adapter-outbound-persistence-jpa-c16.md | 48 - ...pt-adapter-outbound-persistence-jpa-c18.md | 48 - ...pt-adapter-outbound-persistence-jpa-c20.md | 47 - ...pt-adapter-outbound-persistence-jpa-c21.md | 42 - ...pt-adapter-outbound-persistence-jpa-c22.md | 40 - ...pt-adapter-outbound-persistence-jpa-c23.md | 44 - ...pt-adapter-outbound-persistence-jpa-c26.md | 51 - ...pt-adapter-outbound-persistence-jpa-c37.md | 55 - ...pt-adapter-outbound-persistence-jpa-c42.md | 53 - ...pt-adapter-outbound-persistence-jpa-c51.md | 49 - ...-adapter-outbound-persistence-mongo-c03.md | 57 - .../concept/concept-application-core-c03.md | 62 - .../concept/concept-grpc-server-c01.md | 58 - ...ept-messaging-inbox-jdbc-postgresql-c01.md | 61 - ...ept-messaging-inbox-jdbc-postgresql-c02.md | 51 - ...ept-messaging-inbox-jdbc-postgresql-c03.md | 72 - ...pt-messaging-outbox-jdbc-postgresql-c01.md | 53 - ...pt-messaging-outbox-jdbc-postgresql-c02.md | 44 - ...pt-messaging-outbox-jdbc-postgresql-c04.md | 46 - ...pt-messaging-outbox-jdbc-postgresql-c06.md | 51 - ...ond-string-that-broke-every-prod-deploy.md | 2 +- ...validator-checking-the-wrong-datasource.md | 167 - ...erence-deadline-narrows-in-three-stages.md | 60 - ...scoped-settings-outlive-the-transaction.md | 58 - ...rite-transactions-need-a-finite-timeout.md | 55 - ...ability-constant-outlives-its-condition.md | 108 - .../case/case-grpc-spring-boot-starter-f02.md | 74 - .../case/case-messaging-admin-runtime-f02.md | 83 - .../case/case-messaging-admin-runtime-f10.md | 68 - .../case/case-messaging-core-api-f02.md | 79 - ...ase-messaging-inbox-jdbc-postgresql-f03.md | 84 - .../case/case-messaging-kafka-f02.md | 97 - .../case/case-messaging-kafka-f03.md | 97 - .../case/case-messaging-kafka-f04.md | 91 - .../case/case-messaging-kafka-f05.md | 82 - ...-messaging-kafka-share-experimental-f01.md | 98 - .../case-messaging-nats-experimental-f02.md | 72 - .../case-messaging-nats-experimental-f03.md | 97 - .../case/case-messaging-policy-f01.md | 90 - .../case/case-messaging-policy-f03.md | 90 - .../case/case-messaging-policy-f04.md | 77 - .../case-messaging-pulsar-experimental-f01.md | 86 - .../case/case-messaging-rabbit-f01.md | 88 - .../case/case-messaging-rabbit-f03.md | 80 - .../case/case-messaging-rabbit-f05.md | 80 - .../case/case-messaging-testkit-f03.md | 74 - ...penquestion-messaging-observability-f03.md | 48 - ...essaging-spring-cloud-stream-bridge-f02.md | 56 - ...-messaging-kafka-share-experimental-f06.md | 48 - .../reference-messaging-runtime-core-f04.md | 52 - .../reference-messaging-schema-json-f02.md | 48 - .../reference-messaging-transport-spi-f02.md | 48 - .../reference-messaging-transport-spi-f03.md | 48 - .../case/case-grpc-admin-f01.md | 97 - .../case/case-grpc-advanced-bootstrap-f01.md | 88 - .../case/case-grpc-advanced-bootstrap-f04.md | 88 - .../case/case-grpc-advanced-compat-f02.md | 76 - .../case/case-grpc-advanced-compat-f03.md | 84 - .../case/case-grpc-codegen-f01.md | 80 - .../case/case-grpc-codegen-f04.md | 78 - .../case/case-grpc-core-api-f02.md | 86 - .../case/case-grpc-server-f01.md | 87 - .../case/case-grpc-testkit-f01.md | 93 - .../case/case-grpc-testkit-f03.md | 93 - .../case/case-grpc-testkit-f04.md | 82 - .../case/case-messaging-admin-api-f01.md | 89 - .../case/case-messaging-cloudevents-f01.md | 100 - ...ase-messaging-inbox-jdbc-postgresql-f02.md | 94 - .../case-messaging-nats-experimental-f01.md | 97 - .../case-messaging-nats-experimental-f04.md | 80 - ...se-messaging-outbox-jdbc-postgresql-f02.md | 93 - ...se-messaging-outbox-jdbc-postgresql-f03.md | 91 - .../case-messaging-reliability-api-f01.md | 88 - .../case-messaging-spring-boot-starter-f05.md | 83 - .../case/case-messaging-testkit-f07.md | 68 - .../case/case-messaging-transport-spi-f01.md | 79 - .../reference-messaging-cloudevents-f03.md | 50 - ...-messaging-kafka-share-experimental-f05.md | 48 - ...reference-messaging-reliability-api-f04.md | 54 - ...reference-messaging-reliability-api-f07.md | 54 - .../reference-messaging-runtime-core-f06.md | 52 - .../reference-messaging-schema-avro-f04.md | 50 - ...reference-messaging-schema-protobuf-f01.md | 48 - ...reference-messaging-schema-protobuf-f02.md | 48 - .../reference-messaging-security-f05.md | 52 - ...14-f002-webproblemsanitizer-alreadysafe.md | 136 - ...a14-f005-publicpaths-restrictedpathrule.md | 191 - .../case/case-a14-f010-no-store.md | 199 - .../case/case-a14-f011-maxarrayelements.md | 206 - .../case/case-a14-f014-advanced.md | 212 - ...-f015-virtualthreadprofile-propertyname.md | 181 - ...4-f017-springmvcrouteinventorycollector.md | 163 - ...se-a14-f018-webplatformstartupvalidator.md | 144 - .../case/case-analysis-finding-a14-f003.md | 164 - .../case/case-analysis-finding-a14-f004.md | 130 - .../case/case-analysis-finding-a14-f006.md | 137 - .../case/case-analysis-finding-a14-f007.md | 92 - .../case/case-analysis-finding-a14-f008.md | 109 - .../case/case-analysis-finding-a14-f009.md | 112 - .../case/case-analysis-finding-a14-f016.md | 108 - .../case/case-analysis-finding-a14-f019.md | 129 - .../case/case-a17-f002-stomp.md | 145 - .../case/case-analysis-finding-a17-f003.md | 86 - .../case/case-a-catalog-nine-entries-short.md | 119 - ...a-certifying-lane-that-compared-nothing.md | 2 +- .../case/case-a05-f012-identity.md | 152 - ...cationpolicy-specification-unrestricted.md | 156 - ...se-a05-f014-collection-fetch-pagination.md | 199 - .../case-a05-f033-jpa-flyway-migration.md | 170 - ...es-that-assert-what-their-bodies-do-not.md | 93 - ...se-three-versions-declared-one-executed.md | 86 - ...es-name-a-build-gate-that-no-build-runs.md | 93 - .../tech-log-studio/ap3-bff-session-flow.svg | 84 + .../tech-log-studio/ap3-csrf-boundary.svg | 104 + .../ap4-edge-forward-auth-flow.svg | 83 + .../case/case-ap3-bff-session-csrf.md | 5 + .../concept/concept-cookie-auth-csrf.md | 6 + .../concept-forward-auth-and-auth-request.md | 6 + .../tech-log-studio/tech-log-tree.json | 93 +- scripts/build-tech-log-tree.py | 7 +- scripts/tests/test_tech_log_tree.py | 60 +- scripts/verify-project-layout.py | 26 + scripts/verify-tech-log-tree.py | 19 + 851 files changed, 5498 insertions(+), 90638 deletions(-) delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-inbound-graphql-c04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-inbound-graphql-c05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-inbound-web-c05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-inbound-web-c14.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-inbound-web-c18.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-outbound-cache-redis-c04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-outbound-cache-redis-c05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-outbound-cache-redis-c12.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-outbound-fileserver-c06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-outbound-httpclient-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-outbound-httpclient-c05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-outbound-httpclient-c08.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-outbound-objectstorage-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-application-core-c08.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-grpc-advanced-bootstrap-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-grpc-advanced-streaming-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-messaging-admin-api-c04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-messaging-policy-c05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-messaging-schema-avro-c07.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-messaging-spring-cloud-stream-bridge-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-shared-contract-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/case/case-a06-f018-changestreams-false.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/case/case-autoconfiguration-in-name-only.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/case/case-conditionalonbean-evaluated-at-parse-time.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/case/case-narrowing-the-scan-orphaned-eight-components.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/concept/concept-when-conditions-are-evaluated.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/reference/reference-count-the-frameworks-own-autoconfigurations.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/bounding-by-type/case/case-a-report-that-cannot-carry-a-datasource.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/bounding-by-type/case/case-a05-f004-retrydecision-reason.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/bounding-by-type/case/case-cursor-verification-order.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/bounding-by-type/reference/reference-reject-rather-than-sanitize.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/caching-and-redis/case/case-a10-f001-readme.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/caching-and-redis/case/case-a10-f004-pub-sub.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/caching-and-redis/case/case-a10-f005-hyperloglog-merge.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/caching-and-redis/case/case-a10-f006-requireidentifier.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/caching-and-redis/case/case-analysis-finding-a10-f003.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/caching-and-redis/case/case-analysis-finding-a10-f007.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/caching-and-redis/case/case-analysis-finding-a10-f008.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-adapter-inbound-web-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-adapter-inbound-web-c17.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-adapter-outbound-objectstorage-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-adapter-outbound-objectstorage-c08.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-adapter-outbound-persistence-jpa-c24.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-app-bootstrap-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-app-bootstrap-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-app-bootstrap-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-grpc-advanced-diagnostics-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-messaging-kafka-share-experimental-c07.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-messaging-testkit-c05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/capability-declaration-vs-proof/case/case-a-delayed-delivery-flag-without-the-topology-that-delivers-it.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/capability-declaration-vs-proof/case/case-a-transaction-capability-true-and-its-validator-never-run.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/capability-declaration-vs-proof/case/case-a05-f006-stable.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/capability-declaration-vs-proof/case/case-a05-f022-stable.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/capability-declaration-vs-proof/case/case-the-support-matrix-says-nothing-is-deployed-and-eighteen-are.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/capability-declaration-vs-proof/case/case-the-transport-and-the-validator-answer-differently.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/capability-declaration-vs-proof/concept/concept-three-sources-of-a-capability-answer.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/capability-declaration-vs-proof/reference/reference-a-capability-constant-must-derive-from-the-profile.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/capability-declaration-vs-proof/reference/reference-the-weight-of-a-flag-is-set-by-the-code-that-reads-it.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-inbound-graphql-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-inbound-graphql-c06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-inbound-graphql-c09.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-inbound-web-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-inbound-web-c12.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-inbound-web-c13.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-inbound-websocket-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-outbound-cache-redis-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-outbound-cache-redis-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-outbound-fileserver-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-outbound-notification-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-outbound-notification-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-outbound-persistence-jpa-c56.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-outbound-persistence-mongo-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-outbound-persistence-mongo-c08.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-outbound-support-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-grpc-advanced-bootstrap-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-grpc-observability-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-messaging-admin-runtime-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-messaging-kafka-share-experimental-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-messaging-kafka-share-experimental-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-messaging-nats-experimental-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-messaging-runtime-core-c07.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-messaging-security-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-messaging-spring-boot-starter-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-messaging-spring-cloud-stream-bridge-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/composition-root-and-bootstrap/case/case-analysis-finding-a18-f001.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/composition-root-and-bootstrap/case/case-analysis-finding-a18-f002.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-grpc-advanced-diagnostics-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-grpc-advanced-resilience-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-grpc-advanced-streaming-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-grpc-advanced-streaming-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-grpc-core-api-f05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-grpc-discovery-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-grpc-operation-ledger-jpa-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-messaging-core-api-f05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-messaging-observability-f07.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-messaging-reliability-api-f05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-messaging-reliability-api-f08.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/question/openquestion-messaging-claim-check-f05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/reference/reference-messaging-inbox-jdbc-postgresql-f05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/reference/reference-messaging-inbox-jdbc-postgresql-f06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/cross-leaf-integration-facts/case/case-claim-check-has-no-wiring-line-in-the-starter.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/cross-leaf-integration-facts/case/case-messaging-migration-stream-has-no-applier-and-collides-on-adoption.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/cross-leaf-integration-facts/case/case-the-admin-switch-turns-on-the-guard-and-not-the-service.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/cross-leaf-integration-facts/case/case-the-public-surface-contract-test-lives-outside-the-family.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/cross-leaf-integration-facts/case/case-twenty-five-main-files-and-one-test-file.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-a05-f019-ssot.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-a06-f016-flamingock.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-grpc-advanced-bootstrap-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-grpc-advanced-edition-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-grpc-advanced-resilience-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-grpc-client-f04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-grpc-discovery-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-grpc-observability-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-grpc-policy-f06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-grpc-proto-contract-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-grpc-server-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-messaging-kafka-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-messaging-observability-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-messaging-outbox-jdbc-postgresql-f06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-messaging-outbox-jdbc-postgresql-f07.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-messaging-schema-avro-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-messaging-spring-boot-starter-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-messaging-spring-boot-starter-f04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-messaging-testkit-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-messaging-testkit-f08.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/question/openquestion-messaging-inbox-jdbc-postgresql-f04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/reference/reference-messaging-cloudevents-f05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/reference/reference-messaging-observability-f06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/reference/reference-messaging-policy-f06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/reference/reference-messaging-policy-f07.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/reference/reference-messaging-reliability-api-f06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/reference/reference-messaging-schema-api-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/reference/reference-messaging-schema-api-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/reference/reference-messaging-spring-cloud-stream-bridge-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/declared-contract-without-enforcement/case/case-an-order-contract-with-no-implementation.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-inbound-graphql-c08.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-cache-redis-c06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-fileserver-c05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-fileserver-c07.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-messaging-c04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-notification-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-persistence-jpa-c04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-persistence-jpa-c31.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-persistence-jpa-c32.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-persistence-jpa-c33.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-persistence-mongo-c07.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-application-core-c06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-application-core-c07.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-application-core-c11.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-domain-core-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-grpc-admin-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-grpc-admin-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-grpc-advanced-resilience-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-admin-api-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-admin-runtime-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-admin-runtime-c04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-cloudevents-c04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-core-api-c04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-core-api-c06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-kafka-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-kafka-share-experimental-c04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-kafka-share-experimental-c05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-nats-experimental-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-observability-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-observability-c04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-observability-c05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-outbox-jdbc-postgresql-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-policy-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-policy-c04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-policy-c07.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-policy-c08.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-rabbit-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-runtime-core-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-runtime-core-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-runtime-core-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-runtime-core-c05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-schema-json-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-schema-json-c05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-schema-protobuf-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-security-c05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-spring-boot-starter-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-spring-boot-starter-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-spring-cloud-stream-bridge-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-spring-cloud-stream-bridge-c04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-spring-cloud-stream-bridge-c05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-testkit-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-testkit-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-transport-spi-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-shared-contract-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/drain-and-shutdown-ordering/case/case-reject-new-admission-that-rejects-nothing.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/drain-and-shutdown-ordering/case/case-the-last-step-of-secret-erasure-is-not-wired.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/drain-and-shutdown-ordering/concept/concept-the-eight-phase-shutdown-contract.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/drain-and-shutdown-ordering/reference/reference-a-declaration-order-test-is-a-gate-only-if-something-reads-that-order.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/drift-direction/reference/reference-numbers-in-docs-should-be-derived.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/duplicate-mechanisms/case/case-the-same-repository-bound-a-decision-once-and-not-the-other-time.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/duplicate-mechanisms/decision/decision-one-audit-mechanism-per-entity.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/duplicate-mechanisms/reference/reference-two-vocabularies-for-one-concept.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/failure-category-across-adapters/case/case-a-closing-transport-reported-as-a-permanent-business-failure.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/failure-category-across-adapters/case/case-an-authorization-denial-recorded-as-a-configuration-error.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/failure-category-across-adapters/reference/reference-a-category-is-a-contract-between-retry-dlq-and-dashboard.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/failure-category-across-adapters/reference/reference-this-generations-circumstance-is-not-a-permanent-failure.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/file-transfer-and-storage/case/case-a08-f005-transferbufferpool-maxborrowedbytes.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/file-transfer-and-storage/case/case-analysis-finding-a08-f002.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/file-transfer-and-storage/case/case-analysis-finding-a08-f003.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/file-transfer-and-storage/case/case-analysis-finding-a08-f004.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/file-transfer-and-storage/case/case-analysis-finding-a09-f001.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/file-transfer-and-storage/case/case-analysis-finding-a09-f004.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/file-transfer-and-storage/case/case-analysis-finding-a09-f006.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/fileserver-state-and-fencing/case/case-a-cleanup-claim-without-fencing.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/fileserver-state-and-fencing/case/case-a-read-then-delete-race-on-the-upload-lease.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/fileserver-state-and-fencing/decision/decision-no-physical-paths-in-metadata.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/fileserver-state-and-fencing/reference/reference-a-publicly-readable-state-must-be-complete-by-constraint.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/fileserver-state-and-fencing/reference/reference-claim-with-a-conditional-update-not-a-read.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/fileserver-state-and-fencing/reference/reference-prefix-matching-fits-signatures-not-sniffing.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/graphql-surface/case/case-a16-f002-oneof.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/graphql-surface/case/case-a16-f004-graphqloperationnamepolicy.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/graphql-surface/case/case-analysis-finding-a16-f001.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/graphql-surface/case/case-analysis-finding-a16-f003.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/graphql-surface/case/case-analysis-finding-a16-f005.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/graphql-surface/case/case-analysis-finding-a16-f006.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/graphql-surface/case/case-analysis-finding-a16-f008.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/graphql-surface/case/case-analysis-finding-a16-f011.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/graphql-surface/case/case-analysis-finding-a16-f012.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-a20-f003-claude.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-a20-f006-grpcadmissioncontroller-tryadmit.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-a20-f007-grpcstreamadmission.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-a20-f008-grpcserializedstreamwriter-drop-oldest.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-a20-f010-grpcoutcomereplay.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-a20-f011-grpccompletionreconciler-arraylist.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-analysis-finding-a15-f001.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-analysis-finding-a15-f002.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-analysis-finding-a20-f004.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-analysis-finding-a20-f005.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/http-client-and-resilience/case/case-a11-f001-close.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/http-client-and-resilience/case/case-a11-f002-pool-route-exceeds-total.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/http-client-and-resilience/case/case-a11-f004-number.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/http-client-and-resilience/case/case-a11-f006-boundeddatabufferflux.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/http-client-and-resilience/case/case-a11-f007-dns.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/http-client-and-resilience/case/case-analysis-finding-a11-f003.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/http-failure-classification/case/case-a-circuit-breaker-permit-that-leaks-on-local-rejection.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/http-failure-classification/reference/reference-a-classifier-sees-only-what-the-engine-kept.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/http-failure-classification/reference/reference-retryability-needs-both-idempotency-and-category.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/http-failure-classification/reference/reference-verify-runtime-shape-at-runtime.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/http-failure-classification/reference/reference-walk-the-cause-chain-most-specific-wins.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/identity-and-identifier/case/case-a07-f001-uuidcodec.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/identity-and-identifier/case/case-a07-f002-normalize.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/identity-and-identifier/case/case-a07-f004-claude.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/identity-and-identifier/case/case-a07-f006-claude.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/identity-and-identifier/case/case-analysis-finding-a07-f005.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/identity-and-value-contracts/case/case-a01-f002-idfactory-newid.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/identity-and-value-contracts/concept/concept-adapter-inbound-web-c19.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/identity-and-value-contracts/concept/concept-adapter-outbound-cache-redis-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/identity-and-value-contracts/concept/concept-adapter-outbound-notification-c05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/identity-and-value-contracts/concept/concept-adapter-outbound-notification-c06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/identity-and-value-contracts/concept/concept-adapter-outbound-objectstorage-c09.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/identity-and-value-contracts/concept/concept-app-bootstrap-c04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/identity-and-value-contracts/concept/concept-grpc-core-api-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/identity-and-value-contracts/concept/concept-messaging-testkit-c04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a12-f001-check-verifyjsonschemaruntimegraph.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a12-f002-jackson-databind.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a19-f003-messaging-reliability-api.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a19-f006-messaging-cloudevents.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a19-f008-acl.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a19-f014-kafka-msg.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a19-f015-compatibilitymatrix-extension.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a19-f020-messaging-admin-api.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a19-f022-messagingpublicsurfacecontracttest.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-analysis-finding-a19-f001.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-analysis-finding-a19-f002.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-analysis-finding-a19-f004.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-analysis-finding-a19-f005.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-analysis-finding-a19-f009.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-analysis-finding-a19-f013.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-analysis-finding-a19-f018.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-analysis-finding-a19-f019.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a03-f001.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a04-f002.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a04-f004.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a04-f005.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f001.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f010.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f023.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f024.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f028.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f030.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f031.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f032.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f034.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f002.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f004.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f005.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f007.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f008.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f009.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f010.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f014.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f015.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f019.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f021.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f022.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f023.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f024.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f025.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f026.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f027.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f028.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-search-path-survived-the-return-to-the-pool.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-tenant-pools-summed-past-the-server-ceiling.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/concept/concept-four-multitenancy-strategies.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-a02-f001-lro.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-a02-f002-domaincontextkey.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-a05-f003-capabilitysupport-constraints.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-analysis-finding-a02-f003.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-analysis-finding-a02-f004.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-analysis-finding-a02-f005.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-analysis-finding-a03-f002.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-analysis-finding-a03-f004.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-analysis-finding-a04-f006.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-performance-and-capacity-unmeasured.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/reference/reference-analysis-finding-a03-f003.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/reference/reference-hibernate-filter-is-not-a-security-boundary.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/reference/reference-isolation-settings-must-be-transaction-local.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/reference/reference-tenant-column-belongs-in-every-unique-constraint.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/non-atomic-check-then-act/case/case-a-rotation-that-overwrites-and-a-generation-that-cannot-be-reclaimed.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/non-atomic-check-then-act/case/case-a05-f026-enqueue.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/non-atomic-check-then-act/case/case-an-admission-boundary-that-leaks-and-a-counter-that-cannot-return.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/non-atomic-check-then-act/case/case-draining-began-and-a-service-came-back-serving.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/non-atomic-check-then-act/concept/concept-check-then-act-on-atomic-types.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/non-atomic-check-then-act/reference/reference-beginning-a-transition-and-the-set-it-covers-are-one-operation.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/notification-and-delivery/case/case-a13-f002-authentication-failed-resumehealthy.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/notification-and-delivery/case/case-a13-f003-accesscontext.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/notification-and-delivery/case/case-a13-f004-thymeleaf.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/notification-and-delivery/case/case-a13-f005-retry-after-illegalargumentexception.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/notification-and-delivery/case/case-a13-f008-host.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/notification-and-delivery/case/case-a13-f009-sigv4-string.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/notification-and-delivery/case/case-analysis-finding-a13-f007.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/notification-and-delivery/case/case-analysis-finding-a13-f010.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/notification-and-delivery/case/case-analysis-finding-a13-f011.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/objectstorage-staged-lifecycle/case/case-endpoint-check-only-on-upload.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/objectstorage-staged-lifecycle/case/case-the-last-part-cannot-get-a-grant.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/objectstorage-staged-lifecycle/decision/decision-legacy-adoption-requires-two-approvers.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/objectstorage-staged-lifecycle/reference/reference-a-binary-approval-codec-must-round-trip.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/objectstorage-staged-lifecycle/reference/reference-the-powerful-half-must-not-be-one-setting-away.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/observability-models/concept/concept-adapter-inbound-web-c15.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/observability-models/concept/concept-adapter-outbound-persistence-jpa-c44.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/observability-models/concept/concept-adapter-outbound-support-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/observability-models/concept/concept-adapter-outbound-support-c06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/observability-models/concept/concept-grpc-observability-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/observability-models/concept/concept-messaging-admin-runtime-c05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/observability-models/concept/concept-messaging-observability-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/operator-approval-and-destructive-operations/case/case-a-resumed-redrive-skips-what-it-could-not-move.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/operator-approval-and-destructive-operations/case/case-blocking-means-startup-fails-and-nothing-runs-it.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/operator-approval-and-destructive-operations/case/case-the-forgeable-approval-survived-on-the-irreversible-half.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/operator-approval-and-destructive-operations/concept/concept-approval-verification-execution.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/operator-approval-and-destructive-operations/reference/reference-resume-by-identity-not-by-index.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/operator-approval-and-destructive-operations/reference/reference-signing-and-verifying-do-not-share-an-object.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/owner-safe-state-machines/case/case-a-digest-that-covered-who-but-not-what.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/owner-safe-state-machines/case/case-expired-claim-versus-expired-execution.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/owner-safe-state-machines/decision/decision-state-machines-carry-no-stereotype.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/owner-safe-state-machines/reference/reference-digest-must-be-length-framed-and-versioned.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/owner-safe-state-machines/reference/reference-expired-claim-and-expired-execution-differ.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/query-and-pagination-models/concept/concept-adapter-inbound-graphql-c10.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/query-and-pagination-models/concept/concept-adapter-inbound-web-c06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/query-and-pagination-models/concept/concept-adapter-outbound-persistence-jpa-c27.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/query-and-pagination-models/concept/concept-adapter-outbound-persistence-jpa-c28.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/query-and-pagination-models/concept/concept-adapter-outbound-persistence-jpa-c29.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/query-and-pagination-models/concept/concept-application-core-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/redis-command-admission/decision/decision-unclassified-commands-are-refused.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/redis-command-admission/reference/reference-a-single-admission-point-must-count-its-bypasses.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/redis-command-admission/reference/reference-server-metadata-defines-the-command.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-adapter-inbound-grpc-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-adapter-outbound-cache-redis-c09.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-domain-core-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-messaging-cloudevents-c08.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-messaging-inbox-jdbc-postgresql-c07.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-messaging-pulsar-experimental-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-messaging-schema-api-c07.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-messaging-testkit-c06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-messaging-transport-spi-c07.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-shared-contract-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/retention-and-unbounded-growth/case/case-a-replay-store-with-no-eviction-path.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/retention-and-unbounded-growth/case/case-the-cleanup-that-causes-the-outage-it-prevents.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/retention-and-unbounded-growth/concept/concept-bounded-and-unbounded-side-by-side.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/retention-and-unbounded-growth/reference/reference-a-fake-that-cannot-show-the-property-is-not-a-witness.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/retention-and-unbounded-growth/reference/reference-a-port-that-offers-both-forms-has-chosen-the-unsafe-one.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/retention-and-unbounded-growth/reference/reference-one-formula-and-one-enforcement-point-per-safety-rule.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/case/case-a05-f005-jparetrypolicy.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/case/case-grpc-core-api-f04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/case/case-grpc-observability-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/case/case-grpc-spring-boot-starter-f04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/case/case-messaging-claim-check-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/case/case-messaging-core-api-f04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/case/case-messaging-outbox-jdbc-postgresql-f05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/case/case-messaging-outbox-jdbc-postgresql-f08.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/case/case-messaging-spring-boot-starter-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/reference/reference-messaging-kafka-share-experimental-f04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/reference/reference-messaging-policy-f05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/reference/reference-messaging-runtime-core-f05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-a05-f007-transactionprofileregistry.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-a05-f018-sql.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-a06-f003-change-streams-true.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-grpc-advanced-compat-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-grpc-core-api-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-grpc-observability-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-grpc-policy-f07.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-grpc-server-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-grpc-spring-boot-starter-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-grpc-spring-boot-starter-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-grpc-testkit-f05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-admin-api-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-admin-api-f04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-admin-api-f06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-admin-runtime-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-admin-runtime-f04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-admin-runtime-f09.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-claim-check-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-cloudevents-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-inbox-jdbc-postgresql-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-observability-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-outbox-jdbc-postgresql-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-outbox-jdbc-postgresql-f04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-rabbit-f04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-reliability-api-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-runtime-core-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-runtime-core-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-schema-api-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-security-f04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-spring-boot-starter-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-testkit-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-testkit-f04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-testkit-f05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-testkit-f06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-startup-validator-is-the-only-reader-of-four-keys.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-validator-declared-and-never-injected.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/question/openquestion-messaging-core-api-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/question/openquestion-messaging-core-api-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/question/openquestion-messaging-kafka-share-experimental-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/question/openquestion-messaging-policy-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/question/openquestion-messaging-reliability-api-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/question/openquestion-messaging-schema-json-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/reference/reference-a-validator-is-enforced-by-injection.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/reference/reference-messaging-claim-check-f04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/reference/reference-messaging-kafka-share-experimental-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/reference/reference-messaging-runtime-core-f07.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/reference/reference-messaging-security-f07.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/reference/reference-messaging-spring-cloud-stream-bridge-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/reference/reference-messaging-spring-cloud-stream-bridge-f05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-grpc-advanced-bootstrap-f05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-grpc-advanced-edition-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-grpc-codegen-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-grpc-core-api-f06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-grpc-policy-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-grpc-proto-contract-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-grpc-proto-contract-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-grpc-proto-contract-f04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-messaging-observability-f04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-messaging-runtime-core-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-messaging-schema-avro-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-messaging-schema-json-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-messaging-schema-protobuf-f04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/question/openquestion-messaging-cloudevents-f04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/question/openquestion-messaging-schema-protobuf-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/reference/reference-messaging-schema-avro-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/reference/reference-messaging-schema-avro-f05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-inbound-graphql-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-inbound-graphql-c07.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-inbound-graphql-c11.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-inbound-web-c08.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-inbound-web-c10.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-inbound-web-c11.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-inbound-websocket-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-inbound-websocket-c04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-cache-redis-c07.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-cache-redis-c11.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-fileserver-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-httpclient-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-httpclient-c06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-messaging-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-messaging-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-objectstorage-c04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-objectstorage-c06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-persistence-jpa-c05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-persistence-jpa-c25.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-persistence-jpa-c45.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-persistence-jpa-c49.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-persistence-mongo-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-persistence-mongo-c05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-persistence-mongo-c09.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-application-core-c10.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-grpc-policy-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-admin-api-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-cloudevents-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-cloudevents-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-cloudevents-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-cloudevents-c05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-cloudevents-c06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-core-api-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-core-api-c05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-api-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-api-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-api-c04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-avro-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-avro-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-avro-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-avro-c04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-avro-c05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-json-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-json-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-json-c04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-protobuf-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-protobuf-c04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-protobuf-c05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-protobuf-c07.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-testkit-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-shared-contract-c04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-ownership-and-capability-streams/case/case-a-customizer-that-discarded-the-bound-property.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-ownership-and-capability-streams/case/case-registry-column-too-short-for-its-own-path.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-ownership-and-capability-streams/decision/decision-repair-is-not-a-mode.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-ownership-and-capability-streams/reference/reference-an-applied-checksum-is-a-promise.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/schema-ownership-and-capability-streams/reference/reference-each-stream-owns-its-history-table.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/case/case-a06-f020-tls-stable.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-inbound-graphql-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-inbound-web-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-inbound-web-c04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-inbound-web-c07.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-outbound-httpclient-c07.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-outbound-httpclient-c09.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-outbound-notification-c04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-outbound-objectstorage-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-outbound-persistence-jpa-c53.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-outbound-support-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-outbound-support-c05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-app-bootstrap-c05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-application-core-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-grpc-advanced-bootstrap-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-grpc-advanced-diagnostics-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-grpc-observability-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-grpc-spring-boot-starter-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-messaging-admin-api-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-messaging-admin-api-c05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-messaging-rabbit-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-a06-f011-mongoregexpolicy-forbidden.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-grpc-admin-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-grpc-advanced-diagnostics-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-grpc-advanced-diagnostics-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-grpc-advanced-edition-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-grpc-core-api-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-grpc-policy-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-grpc-policy-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-grpc-server-f04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-grpc-testkit-f06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-ipv4-only-mask-passes-every-other-form.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-messaging-admin-api-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-messaging-admin-api-f05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-messaging-admin-api-f07.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-messaging-admin-runtime-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-messaging-admin-runtime-f05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-messaging-admin-runtime-f08.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-messaging-observability-f05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-messaging-security-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-messaging-security-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/question/openquestion-messaging-security-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/reference/reference-messaging-security-f06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/self-disclosure-grading/case/case-a-soak-threshold-written-for-a-grade-that-does-not-exist.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/case/case-a05-f020-inspect-claim.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/case/case-a05-f021-complete-replayttl.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/case/case-a05-f025-filequotaservice-commit.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/case/case-a05-f027-maximum-attempts.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-inbound-graphql-c12.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-inbound-graphql-c13.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-inbound-web-c09.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-inbound-web-c16.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-inbound-websocket-c05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-cache-redis-c08.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-cache-redis-c10.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-fileserver-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-fileserver-c04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-fileserver-c08.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-httpclient-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-httpclient-c04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-messaging-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-objectstorage-c05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-objectstorage-c07.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c07.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c08.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c09.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c12.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c13.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c17.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c19.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c30.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c34.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c35.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c36.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c38.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c39.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c40.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c41.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c43.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c46.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c47.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c48.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c50.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c52.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c54.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c55.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c57.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c58.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-mongo-c04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-mongo-c06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-support-c04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-application-core-c04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-application-core-c05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-application-core-c09.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-domain-core-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-grpc-advanced-resilience-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-grpc-codegen-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-grpc-discovery-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-grpc-operation-ledger-jpa-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-grpc-operation-ledger-jpa-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-grpc-operation-ledger-jpa-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-grpc-spring-boot-starter-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-grpc-testkit-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-admin-api-c06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-admin-api-c07.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-admin-runtime-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-admin-runtime-c06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-admin-runtime-c07.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-claim-check-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-claim-check-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-claim-check-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-claim-check-c04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-claim-check-c05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-claim-check-c06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-claim-check-c07.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-cloudevents-c07.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-core-api-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-core-api-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-core-api-c07.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-core-api-c08.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-inbox-jdbc-postgresql-c04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-inbox-jdbc-postgresql-c05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-inbox-jdbc-postgresql-c06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-kafka-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-kafka-share-experimental-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-kafka-share-experimental-c06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-observability-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-observability-c06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-observability-c07.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-outbox-jdbc-postgresql-c05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-policy-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-policy-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-policy-c06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-policy-c09.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-reliability-api-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-reliability-api-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-reliability-api-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-reliability-api-c04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-reliability-api-c05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-reliability-api-c06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-reliability-api-c07.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-runtime-core-c04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-runtime-core-c06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-schema-api-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-schema-api-c05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-schema-api-c06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-schema-avro-c06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-schema-json-c06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-schema-json-c07.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-schema-protobuf-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-schema-protobuf-c06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-security-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-security-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-security-c04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-security-c06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-security-c07.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-spring-cloud-stream-bridge-c06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-testkit-c07.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-testkit-c08.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-transport-spi-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-transport-spi-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-transport-spi-c04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-transport-spi-c05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-transport-spi-c06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-a05-f029-for-update-skip-locked.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-a06-f013-recordapplied.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-assigned-id-turns-claim-into-upsert.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-complete-drain-rolls-back-a-rotation.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-admin-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-advanced-bootstrap-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-advanced-resilience-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-advanced-streaming-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-client-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-client-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-client-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-codegen-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-discovery-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-operation-ledger-jpa-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-policy-f04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-policy-f05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-testkit-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-messaging-admin-runtime-f06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-messaging-admin-runtime-f07.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-messaging-rabbit-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/reference/reference-messaging-claim-check-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/reference/reference-messaging-security-f08.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/reference/reference-messaging-spring-cloud-stream-bridge-f04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/reference/reference-messaging-spring-cloud-stream-bridge-f06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-inbound-websocket-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-cache-redis-c13.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c10.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c11.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c14.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c15.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c16.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c18.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c20.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c21.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c22.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c23.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c26.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c37.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c42.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c51.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-mongo-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-application-core-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-grpc-server-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-messaging-inbox-jdbc-postgresql-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-messaging-inbox-jdbc-postgresql-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-messaging-inbox-jdbc-postgresql-c03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-messaging-outbox-jdbc-postgresql-c01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-messaging-outbox-jdbc-postgresql-c02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-messaging-outbox-jdbc-postgresql-c04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-messaging-outbox-jdbc-postgresql-c06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transaction-deadline-and-pool/case/case-a-validator-checking-the-wrong-datasource.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transaction-deadline-and-pool/reference/reference-deadline-narrows-in-three-stages.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transaction-deadline-and-pool/reference/reference-session-scoped-settings-outlive-the-transaction.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transaction-deadline-and-pool/reference/reference-write-transactions-need-a-finite-timeout.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-capability-constant-outlives-its-condition.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-grpc-spring-boot-starter-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-admin-runtime-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-admin-runtime-f10.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-core-api-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-inbox-jdbc-postgresql-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-kafka-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-kafka-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-kafka-f04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-kafka-f05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-kafka-share-experimental-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-nats-experimental-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-nats-experimental-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-policy-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-policy-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-policy-f04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-pulsar-experimental-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-rabbit-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-rabbit-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-rabbit-f05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-testkit-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/question/openquestion-messaging-observability-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/question/openquestion-messaging-spring-cloud-stream-bridge-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/reference/reference-messaging-kafka-share-experimental-f06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/reference/reference-messaging-runtime-core-f04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/reference/reference-messaging-schema-json-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/reference/reference-messaging-transport-spi-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/reference/reference-messaging-transport-spi-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-admin-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-advanced-bootstrap-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-advanced-bootstrap-f04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-advanced-compat-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-advanced-compat-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-codegen-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-codegen-f04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-core-api-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-server-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-testkit-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-testkit-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-testkit-f04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-admin-api-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-cloudevents-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-inbox-jdbc-postgresql-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-nats-experimental-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-nats-experimental-f04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-outbox-jdbc-postgresql-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-outbox-jdbc-postgresql-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-reliability-api-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-spring-boot-starter-f05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-testkit-f07.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-transport-spi-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/reference/reference-messaging-cloudevents-f03.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/reference/reference-messaging-kafka-share-experimental-f05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/reference/reference-messaging-reliability-api-f04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/reference/reference-messaging-reliability-api-f07.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/reference/reference-messaging-runtime-core-f06.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/reference/reference-messaging-schema-avro-f04.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/reference/reference-messaging-schema-protobuf-f01.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/reference/reference-messaging-schema-protobuf-f02.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/reference/reference-messaging-security-f05.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-a14-f002-webproblemsanitizer-alreadysafe.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-a14-f005-publicpaths-restrictedpathrule.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-a14-f010-no-store.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-a14-f011-maxarrayelements.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-a14-f014-advanced.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-a14-f015-virtualthreadprofile-propertyname.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-a14-f017-springmvcrouteinventorycollector.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-a14-f018-webplatformstartupvalidator.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-analysis-finding-a14-f003.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-analysis-finding-a14-f004.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-analysis-finding-a14-f006.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-analysis-finding-a14-f007.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-analysis-finding-a14-f008.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-analysis-finding-a14-f009.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-analysis-finding-a14-f016.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-analysis-finding-a14-f019.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/websocket-and-realtime/case/case-a17-f002-stomp.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/websocket-and-realtime/case/case-analysis-finding-a17-f003.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-a-catalog-nine-entries-short.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-a05-f012-identity.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-a05-f013-specificationpolicy-specification-unrestricted.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-a05-f014-collection-fetch-pagination.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-a05-f033-jpa-flyway-migration.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-test-names-that-assert-what-their-bodies-do-not.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-three-versions-declared-one-executed.md delete mode 100644 docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-two-files-name-a-build-gate-that-no-build-runs.md create mode 100644 docs/keycloak/final/assets/tech-log-studio/ap3-bff-session-flow.svg create mode 100644 docs/keycloak/final/assets/tech-log-studio/ap3-csrf-boundary.svg create mode 100644 docs/keycloak/final/assets/tech-log-studio/ap4-edge-forward-auth-flow.svg diff --git a/.agents/skills/writing-tech-log-records/SKILL.md b/.agents/skills/writing-tech-log-records/SKILL.md index 7d24435..b216471 100644 --- a/.agents/skills/writing-tech-log-records/SKILL.md +++ b/.agents/skills/writing-tech-log-records/SKILL.md @@ -35,7 +35,8 @@ Concept 에 담고 `관계`로 가리킨다. `references/record-kinds.md` **`tech-log-tree.json` 에 노드가 없는 글은 쓰지 않는다** — 트리에 먼저 올리고, 그 노드의 후보가 `PROMOTE` 이면서 `dispositionReview: CONFIRMED` 인지 확인한 뒤에 쓴다. `PENDING` 은 사람이 다시 읽지 않았다는 뜻이라, 그 위에 쓴 글은 과분류를 그대로 물려받는다. 계약 밖에서 쓴 - 기록은 색인에 `unlisted` 로 남는다. + 기록은 색인에 `unlisted` 로 남는다. 그 노드의 `ssot-assets`·`ssot-evidence` 도 함께 본다 — + SSOT 가 이미 그린 그림과 이미 돌린 측정 가운데 이 글감에 배정된 것이 거기 적혀 있다. 1. **종류 선택** — 위 표. 애매하면 "재현했나"를 묻는다. 2. **칸 채우기** — 칸과 게시 조건은 `references/record-kinds.md`. 3. **본문 작성**(Case·Concept) — **종류마다 무엇을 어떤 순서로 쓰는지는 @@ -45,9 +46,14 @@ Concept 에 담고 `관계`로 가리킨다. `references/record-kinds.md` 남겨 두고 나중에 걷어내는 순서가 아니다. **문체를 손보기 전에 문장을 고른다** — 설명이 끝난 뒤에 붙은 평가·예고·되풀이·독자 오해 가정을 먼저 뺀다(`ai-tells.md` 첫 절). 문체 규칙의 정본은 `ai-tells.md` 다. - 순서·경계·상태 전이처럼 문장만으로 따라가기 어려운 관계가 있으면 그때 - `technical-visualizer` 로 그림을 만든다. 손으로 SVG 를 그리지 않고, 모든 글에 그림을 만들지도 - 않는다. + 순서·경계·상태 전이처럼 문장만으로 따라가기 어려운 관계가 있으면 **`final/assets/` 에 그 + 그림이 이미 있는지부터 본다.** SSOT 를 만들 때 그려 둔 것이 있고 계약의 `ssot-assets` 가 이 + 글감에 배정해 두었으면 그것을 쓴다 — `final/assets/tech-log-studio/` 로 복사하고 기록의 + `assets` 가 그 사본을 가리킨다. **없을 때만** `technical-visualizer` 로 새로 만든다. 손으로 + SVG 를 그리지 않고, 모든 글에 그림을 만들지도 않는다. + 인용할 측정도 같다. `final/evidence/` 에 있는 원문을 가리키고 같은 것을 다시 돌리지 않는다. + **그림과 측정을 그 자리에서 만들어 내기 전에 SSOT 가 이미 가진 것을 먼저 찾는다** — keycloak + 에서는 그러지 않아 정본까지 갖춘 그림 13 장이 남고 이름이 다른 그림 5 장이 새로 만들어졌다. 4. **검사** — 셋 다 돌린다. 파서와 문장과 증빙은 각각 다른 것을 본다. - `scripts/check_body.mjs` — Case 본문이 Studio 파서를 통과하는지. 같은 파서를 그대로 부른다. - `rewriting-technical-prose-naturally/scripts/check_prose.mjs` — 문장 규범. **error 0 이 될 때까지 고친다.** @@ -116,6 +122,8 @@ SSOT 에 없는데 필요한 인용이라면 순서가 반대다 — `final/docu | 문서마다 같은 문형·같은 길이 | `references/ai-tells.md` | | 표를 `:::table`로 감쌈 | 그냥 파이프로 쓴다 | | `![](https://…외부)` | Asset으로 올려 `/api/v1/public/media/…` | +| SSOT 에 있는 그림을 두고 새로 그림 | `final/assets/` 를 먼저 본다. `ssot-assets` 가 배정한 것을 쓴다 | +| SSOT 에 있는 측정을 두고 다시 돌림 | `final/evidence/` 의 원문을 가리킨다 | | Decision에 근거 없음 | 관계 1개 이상 연결 | | 측정 안 한 검증일 | 비워 둔다 | | 설명 직후에 「~증거다」「~가 아니다」로 평가 | 지운다. 앞 문장이 사실을 말했으면 거기서 끝낸다 | diff --git a/.agents/skills/writing-tech-log-records/references/from-ssot-to-records.md b/.agents/skills/writing-tech-log-records/references/from-ssot-to-records.md index e91a59d..1c7bade 100644 --- a/.agents/skills/writing-tech-log-records/references/from-ssot-to-records.md +++ b/.agents/skills/writing-tech-log-records/references/from-ssot-to-records.md @@ -10,6 +10,8 @@ |---|---| | 코드·설정·실행 증거 | 사실의 근거 | | `final/document.md` | **글감 범위의 SSOT** — 후보를 발견하는 유일한 입력 | +| `final/assets/` · `final/.techviz/` | 이미 그린 그림과 그 정본. 새 후보를 내지 않고 글감에 배정된다 | +| `final/evidence/` | 이미 실행한 측정의 원문. 마찬가지로 배정된다 | | `analysis/**/*.md` | final이 이미 채택한 주장을 상세히 확인하는 보조 근거 | | `tech-log-tree.json` | 사람이 고른 글감. 분해 계약이자 색인이고 이 파일이 정본이다 | @@ -36,6 +38,37 @@ 범위 밖의 앵커는 후보가 아니라 근거다. 제1부에서 나온 글감의 `source`로 건다. +## 그림과 증거는 후보가 아니라 배정 대상이다 + +`final/assets/`의 그림과 `final/evidence/`의 측정은 글감을 새로 만들지 않는다. 이미 정해진 +글감에 붙는다. 그래서 처분을 매기는 자리가 아니라 **배정하는 자리**이고, 계약의 +`ssot-assets`·`ssot-evidence`가 그 자리다. + +글감을 다 고른 뒤 두 폴더를 한 번 훑는다. 물음은 하나다. + +> **이 그림이나 이 측정은 어느 글감의 것인가. 붙을 글감이 없으면 왜 없는가.** + +```json +"ssot-assets": ["ap3-bff-session-flow"], +"ssot-evidence": ["raw/explain/l3-cartesian-join-plan.txt"] +``` + +배정한 것은 기록의 `assets`·`evidence`가 실제로 가리켜야 한다. 배정해 놓고 쓰지 않으면 +`verify-tech-log-tree.py`가 error로 센다. + +**배정하지 않으면 글을 쓸 때 같은 그림을 새로 그린다.** keycloak이 그렇게 됐다. SSOT에 +`ap3-bff-session-flow`, `ap4-edge-forward-auth-flow`를 포함한 그림 13장이 `.techviz` 정본까지 +갖춘 채 있었는데, 기록 24편은 그중 한 장도 가리키지 않고 이름이 다른 그림 5장을 새로 만들어 +썼다. 새로 만든 5장에는 정본이 없어서 고칠 수도 없다. + +붙을 글감이 없는 그림도 있다. 패턴 넷을 나란히 놓고 비교하는 그림은 Reference에 붙어야 맞는데 +Reference에는 본문이 없다. 그런 그림은 그대로 두고, 왜 두는지 계약에 적는다 — 「Reference에만 +쓸 자리가 있어 본문 있는 종류에 담지 못한다」처럼. `verify-project-layout.py`가 「기록이 쓰지 +않는 SSOT 그림」으로 세므로, 센 숫자가 설명되지 않은 채 남지 않게 한다. + +증거도 같다. 재료로만 쓰고 인용하지 않기로 한 측정은 정상이다. 「기록이 인용하지 않는 raw 증거」가 +전부 설명되는지만 본다. + ## 왜 먼저 나누는가 긴 글을 앞에서부터 잘라 기록으로 만들면 절 하나가 기록 하나가 된다. 그러면 Case의 칸도 diff --git a/.agents/skills/writing-tech-log-records/references/review-checklist.md b/.agents/skills/writing-tech-log-records/references/review-checklist.md index 168de9c..4c6b039 100644 --- a/.agents/skills/writing-tech-log-records/references/review-checklist.md +++ b/.agents/skills/writing-tech-log-records/references/review-checklist.md @@ -127,4 +127,6 @@ - [ ] Decision 에 근거가 하나 이상 있고, 무엇을 보고 정했는지가 적혀 있는가 - [ ] 지어낸 경험·실패·동기·감정이 없는가 - [ ] 그림이 실제 asset 파일을 가리키고, 있어야 할 이유가 있는가 +- [ ] `final/assets/` 에 이미 있는 그림을 두고 같은 것을 새로 그리지 않았는가 +- [ ] 계약이 `ssot-assets`·`ssot-evidence` 로 배정한 것을 기록이 가리키는가 - [ ] 그림이 관측하지 않은 사건을 만들어 내지 않았는가 diff --git a/.agents/skills/writing-tech-log-records/references/tech-log-tree-contract.md b/.agents/skills/writing-tech-log-records/references/tech-log-tree-contract.md index ddf89c2..462c87f 100644 --- a/.agents/skills/writing-tech-log-records/references/tech-log-tree-contract.md +++ b/.agents/skills/writing-tech-log-records/references/tech-log-tree-contract.md @@ -83,7 +83,55 @@ whose candidate is `PROMOTE` and `CONFIRMED`. `readiness` · `source` · `code` · `evidence` · `classification` · `relations` and the rest of each kind's fields are written by a person. `build-tech-log-tree.py` never touches them. It refreshes only what it can read from the record files — `file`, `publication`, `status`, -`studioId`, `assets`, `evidenceFiles` — and lists records that have no node in `unlisted`. +`studioId`, `assets`, `assetFiles`, `evidenceFiles` — and lists records that have no node in +`unlisted`. + +### `ssot-assets` · `ssot-evidence` + +The SSOT is not only `final/document.md`. `final/assets/` holds diagrams that were already +drawn, each with its canonical `final/.techviz//`, and `final/evidence/` holds +measurements that were already run. Neither produces candidates — both are **assigned** to +candidates that already exist, and these two fields hold the assignment. + +```json +"ssot-assets": ["ap3-bff-session-flow"], +"ssot-evidence": ["raw/explain/l3-cartesian-join-plan.txt"] +``` + +`ssot-assets` names diagrams by file stem; the file must exist somewhere under +`final/assets/`. `ssot-evidence` takes paths relative to `final/evidence/`. Both are +written by a person and both are optional — a node that needs no picture and cites no +measurement leaves them out. + +What they are not optional about is follow-through. Once a node is assigned a diagram and +its record is written, the record's `assets` must point at that file and its `evidence` at +that path; `verify-tech-log-tree.py` reports the gap as an error. Assigning and then not +using is the failure these fields exist to catch — without them a writer draws the picture +again instead of finding the one that is already there. + +`verify-project-layout.py` counts the other direction: SSOT diagrams and raw evidence that +no record cites at all. Some of that count is correct — a four-pattern comparison diagram +belongs to a Reference, and Reference has no body to render it in. The count is meant to be +explained, not driven to zero. + +### `assetLedger` + +That explanation lives at the top level of the index, next to `candidateScope`. It names +what was assigned and, for everything left over, why it is left over. + +```json +"assetLedger": { + "assigned": ["ap3-bff-session-flow", "ap3-csrf-boundary"], + "unassigned": [ + {"asset": ["four-pattern-request-boundaries"], + "reason": "네 패턴을 비교하는 그림이라 붙을 자리가 Reference 인데 Reference 에는 본문이 없다"} + ] +} +``` + +A diagram left out for a reason is a normal outcome, the same way `KEEP_IN_SSOT` is. What +is not normal is a leftover nobody looked at — that is the state where the next writer +draws the picture again. Write the ledger when the count first appears, not when it grows. ### Case diff --git a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-inbound-graphql-c04.md b/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-inbound-graphql-c04.md deleted file mode 100644 index 5e3e3be..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-inbound-graphql-c04.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-graphql-c04 -title: 다섯 예산 계층 중 요청 계층만 배선돼 있다 -topic: admission-budget-and-backpressure -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-graphql-c04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-graphql-c04 - file: ../../../final/evidence/rendered/adapter-inbound-graphql-c04.svg - - key: adapter-inbound-graphql-c04-diagram - file: ../../../final/assets/diagrams/adapter-inbound-graphql-c04.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-graphql-c04.txt -source: - - 원본 분석 절은 final/document.md#a16#L354 이다. -module: adapter-inbound-graphql ---- - -# 다섯 예산 계층 중 요청 계층만 배선돼 있다 - -설계 §10이 다섯 계층을 정의하고 `GraphQlDeadlinePropagator`가 그 파생을 담는다. 실제로 배선된 것은 요청 계층 하나이고, 나머지 파생 메서드는 프로덕션 호출자가 없다. - -## 본문 - - - -설계 §10이 다섯 계층을 정의하고 `GraphQlDeadlinePropagator`가 그 파생을 담는다. 실제 강제 상태는 이렇다. - -| 계층 | 파생 지점 | 배선 | -|---|---|---| -| 전송 핸드셰이크 | — | (이 sub-scope 밖) | -| **요청** | `GraphQlPlatformWebInterceptor:135` — `GraphQlDeadline.after(policy.maxExecutionTime(), clock)` | **예** | -| 리졸버 | `GraphQlDeadlinePropagator.resolverBudget(...)` | 아니오 | -| DataLoader 배치 | `GraphQlDeadlinePropagator.dataLoaderBatchTimeout(...)` | 아니오 | -| 다운스트림(DB/HTTP) | `GraphQlDeadlinePropagator.downstreamDeadline(...)` | 아니오 | -| 구독 연결 | `GraphQlDeadlinePropagator.subscriptionDeadline(...)` | 아니오 | - -## 요청 예산에서 파생되는 계층 - -:::evidence key="adapter-inbound-graphql-c04-diagram" alt="배선된 요청 데드라인 상자에서 나가는 화살표가 없고, 네 파생 계층이 파생 없음 이라고 이름 붙은 별도 영역 안에 빗금으로 놓인 구조" caption="요청 예산에서 파생되는 계층" zoom="false" -::: - -## 참조가 갇혀 있는 범위 - -`GraphQlTimeoutPolicy`와 `GraphQlResolverBudget`의 main 참조자를 전수하면 전부 `execution` 패키지 안(그리고 미배선 클러스터 안)이다. - -```text -GraphQlTimeoutPolicy <- GraphQlRequestCancelledException, GraphQlDeadlinePropagator, GraphQlResolverBudget -GraphQlResolverBudget <- GraphQlResolverDescriptor, GraphQlResolverCatalog, GraphQlDeadlinePropagator, GraphQlExecutionProfileValidator -``` - -§12.1. - -## GraphQlDeadlinePropagator 참조 위치 - -:::evidence key="adapter-inbound-graphql-c04" alt="코드베이스에서 GraphQlDeadlinePropagator 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GraphQlDeadlinePropagator 코드베이스 검색 — 5줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-inbound-graphql-c05.md b/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-inbound-graphql-c05.md deleted file mode 100644 index 8b9b82a..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-inbound-graphql-c05.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-graphql-c05 -title: 요청 데드라인이 실제로 실행을 끊는 경로가 있다 -topic: admission-budget-and-backpressure -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-graphql-c05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-graphql-c05 - file: ../../../final/evidence/rendered/adapter-inbound-graphql-c05.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-graphql-c05.txt -source: - - 원본 분석 절은 final/document.md#a16#L376 이다. -module: adapter-inbound-graphql ---- - -# 요청 데드라인이 실제로 실행을 끊는 경로가 있다 - -`GraphQlCancellation`(93)이 세 곳에서 쓰인다. 요청 계층의 데드라인은 만들어지기만 하는 것이 아니라 실행을 실제로 끊는다. - -## 본문 - - - -`GraphQlCancellation`(93)은 `cost/GraphQlRuntimeBudgetTracker` · `advanced/incremental` · `advanced/subscription` 세 곳에서 쓰인다. - -## GraphQlCancellation 참조 위치 - -:::evidence key="adapter-inbound-graphql-c05" alt="코드베이스에서 GraphQlCancellation 를 검색한 출력 30줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GraphQlCancellation 코드베이스 검색 — 30줄 · exit 0" zoom="true" -::: - -## 요청 계층이 완결돼 있다는 뜻 - -요청 데드라인이 실제로 실행을 끊는 경로가 존재한다는 뜻이고, `GraphQlRequestContext.withDeadline`의 단조 조이기와 함께 요청 계층은 완결돼 있다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-inbound-web-c05.md b/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-inbound-web-c05.md deleted file mode 100644 index eca89f6..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-inbound-web-c05.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-web-c05 -title: 예산 게이트 프로퍼티가 자바 한 줄에만 있다 -topic: admission-budget-and-backpressure -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-web-c05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-web-c05 - file: ../../../final/evidence/rendered/adapter-inbound-web-c05.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-web-c05.txt -source: - - 원본 분석 절은 final/document.md#a14#L666 이다. -module: adapter-inbound-web ---- - -# 예산 게이트 프로퍼티가 자바 한 줄에만 있다 - -`backend.web.budgets`를 저장소 전체에서 찾으면 자바 한 줄뿐이다. 어떤 `application.yml`에도 없고 `matchIfMissing`도 없으므로 이 핸들러는 기본 꺼짐이다. - -## 본문 - - - -`backend.web.budgets`를 저장소 전체에서 찾으면 자바 한 줄뿐이다. - -```text -main/.../mvc/budget/WebMvcBudgetExceptionHandler.java:40:@ConditionalOnProperty(prefix = "backend.web.budgets", name = "enabled", havingValue = "true") -``` - -어떤 `application.yml`에도 `backend.web.budgets`가 없고 `matchIfMissing`도 없으므로 이 핸들러는 **기본 꺼짐**이다. - -## BudgetProblemMapper 참조 위치 - -:::evidence key="adapter-inbound-web-c05" alt="코드베이스에서 BudgetProblemMapper 를 검색한 출력 18줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="BudgetProblemMapper 코드베이스 검색 — 18줄 · exit 0" zoom="true" -::: - -## 켜더라도 필요한 빈이 없다 - -그 생성자가 요구하는 `BudgetProblemMapper` 빈을 선언하는 코드가 main·app-bootstrap 어디에도 없다 — 참조자는 두 필터와 이 핸들러 자신뿐이고, 셋 다 빈 정의가 아니다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-inbound-web-c14.md b/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-inbound-web-c14.md deleted file mode 100644 index abd2709..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-inbound-web-c14.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-web-c14 -title: forwarded 헤더를 해석하는 쪽은 피어를 검사하지 않는다 -topic: admission-budget-and-backpressure -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-web-c14 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-web-c14 - file: ../../../final/evidence/rendered/adapter-inbound-web-c14.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-web-c14.txt -source: - - 원본 분석 절은 final/document.md#a14#L1098 이다. -module: adapter-inbound-web ---- - -# forwarded 헤더를 해석하는 쪽은 피어를 검사하지 않는다 - -신뢰 프록시 판정을 담은 `proxy` 패키지 421 LOC이 프로덕션 조립에 들어가지 않는다. 실제로 forwarded 헤더를 해석하는 것은 Spring Boot가 등록하는 필터이고, 그것은 피어가 신뢰된 프록시인지 검사하지 않는다. - -## 본문 - - - -신뢰 프록시 판정을 담은 타입들의 참조를 세면 프로덕션 경로가 없다. - -```text -TrustedProxyPolicy 6 test 1 testkit -NormalizedForwardedHeaders 4 main 8 test <- main 참조자는 proxy 패키지 내부 -ForwardedHeaderSanitizer 2 test 1 testkit -``` - -`proxy` 패키지 421 LOC이 프로덕션 조립에 들어가지 않는다. - -## 실제로 헤더를 해석하는 쪽 - -실제로 forwarded 헤더를 해석하는 것은 Spring Boot의 `server.forward-headers-strategy=framework`(app-bootstrap `application.yml:321` 기본값)가 등록하는 `ForwardedHeaderFilter`/`ForwardedHeaderTransformer`이고, 그것은 **피어가 신뢰된 프록시인지 검사하지 않는다**. §32.2. - -## 분석 원문의 참조 집계 - -:::evidence key="adapter-inbound-web-c14" alt="분석 문서 final/document.md#a14 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a14 발췌 — 15줄" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-inbound-web-c18.md b/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-inbound-web-c18.md deleted file mode 100644 index 0af610a..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-inbound-web-c18.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-web-c18 -title: fileserver 매핑 검증은 회로가 닫혀 있다 -topic: admission-budget-and-backpressure -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-web-c18 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-web-c18 - file: ../../../final/evidence/rendered/adapter-inbound-web-c18.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-web-c18.txt -source: - - 원본 분석 절은 final/document.md#a14#L1336 이다. -module: adapter-inbound-web ---- - -# fileserver 매핑 검증은 회로가 닫혀 있다 - -`attestMapping`의 선언·구현·호출이 모두 존재하고, 그 구현을 만드는 자동설정도 있다. 이 leaf의 다른 sub-scope와 달리 회로가 닫혀 있다. - -## 본문 - - - -`attestMapping`의 선언·구현·호출이 모두 존재한다. - -```text -main/.../nginx/DefaultNginxInternalUriMapper.java:41 (구현) -main/.../nginx/NginxInternalUriMapper.java:32 (선언) -BOOT:autoconfigure/fileserver/FileserverStartupConfiguration.java:87 uriMapper.attestMapping() -``` - -## 빈을 만드는 자동설정 - -`FileserverPlatformAutoConfiguration`이 `DefaultNginxInternalUriMapper`(`:215-216`) · `NginxDownloadStrategy`(`:221-223`) · `FileserverRequestContextFactory`(`:159-161`)를 만든다. 회로 닫힘. - -## FileserverPlatformAutoConfiguration 참조 위치 - -:::evidence key="adapter-inbound-web-c18" alt="코드베이스에서 FileserverPlatformAutoConfiguration 를 검색한 출력 7줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="FileserverPlatformAutoConfiguration 코드베이스 검색 — 7줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-outbound-cache-redis-c04.md b/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-outbound-cache-redis-c04.md deleted file mode 100644 index 3d8989c..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-outbound-cache-redis-c04.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-cache-redis-c04 -title: 대칭 검사기 자신을 검사하는 메타 테스트가 있다 -topic: admission-budget-and-backpressure -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-cache-redis-c04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-cache-redis-c04 - file: ../../../final/evidence/rendered/adapter-outbound-cache-redis-c04.svg - - key: adapter-outbound-cache-redis-c04-diagram - file: ../../../final/assets/diagrams/adapter-outbound-cache-redis-c04.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-cache-redis-c04.txt -source: - - 원본 분석 절은 final/document.md#a10#L245 이다. -module: adapter-outbound-cache-redis ---- - -# 대칭 검사기 자신을 검사하는 메타 테스트가 있다 - -`ReactiveRedisOperations`는 "Mirrors `RedisOperations` method for method"라고 주장하고 `ApiParityTest`가 그것을 반사로 강제한다. 그 위에 검사기가 고장 나 항상 통과하는 상태를 잡는 메타 테스트가 하나 더 있다. - -## 본문 - - - -`ReactiveRedisOperations`는 "Mirrors `RedisOperations` method for method"라고 주장한다. `ApiParityTest`가 그것을 반사로 강제한다 — `PAIRS` 맵에 14쌍의 sync/reactive 인터페이스를 놓고 `everySyncOperationHasReactiveCounterpart`, `everyTypedSurfaceIsInParity`, `theTwoEntryPointsExposeTheSameStructureAccessors`, `everyReactiveMethodReturnsAPublisher`를 돌린다. 두 facade의 접근자 12개는 실제로 동일하다(diff 공백). - -## 검사기에 대한 메타 검사 - -:::evidence key="adapter-outbound-cache-redis-c04-diagram" alt="두 진입점 인터페이스와 대칭 검사와 메타 테스트가 위에서 아래로 쌓이고 검사 방향 화살표가 아래로 그려진 구조" caption="검사기에 대한 메타 검사" zoom="false" -::: - -두 가지가 특히 좋다. 첫째, **예외가 이유와 함께 목록에서 빠져 있다** — Pub/Sub은 sync가 핸들러+closeable subscription이고 reactive는 publisher 자신이 전달하며 취소로 구독을 끊으므로 "different shapes on purpose, so mechanical parity would be the wrong check for them". 둘째, `theInspectorDetectsADivergentReturnShape`라는 **검사기에 대한 메타 test**가 있다 — 대칭 검사기가 고장 나 항상 통과하는 상태를 잡는다. - -## ReactiveRedisOperations 참조 위치 - -:::evidence key="adapter-outbound-cache-redis-c04" alt="코드베이스에서 ReactiveRedisOperations 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ReactiveRedisOperations 코드베이스 검색 — 2줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-outbound-cache-redis-c05.md b/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-outbound-cache-redis-c05.md deleted file mode 100644 index 7cf4d79..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-outbound-cache-redis-c05.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-cache-redis-c05 -title: 렌더된 키 문자열을 받는 API가 없다 -topic: admission-budget-and-backpressure -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-cache-redis-c05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-cache-redis-c05 - file: ../../../final/evidence/rendered/adapter-outbound-cache-redis-c05.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-cache-redis-c05.txt -source: - - 원본 분석 절은 final/document.md#a10#L324 이다. -module: adapter-outbound-cache-redis ---- - -# 렌더된 키 문자열을 받는 API가 없다 - -`QualifiedRedisKey`의 javadoc이 이 계층의 규칙이다 — 이미 렌더된 키 문자열을 받는 API가 없으므로 네임스페이스·슬롯·크기 규칙을 우회할 수 없다. 구조가 그것을 강제한다. - -## 본문 - - - -`QualifiedRedisKey`의 javadoc이 이 계층의 규칙이다 — "This is the only key shape the SDK accepts. **There is no API that takes an already rendered key string**, so namespace, slot, and size rules cannot be bypassed." - -## 타입이 강제하는 형태 - -`RedisTypedKey`는 9종만 허용하는 sealed interface고(`ValueKey`·`HashKey`·`ListKey`·`SetKey`·`SortedSetKey`·`BitmapKey`·`HyperLogLogKey`·`GeoKey`·`StreamKey`), 전부 `QualifiedRedisKey` + 코덱으로 구성된다. `QualifiedRedisKey`는 `RedisNamespace`(토큰 3개) + `RedisKeyName`(entity 토큰 + identifier) + 선택적 `RedisSlotTag`다. 그리고 `RedisKeyRenderer`가 **중괄호를 쓰는 유일한 장소**라서 Cluster 해시 태그가 "the tag and nothing else"를 덮는다. - -## 분석 원문의 규칙 서술 - -:::evidence key="adapter-outbound-cache-redis-c05" alt="분석 문서 final/document.md#a10 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a10 발췌 — 15줄" zoom="true" -::: - -## 이 검사가 PII 방지의 완결이 아니라고 적는다 - -`RedisKeyRules`의 자기 한정도 정직하다 — 규칙은 "mechanical"이며 "Values that are indistinguishable from an ordinary surrogate identifier, such as a bare digit string, cannot be rejected here; those must be fingerprinted by the caller before they become a key part." - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-outbound-cache-redis-c12.md b/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-outbound-cache-redis-c12.md deleted file mode 100644 index 57757e5..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-outbound-cache-redis-c12.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-cache-redis-c12 -title: 탈출구가 두 겹의 사전 승인으로 닫혀 있다 -topic: admission-budget-and-backpressure -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-cache-redis-c12 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-cache-redis-c12 - file: ../../../final/evidence/rendered/adapter-outbound-cache-redis-c12.svg - - key: adapter-outbound-cache-redis-c12-diagram - file: ../../../final/assets/diagrams/adapter-outbound-cache-redis-c12.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-cache-redis-c12.txt -source: - - 원본 분석 절은 final/document.md#a10#L684 이다. -module: adapter-outbound-cache-redis ---- - -# 탈출구가 두 겹의 사전 승인으로 닫혀 있다 - -`RedisRawGateway`에는 `execute(String, byte[]...)`가 없다. 원시 명령은 정책 카탈로그의 분류와 배포의 승인 등록 둘 다를 통과해야 하고, 어느 쪽도 요청 시점에 결정되지 않는다. - -## 본문 - - - -`RedisRawGateway`의 javadoc이 존재 이유와 한계를 함께 적는다 — "There is no `execute(String, byte[]...)` here or anywhere else in the SDK. The escape hatch exists because **some commands genuinely have no typed form worth building**, not because arbitrary command execution is acceptable; every one of them is named, bounded, and audited before it can be sent." - -## 원시 명령이 지나야 하는 두 문 - -:::evidence key="adapter-outbound-cache-redis-c12-diagram" alt="정책 카탈로그 분류에서 배포 승인 등록으로, 다시 원시 게이트웨이로 이어지는 왼쪽에서 오른쪽 흐름" caption="원시 명령이 지나야 하는 두 문" zoom="false" -::: - -승인이 **두 개의 독립된 문**을 모두 통과해야 한다(`RawCommandApprovals`). - -1. 명령이 정책 카탈로그에서 `RAW_ONLY`로 분류돼 있어야 한다 — "the organization's decision about which commands may ever leave through this door" -1. 배포가 그 명령에 대한 승인(`ApprovedRawCommand`)을 등록해야 한다 - -"Neither alone is enough, and neither is decided at request time." 그리고 R3/R4는 어느 쪽이든 거부된다. - -## RedisRawGateway 참조 위치 - -:::evidence key="adapter-outbound-cache-redis-c12" alt="코드베이스에서 RedisRawGateway 를 검색한 출력 7줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RedisRawGateway 코드베이스 검색 — 7줄 · exit 0" zoom="true" -::: - -## 승인이 배포 산출물인 이유 - -`ApprovedRawCommand`는 **배포 산출물**이다 — 명령 identity, 최대 인자 수, 요청/응답 바이트 상한, 타임아웃, 응답 디코더를 프로세스 시작 전에 고정한다. 토큰은 `RawCommandApprovals`만 발급하고, 검증은 (a) 토큰 타입이 내부 record인지, (b) **발급 레지스트리 인스턴스가 같은지**(`issued.origin != this`), (c) 정책 id가 일치하는지, (d) 제시된 승인이 등록된 것과 같은지 넷을 본다. - -## 키 위치를 모르면 기본이 거부다 - -**`RawMovableKeys`가 이 패키지에서 가장 흥미롭다.** movable key spec(예: `SORT`)은 키 위치를 인자 목록이 결정하므로 정적으로 알 수 없고, 그러면 네임스페이스 검사를 할 수 없다. 기본은 여전히 거부다. 예외로 `SORT`/`SORT_RO` 파서 하나가 등록돼 있는데, 그 설계가 명시적이다 — "a parser that knows **exactly one command shape** and refuses everything else." - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-outbound-fileserver-c06.md b/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-outbound-fileserver-c06.md deleted file mode 100644 index 718c12f..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-outbound-fileserver-c06.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-fileserver-c06 -title: 거부 메시지가 역할 모델을 설명하지 않는다 -topic: admission-budget-and-backpressure -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-fileserver-c06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-fileserver-c06 - file: ../../../final/evidence/rendered/adapter-outbound-fileserver-c06.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-fileserver-c06.txt -source: - - 원본 분석 절은 final/document.md#a08#L507 이다. -module: adapter-outbound-fileserver ---- - -# 거부 메시지가 역할 모델을 설명하지 않는다 - -`RoleBasedFileAccessPolicy`는 열 개 연산을 READ/WRITE/ADMIN 세 계층으로 접고, 거부 메시지는 필요한 역할도 주체의 역할도 말하지 않는다. - -## 본문 - - - -`RoleBasedFileAccessPolicy`는 열 개 연산을 READ/WRITE/ADMIN 세 계층으로 접는다. 근거가 적혀 있다 — 연산별 역할 맵은 `COPY`를 주고 `CREATE`를 안 주는 조합을 허용하는데 "a copy creates a file"이므로 제한처럼 보이고 제한이 아니다. - -## admin이 write를 상속하지 않는다 - -삭제할 수 있다는 이유로 force-delete까지 되면 감사되는 관리 평면이 일반 데이터 평면으로 도달 가능해진다. 빈 admin 역할 집합은 생성자가 거부한다("would leave the management plane unreachable rather than protected"). - -## RoleBasedFileAccessPolicy 참조 위치 - -:::evidence key="adapter-outbound-fileserver-c06" alt="코드베이스에서 RoleBasedFileAccessPolicy 를 검색한 출력 12줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RoleBasedFileAccessPolicy 코드베이스 검색 — 12줄 · exit 0" zoom="true" -::: - -## 거부 메시지가 담지 않는 것 - -거부 메시지는 필요한 역할도 주체의 역할도 말하지 않는다 — "a denial that reported what was missing would turn every 403 into a readable description of the role model". `LocalStorageFailures`의 어떤 메시지도 경로·마운트·루트를 담지 않고, 감사 어댑터가 쓰는 필드는 전부 지문·코드·불투명 식별자다. - -## 이름 자체가 장치인 클래스 - -`UnenforcedFileAccessPolicy`의 설계도 기록할 만하다. 이름 자체가 장치다 — composition root가 **타입 이름으로 매치해** production startup을 거부한다. "A permissive default that looked like a real policy would ship as one." - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-outbound-httpclient-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-outbound-httpclient-c01.md deleted file mode 100644 index 781535f..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-outbound-httpclient-c01.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-httpclient-c01 -title: 닿지 않는 설정을 무시하지 않고 거부한다 -topic: admission-budget-and-backpressure -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-httpclient-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-httpclient-c01 - file: ../../../final/evidence/rendered/adapter-outbound-httpclient-c01.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-httpclient-c01.txt -source: - - 원본 분석 절은 final/document.md#a11#L90 이다. -module: adapter-outbound-httpclient ---- - -# 닿지 않는 설정을 무시하지 않고 거부한다 - -`ClientProfileValidator`는 바인딩은 되지만 어떤 전송에도 닿지 않는 설정을 무시하지 않고 거부한다. 앞선 열 개 모듈에서 반복해 발견한 "선언되었으나 아무것도 하지 않는 설정" 패턴을 이 모듈은 명시적 거부로 처리한다. - -## 본문 - - - -이 저장소에서 본 가장 조밀한 설정 검증기다. `validate(profile, environment)`가 11개 검사 그룹을 돌리고 결과를 정렬해 "a configuration error reports deterministically across runs and machines"를 보장한다. - -## 소비자가 없던 설정을 거부로 바꾼 이유 - -특히 이 leaf에서만 보이는 태도가 하나 있다 — **바인딩은 되지만 어떤 전송에도 닿지 않는 설정을 무시하지 않고 거부한다.** - -> "Three of them had no consumer anywhere: `timeout.dns`, `proxy.credential-provider` and `proxy.import-ambient-no-proxy`. An operator who set a DNS timeout believed resolution was bounded and it was not; one who named a proxy credential provider believed the proxy was authenticated and it was not… **the honest position is to refuse a value the platform cannot honour instead of accepting it and doing nothing.**" - -기본값은 통과시키므로 "only a deliberate, unmet request fails"다. 같은 논리가 관측 설정에도 적용된다 — `full-url-recording`은 아무도 읽지 않았고 `body-logging`은 actuator 보고에만 닿았다. "Leaving them that way is the worse of the two failure modes — an operator who set them believed the platform was recording full URLs or bodies, and an operator who left them false had no assurance that it was not." 지금은 production에서 둘 다 거부된다. - -## ClientProfileValidator 참조 위치 - -:::evidence key="adapter-outbound-httpclient-c01" alt="코드베이스에서 ClientProfileValidator 를 검색한 출력 8줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ClientProfileValidator 코드베이스 검색 — 8줄 · exit 0" zoom="true" -::: - -## 나머지 검사가 막는 조용한 다운그레이드 - -- **`REACTIVE_REDIRECT_UNSUPPORTED`** — 엔진 리다이렉트는 모든 전송에서 꺼져 있고 hop별 재검증을 하는 coordinator는 블로킹 스택에만 있다. 리액티브 프로파일이 redirect를 켜면 "the caller received the 302 as an ordinary response and read its empty body as the answer." 거부가 정직한 결과다 — "a configured guarantee that silently does nothing is worse than one the platform declines to offer." -- **`HTTP2_REQUIRED_TRANSPORT_UNSUPPORTED`** — `ProtocolIntent`가 "H2를 선호"와 "H2를 요구"를 구분한다. JDK 클라이언트는 `HTTP_2`를 선호로 다뤄 조용히 HTTP/1.1로 협상하고 Apache classic은 HTTP/1.1 전용이라, `HTTP_2`만 선언한 프로파일이 "ran happily over HTTP/1.1, and nothing anywhere said so." -- **`TLS_PROTOCOL_SET_REQUIRED`** — 빈 집합이 통과하면 JVM 기본값이 선택되어, "a profile that meant to pin a TLS floor got whatever the platform default happened to be." -- **`DYNAMIC_TARGET_PROXY_UNSUPPORTED`** — 포워드 프록시는 호스트명을 자기 쪽에서 다시 해석하므로 "The SSRF defence would be present, correct, and bypassed." -- **`RETRY_POLICY_CONTRADICTS_ATTEMPTS`** — `policy`를 실행 경로에서 아무도 읽지 않아 "the actuator could report `retryPolicy: none` for a profile that was retrying three times." - -## 이 검증기는 실제로 조립돼 있다 - -`app-bootstrap`의 `HttpClientStartupValidator:37`이 이 검증기를 생성한다(`168-...` §8.1). 이 leaf는 앞선 cache-redis와 달리 **실제로 조립돼 있다** — app-bootstrap에 이 leaf를 위한 auto-configuration 12개가 있다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-outbound-httpclient-c05.md b/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-outbound-httpclient-c05.md deleted file mode 100644 index 57a2644..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-outbound-httpclient-c05.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-httpclient-c05 -title: 열린 회로가 토큰과 permit을 쓰기 전에 거절한다 -topic: admission-budget-and-backpressure -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-httpclient-c05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-httpclient-c05 - file: ../../../final/evidence/rendered/adapter-outbound-httpclient-c05.svg - - key: adapter-outbound-httpclient-c05-diagram - file: ../../../final/assets/diagrams/adapter-outbound-httpclient-c05.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-httpclient-c05.txt -source: - - 원본 분석 절은 final/document.md#a11#L323 이다. -module: adapter-outbound-httpclient ---- - -# 열린 회로가 토큰과 permit을 쓰기 전에 거절한다 - -`AttemptResiliencePipeline`이 물리 시도마다 Circuit Breaker → Rate Limiter → Bulkhead → HTTP 호출을 고정 순서로 적용하고 역순으로 해제한다. 순서는 장식이 아니다. - -## 본문 - - - -`AttemptResiliencePipeline`이 물리 시도마다 **Circuit Breaker → Rate Limiter → Bulkhead → HTTP 호출**을 고정 순서로 적용하고 역순으로 해제한다. - -## 시도마다 지나는 가드 순서 - -:::evidence key="adapter-outbound-httpclient-c05-diagram" alt="회로 차단기와 요금 제한기와 벌크헤드와 HTTP 호출이 왼쪽에서 오른쪽으로 이어지고 화살표에 허가와 토큰과 permit 이 붙은 구조" caption="시도마다 지나는 가드 순서" zoom="false" -::: - -> "The order is not cosmetic. An open circuit must reject before a rate token or a bulkhead permit is spent, otherwise **a dead upstream keeps consuming the quota and concurrency that healthy upstreams need.**" -> "A local rejection (rate limiter or bulkhead) is deliberately *not* recorded as a circuit error: the upstream never saw the request, and **counting our own back-pressure as upstream failure would open the breaker on a healthy dependency.**" - -## 브레이커가 503을 보지 못하던 이력 - -이전에는 원시 전송만 파이프라인 안에서 돌고 응답→예외 매핑이 밖에서 일어나서 "a 503 completed the call normally, the breaker recorded a success, and **an upstream that answered nothing but 503 never opened its circuit. The thing the breaker is for was the one thing it could not see.**" 지금은 `remoteFailure` 분류기가 반환값을 보고 브레이커에 알린다. - -## AttemptResiliencePipeline 참조 위치 - -:::evidence key="adapter-outbound-httpclient-c05" alt="코드베이스에서 AttemptResiliencePipeline 를 검색한 출력 16줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="AttemptResiliencePipeline 코드베이스 검색 — 16줄 · exit 0" zoom="true" -::: - -## 규칙을 덮는 테스트와 프로토콜 증거 - -test 47개가 이 규칙들을 촘촘히 덮는다 — `appliesCircuitThenRateLimiterThenBulkheadPerAttempt`, `openCircuitDoesNotConsumeRateOrBulkheadPermit`, `bulkheadRejectionReleasesTheRateLimiterAndIsNotACircuitError`, `answeredStatusesDoNotRetryANonIdempotentOperation`, `deniesOneShotBodyEvenForPut`, `honorsRetryAfterOnlyInsideDeadline`, `protocolProofOfNonProcessingWinsOverEverything`, `streamAfterGoAwayLastIdIsPeerNotProcessed` 등. - -`Http2ProtocolEvidence`는 프로토콜 수준 증거를 다룬다 — `REFUSED_STREAM`과 GOAWAY의 last-stream-id보다 큰 스트림 id는 **피어가 처리하지 않았음의 증명**이라 `NOT_SENT`로 승격되고, 그 이하 id의 리셋은 여전히 모호하다(`streamAtOrBelowGoAwayLastIdStaysAmbiguous`, `aBareStreamResetProvesNothing`). - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-outbound-httpclient-c08.md b/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-outbound-httpclient-c08.md deleted file mode 100644 index b1dd0b8..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-outbound-httpclient-c08.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-httpclient-c08 -title: 전송의 선언이 프로파일보다 약하면 startup이 실패한다 -topic: admission-budget-and-backpressure -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-httpclient-c08 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-httpclient-c08 - file: ../../../final/evidence/rendered/adapter-outbound-httpclient-c08.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-httpclient-c08.txt -source: - - 원본 분석 절은 final/document.md#a11#L573 이다. -module: adapter-outbound-httpclient ---- - -# 전송의 선언이 프로파일보다 약하면 startup이 실패한다 - -`TransportCapabilityValidator`가 프로파일이 요구하는 것과 전송이 선언한 것을 대조해 부족분을 이름으로 모아 거부한다. 메시지는 프로파일 설정과 능력 이름만 담고 URL·주소·비밀은 담지 않는다. - -## 본문 - - - -`TransportCapabilityValidator`가 프로파일이 요구하는 것과 전송이 선언한 것을 대조해 부족분을 이름으로 모아 거부한다 — 프로토콜, route pool, 유계 pending 큐, proxy, mutual TLS, 동적 대상 안정성. 메시지는 "profile settings and capability names only — never a URL, address, or secret." - -## 능력이 데이터로 선언된다 - -`ReactiveTransportCapabilities.reactorNetty()`는 9개 능력을 전부 `true`로, `jettyHttp3Experimental()`은 route pool·유계 큐·DNS 핀·동적 안정성을 `false`로 선언한다. HTTP/3는 `compileOnly` 의존이라 클래스가 없으면 `Http3CapabilityReport`가 전송을 거부한다 — "the failure mode is a startup error rather than a `NoClassDefFoundError` mid-call"(§0). - -## TransportCapabilityValidator 참조 위치 - -:::evidence key="adapter-outbound-httpclient-c08" alt="코드베이스에서 TransportCapabilityValidator 를 검색한 출력 11줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="TransportCapabilityValidator 코드베이스 검색 — 11줄 · exit 0" zoom="true" -::: - -## testkit이 별도 source set인 이유 - -계약을 담은 클래스 35개(`BlockingTransportContract`·`ReactiveTransportContract`·`RetrySafetyContract`·`ResourceLifecycleContract`·`ObservabilityContract`·`DynamicTargetSecurityContract`)를 test·performance·jmh 세 lane이 공유한다. `NettyLeakDetectionExtension`은 leak detector 레벨을 **믿지 않고 확인한다** — "asserts the level rather than trusting the flag reached the forked JVM"(§0). - -## 성능 lane이 재지 않는 것 - -성능 lane 7개는 자원 상한을 검증한다 — `PoolSaturationPerformanceTest`·`RetryStormBudgetTest`·`RuntimeRotationDrainTest`·`OAuthRefreshContentionTest`·`LargeBodyResourceTest`·`Http2StreamSaturationTest`. §15에서 남긴 질문(`ObjectBody.replayability()`의 반사 비용을 재는 lane이 있는가)의 답은 **없다** — 풀·재시도·회전·토큰 경합·본문 크기·H2 스트림을 재고 본문 재생 가능성 판정 비용은 재지 않는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-outbound-objectstorage-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-outbound-objectstorage-c02.md deleted file mode 100644 index 7edc26d..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-adapter-outbound-objectstorage-c02.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-objectstorage-c02 -title: 레거시 경로 셋이 서로 다른 스위치로 서로를 배제한다 -topic: admission-budget-and-backpressure -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-objectstorage-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-objectstorage-c02 - file: ../../../final/evidence/rendered/adapter-outbound-objectstorage-c02.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-objectstorage-c02.txt -source: - - 원본 분석 절은 final/document.md#a09#L92 이다. -module: adapter-outbound-objectstorage ---- - -# 레거시 경로 셋이 서로 다른 스위치로 서로를 배제한다 - -폐기 경로가 셋인데 서로 다른 스위치를 쓰고 서로를 배제한다. 겹침은 컴파일러가 거부한다. - -## 본문 - - - -폐기 경로가 셋인데 서로 다른 스위치를 쓰고 서로를 배제한다. - -| 경로 | 스위치 | 성격 | -|---|---|---| -| 선호 임시 활성화 | `app.object-storage.legacy.enabled=true` + 명시적 backend | `ObjectStoragePort`(whole-`byte[]`) 노출 | -| 구 alias | `ca-skeleton.objectstorage.*` | `LegacyObjectStorageActivationGuard` 조건, canonical과 혼용 시 실패 | -| 채택(adoption) | `app.object-storage.legacy-adoption.enabled=true` | raw locator 유지보수 전용, 별도 config 클래스 | - -## 겹침을 거부하는 지점 - -`ObjectStorageBindingCompiler.rejectLegacyOverlap`가 legacy filesystem 루트와 canonical provider 루트가 **어느 방향으로든 포함 관계**면 거부한다. - -## ObjectStorageBindingCompiler 참조 위치 - -:::evidence key="adapter-outbound-objectstorage-c02" alt="코드베이스에서 ObjectStorageBindingCompiler 를 검색한 출력 22줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ObjectStorageBindingCompiler 코드베이스 검색 — 22줄 · exit 0" zoom="true" -::: - -## 채택 모드가 추가로 요구하는 것 - -`LegacyObjectAdoptionSettings`는 `APPLY` 모드일 때 검토된 manifest 경로와 64자리 SHA-256을 요구하고, batch size 1–1000, timeout 5분 이내를 강제한다. legacy runtime은 `AutoCloseable` holder로 감싸 S3 client 수명을 정확히 소유하고, `@Bean(destroyMethod = "close")`로 등록된다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-application-core-c08.md b/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-application-core-c08.md deleted file mode 100644 index 20911c5..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-application-core-c08.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -kind: CONCEPT -slug: application-core-c08 -title: 검증 권한과 삭제 권한을 분리한 staged lifecycle -topic: admission-budget-and-backpressure -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:application-core-c08 -evidenceCapturedOn: 2026-09-01 -assets: - - key: application-core-c08 - file: ../../../final/evidence/rendered/application-core-c08.svg -evidence: - - ../../../final/evidence/raw/application-core-c08.txt -source: - - 원본 분석 절은 final/document.md#a03#L190 이다. -module: application-core ---- - -# 검증 권한과 삭제 권한을 분리한 staged lifecycle - -semantic objectstorage API는 provider/filesystem type을 노출하지 않고, lifecycle을 staged → verified → published로 분리하며, scanner 권한과 purge 권한을 나눠 검증 주체가 임의 삭제까지 할 수 없게 한다. - -## 관계 - -- **legacy storage/notification compatibility surface의 제거 조건 추적** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -semantic objectstorage API는 provider/filesystem type을 노출하지 않는다. object identity/reference는 prefix + check digit를 포함한 opaque routed representation이고 redacted rendering을 제공한다. tampered/cross-prefix reference를 거부한다. - -## content I/O가 무한 루프로 가지 않는 이유 - -content I/O는 bounded pull/push callback context와 budget/cancellation/chunk contract를 사용하며 callback lifetime 밖에서 context를 재사용할 수 없다. zero-progress가 무한 loop로 이어지지 않도록 bounded 후 실패한다. - -## FullContentIdentity 참조 위치 - -:::evidence key="application-core-c08" alt="코드베이스에서 FullContentIdentity 를 검색한 출력 1줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="FullContentIdentity 코드베이스 검색 — 1줄 · exit 0" zoom="true" -::: - -## 검증 주체가 삭제까지 하지 못하게 나눈다 - -lifecycle은 staged -> verified -> published를 분리한다. scanner verdict는 exact stage/version/operation/policy revision에 결합되고 publish/cleanup mutation은 exact-version/fencing을 요구한다. scanner 권한과 purge 권한은 분리돼 검증 주체가 임의 삭제까지 할 수 없게 한다. - -## 상한과 신원 요구 - -transient bearer grant는 URI/header를 redaction하고 TTL은 최대 24시간으로 제한한다. multipart part count는 1..10000이고 completion은 expected content identity를 요구한다. `FullContentIdentity`는 SHA-256 기반으로 ETag를 content identity로 오인하지 않는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-grpc-advanced-bootstrap-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-grpc-advanced-bootstrap-c01.md deleted file mode 100644 index 736e7cd..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-grpc-advanced-bootstrap-c01.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -kind: CONCEPT -slug: grpc-advanced-bootstrap-c01 -title: 능력을 하나씩 등급 매기는 것이 설계다 -topic: admission-budget-and-backpressure -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:grpc-advanced-bootstrap-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-advanced-bootstrap-c01 - file: ../../../final/evidence/rendered/grpc-advanced-bootstrap-c01.svg -evidence: - - ../../../final/evidence/raw/grpc-advanced-bootstrap-c01.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-advanced-bootstrap#L53 이다. -module: grpc-advanced-bootstrap ---- - -# 능력을 하나씩 등급 매기는 것이 설계다 - -능력 15종을 한 깃발로 묶지 않고 각각 등급을 매긴다. 그렇게 하지 않으면 gRPC-Web을 켜는 결정과 xDS를 켜는 결정이 같은 결정이 된다. - -## 본문 - - - -능력을 하나씩 등급 매기는 것이 설계다. - -> "Bundling them under one 'advanced' flag makes enabling gRPC-Web — a compatibility bridge with a proxy in front of it — the same decision as enabling xDS, which brings a control plane and its outage modes. They are not the same decision, and a single switch is how the second one gets made by accident." - -| 등급 | 시작 가능 | production 별도 승인 | -|---|---|---| -| `ADVANCED_STABLE` | 예 | 아니오 | -| `EXPERIMENTAL` | 예 | **예** | -| `WATCH` | 아니오 | — | -| `DISABLED` | 아니오 | — | - -기본 등급 분포는 `ADVANCED_STABLE` 11, `EXPERIMENTAL` 3(`HEDGING`·`CUSTOM_LOAD_BALANCER`·`XDS`), `WATCH` 1(`EDITION_2026`)이다. - -## EXPERIMENTAL에 두 번째 승인을 요구하는 근거 - -> "The flag says somebody wanted the feature; the approval says somebody accepted that its failure modes are not fully characterised, which is a different person's decision on most teams." - -## 이 기록이 다루는 파일 범위 - -:::evidence key="grpc-advanced-bootstrap-c01" alt="코드베이스에서 파일 목록을 만든 출력 9줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 9줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-grpc-advanced-streaming-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-grpc-advanced-streaming-c01.md deleted file mode 100644 index 715113a..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-grpc-advanced-streaming-c01.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: grpc-advanced-streaming-c01 -title: 상한 없는 request(n)은 단계만 늘린 무제한 버퍼링이다 -topic: admission-budget-and-backpressure -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:grpc-advanced-streaming-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-advanced-streaming-c01 - file: ../../../final/evidence/rendered/grpc-advanced-streaming-c01.svg -evidence: - - ../../../final/evidence/raw/grpc-advanced-streaming-c01.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-advanced-streaming#L86 이다. -module: grpc-advanced-streaming ---- - -# 상한 없는 request(n)은 단계만 늘린 무제한 버퍼링이다 - -수동 흐름 제어는 승인이 record의 필드이고 거짓이면 생성자가 거부한다. 수요 상한과 교착 감시가 필수다. - -## 본문 - - - -승인이 record 의 필드이고 거짓이면 생성자가 거부한다. - -> "Approval is a field because this capability is granted per method, not per service. A method that reads a large result set benefits; the one next to it does not, and enabling both because they share a service is how the second one acquires a bug nobody was looking for." - -수요 상한과 교착 감시가 필수다 — 상한 없는 `request(n)` 은 단계만 늘린 무제한 버퍼링이다. - -## 감시견이 비교하는 두 시각 - -감시견은 잠들지 않고 두 시각을 비교한다 — 마지막으로 수요를 요청한 때와 마지막으로 메시지가 움직인 때. 둘 다 시간 제한만큼 멈춰 있으면 교착이다. - -## 이 기록이 다루는 파일 범위 - -:::evidence key="grpc-advanced-streaming-c01" alt="코드베이스에서 파일 목록을 만든 출력 14줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 14줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-messaging-admin-api-c04.md b/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-messaging-admin-api-c04.md deleted file mode 100644 index 424e3af..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-messaging-admin-api-c04.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-admin-api-c04 -title: 실행 경로에서 같은 검사가 세 지점에 겹친다 -topic: admission-budget-and-backpressure -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-admin-api-c04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-admin-api-c04 - file: ../../../final/evidence/rendered/messaging-admin-api-c04.svg -evidence: - - ../../../final/evidence/raw/messaging-admin-api-c04.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-admin-api#L493 이다. -module: messaging-admin-api ---- - -# 실행 경로에서 같은 검사가 세 지점에 겹친다 - -계획·승인 발급·실행 세 경로 중 실행 경로에서 검사가 `verify` / `Approved*Plan` 생성자 / guard 세 지점에 걸쳐 겹친다. 방어적 중복이다. - -## 본문 - - - -실행 경로가 셋으로 나뉜다. - -- **경로 A — 계획** (승인 불필요, dry run 무료) -- **경로 B — 승인 발급** (이 리프 밖, 변경관리 시스템) -- **경로 C — 실행** - -## ApprovalVerifier 참조 위치 - -:::evidence key="messaging-admin-api-c04" alt="코드베이스에서 ApprovalVerifier 를 검색한 출력 29줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ApprovalVerifier 코드베이스 검색 — 29줄 · exit 0" zoom="true" -::: - -## 실행 경로에서 검사가 겹치는 세 지점 - -경로 C 에서 검사가 세 지점(`verify` / `Approved*Plan` / guard)에 걸쳐 겹친다. `ApprovalVerifier` javadoc 이 그 이유를 설명한다. 서명 검증이 통과하면 나머지는 이미 보장되지만, `ApprovedReplayPlan` 생성자와 guard 가 같은 것을 다시 본다. 방어적 중복이며 §12.3(a) 에서 다시 다룬다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-messaging-policy-c05.md b/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-messaging-policy-c05.md deleted file mode 100644 index 1755b03..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-messaging-policy-c05.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-policy-c05 -title: 배치 상한이 개수와 바이트 두 축인 이유 -topic: admission-budget-and-backpressure -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-policy-c05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-policy-c05 - file: ../../../final/evidence/rendered/messaging-policy-c05.svg -evidence: - - ../../../final/evidence/raw/messaging-policy-c05.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-policy#L423 이다. -module: messaging-policy ---- - -# 배치 상한이 개수와 바이트 두 축인 이유 - -개수 상한만으로는 큰 메시지 몇 개가 브로커 프레임을 넘고, 바이트 상한만으로는 아주 많은 작은 메시지가 요청 타임아웃을 넘는다. `checkBatch`가 각 항목에 `checkPayload`도 부르므로 셋이 함께 적용된다. - -## 관계 - -- **구성 오류는 한 예외 타입과 안정 코드로 보고한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **저장소 밖 문서를 절 번호로 인용하지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **부팅 경로의 알고리즘 복잡도는 문서화한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -**배치 상한이 두 축인 이유**가 적혀 있다. - -```java -// PayloadLimitGuard.java:16-18 - *

Batches are limited by count and bytes. A count limit alone lets a handful of large - * messages exceed the broker's frame; a byte limit alone lets a huge number of tiny messages exceed - * its request timeout. -``` - -`checkBatch`가 각 항목에 대해 `checkPayload`도 부르므로 **개별 상한 · 개수 상한 · 총합 상한** 셋이 함께 적용된다. - -## 이 기록이 다루는 파일 범위 - -:::evidence key="messaging-policy-c05" alt="코드베이스에서 파일 목록을 만든 출력 25줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 25줄 · exit 0" zoom="true" -::: - -## 프로파일 검증 실패가 MessagingException 밖인 이유 - -프로파일 검증 실패는 `IllegalArgumentException`이다 — `MessagingException` 계층 밖이다. 시작 시점의 구성 오류이지 메시지 실패가 아니므로 일관적이다. 다만 `MessagingConfigurationException`("Raised at startup wherever possible")이 존재하는데 쓰이지 않는다 — §17의 P3. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-messaging-schema-avro-c07.md b/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-messaging-schema-avro-c07.md deleted file mode 100644 index 6d486f4..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-messaging-schema-avro-c07.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-schema-avro-c07 -title: 바이트 상한이 잘못된 공격에 적용돼 있었다 -topic: admission-budget-and-backpressure -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-schema-avro-c07 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-schema-avro-c07 - file: ../../../final/evidence/rendered/messaging-schema-avro-c07.svg -evidence: - - ../../../final/evidence/raw/messaging-schema-avro-c07.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-schema-avro#L457 이다. -module: messaging-schema-avro ---- - -# 바이트 상한이 잘못된 공격에 적용돼 있었다 - -테스트 클래스 javadoc이 세 결함을 보존한다. 세 번째는 배열 원소 수 주장을 신뢰하고 할당하던 상태이고, 그때 유일하게 있던 방어가 바이트 상한이었다. - -## 관계 - -- **모드 enum을 분기 조건으로 쓰면 각 분기에 테스트를 둔다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **컬렉션 순서가 계약이면 양쪽에서 같은 방향으로 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **안정 코드는 판단 단위로 정하고 구현 단위로 정하지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -테스트 클래스 javadoc이 세 결함을 보존한다. - -| 위치 | 이전 상태 | 그것이 만든 실패 | -|---|---|---| -| `AvroRegistryBoundsTest` javadoc | 중첩 맵에 `Map.copyOf`(얕은 복사) | 호출자가 생성 후 스키마 교체 가능 → Avro는 실패하지 않고 그럴듯한 쓰레기를 만듦 | -| `AvroRegistryBoundsTest` javadoc | `decodeEvolved`에 크기 검사 없음 | producer가 앞서 나간 뒤 **모든 메시지**가 지나는 경로가 무제한 입력을 수용 | -| `AvroHostileInputTest` javadoc | 배열 원소 수 주장을 신뢰하고 할당 | 5바이트로 4억 원소 배열 → `OutOfMemoryError`, codec이 분류할 수 없는 실패, consumer 스레드에서 프로세스 사망 | - -## 세 번째가 형태상 흥미로운 이유 - -**바이트 상한이라는 올바른 도구가 잘못된 공격에 적용되어 있었다.** 테스트 javadoc이 그것을 한 문장으로 적는다: "The byte limit is the wrong instrument for this attack and was the only one in place." - -## 이 기록이 다루는 파일 범위 - -:::evidence key="messaging-schema-avro-c07" alt="코드베이스에서 파일 목록을 만든 출력 2줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 2줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-messaging-spring-cloud-stream-bridge-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-messaging-spring-cloud-stream-bridge-c03.md deleted file mode 100644 index b45fce2..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-messaging-spring-cloud-stream-bridge-c03.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-spring-cloud-stream-bridge-c03 -title: 보장에 의존하는 순간 브리지를 거절한다 -topic: admission-budget-and-backpressure -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-spring-cloud-stream-bridge-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-spring-cloud-stream-bridge-c03 - file: ../../../final/evidence/rendered/messaging-spring-cloud-stream-bridge-c03.svg - - key: messaging-spring-cloud-stream-bridge-c03-diagram - file: ../../../final/assets/diagrams/messaging-spring-cloud-stream-bridge-c03.svg -evidence: - - ../../../final/evidence/raw/messaging-spring-cloud-stream-bridge-c03.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-spring-cloud-stream-bridge#L146 이다. -module: messaging-spring-cloud-stream-bridge ---- - -# 보장에 의존하는 순간 브리지를 거절한다 - -브리지는 상호운용을 위해 존재하고 그 위험은 구체적이다 — Stream이 자기 binder 설정을 소유하므로 목적지 프로파일이 모르는 직렬화기·오류 처리·확인 모드를 바인딩이 조용히 얻을 수 있다. 그래서 플랫폼의 보장에 의존하지 않는 목적지만 허용한다. - -## 관계 - -- **허용 의존 목록은 상한이므로 미사용을 잡지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **등록을 받는 컴포넌트는 해제도 제공한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **함께 읽히는 두 맵은 한 값으로 묶는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **에러 메시지가 지시하는 설정은 그 설정을 읽는 코드와 함께 존재해야 한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **한 개념의 등록 상태를 두 객체가 나눠 갖지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -브리지의 위험이 무엇인지 javadoc이 먼저 적는다. - -> "The bridge exists for interoperability with existing Spring Cloud Stream bindings, and its risk is specific: Stream owns its own binder configuration, so a binding can quietly acquire its own serializer, its own error handling, and its own acknowledgement mode — none of which the destination profile knows about." - -**세 거절이 `DestinationProfile`의 세 필드를 직접 본다.** - -| 조건 | 코드 | -|---|---| -| `profile.isOrdered()` — `orderingScope != NONE` | `STREAM_BRIDGE_ORDERING_UNSUPPORTED` | -| `profile.retry().mode() != RetryMode.NONE` | `STREAM_BRIDGE_RETRY_UNSUPPORTED` | -| `profile.deadLetter().enabled()` | `STREAM_BRIDGE_DLQ_UNSUPPORTED` | - -## 브리지가 허용되는 범위 - -:::evidence key="messaging-spring-cloud-stream-bridge-c03-diagram" alt="가드 경계 안에 순서 없음과 재시도 없음과 DLQ 없음 세 조건이 들어 있고 production 목적지가 경계 밖 점선 상자로 놓인 구조" caption="브리지가 허용되는 범위" zoom="false" -::: - -세 코드 전부 `MessagingConfigurationException`이고 안정 코드를 갖는다 — `messaging-kafka-share-experimental`이 두 거절에 다른 예외 타입을 쓴 것(그쪽 §17)과 대비된다. `!enabled`도 같은 예외 타입이다. 에러 메시지가 **두 선택지를 명시한다** — "remove it from the binding or move the destination to the native adapter". 무엇을 하라고만 하지 않고 어느 쪽을 포기할지를 준다. - -## DestinationProfile 참조 위치 - -:::evidence key="messaging-spring-cloud-stream-bridge-c03" alt="코드베이스에서 DestinationProfile 를 검색한 출력 8줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DestinationProfile 코드베이스 검색 — 8줄 · exit 0" zoom="true" -::: - -## 네 번째 게이트 - -**production 목적지는 무조건 거절한다.** guard의 세 조건을 통과한 목적지(순서 없음·재시도 없음·DLQ 없음)라도 production이면 막는다. 바인딩 이름 패턴 `[a-zA-Z][a-zA-Z0-9-]{0,63}` — 언더스코어와 점을 배제한다. - -## gaps()가 플래그가 아니라 문장을 만드는 이유 - -**"nothing at runtime will show it"**이 이 record가 존재하는 이유다. 네 boolean과 두 factory: `gaps()`가 각 `false`마다 **문장 하나**를 만든다. **각 문장이 결과까지 적는다** — "indistinguishable", "loses the message". 상태 플래그가 아니라 운영자가 읽는 진술이다. `isFullyGuaranteed()`가 `gaps().isEmpty()`다 — 매 호출마다 네 문장을 다시 만든다. 성능 문제는 아니지만 순수 조회가 문자열을 할당한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-shared-contract-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-shared-contract-c02.md deleted file mode 100644 index 6054f74..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/admission-budget-and-backpressure/concept/concept-shared-contract-c02.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: shared-contract-c02 -title: Permission의 정규화는 문법 제한이 아니다 -topic: admission-budget-and-backpressure -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:shared-contract-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: shared-contract-c02 - file: ../../../final/evidence/rendered/shared-contract-c02.svg -evidence: - - ../../../final/evidence/raw/shared-contract-c02.txt -source: - - 원본 분석 절은 final/document.md#a02#L69 이다. -module: shared-contract ---- - -# Permission의 정규화는 문법 제한이 아니다 - -`Permission`은 정확히 한 개의 colon으로 `resource:action`을 분리하고 trim/lowercase normalization을 수행한다. 그러나 component 내부 character set은 제한하지 않는다. - -## 본문 - - - -`Permission`은 정확히 한 개의 colon으로 `resource:action`을 분리하고 trim/lowercase normalization을 수행한다. 테스트는 mixed case, surrounding whitespace, blank component, 0/2+ colon을 검증한다. - -## Permission 참조 위치 - -:::evidence key="shared-contract-c02" alt="코드베이스에서 Permission 를 검색한 출력 21줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="Permission 코드베이스 검색 — 21줄 · exit 0" zoom="true" -::: - -## 정규화와 문법의 차이 - -source는 component 내부 character set을 제한하지 않는다. 즉 "lowercase colon-delimited"는 normalization 결과이지 `[a-z0-9-]+` 같은 strict grammar는 아니다. 현재 test 역시 이를 요구하지 않으므로 observed contract로만 기록한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/case/case-a06-f018-changestreams-false.md b/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/case/case-a06-f018-changestreams-false.md deleted file mode 100644 index 01cdc67..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/case/case-a06-f018-changestreams-false.md +++ /dev/null @@ -1,204 +0,0 @@ ---- -kind: CASE -slug: a06-f018-changestreams-false -title: 플래그는 고정 거짓이라 능력 검사를 끄지만, 조립 조건이 아니라서 소비자 빈은 그대로 생성된다 -topic: assembly-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a06-f018-changestreams-false -evidenceCapturedOn: 2026-09-02 -body: case-a06-f018-changestreams-false.body.md -assets: - - key: a06-f018-changestreams-false - file: ../../../final/evidence/rendered/a06-f018-changestreams-false.svg - - key: a06-f018-changestreams-false-wiring - file: ../../../final/evidence/rendered/a06-f018-changestreams-false-wiring.svg -evidence: - - ../../../final/evidence/raw/a06-f018-changestreams-false.txt - - ../../../final/evidence/raw/a06-f018-changestreams-false-wiring.txt -source: - - 원본 분석 절은 final/document.md#a06#L1069 이다. 등급은 P2 다. 주석의 전제가 더 이상 사실이 아니라는 판정, 형제 불리언과의 대비표, 검증기 분기가 도달 불가라는 사실, 그리고 실패가 장애 조치 런북으로 분류된다는 서술이 그 절에 있다. - - 같은 문서 `#L1015` 는 소비자가 도는 조건을 포크가 다섯을 공급하는 경우로 한정한다. 이 저장소 안에서는 그 다섯의 구현이 전부 시험 픽스처다. - - 같은 문서 `#L156` 은 같은 코드를 P3 으로 판정하면서 변경 스트림 실행체가 애초에 출하되지 않는다는 것을 근거로 든다. 이 리비전에서 그 근거가 성립하지 않으므로 그 절에 붙은 "현재 잘못된 동작을 만들지는 않는다"는 유지될 수 없고, 실질 등급은 이 절의 P2 로 흡수된다. - - 설정 빈이 속성을 받고도 거짓을 보고한다는 것, 그 두 경우의 빈 집합이 같다는 것, 검사 빈 자체에 조건이 있다는 것, 그리고 런북 전체에 단독 서버·오플로그·해당 오류 코드가 없다는 것은 이 기록에서 확인했다. ---- - -# 플래그는 고정 거짓이라 능력 검사를 끄지만, 조립 조건이 아니라서 소비자 빈은 그대로 생성된다 - -설정 주석은 드라이버 쪽 구현이 출하되지 않아 빈이 0 이라는 것을 근거로 플래그를 고정 거짓으로 만든다. 이 리비전에서 그 구현에는 빈 선언이 있고, 배포가 다섯을 공급하면 소비자도 선다. 조립 조건 어디에도 그 플래그는 없다. - -## 관계 - -- **거부라고 적힌 처리가 폐기이고, 그 값을 읽는 시작 검사는 켤 방법이 없다** - 같은 플래그를 다른 절에서 다룬 기록이다. 거부와 폐기의 차이는 그 기록이 다룬다. -- **하위 시스템 전체가 미배선인데 그것을 켜는 플래그는 시작 검사를 수행한다** - 형제 불리언 쪽 기록이다. 트랜잭션 계수와 그 결과는 그 기록이 다룬다. -- **@Bean이 있다는 것은 조립 증거가 아니다** - 플래그와 조립의 관계를 확인하는 규칙이다. - -## 문제 - -시작 검증기에는 인접한 두 분기가 있다. 하나는 트랜잭션이 켜져 있는데 토폴로지가 지원하지 않으면 던지고, 다른 하나는 변경 스트림에 대해 같은 일을 한다. 두 좌항은 같은 설정 타입의 형제 불리언이고 자동 구성의 인접한 두 줄이 넘긴다. - -두 불리언은 같은 검증기에서 서로 다르게 끝난다. 앞의 값은 배포가 넣은 대로 도착하고, 뒤의 값은 컴팩트 생성자가 이미 거짓으로 바꾼 뒤다. - -## 결론 - -플래그가 고정 거짓이므로 검증기의 변경 스트림 분기는 실행되지 않는다. 조립은 그와 무관하게 진행된다. 소비자 빈의 조건은 배포가 공급해야 하는 타입 다섯이고, 그 목록에 이 플래그는 없다. 자동 구성 파일 전체에서 그 이름이 나오는 줄은 검증기 인자 하나뿐이다. - -전체 자동 구성을 올린 스프링 컨텍스트로 확인했다. 설정 빈은 change-streams=true 를 받고도 거짓을 보고하고, 그 두 경우의 빈 집합이 같다. 모듈 opt-in 을 켜고 리액티브 템플릿이 있으면 드라이버 쪽 구현 빈은 만들어지고 소비자 빈은 만들어지지 않는다. 다섯을 함께 넣으면 소비자 빈도 만들어진다. opt-in 을 켜지 않으면 셋 다 없다. - -주석은 이 코드가 있기 전 상태를 서술한다. 빈이 0 이고 스레드가 0 이라는 근거는 이 리비전에서 성립하지 않는다. - -남는 것은 능력 검사만 꺼진 상태다. 다만 그 검사가 열리는 조건이 따로 있다. 검사 빈은 토폴로지 프로브를 조건으로 걸고, 보안 프로파일과 관리 자격 참조와 스키마 버전 범위 중 하나라도 없으면 부분 검증 대신 예외로 닫는다. 그리고 검증기는 선언 토폴로지와 실제를 능력 검사보다 먼저 대조한다. 변경 스트림 분기는 그 둘을 통과한 배포에서만 차례를 얻는데, 그 차례가 와도 좌항이 거짓이다. - -그 다음 실패는 커서를 여는 시점의 드라이버 오류다. 복구 정책은 서버 코드가 이력 소실이 아니고 재개 가능 라벨도 아니면 실패로 확정하며 장애 조치 런북을 붙인다. 런북 어디를 봐도 그 세 낱말이 없다. 이 연쇄는 형제 기록이 오플로그 없는 서버에서 실행으로 확인했다. - -수정은 셋 중 하나다. 플래그를 되살려 조립 조건으로 쓰거나, 소비자 빈이 설 때 능력을 기동에서 확인하거나, 최소한 주석을 현재 사실로 고치는 것이다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 전체 자동 구성을 올린 스프링 컨텍스트에서 빈 집합과 바인딩 값 비교, 코드베이스 정적 검색 -소스 수정 : x - -## 재현 조건 - -1. 시작 검증기의 인접한 두 분기와 그 좌항을 넘기는 두 줄을 읽는다. -2. 그 검사를 만드는 빈의 조건과 입력이 빠졌을 때의 처리를 읽는다. -3. 컴팩트 생성자의 플래그 강제와 그 주석을 읽는다. -4. 소비자 빈에 붙은 조건과 자동 구성 파일에서 그 플래그가 나오는 줄 수를 확인한다. -5. MongoPlatformAutoConfiguration 전체와 블로킹·리액티브 템플릿 빈을 등록한 컨텍스트를 ca-skeleton.persistence-mongo.enabled=true 로 띄우고 두 빈과 설정 빈의 유무, 그리고 바인딩된 플래그 값을 읽는다. -6. 조건 다섯을 함께 넣고 같은 것을 읽는다. -7. 두 경우를 change-streams=true 를 넣은 상태에서 반복한다. -8. opt-in 을 켜지 않은 경우와 리액티브 템플릿이 없는 경우를 각각 읽는다. -9. 복구 정책의 분류와 그것이 붙이는 런북 전체를 읽는다. - -## 본문 - - - -시작 검증기에는 능력을 요구하는 분기가 둘 있고, 두 좌항은 같은 설정 타입의 형제 불리언이다. - -## 두 분기가 읽는 값이 오는 자리 - -:::evidence key="a06-f018-changestreams-false" alt="시작 검증기의 인접한 두 능력 분기와 그 좌항을 넘기는 자동 구성의 두 줄, 그 검사를 만드는 빈의 조건과 입력이 빠졌을 때 닫는 처리, 컴팩트 생성자의 플래그 강제와 그 주석, 자동 구성 파일에서 그 플래그가 나오는 줄 수, 소비자 빈에 붙은 조건 전체, 복구 정책의 분류 메서드, 그리고 그것이 붙이는 런북의 증상 절 전체와 그 런북에서 단독 서버·오플로그·해당 오류 코드가 나오는 줄 수를 출력한 터미널 기록." caption="두 분기의 좌항은 형제 불리언이고 인접한 두 줄이 넘김 · 검사 빈은 토폴로지 프로브 조건이고 입력이 빠지면 닫음 · 변경 스트림은 생성자에서 고정 거짓이고 자동 구성 파일에 그 이름이 나오는 줄은 1 · 소비자 조건은 @ConditionalOnMissingBean 과 타입 다섯 · 실패는 FAILED 와 장애 조치 런북 · 그 런북에 단독 서버·오플로그·40573 은 0줄 — 91줄 · exit 0" zoom="true" -::: - -```java -if (transactionsEnabled && !capabilities.isStable(MongoCapability.TRANSACTION)) { -... -if (changeStreamsEnabled && !capabilities.isStable(MongoCapability.CHANGE_STREAM)) { -``` - -자동 구성의 인접한 두 줄이 그 좌항을 넘긴다. - -```java -properties.transactions(), -properties.changeStreams(), -``` - -앞의 값은 배포가 넣은 대로 도착한다. 그 값에서 무슨 일이 벌어지는지는 형제 기록이 다룬다. 여기서는 뒤의 값이 이미 거짓이라는 것과, 그런데도 조립은 진행된다는 것만 본다. - -## 조립 조건 - -주석이 근거로 든 것은 빈이 0 이라는 사실이다. - -```java -// Experimental, and therefore not a switch (MNG-INT-003). The driver-side source — watch, -// resumeAfter/startAfter, cursor lifetime, reconnection — is not shipped; what exists is policy -// and value objects that do not add up to a running consumer. Accepting the flag and ignoring … -... -changeStreams = false; -``` - -소비자 빈에 붙은 조건은 `@ConditionalOnMissingBean` 과 타입 다섯의 `@ConditionalOnBean` 이다. - -```java -@org.springframework.boot.autoconfigure.condition.ConditionalOnBean({ - dev.caskeleton.adapter.outbound.mongo.changestream.MongoChangeStreamSubscription.class, - dev.caskeleton.adapter.outbound.mongo.changestream.MongoResumeCheckpointStore.class, - dev.caskeleton.adapter.outbound.mongo.changestream.MongoResumeTokenCodec.class, - dev.caskeleton.adapter.outbound.mongo.changestream.projector.MongoChangeProjector.class, - dev.caskeleton.adapter.outbound.mongo.changestream.projector.MongoChangeDeduplicationStore - .class -}) -``` - -다섯 다 배포가 공급해야 하는 타입이다. 이 플래그는 목록에 없고, 자동 구성 파일 전체에서 그 이름이 나오는 줄은 검증기 인자 하나뿐이다. - -## 컨텍스트를 띄운 결과 - -:::evidence key="a06-f018-changestreams-false-wiring" alt="전체 자동 구성을 등록한 스프링 컨텍스트를 모듈 opt-in 없이, opt-in 과 리액티브 템플릿만으로, opt-in 과 배포가 공급해야 하는 다섯을 함께, 그리고 opt-in 과 리액티브 템플릿 없이 각각 띄워 드라이버 쪽 구현 빈과 소비자 빈과 설정 빈의 유무, 그리고 바인딩된 플래그 값을 change-streams 를 넣지 않은 경우와 넣은 경우에 대해 읽은 터미널 기록." caption="opt-in 없으면 셋 다 없음 · opt-in 과 템플릿이면 드라이버 쪽만 섬 · 다섯을 넣으면 소비자도 섬 · 설정 빈은 change-streams=true 를 받고도 changeStreams()=false · 두 경우의 빈 집합이 같음 — 13줄 · exit 0" zoom="true" -::: - -`MongoPlatformAutoConfiguration` 전체를 등록하고 블로킹·리액티브 템플릿을 넣어 띄웠다. - -```text -[opt-in, 리액티브 템플릿만] - 기본 드라이버쪽=있음 소비자=없음 설정빈=있음 changeStreams()=false - change-streams=true 드라이버쪽=있음 소비자=없음 설정빈=있음 changeStreams()=false - -[opt-in, 배포가 공급해야 하는 다섯을 함께] - 기본 드라이버쪽=있음 소비자=있음 설정빈=있음 changeStreams()=false - change-streams=true 드라이버쪽=있음 소비자=있음 설정빈=있음 changeStreams()=false -``` - -설정 빈은 컨텍스트에 있고 속성을 받는다. 받고도 거짓을 보고하므로 조립 조건에 닿기 전에 이미 값이 정해져 있다. 소비자가 서는 조건은 다섯을 공급했는지 하나다. - -모듈 opt-in 이 없으면 세 빈이 모두 만들어지지 않고, opt-in 이 있어도 리액티브 템플릿이 없으면 두 빈이 만들어지지 않는다. - -```text -[모듈 opt-in 없이] - 기본 드라이버쪽=없음 소비자=없음 설정빈=없음 - -[opt-in, 리액티브 템플릿 없이] - 기본 드라이버쪽=없음 소비자=없음 설정빈=있음 changeStreams()=false -``` - -## 능력 검사가 열리는 조건 - -검사가 꺼진 것과 검사가 애초에 만들어지지 않는 것은 다르다. 검사 빈부터 조건이 있다. - -```java -@Bean -@ConditionalOnBean(MongoTopologyProbe.class) -public InitializingBean mongoPlatformStartupCheck( -... - if (security == null || admin == null || versions == null) { - // Fail closed rather than validate a subset. A partial startup check reports success for - // the parts nobody supplied, which is the shape the missing wiring already had. -``` - -토폴로지 프로브가 있어야 만들어지고, 보안 프로파일과 관리 자격 참조와 스키마 버전 범위가 다 있어야 돈다. 그리고 검증기는 능력 검사보다 먼저 선언 토폴로지와 실제를 대조한다. 그 둘을 통과한 배포에서만 변경 스트림 분기가 자기 차례를 얻고, 그 차례에서 좌항이 거짓이다. - -## 그 다음 실패가 가는 곳 - -```java -if (failure.hasLabel("ResumableChangeStreamError")) { - return MongoChangeStreamRecoveryDecision.resume(); -} -return MongoChangeStreamRecoveryDecision.halt(MongoChangeStreamState.FAILED, FAILURE_RUNBOOK); -... -private static boolean isHistoryLost(int serverCode) { - return serverCode == 286 || serverCode == 280; -``` - -토폴로지가 복제 셋이 아니라는 오류는 286 도 280 도 아니고 재개 가능 라벨도 없으므로 셋째 갈래다. 붙는 런북의 증상 절은 네 항목이고 전부 프라이머리 선출과 서버 선택 지연이다. - -```text -- `MongoServerSelectionException` / `MongoConnectionException` spike, then recovery within seconds. -- `MongoSdamObservationListener` reports a topology change (primary removed, new primary elected). -- `MongoPoolObservationListener` shows checkout wait times rising while server-side command duration - stays flat — the wait is topology, not query cost. -- Latency spike on writes with no corresponding rise in read latency. -``` - -증상 절뿐 아니라 그 런북 전체에서 단독 서버도 오플로그도 해당 오류 코드도 나오지 않는다. 이 연쇄를 실제 서버에서 이은 것은 형제 기록이다. - -## 확인하지 못한 것 - -다섯을 공급한 포크의 배포를 오플로그 없는 토폴로지에 올려 기동 통과와 커서 열기 실패를 이어서 재현하지는 않았다. 그 연쇄는 형제 기록이 단독 서버에서 실행으로 확인했다. 여기서는 조립 조건과 검사가 열리는 조건까지 확인했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/case/case-autoconfiguration-in-name-only.md b/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/case/case-autoconfiguration-in-name-only.md deleted file mode 100644 index 4cc0606..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/case/case-autoconfiguration-in-name-only.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -kind: CASE -slug: autoconfiguration-in-name-only -title: 이름만 AutoConfiguration이던 세 클래스가 capability 리포트에 Stable로 올라 있었다 -topic: assembly-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:autoconfiguration-in-name-only -evidenceCapturedOn: 2026-09-01 -assets: - - key: autoconfiguration-in-name-only - file: ../../../final/evidence/rendered/autoconfiguration-in-name-only.svg -evidence: - - ../../../final/evidence/raw/autoconfiguration-in-name-only.txt -source: - - 원본 분석 절은 final/document.md#a05 §14.4 이다. ---- - -# 이름만 AutoConfiguration이던 세 클래스가 capability 리포트에 Stable로 올라 있었다 - -이름이 AutoConfiguration 으로 끝나는 세 클래스가 실제로는 평범한 팩토리였다. 컴포지션 루트는 그 패키지를 스캔에서 뺐고, 자동설정으로 등록되지도 않았다. 능력 리포트는 세 능력을 Stable 로 보고했고 실행 컨텍스트에는 그중 아무것도 없었다. - -## 관계 - -- **@Bean이 있다는 것은 조립 증거가 아니다** - 이름과 위치가 조립을 보장하지 않는다는 사례다. -- **조건부 빈의 평가 시점 — 파싱 시점과 등록 시점** - 같은 클래스에서 이어진 두 번째 결함이 그 개념을 설명한다. -- **시작 검증기가 도는지는 그 능력에 자동설정 루트가 있는지와 일치한다** - 같은 형태의 확인 절차다. - -## 문제 - -세 클래스가 이름을 AutoConfiguration 으로 끝냈다. 그러나 셋 다 다음을 갖고 있지 않았다. - -@AutoConfiguration 애너테이션 -@Bean 메서드 -AutoConfiguration.imports 항목 - -동시에 컴포지션 루트는 이 패키지를 컴포넌트 스캔에서 의도적으로 제외한다. 자동설정이 이 패키지에 들어가는 유일한 경로이기 때문이다. - -세 조건이 겹치면 결과는 하나다. 아무도 이 클래스들을 등록하지 않는다. - -## 결론 - -능력 리포트는 트랜잭션 재시도와 완료 증거와 관측성을 Stable 로 올려 두었고, 실행 컨텍스트에는 그중 아무것도 없었다. - -이 격차의 위험은 리포트를 읽는 사람에게 있다. 재시도에 의존하는 코드를 배포할 수 있고, 그 재시도는 한 번도 일어나지 않는다. 리포트가 그것을 Stable 이라고 말했기 때문이다. - -수정은 등록을 추가하는 것이었다. 팩토리는 그대로 남았다. 팩토리가 조립 결정을 담고 있고, 자기 컴포지션 루트를 직접 배선하는 애플리케이션은 여전히 그것을 직접 호출할 수 있기 때문이다. 달라진 것은 기본 애플리케이션이 이제 빈을 받는다는 점이다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Gradle : 9.0.0 -Spring Boot : 4.0.8 -근거 : 저장소의 javadoc 이 사후 기록으로 남긴 회귀 -소스 수정 : x - -## 재현 조건 - -수정된 형태를 확인하는 절차다. - -1. JpaPlatformRuntimeAutoConfiguration 의 클래스 javadoc 두 번째 문단을 읽는다. 세 클래스가 무엇을 갖고 있지 않았는지 열거되어 있다. -2. CaSkeletonApplication 의 AUTO_CONFIGURED_PACKAGES 에서 이 패키지가 제외되는지 확인한다. -3. 현재 클래스에 Configuration 애너테이션과 조건들이 붙어 있고 실제 @Bean 을 갖는지 확인한다. - -## 본문 - - - -세 클래스가 `...AutoConfiguration`으로 이름 붙었고 plain factory였다 — `@AutoConfiguration`도, `@Bean`도, `.imports` 엔트리도 없었고 합성 루트는 그 패키지를 스캔에서 제외한다. - -## 세 클래스가 갖지 않은 것 - -:::evidence key="autoconfiguration-in-name-only" alt="분석 문서 final/document.md#a05 에서 이 기록의 근거 절을 그대로 잘라낸 16줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a05 발췌 — 16줄" zoom="true" -::: - -## 리포트와 컨텍스트가 어긋났다 - -capability 리포트는 transaction retry·completion evidence·observability를 Stable로 나열했고 **돌고 있는 컨텍스트에는 그중 아무것도 없었다.** 개발자가 재시도되지 않는 재시도에 의존하는 코드를 배포할 수 있었다. - -## 확인하지 못한 것 - -당시 능력 리포트의 출력을 직접 보지 않았다. 이 기록은 저장소가 javadoc 에 남긴 사후 기록에 근거한다. - -없음 — 수정 후 형태를 코드로 확인했다 - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/case/case-conditionalonbean-evaluated-at-parse-time.md b/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/case/case-conditionalonbean-evaluated-at-parse-time.md deleted file mode 100644 index d53282f..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/case/case-conditionalonbean-evaluated-at-parse-time.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -kind: CASE -slug: conditionalonbean-evaluated-at-parse-time -title: '@ConditionalOnBean(DataSource.class)가 클래스 파싱 시점에 평가되어 여덟 빈이 사라졌다' -topic: assembly-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:conditionalonbean-evaluated-at-parse-time -evidenceCapturedOn: 2026-09-01 -assets: - - key: conditionalonbean-evaluated-at-parse-time - file: ../../../final/evidence/rendered/conditionalonbean-evaluated-at-parse-time.svg -evidence: - - ../../../final/evidence/raw/conditionalonbean-evaluated-at-parse-time.txt -source: - - 원본 분석 절은 final/document.md#a05 §14.4 이다. ---- - -# @ConditionalOnBean(DataSource.class)가 클래스 파싱 시점에 평가되어 여덟 빈이 사라졌다 - -@Import 로 들어오는 설정 클래스에 붙은 @ConditionalOnBean 이 데이터소스 빈 정의가 생기기 전에 평가되어 항상 거짓이었다. 여덟 빈이 조용히 사라졌고, 아무것도 그것을 보고하지 않았다. - -## 관계 - -- **조건부 빈의 평가 시점 — 파싱 시점과 등록 시점** - 이 사례가 설명하는 메커니즘이다. -- **ConditionalOnBean 사슬의 실제 평가 순서를 확인하지 않았다** - 이 함정이 현재 리비전에도 남아 있는지에 대한 미해결 질문이다. -- **이름만 AutoConfiguration이던 세 클래스가 capability 리포트에 Stable로 올라 있었다** - 같은 클래스에서 앞서 일어난 결함이다. - -## 문제 - -이 클래스는 예전에 @ConditionalOnBean(DataSource.class) 를 갖고 있었다. - -문제는 이 클래스가 자동설정으로 등록되는 것이 아니라 PersistenceJpaRootAutoConfiguration 이 @Import 로 끌어온다는 점이다. 그래서 조건이 클래스 파싱 시점에 평가된다. 데이터소스 빈 정의가 아직 존재하지 않는 시점이다. - -따라서 조건은 실제 배포 전부에서 거짓이었다. - -## 결론 - -여덟 빈이 조용히 사라졌다. - -아무것도 그것을 보고하지 않았다. 그 여덟에 의존하는 것이 없었기 때문이다. 결함이 드러난 것은 데이터소스 검증기가 마침내 호출자에 연결되고 JPA Compose 레인이 적격 빈 없음이라고 답했을 때다. - -수정은 조건의 순서를 바꾸는 것이 아니라 조건을 제거하는 것이었다. 근거는 이렇다. 이 클래스는 JPA 루트를 통해서만 도달하고 그 루트가 이미 마스터 스위치를 갖고 있으므로, 파싱 시점에는 데이터소스가 있느냐는 질문에 이미 예라고 답한 상태다. 데이터소스가 필요한 빈은 그것을 파라미터로 받고, 스위치가 켜진 채 데이터소스가 없으면 시끄러운 실패가 된다. 계층이 사라지는 것보다 그쪽이 원하던 결과다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Gradle : 9.0.0 -Spring Boot : 4.0.8 -근거 : 저장소의 javadoc 이 사후 기록으로 남긴 회귀 -소스 수정 : x - -## 재현 조건 - -수정된 형태를 확인하는 절차다. - -1. JpaPlatformRuntimeAutoConfiguration 의 클래스 javadoc 다섯 번째와 여섯 번째 문단을 읽는다. -2. 현재 클래스 애너테이션에 ConditionalOnBean 이 없고 ConditionalOnClass 와 ConditionalOnProperty 만 있는지 확인한다. -3. PersistenceJpaRootAutoConfiguration 이 이 클래스를 Import 하는지 확인한다. - -## 본문 - - - -이 클래스는 루트가 **import**하지 auto-configure하지 않으므로, 그 조건이 클래스 파싱 중 — datasource 빈 정의가 존재하기 전에 — 평가됐고 따라서 **모든 실제 배포에서 false**였다. - -## 조건이 평가된 시점 - -:::evidence key="conditionalonbean-evaluated-at-parse-time" alt="분석 문서 final/document.md#a05 에서 이 기록의 근거 절을 그대로 잘라낸 16줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a05 발췌 — 16줄" zoom="true" -::: - -## 여덟 빈이 조용히 사라졌다 - -아무것도 그중 어느 것에도 의존하지 않아 아무것도 보고하지 않았다. - -## 드러난 시점 - -datasource validator가 caller에 배선되고 Compose 레인이 "No qualifying bean"이라고 답했을 때다. 같은 함정을 피하려고 루트의 검사가 validator를 주입받지 않고 직접 생성한다. - -## 확인하지 못한 것 - -현재 리비전의 다른 조건부 빈들이 각각 어느 시점에 평가되는지 런타임에서 확인하지 않았다. debug 부팅의 조건 평가 리포트가 그것을 답한다. - -현재 리비전에서 재발하지 않는지 ConditionEvaluationReport로 확인하지 않았다 - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/case/case-narrowing-the-scan-orphaned-eight-components.md b/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/case/case-narrowing-the-scan-orphaned-eight-components.md deleted file mode 100644 index df3b92a..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/case/case-narrowing-the-scan-orphaned-eight-components.md +++ /dev/null @@ -1,104 +0,0 @@ ---- -kind: CASE -slug: narrowing-the-scan-orphaned-eight-components -title: 넓은 스캔을 좁히자 여덟 컴포넌트에 아무것도 도달하지 않았다 -topic: assembly-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:narrowing-the-scan-orphaned-eight-components -evidenceCapturedOn: 2026-09-01 -assets: - - key: narrowing-the-scan-orphaned-eight-components - file: ../../../final/evidence/rendered/narrowing-the-scan-orphaned-eight-components.svg -evidence: - - ../../../final/evidence/raw/narrowing-the-scan-orphaned-eight-components.txt -source: - - 원본 분석 절은 final/document.md#a05 §14.2 이다. ---- - -# 넓은 스캔을 좁히자 여덟 컴포넌트에 아무것도 도달하지 않았다 - -컴포지션 루트가 퍼시스턴스 패키지를 스캔에서 뺐다. 그 제외는 옳았지만 나머지 절반이 빠져 있었다. 스캔 컴포넌트로 작성된 여덟 클래스에 아무도 도달하지 않았고, 그중 하나는 트랜잭션 포트의 유일한 구현이었다. - -## 관계 - -- **스캔에서 뺀 다섯 패키지의 컴포넌트 여섯을 두 자동설정 어느 쪽도 소유하지 않았다** - 같은 형태가 웹 리프에서 반복된 사례다. -- **꺼짐은 조건의 반복이 아니라 구조여야 한다** - 스캔 제외가 그 규칙을 따른 조치라는 점이 이 사례의 전제다. -- **@Bean이 있다는 것은 조립 증거가 아니다** - 스테레오타입이 붙어 있다는 것도 조립 증거가 아니다. - -## 문제 - -컴포지션 루트의 컴포넌트 스캔은 퍼시스턴스 어댑터 패키지 전체를 정규식으로 제외한다. javadoc 은 그 제외가 옳다고 명시한다. 선택적 능력을 선택적으로 만드는 것이 그 제외이며, JPA 가 꺼진 배포는 퍼시스턴스 빈을 조립하지 않는다. - -빠진 것은 나머지 절반이다. 이 리프의 여덟 클래스가 스캔 컴포넌트로 작성되어 있었다. - -SpringTransactionPort -PersistenceExceptionTranslator -StandardSqlStateErrorMapping -DomainContextAuditContextPort -멱등성 저장소와 그 리퍼 -outbox 저장소와 그 리퍼 - -넓은 스캔이 이들에게 닿지 않게 되자 다른 어떤 것도 닿지 않았다. @Component 와 @Repository 가 붙어 있었지만 실행 중인 어떤 애플리케이션에서도 빈이 아니었다. - -특히 TransactionPort 는 구현이 아예 없는 상태가 됐다. 트랜잭션을 여는 모든 유스케이스가 그것을 열 포트를 갖지 못했다. - -## 결론 - -단위 테스트로는 보이지 않았다. 이 클래스들은 각자의 테스트에서 직접 생성되기 때문이다. - -드러난 것은 트랜잭션이 필요한 능력이 실제로 조립됐을 때다. 알림 오케스트레이터가 local-notification-ingest 레인에서 미충족 의존성으로 실패했다. - -수정은 스캔을 복원하되 원래 덮었어야 할 패키지로 좁히고, PersistenceJpaRootAutoConfiguration 을 통해서만 도달하게 만드는 것이었다. 그 루트가 JPA 마스터 스위치를 갖는다. 꺼짐은 여전히 구조적이다. - -두 패키지는 의도적으로 빠져 있다. fileserver 는 자기 능력 스위치로 게이트되고 자기 설정 클래스가 스캔한다. notification 은 전용 파사드가 빈 단위로 명시적으로 조립한다. - -이 패키지들 아래 컴포넌트는 각자의 ConditionalOnProperty 가드를 유지한다. 스캔 대상이 된다는 것은 후보가 된다는 뜻이지 무조건 빈이 된다는 뜻이 아니다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Gradle : 9.0.0 -Spring Boot : 4.0.8 -근거 : 저장소의 javadoc 이 사후 기록으로 남긴 회귀 -소스 수정 : x - -## 재현 조건 - -수정된 형태를 확인하는 절차다. - -1. JpaAdapterComponentsConfig 의 클래스 javadoc 을 읽는다. 여덟 클래스가 이름으로 열거되어 있다. -2. CaSkeletonApplication 의 제외 정규식에 퍼시스턴스 패키지가 있는지 확인한다. -3. 이 설정 클래스가 PersistenceJpaRootAutoConfiguration 을 통해서만 도달하는지 확인한다. -4. 의도적으로 빠진 두 패키지의 대체 조립 경로를 확인한다. - -## 본문 - - - -합성 루트의 스캔이 persistence 트리를 정규식으로 제외했고 **그 제외는 옳다** — 그것이 optional capability를 optional하게 만든다. 빠진 것은 나머지 절반이다. - -## SpringTransactionPort 참조 위치 - -:::evidence key="narrowing-the-scan-orphaned-eight-components" alt="코드베이스에서 SpringTransactionPort 를 검색한 출력 11줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="SpringTransactionPort 코드베이스 검색 — 11줄 · exit 0" zoom="true" -::: - -## 여덟 클래스에 아무것도 도달하지 않았다 - -`SpringTransactionPort`·`PersistenceExceptionTranslator`·`StandardSqlStateErrorMapping`·`DomainContextAuditContextPort`·idempotency store와 reaper·outbox store와 reaper가 scanned component로 쓰여 있는데, 넓은 스캔이 멈추자 아무것도 도달하지 않았다. - -## 특히 TransactionPort 는 구현이 전혀 없었다 - -트랜잭션을 여는 모든 유스케이스가 열 포트를 갖지 못했고, 단위 테스트는 각 클래스를 직접 생성하므로 볼 수 있는 것이 없었다. - -## 확인하지 못한 것 - -당시 실패했던 local-notification-ingest 레인을 이 리비전에서 재실행하지 않았다. - -없음 — 수정된 @ComponentScan 대상 6개를 코드로 확인했다 - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/concept/concept-when-conditions-are-evaluated.md b/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/concept/concept-when-conditions-are-evaluated.md deleted file mode 100644 index d1617aa..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/concept/concept-when-conditions-are-evaluated.md +++ /dev/null @@ -1,114 +0,0 @@ ---- -kind: CONCEPT -slug: when-conditions-are-evaluated -title: 조건부 빈의 평가 시점 — 파싱 시점과 등록 시점 -topic: assembly-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:when-conditions-are-evaluated -evidenceCapturedOn: 2026-09-01 -assets: - - key: when-conditions-are-evaluated - file: ../../../final/evidence/rendered/when-conditions-are-evaluated.svg -evidence: - - ../../../final/evidence/raw/when-conditions-are-evaluated.txt -source: - - 원본 분석 절은 final/document.md#a05 §14.4 이다. ---- - -# 조건부 빈의 평가 시점 — 파싱 시점과 등록 시점 - -같은 `@ConditionalOnBean`이라도 그 클래스가 자동설정으로 등록되는지 `@Import`로 들어오는지에 따라 평가 시점이 다르다. 그 차이가 조건을 항상 거짓으로 만들 수 있다. - -## 관계 - -- **ConditionalOnBean(DataSource)이 클래스 파싱 시점에 평가되어 여덟 빈이 사라졌다** - 이 개념이 실제로 문제가 된 사례다. -- **ConditionalOnBean은 조건이 만족될 수 있는지까지 확인해야 한다** - 이 개념에서 나온 확인 규칙이다. -- **ConditionalOnBean 사슬의 실제 평가 순서를 확인하지 않았다** - 현재 리비전에 대한 미해결 질문이다. - -## 본문 - - - -`@ConditionalOnBean`은 그 클래스가 **언제 평가되는가**에 따라 답이 달라진다. `@AutoConfiguration`으로 등록되면 다른 자동설정 이후에 평가되지만, plain `@Configuration`이 `@Import`로 들어오면 **클래스가 파싱되는 동안 — 대상 빈 정의가 존재하기 전에** 평가된다. - -## 등록 방식이 평가 시점을 정한다 - -:::evidence key="when-conditions-are-evaluated" alt="분석 문서 final/document.md#a05 에서 이 기록의 근거 절을 그대로 잘라낸 16줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a05 발췌 — 16줄" zoom="true" -::: - -## 이 저장소가 그 함정을 밟았다 - -여덟 빈이 조용히 사라졌다. - -## 남은 회피 방법 둘 - -검증기를 주입받지 않고 직접 생성하기, 그리고 조건을 루트로 올리기. - -:::note - -현재 리비전의 각 조건부 빈이 어느 시점에 평가되는지는 ConditionEvaluationReport로 확인하지 않았다 - -::: - -## 두 시점 - -`@ConditionalOnBean`은 "이 타입의 빈 정의가 이미 등록되어 있는가"를 묻는다. 그 질문의 답은 언제 묻느냐에 달라진다. - -| 클래스가 들어오는 경로 | 조건이 평가되는 시점 | -|---|---| -| 자동설정 (`.imports`) | 자동설정 순서에 따라 등록 단계에서 | -| `@Import` | 그것을 import 하는 클래스가 파싱될 때 | -| 컴포넌트 스캔 | 스캔 단계에서 | - -`@Import`로 들어오는 클래스가 문제다. 부모 설정이 파싱되는 시점에 자식 클래스의 클래스 수준 조건이 함께 평가되는데, 그 시점에는 자동설정이 만들 빈 정의가 아직 존재하지 않는다. - -## 이 저장소가 겪은 형태 - -```java -/** - *

This class used to carry {@code @ConditionalOnBean(DataSource.class)}. It is imported by - * {@code PersistenceJpaRootAutoConfiguration} rather than auto-configured, so that condition was - * evaluated while the class was parsed — before the datasource bean definition existed — and was - * therefore false in every real deployment. All eight beans below silently disappeared, and nothing - * reported it because nothing depended on any of them. It surfaced only when the datasource - * validator was finally wired to a caller and the JPA Compose lane answered "No qualifying bean". - */ -``` - -두 문장이 이 개념의 핵심이다. 조건이 모든 실제 배포에서 거짓이었다는 것, 그리고 아무것도 그것을 보고하지 않았다는 것. - -보고되지 않은 이유가 특히 중요하다. 사라진 여덟 빈에 의존하는 것이 없었기 때문이다. 의존이 있었다면 미충족 의존성으로 시끄럽게 실패했을 것이다. - -## 해결 방향은 순서가 아니라 제거였다 - -```java -/** - *

The condition is removed rather than reordered: this class is reached only through the JPA - * root, which already carries the master switch, so "is there a datasource" has been answered yes - * by the time it is parsed. A bean here that needs one takes it as a parameter, and a missing - * datasource with the switch on is then a loud failure — which is the outcome that was wanted, - * rather than the layer vanishing. - */ -``` - -:::tip - -조건을 옮기거나 순서를 바꾸는 대신, 그 조건이 이미 답해진 지점으로 도달 경로를 제한하고 조건 자체를 없앴다. 그리고 필요한 의존은 파라미터로 받게 해서, 없을 때 조용히 사라지는 대신 시끄럽게 실패하게 만들었다. - -::: - -## 확인 방법 - -정적으로는 두 가지를 본다. - -1. 그 클래스가 `.imports`에 있는가, 아니면 다른 클래스가 `@Import` 하는가 -2. 조건이 요구하는 빈을 누가 언제 등록하는가 - -런타임으로는 `debug=true` 부팅의 조건 평가 리포트가 답한다. `Negative matches` 항목의 사유 문자열이 "빈 정의 없음"인지 "타입 자체가 없음"인지를 구별해 준다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/question/openquestion-conditional-evaluation-order-unverified.md b/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/question/openquestion-conditional-evaluation-order-unverified.md index c647d87..5109428 100644 --- a/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/question/openquestion-conditional-evaluation-order-unverified.md +++ b/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/question/openquestion-conditional-evaluation-order-unverified.md @@ -1,7 +1,7 @@ --- kind: QUESTION slug: conditional-evaluation-order-unverified -title: '@ConditionalOnBean 사슬의 실제 평가 순서를 확인하지 않았다' +title: @ConditionalOnBean 사슬의 실제 평가 순서를 확인하지 않았다 topic: assembly-ownership project: clean-architecture-backend-template status: 게시 전 diff --git a/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/reference/reference-a-bean-is-not-composition-evidence.md b/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/reference/reference-a-bean-is-not-composition-evidence.md index d113625..bea811d 100644 --- a/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/reference/reference-a-bean-is-not-composition-evidence.md +++ b/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/reference/reference-a-bean-is-not-composition-evidence.md @@ -1,7 +1,7 @@ --- kind: REFERENCE slug: a-bean-is-not-composition-evidence -title: '@Bean이 있다는 것은 조립 증거가 아니다' +title: @Bean이 있다는 것은 조립 증거가 아니다 topic: assembly-ownership project: clean-architecture-backend-template status: 게시 전 diff --git a/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/reference/reference-conditionalonbean-must-be-satisfiable.md b/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/reference/reference-conditionalonbean-must-be-satisfiable.md index f0311a0..20d4315 100644 --- a/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/reference/reference-conditionalonbean-must-be-satisfiable.md +++ b/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/reference/reference-conditionalonbean-must-be-satisfiable.md @@ -1,7 +1,7 @@ --- kind: REFERENCE slug: conditionalonbean-must-be-satisfiable -title: '@ConditionalOnBean은 조건이 만족될 수 있는지까지 확인해야 한다' +title: @ConditionalOnBean은 조건이 만족될 수 있는지까지 확인해야 한다 topic: assembly-ownership project: clean-architecture-backend-template status: 게시 전 diff --git a/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/reference/reference-count-the-frameworks-own-autoconfigurations.md b/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/reference/reference-count-the-frameworks-own-autoconfigurations.md deleted file mode 100644 index ab6037a..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/reference/reference-count-the-frameworks-own-autoconfigurations.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -kind: REFERENCE -slug: count-the-frameworks-own-autoconfigurations -title: 프레임워크가 기여하는 자동설정까지 세지 않으면 스위치가 아니다 -topic: assembly-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:count-the-frameworks-own-autoconfigurations -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# 프레임워크가 기여하는 자동설정까지 세지 않으면 스위치가 아니다 - -## 목적 - -프로젝트 코드만 게이트하고 프레임워크가 기여하는 자동설정을 남겨 두어, 꺼진 능력이 여전히 자원을 잡는 것을 막는다. - -## 규칙 - -1. 프로젝트 조건은 프레임워크 자동설정을 막지 못한다 - 후보 집합에 들어온 자동설정은 자기 조건으로 판단한다. 프로젝트의 마스터 스위치는 그 판단에 참여하지 않는다. - -2. 후보 집합을 좁히는 필터가 따로 필요하다 - AutoConfigurationImportFilter 는 어떤 프로젝트 조건보다 먼저 동작하므로 이 일을 할 수 있는 유일한 자리다. - -3. 그 필터는 권한이 아니라 도구다 - 후보를 빼는 일과 능력이 켜졌는지 판정하는 일은 다르다. 판정 권한은 루트 하나가 갖는다. - -4. 자원 필요 여부는 능력 질문으로 묻는다 - 커넥션 풀 같은 공유 자원은 한 능력의 사유물이 아니다. 그것을 필요로 하는 능력이 하나라도 활성인지를 묻는다. - -## 적용 조건 - -프레임워크가 같은 기술에 대해 자기 자동설정을 갖는 모든 능력. 데이터소스, 메시징, 캐시가 대표적이다. - -## 예외 - -프레임워크 자동설정이 이미 프로젝트 조건과 같은 속성을 보도록 설계되어 있으면 필터가 필요 없다. - -## 예시 - -JPA 가 꺼진 배포에서 프레임워크의 데이터소스 자동설정을 후보에서 빼는 필터가 spring.factories 에 등록되어 있다. - -풀이 필요한지는 JPA 가 켜졌는지가 아니라 커넥션을 필요로 하는 능력이 하나라도 활성인지로 묻는다. 이 저장소는 여섯 조건의 논리합으로 판정한다. - -## 관계 - -- **풀이 필요한지는 JPA가 켜졌나가 아니라 커넥션이 필요한 capability가 있나로 묻는다** - 이 규칙을 채택한 결정이다. -- **꺼짐은 조건의 반복이 아니라 구조여야 한다** - 같은 목표의 짝 규칙이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/reference/reference-off-must-be-structural.md b/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/reference/reference-off-must-be-structural.md index b631f55..234412f 100644 --- a/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/reference/reference-off-must-be-structural.md +++ b/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/reference/reference-off-must-be-structural.md @@ -1,7 +1,7 @@ --- kind: REFERENCE slug: off-must-be-structural -title: '"꺼짐"은 조건의 반복이 아니라 구조여야 한다' +title: "꺼짐"은 조건의 반복이 아니라 구조여야 한다 topic: assembly-ownership project: clean-architecture-backend-template status: 게시 전 diff --git a/docs/clean-architecture-backend-template/tech-log-studio/bounding-by-type/case/case-a-report-that-cannot-carry-a-datasource.md b/docs/clean-architecture-backend-template/tech-log-studio/bounding-by-type/case/case-a-report-that-cannot-carry-a-datasource.md deleted file mode 100644 index fa47e45..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/bounding-by-type/case/case-a-report-that-cannot-carry-a-datasource.md +++ /dev/null @@ -1,125 +0,0 @@ ---- -kind: CASE -slug: a-report-that-cannot-carry-a-datasource -title: 진단 리포트가 살아 있는 리소스를 담지 않도록 값 타입을 좁혔다 -topic: bounding-by-type -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a-report-that-cannot-carry-a-datasource -evidenceCapturedOn: 2026-09-02 -assets: - - key: a-report-that-cannot-carry-a-datasource - file: ../../../final/evidence/rendered/a-report-that-cannot-carry-a-datasource.svg -evidence: - - ../../../final/evidence/raw/a-report-that-cannot-carry-a-datasource.txt -source: - - 분석 문서는 persistence-jpa 편 §3.2 가 이 레코드의 컴포넌트와 생성자 계약을, §3.3 이 능력 목록에서 엔드포인트까지의 실제 소비자 사슬을 적는다. §3.4 가 "경계가 있는" 이 강제되지 않는다는 항목이고, 제약 경계를 검증하는 테스트가 없다는 것은 §9.3 이다. 축약된 불리언의 이름이 실제보다 넓다는 판정은 §69 다. - - §3.3 의 사슬은 엔드포인트에서 끝나고 노출 목록까지 따라가지 않는다. 그 마지막 칸은 위 터미널 출력에서 확인할 수 있다. ---- - -# 진단 리포트가 살아 있는 리소스를 담지 않도록 값 타입을 좁혔다 - -능력 선언 레코드가 제약을 평범한 문자열로만 담는다. 제공자 객체를 담을 자리를 만들지 않은 것이고, 그 이유는 이 레코드가 액추에이터로 공개될 수 있는 자리에 있기 때문이다. - -## 관계 - -- **관측을 위해 수집한 데이터가 관측 대상보다 위험할 수 있다** - 이 사례가 그 규칙을 타입으로 실현한 형태다. -- **시그니처가 payload를 받지 않는데 예외 메시지로 PII가 로그에 남았다** - 같은 목표를 시그니처로만 지키려다 실패한 반대 사례다. -- **카디널리티 경계를 타입으로 표현하기** - 카디널리티 경계를 타입으로 표현한 사례다. - -## 문제 - -능력 선언 레코드는 어떤 능력이 어떤 지원 등급에서 어떤 제약 아래 지원되는지를 담는다. - -이 레코드는 리포트로 직렬화되어 액추에이터 엔드포인트의 반환값이 된다. 제약을 무엇으로 담을지가 이 레코드의 선택지였다. - -## 결론 - -제약을 평범한 문자열로만 담기로 했고, 그 이유가 javadoc 에 있다. 값 타입이 제공자 객체를 쥐는 순간 살아 있는 리소스가 딸려 들어오고, 접속 문자열과 자격증명이 리포트를 타고 나갈 수 있다는 것이다. - -같은 방향의 결정이 리포트 두 층에서 반복된다. 특권 리포트는 연결된 역할과 검색 경로와 불리언 둘만 담는다. 밖으로 나가는 리포트는 그 특권 리포트조차 담지 않고 불리언 하나로 줄인다. 엔드포인트에는 쓰기 연산도 파라미터도 없다. - -리포트를 공개할 때 무엇을 지울지 정하는 대신, 지울 것이 애초에 들어올 수 없는 타입을 만들었다. - -세 가지가 이 그림에서 어긋난다. 이 엔드포인트는 지금 노출되지 않는다. 축약된 불리언의 이름이 그 식이 계산하는 것보다 넓다. 그리고 javadoc 이 쓴 "경계가 있는" 은 생성자가 강제하지 않는다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 값 타입 정의와 그 javadoc, 리포트 조립과 엔드포인트 선언과 노출 설정 확인 -소스 수정 : x - -## 재현 조건 - -1. 능력 선언 레코드의 컴포넌트와 클래스 javadoc 을 읽고, 컴팩트 생성자가 무엇을 강제하는지 확인한다. -2. 특권 리포트의 컴포넌트와 javadoc 을 읽는다. -3. 밖으로 나가는 리포트가 특권 리포트를 어떤 식으로 줄이는지, 그 식이 무엇을 보는지 확인한다. -4. 역할 이름과 검색 경로를 검사하는 정책의 프로덕션 호출자를 센다. -5. 엔드포인트의 애너테이션과, 저장소에서 그 엔드포인트 아이디가 나오는 곳과 출하 설정의 노출 목록을 확인한다. - -## 본문 - - - -`CapabilitySupport` 는 능력과 지원 등급과 문자열 목록 셋을 담는 레코드다. 그 세 번째 자리가 이 사례의 대상이다. - -## 담을 자리를 만들지 않은 것이 설계다 - -:::evidence key="a-report-that-cannot-carry-a-datasource" alt="코드베이스에서 능력 선언 레코드의 javadoc 과 컴팩트 생성자, 특권 리포트의 javadoc 과 컴포넌트, 밖으로 나가는 리포트의 javadoc 과 특권 리포트를 불리언으로 줄이는 식과 그 식이 실제로 계산하는 것, 역할 이름과 검색 경로를 검사하는 정책의 프로덕션 호출자 수, 두 리포트 컴포넌트 선언부의 자격증명 식별자 수, 엔드포인트의 읽기 전용 애너테이션과 쓰기 애너테이션 매치 수, 그리고 저장소에서 이 엔드포인트 아이디가 나오는 곳과 출하 설정의 노출 목록을 뽑은 출력 78줄. 엔드포인트 아이디가 선언부 한 곳에만 있고 노출 목록에 없다는 것이 그 출력에 보인다." caption="세 타입이 담지 않는 것 · 불리언이 실제로 계산하는 식 · 정책 호출자 0 · 엔드포인트 아이디는 선언부 1곳 · 노출 목록에 없음 — 78줄" zoom="true" -::: - -javadoc 이 "일부러"라고 적고 이유를 잇는다. 이 레코드는 리포트로 직렬화 가능해야 하고 액추에이터로 공개해도 안전해야 하므로 제공자 객체를 저장하지 않는다는 것이다. `DataSource` 도, `EntityManagerFactory` 도, Hibernate 의 `SessionFactory` 도 이름으로 지목해 배제한다. - -그중 하나를 들고 있으면 살아 있는 리소스를 값 타입 안으로 끌고 들어오게 되고, 리포트가 JDBC URL 이나 자격증명을 흘릴 수 있다. - -담을 수 있었는데 안 담은 것이 아니라, 담을 자리를 만들지 않았다. - -## 같은 태도를 리포트 두 층이 다른 문장으로 적는다 - -특권 리포트는 일부러 좁게 만들었고, JDBC URL 도 패스워드도 호스트도 없으며, 공개 전에 마스킹해야 할 것은 여기 들어오지 않는다고 적는다. - -밖으로 나가는 리포트는 배제 목록을 더 길게 적는다. javadoc 은 이 타입에서 볼 것이 담을 수 있는 값이 아니라 담을 수 없는 값이라고 적고, 관리 포트에 닿는 사람이면 누구나 읽을 수 있으니 URL 도 사용자명도 패스워드도 SQL 도 엔티티 목록도 전부 공짜 정찰 답변이 된다고 잇는다. - -그리고 그 리포트는 특권 리포트조차 담지 않는다. 조립 메서드가 그것을 불리언 하나로 줄인다. - -두 리포트의 컴포넌트 선언부에서 제공자와 자격증명 식별자를 세면 둘 다 0 이다. 마스킹 목록이 아니라 타입이 이 일을 하므로, 목록을 갱신하는 사람이 없어도 성립한다. - -## 엔드포인트도 좁고, 그리고 지금 닫혀 있다 - -읽기 연산 하나뿐이고 파라미터가 없다. 쓰기와 삭제와 선택자 애너테이션은 0 이다. 그 javadoc 이 이유를 적는다 — 마이그레이션이나 복구나 캐시 축출을 부를 수 있는 엔드포인트는 관리 포트에 닿는 사람이면 누구나 HTTP 로 쓸 수 있는 관리 기능이 된다는 것이다. - -다만 이 엔드포인트는 지금 나가지 않는다. 저장소에서 이 엔드포인트 아이디가 나오는 곳은 선언부 한 군데뿐이고, 출하 설정의 노출 목록에는 헬스와 프로메테우스와 정보와 로거와 어댑터 활성화 다섯만 있다. - -등록 지점의 javadoc 에는 애너테이션만으로는 아무것도 등록되지 않으며 노출은 애플리케이션의 기존 정책이 정하고 여기서 그것을 넓히지 않는다고 적혀 있다. - -그러므로 이 타입들이 막는 것은 지금 나가는 값이 아니다. 누군가 노출을 켜는 순간 나가게 될 값이다. 타입이 담을 수 없게 만든 것이, 설정으로 노출을 끄는 것보다 먼저다. - -## 축약의 방향은 옳고 이름은 넓다 - -축약한 자리 javadoc 은 그 불리언을 이렇게 소개한다. 특권 리포트의 역할 이름과 검색 경로가 일부러 하나로 줄었고, 운영자가 알아야 하는 것은 역할이 검증을 통과했다는 사실이지 그 역할이 무엇인지가 아니라는 것이다. - -실제로 계산하는 식은 특권 리포트가 있고 그것이 생성 권한을 갖지 않는다는 것뿐이다. 역할 이름도 검색 경로도 보지 않는다. - -그 둘을 실제로 검사하는 정책이 따로 있다. 그 정책을 부르는 프로덕션 코드가 0 이다. - -그래서 잘못된 역할 이름이나 안전하지 않은 검색 경로를 가진 배포에서도 이 값은 참이 될 수 있다. 분석 문서가 이것을 P1 으로 올려 두었다. - -## "경계가 있는"은 문장이지 제약이 아니다 - -javadoc 은 제약을 평범하고 경계가 있는 문자열이라고 부른다. - -컴팩트 생성자가 강제하는 것은 목록의 불변 복사와 빈 문자열 거절 둘이다. 길이 상한도 형식 상한도 없다. - -문자열 안에 무엇이 들어가는지도 보지 않는다. 능력을 선언하는 팩토리는 공개된 가변 인자를 받고 내용을 검사하지 않으므로, 제약 문자열에 자격증명이 붙은 접속 문자열을 넣으면 그대로 리포트에 실린다. javadoc 이 제공자 객체를 막아 방지하겠다고 한 유출이 문자열 경로로 되돌아온다. - -값을 채우는 쪽이 이 저장소 안이라면 문제가 되지 않는다. 출하 조립이 넣는 것은 짧은 고정 리터럴뿐이다. 이 타입이 공개 API 라는 것이 남는 조건이다. - -## 확인하지 못한 것 - -노출 목록에 없는 엔드포인트라 응답 본문까지는 보지 못했다. 확인한 범위는 값 타입이 무엇을 담을 수 없느냐까지다. 노출을 켠 배포에서의 직렬화 결과는 여기 들어 있지 않다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/bounding-by-type/case/case-a05-f004-retrydecision-reason.md b/docs/clean-architecture-backend-template/tech-log-studio/bounding-by-type/case/case-a05-f004-retrydecision-reason.md deleted file mode 100644 index b352b52..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/bounding-by-type/case/case-a05-f004-retrydecision-reason.md +++ /dev/null @@ -1,133 +0,0 @@ ---- -kind: CASE -slug: a05-f004-retrydecision-reason -title: 안전하다고 적힌 값이 같은 모듈의 저카디널리티 정의를 통과하지 못한다 -topic: bounding-by-type -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a05-f004-retrydecision-reason -evidenceCapturedOn: 2026-09-02 -assets: - - key: a05-f004-retrydecision-reason - file: ../../../final/evidence/rendered/a05-f004-retrydecision-reason.svg - - key: a05-f004-retrydecision-reason-probe - file: ../../../final/evidence/rendered/a05-f004-retrydecision-reason-probe.svg -evidence: - - ../../../final/evidence/raw/a05-f004-retrydecision-reason.txt - - ../../../final/evidence/raw/a05-f004-retrydecision-reason-probe.txt -source: - - 분석 문서는 persistence-jpa 편 §7.4 다. 생성자가 널과 공백만 확인하고 길이·형식 상한이 없다는 판정이 그 절에 있다. §10 의 backlog 가 이것을 P3 로 두고, 십만 자 사유가 통과한 것과 현재 재시도 메트릭이 이 값을 태그로 쓰지 않는다는 것을 각각 관측으로 적는다. ---- - -# 안전하다고 적힌 값이 같은 모듈의 저카디널리티 정의를 통과하지 못한다 - -재시도 결정의 사유 필드가 낮은 카디널리티 메트릭 태그로 안전하다고 설명된다. 같은 모듈이 저카디널리티를 정규식 하나로 정의해 두고 있는데, 프로덕션이 실제로 내는 사유 일곱 개가 그 정의에 하나도 맞지 않는다. - -## 관계 - -- **카디널리티 경계를 타입으로 표현하기** - 이 값이 지켜야 할 경계 체계다. -- **CapabilitySupport.constraints 의 경계가 타입에 없다** - 같은 모듈에서 타입이 값의 경계를 담지 않아 생긴 다른 사례다. -- **이름은 값이 아니라 registry key다** - 사유가 등록된 어휘여야 하는지의 문제다. - -## 문제 - -재시도 결정의 사유 필드는 왜 재시도가 허용되거나 거부되었는지를 담는다. - -타입의 javadoc 이 그 값을 정책이 고른 경계 있는 진단 문자열이라 부르고, 제공자 메시지가 아니므로 로그에도 낮은 카디널리티 메트릭 태그에도 안전하다고 적는다. - -## 결론 - -컴팩트 생성자가 보는 항목은 넷이다. 널 검사 셋과 지연의 부호, 전체 트랜잭션 재시도가 아닐 때 지연이 0 인지, 사유가 공백인지다. 상한, 어휘, 정규식 어느 것도 걸려 있지 않다. - -십만 자를 넣어 봤다. 생성자를 통과하고, 그대로 담긴다. - -공개 팩토리는 넷이고 그중 셋이 사유를 인자로 받는다. 나머지 하나는 고정 문자열을 쓴다. - -경계를 실제로 지키고 있는 것은 생성자가 아니다. 현재 사유는 리터럴 여섯 개와 실패 범주 enum 을 붙인 문자열 하나이고, 개수는 그 두 가지로 이미 유계다. - -문제는 개수가 아니라 형식이다. 같은 모듈의 관측 패키지가 저카디널리티를 정규식 하나로 정의해 뒀다. 등록된 이름은 이미 그 모양을 만족하므로 맞지 않는 것은 정제하지 않고 거절한다는 것이다. 정제해 버리면 호출자가 무한한 값을 계속 넘기면서도 모른다는 것이 그 javadoc 의 설명이다. - -일곱 개를 그 가드에 넣으면 전부 거절된다. 29자에서 54자 사이의 띄어쓴 산문이기 때문이다. 문서가 저카디널리티 태그로 안전하다고 적은 값은, 같은 모듈이 저카디널리티라고 정의한 형식을 하나도 만족하지 않는다. - -값이 닿지 않는 것도 아니다. 코디네이터는 실패한 시도마다 결정을 리스너에 넘기고, 출하되는 리스너는 그 결정에서 처분을 꺼내 태그로 단다. 같은 리스너가 영속 단위 이름은 그 가드에 통과시킨다. 아직 읽히지 않는 것은 사유 하나다. - -시계열이 늘지 않는 것은 값이 못 닿아서가 아니다. 닿은 자리에서 필드 하나를 안 꺼내기 때문이다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 생성자 검사 열람, 사유 전수와 같은 모듈 가드에 대한 통과 여부 실행, 소비 파일의 필드 접근 계수 -소스 수정 : x - -## 재현 조건 - -1. 타입의 javadoc 과 컴팩트 생성자를 읽고, 사유에 대한 검사가 무엇인지 확인한다. -2. 사유를 인자로 받는 공개 팩토리를 센다. -3. 십만 자 문자열로 결정을 만들어 통과하는지 본다. -4. 프로덕션 정책이 내는 사유를 전수로 뽑는다. -5. 같은 모듈의 저카디널리티 가드를 찾아, 그 사유들을 실제로 넣어 본다. -6. 그 타입을 소비하는 main 파일을 추리고, 그 안에서 사유 필드를 읽는 줄을 센다. - -## 본문 - - - -재시도 결정 타입의 javadoc 에는 사유 필드가 정책이 고른 경계 있는 진단 문자열이고, 제공자 메시지가 아니므로 로그에도 낮은 카디널리티 메트릭 태그에도 안전하다고 적혀 있다. - -## 그 경계를 강제하는 코드가 없다 - -:::evidence key="a05-f004-retrydecision-reason" alt="재시도 결정 타입의 javadoc 과 사유 필드 선언, 컴팩트 생성자가 검사하는 것 전부, 공개 팩토리 넷, 길이와 어휘를 검사하는 코드 수, 그 타입을 소비하는 main 파일 목록과 그 안에서 사유를 읽는 줄 수, 그리고 코디네이터가 결정을 리스너에 넘기는 줄과 그 리스너가 다는 태그를 출력한 터미널 기록." caption="javadoc 과 생성자가 보는 것 넷 · 팩토리 넷 중 셋이 사유를 받음 · 길이·어휘 검사 0 · 소비 파일 셋에서 사유를 읽는 줄 0 · 결정은 리스너까지 감 — 40줄 · exit 0" zoom="true" -::: - -컴팩트 생성자가 검사하는 것은 넷이다. 세 필드의 널 아님, 지연이 음수 아님, 전체 트랜잭션 재시도가 아니면 지연이 0 일 것, 그리고 사유가 공백 아님이다. - -사유에 대한 검사는 마지막 하나뿐이다. 길이 상한도, 허용 어휘 목록도, 형식 정규식도 없다. 이 파일에서 그런 검사를 세면 0 이다. - -공개 팩토리는 넷이고, 그중 셋이 호출자가 준 문자열을 그대로 담는다. 사유 인자가 없는 재시도 하나만 고정 문자열을 쓴다. - -## 값은 이미 메트릭을 내는 곳까지 간다 - -코디네이터는 실패한 시도마다 결정을 리스너에 넘긴다. 출하되는 리스너는 그 결정에서 처분을 꺼내 태그로 달고, 영속 단위 이름은 저카디널리티 가드에 통과시킨다. - -사유만 아직 읽히지 않는다. 이 타입을 소비하는 main 파일 셋 안에서 사유를 읽는 줄은 0 이다. - -시계열이 늘지 않는 이유는 값이 닿지 않아서가 아니라, 닿은 자리에서 한 필드를 아직 꺼내지 않아서다. - -## 같은 모듈이 저카디널리티를 이미 정의해 뒀다 - -:::evidence key="a05-f004-retrydecision-reason-probe" alt="프로덕션 정책이 내는 사유 리터럴 전수와, 같은 모듈의 저카디널리티 가드가 쓰는 정규식과 거절 방식, 그리고 그 사유들과 십만 자 문자열을 실제로 그 가드와 생성자에 넣어 본 결과를 출력한 터미널 기록." caption="사유 리터럴 일곱 · 같은 모듈 가드의 정규식과 거절 방식 · 일곱 전부 거절 · 십만 자는 생성자 통과 — 41줄 · exit 0" zoom="true" -::: - -관측 패키지의 `LowCardinality` 가 등록된 이름의 모양을 정규식 하나로 못박는다. - -```text -[a-zA-Z][a-zA-Z0-9._-]{0,95} -``` - -맞지 않으면 정제하지 않고 거절한다. 정제하면 호출자가 계속 무한한 값을 넘기면서도 알아채지 못한다고 그 javadoc 이 적는다. - -그 가드를 부르는 곳 중 하나가 재시도 관측기다. 재시도 결정을 받는 바로 그 리스너가 영속 단위 이름을 통과시킨다. 사유는 그 문을 지나지 않는다. - -## 지금 사유 일곱 개를 그 가드에 넣으면 전부 거절된다 - -프로덕션이 내는 사유는 일곱이다. 결정 타입 안의 고정 문자열 하나, 기본 정책의 리터럴 다섯, 그리고 실패 범주 enum 을 붙인 문자열 하나다. - -컴파일된 가드에 그대로 넣었다. 29자에서 54자 사이이고, 전부 거절된다. 띄어쓴 산문이기 때문이다. - -경계를 실제로 지키고 있는 것은 생성자가 아니라 그 리터럴들과 enum 이다. 개수는 그 둘로 이미 유계다. 위험한 것은 개수가 아니라 형식이고, 문서가 안전하다고 적은 형식이 같은 모듈의 정의와 어긋나 있다. - -같은 생성자에 십만 자를 넣어 봤다. 통과하고, 그대로 담긴다. - -## 고칠 방향 - -길이나 어휘 경계를 타입에 넣거나, 낮은 카디널리티 주장을 실제 사용 범위에 맞게 좁히는 것이다. 분석 문서가 후보로 적은 것도 그 둘이다. - -## 확인하지 못한 것 - -이 값이 실제 메트릭 백엔드에 태그로 도달했을 때의 시계열 증가는 관측하지 않았다. 지금 그 필드를 읽는 코드가 없어 관측할 대상이 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/bounding-by-type/case/case-cursor-verification-order.md b/docs/clean-architecture-backend-template/tech-log-studio/bounding-by-type/case/case-cursor-verification-order.md deleted file mode 100644 index 3b25155..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/bounding-by-type/case/case-cursor-verification-order.md +++ /dev/null @@ -1,101 +0,0 @@ ---- -kind: CASE -slug: cursor-verification-order -title: 커서 서명 검증이 길이·상수시간·순서를 전부 지켜야 했던 이유 -topic: bounding-by-type -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:cursor-verification-order -evidenceCapturedOn: 2026-09-01 -assets: - - key: cursor-verification-order - file: ../../../final/evidence/rendered/cursor-verification-order.svg -evidence: - - ../../../final/evidence/raw/cursor-verification-order.txt -source: - - 원본 분석 절은 final/document.md#a05 §2.4 이다. ---- - -# 커서 서명 검증이 길이·상수시간·순서를 전부 지켜야 했던 이유 - -커서 디코드는 길이 상한을 먼저 보고, 형태를 확인하고, 버전을 확인하고, 디코딩될 크기를 인코딩된 길이로 추정해 거절한 다음에야 페이로드를 푼다. MAC 비교는 상수시간이다. 각 단계가 서로 다른 공격을 막는다. - -## 관계 - -- **서명된 커서의 구조와 검증 순서** - 이 사례가 다루는 메커니즘이다. -- **커서에 서명하는 이유는 기밀성이 아니라 무결성이다** - 이 검증이 무엇을 지키는지 정한 결정이다. - -## 문제 - -서명된 커서를 검증하는 코드는 여러 가지를 확인해야 한다. 형태가 맞는지, 버전이 맞는지, MAC 이 맞는지, 크기가 상한 안인지다. - -그 확인들을 어떤 순서로 하느냐가 코드의 성질을 바꾼다. 순서가 잘못되면 검증 코드 자체가 공격 표면이 된다. - -## 결론 - -디코드 경로는 값을 해석하기 전에 형태부터 검사한다. - -빈 문자열 거절 -인코딩된 전체 길이 상한 확인 -구분자 두 개의 위치 확인 -버전 문자열 일치 확인 -인코딩된 페이로드 길이로 디코딩될 크기를 추정해 상한 확인 -그다음에 디코딩 - -다섯 번째 단계에 붙은 주석이 순서의 핵심을 담는다. Base64 는 4/3 으로 팽창하므로 인코딩된 페이로드 구간의 길이가 디코딩된 크기의 상한을 준다. 즉 디코딩하기 전에 크기를 거절할 수 있다. - -이 단계가 없으면 공격자가 보낸 큰 토큰이 먼저 메모리에 풀리고 나서 거절된다. - -MAC 비교는 MessageDigest 의 상수시간 비교를 쓴다. javadoc 이 이유를 적는다. 여기서 단축 평가 비교를 쓰면 올바른 MAC 이 한 바이트씩 새어 나간다. - -MAC 이 버전과 페이로드를 함께 덮는 것도 같은 계열의 결정이다. 페이로드만 서명하면 공격자가 접두사를 고쳐 옛 커서 형식으로 토큰을 강등할 수 있다. - -인코딩 시점에도 두 상한을 검사한다. 발급하는 쪽이 자기가 받아들일 수 없는 토큰을 만들지 않게 한다. - -키 길이 하한도 생성자에서 검사한다. 32 바이트 미만이면 코덱을 만들 수 없다. - -## 검증 환경 - -OpenJDK : 21.0.12 -알고리즘 : HmacSHA256 -인코딩 : Base64 URL 인코더, 패딩 없음 -확인 방식 : 구현과 javadoc 확인 -소스 수정 : x - -## 재현 조건 - -1. SignedJsonCursorCodec 의 클래스 javadoc 을 읽는다. 인코딩 형태와 서명 목적과 상수시간 비교의 이유가 적혀 있다. -2. 디코드 경로의 검사 순서를 확인한다. -3. 인코딩된 길이로 디코딩될 크기를 추정하는 주석과 그 계산을 확인한다. -4. 인코드 경로가 같은 두 상한을 검사하는지 확인한다. -5. 생성자의 최소 키 길이 검사를 확인한다. - -## 본문 - - - -다섯 방어가 각각 다른 공격을 막고 순서가 계약이다. - -## 다섯 방어와 그 순서 - -:::evidence key="cursor-verification-order" alt="분석 문서 final/document.md#a05 에서 이 기록의 근거 절을 그대로 잘라낸 12줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a05 발췌 — 12줄" zoom="true" -::: - -## 각 순서가 막는 것 - -`MAX_ENCODED_LENGTH(4096)` 검사가 첫 줄에 없으면 decode가 caller가 보낸 크기만큼 할당한다. MAC 길이 확인이 없으면 `MessageDigest.isEqual`의 상수 시간 보장이 깨진다. 서명 검증 전에 파싱하면 서명 없는 토큰이 애플리케이션 JSON 파서에 도달한다. - -## MAC이 버전과 payload를 함께 덮는다 - -prefix 재작성으로 옛 포맷으로 다운그레이드하는 것을 막는다. - -## 확인하지 못한 것 - -타이밍 공격이나 크기 공격을 실제로 시도해 보지 않았다. 이 기록은 각 단계가 무엇을 막도록 배치되었는지에 대한 것이며, 그 방어의 실효성을 측정한 것은 아니다. - -없음 - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/bounding-by-type/reference/reference-reject-rather-than-sanitize.md b/docs/clean-architecture-backend-template/tech-log-studio/bounding-by-type/reference/reference-reject-rather-than-sanitize.md deleted file mode 100644 index 7ef1de0..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/bounding-by-type/reference/reference-reject-rather-than-sanitize.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -kind: REFERENCE -slug: reject-rather-than-sanitize -title: sanitize가 아니라 reject가 기본이다 -topic: bounding-by-type -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:reject-rather-than-sanitize -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# sanitize가 아니라 reject가 기본이다 - -## 목적 - -위험한 입력을 고쳐서 받아들이는 습관이, 고치지 못한 경우를 통과시키는 것을 막는다. - -## 규칙 - -1. 거절이 기본이다 - 허용 목록에 없으면 거절한다. 값을 다듬어 통과시키지 않는다. - -2. 정화는 완전성을 증명할 수 없다 - 무엇을 지웠는지는 말할 수 있지만 무엇을 놓쳤는지는 말할 수 없다. - -3. 정화를 쓴다면 그 지위를 밝힌다 - 심층 방어인지 보증인지 적는다. 보증이 아니면 그것에 기대는 다른 판단이 없어야 한다. - -4. 거절 이유에 코드를 붙인다 - 거절이 운영자가 읽을 수 있는 사건이 되어야 한다. 조용한 거절은 조용한 통과와 구별되지 않는다. - -## 적용 조건 - -외부 입력을 구조 위치에 쓰는 모든 경계 - -로그와 메트릭에 들어가는 값 - -## 예외 - -표시용 문자열을 길이로 자르는 것처럼 의미를 바꾸지 않는 정규화는 이 규칙의 대상이 아니다. - -## 예시 - -로그 마스킹 패턴의 README 자신이 정규식 마스킹을 보증이 아니라 심층 방어라고 적는다. 그리고 임의의 이메일이나 자유 형식 본문을 제거하는 규칙은 없다. - -이 저장소의 HTTP 클라이언트는 절대 URI 를 정화하지 않고 거절한다. - -## 관계 - -- **시그니처가 payload를 받지 않는데 예외 메시지로 PII가 로그에 남았다** - 정화에 기댔을 때의 한계를 보여 주는 사례다. -- **path·identifier는 등록하고 value는 바인딩한다** - 거절 대상을 정하는 규칙이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/caching-and-redis/case/case-a10-f001-readme.md b/docs/clean-architecture-backend-template/tech-log-studio/caching-and-redis/case/case-a10-f001-readme.md deleted file mode 100644 index 0b6ef35..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/caching-and-redis/case/case-a10-f001-readme.md +++ /dev/null @@ -1,224 +0,0 @@ ---- -kind: CASE -slug: a10-f001-readme -title: 표가 증거로 지목한 시험이 그 표를 반증한다 -topic: caching-and-redis -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a10-f001-readme -evidenceCapturedOn: 2026-09-02 -body: case-a10-f001-readme.body.md -assets: - - key: a10-f001-readme - file: ../../../final/evidence/rendered/a10-f001-readme.svg - - key: a10-f001-readme-counts - file: ../../../final/evidence/rendered/a10-f001-readme-counts.svg -evidence: - - ../../../final/evidence/raw/a10-f001-readme.txt - - ../../../final/evidence/raw/a10-f001-readme-counts.txt -source: - - 원본 분석 절은 final/document.md#a10#L121 이다. 등급은 P2 다. 표 네 행 중 셋이 사실과 다르다는 판정, 패키지별 파일과 줄 수, `@Bean` 메서드 일곱, 빌드 파일 주석의 임포트 계수, 그리고 무겁게 보는 세 이유가 그 절에 있다. - - 그 절은 이 수치를 LOC 라고 적지만 실제로 센 것은 물리 줄 수여서, 여기서는 줄 수라고만 적는다. - - 표가 둘째 축의 증거로 지목한 시험이 표를 반증한다는 것, 같은 README 의 산문이 세 줄 뒤에서 표와 어긋난다는 것, 다섯 포트 중 세션만 표가 맞다는 것, 일곱째 `@Bean` 이 조건부라는 것, 그리고 주석의 주어절은 맞고 괄호만 틀렸다는 것은 이 기록에서 확인했다. ---- - -# 표가 증거로 지목한 시험이 그 표를 반증한다 - -리프 README 의 준비도 표와 그 아래 두 문단이 이 리프의 상태를 없음으로 적는다. 표가 둘째 열의 증거로 이름을 대 놓은 시험이 그 빈들이 조립된다고 단언하고, 같은 README 의 산문이 세 줄 뒤에서 같은 기능을 제공한다고 적는다. - -## 관계 - -- **문서 계약 테스트의 단언 경계 밖에 발견된 드리프트 세 건이 전부 있었다** - 그 테스트가 단언하는 범위 밖에서 문서와 코드가 어긋났다. -- **README의 세 가지 사실 오류** - 같은 README 에서 확인한 다른 사실 오류다. -- **산문이 선언한 게이트는 빌드에 있는 게이트가 아니다** - 문서와 빌드를 대조하는 규칙이다. - -## 문제 - -이 리프의 README 는 준비도 보고를 자기 주제로 삼는다. 세 질문을 합치지 말라고 못 박고, 축마다 증거를 지정한 다음 표를 놓는다. - -## 결론 - -둘째 축의 증거로 이름을 댄 시험이 RedisSdkAutoConfigurationTest 다. 그 시험이 RedisRuntimeClient 와 RedisRuntimeOwner 를 hasSingleBean 으로 단언한다. 표가 그 둘을 조립되지 않는다고 적은 자리다. - -같은 README 도 자기와 어긋난다. 표에서 세 줄 뒤 산문이 이 모듈은 Lettuce 연결 수명과 명령 타임아웃과 재연결 재생 차단을 제공한다고 적는다. 표 둘째 행이 없음이라고 적은 바로 그것이다. - -수를 세면 이렇다. - -sdk/lettuce/connection 에 아홉 파일 1,473 줄이 있다. RedisTopologyClientFactory 600, RedisRuntimeOwner 316, SentinelFailoverObserver 153, RedisConnectionRegistry 142 줄이다. - -의미 포트 행은 다섯 이름을 한 칸에 묶는다. 그중 넷은 아홉 파일 2,598 줄로 있고 세션 하나만 표가 맞다. 세션 쪽은 패키지 자체가 없고, 남은 여덟 건은 코드가 아니라 문장과 선택자 값이다. - -상태 기여자 행도 틀렸다. RedisHealthContributor 88 줄과 RedisCorrectnessRoles 62 줄이 있다. - -자동 설정에는 @Bean 메서드가 일곱 있다. 여섯은 조건 없이 조립되고, 일곱째 redisRequired 에는 @Conditional 이 붙어 세션·멱등·레이트리밋·리스 중 하나가 Redis 를 고를 때만 생긴다. 조립되는 자리를 못 찾은 것은 게이트웨이와 의미 어댑터다. - -빌드 파일 주석은 절반만 틀렸다. 주석의 주어는 SDK 이고, sdk 패키지의 어떤 파일도 그 간선들을 임포트하지 않는다. 괄호 안의 일반화가 틀렸다. 메인 소스 전체로 넓히면 애플리케이션 코어를 일곱 파일이, 공유 계약을 세 파일이 임포트한다. 등록된 간선 셋 중 adapter:outbound:support 만 실제로 0 이다. - -같은 주석은 그 어댑터들이 제거됐다고도 적는다. 위에서 센 파일들이 그것이다. 그리고 간선을 남겨 두는 이유로 든 문장 — 여기서 무언가가 그것에 대해 컴파일되기 때문이 아니라는 것 — 도 뒤집힌다. - -스무 줄 뒤에 있는 별개 주석 쪽은 맞다. spring-data-redis 와 io.micrometer 임포트는 실제로 0 이다. - -판정은 P2 다. 코드 결함이 아니라 문서 결함인데 이 저장소 기준으로는 무겁다. - -첫째, 이 문서는 정직한 준비도 보고를 자기 주제로 삼고, 축마다 증거를 지정하기까지 한다. 그 증거가 문서를 반증한다. - -둘째, 방향이 이례적이다. 보통의 표류는 없는 것을 있다고 하는데 여기는 있는 것을 없다고 한다. 포크한 쪽은 있는 것을 다시 만들거나, 있는 줄도 모른 채 지나친다. - -셋째 근거는 자동 설정 안의 문장이다. 이 클래스가 생기기 전까지 설정 검증 메서드에 프로덕션 호출자가 없었다고 적는다. - -## 검증 환경 - -확인 방식 : 패키지별 파일과 줄 수 계수, @Bean 메서드와 조건 계수, 임포트 계수 -소스 수정 : x - -## 재현 조건 - -1. README 가 세 축마다 지정한 증거를 읽는다. -2. 둘째 축의 증거로 지목된 시험이 무엇을 단언하는지 읽는다. -3. 준비도 표 네 행과 그 아래 두 문단을 읽고, 세 줄 뒤 산문까지 이어 읽는다. -4. 표가 없다고 적은 자리마다 패키지의 파일과 줄 수를 센다. -5. 의미 포트 행이 든 다섯 이름과 실제 패키지 이름을 대조하고, 행에 없는 패키지는 합계에서 뺀다. -6. 세션을 리프 메인 전체에서 문자열로 검색한다. -7. 자동 설정의 @Bean 메서드를 조건 애너테이션까지 함께 읽는다. -8. 빌드 파일이 등록한 간선 셋을 각각 임포트하는 파일을 세고, 주석의 주어인 sdk 패키지만으로도 센다. -9. 스무 줄 뒤의 별개 주석과 그 주장을 확인한다. - -## 본문 - - - -README 는 준비도가 서로 다른 세 질문이며 하나로 합치면 안 된다는 문장으로 시작하고, 축마다 무엇을 증거로 삼는지까지 적는다. - -## 표가 지목한 증거 - -:::evidence key="a10-f001-readme" alt="README 가 준비도 세 축마다 지정한 증거, 준비도 표 네 행, 표 아래 두 문단. 표에서 세 줄 뒤 같은 README 의 산문이 이 모듈이 제공한다고 적는 목록. 그리고 둘째 축의 증거로 지목된 시험이 어떤 빈들을 단언하는지 출력한 터미널 기록." caption="표는 연결 수명과 의미 포트와 상태 기여자를 없음으로 적음 · 세 줄 뒤 산문은 같은 모듈이 Lettuce 연결 수명을 제공한다고 적음 · 표가 증거로 지목한 시험은 RedisRuntimeClient 와 RedisRuntimeOwner 를 hasSingleBean 으로 단언 — 46줄 · exit 0" zoom="true" -::: - -```text -| **Spring composition 구현** | `APP_REDIS_ENABLED=true`에서 실제 bean이 조립된다 | `RedisSdkAutoConfigurationTest` | -``` - -그 시험이 무엇을 단언하는지 보면 이렇다. - -```java -assertThat(context).hasSingleBean(RedisSdkSettings.class); -... -assertThat(context).hasSingleBean(RedisRuntimeClient.class); -... -assertThat(context).hasSingleBean(RedisRuntimeOwner.class); -``` - -표는 같은 둘을 조립되지 않는다고 적는다. - -```text -| Topology client / connection lifecycle | 없음 | 없음 | 없음 | -| cache / session / idempotency / rate limit / lease semantic port | 없음 | 없음 | 없음 | -| role-aware health·readiness contributor | 없음 | 없음 | 없음 | -``` - -## 같은 README 가 세 줄 뒤에서 - -```text -모듈은 Lettuce connection lifecycle, -finite command timeout, reconnect replay 차단, finite request queue/admission, positive/negative -TTL, absolute soft/hard expiry, deterministic bounded TTL jitter, digest-protected v2 binary -envelope, HMAC physical key, -invalidation, closed-catalog -`EVALSHA -> NOSCRIPT -> SCRIPT LOAD -> digest verify -> EVALSHA` recovery를 제공한다. -``` - -표 둘째 행이 없음이라고 적은 것을 산문이 제공한다고 적는다. 어긋난 것은 문서 전체가 아니라 표와 그 아래 두 문단이다. - -## 수를 세면 - -:::evidence key="a10-f001-readme-counts" alt="표가 없다고 적은 자리의 파일 수와 줄 수. 의미 포트 행이 든 다섯 이름 중 구현이 있는 넷의 합계와, 그 행에 이름이 없어 합계에서 뺀 두 패키지. 세션 문자열의 리프 전체 계수. 상태 기여자의 줄 수. 자동 설정의 `@Bean` 메서드 전부와 거기 붙은 조건 애너테이션. 빌드 파일이 등록한 간선 셋과 앞 주석, 그 간선들을 임포트하는 파일 수와 주석의 주어인 sdk 패키지만의 계수, 그리고 스무 줄 뒤의 별개 주석을 출력한 터미널 기록." caption="연결 패키지 9 파일 1,473 줄 · 표가 든 다섯 포트 중 넷이 9 파일 2,598 줄이고 세션만 없음 · @Bean 일곱 중 하나에 @Conditional · 등록 간선 셋 중 둘은 열 파일이 임포트하고 support 만 0 · 주석의 주어인 sdk 패키지만 보면 0 — 58줄 · exit 0" zoom="true" -::: - -```text -# sdk/lettuce/connection : 9 파일 1473 줄 - 600 sdk/lettuce/connection/RedisTopologyClientFactory.java - 316 sdk/lettuce/connection/RedisRuntimeOwner.java - 153 sdk/lettuce/connection/SentinelFailoverObserver.java - 142 sdk/lettuce/connection/RedisConnectionRegistry.java -# 표가 든 다섯 포트 중 구현이 있는 넷 - cache 2 파일 641 줄 - idempotency 2 파일 835 줄 - ratelimit 3 파일 516 줄 - lease 2 파일 606 줄 - 합계 9 파일 2598 줄 -# 다섯째 session : 패키지 없음, main 전체에서 session 문자열 8 -# 그 행에 이름이 없어 뺀 패키지 : realtime 591 줄, keyspace 106 줄 -# 상태 기여자 : 88 + 62 -``` - -의미 포트 행은 다섯 이름을 한 칸에 묶는데 그중 하나만 맞다. 세션은 패키지가 없고, 리프 메인 전체에서 그 문자열 여덟 건이 전부 javadoc 산문과 역할 선택자 값이다. `realtime` 과 `keyspace` 는 그 행에 없는 이름이라 합계에서 뺐다. - -## `@Bean` 은 일곱, 무조건은 여섯 - -```text - 273: @Bean(destroyMethod = "close") - 274- public RedisRuntimeOwner redisRuntimeOwner(RedisRuntimeClient client, RedisSdkSettings settings) { - 297: @Bean(RedisCorrectnessRoles.OPTIONAL_HEALTH_CONTRIBUTOR) - 298- public HealthIndicator redisOptional(RedisRuntimeOwner owner, RedisSdkSettings settings) { - 324: @Bean(RedisCorrectnessRoles.REQUIRED_HEALTH_CONTRIBUTOR) - 325- @Conditional(RedisCorrectnessRoleBound.class) - 326- public HealthIndicator redisRequired(RedisRuntimeOwner owner, RedisSdkSettings settings) { -``` - -일곱째만 조건부다. 세션·멱등·레이트리밋·리스 중 하나가 Redis 를 고를 때 생긴다. 나머지 여섯은 스위치 하나로 조립된다. 어디서도 조립되지 않는 것은 게이트웨이와 의미 어댑터 둘이다. - -## 빌드 파일의 의존성 주석 - -```groovy -// Registered edges the semantic port adapters need. The SDK itself imports nothing from them -// today (0 imports across main source) — the semantic cache/session/idempotency/rate-limit -// adapters that did were removed and are restored by Phase E of -// … -// because that restoration is the module's stated responsibility, not because anything here -// compiles against them. -implementation project(':application-core') -implementation project(':shared-contract') -implementation project(':adapter:outbound:support') -``` - -주어절은 맞다. - -```text - dev.caskeleton.application 7 파일 - dev.caskeleton.shared 3 파일 - dev.caskeleton.adapter.outbound.support 0 파일 -# 주석의 주어인 sdk 패키지만 보면 : 0 -``` - -`sdk` 패키지는 세 간선 어디에서도 임포트하지 않는다. 틀린 것은 괄호 안의 일반화다. 메인 소스로 넓히면 열 파일이 임포트하고, 셋 중 `support` 만 주석대로 0 이다. - -같은 주석이 그 어댑터들은 제거됐다고 적는데, 위에서 센 파일들이 그것이다. 간선을 남겨 두는 이유로 든 문장도 뒤집힌다 — 여기서 무언가가 그것에 대해 컴파일되기 때문이 아니라고 적혀 있는데, 컴파일된다. - -스무 줄 뒤의 별개 주석은 맞다. - -```groovy -// Deliberately absent: -// org.springframework.data:spring-data-redis — … Zero imports. -// io.micrometer:micrometer-core — … Zero imports. -``` - -## 어긋난 방향 - -표가 없다고 적은 자리마다 코드가 있다. 보통의 표류는 반대 방향이다. 이 문서를 읽고 분기하는 쪽은 이미 있는 코드를 다시 구현하거나, 조립되지 않은 채 존재하는 코드의 존재 자체를 모른다. - -순서를 시사하는 문장이 자동 설정 안에 있다. - -```java - * RedisSdkSettings#validate()} the fail-fast its own documentation claims — until this class - * existed the method had no production caller at all. -``` - -## 확인하지 못한 것 - -README 가 마지막으로 갱신된 시점과 자동 설정이 추가된 시점을 이력에서 대조하지 않았다. 자동 설정 javadoc 의 문장이 순서를 시사할 뿐이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/caching-and-redis/case/case-a10-f004-pub-sub.md b/docs/clean-architecture-backend-template/tech-log-studio/caching-and-redis/case/case-a10-f004-pub-sub.md deleted file mode 100644 index d7f6ccb..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/caching-and-redis/case/case-a10-f004-pub-sub.md +++ /dev/null @@ -1,219 +0,0 @@ ---- -kind: CASE -slug: a10-f004-pub-sub -title: 상한을 주입받는 자리는 있고 주입하는 곳은 없다 -topic: caching-and-redis -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a10-f004-pub-sub -evidenceCapturedOn: 2026-09-02 -body: case-a10-f004-pub-sub.body.md -assets: - - key: a10-f004-pub-sub - file: ../../../final/evidence/rendered/a10-f004-pub-sub.svg - - key: a10-f004-pub-sub-bound - file: ../../../final/evidence/rendered/a10-f004-pub-sub-bound.svg -evidence: - - ../../../final/evidence/raw/a10-f004-pub-sub.txt - - ../../../final/evidence/raw/a10-f004-pub-sub-bound.txt -source: - - 원본 분석 절은 final/document.md#a10#L263 이다. 등급은 P3 이다. 세 타입 중 하나만 크기 검사를 부른다는 표, 채널 이름이 Redis 에서 키와 같은 문자열 공간을 쓰고 같은 상한을 받는다는 지적, 구성 요소가 각자 토큰 길이를 제한하므로 현실적인 초과는 어렵다는 단서, 그리고 두 채널 타입의 렌더 본문이 같다는 관찰이 그 절에 있다. 원본이 P3 의 근거로 든 것은 검사가 패턴에는 있고 채널에는 없다는 비대칭 자체다. - - 이 기록이 더한 것은 셋이다. 원본이 「현실적인 초과는 어렵다」고만 적은 것에 수를 붙였다 — 최대 388 바이트, 기본 이름공간에서 221 바이트다. 같은 규칙을 받은 조각들로 만든 키가 슬롯 태그가 붙으면 519 바이트가 되어 같은 검사에 거부된다. 그리고 그 검사가 보는 상한이 주입 인자인데 저장소에 주입하는 곳이 없다. - - 원본 backlog 가 reachability 로 적어 둔 「긴 namespace/entity/identifier 조합」은 388 바이트가 답이다. 셋을 최대로 채워도 상수 상한을 넘지 않는다. 넘는 경로는 슬롯 태그 쪽이고, 그것도 채널이 아니라 키에서 일어난다. ---- - -# 상한을 주입받는 자리는 있고 주입하는 곳은 없다 - -조립된 문자열이 최대 바이트를 넘지 않는지 보는 검사가 같은 성격의 세 타입 중 하나에만 있다. 검사를 부르는 다른 한 곳은 키 렌더러이고, 키 렌더러는 상수가 아니라 생성자로 받은 상한을 본다. 그 인자를 채우는 코드가 저장소에 없다. - -## 관계 - -- **채워 넣은 상한은 자기가 잴 요청에서 값을 가져온다** - 같은 리프에서 상한 값을 어디서 가져오는지가 문제가 된 다른 사례다. -- **R1과 R2의 설정 취급이 비대칭이고, 검증된 쪽은 하나뿐이다** - 두 사례 모두 같은 성격의 두 자리 중 한쪽에만 검사가 있다. -- **그 속성을 보일 수 없는 대역 위에서 통과한 테스트는 증인이 아니다** - 경계가 어디서 지켜지는지 묻는 규칙이다. - -## 문제 - -키 규칙에 조립된 문자열의 렌더 크기를 검사하는 메서드가 있다. Pub/Sub 쪽에는 같은 성격의 타입이 셋 있고, 그중 하나만 그 검사를 부른다. - -## 결론 - -부르는 쪽은 패턴 타입이다. 컴팩트 생성자에서 이름공간 접두와 접미를 이어 놓고 크기를 잰다. 재는 때가 렌더보다 한 발 앞이다. - -두 채널 타입은 부르지 않는다. 널 검사만 하고 렌더에서 세 조각을 잇는다. - -상한이 상수 512 인 한 그 빠진 검사가 발화할 입력이 없다. 조각 셋이 모두 같은 규칙을 먼저 통과한 값이다. 이름공간의 세 토큰과 개체 토큰은 최대 64자, 식별자는 최대 128자다. 두 정규식은 ASCII 만 받으므로 길이가 곧 바이트다. 세 토큰을 모두 최대로 채운 렌더가 388 바이트다. 설정 기본 이름공간에서는 221 바이트다. - -패턴은 다르다. 접미에 붙는 규칙은 공백이 아닐 것과 구분자를 넘지 않을 것 둘뿐이고 길이 제한이 없다. 이름공간을 최대치로 잡으면 접미 317자까지 512 바이트로 통과하고 318자에서 513 바이트가 되어 생성자가 거부한다. - -원본의 판단은 조각마다 길이 제한이 있어 현실적인 초과가 어렵다는 것이었다. 그 반례가 같은 패키지의 키 경로에 있다. 키 렌더러는 이름공간과 개체와 식별자 사이에 슬롯 태그를 하나 더 넣는데, 그 태그도 식별자 규칙을 받아 최대 128자다. 넷을 최대로 채우면 519 바이트가 되고 같은 검사가 거부한다. 조각이 규칙을 받는다는 것과 합이 상한 안에 있다는 것은 다른 말이다. 다만 이것도 이름공간이 194 바이트일 때의 값이고, 기본 이름공간에서는 같은 태그를 붙여도 352 바이트다. - -두 번째 차이는 상한을 어디서 가져오느냐다. 키 렌더러가 보는 상한은 생성자 인자이고 1 부터 512 까지 받는다. 패턴은 인자를 받지 않고 상수를 본다. 채널은 아무것도 보지 않는다. 상한 256 으로 만든 렌더러는 388 바이트짜리 키를 거부하는데, 같은 크기의 채널 이름은 그대로 나간다. - -그런데 그 인자를 채우는 배선이 없다. 저장소의 src/main 전체에서 렌더러를 만드는 곳이 0 이고, 만드는 곳 열둘은 전부 시험 소스다. 설정 쪽도 마찬가지다. max-key-bytes 쪽도 같다. 등록과 범위 검증은 지나는데 읽는 자리가 없다. - -그래서 지금 배포에서는 상수 512 가 셋을 다 덮는다. 셋이 갈리는 것은 그 인자가 설정과 이어지는 날이다. - -판정은 P3 이다. 세 타입이 같은 문자열 공간을 쓰는데 상한을 하나는 주입받고 하나는 상수로 박고 하나는 아예 보지 않는다. - -두 채널 타입의 렌더 본문은 서로 완전히 같다. 갈린 이유는 전송 경로이지 렌더링이 아니다 — 군집에서 슬롯을 소유한 샤드로만 전달된다. 타입을 나눈 것은 맞고, 두 벌이 된 것은 렌더 규칙 쪽이다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 세 타입의 생성자와 렌더 대조, 구성 요소 규칙 확인, 배선 탐색, 실행 탐침 -소스 수정 : x - -## 재현 조건 - -1. 세 타입의 컴팩트 생성자와 렌더 메서드를 각각 읽는다. -2. 렌더 크기 검사가 나오는 곳을 저장소 전체에서 센다. 선언과 호출을 구분한다. -3. 채널 구성 요소가 받는 토큰과 식별자 규칙, 그리고 상한 상수를 읽는다. -4. 키 렌더러가 조각을 어떤 순서로 잇는지, 상한을 어디서 받는지 읽는다. -5. 그 렌더러를 만드는 곳과 설정값을 읽는 곳을 src/main 과 src/test 로 나눠 센다. -6. 구성 요소를 최대로 채운 채널과 키를, 그리고 기본 이름공간의 채널과 키를 각각 만들어 길이를 잰다. -7. 슬롯 태그를 붙인 키를 만들어 같은 검사가 거부하는지 본다. -8. 상한 256 으로 만든 렌더러에 같은 키를 넣고, 같은 크기의 채널과 대조한다. - -## 본문 - - - -키 규칙에 조립된 문자열의 렌더 크기를 재는 메서드가 있고, Pub/Sub 쪽 세 타입 중 하나만 그것을 부른다. - -## 부르는 하나와 부르지 않는 둘 - -:::evidence key="a10-f004-pub-sub" alt="Pub/Sub 세 타입의 컴팩트 생성자와 렌더 메서드, 그중 두 채널 타입의 렌더 본문이 같다는 것. 렌더 크기 검사가 나오는 세 줄 — 선언 하나와 호출 둘. 채널 조각이 받는 토큰·식별자 정규식과 상한 상수, 그 규칙을 부르는 세 타입. 키를 렌더하는 메서드가 조각 사이에 슬롯 태그를 넣는 줄과 그 상한을 생성자로 받는 줄. 그 생성자를 부르는 곳을 src/main 과 src/test 로 나눠 센 수, 설정값을 읽는 src/main 코드, 그리고 기본 이름공간 값을 출력한 터미널 기록." caption="크기 검사를 부르는 곳은 키 렌더러와 패턴 생성자 둘 · 두 채널 타입은 널 검사만 하고 렌더 본문이 동일 · 조각은 토큰 64자와 식별자 128자 규칙을 받고 슬롯 태그도 같은 규칙 · 렌더러 생성은 src/main 0건 src/test 12건이고 max-key-bytes 를 읽는 src/main 코드도 없음 — 89줄 · exit 0" zoom="true" -::: - -```java -public PubSubPattern { - ... - if (suffixPattern.isBlank() || suffixPattern.indexOf(':') >= 0) { - throw new IllegalArgumentException( - "a pattern suffix must be non-blank and must not cross a namespace separator"); - } - RedisKeyRules.requireRenderedSize( - namespace.prefix() + ':' + suffixPattern, RedisKeyRules.MAX_KEY_BYTES); -} -``` - -렌더 시점이 아니라 생성 시점에 잰다. 두 채널 타입의 생성자에는 널 검사만 있다. - -```text - sdk/api/key/RedisKeyRules.java:89: public static String requireRenderedSize(String rendered, int maxKeyBytes) { - sdk/api/key/RedisKeyRenderer.java:45: return RedisKeyRules.requireRenderedSize(rendered.toString(), maxKeyBytes); - sdk/api/operations/PubSubPattern.java:30: RedisKeyRules.requireRenderedSize( -``` - -첫 줄은 선언이고 부르는 곳은 아래 둘이다. 시험 소스까지 포함해 이게 전부다. - -## 조각이 받는 규칙 - -```text - 19: public static final int MAX_KEY_BYTES = 512; - 21: private static final Pattern TOKEN = Pattern.compile("^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$"); - 23: private static final Pattern IDENTIFIER = Pattern.compile("^[A-Za-z0-9][A-Za-z0-9._~-]{0,127}$"); - 91: if (maxKeyBytes < 1 || maxKeyBytes > MAX_KEY_BYTES) { - 92: throw new IllegalArgumentException("maximum key bytes must be in 1.." + MAX_KEY_BYTES); - RedisNamespace.java:16: RedisKeyRules.requireToken("environment", environment); - RedisNamespace.java:17: RedisKeyRules.requireToken("service", service); - RedisNamespace.java:18: RedisKeyRules.requireToken("domain", domain); - RedisKeyName.java:12: RedisKeyRules.requireToken("entity", entity); - RedisKeyName.java:13: RedisKeyRules.requireIdentifier(identifier); - RedisSlotTag.java:15: RedisKeyRules.requireIdentifier(value); -``` - -채널이 잇는 세 조각은 전부 이 규칙을 통과한 값이다. 두 정규식이 ASCII 밖을 받지 않아 바이트 수가 자 수와 같다. 패턴이 잇는 접미는 규칙을 받지 않는다 — 공백이 아닐 것과 구분자를 넘지 않을 것뿐이다. - -## 최대로 채워 보면 - -:::evidence key="a10-f004-pub-sub-bound" alt="이름공간 세 토큰과 개체 토큰과 식별자를 각 규칙의 최대치로 채워 만든 두 채널 타입의 렌더 길이, 같은 조각으로 만든 키와 거기에 슬롯 태그를 더했을 때의 결과, 설정 기본 이름공간으로 만든 같은 둘의 길이, 접미 길이를 64·317·318 로 바꿔 가며 만든 패턴의 결과, 그리고 상한 256 으로 만든 렌더러가 같은 키와 같은 크기의 채널을 각각 어떻게 처리하는지 출력한 터미널 기록." caption="최대로 채운 채널 렌더는 388 바이트, 기본 이름공간에서는 221 바이트 · 같은 조각에 슬롯 태그를 더한 키는 519 바이트로 거부되지만 기본 이름공간에서는 352 바이트 · 상한 256 렌더러는 388 바이트 키를 거부하고 같은 크기 채널은 검사 자체가 없음 — 28줄 · exit 0" zoom="true" -::: - -```text -[구성 요소의 상한] 토큰 64, 식별자 128, 슬롯 태그 128 - namespace.prefix() 길이 : 194 - -[채널] 구성 요소를 최대로 채운 렌더 - PubSubChannel 렌더 388 바이트 여유 124 - ShardedPubSubChannel 렌더 388 바이트 여유 124 -``` - -이 388 바이트는 이름공간 세 토큰을 모두 64자로 채웠을 때의 값이다. 설정 기본값인 `local:sample-service:shared` 는 27 바이트라 같은 조각으로 만든 채널이 221 바이트에 그친다. - -패턴은 같은 최대 이름공간에서 접미 317자까지 512 바이트로 통과하고 318자에서 넘긴다. - -## 조각이 규칙을 받는다는 말의 한계 - -원본은 조각이 각자 길이를 제한하므로 현실적인 초과가 어렵다고 봤다. 같은 규칙을 받은 조각들이 상한을 넘는 경우가 같은 패키지에 있다. - -```java -rendered.append(key.namespace().prefix()).append(':'); -key.slotTag().ifPresent(tag -> rendered.append('{').append(tag.value()).append("}:")); -rendered.append(key.name().entity()).append(':').append(key.name().identifier()); -return RedisKeyRules.requireRenderedSize(rendered.toString(), maxKeyBytes); -``` - -키 렌더러는 조각 사이에 슬롯 태그를 하나 더 넣는다. 그 태그도 식별자 규칙을 받아 최대 128자다. - -```text -[키] 같은 규칙을 받은 조각들, 슬롯 태그 하나가 더 붙는다 - 태그 없음 -> 렌더 388 바이트 - 태그 있음 -> rendered key is 519 bytes and exceeds the configured 512 - -[기본 이름공간] 설정 기본값 local:sample-service:shared - prefix 길이 : 27 - 채널 렌더 : 221 바이트 - 태그 붙인 키 : 352 바이트 -``` - -519 바이트도 이름공간이 194 바이트일 때의 값이다. 기본 이름공간에서는 같은 태그를 붙여도 352 바이트로 통과한다. - -## 상한을 어디서 가져오는가 - -```text -[상한을 어디서 가져오는가] - RedisKeyRenderer : 생성자 인자 (1..512) - PubSubPattern : RedisKeyRules.MAX_KEY_BYTES 상수 - 상한 256 렌더러에 키 -> rendered key is 388 bytes and exceeds the configured 256 - 같은 배포의 채널 388 바이트 -> 검사 없음 -``` - -키 렌더러만 상한을 주입받는다. 그런데 주입하는 곳이 없다. - -```text - src/main 에서 new RedisKeyRenderer( : 0 건 - src/test 에서 new RedisKeyRenderer( : 12 건 - getMaxKeyBytes / getLimits 를 부르는 src/main 코드 - sdk/config/RedisSdkSettings.java:272: public int getMaxKeyBytes() { - sdk/config/RedisSdkSettings.java:908: public Limits getLimits() { -``` - -두 줄 다 선언 자신이다. `max-key-bytes` 는 환경 키 레지스트리에 등록되어 있고 부팅 검증이 1..512 범위까지 보지만, 읽는 코드가 없다. - -그래서 이 대비는 지금 배포에서 일어나는 일이 아니다. 그 인자가 설정과 이어지는 날 일어날 일이다. - -채널 이름이 렌더러를 지나가는 일도 없다. Pub/Sub 요청은 `channel.render()` 를 직접 부르고 키 목록으로 빈 리스트를 넘긴다. - -## 남는 것 - -두 채널 타입은 렌더 본문이 한 글자도 다르지 않다. - -```java -public String render() { - return namespace.prefix() + ':' + name.entity() + ':' + name.identifier(); -} -``` - -갈린 이유는 전송 경로다. 군집에서 슬롯을 소유한 샤드로만 전달된다는 성질이지 렌더링이 아니다. 분리 자체는 옳고, 렌더 규칙만 두 벌이라 한 곳에서 고칠 수 없다. - -## 확인하지 못한 것 - -상한을 넘는 채널 이름을 브로커에 보내면 어떻게 되는지 확인하지 않았다. 상수 상한을 넘는 이름은 이 타입들로 만들 수 없고, 상한을 낮춘 렌더러로 만든 키는 애초에 나가지 못한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/caching-and-redis/case/case-a10-f005-hyperloglog-merge.md b/docs/clean-architecture-backend-template/tech-log-studio/caching-and-redis/case/case-a10-f005-hyperloglog-merge.md deleted file mode 100644 index 334f33f..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/caching-and-redis/case/case-a10-f005-hyperloglog-merge.md +++ /dev/null @@ -1,215 +0,0 @@ ---- -kind: CASE -slug: a10-f005-hyperloglog-merge -title: 채워 넣은 상한은 자기가 잴 요청에서 값을 가져온다 -topic: caching-and-redis -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a10-f005-hyperloglog-merge -evidenceCapturedOn: 2026-09-02 -body: case-a10-f005-hyperloglog-merge.body.md -assets: - - key: a10-f005-hyperloglog-merge - file: ../../../final/evidence/rendered/a10-f005-hyperloglog-merge.svg - - key: a10-f005-hyperloglog-merge-scope - file: ../../../final/evidence/rendered/a10-f005-hyperloglog-merge-scope.svg - - key: a10-f005-hyperloglog-merge-budget - file: ../../../final/evidence/rendered/a10-f005-hyperloglog-merge-budget.svg -evidence: - - ../../../final/evidence/raw/a10-f005-hyperloglog-merge.txt - - ../../../final/evidence/raw/a10-f005-hyperloglog-merge-scope.txt - - ../../../final/evidence/raw/a10-f005-hyperloglog-merge-budget.txt -source: - - 원본 분석 절은 final/document.md#a10#L277 이다. 등급은 P3 이다. 다중 키 fan-in 표 다섯 줄 중 확률적 집계 둘만 서명에 예산 인자가 없다는 표, 두 명령의 비용이 입력 레지스터 수에 비례한다는 지적, 레지스터가 12KB 고정이라 폭발 범위가 좁다는 판단, 그리고 규칙의 예외가 이유 없이 존재한다는 문장이 그 절에 있다. 그 표의 첫 줄이 집합 대수 셋을 묶은 것이라 연산 수로는 일곱이다. - - 이 기록에서 확인한 것은 셋이다. 두 명령도 예산 없이는 관문을 통과하지 못하고, 그 예산은 SDK가 채우며, 채운다는 설계는 상한 타입의 첫 문단에 적혀 있다. - - 남는 문제는 원본이 든 것과 다르다. 예산을 만드는 세 메서드가 요청 바이트 상한을 잴 대상에서 그대로 가져오므로, 이 두 명령뿐 아니라 SDK가 예산을 채우는 R2 명령 전부에서 관문의 요청 바이트 검사가 발화하지 못한다. ---- - -# 채워 넣은 상한은 자기가 잴 요청에서 값을 가져온다 - -여러 키를 읽어 하나에 쓰는 연산 일곱 중 다섯은 호출자에게 비용 상한을 받고 둘은 받지 않는다. 그 둘도 예산 없이는 관문을 통과하지 못한다. SDK가 대신 만들어 넣는데, 그 상한의 요청 바이트 항목이 자기가 잴 요청의 크기에서 값을 가져온다. - -## 관계 - -- **상한을 주입받는 자리는 있고 주입하는 곳은 없다** - 같은 리프에서 상한을 받을 자리는 있는데 넣어 주는 코드가 없다. -- **미배선 인터셉터는 누락이 아니라 중복이다** - 두 사례 모두 빠진 것으로 읽은 자리에 실제로는 다른 형태의 구현이 있었다. -- **타입이 문서화한 불변식은 타입이 강제한다** - 예산 타입은 기본값으로 채워지지 않는다고 적어 두고 강제하지 않는다. - -## 문제 - -여러 키를 읽어 하나에 쓰고 비용이 입력 크기에 비례하는 연산은 이 계층에 일곱이다. 원본 표는 집합 대수 셋을 한 줄로 묶어 다섯 줄로 적었다. 그중 다섯에는 호출자가 비용 상한을 건네도록 서명에 인자를 두고, 확률적 집계 계열 둘에는 두지 않는다. - -인자가 없다는 것과 상한이 없다는 것이 같은 말인지 확인했다. - -## 결론 - -같은 말이 아니다. - -정책 카탈로그에서 두 명령은 R2 다. 관문은 R2 요청의 예산이 비어 있으면 거부한다. 예산 없이는 실행 자체가 안 된다. - -빌더가 예산을 직접 조립해 요청에 얹는다. 두 곳 모두 요소 수와 요청 바이트를 인자로 넘긴다. - -채워 넣는다는 것 자체는 적혀 있다. 상한을 모아 둔 타입의 첫 문단이 서명에 예산이 없을 때 적용하는 천장이라고 말하고, 호출자가 건넨 쪽이 언제나 이긴다고 덧붙인다. - -그 문단은 R2 전체를 설명하지 않는다. 비교 대상 셋과 해시 전체 읽기는 허가와 예산을 둘 다 호출자에게 받는다. 반대로 WATCH 와 BLPOP 은 둘 다 SDK가 자기에게 발급한다. 확률적 집계는 SMOVE 나 RENAME 이나 MGET 과 같은 자리에 있다. 허가만 호출자에게 받는 쪽이다. - -이것이 두 곳만의 방식도 아니다. src/main 에 이름이 나오는 R2 명령 쉰여섯 중 서른넷이 SDK 쪽이고 열넷이 호출자 서명 쪽이다. 나머지 여덟은 예산을 다른 파일에서 조립하거나 직접 만들어 이 스캔으로는 가리지 못했다. - -실행해 보면 요청 바이트 항목은 어떤 크기에서도 통과한다. 상한을 요청 크기에서 그대로 가져오기 때문이다. 렌더된 키는 타입 상한인 512 바이트를 넘지 못하고 기본 설정의 요소 천장이 1000 이라 이 경로의 요청은 512000 바이트 아래인데, 그 상한선에서도 예산의 상한은 같은 512000 이다. - -관문은 요소 수를 보지 않는다. 요소 수를 막는 것은 예산을 만드는 쪽이고, 그것도 예산이 만들어지기 전에 던진다. 관문에 도착한 예산이 실제로 기여하는 것은 타임아웃 하나다. - -같은 패키지에 반대 문장이 있다. 예산 타입의 첫 문단은 모든 R2 API가 예산을 요구하며 기본값으로 채워지지 않는다고 적는다. 두 문장 중 코드가 지키는 쪽은 상한 타입이다. - -판정은 P3 이되 이유가 다르다. 원본은 인자가 없는 것을 이유로 들었는데, 실제로 남는 문제는 채워 넣은 상한이 관문의 요청 바이트 검사를 발화시킬 수 없다는 것이고, 그것은 이 두 명령만의 일이 아니다. 예산을 만드는 세 메서드가 전부 같은 식을 쓴다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 서명과 정책 카탈로그 대조, 요청 빌더 추적, 실행 탐침 -소스 수정 : x - -## 재현 조건 - -1. 여러 키를 읽어 하나에 쓰는 연산 일곱의 서명을 나란히 읽는다. -2. 정책 카탈로그에서 그 명령들의 위험 등급을 확인한다. -3. 관문이 R2 요청의 빈 예산을 어떻게 처리하는지 읽는다. -4. 확률적 집계 요청 빌더가 예산을 어디서 얻는지 따라간다. -5. 예산을 만드는 세 메서드가 요청 바이트 항목에 무엇을 넣는지 읽는다. -6. 그 메서드를 여러 요청 크기로 불러 상한과 판정을 출력한다. -7. R2 명령마다 예산이 호출자 서명에서 오는지 SDK가 만드는지 가려 센다. - -## 본문 - - - -확률적 집계 계열의 다중 키 연산 둘은 호출자에게 비용 상한을 받지 않는다. 같은 성격의 다른 다섯은 받는다. - -## 인자가 없는 것과 상한이 없는 것 - -:::evidence key="a10-f005-hyperloglog-merge" alt="여러 키를 읽어 하나에 쓰는 연산 일곱의 서명 — 다섯에는 비용 상한 인자가 있고 둘에는 없다. 정책 카탈로그가 그 명령들에 매긴 위험 등급, 관문이 R2 요청의 빈 예산을 거부하는 구문, 확률적 집계 요청 빌더가 예산을 만들어 넣는 두 줄과 그 메서드가 상한을 정하는 세 줄, 같은 식을 쓰는 다른 두 메서드의 줄, 그리고 이 설계를 적어 둔 문단과 같은 패키지에서 반대로 적어 둔 문단을 출력한 터미널 기록." caption="일곱 중 다섯의 서명에만 OperationBudget 인자가 있고 확률적 집계 둘은 없다 · 카탈로그 등급은 전부 R2 · 관문은 R2 요청의 빈 예산을 거부하므로 요청 빌더가 예산을 만들어 넣는다 · 요청 바이트 상한을 요청 크기에서 가져오는 식이 세 메서드에 모두 있다 — 112줄 · exit 0" zoom="true" -::: - -```java -long count(Collection> keys, MultiKeyPermit permit); -void merge( - HyperLogLogKey destination, - Collection> sources, - MultiKeyPermit permit); -``` - -앞의 다섯에는 `OperationBudget budget` 이 마지막 인자로 붙어 있다. 이 둘에는 없다. 그런데 카탈로그에서 두 명령은 `R2` 이고, 관문은 이렇게 한다. - -```java -if (request.budget().isEmpty()) { - throw new RedisCommandRejectedException( - "R2 command requires permit and budget", metadata(policy, OptionalInt.empty())); -} -``` - -예산이 비면 실행되지 않는다. 빌더가 직접 조립해 얹는다. - -```text -69: Optional.of(context.collectionBudget(rendered.qualified().size(), rendered.requestBytes())), -93: Optional.of(context.collectionBudget(qualified.size(), size)), -``` - -## 채워 넣는다고 적힌 곳 - -```text -/** - * The ceilings the typed operations apply when the public signature does not carry a budget. - * - *

Design section 10 gives some R2 methods a caller-supplied {@code OperationBudget} and others a - * caller-supplied permit, but {@code CommandPolicyGuard} requires both for every R2 command. These - * limits are what the SDK fills in for the half the signature omits, so an R2 command is never - * admitted with an unbounded cost. The caller-supplied half always wins; this only supplies what - * the caller had no way to pass. - * -``` - -확률적 집계는 허가만 호출자에게 받고 예산은 SDK가 채운다. - -이 문단을 R2 전체의 규칙으로 읽으면 틀린다. 집합 대수와 비트 연산과 지리 검색 저장과 해시 전체 읽기는 허가와 예산을 둘 다 호출자에게 받는다. 반대로 `WATCH` 와 `BLPOP` 은 둘 다 SDK가 자기에게 발급한다 — `BLPOP` 의 서명에는 허가 인자가 아예 없다. `BLMOVE` 는 둘 다인 것처럼 보이지만 아니다. 호출자의 다중 키 허가를 문맥이 먼저 검증하고, 관문에는 정책 이름이 맞는 SDK 허가를 대신 건넨다. - -같은 방식이 R2 전반에 있다. - -:::evidence key="a10-f005-hyperloglog-merge-scope" alt="src/main 에 이름이 문자열로 나오는 R2 명령마다, 요청을 만드는 메서드의 서명이 비용 상한을 인자로 받는지 아니면 그 메서드 또는 같은 파일의 헬퍼에서 SDK가 만들어 넣는지 가려 센 터미널 기록. 두 단계까지 따라가고도 출처를 못 가린 명령은 따로 적는다." caption="R2 명령 56개 중 34개는 SDK가 예산을 만들고 14개는 호출자 서명이 받는다 · 나머지 8개는 예산이 다른 파일에 있어 미판정 — 25줄 · exit 0" zoom="true" -::: - -## 채워 넣은 예산이 막는 것 - -:::evidence key="a10-f005-hyperloglog-merge-budget" alt="예산을 만드는 메서드를 세 가지 요청 크기로 불러 얻은 요청 바이트 상한과 그 요청에 대한 판정, 이 경로의 요청이 가질 수 있는 최대치를 키 상한과 요소 천장에서 계산한 값, 같은 크기를 거부하는 호출자 예산 하나, 요소 수를 천장과 천장 초과로 부른 결과, 그리고 두 명령이 선언하는 예상 회신 크기에 대한 판정을 출력한 터미널 기록. 실행에 쓴 자바 판을 첫 줄에 함께 적는다." caption="요청 바이트 상한이 요청 크기와 같아 이 경로의 상한선 512000 바이트에서도 통과 · 호출자가 건넨 상한 4096 은 8192 요청을 거부 · 실제로 거부되는 것은 요소 천장 초과뿐이고 그 거부는 KEY 계열로 기록된다 — 16줄 · exit 0" zoom="true" -::: - -예산을 만드는 메서드는 요청 바이트 상한을 이렇게 정한다. - -```java -long replyCeiling = (long) elements * limits.maxReplyBytesPerElement(); -return new OperationBudget( - elements, Math.max(1L, requestBytes), replyCeiling, limits.collectionTimeout()); -``` - -상한이 요청 크기다. 관문이 그 둘을 비교한다. - -```text - 요청 512 -> maxRequestBytes 512 allowsRequestBytes(요청) true - 요청 4096 -> maxRequestBytes 4096 allowsRequestBytes(요청) true - 요청 512000 -> maxRequestBytes 512000 allowsRequestBytes(요청) true - 이 경로의 요청 최대 : 렌더된 키 512 바이트 x 요소 천장 1000 = 512000 바이트 -``` - -기본 설정에서 이 경로가 만들 수 있는 가장 큰 요청에서도 통과한다. 호출자가 건넨 상한은 다르다. - -```text - maxRequestBytes 4096, 요청 8192 -> allowsRequestBytes false -``` - -회신 검사도 지나가는데, 이쪽은 맞는 값이다. `PFCOUNT` 는 정수 하나, `PFMERGE` 는 `OK` 하나라 두 요청이 선언하는 `0L` 이 실제 크기다. - -거부되는 것은 요소 수 하나다. - -```text - 요소 1001개 -> RedisCommandRejectedException: operation over 1001 elements exceeds the configured ceiling of 1000 [command=KEY, mode=STANDALONE, ambiguous=false] -``` - -그 거부는 관문이 아니라 예산을 만드는 쪽에서, 예산이 만들어지기 전에 나온다. 그리고 `command=KEY` 다. 계열 이름이 `"KEY"` 로 고정돼 있어서, 병합 하나가 천장을 넘긴 일이 운영자에게는 키 연산으로 기록된다. 이 자리를 계열 이름으로 먼저 거르는 검사가 확률적 집계에는 없기 때문이다. - -관문까지 간 예산에서 실제로 쓰이는 것은 타임아웃뿐이다. - -## 두 명령만의 일이 아니다 - -```text -313: elements, Math.max(1L, requestBytes), replyCeiling, limits.collectionTimeout()); -336: Math.max(1L, requestBytes), -349: 1, Math.max(1L, requestBytes), limits.maxReplyBytesPerElement(), limits.scriptTimeout()); -``` - -예산을 만드는 메서드가 셋이고 셋 다 같은 식을 쓴다. 그러니 이 성질은 SDK가 예산을 채우는 R2 명령 서른넷 전부에 있다. - -## 반대로 적어 둔 곳 - -```text -/** - * Explicit bound a caller accepts for one advanced operation. - * - *

Every R2 API requires a budget. The budget is never optional and never defaulted, because the - * whole point is that the caller states the cost it is prepared to pay before Redis is asked. - */ -``` - -구현이 따르는 것은 상한 타입 쪽이다. - -## 확인하지 못한 것 - -실제 서버에 큰 병합을 보내 지연이 얼마나 커지는지 재지 않았다. - -512000 은 기본 설정에서의 상한선이다. 요소 천장은 설정에서 올릴 수 있고, 이 리비전에서 요청 문맥을 만드는 곳은 테스트뿐이라 배포에서 실제로 쓰이는 천장은 아직 없다. - -예산의 출처를 못 가린 여덟 명령은 확인하지 않았다. 그 여덟은 예산을 다른 파일의 공용 실행기에서 받거나 등록된 함수의 자체 한도로 직접 만든다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/caching-and-redis/case/case-a10-f006-requireidentifier.md b/docs/clean-architecture-backend-template/tech-log-studio/caching-and-redis/case/case-a10-f006-requireidentifier.md deleted file mode 100644 index 0f18032..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/caching-and-redis/case/case-a10-f006-requireidentifier.md +++ /dev/null @@ -1,216 +0,0 @@ ---- -kind: CASE -slug: a10-f006-requireidentifier -title: 지워도 test가 초록인 검사가 셋이다 -topic: caching-and-redis -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a10-f006-requireidentifier -evidenceCapturedOn: 2026-09-02 -body: case-a10-f006-requireidentifier.body.md -assets: - - key: a10-f006-requireidentifier - file: ../../../final/evidence/rendered/a10-f006-requireidentifier.svg - - key: a10-f006-requireidentifier-branch - file: ../../../final/evidence/rendered/a10-f006-requireidentifier-branch.svg -evidence: - - ../../../final/evidence/raw/a10-f006-requireidentifier.txt - - ../../../final/evidence/raw/a10-f006-requireidentifier-branch.txt -source: - - 원본 분석 절은 final/document.md#a10#L375 이다. 등급은 P3 이다. 문자 클래스가 메일과 전화 형태의 필수 문자를 이미 배제하므로 두 분기가 도달 불가라는 관찰, 대응 test 가 예외 타입만 보므로 그 사실을 가리지 않는다는 지적, 보안 효과는 그대로이고 잃는 것은 진단 품질과 검증 겹수의 착시라는 판단, 그리고 두 분기를 지우거나 검사 순서를 뒤집으라는 제안이 그 절에 있다. - - 이 기록이 더한 것은 셋이다. 두 입력에 실제로 돌아오는 메시지가 문자 클래스 메시지라는 실행 결과. 웹 토큰 분기는 도달하지만 그것을 겨냥한 test 입력이 접두 검사에도 걸려 지워도 초록이고, 혼자 잡는 것은 실제 토큰이 아니라 합성 값이라는 것. 그리고 네 메시지를 단언하는 곳이 저장소에 하나도 없다는 것이다. 원본이 도달 불가로 센 것은 둘이고, 지워도 test 가 초록인 것은 셋이다. ---- - -# 지워도 test가 초록인 검사가 셋이다 - -식별자 검증이 다섯 겹으로 보인다. 그중 둘은 앞선 문자 클래스 검사가 이미 걸러내 도달하지 않고, 하나는 도달하지만 그것을 겨냥한 test 입력이 뒤 검사에도 걸린다. 셋 다 지워도 test 는 초록이다. - -## 관계 - -- **커버리지 gate 둘이 나란히 있고, 하나는 발화할 수 없다** - 두 사례 모두 조건이 성립할 수 없어 그 분기가 실행되지 않는다. -- **그 속성을 보일 수 없는 대역 위에서 통과한 테스트는 증인이 아니다** - 예외 타입만 보는 단언은 어느 검사가 던졌는지 구분하지 않는다. -- **상한을 주입받는 자리는 있고 주입하는 곳은 없다** - 같은 리프의 다른 검증 사례다. - -## 문제 - -식별자 검증에 검사가 다섯 있다. 문자 클래스 하나와 구체적 형태 넷이다. 메일 주소, 웹 토큰, 국제 전화번호, 인증 재료 접두다. - -각 검사가 실제로 발화하는지, 그리고 발화한다면 어느 test 가 그것을 붙들고 있는지 확인했다. - -## 결론 - -문자 클래스는 첫 문자로 영숫자를 요구하고 이후 문자로 영숫자와 점과 물결과 밑줄과 붙임표를 허용한다. 길이는 128 이하다. - -거기에 골뱅이도 더하기도 없다. - -그래서 메일 분기는 도달하지 않는다. 골뱅이를 포함한 값은 첫 검사에서 탈락한다. 전화 분기도 도달하지 않는다. 국제 전화번호 패턴이 반드시 더하기로 시작하는데 더하기는 첫 문자로도 이후 문자로도 허용되지 않는다. - -실행으로 확인했다. 두 형태를 넣으면 돌아오는 메시지가 문자 클래스 메시지다. 무작위 입력 20만 개에서도 이 두 검사에서 갈린 값이 하나도 없다. - -웹 토큰 분기는 도달한다. 웹 토큰 패턴이 쓰는 문자를 문자 클래스가 모두 허용하므로, 26자에서 128자 사이이고 영숫자로 시작하는 토큰 형태는 첫 검사를 통과해 전용 검사에 닿는다. - -다만 그것이 잡는 것은 진짜 토큰이 아니다. 진짜 토큰은 늘 같은 세 글자로 시작하고 길이도 128자를 넘긴다. 그런 값은 접두 검사나 문자 클래스가 먼저 잡는다. 혼자 걸리는 값은 토큰 흉내를 낸 합성 문자열뿐이다. - -그리고 그것을 겨냥한 test 입력 하나가 다음 검사에도 걸린다. 그 값이 eyJ 로 시작해서 접두 검사가 같은 타입의 예외를 낸다. 웹 토큰 분기를 지워도 그 test 는 초록이다. - -남는 둘은 붙들려 있다. 문자 클래스 검사는 128자 초과 입력과 구분자 주입을 거부하는 test 가 붙들고, 접두 검사는 웹 토큰 형태가 아닌 두 입력이 붙든다. 둘 중 하나를 지우면 대응 test 가 빨개진다. - -저 네 메시지는 저장소에서 던지는 자리에만 있다. 단언하는 곳이 없다. - -판정은 P3 이다. - -거부는 그대로다. 세 형태는 여전히 전부 거부된다. - -잃는 것은 둘이다. 운영자가 받는 메시지가 구체 형태에서 문자 클래스로 내려앉는다. 그리고 세 분기가 test 에 붙들리지 않은 채 검증이 다섯 겹인 것처럼 보이게 만든다. - -원본은 두 분기를 지우고 문자 클래스 메시지에 그 의도를 포함시키거나, 검사 순서를 뒤집어 구체적 형태를 먼저 판정하는 수정을 제안했다. 뒤집는 쪽을 고르면 세 분기가 전부 발화한다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 문자 클래스와 후속 분기 대조, 실행 탐침, test 단언 대상 확인 -소스 수정 : x - -## 재현 조건 - -1. 식별자 검증의 다섯 검사를 순서대로 읽는다. -2. 각 검사가 쓰는 패턴 넷을 읽는다. -3. 각 형태 검사가 요구하는 필수 문자가 문자 클래스 안에 있는지 본다. -4. 이 메서드에 값을 넣는 test 를 모아 무엇을 단언하는지 확인한다. -5. 그 입력들을 그대로 넣고 실제로 돌아오는 메시지를 본다. -6. 각 입력이 다섯 중 어느 검사에 걸리는지 전부 표시한다. -7. 웹 토큰 분기에만 걸리는 값과, 실제 크기의 토큰을 각각 넣어 본다. -8. 무작위 입력으로 각 검사에서 갈린 수를 센다. -9. 저장소 전체에서 네 메시지가 나오는 곳을 센다. - -## 본문 - - - -검사는 아래 순서로 돈다. - -## 다섯 검사 - -:::evidence key="a10-f006-requireidentifier" alt="식별자 검증 메서드의 다섯 검사 전체와 그 검사들이 쓰는 정규식 넷, 이 메서드에 값을 넣는 test 다섯 개가 무엇을 단언하는지, 그리고 저장소 전체에서 네 예외 메시지가 나오는 곳을 파일 형식 제한 없이 센 터미널 기록." caption="검사 다섯은 문자 클래스·메일·웹 토큰·전화·접두 순 · test 다섯 개의 단언은 모두 예외 타입뿐 · 네 메시지는 던지는 자리 넷에만 있고 단언하는 곳이 없다 — 78줄 · exit 0" zoom="true" -::: - -```java -if (value == null || !IDENTIFIER.matcher(value).matches()) { - throw new IllegalArgumentException( - "identifier must be 1..128 characters of [A-Za-z0-9._~-] and must not contain a" - + " key separator"); -} -String lowerCase = value.toLowerCase(Locale.ROOT); -if (value.indexOf('@') >= 0) { - throw new IllegalArgumentException("identifier must not contain a mail address"); -} -if (JSON_WEB_TOKEN.matcher(value).matches()) { - throw new IllegalArgumentException("identifier must not contain a JSON web token"); -} -if (INTERNATIONAL_PHONE.matcher(value).matches()) { - throw new IllegalArgumentException("identifier must not contain a phone number"); -} -if (lowerCase.startsWith("bearer") || lowerCase.startsWith("eyj")) { - throw new IllegalArgumentException("identifier must not contain authentication material"); -} -``` - -첫 검사가 쓰는 문자 클래스에는 골뱅이도 더하기도 없다. - -```text - private static final Pattern IDENTIFIER = Pattern.compile("^[A-Za-z0-9][A-Za-z0-9._~-]{0,127}$"); - - private static final Pattern JSON_WEB_TOKEN = - Pattern.compile("^[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}$"); - - private static final Pattern INTERNATIONAL_PHONE = Pattern.compile("^\\+\\d[\\d.~-]{7,}$"); -``` - -## 넣어 보면 어느 메시지가 오는가 - -:::evidence key="a10-f006-requireidentifier-branch" alt="test 가 쓰는 여섯 입력을 실제 검증 메서드에 넣어 돌아온 메시지와, 각 입력이 다섯 검사 중 어디에 걸리는지 전부 표시한 표. 웹 토큰 분기에만 걸리는 합성 값, 밑줄로 시작하는 값, 실제 크기의 토큰. 그리고 무작위 입력 20만 개로 옮겨 적은 정규식과 실제 메시지를 대조하고 각 검사에서 갈린 수를 함께 센 터미널 기록." caption="메일·전화 입력이 받는 메시지는 문자 클래스 메시지 · 웹 토큰 test 입력은 접두 검사에도 걸리고 실제 크기 토큰은 문자 클래스에서 먼저 걸린다 · 무작위 20만 개에서 메일·전화 검사에서 갈린 입력은 0 — 27줄 · exit 0" zoom="true" -::: - -```text -입력 길이 걸리는 검사 전부 | requireIdentifier 가 낸 메시지 -test: 메일 18 문자클래스 메일 | identifier must be 1..128 characters of [A-Za-z0-9._~-] and must not contain a key separator -test: 전화 13 문자클래스 전화 | identifier must be 1..128 characters of [A-Za-z0-9._~-] and must not contain a key separator -test: eyj 20 접두 | identifier must not contain authentication material -test: bearer 19 접두 | identifier must not contain authentication material -test: JWT 57 JWT 접두 | identifier must not contain a JSON web token -test: 129자 129 문자클래스 | identifier must be 1..128 characters of [A-Za-z0-9._~-] and must not contain a key separator -``` - -메일과 전화 입력은 전용 검사 앞에서 이미 걸린다. 문자 클래스가 골뱅이와 더하기를 빼 놓았으니 그 둘에 도달할 값 자체가 없다. - -웹 토큰 입력은 전용 검사에 닿는다. 다만 같은 값이 다음 검사에도 걸린다 — `eyJ` 로 시작하기 때문이다. - -## 웹 토큰 분기가 혼자 잡는 것 - -```text -합성 JWT 26자 26 JWT | identifier must not contain a JSON web token -_로 시작 26 문자클래스 JWT | identifier must be 1..128 characters of [A-Za-z0-9._~-] and must not contain a key separator -실제 크기 JWT 238 문자클래스 JWT 접두 | identifier must be 1..128 characters of [A-Za-z0-9._~-] and must not contain a key separator -``` - -혼자 걸리려면 26자에서 128자 사이이고 영숫자로 시작하며 `eyJ` 로도 `bearer` 로도 시작하지 않아야 한다. 실제 토큰은 헤더가 `{"` 로 시작해 base64url 로 늘 `eyJ` 가 되고, 길이도 128자를 넘기 일쑤다. 그러니 이 분기가 혼자 잡는 것은 토큰을 닮은 합성 값이다. - -## 어느 검사를 지우면 test 가 빨개지는가 - -웹 토큰 분기는 일을 하지만 붙들고 있는 test 가 없다. 저 test 입력은 분기를 지워도 접두 검사가 같은 타입의 예외를 낸다. - -문자 클래스 검사와 접두 검사는 다르다. 129자 입력과 `1:2` 는 문자 클래스 검사가 없으면 아무 검사에도 안 걸리고, `eyJhbGciOiJIUzI1NiJ9` 와 `bearer-abcdefabcdef` 는 접두 검사 말고 걸리는 데가 없다. - -## 왜 test 가 이것을 못 잡는가 - -```java -assertThatThrownBy(() -> new RedisKeyName("user", "person@example.com")) - .isInstanceOf(IllegalArgumentException.class); -``` - -단언 대상이 예외 타입뿐이다. 어느 검사가 던졌는지 보지 않는다. - -```text - sdk/api/key/RedisKeyRules.java:67: throw new IllegalArgumentException("identifier must not contain a mail address"); - sdk/api/key/RedisKeyRules.java:70: throw new IllegalArgumentException("identifier must not contain a JSON web token"); - sdk/api/key/RedisKeyRules.java:73: throw new IllegalArgumentException("identifier must not contain a phone number"); - sdk/api/key/RedisKeyRules.java:76: throw new IllegalArgumentException("identifier must not contain authentication material"); -``` - -저장소 전체를 파일 형식 제한 없이 훑어도 네 메시지가 나오는 곳은 던지는 자리 넷뿐이다. - -## 옮겨 적은 정규식을 믿어도 되는가 - -표의 「걸리는 검사」 열은 소스에서 옮겨 적은 정규식으로 계산한다. 그 사본이 실제와 같은지 무작위 입력으로 대조했다. - -```text - 200000 / 200000 일치 - 문자클래스 에서 갈린 입력 144166 - 메일 에서 갈린 입력 0 - JWT 에서 갈린 입력 23462 - 전화 에서 갈린 입력 0 - 접두 에서 갈린 입력 30843 - 통과 1529 -``` - -문자 클래스와 웹 토큰과 접두는 각각 수만 번씩 갈렸다. 메일과 전화는 0 인데, 그게 이 기록의 결론이다. 도달할 값이 없으니 대조할 방법도 없다. - -## 잃는 것 - -거부는 그대로다. 세 형태 모두 거부된다. - -운영자가 보는 메시지가 달라진다. 메일 주소를 담지 말라는 문장 대신 문자 클래스 문장이 온다. - -그리고 세 분기가 검증을 다섯 겹처럼 보이게 만든다. - -## 확인하지 못한 것 - -세 분기를 실제로 지운 빌드로 전체 test 를 돌리지 않았다. 소스를 고치지 않는 것이 이 작업의 조건이다. 대신 각 입력이 걸리는 검사를 전부 표시해, 지운 뒤에도 같은 타입의 예외를 낼 검사가 남는지 확인했다. - -분기와 함께 쓰이지 않게 되는 패턴 필드까지 지운 빌드가 경고 없이 컴파일되는지도 확인하지 못했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/caching-and-redis/case/case-analysis-finding-a10-f003.md b/docs/clean-architecture-backend-template/tech-log-studio/caching-and-redis/case/case-analysis-finding-a10-f003.md deleted file mode 100644 index 2b0ca94..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/caching-and-redis/case/case-analysis-finding-a10-f003.md +++ /dev/null @@ -1,108 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a10-f003 -title: SDK가 선언한 두 진입점에 구현이 없다 -topic: caching-and-redis -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a10-f003 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a10-f003.body.md -assets: - - key: analysis-finding-a10-f003 - file: ../../../final/evidence/rendered/analysis-finding-a10-f003.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a10-f003.txt -source: - - 원본 분석 절은 final/document.md#a10#L251 이다. ---- - -# SDK가 선언한 두 진입점에 구현이 없다 - -동기와 반응형 진입점 인터페이스가 각각 열두 접근자를 선언한다. 개별 표면은 사십삼 종이 모두 구현되어 있는데 두 진입점을 구현하는 클래스는 하나도 없다. 대칭 테스트는 인터페이스끼리만 비교하므로 이것을 가리지 못한다. - -## 관계 - -- **README readiness 표가 있는 것을 없다고 적는다** - 같은 리프의 반대 방향 사례이고 원인은 같다. -- **미배선 경계가 문서에만 있고 compile 경로에서 닫히지 않는다** - 같은 형태의 절반 조립이다. -- **인터페이스끼리 비교하는 test는 구현의 부재를 못 본다** - 이 사례가 그 규칙의 형태다. - -## 문제 - -동기 진입점 인터페이스가 자신을 형 있는 API 로의 동기 진입점이라 소개하고, 반응형 인터페이스가 그 짝이다. - -두 인터페이스는 각각 열두 접근자를 선언한다. 값과 해시와 리스트와 집합과 정렬 집합과 비트맵과 비트 필드와 확률적 집계와 지리와 스트림과 키와 배치다. - -## 결론 - -둘 다 구현체가 없다. - -리프 전체의 구현 선언을 전수 조사했다. 개별 표면은 전부 구현되어 있다. 동기 스물여섯 종과 반응형 열일곱 종이다. - -그런데 두 진입점을 구현한다고 선언한 클래스는 0 건이다. - -main 안에서 두 타입을 이름으로 부르는 곳도 없다. 유일한 참조가 반응형 인터페이스 자바독의 링크 하나와 대칭 테스트의 반사 두 줄이다. - -결과적으로 이 SDK 를 쓰는 코드는 진입점을 얻을 수 없다. - -열두 표면을 각각 어디선가 따로 받아야 하고, 진입점이 약속하는 하나의 객체에서 형 있는 표면 전체는 존재하지 않는다. - -접근자를 추가하고 반응형 짝을 맞추는 규율은 실행되고 있다. 그 규율이 만드는 대상을 실제로 만드는 코드가 없다. - -판정은 P2 다. - -데이터 위험은 없다. 없는 타입은 잘못된 답을 주지 않는다. - -위험은 API 계약의 신뢰다. 이 리프의 공개 표면 중 가장 먼저 읽히는 두 타입이 구현되지 않은 상태이고, 대칭 테스트가 그 사실을 가리지 못한다. 인터페이스끼리만 비교하기 때문이다. - -같은 리프의 준비도 표 사례와 방향이 반대이면서 원인은 같다. 조립이 절반이다. - -수정은 이미 존재하는 구현들을 묶는 두 클래스를 추가하고, 대칭 테스트에 두 진입점이 구현을 가진다는 검사를 더하는 것이다. - -## 검증 환경 - -확인 방식 : 구현 선언 전수 조사와 이름 참조 검색 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/160 계열에 있다. - -1. 두 진입점 인터페이스의 접근자 목록을 확인한다. -2. 리프 전체에서 구현 선언을 전수 조사한다. -3. 두 진입점을 구현하는 클래스가 있는지 센다. -4. main 안에서 두 타입 이름을 검색한다. -5. 대칭 테스트가 무엇과 무엇을 비교하는지 확인한다. - -## 본문 - - - -SDK가 선언한 두 진입점에 구현이 없다. - -## SDK 가 선언한 두 진입점 - -:::evidence key="analysis-finding-a10-f003" alt="분석 문서 final/document.md#a10 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a10 발췌 — 15줄" zoom="true" -::: - -## 데이터 위험은 없다 - -없는 타입은 잘못된 답을 주지 않는다. P2. - -## 위험은 API 계약의 신뢰다 - -이 leaf의 공개 표면 중 가장 먼저 읽히는 두 타입이 구현되지 않은 상태이고, 대칭 test가 그 사실을 가리지 못한다 — 인터페이스끼리만 비교하기 때문이다. sub-scope 01의 §5와 방향이 반대이면서 원인은 같다: 조립이 절반이다. - -## 수정 - -이미 존재하는 26개 구현을 묶는 `LettuceRedisOperations` / `LettuceReactiveRedisOperations` 두 클래스를 추가하고, `ApiParityTest`에 "두 facade는 구현을 가진다"는 검사를 더하는 것이다. - -## 확인하지 못한 것 - -두 진입점이 과거에 구현체를 가졌는지 이력에서 확인하지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/caching-and-redis/case/case-analysis-finding-a10-f007.md b/docs/clean-architecture-backend-template/tech-log-studio/caching-and-redis/case/case-analysis-finding-a10-f007.md deleted file mode 100644 index e945d52..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/caching-and-redis/case/case-analysis-finding-a10-f007.md +++ /dev/null @@ -1,123 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a10-f007 -title: 패턴 구독의 R2 승인만 호출자가 아니라 배포에 대해 이루어진다 -topic: caching-and-redis -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a10-f007 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a10-f007.body.md -assets: - - key: analysis-finding-a10-f007 - file: ../../../final/evidence/rendered/analysis-finding-a10-f007.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a10-f007.txt -source: - - 원본 분석 절은 final/document.md#a10#L485 이다. ---- - -# 패턴 구독의 R2 승인만 호출자가 아니라 배포에 대해 이루어진다 - -같은 위험 등급의 연산 대부분은 호출자가 허가를 들고 오도록 서명이 요구한다. 패턴 구독만 서명에 허가 인자가 없고, SDK 가 자기 자신에게 발급한 허가를 쓴 뒤 버린다. 실제 효과는 배포가 그 정책을 켰는지 확인하는 것이다. - -## 관계 - -- **같은 위험 등급에 두 승인 모델이 있으면 차이를 문서가 적어야 한다** - 이 사례가 그 규칙의 형태다. -- **permit 정책 이름이 세 곳에 문자열로 존재하고 교차 검사가 없다** - 같은 리프의 허가 체계 사례다. -- **식별자 검증의 다섯 검사 중 둘은 도달할 수 없다** - 같은 리프의 다른 검증 사례다. - -## 문제 - -이 리프는 위험한 연산을 등급으로 나누고, 상위 등급 연산에 명시적 승인을 요구한다. - -그 승인이 어디서 오는지 연산별로 대조했다. - -## 결론 - -패턴 구독만 다르다. - -집합 연산 셋은 서명이 고급 연산 허가를 요구한다. 호출자가 들고 온다. - -키 훑기와 해시 항목 조회도 마찬가지다. - -비트 필드 실행은 예산이 필수이고 허가는 가드가 목록의 필수 정책으로 요구한다. - -패턴 구독은 서명에 허가 인자가 없다. - -대상을 계산하는 쪽이 부르는 것은 SDK 가 자기 자신에게 발급하는 경로다. - -발급 구현은 정책 이름이 배포의 활성 정책 목록에 없으면 던진다. 그러므로 실제 효과는 이 배포가 패턴 구독을 켰는지 확인하는 것이다. - -그리고 반환된 허가는 버려진다. - -즉 다른 상위 등급 연산은 호출 지점이 승인을 증명하는데, 패턴 구독은 배포가 켜 두었는지만 본다. - -자바독이 허가가 필요한 이유는 적는다. 그 확산 범위를 서버가 정한다는 것이다. - -그런데 그 허가가 호출자가 아니라 SDK 가 스스로 발급한 것이라는 약해진 보증은 적지 않는다. - -고급 연산 허가의 계약이 상위 등급 연산이 명시적으로 승인되었음을 증명한다는 것과 견주면 차이가 있다. - -판정은 P3 다. - -배포 수준 게이트는 실재하고 이름공간 봉쇄도 있으므로 열린 구멍은 아니다. - -기록하는 이유는 같은 위험 등급에 두 가지 다른 승인 모델이 적용되고 그 차이가 문서화되어 있지 않기 때문이다. - -수정은 둘 중 하나다. 패턴 구독 서명에 고급 연산 허가를 추가하거나, 자바독에 배포 수준 승인임을 명시하는 것이다. - -## 검증 환경 - -확인 방식 : 연산별 서명과 허가 발급 경로 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/163 계열에 있다. - -1. 상위 등급 연산 목록을 만든다. -2. 각 연산의 서명에 허가 인자가 있는지 확인한다. -3. 패턴 구독이 부르는 발급 경로를 확인한다. -4. 그 발급 구현이 무엇을 검사하는지 읽는다. -5. 반환된 허가가 어디에 쓰이는지 확인한다. - -## 본문 - - - -R2 연산마다 승인 모델이 다르다. - -| R2 연산 | 호출자가 permit을 들고 오는가 | -|---|---| -| `sets.difference/intersection/union` | **예** — 서명이 `AdvancedOperationPermit`을 요구 | -| `keys.scan` · `hashes.entries` | **예** | -| `bitFields.execute` | budget 필수, permit은 guard가 catalog의 `required-policy`로 요구 | -| `pubSub.patternSubscribe` | **아니오** — 서명에 permit 인자가 없다 | - -## AdvancedOperationPermit 참조 위치 - -:::evidence key="analysis-finding-a10-f007" alt="코드베이스에서 AdvancedOperationPermit 를 검색한 출력 31줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="AdvancedOperationPermit 코드베이스 검색 — 31줄 · exit 0" zoom="true" -::: - -## SDK가 자기 자신에게 발급하고 그 permit을 버린다 - -`patternTargets`가 부르는 `context.sdkPermit(PATTERN_SUBSCRIBE)`는 SDK가 자기 자신에게 발급하는 경로다. `ConfiguredRedisPolicyAuthority.issueAdvanced`는 정책 이름이 배포의 `enabledPolicies`에 없으면 던지므로 실제 효과는 "이 배포가 `pattern-subscribe`를 켰는가"를 확인하는 것이고, 반환된 permit은 **버려진다**. - -## javadoc이 적지 않는 것 - -permit이 필요한 이유("its fan-out is decided by the server")는 적지만, 그 permit이 호출자가 아니라 SDK가 스스로 발급한 것이라는 **약해진 보증**은 적지 않는다. `AdvancedOperationPermit`의 계약이 "proving that an R2 operation was explicitly approved"인 것과 견주면 차이가 있다. - -## 열린 구멍은 아니다 - -배포 수준 게이트는 실재하고 네임스페이스 봉쇄도 있다. 기록하는 이유는 같은 위험 등급에 두 가지 다른 승인 모델이 적용되고 그 차이가 문서화돼 있지 않기 때문이다. 수정은 `patternSubscribe` 서명에 `AdvancedOperationPermit`을 추가하거나, javadoc에 "배포 수준 승인"임을 명시하는 것이다. P3. - -## 확인하지 못한 것 - -정책을 끈 배포에서 패턴 구독이 실제로 거부되는지 실행하지 않았다. 발급 구현상 그 결과가 나온다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/caching-and-redis/case/case-analysis-finding-a10-f008.md b/docs/clean-architecture-backend-template/tech-log-studio/caching-and-redis/case/case-analysis-finding-a10-f008.md deleted file mode 100644 index fe0da40..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/caching-and-redis/case/case-analysis-finding-a10-f008.md +++ /dev/null @@ -1,110 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a10-f008 -title: permit 정책 이름이 세 곳에 문자열로 존재하고 교차 검사가 없다 -topic: caching-and-redis -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a10-f008 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a10-f008.body.md -assets: - - key: analysis-finding-a10-f008 - file: ../../../final/evidence/rendered/analysis-finding-a10-f008.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a10-f008.txt -source: - - 원본 분석 절은 final/document.md#a10#L502 이다. ---- - -# permit 정책 이름이 세 곳에 문자열로 존재하고 교차 검사가 없다 - -허가 정책 이름의 출처가 셋이다. 두 집합의 차분은 정확히 둘이고 양쪽 다 설명이 있다. 문제는 차분이 아니라 차분을 감지하는 장치가 없다는 것이다. 어느 쪽 오타도 빌드를 깨지 않는다. - -## 관계 - -- **패턴 구독의 승인만 호출자가 아니라 배포에 대해 이루어진다** - 같은 리프의 허가 체계 사례다. -- **문자열로 이어진 두 세계는 오타에서 조용히 갈라진다** - 이 사례가 그 규칙의 형태다. -- **catalog drift gate는 서버 메타데이터와 대조하지 Java 상수와 대조하지 않는다** - 감지 장치가 없는 이유다. - -## 문제 - -허가 정책 이름의 출처가 셋이다. - -연산 문맥의 공개 상수 열여덟 개, 명령 정책 설정 파일의 필수 정책 값 열여덟 개, 검색 확장의 비공개 상수 하나다. - -두 집합이 일치하는지 확인했다. - -## 결론 - -두 집합의 차분은 정확히 둘이고 양쪽 다 설명이 있다. - -지속 키 정책은 자바에만 있다. 해당 명령들이 하위 등급이라 목록의 필수 정책이 아니라 연산 문맥의 전용 요구 메서드가 강제한다. - -검색 인덱스 정책은 설정에만 있다. 색인 생성 명령의 필수 정책이고, 자바 쪽 짝은 연산 문맥이 아니라 검색 확장 패키지의 비공개 상수다. - -즉 차분 자체는 설명된다. - -문제는 다른 데 있다. 차분을 감지하는 장치가 없다. - -설정에 오타가 들어가면 그 명령은 아무도 발급받을 수 없는 정책을 요구하게 된다. - -자바 상수 쪽에 오타가 들어가면 발급 구현이 정책이 활성화되지 않았다고 던진다. - -어느 쪽도 빌드를 깨지 않는다. - -목록 표류 게이트는 설정을 서버 메타데이터와 대조한다. 자바 상수 집합과 대조하지 않는다. - -판정은 P3 다. - -확정은 다음 하위 범위로 이월한다. 정책 적재기 테스트가 정책 이름 집합을 검사하는지 그 범위에서 확인한다. - -## 검증 환경 - -확인 방식 : 세 출처의 문자열 집합 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/162 계열에 있다. - -1. 연산 문맥의 정책 상수를 모은다. -2. 명령 정책 설정 파일의 필수 정책 값을 모은다. -3. 두 집합의 차분을 계산한다. -4. 각 차분 항목의 이유를 확인한다. -5. 목록 표류 게이트가 무엇과 무엇을 대조하는지 확인한다. - -## 본문 - - - -정책 이름의 출처가 셋이다. - -| 출처 | 개수 | -|---|---| -| `RedisOperationContext`의 `public static final String` 상수 | 18 | -| `redis-command-policy.yml`의 `required-policy:` 값 | 18 | -| `LettuceRedisSearchOperations:31`의 private 상수 `SEARCH_INDEX` | 1 | - -## RedisCommandPolicyLoaderTest 참조 위치 - -:::evidence key="analysis-finding-a10-f008" alt="코드베이스에서 RedisCommandPolicyLoaderTest 를 검색한 출력 1줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RedisCommandPolicyLoaderTest 코드베이스 검색 — 1줄 · exit 0" zoom="true" -::: - -## 두 집합의 차분에는 설명이 있다 - -`162-...` §8.3 — `persistent-key`는 Java에만 있다(해당 명령들이 R1이라 catalog의 `required-policy`가 아니라 `RedisOperationContext.requirePersistentKeyPermit`이 강제한다, §34). `search-index`는 YAML에만 있다(`FT.CREATE`의 `required-policy`이고, Java 쪽 짝은 `sdk/extensions/search`의 private 상수다). - -## 문제는 차분이 아니라 감지 장치의 부재다 - -YAML에 `required-policy: bounded-collectoin-read`처럼 오타가 들어가면 그 명령은 아무도 발급받을 수 없는 정책을 요구하게 되고, Java 상수 쪽에 오타가 들어가면 `issueAdvanced`가 "policy is not enabled"로 던진다. 어느 쪽도 빌드를 깨지 않는다. catalog drift gate는 YAML을 **서버 메타데이터**와 대조하지, Java 상수 집합과 대조하지 않는다. P3 — 확정은 sub-scope 05로 이월한다. - -## 확인하지 못한 것 - -정책 적재기 테스트가 이름 집합을 검사하는지 확인하지 않았다. 다음 하위 범위로 이월한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-adapter-inbound-web-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-adapter-inbound-web-c01.md deleted file mode 100644 index c4b6623..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-adapter-inbound-web-c01.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-web-c01 -title: 상호배타성이 프로퍼티가 아니라 타입에서 온다 -topic: capability-and-disclosure-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-web-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-web-c01 - file: ../../../final/evidence/rendered/adapter-inbound-web-c01.svg - - key: adapter-inbound-web-c01-diagram - file: ../../../final/assets/diagrams/adapter-inbound-web-c01.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-web-c01.txt -source: - - 원본 분석 절은 final/document.md#a14#L116 이다. -module: adapter-inbound-web ---- - -# 상호배타성이 프로퍼티가 아니라 타입에서 온다 - -MVC와 WebFlux 자동설정 둘 다 `matchIfMissing = true`로 기본 켜짐이다. 두 아티팩트가 클래스패스에 함께 있어도 하나만 활성화되는 이유는 프로퍼티가 아니라 `@ConditionalOnWebApplication`의 타입 조건이다. - -## 본문 - - - -두 자동설정의 게이트는 이렇게 붙어 있다. - -```text -MVC: @ConditionalOnWebApplication(SERVLET) + @ConditionalOnProperty(backend.web.mvc.enabled, matchIfMissing = true) -WebFlux: @ConditionalOnWebApplication(REACTIVE) + @ConditionalOnProperty(backend.web.webflux.enabled, matchIfMissing = true) -``` - -둘 다 `matchIfMissing = true` — **기본 켜짐**이다. notification·messaging·cache-redis가 전부 `matchIfMissing = false`(옵트인)인 것과 반대인데, 이유가 다르다: 저쪽은 선택적 능력이고 이쪽은 웹 애플리케이션의 본체다. - -## 활성화를 가르는 것 - -:::evidence key="adapter-inbound-web-c01-diagram" alt="웹 애플리케이션 타입에서 MVC 자동설정과 WebFlux 자동설정으로 각각 화살표가 나가고 화살표에 SERVLET 과 REACTIVE 가 붙은 구조" caption="타입이 고르는 자동설정" zoom="false" -::: - -상호배타성은 프로퍼티가 아니라 `@ConditionalOnWebApplication`의 타입 수준에서 온다 — "a reactive application cannot accidentally activate the servlet filters even if both artifacts are on the classpath." - -## 등록되는 빈 수 - -두 자동설정이 등록하는 빈은 MVC 12개, WebFlux 11개다. `AutoConfiguration.imports`에는 이 둘만 있다. - -## 분석 원문의 게이트 비교 - -:::evidence key="adapter-inbound-web-c01" alt="분석 문서 final/document.md#a14 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a14 발췌 — 15줄" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-adapter-inbound-web-c17.md b/docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-adapter-inbound-web-c17.md deleted file mode 100644 index e10f14c..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-adapter-inbound-web-c17.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-web-c17 -title: ndjson 스위치 하나가 두 능력을 함께 켠다 -topic: capability-and-disclosure-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-web-c17 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-web-c17 - file: ../../../final/evidence/rendered/adapter-inbound-web-c17.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-web-c17.txt -source: - - 원본 분석 절은 final/document.md#a14#L1255 이다. -module: adapter-inbound-web ---- - -# ndjson 스위치 하나가 두 능력을 함께 켠다 - -`NDJSON`과 `JSON_SEQUENCE`는 enum에서 서로 다른 상수이고 각자 프로퍼티 이름을 갖는데, 실제 조건은 `ndjson` 하나뿐이라 둘이 함께 켜진다. - -## 본문 - - - -조건이 붙은 자리는 하나다. - -```java -// advanced/mvc/MvcStreamingExecutorConfiguration.java:14-15, 36-39 -/** Wires servlet-side record streaming: NDJSON and RFC 7464 JSON text sequences. */ -@ConditionalOnProperty(prefix = "backend.web.advanced.ndjson", name = "enabled", havingValue = "true") -``` - -`NDJSON`과 `JSON_SEQUENCE`는 enum에서 서로 다른 상수이고 각자 프로퍼티 이름을 갖는데, 실제로는 `ndjson` 스위치 하나가 둘을 함께 켠다. - -## javadoc이 금지한 형태 - -`WebAdvancedFeature`의 javadoc이 그 형태를 금지한다 — "A single switch would make those one decision." - -## WebAdvancedFeature 참조 위치 - -:::evidence key="adapter-inbound-web-c17" alt="코드베이스에서 WebAdvancedFeature 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="WebAdvancedFeature 코드베이스 검색 — 5줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-adapter-outbound-objectstorage-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-adapter-outbound-objectstorage-c01.md deleted file mode 100644 index e61ee1a..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-adapter-outbound-objectstorage-c01.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-objectstorage-c01 -title: 컴파일이 전부 끝난 뒤에야 생성이 시작된다 -topic: capability-and-disclosure-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-objectstorage-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-objectstorage-c01 - file: ../../../final/evidence/rendered/adapter-outbound-objectstorage-c01.svg - - key: adapter-outbound-objectstorage-c01-diagram - file: ../../../final/assets/diagrams/adapter-outbound-objectstorage-c01.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-objectstorage-c01.txt -source: - - 원본 분석 절은 final/document.md#a09#L63 이다. -module: adapter-outbound-objectstorage ---- - -# 컴파일이 전부 끝난 뒤에야 생성이 시작된다 - -`ObjectStorageProviderContribution`이 `describe`와 `create`의 계약을 나누고, `ObjectStorageCapabilityAssembler.assemble`이 그 순서를 코드 구조로 지킨다. 컴파일러 자체가 fail-closed다. - -## 본문 - - - -`ObjectStorageProviderContribution`이 두 메서드의 계약을 나눈다. - -> `describe` **must not resolve credentials, create files, clients, threads, or schedulers**. `create` owns cleanup of every partial allocation before it throws; after a successful return the assembler owns the returned lifecycle exactly once. - -## 컴파일과 생성의 순서 - -:::evidence key="adapter-outbound-objectstorage-c01-diagram" alt="설정 컴파일에서 provider 생성으로, 다시 조립된 능력으로 이어지는 왼쪽에서 오른쪽 흐름. 화살표에 검증된 바인딩과 수명주기 소유권이 붙어 있다" caption="컴파일과 생성의 순서" zoom="false" -::: - -`ObjectStorageCapabilityAssembler.assemble`이 그 순서를 지킨다 — `compiler.compile(settings)`가 **전부** 끝난 뒤(`:25`)에야 선택된 destination을 돌며 `contribution.create(provider)`를 부른다(`:48`). 그리고 도중에 실패하면 이미 만든 것을 **역순으로** 닫는다(`:53–56`). `AssembledCapability.close()`도 역순이고 `AtomicBoolean`으로 정확히 한 번만 실행된다. README의 "Settings compile fully before any selected provider creates a directory, client, thread, scheduler, or credential lookup"이 코드 구조로 성립한다. - -## ObjectStorageProviderContribution 참조 위치 - -:::evidence key="adapter-outbound-objectstorage-c01" alt="코드베이스에서 ObjectStorageProviderContribution 를 검색한 출력 39줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ObjectStorageProviderContribution 코드베이스 검색 — 39줄 · exit 0" zoom="true" -::: - -## 컴파일러가 거부하는 것 - -컴파일러 자체가 fail-closed다. 비활성이면 빈 바인딩을 돌려주고, 활성인데 provider·destination·default destination 중 하나라도 비면 거부한다. provider마다 `describe`가 돌려준 서술자와 설정을 **대조**한다 — providerType 일치, version 일치, `maximumObjectBytes`가 서술자 상한 이하, `chunkBytes`가 서술자 상한 이하. chunk는 추가로 `1 ≤ chunk ≤ min(maxObject, 16 MiB)`이고 `Integer.MAX_VALUE`를 넘지 못한다. - -destination은 route token 중복을 거부하고, 요구한 capability를 provider가 `SUPPORTED`로 신고하지 않으면 거부하며, `SCAN_CLEAN`을 요구하는데 scanner seam이 없으면 이름을 대며 거부한다. - -## 식별자 검증의 범위 - -`canonicalId`는 64자 이내, `[a-z0-9][a-z0-9_-]*`, 소문자, 그리고 **0x20–0x7e 밖 문자를 전부 거부**한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-adapter-outbound-objectstorage-c08.md b/docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-adapter-outbound-objectstorage-c08.md deleted file mode 100644 index 5fdc7eb..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-adapter-outbound-objectstorage-c08.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-objectstorage-c08 -title: 레지스트리가 덮는 것은 문서 주장이고 설정 경로가 아니다 -topic: capability-and-disclosure-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-objectstorage-c08 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-objectstorage-c08 - file: ../../../final/evidence/rendered/adapter-outbound-objectstorage-c08.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-objectstorage-c08.txt -source: - - 원본 분석 절은 final/document.md#a09#L560 이다. -module: adapter-outbound-objectstorage ---- - -# 레지스트리가 덮는 것은 문서 주장이고 설정 경로가 아니다 - -§41에서 "R0 경계가 문서에만 있다"고 적었다. §47을 반영해 정확히 다시 말하면, R0 경계는 문서 주장에 대해서는 기계 검사되지만 런타임 설정 경로는 그 검사를 거치지 않는다. - -## 본문 - - - -§41에서 "R0 경계가 문서에만 있다"고 적었다. §47을 반영해 정확히 다시 말한다. - -## 기계 검사의 대상이 무엇인가 - -R0 경계는 **문서 주장에 대해서는** 기계 검사된다(§47). 그러나 그 검사의 대상은 `docs/registries/object-storage-readiness.yaml`이고, `KNOWN_PROVIDERS`는 `filesystem-local-dev` 하나다. - -## S3ProviderBinding 참조 위치 - -:::evidence key="adapter-outbound-objectstorage-c08" alt="코드베이스에서 S3ProviderBinding 를 검색한 출력 19줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="S3ProviderBinding 코드베이스 검색 — 19줄 · exit 0" zoom="true" -::: - -## 레지스트리를 거치지 않는 경로 - -운영자가 `app.object-storage` 설정에 AWS provider용 qualification profile을 쓰면서 `DIRECT_UPLOAD` capability를 주장하는 경로는 이 레지스트리를 **거치지 않는다**. `S3ProviderBinding.compileProfiles`가 그 주장을 MinIO에 대해서만 거부하므로, AWS + DIRECT_* 조합은 여전히 compile을 통과하고 presigner를 할당한다(§41). - -따라서 §41의 판정은 유지되고 오히려 선명해진다 — 이 저장소에는 "이 카드는 R0"를 강제하는 장치가 이미 있는데, 런타임 설정 경로가 그 장치의 사정권 밖에 있다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-adapter-outbound-persistence-jpa-c24.md b/docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-adapter-outbound-persistence-jpa-c24.md deleted file mode 100644 index e1523c6..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-adapter-outbound-persistence-jpa-c24.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c24 -title: 질의 계층은 프레임워크가 아니라 세 단계의 정책층이다 -topic: capability-and-disclosure-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c24 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c24 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c24.svg - - key: adapter-outbound-persistence-jpa-c24-diagram - file: ../../../final/assets/diagrams/adapter-outbound-persistence-jpa-c24.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c24.txt -source: - - 원본 분석 절은 final/document.md#a05#L1610 이다. -module: adapter-outbound-persistence-jpa ---- - -# 질의 계층은 프레임워크가 아니라 세 단계의 정책층이다 - -`springdata`와 `querydsl`은 application-core의 repository contract를 대체하는 generic CRUD layer가 아니다. `JpaRepositoryFragmentSupport`에는 범용 `save/findAll/delete`가 없다. - -## 본문 - - - -현재 code shape는 대략 다음처럼 읽는 것이 맞다. `springdata`와 `querydsl`이 application-core의 repository contract를 대체하는 generic CRUD layer가 아니다. - -## 질의 계층의 세 단계 - -:::evidence key="adapter-outbound-persistence-jpa-c24-diagram" alt="응용 계층 질의 계약과 springdata 와 hibernate 가 위에서 아래로 쌓이고 위임 방향 화살표가 아래로 그려진 구조" caption="질의 계층의 세 단계" zoom="false" -::: - -`springdata/**`는 allowlisted sort, keyset assembly/predicate, fetch-plan catalog, bounded stream lifetime, Specification safety를 맡는다. `hibernate/**`는 provider/version facts, 실제 Statistics/JDBC batch evidence, statement naming, batch/bulk/stateless provider optimization을 맡는다. - -## JpaRepositoryFragmentSupport 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c24" alt="코드베이스에서 JpaRepositoryFragmentSupport 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="JpaRepositoryFragmentSupport 코드베이스 검색 — 5줄 · exit 0" zoom="true" -::: - -## 범용 CRUD가 없는 이유 - -`JpaRepositoryFragmentSupport`에는 범용 `save/findAll/delete`가 없고, domain-owned adapter가 필요한 query mechanism만 조합하게 설계돼 있다. 이 방향은 support matrix의 "platform-owned generic CRUD repository는 unsupported"와 일치한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-app-bootstrap-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-app-bootstrap-c01.md deleted file mode 100644 index 35e6833..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-app-bootstrap-c01.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -kind: CONCEPT -slug: app-bootstrap-c01 -title: 세 환경 검증기의 관심사가 서로 다르다 -topic: capability-and-disclosure-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:app-bootstrap-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: app-bootstrap-c01 - file: ../../../final/evidence/rendered/app-bootstrap-c01.svg -evidence: - - ../../../final/evidence/raw/app-bootstrap-c01.txt -source: - - 원본 분석 절은 final/document.md#a18#L179 이다. -module: app-bootstrap ---- - -# 세 환경 검증기의 관심사가 서로 다르다 - -`EnvironmentPostProcessor`가 셋 있지만 각각 스위치 값 문법, 프로파일, 능력 간 의존이라는 다른 관심사를 본다. 중복 아님. - -## 본문 - - - -`EnvironmentPostProcessor`가 셋이다. - -| 검증기 | 보는 것 | -|---|---| -| `MasterSwitchEnvironmentPostProcessor` | 스위치 값 문법 | -| `RuntimeEnvironmentProfileValidator` (93) | 프로파일 | -| `CapabilityDependencyEnvironmentValidator` (62 → `CapabilityDependencyValidator` 156) | 능력 간 의존 | - -## MasterSwitchEnvironmentPostProcessor 참조 위치 - -:::evidence key="app-bootstrap-c01" alt="코드베이스에서 MasterSwitchEnvironmentPostProcessor 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MasterSwitchEnvironmentPostProcessor 코드베이스 검색 — 3줄 · exit 0" zoom="true" -::: - -## 중복으로 보지 않은 이유 - -셋 다 `EnvironmentPostProcessor`라는 확장 지점을 공유할 뿐 관심사가 다르다. 중복 아님. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-app-bootstrap-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-app-bootstrap-c02.md deleted file mode 100644 index c253e1a..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-app-bootstrap-c02.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -kind: CONCEPT -slug: app-bootstrap-c02 -title: 런타임 멤버십이 결정하는 어댑터 활성화 스위치 범위 -topic: capability-and-disclosure-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:app-bootstrap-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: app-bootstrap-c02 - file: ../../../final/evidence/rendered/app-bootstrap-c02.svg -evidence: - - ../../../final/evidence/raw/app-bootstrap-c02.txt -source: - - 원본 분석 절은 final/document.md#a18#L185 이다. -module: app-bootstrap ---- - -# 런타임 멤버십이 결정하는 어댑터 활성화 스위치 범위 - -`src/config/architecture/modules.json`의 `runtime_memberships`가 인바운드 어댑터의 런타임 포함 여부를 결정한다. gRPC와 WebSocket은 build-only leaf라 런타임 멤버십이 없고, `MasterSwitch`·`env-keys.yaml`·`AdapterActivationReport`에 활성화 스위치가 없는 상태가 이 모델과 일치한다. - -## 본문 - - - -## build-only 전송의 런타임 멤버십 - -`adapter-inbound-grpc`와 `adapter-inbound-websocket`의 `runtime_memberships`는 비어 있다. `ConditionalTransportCompositionContractTest`도 두 leaf가 어떤 런타임에도 올라가지 않아야 한다는 계약을 강제한다. - -## MasterSwitch 참조 위치 - -:::evidence key="app-bootstrap-c02" alt="코드베이스에서 MasterSwitch 를 검색한 출력 17줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MasterSwitch 코드베이스 검색 — 17줄 · exit 0" zoom="true" -::: - -## 멤버십과 활성화 스위치의 관계 - -런타임에 포함되지 않는 build-only 어댑터에는 운영자가 켜고 끌 활성화 스위치가 필요하지 않다. 따라서 gRPC와 WebSocket이 `MasterSwitch`·`env-keys.yaml`·`AdapterActivationReport`에 없는 것은 누락이 아니라 런타임 멤버십 모델과 일치한다. - -## 별도로 남는 web 경계 - -`adapter-inbound-web`은 `["app-bootstrap", "sample-portfolio"]` 두 런타임에 포함된다. `backend.web.mvc.enabled`, `backend.web.webflux.enabled`, `backend.web.budgets.enabled`, `app.web-platform.durable-operations.enabled`는 `MasterSwitch`와 `env-keys.yaml` 341개 키에 포함되지 않는다. 이 경계는 같은 분석의 §4.1b에서 별도 finding으로 다룬다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-app-bootstrap-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-app-bootstrap-c03.md deleted file mode 100644 index 2b872e0..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-app-bootstrap-c03.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: app-bootstrap-c03 -title: .imports 여섯 줄 중 다섯이 능력 루트다 -topic: capability-and-disclosure-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:app-bootstrap-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: app-bootstrap-c03 - file: ../../../final/evidence/rendered/app-bootstrap-c03.svg -evidence: - - ../../../final/evidence/raw/app-bootstrap-c03.txt -source: - - 원본 분석 절은 final/document.md#a18#L264 이다. -module: app-bootstrap ---- - -# .imports 여섯 줄 중 다섯이 능력 루트다 - -`.imports`의 여섯 항목 중 다섯이 능력 루트이고 하나(`AdapterActivationAutoConfiguration`)가 자기 액추에이터다. `MasterSwitch`의 다섯과 일치하지만 `PERSISTENCE_MONGO`만 루트가 없다. - -## 본문 - - - -`.imports`의 여섯 항목 중 다섯이 능력 루트이고 하나(`AdapterActivationAutoConfiguration`)가 자기 액추에이터다. `MasterSwitch`의 다섯과 일치한다. - -## AdapterActivationAutoConfiguration 참조 위치 - -:::evidence key="app-bootstrap-c03" alt="코드베이스에서 AdapterActivationAutoConfiguration 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="AdapterActivationAutoConfiguration 코드베이스 검색 — 2줄 · exit 0" zoom="true" -::: - -## PERSISTENCE_MONGO만 루트가 없다 - -`PERSISTENCE_MONGO`는 `.imports`에 루트가 없고 컴포넌트 스캔 제외 정규식(`adapter\.outbound\.mongo\..*`)으로만 관리된다. §7.1. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-grpc-advanced-diagnostics-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-grpc-advanced-diagnostics-c02.md deleted file mode 100644 index 825fc78..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-grpc-advanced-diagnostics-c02.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -kind: CONCEPT -slug: grpc-advanced-diagnostics-c02 -title: 진단 열람은 망 게이트와 역할 게이트를 함께 요구한다 -topic: capability-and-disclosure-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:grpc-advanced-diagnostics-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-advanced-diagnostics-c02 - file: ../../../final/evidence/rendered/grpc-advanced-diagnostics-c02.svg - - key: grpc-advanced-diagnostics-c02-diagram - file: ../../../final/assets/diagrams/grpc-advanced-diagnostics-c02.svg -evidence: - - ../../../final/evidence/raw/grpc-advanced-diagnostics-c02.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-advanced-diagnostics#L51 이다. -module: grpc-advanced-diagnostics ---- - -# 진단 열람은 망 게이트와 역할 게이트를 함께 요구한다 - -`GrpcChannelDiagnosticsPolicy`는 네트워크와 역할 두 게이트를 모두 요구하고, 하나라도 비면 생성자가 거부한다. CSDS는 그 위에 xDS 사용 여부라는 조건이 더 붙는다. - -## 본문 - - - -`GrpcChannelDiagnosticsPolicy` 는 네트워크와 역할 두 게이트를 모두 요구하고, 하나라도 비면 생성자가 거부한다. - -> "diagnostics need both a network and a role gate; Channelz holds every socket's peer and security detail, so either gate alone is the whole surface" - -## 열람을 여는 조건 - -:::evidence key="grpc-advanced-diagnostics-c02-diagram" alt="정책 경계 안에 네트워크 게이트와 역할 게이트가 나란히 들어 있고 CSDS 는 경계 밖에 점선 상자로 놓인 구조" caption="열람을 여는 조건" zoom="false" -::: - -## GrpcChannelDiagnosticsPolicy 참조 위치 - -:::evidence key="grpc-advanced-diagnostics-c02" alt="코드베이스에서 GrpcChannelDiagnosticsPolicy 를 검색한 출력 12줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcChannelDiagnosticsPolicy 코드베이스 검색 — 12줄 · exit 0" zoom="true" -::: - -## CSDS가 등록되는 조건 - -등록 판정이 능력 깃발에 걸려 있다. CSDS 는 Channelz 가 켜져 있고 xDS 도 켜져 있을 때만 등록된다. - -> "A CSDS service on a deployment that does not use xDS answers every query with nothing, which is harmless, and advertises a control-plane surface that does not exist, which is not." - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-messaging-kafka-share-experimental-c07.md b/docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-messaging-kafka-share-experimental-c07.md deleted file mode 100644 index 930738a..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-messaging-kafka-share-experimental-c07.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-kafka-share-experimental-c07 -title: 이전 결함 대신 막으려는 것 셋을 적는다 -topic: capability-and-disclosure-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-kafka-share-experimental-c07 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-kafka-share-experimental-c07 - file: ../../../final/evidence/rendered/messaging-kafka-share-experimental-c07.svg -evidence: - - ../../../final/evidence/raw/messaging-kafka-share-experimental-c07.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-kafka-share-experimental#L399 이다. -module: messaging-kafka-share-experimental ---- - -# 이전 결함 대신 막으려는 것 셋을 적는다 - -이 leaf의 javadoc에는 이전 결함 서술이 없다. 다른 messaging leaf 대부분이 "X used to …" 형태의 기록을 갖는 것과 대비되며, 대신 막으려는 것을 셋 적는다. - -## 관계 - -- **허용 의존 목록은 상한이므로 미사용을 잡지 않는다 — 그것을 잡으려면 별도 검사가 필요하다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **에러 메시지가 지시하는 설정은 그 설정을 읽는 코드와 함께 존재해야 한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **구성 오류는 한 예외 타입과 안정 코드로 보고한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -이 leaf의 javadoc에 **이전 결함 서술이 없다.** 다른 messaging leaf 대부분이 "X used to …" 형태의 기록을 갖는 것과 대비된다. 대신 **막으려는 것**을 셋 적는다. - -| 위치 | 막으려는 것 | -|---|---| -| `KafkaShareProfileValidator` | 순서 목적지를 share group에 설정 → 브로커가 주지 않는 보장을 광고 | -| 같은 곳 | experimental이 기본 켜져 Stable 배포로 drift | -| `KafkaShareGroupRegistrar` | pause를 조용히 무시 → pause에 의존하는 retry 정책이 동작하는 것처럼 보이며 아무것도 하지 않음 | - -## MessagingCapabilityUnavailableException 참조 위치 - -:::evidence key="messaging-kafka-share-experimental-c07" alt="코드베이스에서 MessagingCapabilityUnavailableException 를 검색한 출력 37줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingCapabilityUnavailableException 코드베이스 검색 — 37줄 · exit 0" zoom="true" -::: - -## 거절이 무시보다 낫다는 원칙이 빠진 자리 - -세 번째가 이 leaf에서 가장 성숙한 판단이다 — **거절이 무시보다 낫다**는 원칙이고, `messaging-core-api`의 `MessagingCapabilityUnavailableException` javadoc과 같은 계열이다. 역설적으로 **그 원칙이 `register(...)`에는 적용되지 않았다** — spec을 받아 무시하고 성공을 반환한다(§17). - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-messaging-testkit-c05.md b/docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-messaging-testkit-c05.md deleted file mode 100644 index 68375f4..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/capability-and-disclosure-models/concept/concept-messaging-testkit-c05.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-testkit-c05 -title: 등급을 판정하는 경로가 증거를 만드는 경로 없이도 돈다 -topic: capability-and-disclosure-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-testkit-c05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-testkit-c05 - file: ../../../final/evidence/rendered/messaging-testkit-c05.svg -evidence: - - ../../../final/evidence/raw/messaging-testkit-c05.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-testkit#L452 이다. -module: messaging-testkit ---- - -# 등급을 판정하는 경로가 증거를 만드는 경로 없이도 돈다 - -실행 경로가 셋이다. 등급 판정(경로 C)은 컨테이너 없이 매 빌드 돌고, 그 판정이 읽는 증거를 만드는 경로 B는 컨테이너를 요구해 `test`에서 제외돼 있다. - -## 본문 - - - -실행 경로가 셋이고 컨테이너 요구가 서로 다르다. - -**경로 A — 어댑터 계약 실행 (컨테이너 불필요, 항상 실행)** `MessagingAdapterHarness extends AutoCloseable` 이고 `close()` 가 checked exception 을 던지지 않도록 재선언되어 있다(`MessagingAdapterHarness.java:71-72`). 7개 테스트 전부 `try (…)` 로 감싸므로 하니스 누수 경로가 없다. - -**경로 B — 인증 증거 생산 (컨테이너 필요, `test` 에서 제외)** - -**경로 C — 등급 판정 (컨테이너 불필요, 매 빌드)** - -## 이 기록이 다루는 파일 범위 - -:::evidence key="messaging-testkit-c05" alt="코드베이스에서 파일 목록을 만든 출력 13줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 13줄 · exit 0" zoom="true" -::: - -## 경로 C가 경로 B 없이 도는 것이 설계의 핵심이다 - -경로 C 가 경로 B 없이도 돌고, 경로 B 가 없으면 매니페스트가 비어 등급 주장이 무너진다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/capability-declaration-vs-proof/case/case-a-delayed-delivery-flag-without-the-topology-that-delivers-it.md b/docs/clean-architecture-backend-template/tech-log-studio/capability-declaration-vs-proof/case/case-a-delayed-delivery-flag-without-the-topology-that-delivers-it.md deleted file mode 100644 index dc5fbd6..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/capability-declaration-vs-proof/case/case-a-delayed-delivery-flag-without-the-topology-that-delivers-it.md +++ /dev/null @@ -1,103 +0,0 @@ ---- -kind: CASE -slug: a-delayed-delivery-flag-without-the-topology-that-delivers-it -title: 지연 배달을 참으로 선언하는데 그 지연을 제공할 토폴로지가 조립되지 않는다 -topic: capability-declaration-vs-proof -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a-delayed-delivery-flag-without-the-topology-that-delivers-it -evidenceCapturedOn: 2026-09-02 -assets: - - key: a-delayed-delivery-flag-without-the-topology-that-delivers-it - file: ../../../final/evidence/rendered/a-delayed-delivery-flag-without-the-topology-that-delivers-it.svg -evidence: - - ../../../final/evidence/raw/a-delayed-delivery-flag-without-the-topology-that-delivers-it.txt -source: - - 원본 분석은 Rabbit 어댑터 문서 §17.4 다. 성분 위치와 소비 사슬, 큐 선언 코드의 부재는 위 자산에서 확인할 수 있다. ---- - -# 지연 배달을 참으로 선언하는데 그 지연을 제공할 토폴로지가 조립되지 않는다 - -Rabbit 어댑터가 지연 배달을 참으로 선언한다. 그 지연을 만드는 큐를 선언하는 코드는 저장소에 없다. - -## 관계 - -- **능력 선언의 세 출처와 그것이 파생되지 않을 때** -- **능력 선언은 프로파일에서 파생되어야 하고 상수는 그것을 할 수 없다** -- **선택할 수 없는 브로커가 지원 매트릭스에 기능 목록과 함께 실려 있다** - -## 문제 - -능력 선언은 재시도 엔진이 읽는 값이다. 지연 배달이 참이면 브로커에게 지연을 맡기는 재시도 모드를 고를 수 있다. - -RabbitMQ 의 코어 브로커에는 메시지별 지연이 없다. 지연 교환 플러그인을 설치하거나, 메시지 수명과 데드레터 라우팅으로 대기 큐를 만들어야 한다. 둘 다 토폴로지 선언을 요구한다. - -## 결론 - -선언과 그것을 뒷받침할 큐 사이가 비어 있다. - -이 어댑터는 그 큐를 어떻게 만드는지 이미 기술해 두었다. 그런데 그 기술을 참조하는 파일이 자기 자신과 시험 하나뿐이고, 큐를 실제로 선언하는 코드는 저장소 전체에 없다. - -값을 읽는 엔진은 조립되어 있다. 지금 그 값이 엔진까지 닿지 않는 이유와, 닿더라도 남는 문제는 본문이 다룬다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Gradle : 9.0.0 -확인 방식 : 능력 성분의 위치 확인, 값을 읽는 엔진과 그 조립 지점 확인, 재시도 결정 소비자의 인자 확인, 큐 선언 코드 검색 -소스 수정 : x - -## 재현 조건 - -1. 능력 record 의 성분 순서에서 지연 배달이 몇 번째인지 확인하고, Rabbit 전송이 그 자리에 넘기는 값을 읽는다. -2. 그 값을 읽는 조건문과 그 엔진이 빈으로 등록되는 지점을 확인한다. -3. 재시도 결정을 소비하는 코드가 지연 값을 어떻게 다루는지 확인한다. -4. 지연 큐를 기술하는 타입을 참조하는 파일과, 큐를 선언하는 코드를 각각 검색한다. - -## 본문 - - - -능력 record 는 열두 개의 불리언을 위치로 받고, 여덟째가 지연 배달이다. Rabbit 전송이 그 자리에 참을 넘긴다. - -## 소비 사슬을 끝까지 따라가면 세 군데가 끊겨 있다 - -:::evidence key="a-delayed-delivery-flag-without-the-topology-that-delivers-it" alt="코드베이스에서 능력 성분의 여덟째 자리와 Rabbit 이 넘기는 값, 그 값을 읽는 엔진과 엔진의 조립 지점, 재시도 결정의 유일한 소비자, 지연 큐 타입을 참조하는 파일, 큐 선언 코드 매치 수를 뽑은 출력 23줄. 엔진이 빈으로 등록되고 소비자가 지연 값을 넘기지 않으며 큐를 선언하는 코드가 0 이라는 것이 그 출력에 그대로 보인다." caption="여덟째 성분 · 엔진과 조립 지점 · 결정 소비자 · 지연 큐 참조 · 큐 선언 매치 0 — 23줄 · exit 0" zoom="true" -::: - -`DefaultRetryDecisionEngine` 이 64행에서 그 값을 읽는다. 그리고 그 엔진은 자동 설정이 빈으로 등록한다 — 오늘 조립되어 돌고 있다. - -끊긴 곳은 그 앞이다. Rabbit 은 전송을 출하하지 않아서 Rabbit 의 능력 record 가 엔진까지 도달하지 못한다. 스타터의 브로커 선택이 rabbit 을 이름으로 거절하고, 이유를 문장으로 적는다 — 검증기와 보안 설정은 출하하지만 전송이 없어 발행이 탈 것이 없다는 것이다. - -## 지연을 만드는 방법은 이미 기술되어 있다 - -`RabbitRetryQueueTopology` 가 대기 큐를 기술한다. javadoc 이 왜 필요한지부터 적는다 — 코어 브로커에 메시지별 지연이 없으므로, 재시도 큐는 메시지 수명이 걸린 큐이고 그 데드레터 교환이 작업 큐를 다시 가리킨다. 메시지는 수명이 다할 때까지 앉아 있다가 다시 라우팅된다. - -플러그인 뒤에 숨기지 않고 명시적으로 모델링한 이유도 적는다 — 그래야 동작이 검토 가능하다는 것이다. 함정까지 같이 적는다. 수명 만료는 큐 머리에서 평가되므로, 한 재시도 큐에 서로 다른 지연이 섞이면 각자 독립적으로 만료되지 않는다. - -그 타입을 참조하는 파일은 자기 자신과 시험 하나다. 그리고 큐를 선언하는 코드를 이름으로 찾으면 매치가 0 이다. - -## 배선해도 지연은 아직 흐르지 않는다 - -전송을 구현하는 것만으로 끝나지 않는다. - -재시도 결정은 목적지와 지연을 함께 담는다. 그런데 그 결정을 소비하는 production 코드가 하나뿐이고 — Kafka 쪽 실행기다 — 그 실행기는 목적지만 넘기고 **지연 값을 넘기지 않는다.** - -Rabbit 에는 대응하는 실행기가 없다. 그러니 배선하는 쪽이 해야 할 일은 전송 구현과 재시도 실행기와 큐 선언 셋이고, 그중 어느 하나만 해도 이 플래그는 여전히 참이다. - -## 상수는 아무것도 강제하지 않는다 - -이 플래그는 프로파일에서 파생된 값이 아니라 소스에 박힌 상수다. 큐가 선언되었는지, 플러그인이 설치되었는지 보지 않는다. - -그래서 위의 세 가지 중 무엇이 언제 채워지든 이 값은 바뀌지 않고, 바꿔야 한다고 알려 주는 것도 없다. - -## 오늘 무엇이 이 결함을 막고 있나 - -Rabbit 능력이 엔진에 닿지 않는다는 것 하나다. 어댑터 자신이 아니라 그 위의 배선 부재가 막고 있다. - -## 확인하지 못한 것 - -실제 배달 시점은 브로커를 띄워 확인해 보지 못했다. 큐 선언의 부재는 이름 기반 검색으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/capability-declaration-vs-proof/case/case-a-transaction-capability-true-and-its-validator-never-run.md b/docs/clean-architecture-backend-template/tech-log-studio/capability-declaration-vs-proof/case/case-a-transaction-capability-true-and-its-validator-never-run.md deleted file mode 100644 index aaa6fac..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/capability-declaration-vs-proof/case/case-a-transaction-capability-true-and-its-validator-never-run.md +++ /dev/null @@ -1,133 +0,0 @@ ---- -kind: CASE -slug: a-transaction-capability-true-and-its-validator-never-run -title: 브로커 트랜잭션을 무조건 참으로 선언하고, 그 조건을 검사하는 검증기는 기동 시 돌지 않는다 -topic: capability-declaration-vs-proof -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a-transaction-capability-true-and-its-validator-never-run -evidenceCapturedOn: 2026-09-02 -assets: - - key: a-transaction-capability-true-and-its-validator-never-run - file: ../../../final/evidence/rendered/a-transaction-capability-true-and-its-validator-never-run.svg -evidence: - - ../../../final/evidence/raw/a-transaction-capability-true-and-its-validator-never-run.txt -source: - - 분석 문서는 메시징 플랫폼 편 §3.5 다. 그 절이 프로파일 검증기 여덟 개의 도달성을 세고, 조립에서 실행되는 셋을 적는다. 실행되지 않는 다섯 중 넷은 빌드 전용 모듈에 있어 조립 지점이 없는 것이 등급과 일치한다. 출하되는 모듈에서 조립되지 않은 것은 이 트랜잭션 검증기 하나뿐이고, 그래서 이 항목이 P2 다. - - 능력 상수의 아홉째가 프로파일과 무관한 상수라는 것은 Kafka 어댑터 편이고, 아홉째와 열째의 독자 수 대비는 같은 플랫폼 편 §3.4 의 표에 있다. ---- - -# 브로커 트랜잭션을 무조건 참으로 선언하고, 그 조건을 검사하는 검증기는 기동 시 돌지 않는다 - -Kafka 어댑터의 능력 상수가 브로커 트랜잭션을 프로파일과 무관하게 참으로 답한다. 그 조건을 검사하는 검증기는 스타터가 빈으로 만들지만 기동 검증에 감싸지 않아 실행되지 않는다. - -## 관계 - -- **능력 선언의 세 출처와 그것이 파생되지 않을 때** - 이 사례가 속한 구조다. -- **검증기는 발행이 아니라 주입이 강제다** - 이 사례의 두 번째 절반에 해당하는 규칙이다. -- **능력 선언은 프로파일에서 파생되어야 하고 상수는 그것을 할 수 없다** - 첫 번째 절반에 해당하는 규칙이다. - -## 문제 - -Kafka 트랜잭션은 조건 다섯이 모두 맞아야 활성화된다. - -그 다섯을 전부 보는 클래스가 이 저장소에 있다. - -## 결론 - -능력 상수가 프로파일을 보지 않는다. 열두 성분 중 아홉째 자리가 고정으로 참이고 그 선언에 프로파일 참조가 없다. - -그 플래그를 읽는 프로덕션 코드는 0 이다. 바로 옆 열째 플래그는 발행 경로가 읽는데, 그 플래그는 일부러 거짓으로 내려져 있다. 그 자리 javadoc 이 이유를 적는다 — 참으로 선언하면 호출자가 브로커가 중복을 제거한다고 믿고 자기 멱등성을 만들지 않는다는 것이다. - -아홉째의 과대 선언이 오늘 낳는 결과는 조회 경로의 피해와 다르다. - -두 번째 절반이 검증 경로다. 스타터가 트랜잭션 검증기를 빈으로 발행하지만 기동 검증에 감싸지 않는다. 자동설정 바깥에서 이것을 아는 코드는 하나도 없다. 다섯 규칙이 어디에서도 실행되지 않는다. - -감쌀 수 없는 이유는 인자 수가 아니다. 래퍼는 소비자 함수를 받으므로 나머지를 캡처하는 람다면 들어간다. 두 번째 인자가 그것을 막는다. 트랜잭션 식별자 접두는 이 저장소의 main 에서 이 검증기의 파라미터 이름으로만 존재한다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Gradle : 9.0.0 -확인 방식 : 능력 상수의 성분 위치와 독자 계수, 검증기의 규칙과 언급 계수, 래퍼 시그니처와 인자 출처 확인 -소스 수정 : x - -## 재현 조건 - -1. 능력 record 에서 브로커 트랜잭션이 몇 번째인지 확인하고, Kafka 가 그 자리에 넘기는 값과 그 선언의 프로파일 참조를 확인한다. -2. 그 플래그를 읽는 프로덕션 코드를 세고, 옆 열째 플래그와 대조한다. -3. 트랜잭션 검증기가 요구하는 조건을 전부 나열한다. -4. 기동 검증으로 감싸이는 검증기와 직접 불리는 검증기를 확인한다. -5. 트랜잭션 검증기를 언급하는 프로덕션 코드와 테스트를 센다. -6. 래퍼의 시그니처와 두 번째 인자의 출처를 확인한다. - -## 본문 - - - -Kafka 트랜잭션은 생산자 설정 넷과 목적지 선언 하나가 동시에 맞아야 성립한다. 그중 어느 하나라도 어긋나면 커밋 경계가 갈라진다. - -이 저장소에는 그 다섯을 전부 검사하는 클래스가 있다. 스타터가 그것을 빈으로 만든다. 그리고 아무도 그것을 부르지 않는다. - -## 아홉째 자리가 프로파일을 보지 않는다 - -:::evidence key="a-transaction-capability-true-and-its-validator-never-run" alt="코드베이스에서 능력 record 의 아홉째 성분과 Kafka 가 그 자리에 넘기는 값과 그 상수의 프로파일 참조 수, 그 플래그를 읽는 코드 수와 바로 옆 열째 플래그를 읽는 코드와 그 열째가 거짓인 이유를 적은 javadoc, 트랜잭션 검증기가 요구하는 다섯 조건, 기동 검증으로 감싸이는 검증기와 직접 불리는 검증기와 감싸이지 않은 채 빈으로만 발행되는 트랜잭션 검증기와 그것을 언급하는 코드 수, 그리고 감쌀 수 없는 진짜 이유인 래퍼 시그니처와 두 번째 인자의 출처를 뽑은 출력 54줄. 아홉째 플래그를 읽는 코드가 0 이고 열째는 읽히면서 일부러 거짓이라는 대비가 그 출력에 보인다." caption="아홉째 성분과 그 값 · 읽는 코드 0 · 옆 열째는 읽히고 거짓 · 검증기의 다섯 조건 · 감싸인 것과 아닌 것 · 두 번째 인자의 출처 없음 — 54줄" zoom="true" -::: - -능력 record 는 열두 개의 불리언을 위치로 받고, 아홉째가 브로커 트랜잭션이다. Kafka 전송이 그 자리에 참을 넘기고, 그 선언에 프로파일 참조는 0 이다. - -트랜잭션 식별자 없이 구성된 배포도 같은 답을 받는다. - -## 옆자리가 이 플래그의 무게를 보여 준다 - -이 아홉째 플래그를 읽는 프로덕션 코드가 0 이다. - -바로 옆 열째는 다르다. 발행 경로가 그 값을 읽어 판단한다. 그리고 Kafka 는 그 자리에 거짓을 넘긴다. - -그 자리 javadoc 이 왜 거짓인지 적는다. 참으로 선언하면 중복 제거 요청이 받아들여진 뒤 조용히 아무 일도 하지 않고, 호출자는 브로커가 중복을 제거한다고 믿어 원래 만들었을 멱등성을 건너뛴다. 거짓으로 두면 그 요청이 기동 실패가 되는데, 그것이 이 플래그가 존재하는 이유라는 것이다. - -같은 종류의 과대 선언이 하나는 실제 피해를 만들고 하나는 만들지 않는다. 차이는 읽는 코드가 있느냐다. - -그러므로 아홉째의 과대 선언이 오늘 만드는 것은 조회 경로의 피해가 아니다. 남는 것은 검증 경로다. - -## 검증기가 요구하는 다섯 - -트랜잭션 식별자 접두가 비어 있지 않을 것, 생산자가 멱등일 것, 응답 확인이 전부일 것, 오프셋 커밋이 수동일 것. 그리고 다섯째로 목적지가 인박스 트랜잭션을 선언하지 않을 것이다. - -다섯째가 중요하다고 javadoc 이 직접 말한다. 목적지가 인박스 트랜잭션을 선언한다는 것은 부작용이 데이터베이스에 있다는 뜻이고, Kafka 트랜잭션은 거기까지 걸칠 수 없다. 둘을 함께 설정할 수 있게 두면 팀이 "트랜잭션"이라는 단어를 두 번 읽고 경로 전체가 원자적이라고 결론짓게 된다는 것이다. - -## 그 검증기는 어디에서도 실행되지 않는다 - -스타터에는 기동 시 프로파일마다 검증기를 돌리는 래퍼가 있다. 그 래퍼로 감싸인 검증기가 셋이다. 목적지 프로파일 검증기는 래퍼 대신 직접 호출로 돈다. - -트랜잭션 검증기는 그냥 빈이다. 자동설정 밖에서 그것을 언급하는 프로덕션 코드가 0 이고, 그것을 만드는 테스트도 0 이다. - -다섯 규칙은 main 에서도 test 에서도 한 번도 실행되지 않는다. - -## 같은 결함이 이 스타터에서 한 번 고쳐졌다 - -래퍼 클래스의 javadoc 이 왜 만들어졌는지 적는다. - -Kafka 와 Rabbit 과 보안 검증기가 전부 빈이었고 어디에도 주입되지 않았다. 컨텍스트는 브로커마다 검증기를 발행했고 아무것도 검증하지 않았다. - -그다음 문장이 결과를 적는다. 브로커가 줄 수 없는 보증을 약속하는 프로파일이 — 비트랜잭션 생산자 위의 정확히 한 번 주장, 복제본 하나짜리의 정족수 확인, 프로덕션 리스너의 평문 자격증명이 — 깨끗하게 부팅한 뒤 그것에 의존하는 첫 메시지에서 실패한다. 그것을 알게 되는 자리로는 틀린 곳이다. - -그 수정이 그 세 검증기에 적용됐다. 트랜잭션 검증기가 남았다. - -## 감쌀 수 없는 이유는 인자 수가 아니다 - -래퍼는 프로파일 공급자와 소비자 함수를 받는다. 인자 수 자체는 장애가 아니다 — 나머지 둘을 캡처하는 람다면 타입이 맞는다. - -걸리는 것은 두 번째 인자다. 트랜잭션 식별자 접두는 이 저장소의 main 에서 이 검증기의 파라미터 이름과 그 javadoc 과 그것을 검사하는 조건문, 셋으로만 존재한다. 브로커 프로파일에도 설정 키에도 그 값이 없다. - -감쌀 자리보다 공급할 값이 먼저 없다. - -## 확인하지 못한 것 - -검증기가 실제로 건너뛰는지 컨텍스트를 세워 보지는 않았다. 판정 근거가 감싸기 목록과 언급 계수의 대조라, 리플렉션으로 부르는 경로까지는 배제하지 못했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/capability-declaration-vs-proof/case/case-a05-f006-stable.md b/docs/clean-architecture-backend-template/tech-log-studio/capability-declaration-vs-proof/case/case-a05-f006-stable.md deleted file mode 100644 index 91e06ac..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/capability-declaration-vs-proof/case/case-a05-f006-stable.md +++ /dev/null @@ -1,149 +0,0 @@ ---- -kind: CASE -slug: a05-f006-stable -title: 안정 등급으로 광고한 증거를 만드는 매니저가 어디서도 만들어지지 않는다 -topic: capability-declaration-vs-proof -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a05-f006-stable -evidenceCapturedOn: 2026-09-02 -assets: - - key: a05-f006-stable - file: ../../../final/evidence/rendered/a05-f006-stable.svg - - key: a05-f006-stable-chain - file: ../../../final/evidence/rendered/a05-f006-stable-chain.svg -evidence: - - ../../../final/evidence/raw/a05-f006-stable.txt - - ../../../final/evidence/raw/a05-f006-stable-chain.txt -source: - - 분석 문서는 persistence-jpa 편 §23 이고 세부는 §23.1 이다. 능력이 안정으로 보고되는데 매니저 생성이 0 이라는 판정과, 루트의 몫으로 남은 분류기 팩토리에도 소비자가 없다는 관찰이 거기 있다. 같은 문서 §23.5 가 범위를 한정한다. ---- - -# 안정 등급으로 광고한 증거를 만드는 매니저가 어디서도 만들어지지 않는다 - -완료 증거 능력이 안정 등급으로 보고된다. 그 증거를 만드는 트랜잭션 매니저를 생성하는 코드는 자기 파일의 정적 팩토리뿐이고, 그것을 부르는 곳이 프로덕션에도 테스트에도 없다. - -## 관계 - -- **커밋 증거 단계 — NOT_STARTED에서 UNKNOWN까지** - 이 능력이 만드는 증거 모델이다. -- **@Bean이 있다는 것은 조립 증거가 아니다** - 이 사례가 그 규칙의 형태다. -- **지원 등급은 추론이 아니라 선언이고 증거 없이는 올라가지 않는다** - 등급과 실제의 거리를 다룬 결정이다. - -## 문제 - -완료 증거란 커밋 진행 지점을 남길 수 있는가의 문제다. 그 기록이 없으면 커밋 실패를 롤백된 것과 결과 미상으로 나눌 수 없다. - -이 능력의 등급은 능력 리포트에 안정으로 올라 있다. - -## 결론 - -계수는 이렇다. 매니저를 만드는 코드 0, 클래스 이름을 언급하는 다른 프로덕션 파일 1 이고 그것도 javadoc 안이다. 설정 리소스에 FQCN 0, 상속 0, 트랜잭션 매니저 빈을 등록하는 main 코드 0 이다. - -그래서 완료 불명 예외는 출하 조립에서 던져질 경로가 없다. 그것을 만드는 프로덕션 코드는 분류기 한 곳이고, 그 분류기를 부르는 프로덕션 코드는 매니저의 커밋 catch 한 곳이며, 그 매니저를 설치하는 코드가 없다. - -컴포지션 루트의 javadoc 에는 루트에서 만들면 ORM 타입이 루트의 컴파일 클래스패스에 올라오기 때문에 매니저를 영속성 리프 안에서 만든다고 적혀 있다. 그 설명은 클래스패스에서 사실로 확인된다. 그런데 영속성 리프 쪽에도 그것을 만드는 코드가 없다. - -그 javadoc 은 루트가 맡을 것으로 둘을 든다. 설치 여부의 결정, 그리고 거기에 쓸 커밋 실패 분류기다. 팩토리는 있는데 그것을 부르는 코드가 없다. 그 팩토리를 담은 클래스는 스프링 설정이 아니라 평범한 클래스이고, 그것을 쓰는 프로덕션 코드가 부르는 메서드는 실행기와 재시도 코디네이터 둘뿐이다. - -프레임이 안 만들어지는 것은 아니다. 실제로 조립되는 실행기는 트랜잭션마다 프레임을 밀어 넣는다. 그 프레임을 커밋 단계로 옮기는 것이 설치되지 않는 매니저뿐이라 프레임은 시작 전 상태로 남는다. - -매니저의 javadoc 에는 코드와 맞지 않는 문단도 있다. 증거가 모든 경로에서 지워진다고 적는데, 커밋과 롤백의 finally 는 둘 다 비어 있고 여기서 지우지 않는다고 주석이 달려 있다. 꺼내는 쪽은 실행기의 스코프이고 매니저가 하는 일은 단계 표시다. 두 주인이 꺼내던 시절의 서술이 남은 것이다. - -단계를 읽는 접근자도 아무도 부르지 않는다. 분류기가 프레임에서 꺼내는 것은 작업 이름과 시작 시각과 시도 횟수와 조정 키이고, 단계는 보지 않는다. 단계에 민감해지는 것은 검사 때문이 아니라 어디서 부르느냐 때문이다. - -그 순서를 실제로 돌리는 테스트도 없다. 이름만 같은 테스트가 정작 그 타입을 건드리지 않고, 순서는 다른 시험이 본다고 자기 javadoc 에 적어 둔다. - -범위는 한정된다. 정규 트랜잭션 경로 쪽은 다른 감시자가 커밋 예외를 불확정 결과로 바꿔 놓고 재실행은 하지 않는다. 없는 것은 자동 재시도 안전이 아니라 안정 등급으로 내건 조정 증거 쪽이다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 생성 지점과 빈 등록의 정적 계수, javadoc 과 코드 대조, 예외 생산·호출 사슬 추적 -소스 수정 : x - -## 재현 조건 - -1. 능력 리포트에서 이 능력의 등급을 확인한다. -2. 매니저의 javadoc 과 같은 파일의 doCommit·doRollback 을 나란히 읽는다. -3. 그 매니저를 만드는 코드를 센다. 자기 파일과 javadoc 을 뺀다. -4. 설정 리소스의 FQCN 과 상속과 트랜잭션 매니저 빈 등록을 각각 센다. -5. 루트가 몫이라고 적은 분류기 팩토리의 호출자를 센다. -6. 완료 불명 예외의 생산 지점과 그것을 부르는 지점을 따라간다. -7. 단계를 읽는 접근자의 호출자를 센다. - -## 본문 - - - -완료 증거는 트랜잭션이 어디까지 갔는지를 기록한다. 커밋 실패를 롤백된 것과 결과를 모르는 것으로 나누려면 그 기록이 있어야 한다. - -능력 리포트는 이 능력을 안정 등급으로 보고한다. - -## javadoc 이 주장하는 것과 코드가 하는 것 - -:::evidence key="a05-f006-stable" alt="능력 리포트가 완료 증거 능력에 매긴 등급, 그 증거를 만드는 트랜잭션 매니저의 javadoc 과 같은 파일의 커밋·롤백 구현, 그 매니저를 만드는 코드와 자기 파일을 뺀 생성 지점 수, 설정 리소스의 FQCN 과 상속 수, 그리고 트랜잭션 매니저 빈을 등록하는 main 과 test 코드 수를 출력한 터미널 기록." caption="능력 등급은 stable · javadoc 의 정리 규칙과 비어 있는 finally · 자기 파일 밖 생성 0 · 설정 리소스 0 · 상속 0 · 매니저 빈 main 0, test 1 — 56줄 · exit 0" zoom="true" -::: - -매니저의 javadoc 에는 단계가 제공자 커밋 직전에 표시되고 그 뒤에는 표시되지 않는다고 한 문장으로 적혀 있다. 커밋 안에서 죽으면 마지막으로 기록된 것은 `we asked, we do not know` 이고, javadoc 은 그것이 롤백으로 오인되어서는 안 되는 상태라고 적는다. - -같은 javadoc 에는 증거가 커밋 성공과 실패와 롤백과 정리, 모든 경로에서 지워진다는 정리 규칙도 적혀 있다. 코드는 그렇지 않다. - -```java -} finally { - // Deliberately not cleared here. The executor's scope owns the frame's lifetime; a second - // owner popping was how an inner REQUIRES_NEW transaction deleted its outer frame. -} -``` - -커밋과 롤백의 `finally` 가 둘 다 비어 있고 같은 주석이 붙어 있다. 프레임을 꺼내는 것은 실행기의 스코프뿐이고 매니저는 단계만 표시한다. 그 javadoc 문단은 주인이 둘이던 시절의 서술이 남은 것이다. - -## 그 매니저를 만드는 코드가 없다 - -자기 파일 안에 정적 팩토리가 있고, 그것을 부르거나 생성자를 쓰는 코드는 자기 파일 밖에 0 이다. 클래스 이름을 언급하는 다른 프로덕션 파일은 하나뿐이고 그것도 자동설정의 javadoc 안이다. - -설정 리소스에 FQCN 이 나오는 곳도 0 이고, 상속하는 코드도 0 이다. 트랜잭션 매니저 빈을 등록하는 main 코드도 0 이다. 테스트에 하나 있는데, 그것은 자동설정 시험이 조건을 만족시키려고 세운 평범한 매니저다. - -이 저장소가 등록하는 매니저가 없다는 뜻이고, 매니저가 없다는 뜻은 아니다. 부트의 JPA 자동설정이 평범한 것을 넣고, 그것은 단계를 표시하지 않는다. - -## 루트가 남긴 몫도 비어 있다 - -:::evidence key="a05-f006-stable-chain" alt="컴포지션 루트가 매니저를 만들지 않는 이유와 루트의 몫으로 지목한 두 가지를 적은 javadoc, 그 분류기 팩토리의 호출자 수, 그 클래스가 스프링 설정이 아니라는 서술과 그것을 쓰는 프로덕션 코드가 부르는 메서드, 완료 불명 예외의 생산 지점과 그것을 부르는 지점, 분류기가 프레임에서 꺼내는 값과 단계 접근자의 호출자 수, 조립되는 실행기가 프레임을 미는 줄, 그리고 같은 이름의 테스트가 그 타입을 참조하는 횟수를 출력한 터미널 기록." caption="루트가 만들지 않는 이유와 남긴 몫 둘 · 분류기 팩토리 호출자 0 · 예외 생산 1곳과 호출 1곳 · 분류기는 단계를 보지 않음 · 단계 접근자 호출자 0 · 실행기는 프레임을 만듦 — 45줄 · exit 0" zoom="true" -::: - -javadoc 이 루트가 여기서 만들지 않는 이유를 적는다. 매니저는 영속성 리프 안에서 만들어지고, 루트가 만들면 `jakarta.persistence` 와 `org.hibernate` 가 루트의 컴파일 클래스패스에 올라온다는 것이다. - -그 이유는 클래스패스 구성으로 성립한다. 다만 영속성 리프 안에도 그것을 만드는 코드는 없다. javadoc 은 일어난 일이 아니라 일어났어야 할 일을 서술한다. - -같은 javadoc 이 루트의 몫으로 둘을 지목한다. 설치할지 말지의 결정과 그것이 쓸 커밋 실패 분류기다. - -분류기를 만드는 팩토리는 존재하고, 부르는 코드는 0 이다. 그 팩토리를 담은 클래스는 `@Configuration` 도 `@Bean` 도 없는 평범한 클래스이고, 스스로 그렇게 적는다. 그것을 쓰는 유일한 프로덕션 코드가 부르는 메서드는 실행기와 재시도 코디네이터 둘이다. - -## 예외로 가는 길이 한 줄씩 끊긴다 - -완료 불명 예외를 만드는 프로덕션 코드는 분류기 97행 한 곳이다. 그 분류기의 번역 메서드를 부르는 프로덕션 코드는 매니저 57행의 커밋 catch 한 곳이다. 그 매니저를 설치하는 코드가 0 이다. - -분류기가 프레임에서 꺼내는 것은 작업 이름, 시작 시각, 시도 횟수, 조정 키다. 단계는 보지 않는다. 단계 민감성은 검사가 아니라 호출 위치에서 나온다. 단계를 읽는 접근자를 부르는 코드는 저장소 전체에 0 이다. - -## 프레임은 만들어지고, 단계만 오르지 않는다 - -조립되는 실행기가 트랜잭션마다 프레임을 민다. 그 실행기는 플랫폼 트랜잭션 매니저 빈이 있을 때 붙는 빈이고, 부트가 넣은 매니저가 그 조건을 만족시킨다. - -그 프레임을 활성과 커밋 중과 커밋됨으로 옮기는 것은 설치되지 않는 매니저뿐이다. 프레임은 시작 전 상태로 남는다. - -순서를 실행하는 테스트도 없다. 같은 이름의 테스트는 그 타입을 한 번도 참조하지 않고 컨텍스트와 분류기를 따로 검증하며, 자기 javadoc 이 실제 순서는 커밋 모호성 계약 시험이 본다고 적는다. - -## 범위 - -이 사건이 모든 유스케이스가 불확정 커밋을 중복 실행한다는 뜻은 아니다. 애플리케이션의 정규 트랜잭션 경로는 별도의 스프링 동기화 감시자로 커밋 예외를 불확정 결과로 되돌리고 재실행하지 않는다. - -빠진 것은 자동 재시도 안전이 아니라, 안정 등급으로 광고한 조정 증거다. 지속되는 기록도 런북 지표도 없고, 애플리케이션이 돌려주는 불확정 결과에도 조정 참조가 비어 있다. - -## 확인하지 못한 것 - -애플리케이션을 부팅해 어떤 트랜잭션 매니저가 실제로 쓰이는지 관측하지 않았다. 조립 코드에 그것을 만드는 자리가 없다는 것까지만 확인했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/capability-declaration-vs-proof/case/case-a05-f022-stable.md b/docs/clean-architecture-backend-template/tech-log-studio/capability-declaration-vs-proof/case/case-a05-f022-stable.md deleted file mode 100644 index b9ebe4b..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/capability-declaration-vs-proof/case/case-a05-f022-stable.md +++ /dev/null @@ -1,185 +0,0 @@ ---- -kind: CASE -slug: a05-f022-stable -title: 검증기는 도는데 정책을 넘기는 한 번의 호출이 없다 -topic: capability-declaration-vs-proof -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a05-f022-stable -evidenceCapturedOn: 2026-09-02 -body: case-a05-f022-stable.body.md -assets: - - key: a05-f022-stable - file: ../../../final/evidence/rendered/a05-f022-stable.svg -evidence: - - ../../../final/evidence/raw/a05-f022-stable.txt -source: - - 원본 분석 절은 `final/document.md#a05` §69 다. 등급은 P1 이다. 검증기 자체는 빈으로 구성되지만 정책을 적용하는 호출자가 없다는 판정과, 액추에이터 불리언이 생성 권한 일부만 확인한다는 관찰이 그 절에 있다. - - 검증기 javadoc 의 두 주장과 실제 호출 시점·예외 처리의 대조, 그리고 두 시험이 각각 절반만 덮는다는 것은 이 기록에서 확인했다. ---- - -# 검증기는 도는데 정책을 넘기는 한 번의 호출이 없다 - -런타임 롤 검증기가 빈으로 등록되고 프로덕션에서 실제로 권한을 읽는다. 다만 기동 시점이 아니라 액추에이터 리포트를 만들 때이고, 읽은 결과를 정책에 넘기지 않는다. 검증기 자신의 javadoc 은 기동 시점에 돌고 닫힌 방식으로 실패한다고 적는다. - -## 관계 - -- **시작 검증기가 도는지는 그 능력에 자동설정 루트가 있는지와 일치한다** - 그 대응이 깨지는 경우다. 루트가 있고 검증기가 빈으로 등록되고 호출까지 되는데, 정책을 넘기는 호출만 빠져 있다. -- **지원 등급은 추론이 아니라 선언이고 증거 없이는 올라가지 않는다** - 등급이 뜻하는 것과 문서가 약속한 것이 다른 경우다. -- **RLS가 성립하기 위한 세 전제** - 런타임 롤 속성이 격리 판정에 관여하는 다른 국면이다. - -## 문제 - -보안 문서가 기동 실패 조건을 적는다. 런타임 롤이 허용 목록에 없거나 스키마나 데이터베이스에 생성 권한을 가지면 기동이 실패한다는 것이다. - -검증기의 클래스 javadoc 은 더 직접적이다. 검증이 기동 시점에 돌고 닫힌 방식으로 실패한다고 적는다. - -## 결론 - -배선 자체는 되어 있다. 자동설정이 검증기를 빈으로 만들고, 액추에이터 리포트를 만들 때마다 roleVerifier.verify(dataSource) 를 부른다. - -기동 시점이 아니다. 기동 검사 빈에 걸린 것은 위험 설정 가드 하나이고, 그 가드의 인자에 롤 정책이 없다. - -닫히지도 않는다. 리포트를 만드는 쪽이 검증기의 예외를 잡아 널을 돌려준다. 실패는 기동을 막는 대신 미검증 표시가 된다. - -정책이 보는 항목은 넷이다. 허용 목록, 스키마 생성 권한, 데이터베이스 생성 권한, 검색 경로다. 그 정책에 검증 결과를 넘기는 두 인자짜리 메서드가 검증기에 있고, 그것을 부르는 곳은 코드베이스 전체에 하나다. PostgreSQL 보안 계약 시험이다. - -정책 객체를 만드는 main 코드는 0 이다. 언급하는 파일을 세면 자기 자신, 검증기, 시험 둘이다. - -액추에이터의 검증 완료 표시는 별개의 문제다. 권한 보고서가 널이 아닌지와 생성 권한을 갖지 않는지 둘로 계산한다. 그 넷 중 가운데 둘만 들어간다. - -그 위 javadoc 은 현재 사용자와 검색 경로를 하나의 불리언으로 일부러 줄였다고 적고, 운영자는 그 롤이 검증을 통과했는지를 알면 된다고 덧붙인다. 이 축소가 하는 일은 둘이다. 리포트에서 어느 롤인지가 빠지고, 그 롤이 허용 목록과 검색 경로 정책을 통과했는지도 같이 빠진다. - -그 불리언을 고정하는 시험 셋은 입력의 롤이 전부 허용된 이름이고 검색 경로도 전부 안전하다. 허용 목록 밖 롤을 넣은 입력이 없다. 정책 쪽 단위 시험은 바로 그 두 조합을 넣지만 액추에이터 불리언은 보지 않는다. - -능력 등급 자체는 다른 이야기다. 안정 등급이란 계약 시험 스위트가 매트릭스 전체를 검증했다는 뜻이고, 그 시험 자체는 존재한다. 어긋난 것은 등급이 아니라 문서와 javadoc 이 약속한 기동 실패, 그리고 액추에이터 불리언의 의미다. - -## 검증 환경 - -OpenJDK : 해당 없음. 정적 검색이다. -확인 방식 : 검증기 호출 경로 추적, 정책의 검사 항목 열람, 액추에이터 계산식과 그 시험 입력 대조 -소스 수정 : x - -## 재현 조건 - -1. 검증기의 클래스 javadoc 과 보안 문서의 기동 실패 조건을 읽는다. -2. 검증기의 verify 를 부르는 프로덕션 코드를 찾고, 그 호출이 언제 일어나는지 본다. -3. 그 호출을 감싼 코드가 예외를 어떻게 다루는지 읽는다. -4. 정책이 검사하는 항목을 열거하고, 정책을 넘기는 두 인자짜리 메서드의 호출처를 레포 전체에서 센다. -5. 정책 객체를 만드는 main 코드와 그 타입을 언급하는 파일을 센다. -6. 액추에이터의 검증 완료 계산식과 그것을 고정하는 시험의 입력을 나란히 본다. -7. 안정 등급의 정의를 읽는다. - -## 본문 - - - -검증기의 클래스 javadoc 이 이렇게 적는다. - -```text - *

The verification runs at startup and fails closed. Discovering after an incident that the - * application's own credential could drop tables is discovering it too late. -``` - -보안 문서도 같은 방향으로 적는다. 롤이 허용 목록에 없거나 생성 권한을 가지면 기동이 실패한다는 것이다. - -## 검증기는 돈다. 기동 시점이 아닐 뿐이다 - -:::evidence key="a05-f022-stable" alt="검증기 클래스의 javadoc 주장, 정책이 검사하는 네 항목, 검증기를 프로덕션에서 부르는 코드와 그 예외 처리, 정책을 넘기는 두 인자짜리 메서드와 그 호출처와 정책 객체를 만드는 코드 수, 기동 검사 빈이 실행하는 것, 액추에이터의 검증 완료 계산식과 그 위 javadoc, 그 표시를 고정하는 시험의 입력과 정책 쪽 단위 시험의 입력, 그리고 안정 등급의 정의를 출력한 터미널 기록." caption="javadoc 은 기동 시점·닫힌 실패를 주장 · 정책은 네 항목 검사 · verify 는 리포트 요청 때 불리고 예외는 널로 삼켜짐 · 두 인자짜리 호출처는 계약 시험 하나, 정책 생성 0 · 표시는 네 항목 중 둘만 · 시험 입력에 허용 목록 밖 롤 없음 — 63줄 · exit 0" zoom="true" -::: - -```java -private DatabasePrivilegeReport readPrivileges(DataSource dataSource) { - try { - return roleVerifier.verify(dataSource); - } catch (IllegalStateException unverified) { - return null; - } -} -``` - -액추에이터 리포트를 만들 때 불린다. 기동 검사 빈이 실행하는 것은 위험 설정 가드 하나이고, 그 가드는 롤 정책을 인자로 받지 않는다. - -닫히지도 않는다. 검증기의 예외는 널이 되고, 널은 미검증 표시가 된다. - -## 정책을 넘기는 호출이 없다 - -정책은 네 항목을 검사한다. - -```java -if (!allowedRoles.contains(currentUser)) { ... } -if (report.canCreateInSchema()) { ... } -if (report.canCreateInDatabase()) { ... } -searchPathPolicy.requireSafe(report.searchPath()); -``` - -검증 결과를 그 정책에 넘기는 메서드는 검증기에 있다. - -```java -public void requireSafe(DataSource dataSource, DatabaseRolePolicy policy) { - Objects.requireNonNull(policy, "policy"); - policy.requireSafe(verify(dataSource)); -} -``` - -그것을 부르는 곳은 코드베이스 전체에 하나이고, PostgreSQL 보안 계약 시험이다. 정책 객체를 만드는 main 코드는 0 이므로 프로덕션에서는 그 메서드를 부를 수도 없다. 정책 타입을 언급하는 파일은 자기 자신과 검증기, 그리고 시험 둘뿐이다. - -## 액추에이터 불리언의 계산식 - -```java -privileges != null && !privileges.holdsCreatePrivilege(), -``` - -정책이 검사하는 네 항목 중 가운데 둘만 들어간다. 허용 목록도 검색 경로도 계산에 없다. - -그 위 javadoc 이 이렇게 적는다. - -```text - *

The privilege report's {@code currentUser} and {@code searchPath} are deliberately reduced - * to a single boolean here: an operator needs to know the runtime role passed verification, not - * which role it is. -``` - -축소는 두 가지를 동시에 한다. 어느 롤인지를 리포트에서 지우고, 그 롤이 허용 목록과 검색 경로 정책을 통과했는지도 함께 지운다. 문서가 기동 실패 조건으로 지목한 값이 그 둘이다. - -같은 불리언이 플랫폼 안전 판정에도 그대로 들어간다. - -```java -return openInViewDisabled && runtimeRoleVerified; -``` - -## 두 시험이 각각 절반만 덮는다 - -이 불리언을 고정하는 시험은 셋인데, 입력의 롤이 전부 `app_runtime` 이고 검색 경로도 전부 안전하다. - -```text -new DatabasePrivilegeReport("app_runtime", "app, pg_catalog", false, false) -new DatabasePrivilegeReport("app_runtime", "app", true, false) -``` - -허용 목록 밖 롤을 넣은 입력이 없으니 시험은 통과한다. - -정책 쪽 단위 시험은 바로 그 조합을 넣는다. - -```text -new DatabasePrivilegeReport("postgres", "app", false, false) -new DatabasePrivilegeReport("app_runtime", "app, public", false, false) -``` - -다만 그 시험은 정책 객체만 보고 액추에이터 불리언은 보지 않는다. 둘 사이의 틈을 아무도 보지 않는다. - -## 등급은 어긋나지 않았다 - -안정 등급의 정의는 계약 시험 스위트가 전체 PostgreSQL 매트릭스에서 검증했다는 것이고, 그 시험은 실제로 있다. 두 인자짜리 호출이 있는 유일한 자리가 바로 그 시험이다. - -어긋난 것은 등급이 아니다. 문서와 javadoc 이 약속한 기동 실패가 어디서도 일어나지 않고, 액추에이터가 내는 판정이 정책의 네 항목 중 둘만 반영한다. - -## 확인하지 못한 것 - -허용 목록 밖 롤로 기동해 실패하지 않는 것을 재현하지 않았다. 정적 도달성과 계산식까지만 확인했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/capability-declaration-vs-proof/case/case-the-support-matrix-says-nothing-is-deployed-and-eighteen-are.md b/docs/clean-architecture-backend-template/tech-log-studio/capability-declaration-vs-proof/case/case-the-support-matrix-says-nothing-is-deployed-and-eighteen-are.md deleted file mode 100644 index 81563e2..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/capability-declaration-vs-proof/case/case-the-support-matrix-says-nothing-is-deployed-and-eighteen-are.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -kind: CASE -slug: the-support-matrix-says-nothing-is-deployed-and-eighteen-are -title: 운영자용 지원 매트릭스가 런타임 편입을 반대로 적고, 틀린 쪽이 옳은 쪽을 권위로 지목한다 -topic: capability-declaration-vs-proof -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:the-support-matrix-says-nothing-is-deployed-and-eighteen-are -evidenceCapturedOn: 2026-09-01 -assets: - - key: the-support-matrix-says-nothing-is-deployed-and-eighteen-are - file: ../../../final/evidence/rendered/the-support-matrix-says-nothing-is-deployed-and-eighteen-are.svg -evidence: - - ../../../final/evidence/raw/the-support-matrix-says-nothing-is-deployed-and-eighteen-are.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-core-api §17 이다. ---- - -# 운영자용 지원 매트릭스가 런타임 편입을 반대로 적고, 틀린 쪽이 옳은 쪽을 권위로 지목한다 - -지원 매트릭스가 messaging 리프는 모두 어느 배포에도 편입되지 않았다고 적는다. 레지스트리는 25개 중 18개가 출하 애플리케이션에 편입되어 있다고 말한다. 틀린 문단이 권위로 지목하는 문서는 이미 그 사실을 정정했다. - -## 관계 - -- **문서의 수치는 세지 말고 파생하거나 게이트로 붙든다** - 이 사례가 만든 규칙의 상위형이다. -- **과대 진술 문서를 과소보다 먼저 고친다** - 이 사례는 과소 진술이고 방향이 반대다. -- **다섯 문서가 "exactly 19 leaf"라고 적고 레지스트리는 62다** - 같은 형태가 다른 숫자에서 나타난 사례다. - -## 문제 - -운영자가 messaging 플랫폼을 도입할 때 먼저 읽는 문서가 지원 매트릭스다. 그 문서에 어떤 리프가 실제 배포에 들어가는지를 적은 문단이 있다. - -## 결론 - -그 문단이 반대를 적는다. - -문서는 registry 의 messaging 리프가 모두 런타임 편입이 비어 있고 어느 composition root 에도 들어가지 않는다고 적는다. 현재 레지스트리는 25개 중 18개가 출하 애플리케이션 소속이고, 그 문서가 속한 리프 자신이 그 안에 있다. - -형태가 특이한 것은 틀린 문단이 자기 권위로 지목하는 문서가 이미 정정을 마쳤다는 점이다. 그 문서는 같은 사실을 고쳤고 결론까지 적어 두었다. - -> 정확한 목록은 registry 가 소유하므로 여기서 세지 않는다 — 세는 순간 다시 drift 한다 - -그 결론이 지원 매트릭스에는 적용되지 않았다. 같은 리비전에서 두 문서가 모순되고, 틀린 쪽이 옳은 쪽을 가리키고 있다. - -운영자에게 남는 결과는 구체적이다. 배포 아티팩트가 실제로 이 리프들을 싣고 설정 한 줄로 켜진다는 사실을 문서에서 알 수 없다. 켜져 있는 것을 꺼져 있다고 읽는 방향이므로 과대 진술보다 덜 위험하지만, 그 대신 도입 검토 자체가 잘못된 전제 위에서 이뤄진다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Gradle : 9.0.0 -확인 방식 : 레지스트리의 런타임 편입 필드 집계와 두 문서의 해당 문단 대조 -소스 수정 : x - -## 재현 조건 - -1. 레지스트리에서 messaging 리프의 런타임 편입 필드를 전부 세어 비어 있지 않은 것의 수를 구한다. -2. 지원 매트릭스에서 편입을 서술하는 문단을 찾는다. -3. 그 문단이 권위로 지목하는 문서의 해당 절을 읽는다. - -## 본문 - - - -지원 매트릭스가 "registry 의 messaging leaf 는 모두 `runtime_memberships` 가 비어 있고 어느 composition root 에도 편입되지 않았다" 고 적는다. 현재 레지스트리는 25개 중 18개가 `["app-bootstrap"]` 이고 `messaging-core-api` 자신이 그 안에 있다. - -## 매트릭스의 문장과 레지스트리의 값 - -:::evidence key="the-support-matrix-says-nothing-is-deployed-and-eighteen-are" alt="분석 문서 final/document.md#a19-messaging-core-api 에서 이 기록의 근거 절을 그대로 잘라낸 18줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a19-messaging-core-api 발췌 — 18줄" zoom="true" -::: - -## 틀린 문단이 권위로 지목하는 문서는 이미 정정을 마쳤다 - -`src/messaging/CLAUDE.md` 는 같은 사실을 고쳤고 "정확한 목록은 registry 가 소유하므로 여기서 세지 않는다 — 세는 순간 다시 drift 한다" 는 결론까지 적었다. 그 결론이 지원 매트릭스에는 적용되지 않았다. - -## 운영자가 문서에서 알 수 없는 것 - -배포 아티팩트가 실제로 이 리프들을 싣고 `app.messaging.enabled` 하나로 켜진다는 사실이다. - -## 확인하지 못한 것 - -없다. 레지스트리와 두 문서를 전수 대조했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/capability-declaration-vs-proof/case/case-the-transport-and-the-validator-answer-differently.md b/docs/clean-architecture-backend-template/tech-log-studio/capability-declaration-vs-proof/case/case-the-transport-and-the-validator-answer-differently.md deleted file mode 100644 index 07c5de7..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/capability-declaration-vs-proof/case/case-the-transport-and-the-validator-answer-differently.md +++ /dev/null @@ -1,95 +0,0 @@ ---- -kind: CASE -slug: the-transport-and-the-validator-answer-differently -title: 같은 어댑터의 능력을 전송과 검증기가 다르게 답하고, 런타임이 쓰는 쪽이 record 의 의미와 어긋난다 -topic: capability-declaration-vs-proof -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:the-transport-and-the-validator-answer-differently -evidenceCapturedOn: 2026-09-01 -assets: - - key: the-transport-and-the-validator-answer-differently - file: ../../../final/evidence/rendered/the-transport-and-the-validator-answer-differently.svg -evidence: - - ../../../final/evidence/raw/the-transport-and-the-validator-answer-differently.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-pulsar-experimental §17.1 이다. ---- - -# 같은 어댑터의 능력을 전송과 검증기가 다르게 답하고, 런타임이 쓰는 쪽이 record 의 의미와 어긋난다 - -Pulsar 어댑터에서 키 공유 구독의 능력을 전송과 검증기가 다르게 답한다. 성분 문서를 기준으로 보면 검증기 쪽이 맞고 전송 쪽이 자기 안에서 모순인데, 런타임이 읽는 것은 전송 쪽이다. - -## 관계 - -- **능력 선언의 세 출처와 그것이 파생되지 않을 때** - 이 사례가 속한 구조다. -- **능력 플래그의 무게는 그것을 읽는 코드가 정한다** - 어느 쪽이 틀렸는지가 아니라 어느 쪽이 읽히는지가 심각도를 정한다. -- **중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다** - 같은 판정 절차의 일반형이다. - -## 문제 - -이 어댑터는 두 구독 종류를 노출한다. 공유 구독은 경쟁 소비자에 순서 없음이고, 키 공유 구독은 경쟁 소비자에 키별 순서다. - -능력을 답하는 자리가 둘이다. 전송이 구독 종류에 따라 두 상수 중 하나를 고르고, 검증기가 같은 판단을 자기 메서드로 한다. - -## 결론 - -키 공유에 대해 두 답이 갈린다. - -전송은 순서 있는 스트림을 거짓, 키별 순서를 참으로 답한다. 검증기는 둘 다 참으로 답한다. - -성분 문서가 판정 기준이다. 순서 있는 스트림은 순서 단위 안에서 순서가 보존되는지를 뜻하고, 키 공유의 순서 단위는 키다. 그 단위 안에서 순서는 보존된다. 그러므로 검증기 쪽이 문서화된 의미와 맞다. - -전송 쪽은 자기 안에서도 모순이다. 키별 순서를 참이라고 하면서 순서 있는 스트림을 거짓이라고 하면, 순서가 보존되는 단위가 있는데 그 단위 안에서 순서가 보존되지 않는다는 말이 된다. - -그리고 어긋난 쪽이 런타임이 읽는 쪽이다. 목적지별 능력을 돌려주는 것은 SPI 메서드이고 그것을 구현하는 것은 전송이다. 순서 있는 스트림은 이 저장소에서 production 코드가 실제로 읽는 몇 안 되는 능력 중 하나로, 재시도 결정 엔진이 그 값을 보고 순서 보존 재시도를 고를지 정한다. 결과적으로 키별 순서를 약속한 목적지가 순서 보존 재시도를 받지 못한다. - -두 리터럴을 묶는 것은 아무것도 없다. 열두 개의 불리언이 두 파일에 각각 손으로 적혀 있다. 테스트는 키별 순서만 단언하고 순서 있는 스트림은 보지 않는다. - -자매 어댑터인 NATS 는 두 곳이 같은 값을 답한다. 다만 그 일치도 공유가 아니라 손으로 복사한 리터럴이므로, 오늘 같다는 것이 내일도 같으리라는 보장은 코드에 없다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Gradle : 9.0.0 -확인 방식 : 두 열두 성분 리터럴의 성분별 대조와 성분 문서 확인 -소스 수정 : x - -## 재현 조건 - -1. 전송의 키 공유용 능력 상수 열두 성분을 순서대로 적는다. -2. 검증기의 능력 메서드가 키 공유에 대해 만드는 열두 성분을 적는다. -3. 두 목록을 성분별로 대조한다. -4. 능력 record 의 성분 문서에서 두 이름의 정의를 읽는다. -5. 순서 있는 스트림을 읽는 production 코드를 찾는다. - -## 본문 - - - -Key_Shared 구독에 대해 전송은 `orderedStream=false, keyedOrdering=true` 를, 검증기는 `orderedStream=true, keyedOrdering=true` 를 답한다. - -## 성분 문서가 판정 기준이다 - -`orderedStream` 은 "순서 단위 안에서 순서가 보존되는가" 이고 Key_Shared 의 순서 단위는 키다. 그러므로 검증기 쪽이 문서화된 의미와 맞고, 전송 쪽은 자기 안에서 모순이다. - -## 어긋난 쪽이 런타임이 읽는 쪽이다 - -:::evidence key="the-transport-and-the-validator-answer-differently" alt="코드베이스에서 DefaultRetryDecisionEngine 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DefaultRetryDecisionEngine 코드베이스 검색 — 3줄 · exit 0" zoom="true" -::: - -`capabilities(DestinationName)` 이 SPI 메서드이고 `orderedStream` 은 production 코드가 실제로 읽는 세 능력 중 하나다 — `DefaultRetryDecisionEngine` 이 그 값으로 순서 보존 재시도를 고른다. - -## 두 리터럴을 묶는 것이 없다 - -테스트는 `keyedOrdering` 만 단언해 `orderedStream` 을 보지 않는다. 자매 어댑터 NATS 는 두 곳이 같은 값을 답하지만 그 일치도 공유가 아니라 손으로 복사한 리터럴이다. - -## 확인하지 못한 것 - -두 답이 실제 재시도 선택을 어떻게 가르는지 실행으로 재현하지 않았다. 이 가족은 배선 경로가 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/capability-declaration-vs-proof/concept/concept-three-sources-of-a-capability-answer.md b/docs/clean-architecture-backend-template/tech-log-studio/capability-declaration-vs-proof/concept/concept-three-sources-of-a-capability-answer.md deleted file mode 100644 index e14894f..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/capability-declaration-vs-proof/concept/concept-three-sources-of-a-capability-answer.md +++ /dev/null @@ -1,97 +0,0 @@ ---- -kind: CONCEPT -slug: three-sources-of-a-capability-answer -title: 능력 선언의 세 출처와 그것이 파생되지 않을 때 -topic: capability-declaration-vs-proof -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:three-sources-of-a-capability-answer -evidenceCapturedOn: 2026-09-01 -assets: - - key: three-sources-of-a-capability-answer - file: ../../../final/evidence/rendered/three-sources-of-a-capability-answer.svg - - key: three-sources-of-a-capability-answer-diagram - file: ../../../final/assets/diagrams/three-sources-of-a-capability-answer.svg -evidence: - - ../../../final/evidence/raw/three-sources-of-a-capability-answer.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-core-api §4.12 · final/document.md#a99 §3.2 이다. ---- - -# 능력 선언의 세 출처와 그것이 파생되지 않을 때 - -이 플랫폼에서 어댑터가 무엇을 증명할 수 있는지에 답하는 곳이 셋이다. 전송의 능력 상수, 검증기의 같은 이름 메서드, 그리고 운영자가 읽는 지원 매트릭스. 셋이 같은 값을 답해야 한다는 것이 계약인데 그것을 붙드는 장치가 없다. - -## 관계 - -- **능력 선언은 프로파일에서 파생되어야 하고 상수는 그것을 할 수 없다** - 이 개념에서 나온 규칙이다. -- **능력 플래그의 무게는 그것을 읽는 코드가 정한다** - 같은 개념의 심각도 판정 쪽이다. -- **능력 상수가 프로파일 조건보다 오래 살아서 중복 제거 없는 목적지가 가드를 통과한다** - 이 개념이 실제로 발현한 사례다. - -## 본문 - - - -이 플랫폼에서 "이 어댑터가 무엇을 증명할 수 있는가" 에 답하는 곳이 셋이다. - -## 능력을 답하는 세 자리 - -:::evidence key="three-sources-of-a-capability-answer-diagram" alt="능력 질문에서 전송의 상수와 검증기의 메서드와 지원 매트릭스 문서 세 갈래가 나온다" caption="능력을 답하는 세 자리" zoom="false" -::: - -전송의 `MessagingCapabilities` 상수(SPI `capabilities(DestinationName)` 가 런타임에 돌려주는 값), 검증기의 같은 이름 메서드(기동 시점 판정용), 그리고 운영자가 읽는 지원 매트릭스 문서다. - -## MessagingCapabilities 참조 위치 - -:::evidence key="three-sources-of-a-capability-answer" alt="코드베이스에서 MessagingCapabilities 를 검색한 출력 13줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingCapabilities 코드베이스 검색 — 13줄 · exit 0" zoom="true" -::: - -## 열두 성분과 그 소유자 - -전부 `boolean` 이고 의미는 record javadoc 이 소유한다 — `brokerAcknowledgement` · `replicationOrPersistenceEvidence` · `perMessageSettlement` · `batchSettlement` · `orderedStream` · `keyedOrdering` · `replay` · `delayedDelivery` · `brokerTransaction` · `deduplicatedPublish` · `nativeDeadLetter` · `topologyManagement`. - -## 세 출처를 붙드는 장치가 없다 - -세 출처가 같은 값을 답해야 한다는 것이 계약인데, 그것을 붙드는 장치가 없다. 그리고 열둘의 무게가 같지 않다 — 부재가 예외를 만드는 것은 `deduplicatedPublish` 하나이고(`DefaultMessagePublisher`), 나머지는 읽히지 않거나 분기에만 쓰인다. record javadoc 이 그 위험을 미리 서술한다 — "a silently weakened guarantee is indistinguishable from a working one until the incident." - -:::note - -세 출처를 전수 대조하는 스크립트를 돌리지 않았다. 어댑터별 SSOT 의 §능력 절을 읽어 대조했다 - -::: - -## 세 출처 - -전송이 SPI 메서드로 돌려주는 값이 런타임의 답이다. 호출자가 목적지를 넘기면 그 목적지에 대한 능력 집합을 받는다. - -검증기가 같은 이름의 메서드를 갖는다. 이쪽은 기동 시점 판정용이고, 목적지 프로파일이 요구하는 보장을 어댑터가 줄 수 있는지 확인할 때 쓴다. - -지원 매트릭스 문서가 셋째다. 운영자가 브로커를 고를 때 읽는 표이고, 어댑터별로 열두 성분의 지원 여부를 적는다. - -## 열두 성분 - -브로커 승인, 복제·지속 증거, 개별 메시지 정착, 배치 정착, 순서 있는 스트림, 키별 순서, 재생, 지연 배달, 브로커 트랜잭션, 중복 제거 발행, 네이티브 데드레터, 토폴로지 관리. - -전부 불리언이고 의미는 record 의 javadoc 이 소유한다. 성분 이름만으로는 판정할 수 없는 것들이 있다. 순서 있는 스트림은 "순서 단위 안에서 순서가 보존되는가" 이고 그 단위가 무엇인지는 구독 형태가 정한다. - -## 무게가 같지 않다 - -열둘 중 부재가 예외를 만드는 것은 중복 제거 발행 하나다. 발행자가 중복 제거를 요구하는 목적지에 대해 그 플래그를 확인하고 없으면 던진다. - -나머지는 읽히지 않거나 분기에만 쓰인다. 순서 있는 스트림은 재시도 결정 엔진이 읽어 순서 보존 재시도를 고를지 정한다. - -그래서 같은 정도의 과대 선언이라도 결과가 다르다. 심각도를 매기려면 그 플래그를 읽는 코드를 먼저 세어야 한다. - -## 이 구조가 미리 경고한 것 - -능력 record 의 클래스 javadoc 이 이 상황을 서술한다. - -> a silently weakened guarantee is indistinguishable from a working one until the incident. - -조용히 약해진 보장은 사고가 나기 전까지 동작하는 보장과 구별되지 않는다. 세 출처가 갈리는 것이 정확히 그 형태다. 어느 것도 오류를 내지 않고, 셋 중 하나만 읽은 사람은 자기가 읽은 것이 사실이라고 믿는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/capability-declaration-vs-proof/reference/reference-a-capability-constant-must-derive-from-the-profile.md b/docs/clean-architecture-backend-template/tech-log-studio/capability-declaration-vs-proof/reference/reference-a-capability-constant-must-derive-from-the-profile.md deleted file mode 100644 index 58ed5aa..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/capability-declaration-vs-proof/reference/reference-a-capability-constant-must-derive-from-the-profile.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -kind: REFERENCE -slug: a-capability-constant-must-derive-from-the-profile -title: 능력 선언은 프로파일에서 파생되어야 하고 상수는 그것을 할 수 없다 -topic: capability-declaration-vs-proof -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:a-capability-constant-must-derive-from-the-profile -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# 능력 선언은 프로파일에서 파생되어야 하고 상수는 그것을 할 수 없다 - -## 목적 - -어댑터가 무엇을 할 수 있는지와 이 구성에서 무엇이 성립하는지를 구분한다. 둘이 갈리는 조건이 프로파일에 있으면 상수는 그 답을 담을 수 없다. - -## 규칙 - -1. 이 플래그가 참이 되는 조건을 문장으로 쓴다 - 조건이 없으면 상수가 맞다. - -2. 그 문장에 프로파일 필드가 등장하는지 본다 - 등장하면 상수는 틀린 표현이다. - -3. 파생시킬 수 없으면 검증기가 그 조건을 기동 시점에 요구한다 - 창이 없는 목적지를 거부하는 것도 답이다. 다만 그 검증기가 실제로 도는지를 함께 확인해야 한다. - -4. 어느 쪽도 못 하겠다면 문서에 조건을 적는다 - 가장 약한 답이고, 문서가 코드보다 먼저 낡는다는 것을 감수하는 선택이다. - -## 적용 조건 - -능력 record 의 모든 성분과 그에 대응하는 gRPC 쪽 선언. 브로커가 제공하는 기능을 어댑터가 대신 선언하는 자리 전부. - -## 예외 - -어댑터가 브로커와 무관하게 항상 제공하는 성질은 상수가 맞다. 구분 기준은 이 값을 거짓으로 만드는 구성이 존재하는지이고, 존재하지 않으면 상수다. - -## 예시 - -NATS 의 중복 제거 발행이 참인데, 중복 제거 식별자는 프로파일에 창이 있을 때만 만들어지고 창은 선택 사항이다. - -Kafka 의 브로커 트랜잭션이 참인데, 트랜잭션은 생산자에 트랜잭션 식별자가 있어야 성립한다. - -Rabbit 의 지연 배달이 참인데, 그 지연을 만드는 토폴로지가 조립되지 않는다. - -셋 다 형태가 같다. 조건을 아는 코드가 같은 리프에 있고, 상수가 그것을 참조하지 않는다. - -## 관계 - -- **능력 선언의 세 출처와 그것이 파생되지 않을 때** - 이 규칙이 나온 구조다. -- **능력 상수가 프로파일 조건보다 오래 살아서 중복 제거 없는 목적지가 가드를 통과한다** - 이 규칙을 어긴 사례 중 가장 무거운 것이다. -- **브로커 트랜잭션을 무조건 참으로 선언하고, 그 조건을 검사하는 검증기는 기동 시 돌지 않는다** - 같은 규칙을 어기면서 검증기까지 함께 빠진 사례다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/capability-declaration-vs-proof/reference/reference-the-weight-of-a-flag-is-set-by-the-code-that-reads-it.md b/docs/clean-architecture-backend-template/tech-log-studio/capability-declaration-vs-proof/reference/reference-the-weight-of-a-flag-is-set-by-the-code-that-reads-it.md deleted file mode 100644 index e2c84c2..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/capability-declaration-vs-proof/reference/reference-the-weight-of-a-flag-is-set-by-the-code-that-reads-it.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -kind: REFERENCE -slug: the-weight-of-a-flag-is-set-by-the-code-that-reads-it -title: 능력 플래그의 무게는 그것을 읽는 코드가 정한다 -topic: capability-declaration-vs-proof -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:the-weight-of-a-flag-is-set-by-the-code-that-reads-it -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# 능력 플래그의 무게는 그것을 읽는 코드가 정한다 - -## 목적 - -같은 record 의 성분이라고 무게가 같지 않다. 과대 선언의 심각도를 매기기 전에 그 플래그의 소비자를 먼저 센다. - -## 규칙 - -1. 성분 접근자 이름으로 저장소를 훑는다 - 호출자를 전부 모은다. - -2. 호출자를 셋으로 나눈다 - 아무도 읽지 않음, 분기에만 쓰임, 부재가 예외를 만듦. - -3. 심각도는 그 분류에서 나온다 - 읽히지 않는 플래그의 과대 선언은 문서 결함이고, 예외를 만드는 플래그의 과대 선언은 가드 우회다. - -4. 배선되지 않은 블록에서는 미래의 소비자를 센다 - 지금 무게가 0 이어도 배선되면 무엇이 그것을 읽게 되는지가 답이고, 그 답은 같은 가족의 배선된 리프에 있다. - -## 적용 조건 - -능력·기능 플래그를 담은 모든 record 와 그것을 읽는 정책 코드. - -## 예외 - -플래그가 외부에 공개되는 계약의 일부이면 소비자 수와 무관하게 정확해야 한다. 지원 매트릭스에 실리는 값이 그렇다. - -## 예시 - -이 플랫폼의 능력 열두 성분 중 부재가 예외를 만드는 것은 중복 제거 발행 하나다. 발행자가 그 플래그를 확인하고 없으면 던진다. - -순서 있는 스트림은 재시도 결정 엔진이 읽어 순서 보존 재시도를 고를지 정한다. 분기에만 쓰이는 쪽이다. - -나머지 열은 production 코드가 읽지 않는다. 같은 정도로 틀렸더라도 결과가 다르다. - -## 관계 - -- **능력 선언의 세 출처와 그것이 파생되지 않을 때** - 이 규칙이 나온 구조다. -- **같은 어댑터의 능력을 전송과 검증기가 다르게 답하고, 런타임이 쓰는 쪽이 문서화된 의미와 어긋난다** - 어느 쪽이 틀렸는지보다 어느 쪽이 읽히는지가 중요했던 사례다. -- **`runtime_memberships`를 먼저 읽고 심각도를 정한다** - 같은 계열의 판정 순서 규칙이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-inbound-graphql-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-inbound-graphql-c03.md deleted file mode 100644 index 4f125df..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-inbound-graphql-c03.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-graphql-c03 -title: 미배선 인터셉터는 누락이 아니라 중복이다 -topic: composition-and-lifecycle-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-graphql-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-graphql-c03 - file: ../../../final/evidence/rendered/adapter-inbound-graphql-c03.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-graphql-c03.txt -source: - - 원본 분석 절은 final/document.md#a16#L336 이다. -module: adapter-inbound-graphql ---- - -# 미배선 인터셉터는 누락이 아니라 중복이다 - -`GraphQlOperationNameInterceptor.apply(...)`(미배선)와 `runtime/GraphQlOperationSelectionHandler`(autoconf=2, 배선됨)가 같은 일을 하는데 배선된 쪽이 더 많이 한다. - -## 본문 - - - -`GraphQlOperationNameInterceptor.apply(...)`(미배선)와 `runtime/GraphQlOperationSelectionHandler`(autoconf=2, 배선됨)가 같은 일을 한다. **배선된 쪽이 더 많이 한다**: 익명 연산 거부 · 다중 연산 시 `operationName` 요구 · 연산 정체성 정규화가 전부 배선된 경로에 있다. - -## 정규화가 거부가 아니라 익명으로 떨어지는 이유 - -규칙에 근거가 붙어 있다 — "any name that cannot survive normalisation becomes the anonymous identity rather than being rejected — **a naming convention is not a reason to refuse an otherwise valid request**." - -## GraphQlOperationNamePolicy 참조 위치 - -:::evidence key="adapter-inbound-graphql-c03" alt="코드베이스에서 GraphQlOperationNamePolicy 를 검색한 출력 16줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GraphQlOperationNamePolicy 코드베이스 검색 — 16줄 · exit 0" zoom="true" -::: - -## 정책 객체까지 함께 미배선이다 - -따라서 미배선 인터셉터는 **누락이 아니라 중복**이다. 다만 그것이 쓰는 `GraphQlOperationNamePolicy`(85줄, 참조자 = 인터셉터와 자기 자신뿐)도 함께 미배선이고, 배선된 핸들러는 다른 정책 객체를 쓴다. §12.2. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-inbound-graphql-c06.md b/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-inbound-graphql-c06.md deleted file mode 100644 index 00233ab..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-inbound-graphql-c06.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-graphql-c06 -title: 매니페스트 조회를 설계했는데 단일 빈이 대신 주입된다 -topic: composition-and-lifecycle-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-graphql-c06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-graphql-c06 - file: ../../../final/evidence/rendered/adapter-inbound-graphql-c06.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-graphql-c06.txt -source: - - 원본 분석 절은 final/document.md#a16#L471 이다. -module: adapter-inbound-graphql ---- - -# 매니페스트 조회를 설계했는데 단일 빈이 대신 주입된다 - -설계는 `GraphQlClientPolicyManifest`에서 프로파일을 해석하는 것을 말하지만, 자동설정은 `GraphQlClientPolicy.defaults(...)` 단일 빈을 만들어 여덟 개 빈에 주입한다. 매니페스트는 만들어지지 않는다. - -## 본문 - - - -설계는 매니페스트 조회를 말한다. - -> `GraphQlClientPolicyManifest` — "The design keeps benchmarked limits in an environment manifest rather than in application code, so **this is the one place a profile is resolved from**. An unknown profile is a startup or request failure rather than a silent fallback to a permissive default." - -자동설정은 단일 빈을 만든다. - -```java -// GraphQlPlatformAutoConfiguration:302-303 -@Bean public GraphQlClientPolicy graphQlClientPolicy(GraphQlPlatformSettings properties) { - return GraphQlClientPolicy.defaults(...); -} -``` - -그리고 그 하나가 여덟 개 빈(`:123` · `:237` · `:380` · `:389` · `:397` · `:453` · `:463` …)에 주입된다. 매니페스트는 만들어지지 않는다. §16.2. - -## GraphQlPlatformWebInterceptor 참조 위치 - -:::evidence key="adapter-inbound-graphql-c06" alt="코드베이스에서 GraphQlPlatformWebInterceptor 를 검색한 출력 10줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GraphQlPlatformWebInterceptor 코드베이스 검색 — 10줄 · exit 0" zoom="true" -::: - -## 프로파일은 여전히 신뢰된 경로에서 온다 - -**프로파일 자체는 신뢰된 경로에서 온다** — `GraphQlAuthenticationContextFactory:59`가 `principal.clientProfile()`을 쓰고(검증된 principal), 미인증 호출자에는 `GraphQlPlatformWebInterceptor`의 `anonymousProfile`이 붙는다. 즉 `GraphQlClientProfileResolver`가 막으려는 노출(호출자가 자기 프로파일을 지정)은 배선된 경로에서도 발생하지 않는다. 그 타입은 중복이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-inbound-graphql-c09.md b/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-inbound-graphql-c09.md deleted file mode 100644 index 96c6181..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-inbound-graphql-c09.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-graphql-c09 -title: 선언한 전송 프로파일과 실제 응답을 만드는 쪽이 다르다 -topic: composition-and-lifecycle-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-graphql-c09 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-graphql-c09 - file: ../../../final/evidence/rendered/adapter-inbound-graphql-c09.svg - - key: adapter-inbound-graphql-c09-diagram - file: ../../../final/assets/diagrams/adapter-inbound-graphql-c09.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-graphql-c09.txt -source: - - 원본 분석 절은 final/document.md#a16#L660 이다. -module: adapter-inbound-graphql ---- - -# 선언한 전송 프로파일과 실제 응답을 만드는 쪽이 다르다 - -`http` 패키지 19개 파일 중 자동설정이 값으로 소비하는 둘을 빼면 나머지는 실행되지 않는다. 실제로 응답을 만드는 것은 Spring GraphQL이다. - -## 본문 - - - -`http` 패키지 19개 파일 중 자동설정이 값으로 소비하는 둘(`GraphQlHttpProfile` autoconf=2, `GraphQlJsonStructurePolicy` autoconf=4)을 빼면, 나머지는 실행되지 않는다. - -## 응답을 실제로 만드는 쪽 - -:::evidence key="adapter-inbound-graphql-c09-diagram" alt="Spring GraphQL 경계 안에 상태 코드 규칙과 미디어 타입 협상이 들어 있고 http 패키지가 경계 밖에 빗금 상자로 놓인 구조" caption="응답을 실제로 만드는 쪽" zoom="false" -::: - -GraphQL-over-HTTP에서 상태 코드 규칙은 미디어 타입에 달려 있다 — `application/json`은 실행 오류에도 200을, `application/graphql-response+json`은 실제 상태를 쓴다. 그 규칙을 `GraphQlHttpStatusMapper`와 `GraphQlAcceptHeader`가 담고 있고, 실제로 응답을 만드는 것은 Spring GraphQL이다. - -## GraphQlHttpProfile 참조 위치 - -:::evidence key="adapter-inbound-graphql-c09" alt="코드베이스에서 GraphQlHttpProfile 를 검색한 출력 30줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GraphQlHttpProfile 코드베이스 검색 — 30줄 · exit 0" zoom="true" -::: - -## 노출이 아니라 통제권의 문제다 - -Spring GraphQL 자신이 GraphQL-over-HTTP 스펙을 구현하므로 동작은 합리적이다. 잃는 것은 (a) 이 플랫폼이 선언한 프로파일(`V1`)이 실제 동작과 일치한다는 보장, (b) 사전 파싱 한계 중 봉투 검증기에만 있는 부분, (c) "새 결과 종류가 임의 상태를 갖고 한 호출 지점에 생기는 것"을 막겠다는 단일 팩토리의 목적. - -## 운영자가 문서대로 클라이언트를 쓸 때 - -운영자가 `GraphQlPlatformConfigurationReport`(§8.1을 고쳐 발행하게 된 뒤)에서 `httpProfile=V1`을 읽고 그 프로파일 문서대로 클라이언트를 작성한다. 실제 응답 상태와 미디어 타입은 Spring GraphQL이 정하며, 두 문서가 다른 지점에서 클라이언트가 깨진다. - -## 두 갈래 권고 - -(a) 프레임워크 전송을 정본으로 인정하고 `http` 패키지에서 전송 기계를 제거한 뒤 `GraphQlHttpProfile`을 프레임워크 동작의 서술로 좁힌다. (b) `WebGraphQlInterceptor`(`GraphQlPlatformWebInterceptor`가 이미 그 자리에 있다)에서 봉투 검증과 응답 정책을 적용해 프로파일을 실제로 강제한다. 지금은 선언과 실행이 분리돼 있다. - -## 무엇이 미배선인가 - -`GraphQlAcceptHeader`(151), `GraphQlRequestEnvelopeValidator`(152), `GraphQlHttpResponseFactory`(84), `GraphQlMediaTypes`(100), 그리고 봉투·결과·확장 정책 타입 470줄이 실행되지 않는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-inbound-web-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-inbound-web-c02.md deleted file mode 100644 index eaad9e9..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-inbound-web-c02.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-web-c02 -title: 실패를 보여 준 적 없는 경계 테스트는 잘못된 디렉터리를 스캔한 것과 구별되지 않는다 -topic: composition-and-lifecycle-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-web-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-web-c02 - file: ../../../final/evidence/rendered/adapter-inbound-web-c02.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-web-c02.txt -source: - - 원본 분석 절은 final/document.md#a14#L140 이다. -module: adapter-inbound-web ---- - -# 실패를 보여 준 적 없는 경계 테스트는 잘못된 디렉터리를 스캔한 것과 구별되지 않는다 - -`WebModuleBoundaryTest`가 다섯 개의 긍정 규칙마다 부정 픽스처를 붙이고, 스캔이 아무것도 못 찾으면 통과가 아니라 실패하도록 만들었다. - -## 본문 - - - -`WebModuleBoundaryTest`가 다섯 개의 긍정 규칙과 **네 개의 부정 픽스처**를 갖는다. - -| 규칙 | 부정 픽스처 | -|---|---| -| 모든 프로덕션 패키지가 선언된 모듈 정체성을 가진다 | `ROOT.undeclared` 패키지를 만들어 거부되는지 확인 | -| 선언된 모든 모듈이 트리에 존재한다 | — | -| 모든 교차 모듈 import가 선언된 edge다 | `conditional → ratelimit` 위반을 만들어 확인 | -| CORE 모듈은 프레임워크 자유다 | `cursor`가 `@Component`를 import하게 만들어 확인 | -| 스캔이 아무것도 못 찾으면 통과가 아니라 실패다 | 빈 디렉터리로 `IllegalStateException` 확인 | - -## 부정 픽스처가 있는 이유 - -"A boundary test that has never been shown to fail is indistinguishable from one that scans the wrong directory." 그리고 프로덕션 스캔에 `fileCount() > 100` 하한과 `packages()`에 특정 패키지 두 개가 있어야 한다는 확인이 함께 붙는다. - -## WebModuleBoundaryTest 참조 위치 - -:::evidence key="adapter-inbound-web-c02" alt="코드베이스에서 WebModuleBoundaryTest 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="WebModuleBoundaryTest 코드베이스 검색 — 2줄 · exit 0" zoom="true" -::: - -## 탐지 정규식에 Jackson 두 버전이 함께 있는 이유 - -"this repository runs on Spring 7, whose message converters take Jackson 3 — so a CORE module could have imported a mapper without this detector noticing, which is **a hole in exactly the check that is supposed to have none**." - -이것은 이 저장소에서 확인한 경계 강제 중 가장 강하다. notification의 `EndpointGuardCallSiteTest`(호출처 목록이 가드 javadoc과 달랐던)와 달리, 여기서는 목록 자체가 스캔으로 생성된다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-inbound-web-c12.md b/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-inbound-web-c12.md deleted file mode 100644 index 7d3786f..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-inbound-web-c12.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-web-c12 -title: compileOnly로 막았지만 컨버터를 등록하는 코드도 없다 -topic: composition-and-lifecycle-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-web-c12 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-web-c12 - file: ../../../final/evidence/rendered/adapter-inbound-web-c12.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-web-c12.txt -source: - - 원본 분석 절은 final/document.md#a14#L986 이다. -module: adapter-inbound-web ---- - -# compileOnly로 막았지만 컨버터를 등록하는 코드도 없다 - -`build.gradle`이 XML·CBOR 백엔드를 `compileOnly`로 두는 이유는 명확하고 의도된 설계다. 그런데 배포가 그 백엔드를 추가하더라도 메시지 컨버터를 등록하는 코드가 없다. - -## 본문 - - - -`build.gradle`이 두 백엔드를 `compileOnly`로 두고 그 이유를 길게 적는다(§2) — `implementation`이었을 때 "silently began parsing `application/xml` request bodies... an XXE surface nobody chose"였기 때문이다. 의도된 설계다. - -## WebXmlMapperFactory 참조 위치 - -:::evidence key="adapter-inbound-web-c12" alt="코드베이스에서 WebXmlMapperFactory 를 검색한 출력 4줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="WebXmlMapperFactory 코드베이스 검색 — 4줄 · exit 0" zoom="true" -::: - -## 백엔드를 추가해도 등록하는 코드가 없다 - -그 잭슨 백엔드를 배포가 추가하더라도 메시지 컨버터를 등록하는 코드가 없다. `WebXmlMapperFactory`·`WebCborMapperFactory`·`RepresentationNegotiationPolicy`를 참조하는 파일은 자기 패키지와 테스트뿐이고, 두 자동설정(MVC 12빈 · WebFlux 11빈)에도 없다. `WebRepresentation.available()`이 "absent backend를 문장으로 바꾼다"는 장치는 그 문장을 낼 호출자가 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-inbound-web-c13.md b/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-inbound-web-c13.md deleted file mode 100644 index 8929fd1..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-inbound-web-c13.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-web-c13 -title: 나중에 도는 필터가 클라이언트 값으로 덮어쓴다 -topic: composition-and-lifecycle-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-web-c13 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-web-c13 - file: ../../../final/evidence/rendered/adapter-inbound-web-c13.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-web-c13.txt -source: - - 원본 분석 절은 final/document.md#a14#L1071 이다. -module: adapter-inbound-web ---- - -# 나중에 도는 필터가 클라이언트 값으로 덮어쓴다 - -두 필터 모두 서블릿 배포에서 등록되고 둘 다 `X-Request-Id` 응답 헤더를 쓰는데, 신뢰 정책이 반대다. - -## 본문 - - - -두 필터 모두 서블릿 배포에서 등록되고 둘 다 `X-Request-Id` 응답 헤더를 쓰는데, 신뢰 정책이 반대다. - -```java -// mvc/filter/WebMvcRequestIdFilter.java:105-112 (기본 trustInboundRequestId = false) -private WebRequestId resolveRequestId(HttpServletRequest request) { - if (!trustInboundRequestId) { - return new WebRequestId(UUID.randomUUID().toString()); // 클라이언트 값을 보지 않는다 - } - return sanitized(request.getHeader(REQUEST_ID_HEADER)) ... -} -``` - -## WebMvcRequestIdFilter 참조 위치 - -:::evidence key="adapter-inbound-web-c13" alt="코드베이스에서 WebMvcRequestIdFilter 를 검색한 출력 27줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="WebMvcRequestIdFilter 코드베이스 검색 — 27줄 · exit 0" zoom="true" -::: - -## 순서가 만드는 결과 - -순서상 `WebMvcRequestIdFilter`(`HIGHEST_PRECEDENCE + 10`)가 먼저 돌아 새 UUID를 헤더에 쓰고, `RequestLoggingFilter`(`LOWEST_PRECEDENCE`)가 나중에 돌아 **클라이언트가 보낸 값으로 덮어쓴다**. MDC의 `request_id`와 접근 로그도 클라이언트 값이다. §32.1. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-inbound-websocket-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-inbound-websocket-c01.md deleted file mode 100644 index 37cb044..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-inbound-websocket-c01.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-websocket-c01 -title: 세 설정 접두사 중 하나에 소비자가 없다 -topic: composition-and-lifecycle-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-websocket-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-websocket-c01 - file: ../../../final/evidence/rendered/adapter-inbound-websocket-c01.svg - - key: adapter-inbound-websocket-c01-diagram - file: ../../../final/assets/diagrams/adapter-inbound-websocket-c01.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-websocket-c01.txt -source: - - 원본 분석 절은 final/document.md#a17#L24 이다. -module: adapter-inbound-websocket ---- - -# 세 설정 접두사 중 하나에 소비자가 없다 - -`META-INF` 자동설정 리소스가 없고 조립은 전적으로 컴포넌트 스캔에 달려 있는데, 스캔이 잡을 수 있는 Spring 애노테이션을 가진 파일이 169개 중 7개다. 그리고 세 설정 접두사 중 하나에는 그것을 읽어 조립하는 `@Configuration`이 없다. - -## 본문 - - - -여섯 소스셋(inbound-web과 같은 형태)이고 `META-INF` 자동설정 리소스가 **없다**. 조립은 전적으로 컴포넌트 스캔에 달려 있으며, 컴포지션 루트는 이 leaf를 스캔에서 제외하지 **않는다**(graphql과 반대). 그런데 스캔이 잡을 수 있는 Spring 애노테이션을 가진 파일이 **169개 중 7개**다. - -```text -config/WebSocketPlatformSettings.java @ConfigurationProperties(prefix = "backend.websocket") -stomp/WebSocketProperties.java @ConfigurationProperties(prefix = "ca-skeleton.websocket") -stomp/WebSocketConfig.java @Configuration + @ConditionalOnProperty("ca-skeleton.websocket.enabled") -advanced/sockjs/SockJsConfiguration.java @Configuration + prefix "app.websocket-platform.advanced.sockjs" -advanced/stomp/StompConfiguration.java @Configuration + prefix "app.websocket-platform.advanced.stomp" -advanced/stomp/StompDefaultsConfiguration.java @Configuration + prefix "app.websocket-platform.advanced.stomp" -advanced/stomp/rabbit/RabbitBrokerRelayConfiguration.java prefix "app.websocket-platform.advanced.stomp.relay" -``` - -## 설정 접두사와 소비자 - -:::evidence key="adapter-inbound-websocket-c01-diagram" alt="설정 접두사에서 두 Configuration 으로만 화살표가 가고, backend.websocket 은 읽는 Configuration 없음 이라고 이름 붙은 별도 영역 안에 화살표 없이 놓인다" caption="설정 접두사와 소비자" zoom="false" -::: - -세 번째가 이 모듈의 핵심 사실이다. `backend.websocket` 네임스페이스가 규정하는 "플랫폼"이 main 169 파일 중 약 90개를 차지하고, 그것을 조립하는 `@Configuration`이 하나도 없다. - -## 분석 원문의 접두사 표 - -:::evidence key="adapter-inbound-websocket-c01" alt="분석 문서 final/document.md#a17 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a17 발췌 — 15줄" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-outbound-cache-redis-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-outbound-cache-redis-c01.md deleted file mode 100644 index 04915c0..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-outbound-cache-redis-c01.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-cache-redis-c01 -title: 꺼져 있을 때 아무것도 기여하지 않는다는 말이 문자 그대로다 -topic: composition-and-lifecycle-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-cache-redis-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-cache-redis-c01 - file: ../../../final/evidence/rendered/adapter-outbound-cache-redis-c01.svg - - key: adapter-outbound-cache-redis-c01-diagram - file: ../../../final/assets/diagrams/adapter-outbound-cache-redis-c01.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-cache-redis-c01.txt -source: - - 원본 분석 절은 final/document.md#a10#L85 이다. -module: adapter-outbound-cache-redis ---- - -# 꺼져 있을 때 아무것도 기여하지 않는다는 말이 문자 그대로다 - -`RedisSdkSettings`가 애플리케이션 전역 `@ConfigurationPropertiesScan`이 아니라 `RedisSdkAutoConfiguration`의 `@Bean`으로만 존재한다. 그래서 스위치가 꺼져 있으면 속성이 묶이지도 않는다. - -## 본문 - - - -`RedisSdkAutoConfiguration`의 javadoc이 규칙을 적는다. - -> "`app.redis.enabled` is the whole switch. While it is false this class contributes nothing, and because `RedisSdkSettings` is registered here rather than by the application-wide `@ConfigurationPropertiesScan`, **'contributes nothing' is literal**: the properties are not bound, the cross-field rules are not run, no credential is resolved, and no policy resource, TLS material, client, connection or thread is created." - -## 조립이 고정된 순서 - -:::evidence key="adapter-outbound-cache-redis-c01-diagram" alt="설정 바인딩과 교차 필드 검증과 클라이언트 생성이 왼쪽에서 오른쪽으로 이어지고 화살표에 bound settings 와 검증 통과가 붙은 구조" caption="조립이 고정된 순서" zoom="false" -::: - -`RedisSdkSettings`는 `@ConfigurationPropertiesScan` 대상이 아니라 이 클래스의 `@Bean` + `@ConfigurationProperties`로만 존재한다. 그래서 Redis를 쓰지 않는 배포는 Redis 설정을 들고 다니지 않고, **켠 적 없는 잘못된 Redis 설정 때문에 벌을 받지도 않는다**. test가 그 넷을 이름으로 고정한다 — `absentSwitchRegistersNothing`, `disabledRegistersNothing`, `disabledIgnoresMalformedRedisConfiguration`, `disabledNeverAsksForASecretOrAConnection`. - -## RedisSdkAutoConfiguration 참조 위치 - -:::evidence key="adapter-outbound-cache-redis-c01" alt="코드베이스에서 RedisSdkAutoConfiguration 를 검색한 출력 30줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RedisSdkAutoConfiguration 코드베이스 검색 — 30줄 · exit 0" zoom="true" -::: - -## 검증이 bean factory 메서드 안에 있는 이유 - -순서도 bind → validate → build로 고정된다. 검증이 `@PostConstruct`나 리스너가 아니라 **bean factory 메서드 안**에 있어서, 설정 오류가 "그 bean을 만들지 못했다"는 실패로 보고되고 그 아래 어떤 것도 검증되지 않은 settings를 잡을 수 없다. 그리고 `redisSdkSettingsValidation`이 별도 bean인 이유도 적혀 있다 — Spring은 factory 메서드가 **반환한 뒤에** binder를 돌리므로 검증이 `redisSdkSettings()` 안에 있을 수 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-outbound-cache-redis-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-outbound-cache-redis-c02.md deleted file mode 100644 index 1f3f1a2..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-outbound-cache-redis-c02.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-cache-redis-c02 -title: 기본값이 가리키는 리소스가 없고 그것이 시작 실패로 잡힌다 -topic: composition-and-lifecycle-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-cache-redis-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-cache-redis-c02 - file: ../../../final/evidence/rendered/adapter-outbound-cache-redis-c02.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-cache-redis-c02.txt -source: - - 원본 분석 절은 final/document.md#a10#L107 이다. -module: adapter-outbound-cache-redis ---- - -# 기본값이 가리키는 리소스가 없고 그것이 시작 실패로 잡힌다 - -`RedisSdkSettings.Raw.policyResource` 기본값 `classpath:redis-sdk/raw-command-allowlist.yml`은 저장소에 없는 파일이다. 결함이 아니라 이미 잡혀 있는 함정이다. - -## 본문 - - - -`RedisSdkSettings.Raw.policyResource` 기본값은 `classpath:redis-sdk/raw-command-allowlist.yml`인데, 저장소에 그 파일은 **없다**(`159-...` §8.3b, `git ls-files` 매치 0. 이 leaf의 main resource는 `AutoConfiguration.imports`와 `redis-sdk/redis-command-policy.yml` 둘뿐). - -## 결함이 아니라 이미 잡혀 있는 함정이다 - -`requireRawPolicyResource`가 그 사실과 과거 증상을 함께 적는다. - -> "`validate()` only checks that the setting is non-blank, and the default points at … a resource this module does not ship. So enabling the raw gateway passed configuration validation and then **failed at the first raw command, from inside a request, against a live connection.** The allowlist is the entire authorisation model for that gateway; not being able to read it is a startup failure." - -test `enabledRejectsAMissingRawAllowlistResource`와 `enabledAcceptsAReadableRawAllowlistResource`가 양쪽을 고정한다. - -## 분석 원문의 확인 절차 - -:::evidence key="adapter-outbound-cache-redis-c02" alt="분석 문서 final/document.md#a10 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a10 발췌 — 15줄" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-outbound-fileserver-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-outbound-fileserver-c01.md deleted file mode 100644 index dea2b01..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-outbound-fileserver-c01.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-fileserver-c01 -title: 적재는 import filter가 아니라 명시적 component scan이다 -topic: composition-and-lifecycle-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-fileserver-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-fileserver-c01 - file: ../../../final/evidence/rendered/adapter-outbound-fileserver-c01.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-fileserver-c01.txt -source: - - 원본 분석 절은 final/document.md#a08#L127 이다. -module: adapter-outbound-fileserver ---- - -# 적재는 import filter가 아니라 명시적 component scan이다 - -이 leaf에는 `AutoConfiguration.imports`가 없다. 실제 적재는 `CaSkeletonApplication`의 명시적 `@ComponentScan`이 하고, bean 생성만 `@ConditionalOnProperty`로 막힌다. - -## 본문 - - - -이 leaf에는 `META-INF/spring/…AutoConfiguration.imports`가 **없다**(`142-...` §8.1). `FileExportConfig`/`FileserverR2Config`를 leaf 밖에서 이름으로 참조하는 production 코드도 없고, 유일한 외부 참조는 app-bootstrap의 test(`OptionalAdapterBeanGatingTest`)다. - -## FileExportConfig 참조 위치 - -:::evidence key="adapter-outbound-fileserver-c01" alt="코드베이스에서 FileExportConfig 를 검색한 출력 7줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="FileExportConfig 코드베이스 검색 — 7줄 · exit 0" zoom="true" -::: - -## 실제 적재 경로 - -`CaSkeletonApplication`의 명시적 `@ComponentScan`이 `dev.caskeleton.adapter.outbound.fileserver`를 목록에 올려서 이루어진다(`:75`). 즉 CLAUDE.md의 "never activates unexpectedly when merely present on the classpath"는 **classpath 존재만으로 bean이 생기지 않는다**는 뜻으로는 정확하지만, 기전은 import filter가 아니라 "@Configuration은 스캔되고 bean 생성만 `@ConditionalOnProperty`로 막힌다"이다. - -## 기전이 다른 것을 기록해 두는 이유 - -fail-closed는 성립한다 — 기록해 두는 이유는 mongo leaf의 4중 opt-in(§sub-scope 01, 06번 문서)과 기전이 다르기 때문이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-outbound-notification-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-outbound-notification-c01.md deleted file mode 100644 index e517336..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-outbound-notification-c01.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-notification-c01 -title: 이름 없던 상태에 이름을 붙인 세 자리 -topic: composition-and-lifecycle-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-notification-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-notification-c01 - file: ../../../final/evidence/rendered/adapter-outbound-notification-c01.svg - - key: adapter-outbound-notification-c01-diagram - file: ../../../final/assets/diagrams/adapter-outbound-notification-c01.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-notification-c01.txt -source: - - 원본 분석 절은 final/document.md#a13#L84 이다. -module: adapter-outbound-notification ---- - -# 이름 없던 상태에 이름을 붙인 세 자리 - -세 클래스가 각각 이전에는 구분되지 않던 두 상황을 구분한다 — 공급자가 없는 플랫폼, 알 수 없는 공급자 타입, 그리고 능력별로 필요한 비밀 키. - -## 본문 - - - -세 클래스가 각각 이전에는 **구분되지 않던 두 상황**을 구분한다. - -## 이름 없던 상태에 이름 붙이기 - -:::evidence key="adapter-outbound-notification-c01-diagram" alt="이전과 지금 두 열에 세 상태가 같은 높이로 놓이고 이전 쪽 세 상자만 빗금으로 표시된 구조" caption="이름 없던 상태에 이름 붙이기" zoom="false" -::: - -**`NotificationPlatformMode`** — 공급자가 하나도 조립되지 않은 플랫폼이 공급자가 있는 플랫폼과 똑같이 보였다. - -> "A platform with no assembled provider used to look identical to one with providers: the same beans, the same scheduler, the same readiness. **Requests were accepted durably and then sat in the queue with no eligible route.** Naming the state makes it a decision an operator takes rather than a situation they discover." - -`INGEST_ONLY`는 **명시적으로 선택해야** 하고("A deployment that reaches zero providers by accident is a misconfiguration, and the whole point of this enum is that the two are told apart"), `NotificationProviderAssembly:183`이 그것을 강제한다 — 경로가 비었는데 모드가 `INGEST_ONLY`가 아니면 조립을 거부하고 메시지로 그 모드를 안내한다. - -## NotificationPlatformMode 참조 위치 - -:::evidence key="adapter-outbound-notification-c01" alt="코드베이스에서 NotificationPlatformMode 를 검색한 출력 23줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="NotificationPlatformMode 코드베이스 검색 — 23줄 · exit 0" zoom="true" -::: - -app-bootstrap 쪽에서도 `NotificationPlatformWorkerConfig`가 "everything that starts a thread, and therefore everything `INGEST_ONLY` must not have"를 그 모드로 가른다. - -## 자유 문자열이던 타입이 닫힌 enum이 됐다 - -**`ProviderType`** — 설정이 타입을 자유 문자열로 날랐고 "the only thing that read it was a" 비교였다. 지금은 닫힌 enum이라 알 수 없는 타입이 startup 바인딩 실패가 되고 채널도 타입에서 유도된다. - -## 여덟 키를 항상 요구하던 것이 잘못된 방향이었던 이유 - -**`NotificationSecretRequirements`** — 이전에는 여덟 개 키를 **항상** 요구했다. - -> "That is **fail-closed in the wrong direction**: it made every deployment provision and rotate keys for capabilities it had switched off — a Web Push signing key for a platform with no Web Push profile… and **a key that exists but is never used is a key nobody notices leaking.** It also made the eight look equally load-bearing." - -지금은 네 개(`CONTACT_ENCRYPTION`·`CONTACT_LOOKUP_HMAC`·`PAYLOAD_ENCRYPTION`·`PROVIDER_REQUEST_LOOKUP_HMAC`)가 모든 모드에 필요하고 — **수용 경로**에 있으므로 `INGEST_ONLY`에서도 필요하다 — 나머지 넷은 능력을 따라간다. 약해지면 안 되는 방향은 명시된다: "a capability that is switched *on* and whose key is missing still refuses the boot, because the alternative is discovering it on a user's notification." - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-outbound-notification-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-outbound-notification-c02.md deleted file mode 100644 index 2a72092..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-outbound-notification-c02.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-notification-c02 -title: 같은 종료 절차를 쓰지만 대상이 다르다 -topic: composition-and-lifecycle-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-notification-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-notification-c02 - file: ../../../final/evidence/rendered/adapter-outbound-notification-c02.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-notification-c02.txt -source: - - 원본 분석 절은 final/document.md#a13#L306 이다. -module: adapter-outbound-notification ---- - -# 같은 종료 절차를 쓰지만 대상이 다르다 - -`NotificationSchedulerWorker.close`와 `NotificationBackgroundWorkers.close` 둘 다 취소 → shutdown → awaitTermination(grace) → shutdownNow를 수행한다. 형태는 같지만 대상이 다르다. - -## 본문 - - - -`NotificationSchedulerWorker.close`와 `NotificationBackgroundWorkers.close` 둘 다 "취소 → shutdown → awaitTermination(grace) → shutdownNow"를 수행한다. 형태는 같지만 대상이 다르다(폴링 스레드 + virtual-thread executor 대 단일 데몬 scheduler). 중복 아님. - -## NotificationSchedulerWorker 참조 위치 - -:::evidence key="adapter-outbound-notification-c02" alt="코드베이스에서 NotificationSchedulerWorker 를 검색한 출력 7줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="NotificationSchedulerWorker 코드베이스 검색 — 7줄 · exit 0" zoom="true" -::: - -## 인터럽트가 닿지 않는 한 지점 - -스케줄러의 `close()`는 폴링 스레드를 `interrupt()`하지만(`:173`), `runOnce`의 `globalConcurrency.acquireUninterruptibly()`(`:90`)는 인터럽트에 반응하지 않는다. 주석(`:169`)은 "인터럽트가 poll-interval sleep을 깬다"고만 말하고 그 점은 정확하다. - -## 그래도 결함으로 보지 않은 이유 - -세마포어는 in-flight 작업이 `finally`에서 반납하므로 결국 풀리고, 최악의 경우 `join(shutdownGrace)`가 만료된 뒤 종료가 계속된다. 결함 아님. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-outbound-persistence-jpa-c56.md b/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-outbound-persistence-jpa-c56.md deleted file mode 100644 index f7badd5..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-outbound-persistence-jpa-c56.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c56 -title: 항상 설치되는 스캔이 opt-in package를 끌고 오지 않는다 -topic: composition-and-lifecycle-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c56 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c56 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c56.svg - - key: adapter-outbound-persistence-jpa-c56-diagram - file: ../../../final/assets/diagrams/adapter-outbound-persistence-jpa-c56.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c56.txt -source: - - 원본 분석 절은 final/document.md#a05#L4143 이다. -module: adapter-outbound-persistence-jpa ---- - -# 항상 설치되는 스캔이 opt-in package를 끌고 오지 않는다 - -`PersistenceJpaConfig`는 persistence root를 통째로 스캔하지 않고 20개 package를 열거한다. opt-in 두 개는 각자의 조건부 configuration이 자기 package만 스캔한다. - -## 본문 - - - -`PersistenceJpaConfig`는 persistence root를 통째로 스캔하지 않고 20개 package를 열거한다. 빠진 것은 `config`, `h2`(JPA stereotype 없음)와 opt-in 두 개(`notification`, `fileserver`)다. 두 opt-in은 각자의 `@ConditionalOnProperty` configuration이 자기 package만 스캔한다. - -## 두 스캔을 가르는 선 - -:::evidence key="adapter-outbound-persistence-jpa-c56-diagram" alt="PersistenceJpaConfig 경계 안에 열거된 스무 개 package 가 들어 있고 notification 과 fileserver 가 경계 밖 점선 상자로 놓인 구조" caption="두 스캔을 가르는 선" zoom="false" -::: - -이 배치의 이유는 javadoc과 `PersistenceEntityScanCoverageTest`에 기록돼 있다 — 과거에 root를 스캔해서 capability를 끈 배포가 `ddl-auto=validate`에서 `notification_request` / `fs_cleanup_item`을 요구하며 부팅에 실패했다. - -## PersistenceJpaConfig 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c56" alt="코드베이스에서 PersistenceJpaConfig 를 검색한 출력 18줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="PersistenceJpaConfig 코드베이스 검색 — 18줄 · exit 0" zoom="true" -::: - -## 측정 결과 경계가 유지되고 있다 - -`@Entity` 25개 중 opt-in package(`notification` 13, `fileserver` 6) 밖의 4개는 `idempotency_record`, `outbox_event`, `live_event_log`, `durable_operation`이고, 이 네 테이블은 모두 default location `db/migration/postgresql`(V1/V3/V11/V12)이 만든다. `postgresql` package는 scan 대상이지만 그 안의 candidate adapter들(`inbox`, `outbox` v2, `idempotency` v2)은 `@Entity`가 아니라 native SQL 기반이라 persistence unit에 들어오지 않는다. - -## 커버리지 테스트가 한쪽만 막는다 - -opt-in configuration 두 개에 대해서는 `@EntityScan` 목록과 `@EnableJpaRepositories` 목록이 **정확히 같은지** `containsExactly`로 검사한다("entities without repositories is half a scan, and fails at the first query"). 그런데 always-install `PersistenceJpaConfig`에 대해서는 `@EntityScan` 목록만 읽어 디스크와 대조하고, 두 목록의 일치는 검사하지 않는다. 현재 두 목록은 20개로 동일하다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-outbound-persistence-mongo-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-outbound-persistence-mongo-c01.md deleted file mode 100644 index 65177e5..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-outbound-persistence-mongo-c01.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-mongo-c01 -title: 네 겹이 같은 스위치를 읽고 각각 다른 실패를 막는다 -topic: composition-and-lifecycle-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-mongo-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-mongo-c01 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-mongo-c01.svg - - key: adapter-outbound-persistence-mongo-c01-diagram - file: ../../../final/assets/diagrams/adapter-outbound-persistence-mongo-c01.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-mongo-c01.txt -source: - - 원본 분석 절은 final/document.md#a06#L98 이다. -module: adapter-outbound-persistence-mongo ---- - -# 네 겹이 같은 스위치를 읽고 각각 다른 실패를 막는다 - -opt-in이 네 겹이고 전부 `ca-skeleton.persistence-mongo.enabled=true`를 읽는다. 중복이 아니라 계층별 차단이다 — filter는 Boot의 후보군을, 나머지 셋은 자기 bean 그래프를 담당한다. - -## 본문 - - - -네 겹 모두 `ca-skeleton.persistence-mongo.enabled=true`라는 같은 조건을 읽는다(`evidence/raw/123-...` §8.2). 이것은 중복이 아니라 계층별 차단이다: filter는 Boot의 후보군, 나머지 셋은 자기 bean 그래프를 담당한다. - -## 옵트인을 이루는 네 겹 - -:::evidence key="adapter-outbound-persistence-mongo-c01-diagram" alt="import filter 와 root 자동설정과 infrastructure 설정과 platform 자동설정이 위에서 아래로 쌓이고 같은 스위치 화살표가 아래로 그려진 구조" caption="옵트인을 이루는 네 겹" zoom="false" -::: - -Mongo starter는 classpath만으로 auto-configuration 후보를 등록하고 project condition은 후보 선정 **뒤에** 평가되므로, 후보 단계에서 9개 Boot Mongo auto-configuration을 빼지 않으면 평범한 `@EnableAutoConfiguration` 앱이 client와 template을 만든다. `MongoPersistenceConfig`의 `@ImportAutoConfiguration`은 **명시적** import라 `spring.autoconfigure.exclude`의 영향을 받지 않는다. - -## MongoPersistenceConfigTest 참조 위치 - -:::evidence key="adapter-outbound-persistence-mongo-c01" alt="코드베이스에서 MongoPersistenceConfigTest 를 검색한 출력 1줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MongoPersistenceConfigTest 코드베이스 검색 — 1줄 · exit 0" zoom="true" -::: - -`MongoPersistenceConfigTest`가 실제 `@EnableAutoConfiguration` context로 default/false에서 `MongoClient`·`MongoTemplate` 부재를, `enabled=true` + mock client에서 `MongoTemplate` 단일 bean을 확인한다. - -## 남은 미연결의 성격이 다른 이유 - -`MongoPlatformAutoConfiguration`(443줄)은 이 leaf에서 가장 밀도가 높은 파일이고, 거의 모든 `@Bean`의 javadoc이 **과거에 "shipped했지만 아무 configuration도 만들지 않던" 경로**를 기록한다 — atomic/bulk template, reactive 실행 경로 일체, change-stream source와 consumer, startup validator, client generation registry, health indicator. 이 leaf는 그 미연결들을 한 번 훑어 고친 이력을 갖고 있고, 그 사실이 이 sub-scope의 판단 기준을 바꾼다: 남아 있는 미연결은 "아직 안 한 것"이 아니라 "훑고도 남은 것"이다. - -## 조건이 곧 탈출구가 되지 않게 하는 장치 - -`mongoPlatformStartupCheck`는 `MongoTopologyProbe` bean이 있을 때만 돌지만, 그 조건이 곧 탈출구가 되는 것을 막기 위해 `mongoTopologyProbeRequirement`가 **probe 조건 없이** 등록되어 "platform profile이 있는데 probe가 없으면" 실패시킨다. javadoc이 그 이유를 한 줄로 적는다 — "a requirement that only applies when the thing it requires is present is not a requirement". - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-outbound-persistence-mongo-c08.md b/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-outbound-persistence-mongo-c08.md deleted file mode 100644 index 1699148..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-outbound-persistence-mongo-c08.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-mongo-c08 -title: phase를 code table 위에 두는 순서까지 논증돼 있다 -topic: composition-and-lifecycle-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-mongo-c08 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-mongo-c08 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-mongo-c08.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-mongo-c08.txt -source: - - 원본 분석 절은 final/document.md#a06#L1142 이다. -module: adapter-outbound-persistence-mongo ---- - -# phase를 code table 위에 두는 순서까지 논증돼 있다 - -`MongoFailureClassifier`와 `MongoFailureTranslator`는 auto-configuration의 실제 bean이고 imperative·reactive 두 executor가 모두 그것을 통해 번역한다. 규칙 사슬은 label → phase → 적용 가능성 → code table → fail closed다. - -## 본문 - - - -`MongoFailureClassifier`와 `MongoFailureTranslator`는 auto-configuration의 실제 bean이고(`MongoPlatformAutoConfiguration:85·92`), imperative·reactive 두 executor가 모두 그것을 통해 번역한다. 규칙 사슬도 순서까지 논증돼 있다 — **label → phase → 적용 가능성 → code table → fail closed**. - -> Phase sits above the code table because a failure that never reached a server is safe to repeat whatever code accompanies it, and a commit failure is unsafe to replay whatever code accompanies it — **both were decided by the code table before, and the code table knows neither.** - -## MongoFailureClassifier 참조 위치 - -:::evidence key="adapter-outbound-persistence-mongo-c08" alt="코드베이스에서 MongoFailureClassifier 를 검색한 출력 31줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MongoFailureClassifier 코드베이스 검색 — 31줄 · exit 0" zoom="true" -::: - -## 고쳐진 결함 네 가지 - -- **번역기가 phase를 버렸다.** `DefaultMongoFailureTranslator`가 operationType을 들고도 context-free overload를 불러서, "FIND의 응답 유실 = 재현 가능한 읽기 / UPDATE의 같은 유실 = 결과 불명 쓰기"라는 구분이 **transaction이 아닌 모든 경로에서** 버려졌다 — 즉 모든 평범한 연산에서. 실패한 읽기가 ambiguous write로 보고됐다. -- **server-selection이 terminal이었다.** label도 code도 없는 실패가 `UNCLASSIFIED`로 떨어져 재시도 불가로 처리됐다 — 재시도가 명백히 안전한 유일한 경우인데. -- **Spring 래핑이 분류를 통째로 건너뛰었다.** `MongoFailureExtractor`가 그 수리다. cause 사슬을 깊이 16까지, `IdentityHashMap`으로 순환 안전하게 탐색한다("a cycle is about the same object appearing twice"). -- **message는 절대 읽지 않는다.** `MongoDriverFailureView`가 driver 예외를 label·code·boolean 둘로 좁히는 지점이고, 그 이후 어느 계층도 나머지에 닿을 수 없다 — "no later layer can reach the rest, because no later layer is ever handed it". - -## 생성자가 조합을 좁히는 지점 - -`MongoFailureClassification`의 생성자가 `COMMIT_ONLY`를 `TRANSACTION_COMMIT_UNKNOWN`에만 허용하는 것도 §15의 불변식과 맞물린다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-outbound-support-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-outbound-support-c01.md deleted file mode 100644 index b94e7a1..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-adapter-outbound-support-c01.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-support-c01 -title: 허용된 의존과 실제 의존이 다르다 -topic: composition-and-lifecycle-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-support-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-support-c01 - file: ../../../final/evidence/rendered/adapter-outbound-support-c01.svg - - key: adapter-outbound-support-c01-diagram - file: ../../../final/assets/diagrams/adapter-outbound-support-c01.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-support-c01.txt -source: - - 원본 분석 절은 final/document.md#a04#L52 이다. -module: adapter-outbound-support ---- - -# 허용된 의존과 실제 의존이 다르다 - -`adapter-outbound-support`의 프로덕션 표면은 타입 셋뿐이다. 레지스트리가 허용하는 project dependency는 셋이지만 실제 project dependency는 0개다. - -## 본문 - - - -`adapter-outbound-support`는 application port를 구현하는 하나의 기술 adapter라기보다 **여러 outbound adapter가 공유할 수 있는 기술적 보조 seam**이다. - -## 이 리프의 프로덕션 표면 - -:::evidence key="adapter-outbound-support-c01-diagram" alt="리프 경계 안에 상관 식별자 조회와 fail-open 로거와 기본 bean 설정 세 상자가 나란히 들어 있는 구조" caption="이 리프의 프로덕션 표면" zoom="false" -::: - -`OutboundCorrelation`은 SLF4J MDC에서 `correlation_id`를 조회하고 값이 없거나 blank면 `"unknown"`을 반환한다. `FailOpenDependencyLogger`는 optional/fail-open outbound 호출의 success/failure observation을 공통 포맷으로 기록한다 — success는 DEBUG, failure는 WARN이다. `OutboundSupportConfig`는 `FailOpenDependencyLogger` default bean을 제공하고, `@ConditionalOnMissingBean`으로 fork/application이 같은 타입을 override할 수 있게 한다. - -## OutboundCorrelation 참조 위치 - -:::evidence key="adapter-outbound-support-c01" alt="코드베이스에서 OutboundCorrelation 를 검색한 출력 15줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="OutboundCorrelation 코드베이스 검색 — 15줄 · exit 0" zoom="true" -::: - -## 허용된 의존과 실제 의존 - -`src/config/architecture/modules.json`은 support leaf가 `domain-core`·`application-core`·`shared-contract`를 project dependency로 **허용**한다. 그러나 현재 `build.gradle`과 fresh `compileClasspath` 결과를 보면 실제 project dependency는 **0개**이고, 실제 compile dependency는 `spring-boot-autoconfigure` 4.0.8과 `slf4j-api` 2.0.18뿐이다. - -즉 registry의 `allowed_dependencies`는 가능한 최대 경계를 나타내고, 현재 source graph가 그 edge를 모두 사용한다는 뜻이 아니다. support는 현 snapshot에서 domain/application/shared 타입과도 결합하지 않는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-grpc-advanced-bootstrap-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-grpc-advanced-bootstrap-c02.md deleted file mode 100644 index 9fcf43d..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-grpc-advanced-bootstrap-c02.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -kind: CONCEPT -slug: grpc-advanced-bootstrap-c02 -title: 세 조건을 하나의 boolean으로 접지 않는다 -topic: composition-and-lifecycle-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:grpc-advanced-bootstrap-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-advanced-bootstrap-c02 - file: ../../../final/evidence/rendered/grpc-advanced-bootstrap-c02.svg - - key: grpc-advanced-bootstrap-c02-diagram - file: ../../../final/assets/diagrams/grpc-advanced-bootstrap-c02.svg -evidence: - - ../../../final/evidence/raw/grpc-advanced-bootstrap-c02.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-advanced-bootstrap#L76 이다. -module: grpc-advanced-bootstrap ---- - -# 세 조건을 하나의 boolean으로 접지 않는다 - -게이트가 깃발 설정 여부, 등급의 시작 가능 여부, 운영 별도 승인을 순서대로 따로 본다. 접으면 처방이 서로 다른 세 상황이 같은 메시지를 받는다. - -## 본문 - - - -게이트가 세 조건을 순서대로 본다. - -```java -if (!flags.flagSet(capability)) → "its feature flag is not set" -if (!grade.startable()) → "it is graded WATCH, which cannot start" -if (production && requiresApproval && !approved) → "production needs a separate approval" -``` - -> "Collapsing them into one boolean produces a 'not enabled' message for three situations with three different remedies." - -## 게이트가 보는 세 조건 - -:::evidence key="grpc-advanced-bootstrap-c02-diagram" alt="깃발 설정 여부와 등급의 시작 가능 여부와 운영 별도 승인이 왼쪽에서 오른쪽으로 이어지는 구조" caption="게이트가 보는 세 조건" zoom="false" -::: - -## 이 기록이 다루는 파일 범위 - -:::evidence key="grpc-advanced-bootstrap-c02" alt="코드베이스에서 파일 목록을 만든 출력 9줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 9줄 · exit 0" zoom="true" -::: - -## 같은 불변식의 런타임 확인은 실행되지 않는다 - -`requireStableStarterIsClean` 이 같은 불변식을 런타임에서도 확인한다 — 팻 자, 셰이드 산출물, 테스트 하네스처럼 다른 방식으로 조립된 런타임을 위해서다. **다만 그 메서드를 부르는 런타임이 없다**(§12.1). 지금 그 검사를 실행하는 것은 이 리프의 자기 테스트뿐이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-grpc-observability-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-grpc-observability-c02.md deleted file mode 100644 index 9c3a543..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-grpc-observability-c02.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: grpc-observability-c02 -title: 네 타입을 쓰지만 그 관측을 만드는 코드가 없다 -topic: composition-and-lifecycle-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:grpc-observability-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-observability-c02 - file: ../../../final/evidence/rendered/grpc-observability-c02.svg -evidence: - - ../../../final/evidence/raw/grpc-observability-c02.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-observability#L59 이다. -module: grpc-observability ---- - -# 네 타입을 쓰지만 그 관측을 만드는 코드가 없다 - -`grpc-core-api`에서 쓰는 타입은 `GrpcMethodName`, `GrpcStatusCode`, `RpcType`, `GrpcCompletionOutcome` 넷이고 전부 `GrpcRpcObservation`의 record 성분이다. 그런데 그 관측을 만드는 production 코드가 없다. - -## 본문 - - - -`grpc-core-api` 에서 쓰는 타입은 넷이다 — `GrpcMethodName`, `GrpcStatusCode`, `RpcType`, `GrpcCompletionOutcome`. 네 타입 모두 `GrpcRpcObservation` 의 record 성분이다. `GrpcStreamObservation` 은 `GrpcMethodName` 하나만 쓴다. - -## GrpcMethodName 참조 위치 - -:::evidence key="grpc-observability-c02" alt="코드베이스에서 GrpcMethodName 를 검색한 출력 7줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcMethodName 코드베이스 검색 — 7줄 · exit 0" zoom="true" -::: - -## 관측을 만드는 production 코드가 없다 - -배선 없음(`EVD-325`). `runtime_memberships` 가 비어 있고, 저장소 어디에서도 `new GrpcObservationConvention(...)` 을 만드는 production 코드가 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-messaging-admin-runtime-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-messaging-admin-runtime-c02.md deleted file mode 100644 index 751a0a3..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-messaging-admin-runtime-c02.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-admin-runtime-c02 -title: 선언한 여섯 의존 중 셋이 import 0건이다 -topic: composition-and-lifecycle-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-admin-runtime-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-admin-runtime-c02 - file: ../../../final/evidence/rendered/messaging-admin-runtime-c02.svg -evidence: - - ../../../final/evidence/raw/messaging-admin-runtime-c02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-admin-runtime#L78 이다. -module: messaging-admin-runtime ---- - -# 선언한 여섯 의존 중 셋이 import 0건이다 - -`build.gradle`이 여섯 project를 `api`로 선언하는데 실측 import는 셋이 0건이다. 지금까지 본 리프 중 가장 많다. - -## 본문 - - - -`build.gradle`이 여섯 project를 `api`로 선언하는데 실측 import(`EVD-308`)는 셋이 0건이다. 지금까지 본 리프 중 가장 많다. - -| 선언 | 패키지 | import | 판정 | -|---|---|---:|---| -| `messaging-admin-api` | `…messaging.admin` | 45 | O | -| `messaging-core-api` | `…messaging.api` | 6 | O | -| `messaging-observability` | `…messaging.observation` | 1 | O | -| `messaging-policy` | `…messaging.policy` | **0** | X | -| `messaging-transport-spi` | `…messaging.transport` | **0** | X | -| `messaging-security` | `…messaging.security` | **0** | X | - -## MessagingAdminService 참조 위치 - -:::evidence key="messaging-admin-runtime-c02" alt="코드베이스에서 MessagingAdminService 를 검색한 출력 4줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingAdminService 코드베이스 검색 — 4줄 · exit 0" zoom="true" -::: - -## 빈이 되는 것은 둘뿐이다 - -배선은 starter 한 곳뿐이고, 이 리프에서 빈이 되는 것은 **둘**이다(`EVD-307`). `MessagingAdminService`, `ReplayService`, `RedriveService`, `DestructiveMessagingAdmin` — 넷 다 빈이 없다. starter 는 그중 하나에 대해서만 이유를 밝힌다. 나머지 셋의 부재에 대한 설명은 어디에도 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-messaging-kafka-share-experimental-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-messaging-kafka-share-experimental-c01.md deleted file mode 100644 index b3f173b..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-messaging-kafka-share-experimental-c01.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-kafka-share-experimental-c01 -title: 이 리프의 실질은 세 가지 거절이다 -topic: composition-and-lifecycle-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-kafka-share-experimental-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-kafka-share-experimental-c01 - file: ../../../final/evidence/rendered/messaging-kafka-share-experimental-c01.svg - - key: messaging-kafka-share-experimental-c01-diagram - file: ../../../final/assets/diagrams/messaging-kafka-share-experimental-c01.svg -evidence: - - ../../../final/evidence/raw/messaging-kafka-share-experimental-c01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-kafka-share-experimental#L54 이다. -module: messaging-kafka-share-experimental ---- - -# 이 리프의 실질은 세 가지 거절이다 - -Kafka Share Group(KIP-932, 경쟁 소비자 work queue)을 실험적 어댑터로 감싼다. 190줄 중 실제 동작을 하는 코드는 거의 없고 세 가지를 거절한다. - -## 관계 - -- **허용 의존 목록은 상한이므로 미사용을 잡지 않는다 — 그것을 잡으려면 별도 검사가 필요하다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **에러 메시지가 지시하는 설정은 그 설정을 읽는 코드와 함께 존재해야 한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **구성 오류는 한 예외 타입과 안정 코드로 보고한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -Kafka **Share Group**(KIP-932, 경쟁 소비자 work queue)을 실험적 어댑터로 감싼다. `runtime_memberships: []`이고 이름 자체가 `-experimental`이다. 이 leaf의 실질은 **거절**이다 — 190줄 중 실제 동작을 하는 코드는 거의 없고, 세 가지를 거절한다. - -| 거절 | 코드 | 이유 | -|---|---|---| -| 비활성 상태의 사용 | `KAFKA_SHARE_DISABLED` | experimental이 기본 켜지지 않게 | -| 순서 보장 목적지 | `IllegalArgumentException` | share group이 순서를 줄 수 없음 | -| pause/resume | `KAFKA_SHARE_NO_PAUSE`/`_NO_RESUME` | 일시정지할 파티션 할당이 없음 | - -## 이 리프가 실제로 하는 일 - -:::evidence key="messaging-kafka-share-experimental-c01-diagram" alt="리프 경계 안에 프로파일 검증기와 능력 선언이 들어 있고 소비자 구성이 경계 밖 빗금 상자로 놓인 구조" caption="이 리프가 실제로 하는 일" zoom="false" -::: - -## 순서를 낮춰 주지 않고 거절하는 이유 - -핵심 진술이 validator javadoc에 있다. - -> "A share group hands individual records to competing consumers and acknowledges them individually. That is a work queue, and it is fundamentally incompatible with partition ordering: two consumers in the same share group can process records from one partition concurrently and finish in either order. Configuring an ordered destination on a share group would therefore advertise a guarantee the broker is not providing, so it is refused rather than degraded." - -## 기본 꺼짐이 drift 방지 수단이다 - -두 번째 문단이 이 저장소의 experimental 정책을 한 문장으로 담는다 — "The adapter is also off unless explicitly enabled, so an Experimental capability cannot drift into a Stable deployment by default." - -## 이 기록이 다루는 파일 범위 - -:::evidence key="messaging-kafka-share-experimental-c01" alt="코드베이스에서 파일 목록을 만든 출력 4줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 4줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-messaging-kafka-share-experimental-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-messaging-kafka-share-experimental-c02.md deleted file mode 100644 index 5e6e829..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-messaging-kafka-share-experimental-c02.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-kafka-share-experimental-c02 -title: 소비자 없음과 membership 없음과 조립 없음이 서로 맞는다 -topic: composition-and-lifecycle-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-kafka-share-experimental-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-kafka-share-experimental-c02 - file: ../../../final/evidence/rendered/messaging-kafka-share-experimental-c02.svg -evidence: - - ../../../final/evidence/raw/messaging-kafka-share-experimental-c02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-kafka-share-experimental#L84 이다. -module: messaging-kafka-share-experimental ---- - -# 소비자 없음과 membership 없음과 조립 없음이 서로 맞는다 - -나가는 의존이 하나도 없고 `runtime_memberships`가 비어 있으며 Spring 주석이 0개다. incubating leaf의 올바른 상태다. - -## 관계 - -- **허용 의존 목록은 상한이므로 미사용을 잡지 않는다 — 그것을 잡으려면 별도 검사가 필요하다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **에러 메시지가 지시하는 설정은 그 설정을 읽는 코드와 함께 존재해야 한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **구성 오류는 한 예외 타입과 안정 코드로 보고한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -들어오는 것(project): `messaging-core-api`, `messaging-policy`, `messaging-transport-spi`, `messaging-kafka` — 넷 다 `api`. 들어오는 것(vendor): `org.apache.kafka:kafka-clients`(`implementation`) — **어떤 소스도 import하지 않는다**(§12.4). 나가는 것: **없다.** 어떤 leaf의 `allowed_dependencies`에도 이 leaf가 없고 starter 목록에도 없다. - -## 이 기록이 다루는 파일 범위 - -:::evidence key="messaging-kafka-share-experimental-c02" alt="코드베이스에서 파일 목록을 만든 출력 4줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 4줄 · exit 0" zoom="true" -::: - -## 세 가지 부재가 서로 맞는다 - -런타임 배선: 없음. `runtime_memberships: []`. bean 없음(Spring 주석 0개). **소비자 없음·membership 없음·조립 없음의 삼중 정합**이다 — `messaging-schema-avro`·`messaging-schema-protobuf`와 같은 형태이고, incubating leaf의 올바른 상태다. - -## 선언했지만 쓰이지 않는 의존 - -네 project 의존 중 둘, 벤더 의존 하나가 미사용이다. §17. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-messaging-nats-experimental-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-messaging-nats-experimental-c02.md deleted file mode 100644 index aad8a2e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-messaging-nats-experimental-c02.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-nats-experimental-c02 -title: 검증기가 불리지 않는 지금 실제로 도는 게이트는 생성자다 -topic: composition-and-lifecycle-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-nats-experimental-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-nats-experimental-c02 - file: ../../../final/evidence/rendered/messaging-nats-experimental-c02.svg -evidence: - - ../../../final/evidence/raw/messaging-nats-experimental-c02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-nats-experimental#L94 이다. -module: messaging-nats-experimental ---- - -# 검증기가 불리지 않는 지금 실제로 도는 게이트는 생성자다 - -`NatsJetStreamProfile`은 record이고 압축 생성자가 이 어댑터의 불변식을 전부 들고 있다. 검증기가 호출되지 않는 지금 실제로 실행되는 유일한 게이트가 거기다. - -## 본문 - - - -`NatsJetStreamProfile` 은 record 이고, 압축 생성자가 이 어댑터의 불변식을 전부 들고 있다. 검증기가 호출되지 않는 지금, **실제로 실행되는 유일한 게이트가 여기다.** - -## 거부 사유를 enum이 문장으로 들고 있다 - -`ackMode` 거부 사유는 `NatsAckMode` 자신이 문장으로 들고 있고(`rejectionReason()`), 프로파일이 그 문장을 예외 메시지에 그대로 싣는다. `NONE` 은 "forgotten", `ALL` 은 "still in flight" — 테스트가 그 두 낱말로 각각 걸어 잠근다. - -## NatsJetStreamProfile 참조 위치 - -:::evidence key="messaging-nats-experimental-c02" alt="코드베이스에서 NatsJetStreamProfile 를 검색한 출력 17줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="NatsJetStreamProfile 코드베이스 검색 — 17줄 · exit 0" zoom="true" -::: - -## 임계값의 정의가 한 곳에만 있다 - -`PARKING_HEADROOM = 1` 상수와 `parkAtDelivery() = maxDeliver - PARKING_HEADROOM` 가 §2 의 시점 선택을 숫자로 못 박는다. `NatsMaxDeliverParkingWorkflow.parkingThreshold()` 는 이 값을 그대로 위임한다 — 임계값의 정의가 한 곳에만 있다. - -## 팩토리가 안전하다는 사실이 구멍을 닫지 않는다 - -**주의할 비대칭.** 편의 팩토리 `durable(subject, stream, durableName)` 는 중복 제거 창을 `Optional.of(2분)` 으로 채운다. 즉 팩토리를 거친 프로파일은 §17.1 의 구멍에 빠지지 않는다. 그러나 팩토리에도 호출자가 없고(저장소 전역 grep 0건), 정규 생성자는 빈 창을 정상값으로 받는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-messaging-runtime-core-c07.md b/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-messaging-runtime-core-c07.md deleted file mode 100644 index 0ad6f42..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-messaging-runtime-core-c07.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-runtime-core-c07 -title: 이 리프는 통째로 하나의 수정이다 -topic: composition-and-lifecycle-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-runtime-core-c07 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-runtime-core-c07 - file: ../../../final/evidence/rendered/messaging-runtime-core-c07.svg -evidence: - - ../../../final/evidence/raw/messaging-runtime-core-c07.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-runtime-core#L631 이다. -module: messaging-runtime-core ---- - -# 이 리프는 통째로 하나의 수정이다 - -MSG-INT-003이라는 식별자가 세 파일의 javadoc에 나온다. 이 리프가 채운 것은 기능이 아니라 조립의 빈칸이다. - -## 관계 - -- **만들어 두고 흘리지 않는 진단값은 진단이 아니다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **증가한다고 문서화한 값이 리터럴이면 그 사실을 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **안정 코드는 운영자의 행동이 갈리는 지점마다 나눈다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **`CompletionStage`를 반환하는 메서드는 동기적으로 던지지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -이 leaf는 **통째로 하나의 수정**이다. MSG-INT-003이라는 식별자가 세 파일의 javadoc에 나온다(`DeclaredDestinationAccess`, `TransportMessagingRuntime`, `MessagingCoreAutoConfiguration:461`). - -| 위치 | 이전 상태 | 그것이 만든 실패 | -|---|---|---| -| `build.gradle` 주석 | `MessagePublisher` 구현 없음 | 자동설정이 없는 bean 위에 DLQ·facade bean을 쌓음 | -| `TransportMessagingRuntime` javadoc | `MessagingRuntime` 구현 없음 | registry가 빈 채로 만들어져 모든 발행이 `PUBLISH_RUNTIME_UNAVAILABLE` — 목적지 해석·접근 확인·인코딩을 **전부 마친 뒤에** | -| `DestinationProfileRegistry` javadoc | 논리 이름→프로파일 해석 없음 | 어댑터는 해석된 프로파일을 받는데 그것을 만들 publisher가 없었음 | -| `DefaultDeliveryProcessor` javadoc | `HandleResult`→정산 연결 없음 | 각 어댑터가 retry/dead-letter의 뜻을 각자 결정 | -| `withDeadline` javadoc | transport가 마감을 무시 | 확인이 오지 않는 Rabbit publish에 마감이 없어 호출자 스레드가 완료 불가능한 stage에 묶임 | - -## DeclaredDestinationAccess 참조 위치 - -:::evidence key="messaging-runtime-core-c07" alt="코드베이스에서 DeclaredDestinationAccess 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DeclaredDestinationAccess 코드베이스 검색 — 3줄 · exit 0" zoom="true" -::: - -## 가짜로 빈칸을 채우면 안 되는 이유 - -`build.gradle` 주석의 마지막 문장이 이 leaf 전체의 교훈이다 — "A starter that filled the gap with an application-supplied fake would pass a context test while running none of them." - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-messaging-security-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-messaging-security-c02.md deleted file mode 100644 index f40b9e7..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-messaging-security-c02.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-security-c02 -title: 이 리프는 messaging family에서 배선이 가장 잘 된 축이다 -topic: composition-and-lifecycle-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-security-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-security-c02 - file: ../../../final/evidence/rendered/messaging-security-c02.svg -evidence: - - ../../../final/evidence/raw/messaging-security-c02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-security#L91 이다. -module: messaging-security ---- - -# 이 리프는 messaging family에서 배선이 가장 잘 된 축이다 - -들어오는 것은 `messaging-core-api` 하나이고 나가는 것은 일곱이다. 어댑터 두 곳이 직접 소비하고, 이 리프 자체는 Spring 주석을 갖지 않는다. - -## 관계 - -- **배선된 게이트는 자기 leaf 레인에서 검증한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 술어가 두 타입에 있으면 하나가 다른 하나를 부른다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **가변 필드로 상태 전이를 표현하면 가시성을 함께 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **맵 갱신 함수 안에서 I/O를 하면 그 지연이 락 범위가 된다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -들어오는 것: `messaging-core-api`(api) 하나. 나가는 것: `messaging-runtime-core`, `messaging-kafka`, `messaging-rabbit`, `messaging-admin-runtime`, `messaging-pulsar-experimental`, `messaging-nats-experimental`, `messaging-spring-boot-starter`. - -## 실제로 소비하는 곳 - -| 소비자 | 무엇을 쓰는가 | -|---|---| -| `messaging-kafka/KafkaSecurityConfigurer` | `BrokerTlsPolicy`, `CredentialRuntimeRegistry`, `CredentialProvider` | -| `messaging-rabbit/RabbitSecurityConfigurer` | `BrokerTlsPolicy`, `CredentialRuntimeRegistry` | -| `messaging-runtime-core/DefaultMessagePublisher` | `DestinationAccessPolicy` | -| `messaging-runtime-core/DeclaredDestinationAccess` | `DestinationAccessPolicy` | -| starter `MessagingCoreAutoConfiguration` | `MessageSecurityValidator`·`BrokerTlsPolicy`·`CredentialRuntimeRegistry` bean | -| starter `MessagingCredentialRequirementValidator` | `CredentialProvider` | - -## 이 기록이 다루는 파일 범위 - -:::evidence key="messaging-security-c02" alt="코드베이스에서 파일 목록을 만든 출력 12줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 12줄 · exit 0" zoom="true" -::: - -## 리프 자체는 프레임워크를 모른다 - -이 leaf 자체는 Spring 주석을 갖지 않는다. 배선은 전부 소비자 쪽에서 이루어진다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-messaging-spring-boot-starter-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-messaging-spring-boot-starter-c01.md deleted file mode 100644 index d70632f..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-messaging-spring-boot-starter-c01.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-spring-boot-starter-c01 -title: 등록되어 있다는 것과 조립할 수 있다는 것을 분리했다 -topic: composition-and-lifecycle-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-spring-boot-starter-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-spring-boot-starter-c01 - file: ../../../final/evidence/rendered/messaging-spring-boot-starter-c01.svg - - key: messaging-spring-boot-starter-c01-diagram - file: ../../../final/assets/diagrams/messaging-spring-boot-starter-c01.svg -evidence: - - ../../../final/evidence/raw/messaging-spring-boot-starter-c01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-spring-boot-starter#L94 이다. -module: messaging-spring-boot-starter ---- - -# 등록되어 있다는 것과 조립할 수 있다는 것을 분리했다 - -`MessagingProviderSelection`에 지도가 셋이고 셋째 지도가 이 클래스의 판단이다. 오늘 조립 가능한 전송은 `kafka` 하나다. - -## 관계 - -- **검증기는 발행이 아니라 주입이 강제다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -`MessagingProviderSelection` 에 지도가 셋이다. - -```java -REGISTERED_BROKERS = {kafka: org.apache.kafka.clients.producer.Producer, - rabbit: com.rabbitmq.client.Channel} -PROVIDER_CONFIGURATIONS = {kafka: KafkaMessagingAutoConfiguration, - rabbit: RabbitMessagingAutoConfiguration} -BROKERS_WITHOUT_A_TRANSPORT = {rabbit: "…ships its validators and security configuration but no - MessagingTransport…"} -``` - -## 등록과 조립의 분리 - -:::evidence key="messaging-spring-boot-starter-c01-diagram" alt="선택 레지스트리에서 kafka 와 rabbit 으로 화살표가 나가고 rabbit 상자만 빗금으로 표시된 구조" caption="등록과 조립의 분리" zoom="false" -::: - -셋째 지도가 이 클래스의 판단이다. 등록되어 있다는 것과 조립할 수 있다는 것을 분리했고, 그 이유를 적었다 — Rabbit 을 고르면 핵심 설정 깊은 곳에서 `MessagingTransport` 빈이 없다는 오류가 나는데, 그것은 운영자에게 빈이 없다고만 말하지 고른 전송이 완성되지 않았다고는 말하지 않는다. - -## MessagingProviderSelection 참조 위치 - -:::evidence key="messaging-spring-boot-starter-c01" alt="코드베이스에서 MessagingProviderSelection 를 검색한 출력 6줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingProviderSelection 코드베이스 검색 — 6줄 · exit 0" zoom="true" -::: - -## 오늘 조립 가능한 전송은 하나다 - -결과로 오늘 조립 가능한 전송은 `kafka` 하나다. `RabbitMessagingAutoConfiguration` 98줄은 선택 단계에서 거부되므로 **어떤 경로로도 도달하지 않는다**(§12.3). - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-messaging-spring-cloud-stream-bridge-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-messaging-spring-cloud-stream-bridge-c01.md deleted file mode 100644 index 9e5a387..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/composition-and-lifecycle-models/concept/concept-messaging-spring-cloud-stream-bridge-c01.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-spring-cloud-stream-bridge-c01 -title: 플랫폼 보장과 헷갈릴 만큼 닮은 것이 이 리프의 위협 모델이다 -topic: composition-and-lifecycle-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-spring-cloud-stream-bridge-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-spring-cloud-stream-bridge-c01 - file: ../../../final/evidence/rendered/messaging-spring-cloud-stream-bridge-c01.svg -evidence: - - ../../../final/evidence/raw/messaging-spring-cloud-stream-bridge-c01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-spring-cloud-stream-bridge#L56 이다. -module: messaging-spring-cloud-stream-bridge ---- - -# 플랫폼 보장과 헷갈릴 만큼 닮은 것이 이 리프의 위협 모델이다 - -Spring Cloud Stream 바인딩을 이미 쓰는 서비스가 같은 논리 목적지에 닿게 하는 상호운용 seam이다. 바인더 의미론을 플랫폼 보장으로 승격하지 않는다. - -## 관계 - -- **허용 의존 목록은 상한이므로 미사용을 잡지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **등록을 받는 컴포넌트는 해제도 제공한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **함께 읽히는 두 맵은 한 값으로 묶는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **에러 메시지가 지시하는 설정은 그 설정을 읽는 코드와 함께 존재해야 한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **한 개념의 등록 상태를 두 객체가 나눠 갖지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -Spring Cloud Stream 바인딩을 이미 쓰는 서비스가 같은 논리 목적지에 닿게 하는 **상호운용 seam**이다. - -> "The bridge is an interoperability seam, not a second messaging API… Binder semantics are never promoted to platform guarantees. Stream's binder has its own retry, its own dead-letter, and its own acknowledgement mode, and they look enough like the platform's to be mistaken for them — so a destination that actually relies on the platform's versions is refused by `StreamBridgePolicyGuard` rather than served with the binder's." - -**"look enough like the platform's to be mistaken for them"**이 이 leaf 전체의 위협 모델이다. - -## 차이를 드러내는 세 층 - -브리지는 기능을 추가하지 않고 **차이를 드러낸다.** - -| 층 | 무엇을 | -|---|---| -| `StreamBridgePolicyGuard` | 플랫폼 보장에 의존하는 목적지를 아예 거절 | -| `BindingProfileValidator` | 바인더 확장 속성이 프로파일 결정을 덮는 것을 거절 | -| `BindingCapabilityReport` | 남은 차이를 **문장으로** 기록 | - -세 번째가 특이하다 — 거절할 수 없는 차이를 문서화 가능한 값으로 만든다. - -## 이 기록이 다루는 파일 범위 - -:::evidence key="messaging-spring-cloud-stream-bridge-c01" alt="코드베이스에서 파일 목록을 만든 출력 6줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 6줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/composition-root-and-bootstrap/case/case-analysis-finding-a18-f001.md b/docs/clean-architecture-backend-template/tech-log-studio/composition-root-and-bootstrap/case/case-analysis-finding-a18-f001.md deleted file mode 100644 index 27f7c01..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/composition-root-and-bootstrap/case/case-analysis-finding-a18-f001.md +++ /dev/null @@ -1,110 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a18-f001 -title: 출하되는 web 어댑터의 스위치가 활성화 모델 밖에 있다 -topic: composition-root-and-bootstrap -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a18-f001 -evidenceCapturedOn: 2026-09-01 -assets: - - key: analysis-finding-a18-f001 - file: ../../../final/evidence/rendered/analysis-finding-a18-f001.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a18-f001.txt -source: - - 원본 분석 절은 final/document.md#a18#L214 이다. ---- - -# 출하되는 web 어댑터의 스위치가 활성화 모델 밖에 있다 - -이 어댑터는 두 런타임의 구성원이라 빌드 전용 예외가 아니다. 그런데 네 스위치가 조건 안의 문자열 리터럴로만 존재해 마스터 스위치 자바독이 경계하는 상태다. 다만 이 어댑터는 성질이 달라 그 모델에 그대로 넣을 수 없다. - -## 관계 - -- **이 leaf의 리액티브 절반 29개 파일은 어떤 출하 배포에서도 활성화될 수 없다** - 같은 어댑터의 조립 미해결 사례다. -- **능력 프로퍼티 이름을 만드는 코드와 실제 게이트가 다른 접두사를 쓴다** - 같은 계열의 스위치 이름 사례다. -- **무엇을 스위치로 부를지 정하기 전에는 활성화 모델에 넣을 수 없다** - 이 사례가 그 규칙의 형태다. - -## 문제 - -이 조립 루트는 선택 어댑터의 활성화를 명시적 스위치 모델로 다룬다. - -전부 기본 꺼짐이고, 환경 키 파일에 행이 있고, 삼자 일치 테스트가 강제한다. - -웹 어댑터가 그 모델 안에 있는지 확인했다. - -## 결론 - -밖에 있다. - -이 어댑터는 두 런타임의 구성원이므로 빌드 전용 예외에 해당하지 않는다. - -그런데 그 네 스위치가 조건 안의 문자열 리터럴로만 존재한다. - -마스터 스위치의 자바독이 경계하는 상태다. 조건 곳곳에 문자열 리터럴로 흩어지면 이름 변경이 조용한 활성화 변경이 된다는 것이다. - -다만 이 어댑터는 다른 넷과 성질이 다르다. - -두 전송 스위치가 없으면 참으로 처리되도록 되어 있어 기본이 켜짐이다. 그러므로 선택 어댑터가 아니다. - -마스터 스위치가 규정하는 명시적 스위치 모델에 그대로 넣을 수 없다. - -그리고 그 어댑터의 조립 자체가 미해결이다. 훑기에서 다섯 패키지를 빼고 넘겨받는 자동 설정을 만들지 않은 상태다. - -기록하는 것은 순서다. - -이 어댑터의 스위치를 활성화 모델에 넣는 것은 어떤 자동 설정이 무엇을 소유하는가를 먼저 정한 뒤에 할 수 있는 일이다. - -지금은 무엇을 스위치로 부를지가 결정되지 않았다. - -판정은 P3 다. - -## 검증 환경 - -Spring Boot : 4.0.8 -확인 방식 : 스위치 선언 위치 확인과 활성화 모델 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/188 계열에 있다. - -1. 활성화 모델의 규칙 셋을 확인한다. -2. 이 어댑터의 런타임 구성원 목록을 확인한다. -3. 네 스위치가 어디에 선언되어 있는지 확인한다. -4. 두 전송 스위치의 기본값을 확인한다. -5. 마스터 스위치 자바독의 경계 문장을 읽는다. - -## 본문 - - - -`adapter-inbound-web`은 두 런타임 멤버이므로 build-only 예외에 해당하지 않는다(§4.1). - -## MasterSwitch 참조 위치 - -:::evidence key="analysis-finding-a18-f001" alt="코드베이스에서 MasterSwitch 를 검색한 출력 17줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MasterSwitch 코드베이스 검색 — 17줄 · exit 0" zoom="true" -::: - -## 네 스위치가 문자열 리터럴로만 존재한다 - -`backend.web.mvc.enabled` · `backend.web.webflux.enabled`(둘 다 `matchIfMissing=true`, 기본 켜짐) · `backend.web.budgets.enabled` · `app.web-platform.durable-operations.enabled`가 `MasterSwitch`에도 `env-keys.yaml` 341개 키에도 없다. `MasterSwitch`의 javadoc이 경계하는 상태다 — "Spread across conditions as string literals, a rename becomes a silent activation change." - -## 다만 web은 다른 넷과 성질이 다르다 - -MVC/WebFlux 스위치는 `matchIfMissing = true`로 기본 켜짐이므로 "옵션 어댑터"가 아니고, `MasterSwitch`가 규정하는 explicit switch 모델(전부 기본 꺼짐, `env-keys.yaml`에 행이 있고 삼자 일치 테스트가 강제)에 그대로 넣을 수 없다. 그리고 모듈 14 §8.1이 확인했듯 web의 조립 자체가 미해결이다. - -## 기록하는 것은 순서다 - -web의 스위치를 활성화 모델에 넣는 것은 모듈 14 §8.1(어떤 자동설정이 무엇을 소유하는가)을 먼저 정한 뒤에 할 수 있는 일이다. 지금은 "무엇을 스위치로 부를지"가 결정되지 않았다. P3. - -## 확인하지 못한 것 - -스위치 이름을 바꿔 조용한 활성화 변경이 일어나는지 재현하지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/composition-root-and-bootstrap/case/case-analysis-finding-a18-f002.md b/docs/clean-architecture-backend-template/tech-log-studio/composition-root-and-bootstrap/case/case-analysis-finding-a18-f002.md deleted file mode 100644 index d00d979..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/composition-root-and-bootstrap/case/case-analysis-finding-a18-f002.md +++ /dev/null @@ -1,124 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a18-f002 -title: 실패는 환경 원인이며, 그 테스트의 도구 가드가 불완전하다 -topic: composition-root-and-bootstrap -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a18-f002 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a18-f002.body.md -assets: - - key: analysis-finding-a18-f002 - file: ../../../final/evidence/rendered/analysis-finding-a18-f002.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a18-f002.txt -source: - - 원본 분석 절은 final/document.md#a18#L549 이다. ---- - -# 실패는 환경 원인이며, 그 테스트의 도구 가드가 불완전하다 - -이 묶음의 유일한 실패는 저장소 결함이 아니다. 분석 환경에 도구 하나가 없고 위임된 스크립트가 그것을 요구하며 정직하게 실패한다. 기록하는 것은 같은 테스트가 첫째 도구는 건너뛰고 둘째 도구는 실패로 다룬다는 비대칭이다. - -## 관계 - -- **아무것도 발견하지 못한 레인은 성공이 아니라 실패여야 한다** - 이 저장소가 가진 반대 방향 원칙이다. -- **초록일 수 없는 게이트는 사람들이 건너뛰는 법을 배우게 만든다** - 같은 계열의 규칙이다. -- **한 test 안에서 도구별로 갈리지 않는 편이 낫다** - 권고의 형태다. - -## 문제 - -부트스트랩 묶음을 돌렸다. - -천열다섯 테스트가 통과하고 하나가 실패하고 넷이 건너뛰어졌다. - -그 하나를 확인했다. - -## 결론 - -저장소 결함이 아니다. - -실패 메시지가 원인을 그대로 적는다. 위임된 스크립트가 도구 하나를 요구하고 없어서 종료 코드 칠십팔로 끝났다는 것이다. - -분석 환경에 그 도구가 없다. - -기록하는 것은 가드의 비대칭이다. - -테스트는 컨테이너 도구 부재를 가정으로 처리해 건너뛴다. - -같은 스크립트가 요구하는 다른 도구의 부재는 실패로 나타난다. - -도구가 없는 기계에서 이 테스트는 계약 위반처럼 읽히는 실패를 낸다. - -메시지가 원인을 드러내므로 오해가 오래가지는 않는다. 다만 이미 건너뛰기를 선택한 테스트가 두 번째 도구에 대해서만 다르게 행동한다. - -이 저장소는 반대 방향의 원칙도 갖고 있다. - -역방향 프록시 레인이 건너뛰기를 거부하며 그 이유를 적는다. 컨테이너 실행기가 없을 때 조용히 통과하는 레인은 그 실행기가 마지막으로 망가진 이래로 아무것도 인증하지 않은 레인이라는 것이다. - -두 원칙 중 어느 쪽을 택하든 한 테스트 안에서 도구별로 갈리지는 않는 편이 낫다. - -판정은 P3 다. - -## 검증 환경 - -Gradle : 9.0.0 -확인 방식 : 묶음 실행과 실패 메시지 확인, 도구 존재 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/188 계열에 있다. - -1. 부트스트랩 묶음을 실행한다. -2. 실패한 테스트와 메시지를 확인한다. -3. 위임된 스크립트가 요구하는 도구를 확인한다. -4. 그 도구가 환경에 있는지 확인한다. -5. 테스트의 가정 처리 대상을 확인한다. - -## 본문 - - - -테스트 실패의 형태는 다음이다. - -``` -org.opentest4j.AssertionFailedError: [verify-compose-profile-contracts.sh said: -jq is required -] -expected: 0 but was: 78 - at ComposeMergeCharacterizationTest.everyLaneMatchesItsContract(ComposeMergeCharacterizationTest.java:62) - -$ which jq -> NO_JQ -``` - -## 테스트 실패의 형태 - -:::evidence key="analysis-finding-a18-f002" alt="분석 문서 final/document.md#a18 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a18 발췌 — 15줄" zoom="true" -::: - -## 기록하는 것은 가드의 비대칭이다 - -```java -@Test -void everyLaneMatchesItsContract() { - Assumptions.assumeTrue(dockerComposeIsAvailable(), "docker compose is not on this machine"); - ProcessResult result = run(List.of("./scripts/verify-compose-profile-contracts.sh")); - assertThat(result.exitCode()).isZero(); -} -``` - -docker compose는 가정으로 확인하고 `jq`는 확인하지 않는다. P3. - -## 확인하지 못한 것 - -없는 도구를 설치하지 않았다. 분석 환경을 바꾸지 않는다는 원칙 때문이다. 계약 일치 자체는 독립 구현으로 따로 확인했다. - -저장소 결함이 아니다. 분석 컨테이너에 jq가 없고, 위임된 스크립트가 그것을 요구하며 exit 78로 정직하게 실패한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-grpc-advanced-diagnostics-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-grpc-advanced-diagnostics-f03.md deleted file mode 100644 index 1cc37fb..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-grpc-advanced-diagnostics-f03.md +++ /dev/null @@ -1,95 +0,0 @@ ---- -kind: CASE -slug: grpc-advanced-diagnostics-f03 -title: "실환경 증거" 가 두 리프에 반씩 있고 서로 만나지 않는다 -topic: contract-domain-and-bounds -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-advanced-diagnostics-f03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-advanced-diagnostics-f03 - file: ../../../final/evidence/rendered/grpc-advanced-diagnostics-f03.svg -evidence: - - ../../../final/evidence/raw/grpc-advanced-diagnostics-f03.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-advanced-diagnostics#L218 이다. -module: grpc-advanced-diagnostics -priority: P3 ---- - -# "실환경 증거" 가 두 리프에 반씩 있고 서로 만나지 않는다 - -이 리프가 능력별로 무엇이 실환경인지 정의한다. 그리고 grpc-advanced-bootstrap 이 승격 증거로 그것을 요구한다. - -## 문제 - -이 리프가 능력별로 무엇이 실환경인지 정의한다. - -그리고 grpc-advanced-bootstrap 이 승격 증거로 그것을 요구한다. - -## 결론 - -GrpcAdvancedPromotionGate.evaluate 는 그 불리언이 거짓이면 "xds has no real environment test" 를 차단 사유로 낸다. - -그 불리언을 무엇으로 채워야 하는지는 그쪽에서 답하지 않고, 답하는 코드가 이 리프에 있는데 두 쪽이 서로를 부르지 않는다. - -결과: GrpcAdvancedPromotionEvidence.complete(XDS, 7일) 은 realEnvironmentTest = true 를 그냥 넣는다. - -xDS 통제 평면이 실제로 있었는지와 무관하다. - -이 리프의 javadoc 이 경계한 상태 — "a suite that runs without the infrastructure passes and establishes nothing" — 를 승격 게이트가 그대로 통과시킬 수 있다. - -두 리프 모두 배선되지 않았고 승격은 사람이 수행한다. - -다만 이 두 조각이 존재하는 이유가 "그 판단을 코드로 적어 두는 것" 이므로, 판단의 절반이 다른 절반을 부르지 않는 것은 그 목적에 어긋난다. - -grpc-advanced-edition §17.2 가 같은 가족에서 같은 모양을 기록했다 — 두 승격 게이트가 서로를 부르지 않는다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : GrpcAdvancedPromotionGate 참조 14건 검색과 두 리프의 실환경 증거 정의 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-advanced-diagnostics#L218 에 있다. - -## 본문 - - - -이 리프가 능력별로 무엇이 실환경인지 정의한다. - -```java -public static Set requiredFor(GrpcAdvancedCapability capability) { … } -public static List missingInfrastructure(GrpcAdvancedCapability capability, Set available) { … } -``` - -그리고 `grpc-advanced-bootstrap` 이 승격 증거로 그것을 요구하는데, 증거는 불리언 하나다. - -## GrpcAdvancedPromotionGate 참조 위치 - -:::evidence key="grpc-advanced-diagnostics-f03" alt="코드베이스에서 GrpcAdvancedPromotionGate 를 검색한 출력 14줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcAdvancedPromotionGate 코드베이스 검색 — 14줄 · exit 0" zoom="true" -::: - -## 두 쪽이 서로를 부르지 않는다 - -`GrpcAdvancedPromotionGate.evaluate` 는 그 불리언이 거짓이면 "xds has no real environment test" 를 차단 사유로 낸다. 그 불리언을 무엇으로 채워야 하는지는 그쪽에서 답하지 않고, 답하는 코드가 이 리프에 있다. 결과적으로 `GrpcAdvancedPromotionEvidence.complete(XDS, 7일)` 은 `realEnvironmentTest = true` 를 그냥 넣는다 — xDS 통제 평면이 실제로 있었는지와 무관하게. 이 리프의 javadoc 이 경계한 상태("a suite that runs without the infrastructure passes and establishes nothing")를 승격 게이트가 그대로 통과시킬 수 있다. - -## 왜 P3 인가 - -두 리프 모두 배선되지 않았고 승격은 사람이 수행한다. 다만 이 두 조각이 존재하는 이유가 "그 판단을 코드로 적어 두는 것" 이므로, 판단의 절반이 다른 절반을 부르지 않는 것은 그 목적에 어긋난다. `grpc-advanced-edition` §17.2 가 같은 가족에서 같은 모양을 기록했다. - -## 수정 - -`GrpcAdvancedPromotionEvidence.realEnvironmentTest` 를 불리언 대신 `Set availableInfrastructure` 로 바꾸고, 게이트가 `missingInfrastructure(capability, available)` 를 불러 그 결과를 차단 사유에 합친다. 그러면 "실환경 테스트를 했다" 가 선언이 아니라 능력별 목록에 대한 대조가 된다. - -## 확인하지 못한 것 - -두 리프를 실제로 이어 승격 증거가 흐르는지 확인하지 않았다. 각 리프가 선언한 항목의 대조로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-grpc-advanced-resilience-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-grpc-advanced-resilience-f02.md deleted file mode 100644 index 904ea31..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-grpc-advanced-resilience-f02.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -kind: CASE -slug: grpc-advanced-resilience-f02 -title: 대체 선택기는 사용자 정의 선택기가 받는 보호를 받지 않는다 -topic: contract-domain-and-bounds -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-advanced-resilience-f02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-advanced-resilience-f02 - file: ../../../final/evidence/rendered/grpc-advanced-resilience-f02.svg -evidence: - - ../../../final/evidence/raw/grpc-advanced-resilience-f02.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-advanced-resilience#L155 이다. -module: grpc-advanced-resilience -priority: P3 ---- - -# 대체 선택기는 사용자 정의 선택기가 받는 보호를 받지 않는다 - -fallback.pick(selectable) 은 감싸이지 않는다. 대체가 던지면 예외가 그대로 올라가고, 널이나 목록 밖 엔드포인트를 돌려주면 그대로 결정이 된다. - -## 문제 - -fallback.pick(selectable) 은 감싸이지 않는다. - -대체가 던지면 예외가 그대로 올라가고, 널이나 목록 밖 엔드포인트를 돌려주면 그대로 결정이 된다. - -## 결론 - -기본 생성자는 플랫폼의 라운드 로빈을 대체로 쓰므로 지금은 안전하다. - -그러나 두 인자 생성자가 임의의 선택기를 대체로 받고, 그 인자에는 아무 제약이 없다. - -이 클래스의 존재 이유가 "선택기 버그가 가용성이 아니라 균형을 저하시키게 하는 것" 인데, 대체 선택기의 버그는 가용성을 저하시킨다. - -수정은 대체 호출도 같은 검사를 지나게 하거나(그 결과가 널이거나 목록 밖이면 플랫폼 라운드 로빈으로 한 번 더 떨어진다), 두 인자 생성자를 없애 대체를 플랫폼 것으로 고정하는 것이다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : 사용자 정의 선택기와 대체 선택기의 호출 지점에 걸린 방어 코드 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-advanced-resilience#L155 에 있다. - -## 본문 - - - -`fallback.pick(selectable)` 은 감싸이지 않는다. 대체가 던지면 예외가 그대로 올라가고, 널이나 목록 밖 엔드포인트를 돌려주면 그대로 결정이 된다. - -## 대체 선택기가 감싸이지 않는다 - -:::evidence key="grpc-advanced-resilience-f02" alt="분석 문서 final/document.md#a20-grpc-advanced-resilience 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-advanced-resilience 발췌 — 15줄" zoom="true" -::: - -## 지금은 안전하다 - -기본 생성자는 플랫폼의 라운드 로빈을 대체로 쓴다. 그러나 두 인자 생성자가 임의의 선택기를 대체로 받고, 그 인자에는 아무 제약이 없다. - -## 클래스의 존재 이유가 뒤집힌다 - -"선택기 버그가 가용성이 아니라 균형을 저하시키게 하는 것" 인데, 대체 선택기의 버그는 가용성을 저하시킨다. 수정은 대체 호출도 같은 검사를 지나게 하거나(결과가 널이거나 목록 밖이면 플랫폼 라운드 로빈으로 한 번 더 떨어진다), 두 인자 생성자를 없애 대체를 플랫폼 것으로 고정하는 것이다. - -## 확인하지 못한 것 - -대체 선택기가 널이나 목록 밖 엔드포인트를 돌려주는 상황을 실행으로 재현하지 않았다. 두 호출 지점의 감싸기 유무로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-grpc-advanced-streaming-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-grpc-advanced-streaming-f01.md deleted file mode 100644 index dc540c6..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-grpc-advanced-streaming-f01.md +++ /dev/null @@ -1,84 +0,0 @@ ---- -kind: CASE -slug: grpc-advanced-streaming-f01 -title: 클래스가 비판한 무제한 증가를 형제 맵이 그대로 한다 -topic: contract-domain-and-bounds -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-advanced-streaming-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-advanced-streaming-f01 - file: ../../../final/evidence/rendered/grpc-advanced-streaming-f01.svg -evidence: - - ../../../final/evidence/raw/grpc-advanced-streaming-f01.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-advanced-streaming#L125 이다. -module: grpc-advanced-streaming -priority: P3 ---- - -# 클래스가 비판한 무제한 증가를 형제 맵이 그대로 한다 - -클래스 javadoc 이 집합 방식을 거부한 이유가 무제한 증가다 — "A set grows without bound for the life of a session". 체크포인트는 그 비판을 지킨다. - -## 문제 - -클래스 javadoc 이 집합 방식을 거부한 이유가 무제한 증가다 — "A set grows without bound for the life of a session". - -체크포인트는 그 비판을 지킨다. - -## 결론 - -세션당 항목 하나이고 순번만 앞으로 간다. - -형제 맵은 지키지 않는다. - -제거는 endSession 뿐이고, 그때 그 세션의 접두를 가진 키를 전부 지운다. - -그러므로 결과 참조를 기록하는 세션에서는 적용된 메시지 수만큼 항목이 쌓인다. - -상한도 만료도 없다. - -클래스 javadoc 은 다르게 말한다. - -작은 창이 코드에 없다. - -체크포인트가 앞으로 가도 그 이전 결과들은 남는다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : 체크포인트와 형제 맵의 제거 경로 유무 대조, 클래스 javadoc 의 거부 근거 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-advanced-streaming#L125 에 있다. - -## 본문 - - - -클래스 javadoc 이 집합 방식을 거부한 이유가 무제한 증가다 — "A set grows without bound for the life of a session". - -## javadoc 이 집합 방식을 거부한 이유 - -:::evidence key="grpc-advanced-streaming-f01" alt="분석 문서 final/document.md#a20-grpc-advanced-streaming 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-advanced-streaming 발췌 — 15줄" zoom="true" -::: - -## 체크포인트는 그 비판을 지키고 형제 맵은 지키지 않는다 - -체크포인트는 세션당 항목 하나이고 순번만 앞으로 간다. 형제 맵의 제거는 `endSession` 뿐이고, 그때 그 세션의 접두를 가진 키를 전부 지운다. 그러므로 결과 참조를 기록하는 세션에서는 적용된 메시지 수만큼 항목이 쌓인다 — 상한도 만료도 없다. 클래스 javadoc 은 다르게 말한다: 작은 창이 코드에 없다. - -## 실제로 필요한 창은 좁다 - -판정이 `alreadyApplied(sequence)` 로 재생을 결정하고, 재생 응답에 쓰이는 것은 그 순번의 결과 하나다. 체크포인트보다 한참 뒤처진 순번의 결과가 필요할 상황은 재개 직후의 좁은 구간뿐이다. 수정은 창을 실제로 만드는 것이다 — 세션당 최근 N개만 유지하거나, 체크포인트가 앞으로 갈 때 그보다 오래된 항목을 지운다. 후자가 자바독의 서술과 정확히 같다. - -## 확인하지 못한 것 - -장시간 실행으로 이 맵의 증가를 측정하지 않았다. 제거 경로가 없다는 것으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-grpc-advanced-streaming-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-grpc-advanced-streaming-f02.md deleted file mode 100644 index 2b2c538..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-grpc-advanced-streaming-f02.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -kind: CASE -slug: grpc-advanced-streaming-f02 -title: 클라이언트 스트림 정책의 네 상한 중 둘은 읽는 코드가 없다 -topic: contract-domain-and-bounds -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-advanced-streaming-f02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-advanced-streaming-f02 - file: ../../../final/evidence/rendered/grpc-advanced-streaming-f02.svg -evidence: - - ../../../final/evidence/raw/grpc-advanced-streaming-f02.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-advanced-streaming#L159 이다. -module: grpc-advanced-streaming -priority: P3 ---- - -# 클라이언트 스트림 정책의 네 상한 중 둘은 읽는 코드가 없다 - -GrpcClientStreamPolicy javadoc 이 네 상한을 모두 든다. 저장소 전체에서 접근자 호출을 세면 둘이 0 이다. - -## 문제 - -GrpcClientStreamPolicy javadoc 이 네 상한을 모두 든다. - -저장소 전체에서 접근자 호출을 세면 둘이 0 이다. - -## 결론 - -Advanced 가족이 미배선이라는 사실과는 별개다 — 이 리프 안에도 그 값을 쓰는 코드가 없다. - -수요 상한을 강제하는 GrpcDemandController 는 GrpcManualFlowControlPolicy 를 쓰고, 이 정책을 보지 않는다. - -wholeStreamRetryAllowed() 는 항상 거짓을 돌려주는 형태이므로 그 자체가 문서화 장치다. - -나머지 둘은 강제 지점이 필요하다. - -수정은 상한을 강제하는 지점을 만들거나(수신 경로에 속도·미확인 수 검사), 강제되지 않는 값이 강제되는 것처럼 읽히지 않도록 자바독을 낮추는 것이다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : GrpcClientStreamPolicy 참조 10건 검색으로 네 상한의 접근자 호출 수 집계 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-advanced-streaming#L159 에 있다. - -## 본문 - - - -`GrpcClientStreamPolicy` javadoc 이 네 상한을 모두 든다. 저장소 전체에서 접근자 호출을 세면 둘이 0 이다. - -## GrpcClientStreamPolicy 참조 위치 - -:::evidence key="grpc-advanced-streaming-f02" alt="코드베이스에서 GrpcClientStreamPolicy 를 검색한 출력 10줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcClientStreamPolicy 코드베이스 검색 — 10줄 · exit 0" zoom="true" -::: - -## Advanced 미배선과는 별개다 - -이 리프 안에도 그 값을 쓰는 코드가 없다. 수요 상한을 강제하는 `GrpcDemandController` 는 `GrpcManualFlowControlPolicy` 를 쓰고, 이 정책을 보지 않는다. - -## 넷 중 하나는 그 자체가 문서화 장치다 - -`wholeStreamRetryAllowed()` 는 항상 거짓을 돌려주는 형태다. 나머지 둘은 강제 지점이 필요하다. 수정은 상한을 강제하는 지점을 만들거나(수신 경로에 속도·미확인 수 검사), 강제되지 않는 값이 강제되는 것처럼 읽히지 않도록 자바독을 낮추는 것이다. - -## 확인하지 못한 것 - -실제 스트림을 열어 두 상한이 무시되는 것을 관측하지 않았다. 배선 경로가 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-grpc-core-api-f05.md b/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-grpc-core-api-f05.md deleted file mode 100644 index ca79fd3..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-grpc-core-api-f05.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -kind: CASE -slug: grpc-core-api-f05 -title: 메타데이터 예산의 두 성분 중 하나는 강제되지 않고, 나머지 하나는 바이트가 아니라 문자를 센다 -topic: contract-domain-and-bounds -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-core-api-f05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-core-api-f05 - file: ../../../final/evidence/rendered/grpc-core-api-f05.svg -evidence: - - ../../../final/evidence/raw/grpc-core-api-f05.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-core-api#L219 이다. -module: grpc-core-api -priority: P3 ---- - -# 메타데이터 예산의 두 성분 중 하나는 강제되지 않고, 나머지 하나는 바이트가 아니라 문자를 센다 - -GrpcMetadataBudget 은 세 성분을 갖는다 — maxTotalBytes·maxUserDefinedBytes·maxEntries. check(...) 가 보는 것은 뒤의 둘뿐이다. - -## 문제 - -GrpcMetadataBudget 은 세 성분을 갖는다 — maxTotalBytes·maxUserDefinedBytes·maxEntries. - -check(...) 가 보는 것은 뒤의 둘뿐이다. - -## 결론 - -저장소 전체에서 이 접근자를 부르는 곳은 compact constructor 의 순서 가드와 테스트 단언 하나뿐이다. - -자바독은 그 이유를 설명한다 — 하드 총계를 넘기는 것은 프레임워크가 던지는 전송 거절이고, 여기서 함께 검사하면 "고칠 수 있는 쪽" 과 "고칠 수 없는 쪽" 이 한 자리에서 발견된다는 것. - -판단은 옳다. - -다만 그 결과로 이 record 는 자기가 쓰지 않는 수를 성분으로 들고 있고, 이름은 그것이 강제된다고 읽힌다. - -성분 이름은 ...Bytes 인데 세는 것은 String.length(), 즉 UTF-16 코드 단위다. - -키는 [a-z0-9._-] 로 제한되어 ASCII 지만 값에는 문자 집합 제약이 없다. - -다중 바이트 문자를 담은 값은 실제 프레임보다 적게 계산된다. - -gRPC 의 ASCII 메타데이터 값은 프로토콜 상 인쇄 가능 ASCII 여야 하므로 실무에서는 대개 일치한다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : GrpcMetadataBudget 참조 26건 검색과 check 가 실제로 보는 성분 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-core-api#L219 에 있다. - -## 본문 - - - -`GrpcMetadataBudget` 은 세 성분을 갖는다 — `maxTotalBytes`·`maxUserDefinedBytes`·`maxEntries`. `check(...)` 가 보는 것은 뒤의 둘뿐이다. 저장소 전체에서 `maxTotalBytes` 접근자를 부르는 곳은 compact constructor 의 순서 가드와 테스트 단언 하나뿐이다. - -## GrpcMetadataBudget 참조 위치 - -:::evidence key="grpc-core-api-f05" alt="코드베이스에서 GrpcMetadataBudget 를 검색한 출력 26줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcMetadataBudget 코드베이스 검색 — 26줄 · exit 0" zoom="true" -::: - -## 그 판단은 옳다 - -자바독이 이유를 설명한다 — 하드 총계를 넘기는 것은 프레임워크가 던지는 전송 거절이고, 여기서 함께 검사하면 "고칠 수 있는 쪽" 과 "고칠 수 없는 쪽" 이 한 자리에서 발견된다. 다만 그 결과로 이 record 는 자기가 쓰지 않는 수를 성분으로 들고 있고, 이름은 그것이 강제된다고 읽힌다. - -## 이름은 Bytes 인데 세는 것은 문자다 - -`String.length()`, 즉 UTF-16 코드 단위다. 키는 `[a-z0-9._-]` 로 제한되어 ASCII 지만 값에는 문자 집합 제약이 없으므로, 다중 바이트 문자를 담은 값은 실제 프레임보다 적게 계산된다. - -## 실무에서는 대개 일치한다 - -gRPC 의 ASCII 메타데이터 값은 프로토콜 상 인쇄 가능 ASCII 여야 한다. 다만 그 제약을 이 클래스가 검사하지 않으므로, 일치는 보장이 아니라 관행이다. 수정은 둘 다 작다 — `value.getBytes(StandardCharsets.US_ASCII).length` 로 세거나 값의 문자 집합을 `GrpcMetadataKey.Kind.ASCII` 에 맞춰 검증하고, `maxTotalBytes` 는 성분에서 빼고 javadoc 의 서술로 남긴다. - -## 확인하지 못한 것 - -다국어 헤더 값으로 문자 수와 바이트 수의 차이를 실행으로 관측하지 않았다. 계산 대상의 코드 형태로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-grpc-discovery-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-grpc-discovery-f01.md deleted file mode 100644 index abf085f..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-grpc-discovery-f01.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -kind: CASE -slug: grpc-discovery-f01 -title: 프로파일이 스트림 재접속 예산을 선언하는데 그것이 함의하는 DNS 갱신 주기를 정하지 않는다 -topic: contract-domain-and-bounds -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-discovery-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-discovery-f01 - file: ../../../final/evidence/rendered/grpc-discovery-f01.svg -evidence: - - ../../../final/evidence/raw/grpc-discovery-f01.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-discovery#L152 이다. -module: grpc-discovery -priority: P3 ---- - -# 프로파일이 스트림 재접속 예산을 선언하는데 그것이 함의하는 DNS 갱신 주기를 정하지 않는다 - -GrpcKubernetesProfile 은 세 시간 값을 다룬다. 검증기는 앞의 둘만 비교한다 — 배수 유예가 재접속 예산보다 짧으면 위반. - -## 문제 - -GrpcKubernetesProfile 은 세 시간 값을 다룬다. - -검증기는 앞의 둘만 비교한다 — 배수 유예가 재접속 예산보다 짧으면 위반. - -## 결론 - -셋째는 비교 대상에 없다. - -그래서 headlessStreaming()(재접속 예산 5초, 배수 유예 30초)에서 갱신 주기는 여전히 30초다. - -롤아웃으로 스트림이 끊긴 클라이언트가 5초 예산 안에 재접속하려 할 때, 그 클라이언트의 DNS 캐시는 최대 30초 동안 사라진 파드 주소를 들고 있을 수 있다. - -그 실패가 GrpcResolverProfile 자신의 javadoc 이 서술한 것이다 — "A channel that resolved once at startup keeps sending to addresses that stopped existing an hour ago; the calls fail with UNAVAILABLE and the deployment looks unhealthy long after it finished." 수정은 갱신 주기를 재접속 예산에서 파생시키거나(예: 예산 이하), 검증기에 세 값의 순서 규칙을 추가하는 것이다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : GrpcKubernetesProfile 참조 26건 검색과 검증기가 비교하는 시간 값 쌍 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-discovery#L152 에 있다. - -## 본문 - - - -`GrpcKubernetesProfile` 은 세 시간 값을 다룬다. 검증기는 앞의 둘만 비교한다 — 배수 유예가 재접속 예산보다 짧으면 위반. - -## GrpcKubernetesProfile 참조 위치 - -:::evidence key="grpc-discovery-f01" alt="코드베이스에서 GrpcKubernetesProfile 를 검색한 출력 26줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcKubernetesProfile 코드베이스 검색 — 26줄 · exit 0" zoom="true" -::: - -## 셋째는 비교 대상에 없다 - -그래서 `headlessStreaming()`(재접속 예산 5초, 배수 유예 30초)에서 갱신 주기는 여전히 30초다. - -## 그 조합이 만드는 상황 - -롤아웃으로 스트림이 끊긴 클라이언트가 5초 예산 안에 재접속하려 할 때, 그 클라이언트의 DNS 캐시는 최대 30초 동안 사라진 파드 주소를 들고 있을 수 있다. 그 실패가 `GrpcResolverProfile` 자신의 javadoc 이 서술한 것이다 — "A channel that resolved once at startup keeps sending to addresses that stopped existing an hour ago; the calls fail with `UNAVAILABLE` and the deployment looks unhealthy long after it finished." - -## 수정 - -갱신 주기를 재접속 예산에서 파생시키거나(예: 예산 이하), 검증기에 세 값의 순서 규칙을 추가하는 것이다. - -## 확인하지 못한 것 - -실제 DNS 리졸버로 헤드리스 레코드를 조회해 갱신 주기와 재접속 예산의 관계를 관측하지 않았다. 이 리프는 주소 수를 입력으로 받는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-grpc-operation-ledger-jpa-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-grpc-operation-ledger-jpa-f01.md deleted file mode 100644 index c784f31..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-grpc-operation-ledger-jpa-f01.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -kind: CASE -slug: grpc-operation-ledger-jpa-f01 -title: 낙관적 잠금 컬럼이 없어 전이 가드가 메모리 안에만 있다 -topic: contract-domain-and-bounds -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-operation-ledger-jpa-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-operation-ledger-jpa-f01 - file: ../../../final/evidence/rendered/grpc-operation-ledger-jpa-f01.svg -evidence: - - ../../../final/evidence/raw/grpc-operation-ledger-jpa-f01.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-operation-ledger-jpa#L184 이다. -module: grpc-operation-ledger-jpa -priority: P3 ---- - -# 낙관적 잠금 컬럼이 없어 전이 가드가 메모리 안에만 있다 - -requireInProgress() 가 두 번째 종결 전이를 막는다. 그 가드는 한 영속성 컨텍스트 안의 인스턴스 상태에만 적용된다. - -## 문제 - -requireInProgress() 가 두 번째 종결 전이를 막는다. - -그 가드는 한 영속성 컨텍스트 안의 인스턴스 상태에만 적용된다. - -## 결론 - -엔티티에 @Version 이 없으므로 두 트랜잭션이 같은 행을 읽어 각각 전이하면 나중 쓰기가 앞의 것을 덮는다. - -DB 의 세 CHECK 제약은 행의 모양을 지키지 지 전이 순서를 지키지 않는다. - -COMMITTED 행이 다른 결과 참조로 갱신되는 것을 막는 제약이 없다. - -청구가 배타적이라는 설계 전제 아래서는 도달성이 낮다. - -다만 §17.1 을 고치면 이 전제가 실제로 성립하는지가 함께 확인되어야 한다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : 엔티티의 잠금 컬럼 선언 유무와 requireInProgress 가드가 적용되는 범위 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-operation-ledger-jpa#L184 에 있다. - -## 본문 - - - -`requireInProgress()` 가 두 번째 종결 전이를 막는다. 그 가드는 한 영속성 컨텍스트 안의 인스턴스 상태에만 적용된다. - -## requireInProgress() 가 적용되는 범위 - -:::evidence key="grpc-operation-ledger-jpa-f01" alt="분석 문서 final/document.md#a20-grpc-operation-ledger-jpa 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-operation-ledger-jpa 발췌 — 15줄" zoom="true" -::: - -## @Version 이 없다 - -엔티티에 낙관적 잠금 컬럼이 없으므로 두 트랜잭션이 같은 행을 읽어 각각 전이하면 나중 쓰기가 앞의 것을 덮는다. - -## DB 제약은 모양을 지키지 순서를 지키지 않는다 - -세 CHECK 제약은 행의 모양을 지킨다. `COMMITTED` 행이 다른 결과 참조로 갱신되는 것을 막는 제약이 없다. - -## 청구가 배타적이라는 전제 아래서는 도달성이 낮다 - -다만 §17.1 을 고치면 이 전제가 실제로 성립하는지가 함께 확인되어야 한다. - -## 확인하지 못한 것 - -실제 데이터베이스로 청구를 두 번 돌려 재현하지 않았다. 동시 청구를 실제 커넥션 둘로도 재현하지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-messaging-core-api-f05.md b/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-messaging-core-api-f05.md deleted file mode 100644 index a2f9364..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-messaging-core-api-f05.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -kind: CASE -slug: messaging-core-api-f05 -title: WireSafeText의 규칙이 leaf 경계에서 멈춘다 -topic: contract-domain-and-bounds -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-core-api-f05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-core-api-f05 - file: ../../../final/evidence/rendered/messaging-core-api-f05.svg -evidence: - - ../../../final/evidence/raw/messaging-core-api-f05.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-core-api#L880 이다. -module: messaging-core-api -priority: P3 ---- - -# WireSafeText의 규칙이 leaf 경계에서 멈춘다 - -WireSafeText의 leaf 밖 참조 0. 제어문자·인코딩 경계를 각자 검사하는 곳이 저장소에 최소 15개. - -## 문제 - -WireSafeText의 leaf 밖 참조 0. - -제어문자·인코딩 경계를 각자 검사하는 곳이 저장소에 최소 15개. - -## 결론 - -javadoc이 "Each copy of this check ... - -was one more place for the rule to drift"라고 적었고 그 통합을 leaf 안에서만 했다. - -저장소 수준에서는 같은 drift가 그대로 남아 있다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : WireSafeText 참조 11건 검색과 같은 검사를 각자 구현한 지점 집계 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-core-api#L880 에 있다. - -## 본문 - - - -`WireSafeText`의 leaf 밖 참조가 0이다. 제어문자·인코딩 경계를 각자 검사하는 곳이 저장소에 최소 15개 있다. - -## WireSafeText 참조 위치 - -:::evidence key="messaging-core-api-f05" alt="코드베이스에서 WireSafeText 를 검색한 출력 11줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="WireSafeText 코드베이스 검색 — 11줄 · exit 0" zoom="true" -::: - -## javadoc 이 통합을 이유로 들었다 - -"Each copy of this check ... was one more place for the rule to drift" 라고 적었고, 그 통합을 leaf 안에서만 했다. 저장소 수준에서는 같은 drift가 그대로 남아 있다. - -## 후보 - -규칙을 공유 위치(`shared-contract`)로 올리거나, leaf 경계를 이유로 중복을 명시적으로 수용한다고 적는다. 저장소 전역 판단이므로 cross-scope 소유이고 여기서는 관측만 기록한다. - -## 확인하지 못한 것 - -각 지점의 검사가 실제로 이 규칙과 어긋나는 입력을 통과시키는지 확인하지 않았다. 참조 경계와 구현 지점 수로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-messaging-observability-f07.md b/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-messaging-observability-f07.md deleted file mode 100644 index f57d102..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-messaging-observability-f07.md +++ /dev/null @@ -1,75 +0,0 @@ ---- -kind: CASE -slug: messaging-observability-f07 -title: extract가 손상된 추적 헤더에 분류되지 않은 예외를 던진다 -topic: contract-domain-and-bounds -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-observability-f07 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-observability-f07 - file: ../../../final/evidence/rendered/messaging-observability-f07.svg -evidence: - - ../../../final/evidence/raw/messaging-observability-f07.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-observability#L726 이다. -module: messaging-observability -priority: P3 ---- - -# extract가 손상된 추적 헤더에 분류되지 않은 예외를 던진다 - -MessagingTracer.extract가 new TraceContext(...)를 부르고, 그 생성자는 W3C 문법·바이트 상한·all-zero를 검사해 IllegalArgumentException을 던진다. extract는 잡지 않는다. - -## 관계 - -- **타입이 문서화한 불변식은 타입이 강제한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 문제 - -MessagingTracer.extract가 new TraceContext(...)를 부르고, 그 생성자는 W3C 문법·바이트 상한·all-zero를 검사해 IllegalArgumentException을 던진다. - -extract는 잡지 않는다. - -## 결론 - -다른 시스템이 보낸 메시지의 헤더는 신뢰할 수 없는 입력이다. - -손상된 traceparent 하나가 MessagingException이 아닌 예외로 소비 경로를 끊는다 — 추적이 없어야 할 자리에서 메시지 처리가 실패한다. - -messaging-cloudevents의 id 파싱과 같은 형태다(그쪽 §17). - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : MessagingTracer 참조 4건 검색과 생성자가 던지는 조건 및 extract 의 catch 유무 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-observability#L726 에 있다. - -## 본문 - - - -`MessagingTracer.extract`가 `new TraceContext(...)`를 부르고, 그 생성자는 W3C 문법·바이트 상한·all-zero를 검사해 `IllegalArgumentException`을 던진다. `extract`는 잡지 않는다. - -## MessagingTracer 참조 위치 - -:::evidence key="messaging-observability-f07" alt="코드베이스에서 MessagingTracer 를 검색한 출력 4줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingTracer 코드베이스 검색 — 4줄 · exit 0" zoom="true" -::: - -## 신뢰할 수 없는 입력이다 - -다른 시스템이 보낸 메시지의 헤더다. 손상된 `traceparent` 하나가 `MessagingException`이 아닌 예외로 소비 경로를 끊는다 — 추적이 없어야 할 자리에서 메시지 처리가 실패한다. `messaging-cloudevents`의 id 파싱과 같은 형태다(그쪽 §17). - -## 확인하지 못한 것 - -손상된 traceparent 가 실제 배포에서 얼마나 자주 오는지 확인하지 않았다. 생성자의 검사와 extract 의 예외 처리 부재로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-messaging-reliability-api-f05.md b/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-messaging-reliability-api-f05.md deleted file mode 100644 index 30e8a5f..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-messaging-reliability-api-f05.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -kind: CASE -slug: messaging-reliability-api-f05 -title: inbox 보존 규칙이 문서로만 있다 -topic: contract-domain-and-bounds -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-reliability-api-f05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-reliability-api-f05 - file: ../../../final/evidence/rendered/messaging-reliability-api-f05.svg -evidence: - - ../../../final/evidence/raw/messaging-reliability-api-f05.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-reliability-api#L725 이다. -module: messaging-reliability-api -priority: P3 ---- - -# inbox 보존 규칙이 문서로만 있다 - -InboxRepository.purgeProcessedBefore javadoc이 "Retention must outlive the broker's maximum redelivery window, otherwise a late redelivery arrives after its inbox row was pruned and is processed a second time"라고 한다. 그 비교를 하는 코드가 이 leaf에도 messaging-policy의 프로파일 검증기에도 없다. - -## 관계 - -- **계약만 담는 leaf도 계약의 거절 조건은 자기 레인에서 검증한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **record의 `equals`를 좁히면 이유를 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **호출 컨텍스트가 계약이면 그 컨텍스트를 검증할 수단을 함께 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 문제 - -InboxRepository.purgeProcessedBefore javadoc이 "Retention must outlive the broker's maximum redelivery window, otherwise a late redelivery arrives after its inbox row was pruned and is processed a second time"라고 한다. - -그 비교를 하는 코드가 이 leaf에도 messaging-policy의 프로파일 검증기에도 없다. - -## 결론 - -위반의 결과가 부작용의 이중 실행이다 — Inbox가 존재하는 이유 그 자체가 무효화된다. - -그리고 위반이 조용하다: 짧은 보존은 정상 동작처럼 보이고 늦은 재전달이 올 때만 드러난다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : InboxRepository 참조 20건 검색과 보존·재전달 창을 비교하는 코드의 존재 여부 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-reliability-api#L725 에 있다. - -## 본문 - - - -`InboxRepository.purgeProcessedBefore` javadoc이 "Retention must outlive the broker's maximum redelivery window, otherwise a late redelivery arrives after its inbox row was pruned and is processed a second time"라고 한다. 그 비교를 하는 코드가 이 leaf에도 `messaging-policy`의 프로파일 검증기에도 없다. - -## InboxRepository 참조 위치 - -:::evidence key="messaging-reliability-api-f05" alt="코드베이스에서 InboxRepository 를 검색한 출력 20줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="InboxRepository 코드베이스 검색 — 20줄 · exit 0" zoom="true" -::: - -## 위반의 결과가 부작용의 이중 실행이다 - -Inbox가 존재하는 이유 그 자체가 무효화된다. 그리고 위반이 조용하다 — 짧은 보존은 정상 동작처럼 보이고 늦은 재전달이 올 때만 드러난다. - -## 확인하지 못한 것 - -inbox 보존 기간이 실제 배포에서 브로커 재전달 창보다 긴지 확인할 수 없었다. 비교하는 코드가 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-messaging-reliability-api-f08.md b/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-messaging-reliability-api-f08.md deleted file mode 100644 index 697d286..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/case/case-messaging-reliability-api-f08.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -kind: CASE -slug: messaging-reliability-api-f08 -title: 포트가 bounded/unbounded purge 두 오버로드를 나란히 노출하고, 호출자가 무제한 쪽을 고른다 -topic: contract-domain-and-bounds -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-reliability-api-f08 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-reliability-api-f08 - file: ../../../final/evidence/rendered/messaging-reliability-api-f08.svg -evidence: - - ../../../final/evidence/raw/messaging-reliability-api-f08.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-reliability-api#L752 이다. -module: messaging-reliability-api -priority: P3 ---- - -# 포트가 bounded/unbounded purge 두 오버로드를 나란히 노출하고, 호출자가 무제한 쪽을 고른다 - -InboxRepository와 OutboxRepository가 각각 purge*Before(Instant)와 purge*Before(Instant, int)를 선언한다. 후자에 호출 지점이 0이고 두 cleanup job이 전자를 부른다. - -## 관계 - -- **계약만 담는 leaf도 계약의 거절 조건은 자기 레인에서 검증한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **record의 `equals`를 좁히면 이유를 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **호출 컨텍스트가 계약이면 그 컨텍스트를 검증할 수단을 함께 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 문제 - -InboxRepository와 OutboxRepository가 각각 purge*Before(Instant)와 purge*Before(Instant, int)를 선언한다. - -후자에 호출 지점이 0이고 두 cleanup job이 전자를 부른다. - -## 결론 - -§12.1(a)의 두 세대 전이와 같은 형태다 — 한 인터페이스가 안전한 형태와 그렇지 않은 형태를 나란히 두고, @Deprecated도 이름 차이도 없으며, 호출자가 짧은 쪽을 골랐다. - -두 경우 모두 포트의 형태가 오용을 가능하게 했다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : InboxRepository 참조 20건 검색과 두 오버로드의 호출 지점 집계 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-reliability-api#L752 에 있다. - -## 본문 - - - -`InboxRepository`와 `OutboxRepository`가 각각 `purge*Before(Instant)`와 `purge*Before(Instant, int)`를 선언한다. 후자에 호출 지점이 0이고 두 cleanup job이 전자를 부른다. - -## InboxRepository 참조 위치 - -:::evidence key="messaging-reliability-api-f08" alt="코드베이스에서 InboxRepository 를 검색한 출력 20줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="InboxRepository 코드베이스 검색 — 20줄 · exit 0" zoom="true" -::: - -## §12.1(a)의 두 세대 전이와 같은 형태다 - -한 인터페이스가 안전한 형태와 그렇지 않은 형태를 나란히 두고, `@Deprecated`도 이름 차이도 없으며, 호출자가 짧은 쪽을 골랐다. 두 경우 모두 포트의 형태가 오용을 가능하게 했다. - -## 확인하지 못한 것 - -무제한 오버로드가 실제 백로그에서 무엇을 잠그는지 측정하지 않았다. 판정은 구현 리프의 SSOT 가 소유하고 여기서는 포트 형태의 기여만 남겼다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/question/openquestion-messaging-claim-check-f05.md b/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/question/openquestion-messaging-claim-check-f05.md deleted file mode 100644 index f0780b0..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/question/openquestion-messaging-claim-check-f05.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -kind: QUESTION -slug: messaging-claim-check-f05 -title: 보존 sweep이 없다 -topic: contract-domain-and-bounds -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: open-question:messaging-claim-check-f05 -questionStatus: OPEN -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-claim-check#L543 ---- - -# 보존 sweep이 없다 - -실패한 발행이 남긴 claim check 객체를 회수할 주체가 저장소 안에 없다. 저장소 자체의 lifecycle 정책이 그 자리를 대신할 수 있지만, 정책 값과 그 lifecycle 이 연결돼 있지 않다. - -## 관계 - -- **leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 튜닝 값이 두 계층에 있으면 어느 쪽이 이기는지 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 사실 - -ClaimCheckStore.delete 가 선언돼 있고 이 leaf 에서 호출되지 않는다. git grep -n 'delete(' -- src/messaging/messaging-claim-check 가 인터페이스 선언만 돌려준다. - -ClaimCheckPublisher javadoc 이 "the retention sweep reclaims it" 이라고 그 sweep 의 존재를 전제한다. - -저장소 lifecycle(예: S3 object expiration)이 대신할 수 있으나 ClaimCheckPolicy.retention 이 그것과 연결되지 않는다. - -## 미지수 - -회수 책임을 애플리케이션이 질 것인가 저장소 lifecycle 에 맡길 것인가. 이 판정이 ClaimCheckStore 구현 계획에 걸려 있다. - -## 선택지 - -sweep 작업을 만든다 - retention 값이 실제 삭제 시점을 정하고, 정책이 하나의 주인을 갖는다. - -저장소 lifecycle 에 위임한다 - 위임한다는 사실을 javadoc 에 명시해야 retention 값이 무엇을 뜻하는지 읽힌다. - -## 다음 검증 - -delete 호출자 검색으로 현재 상태는 확정된다. 남은 것은 구현 계획의 결정이고, 그것은 저장소 안의 사실로 닫히지 않는다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/reference/reference-messaging-inbox-jdbc-postgresql-f05.md b/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/reference/reference-messaging-inbox-jdbc-postgresql-f05.md deleted file mode 100644 index 5642a75..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/reference/reference-messaging-inbox-jdbc-postgresql-f05.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-inbox-jdbc-postgresql-f05 -title: 컬럼 폭은 애플리케이션 검증과 짝을 이룬다 -topic: contract-domain-and-bounds -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-inbox-jdbc-postgresql-f05 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-inbox-jdbc-postgresql#L684 ---- - -# 컬럼 폭은 애플리케이션 검증과 짝을 이룬다 - -## 관계 - -- **bounded purge가 구현돼 있고 호출되지 않아, cleanup이 스스로 막겠다고 한 장애를 일으킨다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **속성을 이름으로 주장하는 테스트가 그 속성을 보일 수 없는 fake 위에서 통과한다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **SQL 실패가 재시도 불가로 분류된다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. - -## 목적 - -DB 가 먼저 거절하면 그 실패는 설정 오류가 아니라 인프라 오류의 모양으로 도착한다. 여기서는 긴 consumerId 가 SQLException 이 되고, 그것이 INBOX_RESERVE_FAILED · CONFIGURATION · retryable=false 로 분류돼 결국 설정 실수가 메시지 파킹으로 나타난다. - -## 규칙 - -1. 컬럼에 폭이 선언돼 있으면 애플리케이션 층에도 같은 상한을 둔다 - migration 이 consumer_id VARCHAR(160) 을 선언한다. - -2. 지금 무엇을 검증하는지 확인한다 - IdempotentConsumer · TransactionalInboxHandler · JdbcInboxRepository 셋 다 공백만 거절하고 길이를 보지 않는다. - -3. 값 객체가 있으면 그 생성자가 상한을 갖는다 - messaging-core-api 의 값 객체들이 바이트 상한을 생성자에서 강제하는 형태가 그쪽 §4.5 에 있다. consumerId 는 아직 값 객체가 아니다. - -## 적용 조건 - -스키마가 폭을 정하고 그 값이 애플리케이션에서 문자열로 다뤄지는 모든 컬럼. - -## 예외 - -폭이 실질적으로 도달 불가능할 만큼 넉넉한 경우는 예외로 둘 수 있다. 다만 그 판단이 어디에도 적혀 있지 않으면 예외가 아니라 미검증이다. - -## 예시 - -V2__messaging_inbox.sql:10 의 컬럼 선언과 세 클래스의 검증 코드. 확인 방법은 161자 consumerId 로 reserve 를 부르는 것이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/reference/reference-messaging-inbox-jdbc-postgresql-f06.md b/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/reference/reference-messaging-inbox-jdbc-postgresql-f06.md deleted file mode 100644 index 8650ed2..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/contract-domain-and-bounds/reference/reference-messaging-inbox-jdbc-postgresql-f06.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-inbox-jdbc-postgresql-f06 -title: 같은 안전 규칙은 한 공식과 한 강제 시점을 갖는다 -topic: contract-domain-and-bounds -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-inbox-jdbc-postgresql-f06 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-inbox-jdbc-postgresql#L693 ---- - -# 같은 안전 규칙은 한 공식과 한 강제 시점을 갖는다 - -## 관계 - -- **bounded purge가 구현돼 있고 호출되지 않아, cleanup이 스스로 막겠다고 한 장애를 일으킨다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **속성을 이름으로 주장하는 테스트가 그 속성을 보일 수 없는 fake 위에서 통과한다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **SQL 실패가 재시도 불가로 분류된다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. - -## 목적 - -같은 종류의 시간 관계가 곱셈과 덧셈으로 다르게 표현되고 강제 시점까지 다르면, 어느 배포가 그 규칙의 보호를 받는지 코드에서 읽을 수 없게 된다. - -## 규칙 - -1. 같은 규칙이 몇 곳에 있는지 센다 - 보존 규칙은 세 곳에 있다. InboxRepository javadoc 은 강제하지 않고, 이 leaf 의 InboxRetentionPolicy 는 × 2.0 이며, messaging-claim-check 의 ClaimCheckPolicy 는 brokerRetention + maxRedeliveryWindow 다. - -2. 공식이 같은지 본다 - 곱셈과 덧셈은 같은 안전 여유를 표현하지 않는다. - -3. 언제 강제되는지 본다 - InboxRetentionPolicy.validate() 는 InboxCleanupJob 을 만들 때만 불린다. cleanup 을 배선하지 않은 배포는 보존 검사를 아예 받지 않는다. ClaimCheckPolicy 는 항상 강제한다. - -## 적용 조건 - -보존 기간·재전달 창·타임아웃처럼 시간 관계를 안전 여유로 표현하는 정책이 여러 leaf 에 흩어져 있는 자리. - -## 예외 - -leaf 마다 다른 여유가 필요한 경우는 공식이 아니라 계수가 달라야 한다. 지금은 계수가 아니라 연산 자체가 다르고, 그 차이의 이유가 어느 쪽에도 적혀 있지 않다. - -## 예시 - -세 위치의 공식과 강제 시점. 확인 방법은 그 셋을 나란히 놓고 대조하는 것이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/cross-leaf-integration-facts/case/case-claim-check-has-no-wiring-line-in-the-starter.md b/docs/clean-architecture-backend-template/tech-log-studio/cross-leaf-integration-facts/case/case-claim-check-has-no-wiring-line-in-the-starter.md deleted file mode 100644 index c9ddb34..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/cross-leaf-integration-facts/case/case-claim-check-has-no-wiring-line-in-the-starter.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -kind: CASE -slug: claim-check-has-no-wiring-line-in-the-starter -title: claim-check는 starter에 배선 코드가 한 줄도 없다 -topic: cross-leaf-integration-facts -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:claim-check-has-no-wiring-line-in-the-starter -evidenceCapturedOn: 2026-09-01 -assets: - - key: claim-check-has-no-wiring-line-in-the-starter - file: ../../../final/evidence/rendered/claim-check-has-no-wiring-line-in-the-starter.svg -evidence: - - ../../../final/evidence/raw/claim-check-has-no-wiring-line-in-the-starter.txt -source: - - 원본 분석 절은 final/document.md#a19#L926 이다. -module: integration/19-messaging-platform -priority: P3 ---- - -# claim-check는 starter에 배선 코드가 한 줄도 없다 - -claim-check 리프는 runtime_memberships 가 출하 컴포지션이고 starter 의 허용 의존에도 들어 있다. 그런데 starter 에 저장소 구현도, 빈도, 오프로드를 부르는 발행 경로도 없다. - -## 문제 - -claim-check 리프는 runtime_memberships 가 출하 컴포지션이고 starter 의 허용 의존에도 들어 있다. - -그런데 starter 에 저장소 구현도, 빈도, 오프로드를 부르는 발행 경로도 없다. - -## 결론 - -리프 SSOT 는 자기 안에서 '소비자 0' 까지만 말할 수 있고, '배포에는 실려 있다' 는 레지스트리와 starter 를 함께 읽어야 나온다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : starter 자동설정 전문에서 ClaimCheck 문자열 검색과 두 클래스의 main 참조 수 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19#L926 에 있다. - -## 본문 - - - -claim-check 리프는 `runtime_memberships` 가 출하 컴포지션이고 starter 의 허용 의존에도 들어 있다. 그런데 starter 에 저장소 구현도, 빈도, 오프로드를 부르는 발행 경로도 없다. - -## 리프가 출하 컴포지션에 든 상태 - -:::evidence key="claim-check-has-no-wiring-line-in-the-starter" alt="분석 문서 final/document.md#a19 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a19 발췌 — 15줄" zoom="true" -::: - -## 리프 SSOT 혼자서는 이 사실을 말할 수 없다 - -리프 SSOT 는 자기 안에서 "소비자 0" 까지만 말할 수 있고, "배포에는 실려 있다" 는 레지스트리와 starter 를 함께 읽어야 나온다. - -## 확인하지 못한 것 - -부팅된 컨텍스트에서 임계값을 넘는 payload 를 발행해 claim check 가 일어나지 않는 것을 관측하지 않았다. 자동설정에 이 리프의 이름이 등장하지 않는다는 것으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/cross-leaf-integration-facts/case/case-messaging-migration-stream-has-no-applier-and-collides-on-adoption.md b/docs/clean-architecture-backend-template/tech-log-studio/cross-leaf-integration-facts/case/case-messaging-migration-stream-has-no-applier-and-collides-on-adoption.md deleted file mode 100644 index 7a7b956..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/cross-leaf-integration-facts/case/case-messaging-migration-stream-has-no-applier-and-collides-on-adoption.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -kind: CASE -slug: messaging-migration-stream-has-no-applier-and-collides-on-adoption -title: messaging 마이그레이션 스트림을 적용하는 곳이 없고, 적용하려는 순간 버전이 충돌한다 -topic: cross-leaf-integration-facts -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-migration-stream-has-no-applier-and-collides-on-adoption -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-migration-stream-has-no-applier-and-collides-on-adoption - file: ../../../final/evidence/rendered/messaging-migration-stream-has-no-applier-and-collides-on-adoption.svg - - key: messaging-migration-stream-has-no-applier-and-collides-on-adoption-diagram - file: ../../../final/assets/diagrams/messaging-migration-stream-has-no-applier-and-collides-on-adoption.svg -evidence: - - ../../../final/evidence/raw/messaging-migration-stream-has-no-applier-and-collides-on-adoption.txt -source: - - 원본 분석 절은 final/document.md#a19#L860 이다. -module: integration/19-messaging-platform -priority: P2 ---- - -# messaging 마이그레이션 스트림을 적용하는 곳이 없고, 적용하려는 순간 버전이 충돌한다 - -리프 하나만 읽어서는 보이지 않는 사실이다. 두 messaging 리프가 각자 마이그레이션을 갖는데 그 스트림을 적용하는 Flyway location 이 어떤 컴포지션에도 없다. - -## 관계 - -- **messaging 마이그레이션 두 leaf가 같은 디렉터리에서 V2를 둘 만들었다** - 같은 구조에서 실제로 확인된 사건이다. - -## 문제 - -리프 하나만 읽어서는 보이지 않는 사실이다. - -두 messaging 리프가 각자 마이그레이션을 갖는데 그 스트림을 적용하는 Flyway location 이 어떤 컴포지션에도 없다. - -## 결론 - -그리고 적용하려는 순간 두 리프가 같은 디렉터리에서 같은 버전 번호를 만들어 둔 것이 드러난다 — 즉 배선 부재가 번호 충돌을 가려 왔다. - -두 사실이 한 사건인 이유는 순서다. - -적용이 시작되는 날 첫 실패가 충돌이고, 그때까지는 어느 쪽도 관측되지 않는다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : 두 리프의 마이그레이션 파일 목록과 컴포지션들의 Flyway location 설정 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19#L860 에 있다. - -## 본문 - - - -리프 하나만 읽어서는 보이지 않는 사실이다. 두 messaging 리프가 각자 마이그레이션을 갖는데 그 스트림을 적용하는 Flyway location 이 어떤 컴포지션에도 없다. - -## 적용이 없어 가려진 충돌 - -:::evidence key="messaging-migration-stream-has-no-applier-and-collides-on-adoption-diagram" alt="리프 둘의 마이그레이션만 존재하는 것 안에 놓이고 Flyway 적용 위치가 바깥에 빗금으로 놓인다" caption="적용이 없어 가려진 충돌" zoom="false" -::: - -## 스트림을 적용하는 컴포지션이 없다 - -:::evidence key="messaging-migration-stream-has-no-applier-and-collides-on-adoption" alt="분석 문서 final/document.md#a19 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a19 발췌 — 15줄" zoom="true" -::: - -## 두 사실이 한 사건인 이유는 순서다 - -적용하려는 순간 두 리프가 같은 디렉터리에서 같은 버전 번호를 만들어 둔 것이 드러난다 — 배선 부재가 번호 충돌을 가려 왔다. 적용이 시작되는 날 첫 실패가 충돌이고, 그때까지는 어느 쪽도 관측되지 않는다. - -## 확인하지 못한 것 - -그 디렉터리를 Flyway location 에 넣어 부팅 실패를 재현하지 않았다. 적용 위치가 없어 충돌이 아직 발현하지 않는다는 것이 이 기록의 요지다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/cross-leaf-integration-facts/case/case-the-admin-switch-turns-on-the-guard-and-not-the-service.md b/docs/clean-architecture-backend-template/tech-log-studio/cross-leaf-integration-facts/case/case-the-admin-switch-turns-on-the-guard-and-not-the-service.md deleted file mode 100644 index 9fc16ed..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/cross-leaf-integration-facts/case/case-the-admin-switch-turns-on-the-guard-and-not-the-service.md +++ /dev/null @@ -1,75 +0,0 @@ ---- -kind: CASE -slug: the-admin-switch-turns-on-the-guard-and-not-the-service -title: admin 스위치가 가드를 켜고 서비스는 켜지 않는다 -topic: cross-leaf-integration-facts -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:the-admin-switch-turns-on-the-guard-and-not-the-service -evidenceCapturedOn: 2026-09-01 -assets: - - key: the-admin-switch-turns-on-the-guard-and-not-the-service - file: ../../../final/evidence/rendered/the-admin-switch-turns-on-the-guard-and-not-the-service.svg - - key: the-admin-switch-turns-on-the-guard-and-not-the-service-diagram - file: ../../../final/assets/diagrams/the-admin-switch-turns-on-the-guard-and-not-the-service.svg -evidence: - - ../../../final/evidence/raw/the-admin-switch-turns-on-the-guard-and-not-the-service.txt -source: - - 원본 분석 절은 final/document.md#a19#L975 이다. -module: integration/19-messaging-platform -priority: P2 ---- - -# admin 스위치가 가드를 켜고 서비스는 켜지 않는다 - -운영자가 admin 기능을 켜는 프로퍼티 하나가 파괴적 작업 가드를 활성화한다. 같은 스위치가 그 가드를 실제로 부르는 admin 서비스 빈은 만들지 않는다. - -## 문제 - -운영자가 admin 기능을 켜는 프로퍼티 하나가 파괴적 작업 가드를 활성화한다. - -같은 스위치가 그 가드를 실제로 부르는 admin 서비스 빈은 만들지 않는다. - -## 결론 - -결과적으로 스위치는 '켜졌다' 는 상태를 만들고 그 상태를 소비하는 경로가 없다. - -두 리프의 SSOT 를 겹쳐야만 보이는 형태다 — 한쪽은 가드의 조건을, 다른 쪽은 서비스 빈의 부재를 소유한다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : admin 스위치가 만드는 빈 4종 전수 확인과 그 가드를 부르는 서비스 빈의 생성 지점 검색 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19#L975 에 있다. - -## 본문 - - - -운영자가 admin 기능을 켜는 프로퍼티 하나가 파괴적 작업 가드를 활성화한다. 같은 스위치가 그 가드를 실제로 부르는 admin 서비스 빈은 만들지 않는다. - -## 스위치가 닿는 범위 - -:::evidence key="the-admin-switch-turns-on-the-guard-and-not-the-service-diagram" alt="admin 스위치에서 가드와 저널과 검증기는 만들어지고 admin 서비스 빈은 빗금인 두 갈래가 나온다" caption="스위치가 닿는 범위" zoom="false" -::: - -## 스위치가 만드는 빈 넷 - -:::evidence key="the-admin-switch-turns-on-the-guard-and-not-the-service" alt="분석 문서 final/document.md#a19 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a19 발췌 — 15줄" zoom="true" -::: - -## 켜졌다는 상태를 소비하는 경로가 없다 - -두 리프의 SSOT 를 겹쳐야만 보이는 형태다 — 한쪽은 가드의 조건을, 다른 쪽은 서비스 빈의 부재를 소유한다. - -## 확인하지 못한 것 - -부팅된 컨텍스트에서 admin 평면을 켜고 빈 그래프를 관측하지 않았다. 런타임 관측을 수행하지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/cross-leaf-integration-facts/case/case-the-public-surface-contract-test-lives-outside-the-family.md b/docs/clean-architecture-backend-template/tech-log-studio/cross-leaf-integration-facts/case/case-the-public-surface-contract-test-lives-outside-the-family.md deleted file mode 100644 index 9b255ca..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/cross-leaf-integration-facts/case/case-the-public-surface-contract-test-lives-outside-the-family.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -kind: CASE -slug: the-public-surface-contract-test-lives-outside-the-family -title: MessagingPublicSurfaceContractTest가 가족 밖(app-bootstrap)에 있다 -topic: cross-leaf-integration-facts -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:the-public-surface-contract-test-lives-outside-the-family -evidenceCapturedOn: 2026-09-01 -assets: - - key: the-public-surface-contract-test-lives-outside-the-family - file: ../../../final/evidence/rendered/the-public-surface-contract-test-lives-outside-the-family.svg -evidence: - - ../../../final/evidence/raw/the-public-surface-contract-test-lives-outside-the-family.txt -source: - - 원본 분석 절은 final/document.md#a19#L1114 이다. -module: integration/19-messaging-platform -priority: P3 ---- - -# MessagingPublicSurfaceContractTest가 가족 밖(app-bootstrap)에 있다 - -messaging 가족의 공개 표면을 붙드는 계약 테스트가 그 가족이 아니라 컴포지션 리프에 있다. 그래서 messaging 리프만 빌드하는 경로에서는 그 계약이 돌지 않고, 가족 안의 어느 SSOT 도 자기 표면이 어디서 검증되는지 알 수 없다. - -## 문제 - -messaging 가족의 공개 표면을 붙드는 계약 테스트가 그 가족이 아니라 컴포지션 리프에 있다. - -그래서 messaging 리프만 빌드하는 경로에서는 그 계약이 돌지 않고, 가족 안의 어느 SSOT 도 자기 표면이 어디서 검증되는지 알 수 없다. - -## 결론 - -테스트의 소재가 곧 그 테스트가 도는 조건이라는 점이 이 관측의 요지다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : MessagingPublicSurfaceContractTest 참조 1건 검색으로 파일의 소속 소스셋 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19#L1114 에 있다. - -## 본문 - - - -messaging 가족의 공개 표면을 붙드는 계약 테스트가 그 가족이 아니라 컴포지션 리프에 있다. - -## MessagingPublicSurfaceContractTest 참조 위치 - -:::evidence key="the-public-surface-contract-test-lives-outside-the-family" alt="코드베이스에서 MessagingPublicSurfaceContractTest 를 검색한 출력 1줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingPublicSurfaceContractTest 코드베이스 검색 — 1줄 · exit 0" zoom="true" -::: - -## 두 결과 - -messaging 리프만 빌드하는 경로에서는 그 계약이 돌지 않고, 가족 안의 어느 SSOT 도 자기 표면이 어디서 검증되는지 알 수 없다. - -## 이 관측의 요지 - -테스트의 소재가 곧 그 테스트가 도는 조건이다. - -## 확인하지 못한 것 - -messaging 리프만 빌드하는 경로를 실제로 돌려 그 계약이 빠지는 것을 관측하지 않았다. 파일 위치로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/cross-leaf-integration-facts/case/case-twenty-five-main-files-and-one-test-file.md b/docs/clean-architecture-backend-template/tech-log-studio/cross-leaf-integration-facts/case/case-twenty-five-main-files-and-one-test-file.md deleted file mode 100644 index 877f07e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/cross-leaf-integration-facts/case/case-twenty-five-main-files-and-one-test-file.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -kind: CASE -slug: twenty-five-main-files-and-one-test-file -title: messaging-admin-api는 main 25파일 · 1,613 LOC에 테스트 파일이 1개다 -topic: cross-leaf-integration-facts -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:twenty-five-main-files-and-one-test-file -evidenceCapturedOn: 2026-09-01 -assets: - - key: twenty-five-main-files-and-one-test-file - file: ../../../final/evidence/rendered/twenty-five-main-files-and-one-test-file.svg -evidence: - - ../../../final/evidence/raw/twenty-five-main-files-and-one-test-file.txt -source: - - 원본 분석 절은 final/document.md#a19#L997 이다. -module: integration/19-messaging-platform -priority: P3 ---- - -# messaging-admin-api는 main 25파일 · 1,613 LOC에 테스트 파일이 1개다 - -가족 관점에서만 나오는 비율 관측이다. 이 리프의 운영자 표면 전체가 테스트 한 파일에 기대고 있고, 그 파일이 겨냥하지 않는 타입들이 §17 에서 각각 미검증으로 잡힌다. - -## 문제 - -가족 관점에서만 나오는 비율 관측이다. - -이 리프의 운영자 표면 전체가 테스트 한 파일에 기대고 있고, 그 파일이 겨냥하지 않는 타입들이 §17 에서 각각 미검증으로 잡힌다. - -## 결론 - -리프 SSOT 는 개별 타입의 미검증을 말하고, 이 관측은 그것들이 한 원인에서 나온다는 것을 말한다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : 두 리프의 main·test 파일 수와 LOC 집계 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19#L997 에 있다. - -## 본문 - - - -가족 관점에서만 나오는 비율 관측이다. 이 리프의 운영자 표면 전체가 테스트 한 파일에 기대고 있다. - -## main 과 test 파일 수 - -:::evidence key="twenty-five-main-files-and-one-test-file" alt="분석 문서 final/document.md#a19 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a19 발췌 — 15줄" zoom="true" -::: - -## 그 파일이 겨냥하지 않는 타입들 - -§17 에서 각각 미검증으로 잡힌다. 리프 SSOT 는 개별 타입의 미검증을 말하고, 이 관측은 그것들이 한 원인에서 나온다는 것을 말한다. - -## 확인하지 못한 것 - -테스트 한 파일이 겨냥하지 않는 타입들이 실제로 어떻게 깨지는지 확인하지 않았다. 리프 SSOT 의 §17 이 개별 미검증을 소유한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-a05-f019-ssot.md b/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-a05-f019-ssot.md deleted file mode 100644 index d5d736b..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-a05-f019-ssot.md +++ /dev/null @@ -1,141 +0,0 @@ ---- -kind: CASE -slug: a05-f019-ssot -title: 정본이 어디인지 주석에 적어 두고 정의는 다시 타이핑한다 -topic: declaration-and-document-drift -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a05-f019-ssot -evidenceCapturedOn: 2026-09-02 -assets: - - key: a05-f019-ssot - file: ../../../final/evidence/rendered/a05-f019-ssot.svg -evidence: - - ../../../final/evidence/raw/a05-f019-ssot.txt -source: - - 원본 분석 절은 `final/document.md#a05` §51 이다. 두 목록의 이름과 차이가 `postgresql` 과 `h2` 라는 것이 §51.1 과 §51.2 에, 리프 목록이 바깥 소비자를 스캔하지 않는다는 것이 §51.3 에 있다. 같은 문서 §55 의 backlog 가 이것을 P2/P3 아키텍처 거버넌스 강화로 분류한다. - - 사본의 javadoc 이 정본이 어디인지 적어 두고도 그 값을 코드로 읽지 않는다는 것은 여기서 확인했다. ---- - -# 정본이 어디인지 주석에 적어 두고 정의는 다시 타이핑한다 - -리프가 내보내는 패키지 목록을 선언하고, 컴포지션 루트의 소비자 규칙이 같은 목록을 자기 안에 다시 적는다. 사본의 javadoc 은 정본이 리프 쪽이라고 이름으로 적지만, 그 이름을 코드로 읽는 곳은 없다. 두 목록은 이미 두 항목 다르다. - -## 관계 - -- **문서의 수치는 세지 말고 파생하거나 게이트로 붙든다** - 문서의 수치를 세는 대신 파생하거나 게이트로 붙들라는 규칙이다. -- **빠뜨림이 통과가 되는 게이트는 게이트가 아니다** - 한쪽에 항목을 더해도 다른 쪽이 조용한 형태가 같다. -- **같은 개념의 두 어휘가 공존하면 하나를 죽은 것으로 표시한다** - 같은 값을 두 곳이 각자 적고 있어 어느 쪽이 정본인지 정해야 한다. - -## 문제 - -이 리프에는 jar 하나에 공개 구현 타입이 많이 들어 있다. 그래서 자바 접근 제어와 아키텍처 노출 목록을 각각 따로 둔다. - -내보낼 패키지 목록을 두 곳이 각각 들고 있다. - -## 결론 - -리프의 경계 시험 쪽에는 열 개가 선언돼 있다. springdata 와 querydsl 은 거기 없다. - -컴포지션 루트의 아키텍처 시험이 같은 열 개를 자기 안에 다시 적고, 벤더 진입점 둘을 더 넣는다. postgresql 과 h2 다. 이유는 그 자리 주석에 적혀 있다. 벤더 설정이 코어 JPA 설정을 임포트하는 방향이라 컴포지션 루트가 대신 막을 단일 내부 진입점이 없고, 방향을 뒤집으면 패키지 순환이 생겼다는 것이다. - -사본의 javadoc 에는 이 목록이 리프의 export 허용 목록을 옮겨 적은 것이고 정의는 리프의 경계 시험에 있다고 적혀 있다. - -그 문장이 컴포지션 루트에서 그 클래스를 언급하는 유일한 줄이다. 선언 파일 밖에서 그 목록 상수를 참조하는 자바 코드는 저장소 전체에 0 이다. 어느 쪽이 정본인지는 산문이 말하고, 값은 사람이 옮겨 적는다. - -리프 목록은 바깥 소비자를 검사하지도 않는다. 그 목록을 쓰는 시험 둘은 목록에 적힌 패키지가 실제로 있는지, 그 패키지가 카탈로그의 거버넌스 대상인지를 본다. 임포트 관계는 컴포지션 루트 쪽 규칙이 따로 본다. - -그래서 두 시험 모두 통과한다. 통과는 각자의 규칙을 만족한다는 뜻이고, 두 목록이 같다는 뜻은 아니다. 지금 이미 두 항목 다르다. - -권고는 등록부를 한 곳에 두고 두 검사가 같은 데이터를 보게 하라는 것이다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 두 목록의 항목 추출과 집합 대조, 정본 참조 계수, 리프 시험의 검사 범위 확인 -소스 수정 : x -실행 : 없음. 정적 검색과 집합 연산이다. - -## 재현 조건 - -1. 리프 경계 시험의 export 목록을 읽는다. -2. 컴포지션 루트의 소비자 규칙 안에 있는 같은 이름의 목록을 읽는다. -3. 두 집합을 뽑아 차집합을 구한다. -4. 사본의 javadoc 이 정본을 어떻게 지목하는지 읽는다. -5. 컴포지션 루트에서 그 정본 클래스를 언급하는 줄과, 선언 파일 밖에서 그 상수를 참조하는 코드를 각각 센다. -6. 리프 목록을 쓰는 시험 둘이 무엇을 확인하는지 읽는다. - -## 본문 - - - -이 리프는 하나의 jar 안에 공개 구현 타입이 많다. 그래서 자바의 `public` 과 아키텍처가 내보내는 패키지를 따로 관리한다. - -내보내는 패키지 목록이 두 곳에 있다. - -## 두 목록과 그 차이 - -:::evidence key="a05-f019-ssot" alt="리프 경계 시험이 선언하는 export 패키지 목록, 컴포지션 루트의 소비자 규칙 안에 다시 적힌 같은 목록과 거기 더해진 벤더 진입점 둘과 그 이유 주석, 두 집합의 크기와 차집합, 사본의 javadoc 이 정본을 지목하는 줄과 그 정본 클래스를 언급하는 줄 수와 선언 파일 밖의 상수 참조 수, 그리고 리프 목록을 쓰는 두 시험이 무엇을 보는지를 출력한 터미널 기록." caption="리프 10개와 루트 12개, 차이는 postgresql 과 h2 · 사본 javadoc 이 정본을 어디라고 적는지 · 그 클래스 언급 1줄은 그 주석뿐 · 상수 참조 0 · 리프 시험 둘은 목록의 자기 정합만 확인 — 65줄 · exit 0" zoom="true" -::: - -리프의 경계 시험이 열 개를 선언한다. - -```java -private static final Set EXPORTED_PACKAGES = - Set.of( - "api", "notification.configuration", "transaction", "security", - "observation", "migration", "hibernate", "fileserver", "failure", "config"); -``` - -컴포지션 루트의 아키텍처 시험은 같은 열 개에 둘을 더 적는다. 그 자리의 주석이 이유를 적는다. - -```text -The two vendor entry points. A vendor configuration imports the core JPA config rather -than the reverse, so there is no single internal entry the composition root could gate -instead — inverting the import to make one produced a package cycle. -``` - -집합으로 빼면 차이가 정확히 둘이다. - -```text -리프 10개 / 루트 12개 -루트에만 있는 것: ['h2', 'postgresql'] -리프에만 있는 것: [] -``` - -## 사본이 정본을 지목하는 방식 - -```text -The list is the leaf's export allowlist, mirrored here because this is the consumer side -of the same boundary. JpaModuleBoundaryTest owns the definition. -``` - -그 문장이 컴포지션 루트에서 리프 경계 시험을 언급하는 유일한 줄이다. 선언 파일 밖에서 `EXPORTED_PACKAGES` 를 참조하는 자바 코드는 저장소 전체에 0 이다. - -정본을 지목하는 것은 산문이고, 값은 손으로 옮겨져 있다. - -## 리프 목록은 바깥 소비자를 보지 않는다 - -그 목록을 쓰는 시험은 둘이다. 목록에 적힌 패키지가 소스 트리에 실제로 있는지, 그리고 그 패키지가 카탈로그의 거버넌스 대상인지를 본다. - -누가 무엇을 임포트하는지는 컴포지션 루트의 규칙이 따로 본다. split 은 실수가 아니라 역할 분리의 결과이고, 그래서 어느 쪽도 상대를 검사할 이유가 없다. - -## 두 시험이 각각 무엇을 보는가 - -두 시험의 입력이 겹치지 않는다. 리프 시험은 리프의 소스 트리와 자기 카탈로그만, 루트 시험은 루트의 임포트 그래프와 자기 목록만 읽는다. - -한쪽 목록이 늘어도 다른 쪽 단언의 입력은 그대로다. 실패할 근거가 없다. - -## 고칠 방향 - -내보내는 패키지 등록부를 한 곳으로 옮기고, 리프의 패키지 그래프 검사와 소비자 규칙이 같은 데이터를 읽게 한다. 분석 문서의 권고가 그것이다. - -## 확인하지 못한 것 - -한쪽 목록에 항목을 더해 다른 쪽이 조용한지 실행으로 확인하지 않았다. 두 선언을 대조하고 참조를 센 것까지가 확인 범위다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-a06-f016-flamingock.md b/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-a06-f016-flamingock.md deleted file mode 100644 index 8cbb444..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-a06-f016-flamingock.md +++ /dev/null @@ -1,186 +0,0 @@ ---- -kind: CASE -slug: a06-f016-flamingock -title: javadoc은 재개 가능한 것만 거부한다고 적는데 검사는 마이그레이션을 보지 않는다 -topic: declaration-and-document-drift -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a06-f016-flamingock -evidenceCapturedOn: 2026-09-02 -assets: - - key: a06-f016-flamingock - file: ../../../final/evidence/rendered/a06-f016-flamingock.svg - - key: a06-f016-flamingock-probe - file: ../../../final/evidence/rendered/a06-f016-flamingock-probe.svg -evidence: - - ../../../final/evidence/raw/a06-f016-flamingock.txt - - ../../../final/evidence/raw/a06-f016-flamingock-probe.txt -source: - - 원본 분석 절은 final/document.md#a06#L942 이다. 등급은 P3 이다. 그 절이 담은 것은 넷이다. 펜스 반환값과 그 근거, javadoc 마지막 문장의 범위, 82행 검사가 적용 스트림보다 앞이라는 위치, 그리고 시험이 이 조합을 다루지 않는다는 것. - - 빈 목록도 같은 문장으로 거절된다는 것, 같은 마이그레이션이 펜스가 있는 리스에서는 완료된다는 것, 154행이 재개 가능한 마이그레이션에만 걸리는 펜스 조건이라는 것, 그리고 검사를 옮겨도 157행에서 다시 막힌다는 것은 이 기록에서 확인했다. ---- - -# javadoc은 재개 가능한 것만 거부한다고 적는데 검사는 마이그레이션을 보지 않는다 - -잠금 어댑터는 펜스로 미지정 값을 돌려주고 그 근거를 javadoc 에 적는다. 이어지는 한 문장이 실행자의 거부 범위를 재개 가능한 마이그레이션으로 한정한다. 실제 조건은 리스의 펜스 값 하나여서 마이그레이션이 하나도 없어도 거부한다. - -## 관계 - -- **fenced lease — 만료 시각만으로는 부족한 이유** - 펜싱이 무엇을 보장하는지 다룬 개념이다. -- **recordApplied가 펜스를 비교하지 않아 밀려난 실행자가 원장을 차지한다** - 같은 원장의 펜스 처리 사례다. -- **문서가 UUIDv7이라 말하고 생성되는 것은 v4다** - 두 사례 모두 javadoc 이 적은 것과 코드가 만드는 값이 다르다. - -## 문제 - -펜스 메서드가 미지정 값을 돌려주는 것과 그 근거는 옳다. 로컬 카운터로 펜싱을 흉내내면 펜싱처럼 보이면서 아무것도 보호하지 않고, 프로세스마다 따로 세는 수는 다른 프로세스가 무엇을 획득했는지에 대해 아무 말도 하지 않는다. - -어긋난 것은 같은 javadoc 의 마지막 한 문장이다. 실행자가 바로 이 이유로 재개 가능한 마이그레이션을 거부한다고 적는다. - -## 결론 - -그 문장이 가리키는 장치는 실재한다. 재개 가능한 마이그레이션만 체크포인트를 남기고, 그 저장에 펜스가 넘어가며, 원장은 미지정 펜스로 들어온 체크포인트를 거절한다. 그 자리를 지키는 조건은 재개 가능성 하나만 본다. - -닿지 못할 뿐이다. 진입점의 세 관문 중 세 번째가 리스의 펜스 값만 보고 던지고, 그 자리는 적용 스트림보다 앞이다. 조건에 마이그레이션이 들어가지 않으므로 목록이 비어 있어도 같은 문장이 나온다. - -탐침으로 확인했다. 빈 목록을 이 리스로 적용해도 거절된다. 목록에 아무것도 없어 재개 가능한지 여부가 존재하지 않는데도 같다. 체크포인트를 만들지 않는 마이그레이션 한 건도 마찬가지이고, 그 한 건은 펜스가 있는 리스에서는 완료로 끝난다. - -거절 문장 자체는 정확하다. 펜스 없는 리스는 멈춘 실행자를 배제할 수 없다고 말하고 재개 가능성을 언급하지 않는다. 그리고 거절 자체가 안전한 선택이다. 문제는 javadoc 을 읽고 이 어댑터를 고른 쪽이 재개 불가능한 마이그레이션은 돈다고 읽는다는 것이다. - -수정은 사실상 하나다. javadoc 을 실제 범위로 고치는 것이다. 검사를 스트림 안으로 옮기는 쪽은 둘에 막힌다. 재개 가능성은 실행이 체크포인트를 돌려준 뒤에야 드러나므로 실행 전에 판별할 수 없고, 옮기더라도 적용이 끝난 뒤 같은 미지정 값이 원장 기록으로 넘어가 거기서 거절된다. 거절 시점이 실행 전에서 실행 후로 밀릴 뿐이다. - -지금 배포를 멈추는 결함은 아니다. 실행자와 원장과 잠금을 프로덕션에서 참조하는 곳이 없고, 이 어댑터를 만드는 곳은 시험 두 곳이며 그 시험들은 적용을 부르지 않는다. 이 리프를 가져다 조립하는 쪽이 처음 실행할 때 드러난다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 밀폐된 탐침으로 실행자 진입점 호출 -소스 수정 : x - -## 재현 조건 - -1. 잠금 어댑터의 펜스 메서드가 돌려주는 값과 그 javadoc 을 읽는다. -2. 진입점이 지나는 세 관문과 적용 스트림의 줄 번호를 확인한다. -3. 그 파일에서 리스의 펜스를 읽는 곳을 전수로 세고, 각각이 그 값을 어디로 넘기는지 확인한다. -4. 원장이 미지정 펜스를 어떻게 다루는지 읽는다. -5. 마이그레이션 인터페이스에 재개 가능성을 선언하는 멤버가 있는지 확인한다. -6. 어댑터로 빈 목록을 적용한다. -7. 체크포인트를 만들지 않는 마이그레이션 한 건을 같은 리스로, 그리고 펜스가 있는 리스로 각각 적용한다. -8. 어댑터가 나오는 곳과 프로덕션 참조를 전수로 센다. - -## 본문 - - - -잠금 어댑터의 펜스 메서드는 미지정 값을 돌려주고, javadoc 이 그 선택의 근거를 적는다. - -## 근거 문단과 그 뒤의 한 문장 - -:::evidence key="a06-f016-flamingock" alt="잠금 어댑터의 펜스 메서드와 그 javadoc, 실행자 진입점이 지나는 세 관문, 그 파일에서 리스의 펜스를 읽는 곳 전수와 각각의 줄 번호, 원장이 미지정 펜스를 거절하는 자리, 마이그레이션 인터페이스의 멤버 목록, 이 어댑터가 나오는 곳 전수, 실행자와 원장과 잠금의 프로덕션 참조, 그리고 마이그레이션을 적용하는 진입점 전수를 출력한 터미널 기록." caption="펜스는 UNFENCED 를 반환하고 javadoc 은 재개 가능한 것을 거부한다고 적음 · 관문은 보유 검사 74행, 목록 검증 80행, 펜스 검사 82행이고 적용 스트림은 93행 · 펜스를 읽는 곳은 82·154·157행이고 뒤 둘은 원장으로 넘어가 미지정이면 거절 · 인터페이스에 재개 가능성 멤버 없음 · 프로덕션 참조 0 — 73줄 · exit 0" zoom="true" -::: - -```java -/** - * Unfenced: the engine's lock exposes no monotonic token. - * - *

Reported honestly rather than synthesised from a local counter, which would look like - * fencing and protect nothing — a per-process counter says nothing about what another process - * acquired. The runner refuses resumable migrations under an unfenced lease for exactly this - * reason. - */ -``` - -앞 두 문장은 왜 흉내내지 않았는지를 적는다. 문제는 마지막 문장이 거절 범위를 재개 가능한 마이그레이션으로 한정한 것이다. - -## 그 문장이 가리키는 장치는 실재한다 - -실행자가 리스의 펜스를 읽는 곳은 셋이다. - -```text - 82: if (lock.fence() == MongoMigrationLock.UNFENCED) { -154: result.resumePoint().ifPresent(checkpoint -> ledger.saveCheckpoint(checkpoint, lock.fence())); -157: migration.id(), migration.checksum(), context.operator(), clock.instant(), lock.fence()); -``` - -154행은 재개 지점을 남긴 마이그레이션에만 걸린다. 그 값을 받은 원장은 미지정이면 거절한다. - -```java -private static void requireCurrentFence(long fence, String what) { - if (fence == MongoMigrationLock.UNFENCED) { -``` - -재개 가능성에만 반응하는 펜스 조건이 그 자리에 있다. javadoc 의 마지막 문장은 근거 없는 착오가 아니다. - -## 세 번째 관문이 먼저 던진다 - -```text -74: if (!lock.held()) { -80: migrations.forEach(this::validate); -82: if (lock.fence() == MongoMigrationLock.UNFENCED) { -93: return migrations.stream() -``` - -82행의 조건은 리스의 펜스 값 하나다. 80행은 목록을 순회하지만 원장과 체크섬을 볼 뿐이고, 82행은 그 결과도 마이그레이션도 참조하지 않는다. 154행에 닿으려면 93행의 스트림에 들어가야 하는데 82행이 그보다 앞이다. - -그 자리에서 나오는 문장도 재개 가능성을 말하지 않는다. - -```java -"this migration lease exposes no fencing token, so a stalled runner cannot be excluded;" - + " use a fenced lease or run the migration inside a maintenance window with the" - + " application stopped" -``` - -## 빈 목록도 거절된다 - -:::evidence key="a06-f016-flamingock-probe" alt="잠금 어댑터가 보고하는 보유 여부와 펜스 값, 빈 마이그레이션 목록과 체크포인트를 만들지 않는 마이그레이션 한 건을 그 리스로 적용한 결과와 거절 문장, 그리고 같은 마이그레이션을 펜스가 있는 리스로 적용한 결과를 출력한 터미널 기록." caption="펜스는 -1 · 빈 목록도 거절, 재개 불가 한 건도 같은 문장으로 거절 · 같은 마이그레이션이 펜스가 있는 리스에서는 COMPLETED — 11줄 · exit 0" zoom="true" -::: - -이 어댑터로 리스를 만들고 아무것도 없는 목록을 적용했다. - -```text -[검사가 마이그레이션을 보기 전에 걸린다] - 마이그레이션 0건 -> 거절, MongoOperationRejectedException - this migration lease exposes no fencing token, so a stalled runner cannot be excluded; ... - 재개 불가 1건 -> 거절, MongoOperationRejectedException - this migration lease exposes no fencing token, so a stalled runner cannot be excluded; ... -``` - -목록이 비어 있으면 재개 가능한지 여부가 존재하지 않는데 같은 문장이 나온다. 한 건을 넣어도 결과는 같다. 그 한 건은 한 번의 호출로 완료를 돌려주므로 체크포인트를 남기지 않는다. - -같은 마이그레이션을 펜스가 있는 리스로 적용하면 이렇게 된다. - -```text -[같은 마이그레이션, 펜스가 있는 리스] - 재개 불가 1건 -> 실행, 결과 1건 COMPLETED -``` - -거절의 대상은 마이그레이션이 아니라 리스다. - -## 검사를 옮기는 수정은 두 곳에 막힌다 - -마이그레이션 인터페이스에는 재개 가능성을 선언하는 멤버가 없다. - -```text -17: MongoMigrationId id(); -20: MongoMigrationChecksum checksum(); -23: MongoMigrationPrecondition precondition(); -26: MongoMigrationResult execute(MongoMigrationContext context); -29: MongoMigrationPostcondition postcondition(); -``` - -재개 가능한지는 `execute` 가 체크포인트를 돌려준 뒤에야 드러난다. 그리고 82행을 스트림 안으로 옮겨도 157행이 같은 미지정 값을 원장 기록으로 넘긴다. 원장은 그것을 거절하므로, 데이터베이스는 바뀌고 원장에는 아무것도 남지 않는 상태가 된다. - -## 이 어댑터가 나오는 곳 - -어댑터를 만드는 곳은 시험 두 곳이다. 하나는 획득 실패를, 다른 하나는 해제와 해제 뒤 갱신 거절을 확인한다. 적용을 부르는 시험은 없어서 javadoc 과 82행의 어긋남이 시험에도 걸리지 않는다. - -조립 쪽도 같다. 실행자와 원장과 잠금을 프로덕션에서 참조하는 곳이 없고, 자동 구성이 이 패키지에서 만드는 빈도 없다. 지금 도는 배포를 멈추는 결함이 아니라, 이 리프를 가져다 조립하는 쪽이 처음 실행하는 순간에 드러날 결함이다. - -## 확인하지 못한 것 - -이 어댑터를 실제 배포에서 골랐을 때의 운영 경험은 다루지 않았다. 확인한 것은 세 번째 관문의 조건과 그 조건이 만드는 거절의 범위다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-grpc-advanced-bootstrap-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-grpc-advanced-bootstrap-f02.md deleted file mode 100644 index 7bd3d74..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-grpc-advanced-bootstrap-f02.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -kind: CASE -slug: grpc-advanced-bootstrap-f02 -title: 승격 게이트가 하향 전이도 승격 규칙으로 판정하고, javadoc 이 약속한 거부는 없다 -topic: declaration-and-document-drift -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-advanced-bootstrap-f02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-advanced-bootstrap-f02 - file: ../../../final/evidence/rendered/grpc-advanced-bootstrap-f02.svg -evidence: - - ../../../final/evidence/raw/grpc-advanced-bootstrap-f02.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-advanced-bootstrap#L182 이다. -module: grpc-advanced-bootstrap -priority: P3 ---- - -# 승격 게이트가 하향 전이도 승격 규칙으로 판정하고, javadoc 이 약속한 거부는 없다 - -던지는 경우는 널과 from == to 둘뿐이다. "이 게이트가 다루는 전이가 아닐 때" 라는 조건에 해당하는 검사가 없다. - -## 문제 - -던지는 경우는 널과 from == to 둘뿐이다. - -"이 게이트가 다루는 전이가 아닐 때" 라는 조건에 해당하는 검사가 없다. - -## 결론 - -그래서 하향 전이가 승격 규칙으로 판정된다. - -능력을 철회하려는 결정이 증거 부족을 이유로 막힌다. - -방향이 뒤집혀 있다. - -지금은 도달성이 낮다 — 이 게이트를 부르는 production 코드가 없고 테스트도 상향 전이만 넣는다. - -기록하는 이유는 javadoc 이 그 거부를 이미 약속했다는 점이다. - -수정은 to.ordinal() 이 아니라 등급의 서열을 명시한 뒤 상향 전이만 받고 나머지는 던지는 것이다. - -철회는 별도 경로가 필요하다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : 승격 게이트가 던지는 조건 전수 확인과 javadoc 이 약속한 거부의 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-advanced-bootstrap#L182 에 있다. - -## 본문 - - - -던지는 경우는 널과 `from == to` 둘뿐이다. "이 게이트가 다루는 전이가 아닐 때" 라는 조건에 해당하는 검사가 없다. - -## 게이트가 던지는 두 경우 - -:::evidence key="grpc-advanced-bootstrap-f02" alt="분석 문서 final/document.md#a20-grpc-advanced-bootstrap 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-advanced-bootstrap 발췌 — 15줄" zoom="true" -::: - -## 하향 전이가 승격 규칙으로 판정된다 - -능력을 철회하려는 결정이 증거 부족을 이유로 막힌다. 방향이 뒤집혀 있다. - -## 지금은 도달성이 낮다 - -이 게이트를 부르는 production 코드가 없고 테스트도 상향 전이만 넣는다. 기록하는 이유는 javadoc 이 그 거부를 이미 약속했다는 점이다. - -## 수정 - -`to.ordinal()` 이 아니라 등급의 서열을 명시한 뒤 상향 전이만 받고 나머지는 던지는 것이다. 철회는 별도 경로가 필요하다. - -## 확인하지 못한 것 - -하향 전이를 실제로 넣어 게이트의 판정을 관측하지 않았다. 던지는 조건이 널과 from == to 둘뿐이라는 것으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-grpc-advanced-edition-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-grpc-advanced-edition-f02.md deleted file mode 100644 index bcfcf2a..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-grpc-advanced-edition-f02.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -kind: CASE -slug: grpc-advanced-edition-f02 -title: 승격 차단 목록에 담금 기간과 실환경 항목이 없다 -topic: declaration-and-document-drift -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-advanced-edition-f02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-advanced-edition-f02 - file: ../../../final/evidence/rendered/grpc-advanced-edition-f02.svg -evidence: - - ../../../final/evidence/raw/grpc-advanced-edition-f02.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-advanced-edition#L206 이다. -module: grpc-advanced-edition -priority: P3 ---- - -# 승격 차단 목록에 담금 기간과 실환경 항목이 없다 - -GrpcEdition2024Gate.promotionBlockers 가 보는 것은 셋이다 — 호환성 보고서의 문제들, 소비자 이관 계획, 승격 ADR. 같은 가족의 GrpcAdvancedPromotionGate 는 EDITION_2024 능력에 대해 일곱 증거 항목과 7일 담금을 요구한다. - -## 문제 - -GrpcEdition2024Gate.promotionBlockers 가 보는 것은 셋이다 — 호환성 보고서의 문제들, 소비자 이관 계획, 승격 ADR. - -같은 가족의 GrpcAdvancedPromotionGate 는 EDITION_2024 능력에 대해 일곱 증거 항목과 7일 담금을 요구한다. - -## 결론 - -두 게이트가 같은 능력의 승격을 서로 다른 기준으로 판정한다. - -두 게이트가 각각 다른 것을 묻는다고 볼 수도 있다 — 하나는 편집 자체의 호환성, 하나는 능력의 운영 준비도. - -다만 어느 쪽도 상대를 부르지 않고, 문서에도 두 게이트의 관계가 적혀 있지 않다. - -승격을 실제로 수행할 때 어느 쪽을 만족해야 하는지가 코드에서 답해지지 않는다. - -수정은 promotionBlockers 가 GrpcAdvancedPromotionGate.evaluate 의 결과를 포함하게 하거나, 두 게이트의 역할 분담을 자바독에 적는 것이다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : GrpcEdition2024Gate 참조 9건 검색과 GrpcAdvancedPromotionGate 의 요구 항목 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-advanced-edition#L206 에 있다. - -## 본문 - - - -`GrpcEdition2024Gate.promotionBlockers` 가 보는 것은 셋이다 — 호환성 보고서의 문제들, 소비자 이관 계획, 승격 ADR. - -## GrpcEdition2024Gate 참조 위치 - -:::evidence key="grpc-advanced-edition-f02" alt="코드베이스에서 GrpcEdition2024Gate 를 검색한 출력 9줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcEdition2024Gate 코드베이스 검색 — 9줄 · exit 0" zoom="true" -::: - -## 같은 가족의 다른 게이트는 다른 기준을 쓴다 - -`GrpcAdvancedPromotionGate` 는 `EDITION_2024` 능력에 대해 일곱 증거 항목과 7일 담금을 요구한다. 두 게이트가 같은 능력의 승격을 서로 다른 기준으로 판정한다. - -## 각각 다른 것을 묻는다고 볼 수도 있다 - -하나는 편집 자체의 호환성, 하나는 능력의 운영 준비도다. 다만 어느 쪽도 상대를 부르지 않고, 문서에도 두 게이트의 관계가 적혀 있지 않다. 승격을 실제로 수행할 때 어느 쪽을 만족해야 하는지가 코드에서 답해지지 않는다. 수정은 `promotionBlockers` 가 `GrpcAdvancedPromotionGate.evaluate` 의 결과를 포함하게 하거나, 두 게이트의 역할 분담을 자바독에 적는 것이다. - -## 확인하지 못한 것 - -두 게이트를 실제로 실행해 판정 차이를 관측하지 않았다. 차단 목록의 구성 요소 대조로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-grpc-advanced-resilience-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-grpc-advanced-resilience-f01.md deleted file mode 100644 index d84d0f8..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-grpc-advanced-resilience-f01.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -kind: CASE -slug: grpc-advanced-resilience-f01 -title: 부트스트랩 대조가 문서 어디든의 부분 문자열을 본다 -topic: declaration-and-document-drift -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-advanced-resilience-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-advanced-resilience-f01 - file: ../../../final/evidence/rendered/grpc-advanced-resilience-f01.svg -evidence: - - ../../../final/evidence/raw/grpc-advanced-resilience-f01.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-advanced-resilience#L136 이다. -module: grpc-advanced-resilience -priority: P3 ---- - -# 부트스트랩 대조가 문서 어디든의 부분 문자열을 본다 - -세 검사가 모두 문서 전체에 대한 부분 문자열 포함이다. JSON 파서를 쓰지 않은 이유는 자바독이 밝힌다 — 세 필드를 보려고 파서를 xDS 를 켜는 모든 배포의 실행 클래스패스에 올리지 않겠다는 것이다. - -## 문제 - -세 검사가 모두 문서 전체에 대한 부분 문자열 포함이다. - -JSON 파서를 쓰지 않은 이유는 자바독이 밝힌다 — 세 필드를 보려고 파서를 xDS 를 켜는 모든 배포의 실행 클래스패스에 올리지 않겠다는 것이다. - -## 결론 - -그 판단 자체는 이 저장소의 다른 결정들과 일관된다. - -다만 검사의 형태가 그 판단보다 느슨하다. - -"tls" 가 문서 어디에든 있으면 통과한다. - -통제 평면 채널이 insecure 로 설정되어 있고 다른 곳(예: 서버 리스너 설정)에 tls 라는 낱말이 있으면 두 번째 검사가 지나간다. - -자원 이름공간이 주석·다른 필드·다른 서버 항목에 있어도 통과한다. - -세 번째 검사가 막으려는 것은 "이 클라이언트가 자기 이름공간 밖을 구독하는 것" 인데, 문자열이 어딘가에 있다는 것은 그것이 이 클라이언트의 구독 대상이라는 뜻이 아니다. - -그리고 이 검사가 막으려는 실패는 자바독이 스스로 "조용하다" 고 적은 것이다 — 아무것도 오류가 되지 않는 종류다. - -느슨한 검사와 조용한 실패의 조합이 이 항목을 기록하는 이유다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : 세 검사가 쓰는 대조 방식과 자바독이 밝힌 파서 미사용 근거의 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-advanced-resilience#L136 에 있다. - -## 본문 - - - -세 검사가 모두 문서 전체에 대한 부분 문자열 포함이다. - -## 세 검사가 쓰는 대조 방식 - -:::evidence key="grpc-advanced-resilience-f01" alt="분석 문서 final/document.md#a20-grpc-advanced-resilience 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-advanced-resilience 발췌 — 15줄" zoom="true" -::: - -## 파서를 쓰지 않은 판단 자체는 일관된다 - -자바독이 밝히듯 세 필드를 보려고 파서를 xDS 를 켜는 모든 배포의 실행 클래스패스에 올리지 않겠다는 것이다. 다만 검사의 형태가 그 판단보다 느슨하다. - -## 낱말이 어디에 있든 통과한다 - -`"tls"` 가 문서 어디에든 있으면 통과한다. 통제 평면 채널이 `insecure` 로 설정되어 있고 다른 곳(예: 서버 리스너 설정)에 `tls` 라는 낱말이 있으면 두 번째 검사가 지나간다. 자원 이름공간이 주석·다른 필드·다른 서버 항목에 있어도 통과한다. 세 번째 검사가 막으려는 것은 "이 클라이언트가 자기 이름공간 밖을 구독하는 것" 인데, 문자열이 어딘가에 있다는 것은 그것이 이 클라이언트의 구독 대상이라는 뜻이 아니다. - -## 느슨한 검사와 조용한 실패의 조합 - -이 검사가 막으려는 실패는 자바독이 스스로 "조용하다" 고 적은 것이다. 수정은 파서를 들이지 않고도 가능하다 — `"channel_creds"` 를 포함하는 객체 범위 안에서 `"type"` 값을 찾는 정도의 구조 인식이면 두 번째 검사가 실제 조건에 가까워진다. 또는 파서를 테스트 범위에만 두고 이 가드는 형태를 좁힌 정규식으로 바꾼다. - -## 확인하지 못한 것 - -실제 xDS 통제 평면을 세워 부트스트랩 대조를 재현하지 않았다. 세 검사의 문자열 포함 조건으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-grpc-client-f04.md b/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-grpc-client-f04.md deleted file mode 100644 index 8ba5d38..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-grpc-client-f04.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -kind: CASE -slug: grpc-client-f04 -title: 프로파일 검증기가 javadoc 이 든 두 실수 중 하나만 검사한다 -topic: declaration-and-document-drift -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-client-f04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-client-f04 - file: ../../../final/evidence/rendered/grpc-client-f04.svg -evidence: - - ../../../final/evidence/raw/grpc-client-f04.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-client#L199 이다. -module: grpc-client -priority: P3 ---- - -# 프로파일 검증기가 javadoc 이 든 두 실수 중 하나만 검사한다 - -구현된 것은 첫째와 다른 것이다. 이름이 같은 프로파일이 두 번 선언된 경우를 잡는다. - -## 문제 - -구현된 것은 첫째와 다른 것이다. - -이름이 같은 프로파일이 두 번 선언된 경우를 잡는다. - -## 결론 - -javadoc 이 든 둘째는 이름이 다르고 대상이 같은 경우인데, 그 검사가 없다. - -지도는 이름을 키로 쓰므로 같은 대상을 가리키는 두 이름은 서로를 만나지 않는다. - -그리고 둘째가 실제로 더 찾기 어려운 형태다 — 이름이 같으면 설정 결속이 먼저 실패하거나 나중 것이 이기지만, 이름이 다르면 조용히 두 채널이 생긴다. - -수정은 대상과 설정을 키로 하는 두 번째 지도를 두고 역방향 중복을 보고하는 것이다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : javadoc 이 든 두 실수와 검증기에 실제로 구현된 검사의 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-client#L199 에 있다. - -## 본문 - - - -구현된 것은 javadoc 이 든 첫째와 **다른 것**이다 — 이름이 같은 프로파일이 두 번 선언된 경우를 잡는다. - -## 검증기가 실제로 잡는 실수 - -:::evidence key="grpc-client-f04" alt="분석 문서 final/document.md#a20-grpc-client 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-client 발췌 — 15줄" zoom="true" -::: - -## 둘째에 해당하는 검사가 없다 - -javadoc 이 든 둘째는 **이름이 다르고 대상이 같은** 경우인데, 지도는 이름을 키로 쓰므로 같은 대상을 가리키는 두 이름은 서로를 만나지 않는다. - -## 둘째가 더 찾기 어려운 형태다 - -이름이 같으면 설정 결속이 먼저 실패하거나 나중 것이 이기지만, 이름이 다르면 조용히 두 채널이 생긴다. 수정은 대상과 설정을 키로 하는 두 번째 지도를 두고 역방향 중복을 보고하는 것이다. - -## 확인하지 못한 것 - -검증되지 않는 둘째 실수를 담은 프로파일로 시작을 시도하지 않았다. 검증기 본문의 검사 목록으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-grpc-discovery-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-grpc-discovery-f02.md deleted file mode 100644 index 6881dda..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-grpc-discovery-f02.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -kind: CASE -slug: grpc-discovery-f02 -title: 리졸버 검증기의 규칙이 하나뿐인데 javadoc 은 복수형으로 서술한다 -topic: declaration-and-document-drift -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-discovery-f02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-discovery-f02 - file: ../../../final/evidence/rendered/grpc-discovery-f02.svg -evidence: - - ../../../final/evidence/raw/grpc-discovery-f02.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-discovery#L177 이다. -module: grpc-discovery -priority: P3 ---- - -# 리졸버 검증기의 규칙이 하나뿐인데 javadoc 은 복수형으로 서술한다 - -javadoc 은 "Checks a discovery configuration for the things that look right and are not" 라고 적는다. 실제로 담긴 규칙은 균형 정책의 무의미함 하나다. - -## 문제 - -javadoc 은 "Checks a discovery configuration for the things that look right and are not" 라고 적는다. - -실제로 담긴 규칙은 균형 정책의 무의미함 하나다. - -## 결론 - -나머지 위험 조합은 GrpcResolverProfile 정규 생성자가 이미 거부하므로 결과적으로 빈틈은 아니다. - -다만 목록으로 보고하는 API 형태와 규칙 하나라는 내용이 어긋나 있어, 다음 사람이 여기에 규칙을 더할 자리로 읽거나 이미 여러 규칙이 있다고 읽는다. - -§17.1 이 실제로 그 자리다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : GrpcResolverProfile 참조 16건 검색과 검증기에 실제로 담긴 규칙 수 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-discovery#L177 에 있다. - -## 본문 - - - -javadoc 은 "Checks a discovery configuration for **the things** that look right and are not" 라고 적는다. 실제로 담긴 규칙은 균형 정책의 무의미함 하나다. - -## GrpcResolverProfile 참조 위치 - -:::evidence key="grpc-discovery-f02" alt="코드베이스에서 GrpcResolverProfile 를 검색한 출력 16줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcResolverProfile 코드베이스 검색 — 16줄 · exit 0" zoom="true" -::: - -## 결과적으로 빈틈은 아니다 - -나머지 위험 조합은 `GrpcResolverProfile` 정규 생성자가 이미 거부한다. - -## 어긋난 것은 형태와 내용이다 - -목록으로 보고하는 API 형태와 규칙 하나라는 내용이 어긋나 있어, 다음 사람이 여기에 규칙을 더할 자리로 읽거나 이미 여러 규칙이 있다고 읽는다. §17.1 이 실제로 그 자리다. - -## 확인하지 못한 것 - -시작 검증기를 통한 스킴 거부를 실행으로 확인하지 않았다. 그 검증기가 돌지 않는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-grpc-observability-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-grpc-observability-f02.md deleted file mode 100644 index 9a0ae34..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-grpc-observability-f02.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -kind: CASE -slug: grpc-observability-f02 -title: deadlineRemaining 도 meter 가 없다. javadoc 은 그것이 기록된다고 말한다 -topic: declaration-and-document-drift -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-observability-f02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-observability-f02 - file: ../../../final/evidence/rendered/grpc-observability-f02.svg -evidence: - - ../../../final/evidence/raw/grpc-observability-f02.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-observability#L213 이다. -module: grpc-observability -priority: P3 ---- - -# deadlineRemaining 도 meter 가 없다. javadoc 은 그것이 기록된다고 말한다 - -§17.1 과 같은 형태가 GrpcRpcObservation 에도 있고, 이쪽은 클래스 javadoc 이 명시적으로 어긋난다. 두 값을 함께 들면서 "기록된다"고 단언하는데, record(GrpcRpcObservation) 이 등록하는 meter 는 넷이다. - -## 문제 - -§17.1 과 같은 형태가 GrpcRpcObservation 에도 있고, 이쪽은 클래스 javadoc 이 명시적으로 어긋난다. - -두 값을 함께 들면서 "기록된다"고 단언하는데, record(GrpcRpcObservation) 이 등록하는 meter 는 넷이다. - -## 결론 - -queueWaitTime 은 QUEUE_WAIT 타이머로 나간다. - -deadlineRemaining 은 나가는 곳이 없다 — meter 이름 상수 일곱 개 중에도 마감 잔량에 해당하는 것이 없고, tags() 에도 들어가지 않는다(태그로 넣으면 카디널리티가 터지므로 그것이 옳다). - -그래서 이 성분을 읽는 코드는 unusedDeadline() 하나이고, 그 메서드의 production 호출자는 0 이다(§12.1). - -§4.6 은 이 성분의 검증 비대칭(음수 허용)이 "마감을 넘긴 호출을 표현하기 위한 것" 이라고 읽었다. - -그 해석은 그대로 유효하다 — 다만 그 표현이 도달하는 곳이 아직 없다. - -관측값으로서는 §17.1 의 queueHighWatermark 와 같은 처지다. - -queueWaitTime 과 같은 형태로 타이머를 하나 더 둔다(마감을 넘긴 경우는 unusedDeadline() 이 이미 빈 값으로 구분해 주므로 기록 대상에서 빼면 된다). - -아니면 javadoc 의 "recorded" 를 "carried" 로 낮춘다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : GrpcRpcObservation 참조 5건 검색과 클래스 javadoc 의 기록 주장 대비 meter 넷 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-observability#L213 에 있다. - -## 본문 - - - -§17.1 과 같은 형태가 `GrpcRpcObservation` 에도 있고, 이쪽은 클래스 javadoc 이 명시적으로 어긋난다. - -> "{@code deadlineRemaining} and {@code queueWaitTime} are **recorded** because they are the two numbers that explain a latency change without being latency." - -## GrpcRpcObservation 참조 위치 - -:::evidence key="grpc-observability-f02" alt="코드베이스에서 GrpcRpcObservation 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcRpcObservation 코드베이스 검색 — 5줄 · exit 0" zoom="true" -::: - -## 둘 중 하나만 나간다 - -`record(GrpcRpcObservation)` 이 등록하는 meter 는 넷이고, `queueWaitTime` 은 `QUEUE_WAIT` 타이머로 나간다. `deadlineRemaining` 은 나가는 곳이 없다 — meter 이름 상수 일곱 개 중에도 마감 잔량에 해당하는 것이 없고, `tags()` 에도 들어가지 않는다(태그로 넣으면 카디널리티가 터지므로 그것이 옳다). - -## 읽는 코드가 하나이고 그 호출자가 0이다 - -`unusedDeadline()` 하나이고 production 호출자는 0 이다(§12.1). §4.6 은 이 성분의 검증 비대칭(음수 허용)이 "마감을 넘긴 호출을 표현하기 위한 것" 이라고 읽었다 — 그 해석은 그대로 유효하고, 다만 그 표현이 도달하는 곳이 아직 없다. - -## 수정 - -`queueWaitTime` 과 같은 형태로 타이머를 하나 더 둔다(마감을 넘긴 경우는 `unusedDeadline()` 이 이미 빈 값으로 구분해 준다). 아니면 javadoc 의 "recorded" 를 "carried" 로 낮춘다. - -## 확인하지 못한 것 - -meter 이름 상수 일곱 개와 두 record 오버로드 본문으로 판정했다. 다른 이름의 상수가 그 역할을 겸하는지는 이름만 보고 배제했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-grpc-policy-f06.md b/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-grpc-policy-f06.md deleted file mode 100644 index 7efa173..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-grpc-policy-f06.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -kind: CASE -slug: grpc-policy-f06 -title: 오류 노출 거부 목록의 "호스트와 포트" 규칙이 IPv4 점표기만 본다 -topic: declaration-and-document-drift -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-policy-f06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-policy-f06 - file: ../../../final/evidence/rendered/grpc-policy-f06.svg -evidence: - - ../../../final/evidence/raw/grpc-policy-f06.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-policy#L305 이다. -module: grpc-policy -priority: P3 ---- - -# 오류 노출 거부 목록의 "호스트와 포트" 규칙이 IPv4 점표기만 본다 - -아홉 패턴을 전부 읽으면 주소 형태를 보는 것은 이 하나다. 클래스 javadoc 은 거부 대상을 "a stack frame, a SQL fragment, a JDBC URL, a bearer token, a host and port, a file path" 로 서술하는데, 실제로 걸리는 host 는 IPv4 점표기뿐이다. - -## 문제 - -아홉 패턴을 전부 읽으면 주소 형태를 보는 것은 이 하나다. - -클래스 javadoc 은 거부 대상을 "a stack frame, a SQL fragment, a JDBC URL, a bearer token, a host and port, a file path" 로 서술하는데, 실제로 걸리는 host 는 IPv4 점표기뿐이다. - -## 결론 - -IPv6 리터럴 — fe80::1, [2001:db8::1]:5432 DNS 이름과 포트 — documents-db.internal:5432, kafka-0.kafka-headless:9092 jdbc:postgresql://db/app 이 막히는 것은 host 규칙이 아니라 jdbc: 규칙 때문이다. - -즉 이 구멍은 테스트에도 없다 — exposurePolicyRefusesLeakyStrings 의 아홉 사례 중 주소는 upstream 10.0.3.14:5432 refused 하나이고 IPv4 다. - -닿는 경로는 mapUnknown 이다. - -인식되지 않은 예외의 메시지를 safeToExpose 가 통과시키면 그대로 클라이언트로 간다. - -IPv6 클러스터나 쿠버네티스 서비스 이름을 쓰는 배포에서 상류 좌표가 밖으로 나간다. - -등급이 P3 인 이유는 두 가지다. - -이 리프가 build-only 라 오늘 닿지 않고, 노출되는 것이 자격증명이 아니라 내부 좌표다. - -다만 이 정책이 존재하는 이유 자체가 "부분 마스킹이 아니라 통째 교체" 이므로, 목록에 빠진 형태는 통째로 통과한다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : 거부 목록 아홉 패턴 전수 확인과 클래스 javadoc 의 거부 대상 서술 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-policy#L305 에 있다. - -## 본문 - - - -아홉 패턴을 전부 읽으면 주소 형태를 보는 것은 하나이고 그것이 IPv4 점표기만 본다. 클래스 javadoc 은 거부 대상을 "a stack frame, a SQL fragment, a JDBC URL, a bearer token, **a host and port**, a file path" 로 서술한다. - -## 아홉 패턴 중 주소를 보는 하나 - -:::evidence key="grpc-policy-f06" alt="분석 문서 final/document.md#a20-grpc-policy 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-policy 발췌 — 15줄" zoom="true" -::: - -## 걸리지 않는 형태들 - -IPv6 리터럴(`fe80::1`, `[2001:db8::1]:5432`)과 DNS 이름과 포트(`documents-db.internal:5432`, `kafka-0.kafka-headless:9092`)다. `jdbc:postgresql://db/app` 이 막히는 것은 host 규칙이 아니라 `jdbc:` 규칙 때문이다. - -## 테스트에도 없다 - -`exposurePolicyRefusesLeakyStrings` 의 아홉 사례 중 주소는 `upstream 10.0.3.14:5432 refused` 하나이고 IPv4 다. - -## 닿는 경로 - -`mapUnknown` 이다. 인식되지 않은 예외의 메시지를 `safeToExpose` 가 통과시키면 그대로 클라이언트로 간다. IPv6 클러스터나 쿠버네티스 서비스 이름을 쓰는 배포에서 상류 좌표가 밖으로 나간다. - -## 등급이 P3 인 이유 둘 - -이 리프가 build-only 라 오늘 닿지 않고, 노출되는 것이 자격증명이 아니라 내부 좌표다. 다만 이 정책이 존재하는 이유 자체가 "부분 마스킹이 아니라 통째 교체" 이므로, 목록에 빠진 형태는 통째로 통과한다. - -## 확인하지 못한 것 - -IPv6 누출을 실제 예외 메시지로 재현하지 않았다. 아홉 패턴 중 IPv4 점표기 외에 주소 형태를 보는 것이 없음을 확인해 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-grpc-proto-contract-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-grpc-proto-contract-f02.md deleted file mode 100644 index abcb1df..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-grpc-proto-contract-f02.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -kind: CASE -slug: grpc-proto-contract-f02 -title: 반환 목록이 자바독이 약속한 source order 가 아니다 -topic: declaration-and-document-drift -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-proto-contract-f02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-proto-contract-f02 - file: ../../../final/evidence/rendered/grpc-proto-contract-f02.svg -evidence: - - ../../../final/evidence/raw/grpc-proto-contract-f02.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-proto-contract#L191 이다. -module: grpc-proto-contract -priority: P3 ---- - -# 반환 목록이 자바독이 약속한 source order 가 아니다 - -validate 의 javadoc 은 "@return every violation found, in source order" 라고 적는다. 실제로는 파일 앞머리의 syntax·package 위반이 40번째 줄의 map 위반보다 뒤에 온다. - -## 문제 - -validate 의 javadoc 은 "@return every violation found, in source order" 라고 적는다. - -실제로는 파일 앞머리의 syntax·package 위반이 40번째 줄의 map 위반보다 뒤에 온다. - -## 결론 - -describe() 가 file:line rule — detail 형태를 만들고 그 형태의 목적이 빌드 로그를 읽는 것이므로, 정렬이 어긋나면 리뷰 목록으로서의 값이 줄어든다. - -수정은 반환 직전에 line 으로 안정 정렬하는 것이다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : validate 의 javadoc 이 약속한 정렬과 실제 위반 수집 순서의 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-proto-contract#L191 에 있다. - -## 본문 - - - -`validate` 의 javadoc 은 "@return every violation found, **in source order**" 라고 적는다. 실제로는 파일 앞머리의 `syntax`·`package` 위반이 40번째 줄의 `map` 위반보다 뒤에 온다. - -## javadoc 이 약속한 정렬 - -:::evidence key="grpc-proto-contract-f02" alt="분석 문서 final/document.md#a20-grpc-proto-contract 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-proto-contract 발췌 — 15줄" zoom="true" -::: - -## 그 형태의 목적이 빌드 로그를 읽는 것이다 - -`describe()` 가 `file:line rule — detail` 형태를 만들므로, 정렬이 어긋나면 리뷰 목록으로서의 값이 줄어든다. 수정은 반환 직전에 `line` 으로 안정 정렬하는 것이다. - -## 확인하지 못한 것 - -실제 빌드 로그에서 순서 뒤바뀜을 관측하지 않았다. 수집 순서 코드로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-grpc-server-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-grpc-server-f02.md deleted file mode 100644 index 02bd638..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-grpc-server-f02.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -kind: CASE -slug: grpc-server-f02 -title: 원시 API 규칙이 import 문만 보므로 완전 수식 사용과 와일드카드를 놓친다 -topic: declaration-and-document-drift -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-server-f02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-server-f02 - file: ../../../final/evidence/rendered/grpc-server-f02.svg -evidence: - - ../../../final/evidence/raw/grpc-server-f02.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-server#L167 이다. -module: grpc-server -priority: P3 ---- - -# 원시 API 규칙이 import 문만 보므로 완전 수식 사용과 와일드카드를 놓친다 - -이것이 가정에 그치지 않는 이유는 이 저장소 자신의 문체다. 같은 가족의 여러 파일이 완전 수식 참조를 본문에 그대로 쓴다. - -## 문제 - -이것이 가정에 그치지 않는 이유는 이 저장소 자신의 문체다. - -같은 가족의 여러 파일이 완전 수식 참조를 본문에 그대로 쓴다. - -## 결론 - -즉 이 코드베이스에서 완전 수식 사용은 예외가 아니라 흔한 형태다. - -규칙 클래스의 자바독은 "there is nothing to reach for" 를 목표로 든다. - -지금 형태는 손이 닿는 경로 하나만 본다. - -수정은 정규식을 타입 이름의 등장 자체로 넓히거나(오탐이 생기므로 주석·문자열 제거가 필요), 바이트코드 기반 검사로 옮기는 것이다. - -후자가 이 저장소의 다른 아키텍처 게이트와 형태가 같다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : 규칙이 스캔하는 구문 범위와 같은 가족 파일들의 완전 수식 참조 사용 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-server#L167 에 있다. - -## 본문 - - - -원시 API 규칙이 import 문만 보므로 완전 수식 사용과 와일드카드를 놓친다. - -## 규칙이 보는 범위 - -:::evidence key="grpc-server-f02" alt="분석 문서 final/document.md#a20-grpc-server 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-server 발췌 — 15줄" zoom="true" -::: - -## 가정에 그치지 않는 이유는 이 저장소 자신의 문체다 - -같은 가족의 여러 파일이 완전 수식 참조를 본문에 그대로 쓴다. 즉 이 코드베이스에서 완전 수식 사용은 예외가 아니라 흔한 형태다. - -## 규칙의 목표와 어긋난다 - -규칙 클래스의 자바독은 "there is nothing to reach for" 를 목표로 든다. 지금 형태는 손이 닿는 경로 하나만 본다. 수정은 정규식을 타입 이름의 등장 자체로 넓히거나(오탐이 생기므로 주석·문자열 제거가 필요), 바이트코드 기반 검사로 옮기는 것이다 — 후자가 이 저장소의 다른 아키텍처 게이트와 형태가 같다. - -## 확인하지 못한 것 - -완전 수식 사용을 담은 파일로 규칙을 실행해 통과를 관측하지 않았다. 규칙이 import 문만 본다는 코드 형태로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-messaging-kafka-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-messaging-kafka-f01.md deleted file mode 100644 index c9fd2f0..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-messaging-kafka-f01.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -kind: CASE -slug: messaging-kafka-f01 -title: deduplicatedPublish 를 문서는 지원으로 적고 코드는 거짓으로 둔다 -topic: declaration-and-document-drift -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-kafka-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-kafka-f01 - file: ../../../final/evidence/rendered/messaging-kafka-f01.svg - - key: messaging-kafka-f01-diagram - file: ../../../final/assets/diagrams/messaging-kafka-f01.svg -evidence: - - ../../../final/evidence/raw/messaging-kafka-f01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-kafka#L208 이다. -module: messaging-kafka -priority: P1 ---- - -# deduplicatedPublish 를 문서는 지원으로 적고 코드는 거짓으로 둔다 - -코드의 판정이 옳고 그 근거가 javadoc 에 있다. docs/messaging/support-matrix.md:55 의 능력 표는 이 칸을 O 로 적는다. - -## 문제 - -코드의 판정이 옳고 그 근거가 javadoc 에 있다. - -docs/messaging/support-matrix.md:55 의 능력 표는 이 칸을 O 로 적는다. - -## 결론 - -그 차이가 무거운 이유는 이 플랫폼에서 이 플래그가 특별하기 때문이다. - -능력 열둘 중 부재가 예외를 만드는 유일한 플래그다. - -그래서 표를 읽고 중복 제거를 전제한 목적지를 설계한 팀은 실행 시점에 능력 예외를 만난다. - -반대로 표를 읽고 "중복 제거가 있으니 모호를 그냥 재시도해도 된다" 고 결론지으면, 실제로는 중복이 저장된다. - -MessagingCapabilities 의 클래스 javadoc 이 그 피해를 미리 적는다 — "a silently weakened guarantee is indistinguishable from a working one until the incident." 수정은 문서 쪽이다. - -코드가 이미 옳다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : MessagingCapabilities 참조 39건 검색과 지원 문서 능력 표의 해당 칸 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-kafka#L208 에 있다. - -## 본문 - - - -코드의 판정이 옳고 그 근거가 javadoc 에 있다. `docs/messaging/support-matrix.md:55` 의 능력 표는 이 칸을 `O` 로 적는다. - -## 문서와 상수가 갈리는 칸 - -:::evidence key="messaging-kafka-f01-diagram" alt="지원 문서 쪽에 deduplicatedPublish 지원과 표를 읽은 설계가 놓이고 코드 상수 쪽에 거짓과 실행 시점 능력 예외가 빗금으로 놓인다" caption="문서와 상수가 갈리는 칸" zoom="false" -::: - -## MessagingCapabilities 참조 위치 - -:::evidence key="messaging-kafka-f01" alt="코드베이스에서 MessagingCapabilities 를 검색한 출력 39줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingCapabilities 코드베이스 검색 — 39줄 · exit 0" zoom="true" -::: - -## 이 플래그가 특별해서 차이가 무겁다 - -능력 열둘 중 **부재가 예외를 만드는 유일한 플래그**다. 그래서 표를 읽고 중복 제거를 전제한 목적지를 설계한 팀은 실행 시점에 능력 예외를 만난다. 반대로 표를 읽고 "중복 제거가 있으니 모호를 그냥 재시도해도 된다" 고 결론지으면, 실제로는 중복이 저장된다. - -## 수정은 문서 쪽이다 - -`MessagingCapabilities` 의 클래스 javadoc 이 그 피해를 미리 적는다 — "a silently weakened guarantee is indistinguishable from a working one until the incident." 코드가 이미 옳다. - -## 확인하지 못한 것 - -실제 브로커로 이 능력을 켠 소비자를 만들어 중복 도착을 관측하지 않았다. 문서와 코드 상수의 대조로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-messaging-observability-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-messaging-observability-f01.md deleted file mode 100644 index f6b0b2c..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-messaging-observability-f01.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -kind: CASE -slug: messaging-observability-f01 -title: 태그 어휘가 존재하고 유일한 호출부가 우회해, 실패 분류가 기록되지 않는다 -topic: declaration-and-document-drift -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-observability-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-observability-f01 - file: ../../../final/evidence/rendered/messaging-observability-f01.svg - - key: messaging-observability-f01-diagram - file: ../../../final/assets/diagrams/messaging-observability-f01.svg -evidence: - - ../../../final/evidence/raw/messaging-observability-f01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-observability#L673 이다. -module: messaging-observability -priority: P2 ---- - -# 태그 어휘가 존재하고 유일한 호출부가 우회해, 실패 분류가 기록되지 않는다 - -DefaultMessagingObservationConvention은 소비자가 0이다. 유일한 production 호출부(DefaultMessagePublisher.observe:260-269)가 "publish" 리터럴과 4인자 MessagingTags.of(...)를 쓴다. - -## 관계 - -- **타입이 문서화한 불변식은 타입이 강제한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 문제 - -DefaultMessagingObservationConvention은 소비자가 0이다. - -유일한 production 호출부(DefaultMessagePublisher.observe:260-269)가 "publish" 리터럴과 4인자 MessagingTags.of(...)를 쓴다. - -## 결론 - -그 factory는 failureCategory와 retryStage를 NONE으로 고정한다. - -convention의 publish(broker, dest, completion, Optional)는 정확히 failureCategory를 채우려고 존재한다. - -convention javadoc이 "an adapter inventing its own spelling of 'rejected' silently breaks every alert that was watching for it"를 이유로 중앙화를 선언했고, 첫 호출부가 그것을 지나쳤다. - -그리고 결과가 철자 문제에 그치지 않는다 — MessagingTags가 선언한 6차원 중 4개만 채워진다. - -메트릭이 배선되더라도(§다음 항목) 실패한 발행이 failureCategory=none으로 기록되어, "왜 실패했는가"를 메트릭에서 나눌 수 없다. - -PublishResult.failure()에 FailureDescriptor가 이미 있으므로 값은 손에 있다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : DefaultMessagingObservationConvention 참조 1건 검색과 유일한 production 호출부가 넘기는 인자 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-observability#L673 에 있다. - -## 본문 - - - -`DefaultMessagingObservationConvention`은 소비자가 0이다. 유일한 production 호출부(`DefaultMessagePublisher.observe:260-269`)가 `"publish"` 리터럴과 **4인자** `MessagingTags.of(...)`를 쓴다. - -## 기록이 끊기는 자리 - -:::evidence key="messaging-observability-f01-diagram" alt="convention 이 채우는 것 쪽에 여섯 차원과 failureCategory 가 놓이고 호출부가 채우는 것 쪽에 네 차원과 NONE 고정이 빗금으로 놓인다" caption="기록이 끊기는 자리" zoom="false" -::: - -그 factory는 `failureCategory`와 `retryStage`를 `NONE`으로 고정한다. - -## DefaultMessagingObservationConvention 참조 위치 - -:::evidence key="messaging-observability-f01" alt="코드베이스에서 DefaultMessagingObservationConvention 를 검색한 출력 1줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DefaultMessagingObservationConvention 코드베이스 검색 — 1줄 · exit 0" zoom="true" -::: - -## convention 이 존재하는 이유가 그 필드다 - -`publish(broker, dest, completion, Optional)`는 정확히 `failureCategory`를 채우려고 존재한다. convention javadoc이 "an adapter inventing its own spelling of 'rejected' silently breaks every alert that was watching for it"를 이유로 중앙화를 선언했고, 첫 호출부가 그것을 지나쳤다. - -## 철자 문제에 그치지 않는다 - -`MessagingTags`가 선언한 6차원 중 4개만 채워진다. 메트릭이 배선되더라도 실패한 발행이 `failureCategory=none`으로 기록되어, "왜 실패했는가"를 메트릭에서 나눌 수 없다. `PublishResult.failure()`에 `FailureDescriptor`가 이미 있으므로 값은 손에 있다. - -## 확인하지 못한 것 - -실제 MeterRegistry 에 붙여 태그가 어떻게 기록되는지 관측하지 않았다. 호출부의 인자 수와 리터럴로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-messaging-outbox-jdbc-postgresql-f06.md b/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-messaging-outbox-jdbc-postgresql-f06.md deleted file mode 100644 index d70634f..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-messaging-outbox-jdbc-postgresql-f06.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -kind: CASE -slug: messaging-outbox-jdbc-postgresql-f06 -title: 백오프 지터가 인스턴스를 분산시키지 못한다 -topic: declaration-and-document-drift -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-outbox-jdbc-postgresql-f06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-outbox-jdbc-postgresql-f06 - file: ../../../final/evidence/rendered/messaging-outbox-jdbc-postgresql-f06.svg -evidence: - - ../../../final/evidence/raw/messaging-outbox-jdbc-postgresql-f06.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-outbox-jdbc-postgresql#L923 이다. -module: messaging-outbox-jdbc-postgresql -priority: P3 ---- - -# 백오프 지터가 인스턴스를 분산시키지 못한다 - -jittered = capped - (capped/8) * (exponent % 3) 는 exponent 만의 함수다. 같은 상태의 복제본들은 같은 값을 계산한다. - -## 문제 - -jittered = capped - (capped/8) * (exponent % 3) 는 exponent 만의 함수다. - -같은 상태의 복제본들은 같은 값을 계산한다. - -## 결론 - -javadoc 이 약속하는 "thundering herd 방지" 가 성립하지 않는다. - -OutboxRelay 가 이미 defaultOwner() 로 프로세스별 안정 식별자를 만든다(pid@uuid8). - -그것의 해시를 지터에 섞으면 결정성(같은 프로세스에서 재현 가능)을 유지하면서 인스턴스 간 위상차가 생긴다. - -javadoc 이 난수를 거부한 이유("a random source would make the schedule impossible to test")도 그대로 지켜진다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : 지터 계산식의 입력 변수 확인 — exponent 만의 함수임을 코드로 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-outbox-jdbc-postgresql#L923 에 있다. - -## 본문 - - - -`jittered = capped - (capped/8) * (exponent % 3)` 는 `exponent` 만의 함수다. 같은 상태의 복제본들은 같은 값을 계산한다. - -## OutboxRelay 참조 위치 - -:::evidence key="messaging-outbox-jdbc-postgresql-f06" alt="코드베이스에서 OutboxRelay 를 검색한 출력 27줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="OutboxRelay 코드베이스 검색 — 27줄 · exit 0" zoom="true" -::: - -## javadoc 이 약속한 성질이 성립하지 않는다 - -"thundering herd 방지" 다. - -## 재료가 이미 있다 - -`OutboxRelay` 가 `defaultOwner()` 로 프로세스별 안정 식별자를 만든다(`pid@uuid8`). 그것의 해시를 지터에 섞으면 결정성(같은 프로세스에서 재현 가능)을 유지하면서 인스턴스 간 위상차가 생긴다. javadoc 이 난수를 거부한 이유("a random source would make the schedule impossible to test")도 그대로 지켜진다. - -## 확인하지 못한 것 - -지터 동기화를 다중 인스턴스로 재현하지 않았다. 함수가 exponent 만의 함수라는 것은 코드로 확인했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-messaging-outbox-jdbc-postgresql-f07.md b/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-messaging-outbox-jdbc-postgresql-f07.md deleted file mode 100644 index 9ecfcaa..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-messaging-outbox-jdbc-postgresql-f07.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -kind: CASE -slug: messaging-outbox-jdbc-postgresql-f07 -title: 커넥션 획득 방식이 리프 안에서 갈린다 -topic: declaration-and-document-drift -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-outbox-jdbc-postgresql-f07 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-outbox-jdbc-postgresql-f07 - file: ../../../final/evidence/rendered/messaging-outbox-jdbc-postgresql-f07.svg -evidence: - - ../../../final/evidence/raw/messaging-outbox-jdbc-postgresql-f07.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-outbox-jdbc-postgresql#L929 이다. -module: messaging-outbox-jdbc-postgresql -priority: P3 ---- - -# 커넥션 획득 방식이 리프 안에서 갈린다 - -JdbcOutboxRepository.append 는 DataSourceUtils, 나머지는 raw dataSource.getConnection(), JdbcAdminOperationJournal 은 전부 DataSourceUtils. 릴레이 연산이 비즈니스 트랜잭션에 합류하면 안 된다는 판단은 타당하지만 어디에도 적혀 있지 않고, 같은 리프의 저널이 반대로 한다. - -## 문제 - -JdbcOutboxRepository.append 는 DataSourceUtils, 나머지는 raw dataSource.getConnection(), JdbcAdminOperationJournal 은 전부 DataSourceUtils. - -릴레이 연산이 비즈니스 트랜잭션에 합류하면 안 된다는 판단은 타당하지만 어디에도 적혀 있지 않고, 같은 리프의 저널이 반대로 한다. - -## 결론 - -withConnection 에 한 문장 — "릴레이 연산은 호출자 트랜잭션에 합류하지 않는다" — 을 붙이면 append 의 상세한 주석과 짝이 맞는다. - -저널이 DataSourceUtils 를 쓰는 것이 의도인지도 확인이 필요하다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : JdbcOutboxRepository 참조 5건 검색과 리프 안 세 지점의 커넥션 획득 방식 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-outbox-jdbc-postgresql#L929 에 있다. - -## 본문 - - - -`JdbcOutboxRepository.append` 는 `DataSourceUtils`, 나머지는 raw `dataSource.getConnection()`, `JdbcAdminOperationJournal` 은 전부 `DataSourceUtils` 를 쓴다. - -## JdbcOutboxRepository 참조 위치 - -:::evidence key="messaging-outbox-jdbc-postgresql-f07" alt="코드베이스에서 JdbcOutboxRepository 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="JdbcOutboxRepository 코드베이스 검색 — 5줄 · exit 0" zoom="true" -::: - -## 판단은 타당하고 어디에도 적혀 있지 않다 - -릴레이 연산이 비즈니스 트랜잭션에 합류하면 안 된다는 판단이다. 그리고 같은 리프의 저널이 반대로 한다. - -## 수정 - -`withConnection` 에 한 문장 — "릴레이 연산은 호출자 트랜잭션에 합류하지 않는다" — 을 붙이면 `append` 의 상세한 주석과 짝이 맞는다. 저널이 `DataSourceUtils` 를 쓰는 것이 의도인지도 확인이 필요하다. - -## 확인하지 못한 것 - -비즈니스 트랜잭션 합류 여부가 실제 동작에서 갈리는 것을 관측하지 않았다. 획득 방식의 코드 대조로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-messaging-schema-avro-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-messaging-schema-avro-f01.md deleted file mode 100644 index b71f4d4..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-messaging-schema-avro-f01.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -kind: CASE -slug: messaging-schema-avro-f01 -title: CI에서 돈다고 선언한 게이트를 부르는 CI가 없다 -topic: declaration-and-document-drift -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-schema-avro-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-schema-avro-f01 - file: ../../../final/evidence/rendered/messaging-schema-avro-f01.svg - - key: messaging-schema-avro-f01-diagram - file: ../../../final/assets/diagrams/messaging-schema-avro-f01.svg -evidence: - - ../../../final/evidence/raw/messaging-schema-avro-f01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-schema-avro#L524 이다. -module: messaging-schema-avro -priority: P2 ---- - -# CI에서 돈다고 선언한 게이트를 부르는 CI가 없다 - -AvroCompatibilityGate javadoc이 "Run in CI rather than at runtime"이라고 선언한다. 저장소 전체에서 이 클래스 참조는 자기 선언과 자기 테스트뿐이고, src/build.gradle의 9개 verifyMessaging* task 중 스키마 진화를 검사하는 것이 없다. - -## 관계 - -- **모드 enum을 분기 조건으로 쓰면 각 분기에 테스트를 둔다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **컬렉션 순서가 계약이면 양쪽에서 같은 방향으로 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **안정 코드는 판단 단위로 정하고 구현 단위로 정하지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 문제 - -AvroCompatibilityGate javadoc이 "Run in CI rather than at runtime"이라고 선언한다. - -저장소 전체에서 이 클래스 참조는 자기 선언과 자기 테스트뿐이고, src/build.gradle의 9개 verifyMessaging* task 중 스키마 진화를 검사하는 것이 없다. - -## 결론 - -게이트의 존재 이유가 "한 번 발행되면 보존 로그에 영구히 남는다"인데, 그 보호가 어느 파이프라인에도 붙어 있지 않다. - -AvroMessageCodec의 미사용과 달리 이것은 membership으로 설명되지 않는다 — 런타임 편입 여부와 무관하게 CI 게이트는 붙었어야 한다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : AvroCompatibilityGate 참조 3건 검색과 이 게이트를 부르는 Gradle 태스크·CI 워크플로 검색 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-schema-avro#L524 에 있다. - -## 본문 - - - -`AvroCompatibilityGate` javadoc이 "Run in CI rather than at runtime"이라고 선언한다. - -## 선언과 호출의 거리 - -:::evidence key="messaging-schema-avro-f01-diagram" alt="자기 테스트만 게이트를 부르는 것 안에 놓이고 verifyMessaging 태스크와 CI 워크플로가 바깥에 빗금으로 놓인다" caption="선언과 호출의 거리" zoom="false" -::: - -저장소 전체에서 이 클래스 참조는 자기 선언과 자기 테스트뿐이고, `src/build.gradle`의 9개 `verifyMessaging*` task 중 스키마 진화를 검사하는 것이 없다. - -## AvroCompatibilityGate 참조 위치 - -:::evidence key="messaging-schema-avro-f01" alt="코드베이스에서 AvroCompatibilityGate 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="AvroCompatibilityGate 코드베이스 검색 — 3줄 · exit 0" zoom="true" -::: - -## membership 으로 설명되지 않는다 - -게이트의 존재 이유가 "한 번 발행되면 보존 로그에 영구히 남는다"인데, 그 보호가 어느 파이프라인에도 붙어 있지 않다. `AvroMessageCodec`의 미사용과 달리 런타임 편입 여부와 무관하게 CI 게이트는 붙었어야 한다. - -## 확인하지 못한 것 - -파생 프로젝트가 이 게이트를 자기 CI 에서 부르는지 확인할 수 없었다. 저장소 안에 확인 수단이 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-messaging-spring-boot-starter-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-messaging-spring-boot-starter-f02.md deleted file mode 100644 index e4999cd..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-messaging-spring-boot-starter-f02.md +++ /dev/null @@ -1,102 +0,0 @@ ---- -kind: CASE -slug: messaging-spring-boot-starter-f02 -title: 출고되는 신뢰성 체인 전체가 아무도 공급하지 않는 빈 뒤에 있고, 그 사슬이 자기 클래스 안을 가리킨다 -topic: declaration-and-document-drift -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-spring-boot-starter-f02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-spring-boot-starter-f02 - file: ../../../final/evidence/rendered/messaging-spring-boot-starter-f02.svg - - key: messaging-spring-boot-starter-f02-diagram - file: ../../../final/assets/diagrams/messaging-spring-boot-starter-f02.svg -evidence: - - ../../../final/evidence/raw/messaging-spring-boot-starter-f02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-spring-boot-starter#L300 이다. -module: messaging-spring-boot-starter -priority: P2 ---- - -# 출고되는 신뢰성 체인 전체가 아무도 공급하지 않는 빈 뒤에 있고, 그 사슬이 자기 클래스 안을 가리킨다 - -여섯 빈 전부가 애플리케이션이 공급해야 하는 타입에 걸려 있다. 조건 자체는 옳고 근거도 정확하다 — 플랫폼이 기본 구현을 주면 조용히 엉뚱한 곳에, 또는 아무 데도 쓰지 않게 된다. - -## 관계 - -- **검증기는 발행이 아니라 주입이 강제다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 문제 - -여섯 빈 전부가 애플리케이션이 공급해야 하는 타입에 걸려 있다. - -조건 자체는 옳고 근거도 정확하다 — 플랫폼이 기본 구현을 주면 조용히 엉뚱한 곳에, 또는 아무 데도 쓰지 않게 된다. - -## 결론 - -문제는 저장소 안에 그 타입을 공급하는 코드가 없다는 것이다. - -messaging-outbox-jdbc-postgresql 의 JdbcOutboxRepository 는 스프링 스테레오타입도 @Bean 선언도 없고, new JdbcOutboxRepository 가 main 에 0 건이다. - -그래서 이 스타터를 켠 배포는 발행 경로는 얻고 발신함 경로는 얻지 못하며, 그 사실이 시작 시점에 어떤 신호도 내지 않는다. - -둘째와 셋째가 같은 설정 클래스 안에서 방금 선언된 빈의 존재를 조건으로 삼는다. - -스프링은 @ConditionalOnBean 을 자동 설정 클래스에서만, 그리고 등록 순서에 의존하는 방식으로만 신뢰할 수 있다고 문서화한다. - -지금은 첫 조건이 이미 거짓이라 결과가 드러나지 않는다. - -발신함을 배선하는 순간 이 사슬이 실제로 평가된다. - -같은 가족의 다른 결정과 대비된다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : JdbcOutboxRepository 참조 5건 검색과 여섯 빈의 @ConditionalOnBean 대상 타입 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-spring-boot-starter#L300 에 있다. - -## 본문 - - - -여섯 빈 전부가 애플리케이션이 공급해야 하는 타입에 걸려 있다. 조건 자체는 옳고 근거도 정확하다 — 플랫폼이 기본 구현을 주면 조용히 엉뚱한 곳에, 또는 아무 데도 쓰지 않게 된다. - -## 조건이 가리키는 대상 - -:::evidence key="messaging-spring-boot-starter-f02-diagram" alt="애플리케이션 공급 타입만 조건이 보는 것 안에 놓이고 같은 클래스에서 방금 선언된 빈이 바깥에 빗금으로 놓인다" caption="조건이 가리키는 대상" zoom="false" -::: - -문제는 저장소 안에 그 타입을 공급하는 코드가 없다는 것이다. `messaging-outbox-jdbc-postgresql` 의 `JdbcOutboxRepository` 는 스프링 스테레오타입도 `@Bean` 선언도 없고, `new JdbcOutboxRepository` 가 main 에 0 건이다. - -## JdbcOutboxRepository 참조 위치 - -:::evidence key="messaging-spring-boot-starter-f02" alt="코드베이스에서 JdbcOutboxRepository 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="JdbcOutboxRepository 코드베이스 검색 — 5줄 · exit 0" zoom="true" -::: - -## 그래서 발신함 경로만 조용히 빠진다 - -이 스타터를 켠 배포는 발행 경로는 얻고 발신함 경로는 얻지 못하며, 그 사실이 시작 시점에 어떤 신호도 내지 않는다. - -## 사슬이 자기 클래스 안을 가리킨다 - -둘째와 셋째가 같은 설정 클래스 안에서 방금 선언된 빈의 존재를 조건으로 삼는다. 스프링은 `@ConditionalOnBean` 을 자동 설정 클래스에서만, 그리고 등록 순서에 의존하는 방식으로만 신뢰할 수 있다고 문서화한다. 지금은 첫 조건이 이미 거짓이라 결과가 드러나지 않고, 발신함을 배선하는 순간 이 사슬이 실제로 평가된다. - -## 같은 가족의 다른 결정과 대비된다 - -관리 평면은 스위치가 켜졌을 때 만들어지지 **않는** 타입의 부재를 javadoc 에 명시한다(`DestructiveMessagingAdmin` 하나). 이쪽은 여섯이 조용히 빠진다. 수정은 둘이다 — 발신함을 요구하는 설정에서 저장소 빈이 없으면 시작을 거부하는 검증(`StartupProfileValidation` 형태), 그리고 중계·작업자·수명을 하나의 `@Bean` 으로 합치거나 조건을 전부 최초 두 타입으로 표현하는 것. - -## 확인하지 못한 것 - -애플리케이션이 저장소 빈을 공급한 상태로 컨텍스트를 세우지 않았다. 저장소에 그런 애플리케이션이 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-messaging-spring-boot-starter-f04.md b/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-messaging-spring-boot-starter-f04.md deleted file mode 100644 index b259eda..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-messaging-spring-boot-starter-f04.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -kind: CASE -slug: messaging-spring-boot-starter-f04 -title: 설정 경로의 재시도가 예외 분류를 표현할 수 없다 -topic: declaration-and-document-drift -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-spring-boot-starter-f04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-spring-boot-starter-f04 - file: ../../../final/evidence/rendered/messaging-spring-boot-starter-f04.svg -evidence: - - ../../../final/evidence/raw/messaging-spring-boot-starter-f04.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-spring-boot-starter#L342 이다. -module: messaging-spring-boot-starter -priority: P3 ---- - -# 설정 경로의 재시도가 예외 분류를 표현할 수 없다 - -RetryPolicy 는 성분 열이고 그중 둘이 분류 집합이다. DestinationSettings.Retry 에는 이 둘에 대응하는 키가 없고, 컴파일러가 상수로 채운다. - -## 관계 - -- **검증기는 발행이 아니라 주입이 강제다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 문제 - -RetryPolicy 는 성분 열이고 그중 둘이 분류 집합이다. - -DestinationSettings.Retry 에는 이 둘에 대응하는 키가 없고, 컴파일러가 상수로 채운다. - -## 결론 - -빈 집합은 "기본 분류 그대로" 라는 중립값이므로 오동작은 아니다. - -문제는 비대칭이다. - -DestinationProfile 을 자바로 선언한 배포는 두 집합을 조정할 수 있고, 문서대로 YAML 로 설정한 배포는 할 수 없다. - -이 리프가 반복해서 근거로 든 규칙이 정확히 그 비대칭을 금지한다. - -수정은 Retry 에 두 키를 더하는 것이다. - -FailureCategory 는 열거이므로 relaxed binding 이 그대로 처리한다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : RetryPolicy 참조 36건 검색과 설정 record 의 키 목록 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-spring-boot-starter#L342 에 있다. - -## 본문 - - - -`RetryPolicy` 는 성분 열이고 그중 둘이 분류 집합이다. `DestinationSettings.Retry` 에는 이 둘에 대응하는 키가 없고, 컴파일러가 상수로 채운다. - -## RetryPolicy 참조 위치 - -:::evidence key="messaging-spring-boot-starter-f04" alt="코드베이스에서 RetryPolicy 를 검색한 출력 36줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RetryPolicy 코드베이스 검색 — 36줄 · exit 0" zoom="true" -::: - -## 오동작은 아니다 - -빈 집합은 "기본 분류 그대로" 라는 중립값이다. - -## 문제는 비대칭이다 - -`DestinationProfile` 을 자바로 선언한 배포는 두 집합을 조정할 수 있고, 문서대로 YAML 로 설정한 배포는 할 수 없다. 이 리프가 반복해서 근거로 든 규칙이 정확히 그 비대칭을 금지한다. 수정은 `Retry` 에 두 키를 더하는 것이다 — `FailureCategory` 는 열거이므로 relaxed binding 이 그대로 처리한다. - -## 확인하지 못한 것 - -설정으로 분류를 지정해 무시되는 것을 실행으로 확인하지 않았다. 대응 키가 없어 컴파일러가 상수로 채운다는 것으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-messaging-testkit-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-messaging-testkit-f02.md deleted file mode 100644 index 799b94a..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-messaging-testkit-f02.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -kind: CASE -slug: messaging-testkit-f02 -title: 클래스 javadoc 이 강제되지 않는 규칙을 강제된다고 말한다 -topic: declaration-and-document-drift -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-testkit-f02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-testkit-f02 - file: ../../../final/evidence/rendered/messaging-testkit-f02.svg - - key: messaging-testkit-f02-diagram - file: ../../../final/assets/diagrams/messaging-testkit-f02.svg -evidence: - - ../../../final/evidence/raw/messaging-testkit-f02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-testkit#L950 이다. -module: messaging-testkit -priority: P2 ---- - -# 클래스 javadoc 이 강제되지 않는 규칙을 강제된다고 말한다 - -BrokerFailureMatrix.java:18-20 이 "A Stable adapter must cover every scenario. That rule is enforced by a test rather than documented" 라고 쓰고 있으나, isComplete 를 Stable 어댑터에 거는 테스트는 없다(EVD-300). - -## 문제 - -BrokerFailureMatrix.java:18-20 이 "A Stable adapter must cover every scenario. - -That rule is enforced by a test rather than documented" 라고 쓰고 있으나, isComplete 를 Stable 어댑터에 거는 테스트는 없다(EVD-300). - -## 결론 - -유일한 호출부는 Experimental 어댑터가 불완전함을 단언한다. - -실제 Stable 인 messaging-kafka 는 connection-refused gap 을 가진 채 통과하며, 그 gap 은 같은 모듈이 명시적으로 단언한다. - -코드 쪽 결정("gap 을 열거하되 비어 있음을 단언하지 않는다")은 옳고, 그 이유도 CrossBrokerContractSuite.java:45-47 에 적혀 있다. - -문제는 javadoc 이 갱신되지 않은 것이다. - -이 리프의 다른 javadoc 여섯 곳이 자기 이력을 정확히 남긴 것과 대비되어 더 눈에 띈다. - -같은 이유로 테스트 메서드 이름 everyStableAdapterCoversEveryFaultScenario 도 본문과 맞지 않는다. - -everyStableAdaptersGapsAreExactlyWhatTheEvidenceShows 같은 이름이 본문을 정확히 기술한다. - -수정 방향: javadoc 을 현재 규칙("Stable 은 live-broker 증거를 하나 이상 요구한다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : isComplete 호출부 1건 확인과 그 호출이 Stable 어댑터를 대상으로 하는지 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-testkit#L950 에 있다. - -## 본문 - - - -`BrokerFailureMatrix.java:18-20` 이 "A Stable adapter must cover every scenario. That rule is enforced by a test rather than documented" 라고 쓰고 있으나, `isComplete` 를 Stable 어댑터에 거는 테스트는 없다(`EVD-300`). - -## 강제가 없는 규칙 - -:::evidence key="messaging-testkit-f02-diagram" alt="클래스 javadoc 만 규칙을 표현하는 것 안에 놓이고 Stable 에 거는 테스트가 바깥에 빗금으로 놓인다" caption="강제가 없는 규칙" zoom="false" -::: - -유일한 호출부는 Experimental 어댑터가 불완전함을 단언한다. 실제 Stable 인 `messaging-kafka` 는 `connection-refused` gap 을 가진 채 통과하며, 그 gap 은 같은 모듈이 명시적으로 단언한다. - -## javadoc 이 강제된다고 말한 규칙 - -:::evidence key="messaging-testkit-f02" alt="분석 문서 final/document.md#a19-messaging-testkit 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a19-messaging-testkit 발췌 — 15줄" zoom="true" -::: - -## 코드 쪽 결정은 옳다 - -"gap 을 열거하되 비어 있음을 단언하지 않는다" 이고 그 이유도 `CrossBrokerContractSuite.java:45-47` 에 적혀 있다. 문제는 **javadoc 이 갱신되지 않은 것**이다. 이 리프의 다른 javadoc 여섯 곳이 자기 이력을 정확히 남긴 것과 대비되어 더 눈에 띈다. - -## 테스트 이름도 본문과 맞지 않는다 - -`everyStableAdapterCoversEveryFaultScenario` 대신 `everyStableAdaptersGapsAreExactlyWhatTheEvidenceShows` 같은 이름이 본문을 정확히 기술한다. 수정 방향은 javadoc 을 현재 규칙("Stable 은 live-broker 증거를 하나 이상 요구한다. 전 시나리오 커버리지는 목표이지 게이트가 아니며, gap 은 `knownGaps` 로 명명된다")으로 바꾸는 것이다. - -## 확인하지 못한 것 - -매니페스트를 손으로 고쳐 게이트가 실패하는 것은 확인하지 않았다. 그것은 애플리케이션 소스 수정에 해당해 하지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-messaging-testkit-f08.md b/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-messaging-testkit-f08.md deleted file mode 100644 index 78137f6..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-messaging-testkit-f08.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -kind: CASE -slug: messaging-testkit-f08 -title: gitCommit 은 기록되지만 읽혀 판정되지 않는다 -topic: declaration-and-document-drift -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-testkit-f08 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-testkit-f08 - file: ../../../final/evidence/rendered/messaging-testkit-f08.svg -evidence: - - ../../../final/evidence/raw/messaging-testkit-f08.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-testkit#L984 이다. -module: messaging-testkit -priority: P3 ---- - -# gitCommit 은 기록되지만 읽혀 판정되지 않는다 - -BrokerCertificationEvidence javadoc 이 "without them the evidence cannot be checked against anything later" 라고 쓰지만, 실제로 gitCommit 을 읽어 무언가를 결정하는 코드는 없고 게이트는 오히려 그 필드를 비교에서 제외한다(§12.4(c)). 현재 매니페스트의 커밋은 HEAD 가 아니다(e98b56eb vs 21234e38). - -## 문제 - -BrokerCertificationEvidence javadoc 이 "without them the evidence cannot be checked against anything later" 라고 쓰지만, 실제로 gitCommit 을 읽어 무언가를 결정하는 코드는 없고 게이트는 오히려 그 필드를 비교에서 제외한다(§12.4(c)). - -현재 매니페스트의 커밋은 HEAD 가 아니다(e98b56eb vs 21234e38). - -## 결론 - -"증거가 얼마나 오래된 트리에서 나왔는가" 를 보고하는 것은 유용한 진단이 될 수 있다 — 게이트로 만들 필요는 없고, knownGaps 처럼 사실로 노출하면 이 리프의 나머지 설계와 결이 맞는다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : BrokerCertificationEvidence 참조 26건 검색과 게이트가 비교에서 제외하는 필드 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-testkit#L984 에 있다. - -## 본문 - - - -`BrokerCertificationEvidence` javadoc 이 "without them the evidence cannot be checked against anything later" 라고 쓰지만, 실제로 `gitCommit` 을 읽어 무언가를 결정하는 코드는 없고 게이트는 오히려 그 필드를 비교에서 제외한다(§12.4(c)). - -## BrokerCertificationEvidence 참조 위치 - -:::evidence key="messaging-testkit-f08" alt="코드베이스에서 BrokerCertificationEvidence 를 검색한 출력 26줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="BrokerCertificationEvidence 코드베이스 검색 — 26줄 · exit 0" zoom="true" -::: - -## 현재 매니페스트의 커밋은 HEAD 가 아니다 - -`e98b56eb` vs `21234e38`. - -## 게이트로 만들 필요는 없다 - -"증거가 얼마나 오래된 트리에서 나왔는가" 를 보고하는 것은 유용한 진단이 될 수 있다. `knownGaps` 처럼 사실로 노출하면 이 리프의 나머지 설계와 결이 맞는다. - -## 확인하지 못한 것 - -게이트를 다른 커밋의 매니페스트로 돌려 보지 않았다. 비교 대상 필드 목록으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/question/openquestion-messaging-inbox-jdbc-postgresql-f04.md b/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/question/openquestion-messaging-inbox-jdbc-postgresql-f04.md deleted file mode 100644 index 79b257a..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/question/openquestion-messaging-inbox-jdbc-postgresql-f04.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -kind: QUESTION -slug: messaging-inbox-jdbc-postgresql-f04 -title: 세 갈래 판정이 포트의 boolean에서 두 갈래로 접힌다 -topic: declaration-and-document-drift -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: open-question:messaging-inbox-jdbc-postgresql-f04 -questionStatus: OPEN -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-inbox-jdbc-postgresql#L675 ---- - -# 세 갈래 판정이 포트의 boolean에서 두 갈래로 접힌다 - -같은 `false` 가 "이미 처리됐다" 와 "다른 인스턴스가 처리 중이다" 를 함께 뜻한다. 후자가 실제로 도달 가능한 상태인지에 따라 이것이 결함인지 과설계인지가 갈린다. - -## 관계 - -- **컬럼 폭은 애플리케이션 검증과 짝을 이룬다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 안전 규칙은 한 공식과 한 강제 시점을 갖는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 사실 - -InboxResult 가 세 값과 isSafeToSettle() 을 갖는데 production 은 APPLIED 만 만든다. - -InboxRepository.reserve 가 boolean 을 반환하므로 ALREADY_APPLIED 와 CLAIMED_ELSEWHERE 가 같은 false 로 들어온다. TransactionalInboxHandler 는 그 경우 HandleResult.success() 를 반환한다 — 정산한다. - -InboxResult javadoc 이 세 값이 필요한 이유로 정확히 그 정산을 든다 — "would settle a message whose effect is still only half-written by another instance". - -## 미지수 - -ON CONFLICT DO NOTHING 이 미커밋 충돌에 대해 대기하는가 즉시 0을 반환하는가. 대기하면 CLAIMED_ELSEWHERE 는 도달 불가능한 상태이고 enum 이 과설계인 것이며, 즉시 0을 반환하면 이것은 실제 결함이다. - -## 선택지 - -포트 반환 타입을 InboxResult 로 바꾼다 - 세 갈래가 호출자까지 도달하고 정산 판단이 갈린다. 확인 결과 발생 가능할 때의 선택이다. - -현 형태를 유지하고 도달 불가임을 적는다 - 대기가 확인되면 enum 의 세 번째 값이 왜 남아 있는지가 기록돼야 한다. - -## 다음 검증 - -두 커넥션에서 같은 (message, consumer) 를 예약하고 한쪽을 커밋하지 않은 채 다른 쪽의 executeUpdate() 반환을 관측한다. InboxPostgresIT 에 추가 가능하다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/reference/reference-messaging-cloudevents-f05.md b/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/reference/reference-messaging-cloudevents-f05.md deleted file mode 100644 index 8200e40..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/reference/reference-messaging-cloudevents-f05.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-cloudevents-f05 -title: 문서가 범위를 제한하면 그 제한을 누가 강제하는지 같이 적는다 -topic: declaration-and-document-drift -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-cloudevents-f05 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-cloudevents#L556 ---- - -# 문서가 범위를 제한하면 그 제한을 누가 강제하는지 같이 적는다 - -## 관계 - -- **배포 아티팩트가 싣지만 아무도 부르지 않는다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **상호운용을 위한 매퍼가 명세 준수 이벤트를 분류되지 않은 예외로 거절한다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. - -## 목적 - -javadoc 이 적용 범위를 좁혀 놓고 코드에 그 분기가 없으면, 제한은 읽는 사람의 기억에만 존재한다. 지금은 소비자가 0 이라 무해하고, 배선되는 순간 범위 밖 봉투가 그대로 통과한다. - -## 규칙 - -1. 범위를 제한하는 문장을 찾으면 그 옆에 강제 주체를 적는다 - CloudEventMapper 는 "Offered for domain and integration events only. Commands and work items are not forced through CloudEvents." 라고 적는다. - -2. 코드에 그 분기가 있는지 확인한다 - DefaultCloudEventMapper 전문에 DestinationKind 를 보는 분기가 없다. - -3. 강제하지 않기로 했다면 호출자 책임임을 명시한다 - 강제할 것이면 toCloudEvent 가 DestinationKind 를 받아 검사한다. - -## 적용 조건 - -javadoc·README·계약 문서가 적용 대상을 열거하는 모든 자리. - -## 예외 - -호출 지점이 하나뿐이고 그 호출자가 범위를 이미 좁히는 경우는 예외다. 이 매퍼는 호출자가 0 이라 그 예외에 해당하지 않는다. - -## 예시 - -CloudEventMapper.java:11-13 의 범위 진술과, git grep -n 'DestinationKind' -- 'src/messaging/messaging-cloudevents/**' 가 매치를 돌려주지 않는다는 것. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/reference/reference-messaging-observability-f06.md b/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/reference/reference-messaging-observability-f06.md deleted file mode 100644 index ee7d50a..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/reference/reference-messaging-observability-f06.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-observability-f06 -title: 타입이 문서화한 불변식은 타입이 강제한다 -topic: declaration-and-document-drift -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-observability-f06 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-observability#L717 ---- - -# 타입이 문서화한 불변식은 타입이 강제한다 - -## 관계 - -- **`extract`가 손상된 추적 헤더에 분류되지 않은 예외를 던진다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **관측 구현이 조립되지 않고, 그 재료 둘만 bean으로 존재한다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **감사 sink 인터페이스가 사용처에서 다시 선언된다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **자격증명 판정이 core-api보다 약하다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **태그 어휘가 존재하고 유일한 호출부가 우회해, 실패 분류가 기록되지 않는다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. - -## 목적 - -javadoc 이 "the details are passed through MessagingRedactor" 라고 적는데 생성자는 Map.copyOf 만 한다. 감사 기록은 "often retained far longer than the source topic" 이고 운영자가 읽는다. redaction 이 호출자 책임이면 새 호출부가 그것을 잊는 순간 민감한 값이 가장 오래 남는 곳에 들어간다. - -## 규칙 - -1. javadoc 이 서술하는 불변식을 생성자 본문과 대조한다 - MessagingAuditEvent.java:30-38 이 그 대조 지점이다. - -2. 강제하지 않으면 문장을 호출자 책임으로 고친다 - 둘 중 하나만 참일 수 있다. - -3. 같은 가족의 강제 사례를 기준으로 삼는다 - messaging-core-api 의 FailureDescriptor 는 512자 절단을 생성자에서 한다. - -## 적용 조건 - -javadoc 이 값의 형태·상한·정제 여부를 단정하는 모든 record·값 객체. - -## 예외 - -호출 지점이 전부 한 파일 안에 있고 그 파일이 불변식을 지키는 것을 테스트가 붙드는 경우는 예외로 볼 수 있다. 여기서는 RedriveService:126 과 ReplayService:73 이 redactor 를 부르는지부터 확인해야 한다. - -## 예시 - -MessagingAuditEvent.java:30-38 의 생성자 본문과 그 javadoc 문장. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/reference/reference-messaging-policy-f06.md b/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/reference/reference-messaging-policy-f06.md deleted file mode 100644 index 8925af7..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/reference/reference-messaging-policy-f06.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-policy-f06 -title: 구성 오류는 한 예외 타입과 안정 코드로 보고한다 -topic: declaration-and-document-drift -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-policy-f06 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-policy#L826 ---- - -# 구성 오류는 한 예외 타입과 안정 코드로 보고한다 - -## 관계 - -- **재시도 엔진과 DLQ 조정자가 bean으로 만들어지고 주입되는 곳이 없다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **재시도와 DLQ 각각에 두 개의 구현이 있고 정본이 표시되지 않았다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **DLQ 메타데이터의 두 시각이 항상 같다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. - -## 목적 - -부팅 실패라서 실무 영향은 낮지만, FailureDescriptor 가 없으면 그 거절에 코드도 카테고리도 붙지 않는다. 같은 leaf 안에서 구성 오류가 두 방식으로 보고되면 운영자가 받는 신호가 규칙마다 달라진다. - -## 규칙 - -1. 한 leaf 안의 구성 거절이 같은 예외 타입을 쓰는지 센다 - DestinationProfileValidator 의 거절 16개가 전부 IllegalArgumentException 이다. 같은 leaf 의 DeadLetterOrchestrator 는 MessagingConfigurationException("DEAD_LETTER_NOT_CONFIGURED") 을 쓴다. - -2. 플랫폼 예외가 이미 있는지 확인한다 - MessagingConfigurationException 의 javadoc 이 "Raised at startup wherever possible" 이라고 적는다. 자리를 비워 둔 것이 아니라 이미 그 용도로 선언돼 있다. - -3. 규칙마다 안정 코드를 준다 - 16개 규칙이 하나의 예외 타입을 공유하더라도 코드가 갈라져야 어느 규칙이 걸렸는지 읽힌다. - -## 적용 조건 - -시작 시점에 설정을 거절하는 검증기가 여러 개 있고 그것들이 한 leaf 를 공유하는 자리. - -## 예외 - -SSOT 가 이 규칙의 반례를 적지 않았다. 표준 예외를 유지할 근거가 있다면 그것이 javadoc 에 있어야 하는데, 16개 거절 어디에도 없다. - -## 예시 - -DestinationProfileValidator 전문의 throw 문과 MessagingConfigurationException 의 javadoc. 확인 방법은 두 클래스의 throw 문을 대조하는 것이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/reference/reference-messaging-policy-f07.md b/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/reference/reference-messaging-policy-f07.md deleted file mode 100644 index 27ceda4..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/reference/reference-messaging-policy-f07.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-policy-f07 -title: 저장소 밖 문서를 절 번호로 인용하지 않는다 -topic: declaration-and-document-drift -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-policy-f07 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-policy#L835 ---- - -# 저장소 밖 문서를 절 번호로 인용하지 않는다 - -## 관계 - -- **재시도 엔진과 DLQ 조정자가 bean으로 만들어지고 주입되는 곳이 없다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **재시도와 DLQ 각각에 두 개의 구현이 있고 정본이 표시되지 않았다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **DLQ 메타데이터의 두 시각이 항상 같다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. - -## 목적 - -인용된 내용이 코드와 일치해도, 근거를 확인하려는 사람이 그 문서에 도달할 수 없으면 인용은 검증 불가능한 권위가 된다. - -## 규칙 - -1. javadoc 의 문서 인용이 저장소 안에서 해소되는지 확인한다 - InFlightLimiter javadoc 이 "Section 40.3 of the design specifies 'bounded wait, then MessageBackpressureException'" 이라고 적는다. 그 절 번호를 가진 문서를 저장소에서 찾지 못했다. - -2. 해소되지 않으면 절 번호를 빼고 인용문만 남긴다 - 인용문 자체는 코드와 일치하므로 내용 drift 가 아니다. 문제는 좌표다. - -3. 실제 문서가 있으면 그 경로로 바꾼다 - 경로는 검색으로 확인 가능하고 절 번호는 아니다. - -## 적용 조건 - -javadoc·주석·README 가 외부 설계 문서를 좌표로 인용하는 모든 자리. - -## 예외 - -저장소에 함께 커밋된 문서의 절 번호는 이 규칙의 대상이 아니다. 그 좌표는 같은 리비전 안에서 해소된다. - -## 예시 - -InFlightLimiter.java:11-13 의 인용문과, docs/messaging/*.md 10개 및 계획 문서에 절 40.3 이 없다는 것. 확인 방법은 git grep -n '40\.3' -- docs 다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/reference/reference-messaging-reliability-api-f06.md b/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/reference/reference-messaging-reliability-api-f06.md deleted file mode 100644 index 82de4cc..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/reference/reference-messaging-reliability-api-f06.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-reliability-api-f06 -title: 호출 컨텍스트가 계약이면 그 컨텍스트를 검증할 수단을 함께 정한다 -topic: declaration-and-document-drift -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-reliability-api-f06 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-reliability-api#L734 ---- - -# 호출 컨텍스트가 계약이면 그 컨텍스트를 검증할 수단을 함께 정한다 - -## 관계 - -- **inbox 보존 규칙이 문서로만 있다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **포트가 bounded/unbounded purge 두 오버로드를 나란히 노출하고, 호출자가 무제한 쪽을 고른다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **fencing token 경로가 실제 데이터베이스에 대해 실행되지 않는다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **한 인터페이스가 같은 전이의 두 세대를 갖고, 안전하지 않은 쪽에 `@Deprecated`가 없다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. - -## 목적 - -위반이 조용하다는 것이 이 계약들의 특징이다. InboxRepository.reserve 를 별도 트랜잭션에서 부르면 "exactly the gap the Inbox exists to close" 가 다시 열리는데, 그 순간 어떤 예외도 나지 않는다. - -## 규칙 - -1. javadoc 이 요구하는 호출 컨텍스트를 목록으로 만든다 - 셋이다. OutboxRepository.append 는 호출자 트랜잭션 안, InboxRepository.reserve 는 부작용과 같은 트랜잭션, TransactionalMessageAction 은 자기 트랜잭션을 시작하지 않을 것. - -2. 타입으로 표현된 부분과 문장으로만 남은 부분을 가른다 - ReliableMessagePublisher 는 void 반환으로 계약의 일부를 타입에 담았다 — "Handing back a PublishResult here would be a lie". 나머지 셋에는 그런 장치가 없다. - -3. 타입으로 못 담으면 검증 수단을 정한다 - 구현 leaf 가 트랜잭션 참여를 검증하는 테스트를 두거나, ArchUnit 으로 append 와 reserve 호출부의 트랜잭션 컨텍스트를 검사한다. - -## 적용 조건 - -포트 javadoc 이 호출자 쪽 조건을 요구하는 모든 인터페이스. 트랜잭션 참여·락 보유·스레드 소속이 대표적이다. - -## 예외 - -반환 타입이나 파라미터로 컨텍스트를 강제할 수 있으면 별도 검증이 필요 없다. ReliableMessagePublisher 의 void 가 그 경우다. - -## 예시 - -세 javadoc 의 요구 문장. 확인 방법은 그 셋과 구현의 @Transactional 배치를 대조하는 것이고, 그 대조는 구현 leaf 가 소유한다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/reference/reference-messaging-schema-api-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/reference/reference-messaging-schema-api-f02.md deleted file mode 100644 index 8c1c65e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/reference/reference-messaging-schema-api-f02.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-schema-api-f02 -title: port 계약은 동시성 요구를 적는다 -topic: declaration-and-document-drift -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-schema-api-f02 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-schema-api#L503 ---- - -# port 계약은 동시성 요구를 적는다 - -## 관계 - -- **포맷 독립 진화 규칙이 호출되지 않고, 그것이 막으려던 중복이 실제로 생겼다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. - -## 목적 - -MessageCodecRegistry 의 유일한 구현 RegisteredMessageCodecs 는 Map.copyOf 로 불변이라 안전하다. 그것은 구현의 성질이지 계약이 아니다. 외부 registry 를 감싸는 SchemaRegistry 구현은 브로커 소비자 스레드들에서 동시에 호출된다. - -## 규칙 - -1. port javadoc 에 동시성 문장이 있는지 센다 - SchemaRegistry 와 MessageCodecRegistry 에는 없다. BoundedByteSink 만 "not thread-safe" 를 명시한다. - -2. 현재 구현이 안전하다는 사실과 계약을 구분한다 - 구현이 하나뿐이라 안전한 것과 계약이 안전을 요구하는 것은 다르다. - -3. 요구를 문장으로 적는다 - port javadoc 에 "구현은 스레드 안전해야 한다" 를 명시한다. - -## 적용 조건 - -구현체가 다른 leaf 나 파생 프로젝트에서 만들어질 수 있는 모든 port. - -## 예외 - -호출이 단일 스레드에 갇혀 있음을 타입이 보장하는 경우는 대상이 아니다. BoundedByteSink 는 그 성질을 명시해 이 규칙을 이미 지킨 쪽이다. - -## 예시 - -세 타입의 javadoc 전문. 확인 방법은 그 셋을 나란히 읽는 것이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/reference/reference-messaging-schema-api-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/reference/reference-messaging-schema-api-f03.md deleted file mode 100644 index 6b2abd8..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/reference/reference-messaging-schema-api-f03.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-schema-api-f03 -title: 도달성 판정은 단어가 아니라 import로 확인한다 -topic: declaration-and-document-drift -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-schema-api-f03 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-schema-api#L512 ---- - -# 도달성 판정은 단어가 아니라 import로 확인한다 - -## 관계 - -- **포맷 독립 진화 규칙이 호출되지 않고, 그것이 막으려던 중복이 실제로 생겼다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. - -## 목적 - -이 저장소에서 실제로 오탐이 났다. 단어 검색이 SchemaRegistry 2건을 맞췄고 둘 다 다른 타입이었다. 사람이 같은 실수를 한다. - -## 규칙 - -1. 같은 단순 이름이 여러 패키지에 있는지 먼저 확인한다 - dev.caskeleton.messaging.schema.SchemaRegistry(이 leaf 의 port)와 com.networknt.schema.SchemaRegistry(JSON Schema 라이브러리)가 공존하고, 실제로 import 되는 것은 후자뿐이다. - -2. 판정은 import 문으로 한다 - git grep -n 'import .*\.SchemaRegistry;' -- src 가 그 판정을 준다. - -3. 이름 충돌 자체는 고치지 않아도 된다 - 충돌을 없애는 것보다 판정 방법을 고정하는 것이 싸다. - -## 적용 조건 - -타입 참조 수를 세어 도달성·미사용을 판정하는 모든 조사. - -## 예외 - -단순 이름이 저장소 안에서 유일하다고 확인된 경우에는 단어 검색으로 충분하다. 그 확인 자체가 이 규칙의 첫 단계다. - -## 예시 - -evidence/raw/272 §C 의 검색 결과와 두 패키지의 공존. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/reference/reference-messaging-spring-cloud-stream-bridge-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/reference/reference-messaging-spring-cloud-stream-bridge-f03.md deleted file mode 100644 index f3ec591..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/reference/reference-messaging-spring-cloud-stream-bridge-f03.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-spring-cloud-stream-bridge-f03 -title: 한 개념의 등록 상태를 두 객체가 나눠 갖지 않는다 -topic: declaration-and-document-drift -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-spring-cloud-stream-bridge-f03 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-spring-cloud-stream-bridge#L570 ---- - -# 한 개념의 등록 상태를 두 객체가 나눠 갖지 않는다 - -## 목적 - -한 바인딩에 대해 두 객체가 각자 등록을 갖고 서로를 모른다. bindConsumer 를 부르고 register 를 부르지 않으면 publisher 쪽은 바인딩이 있다고 보고하고 실제 전달은 NO_BRIDGED_HANDLER 로 실패한다. consumerBinding(dest) 는 그 불일치를 드러내지 않는다. - -## 규칙 - -1. 인터페이스가 선언한 등록 메서드를 누가 구현하는지 본다 - MessagingBindingBridge 가 bindPublisher 와 bindConsumer 둘을 선언한다. SpringCloudStreamPublisherBridge 가 둘 다 구현하고 inputBindings 맵에 기록한다. - -2. 같은 개념의 상태가 다른 객체에도 있는지 본다 - SpringCloudStreamConsumerBridge 는 이 인터페이스를 구현하지 않고 자기 handlers 와 destinations 맵에 기록한다. - -3. 상태를 한쪽으로 모으거나 인터페이스를 나눈다 - consumer bridge 가 MessagingBindingBridge 를 구현하고 publisher 가 bindConsumer 를 위임하거나, 인터페이스를 발행과 수신으로 나눈다. - -## 적용 조건 - -등록 API 가 인터페이스로 선언되고 그 인터페이스를 일부 구현체만 구현하는 자리. - -## 예외 - -SSOT 가 이 규칙의 반례를 적지 않았다. 두 상태가 의도적으로 독립이라면 조회 API 가 그 독립을 드러내야 하는데, consumerBinding(dest) 는 드러내지 않는다. - -## 예시 - -evidence/raw/296 §C. 확인 방법은 두 클래스의 필드와 인터페이스 구현을 보는 것이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/declared-contract-without-enforcement/case/case-an-order-contract-with-no-implementation.md b/docs/clean-architecture-backend-template/tech-log-studio/declared-contract-without-enforcement/case/case-an-order-contract-with-no-implementation.md deleted file mode 100644 index bab3d36..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/declared-contract-without-enforcement/case/case-an-order-contract-with-no-implementation.md +++ /dev/null @@ -1,136 +0,0 @@ ---- -kind: CASE -slug: an-order-contract-with-no-implementation -title: 순서 계약을 구현하는 것이 없고, 그것을 검증한다는 테스트는 enum 선언 순서만 본다 -topic: declared-contract-without-enforcement -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:an-order-contract-with-no-implementation -evidenceCapturedOn: 2026-09-04 -body: case-an-order-contract-with-no-implementation.body.md -assets: - - key: an-order-contract-with-no-implementation - file: ../../../final/evidence/rendered/an-order-contract-with-no-implementation.svg -evidence: - - ../../../final/evidence/raw/an-order-contract-with-no-implementation.txt -source: - - 원본 분석은 evidence/raw/280-transport-spi-lifecycle-unimplemented.txt 에 남은 덤프이고 별도의 분석 절은 없다. ---- - -# 순서 계약을 구현하는 것이 없고, 그것을 검증한다는 테스트는 enum 선언 순서만 본다 - -`MessagingLifecycle` 은 `ShutdownPhase` 여덟 상수로 종료 순서를 적어 두었는데 이것을 구현한다고 선언한 클래스가 저장소에 없다. 순서를 검증한다는 시험 다섯은 `values()` 가 돌려주는 선언 순서를 보고, 사본에서 `CLOSE_CONNECTIONS` 를 앞으로 옮기면 그중 셋이 깨진다. - -## 관계 - -- **8단계 종료 순서 계약과 실제 종료 경로** - 이 사례가 속한 구조다. 계약은 여덟 단계를 선언하고 실제 종료는 `SmartLifecycle` 을 구현한 두 클래스가 맡는다. -- **선언 순서를 단언하는 테스트는 그 순서를 읽는 코드가 있을 때만 게이트다** - 이 사례가 만든 규칙이다. 인덱스를 읽는 프로덕션 코드가 없으므로 재배열 말고는 아무것도 막지 못한다. -- **같은 안전 규칙은 하나의 공식과 하나의 강제 시점을 갖는다** - 그 규칙이 든 예시는 공개 상수와 비공개 복사본과 생성자 인자 셋이다. `DEFAULT_DRAIN_DEADLINE` 이라는 이름으로 다시 세면 `MessagingLifecycle:40` 과 `OutboxRelayWorker:35` 와 `DefaultMessagingRuntimeRegistry:28` 이 나온다. -- **드레인 마감 30초가 세 곳에서 독립적으로 결정된다** - 같은 상수를 먼저 센 기록이다. 여기서는 실제로 주입되는 값이 `MessagingSettings:281` 의 프로퍼티 기본값이라는 것까지 확인했다. - -## 문제 - -종료 순서를 열거형으로 적어 두면 순서가 한 파일에 모이고, 그 단계들을 인터페이스로 두면 구현한다고 선언한 클래스가 여덟 단계를 다 채워야 컴파일된다. - -이 리프가 그 두 가지를 함께 노린 형태라, 둘 중 어느 쪽이 실제로 성립하는지 확인했다. - -## 결론 - -인터페이스는 ShutdownPhase 여덟 상수와 start() · shutdown(Duration) · isRunning() 을 선언한다. 자바독 :8~:11 은 단계의 배열이 각 어댑터가 알아서 정할 몫이 아니라고 못 박는다. - -구현체가 없다. implements MessagingLifecycle 로 검색하면 0 건이고, 두 이름을 언급하는 파일은 설계 계획 문서와 인터페이스와 그 시험 셋뿐이다. 같은 검색식으로 센 implements MessagingTransport 가 6 건을 내므로 이 0 은 매치를 놓친 결과가 아니다. 그 여섯 중 넷이 어댑터이고 둘은 시험 클래스다. - -컴파일 강제도 생기지 않는다. 자바에서 메서드 구현을 요구받는 쪽은 인터페이스가 아니라 그것을 구현한다고 적은 클래스이기 때문이다. - -순서를 검증한다는 시험은 MessagingLifecycleTest 안의 다섯이다. 넷은 List.of(ShutdownPhase.values()) 에서 두 상수의 indexOf 를 비교하고, connectionsCloseLastOfAll:30 은 values() 배열의 마지막 원소가 CLOSE_CONNECTIONS 인지 본다. - -두 파일만 /tmp 로 복사해 CLOSE_CONNECTIONS 를 첫 상수로 옮기고 컴파일했더니 여섯 중 셋이 깨졌다. 열거형을 읽는 프로덕션 코드가 0 건이므로 이 재배열의 영향은 그 시험 파일 안에서 끝난다. - -이름이 DEFAULT_DRAIN_DEADLINE 인 30 초짜리 선언은 셋이다. 다만 배선된 경로가 쓰는 값은 그 셋이 아니라 MessagingSettings:281 의 프로퍼티 기본값이고, MessagingCoreAutoConfiguration 이 :192·:204·:243 에서 그것을 넘긴다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Gradle : 9.0.0 -확인 방식 : 인터페이스와 시험 파일 전문 게재, ShutdownPhase 상수 계수와 인터페이스가 요구하는 메서드 나열, 두 이름의 pathspec 없는 전수 검색, implements 검색을 대조군과 자기시험과 함께 셋으로 제시, DEFAULT_DRAIN_DEADLINE 선언과 사용처, 프로퍼티 기본값과 그것을 넘기는 자리, SmartLifecycle 구현 둘, 두 파일 사본을 /tmp 에서 컴파일해 상수 순서를 바꾸기 전후로 실행 -소스 수정 : x - -## 재현 조건 - -1. 인터페이스와 시험 파일을 각각 전문으로 싣는다. -2. 열거형 상수를 세고 인터페이스가 요구하는 메서드를 나열한다. -3. 경로 한정 없이 저장소 전체에 두 이름을 검색해 언급 파일을 뽑는다. -4. implements 검색을 세 가지로 건다 — 대상, 없는 이름, 대조 타입. -5. DEFAULT_DRAIN_DEADLINE 선언과 사용처를 모듈과 소스 세트를 남긴 채 나열한다. -6. 프로퍼티로 들어오는 드레인 마감과 그것을 받는 자리를 함께 찾는다. -7. 두 파일을 /tmp 로 복사해 그대로 한 번, 상수 순서를 바꿔 한 번 컴파일하고 시험을 돌린다. 끝나고 저장소 변경이 0 건인지 확인한다. - -## 본문 - - - -`MessagingLifecycle` 은 종료 순서를 타입으로 적어 둔 인터페이스다. `ShutdownPhase` 열거형이 여덟 단계를 선언하고(`:20`\~`:37`), 인터페이스 자신은 `start()` 와 `shutdown(Duration)` 과 `isRunning()` 을 요구한다. - -## ShutdownPhase 여덟 상수와 자바독이 적는 순서 이유 - -:::evidence key="an-order-contract-with-no-implementation" alt="저장소 루트에서 돌린 정적 검색과 /tmp 사본 실행의 출력 181줄. 먼저 MessagingLifecycle.java 67줄이 전문으로 실린다. 클래스 자바독은 ShutdownPhase 의 순서가 계약이지 구현 세부가 아니라고 적고, 열거형이 STOP_PUBLISH_ADMISSION 부터 CLOSE_CONNECTIONS 까지 여덟 상수를 각각의 설명과 함께 선언하며, 40번 줄에 30초짜리 DEFAULT_DRAIN_DEADLINE 이 있다. 상수 개수가 8 개로 세어지고, 인터페이스가 요구하는 메서드로 start 와 shutdown 과 isRunning 셋이 나온다. 이어서 MessagingLifecycleTest.java 59줄이 전문으로 실린다. 시험은 여섯이고, 넷은 List.of 로 만든 목록에서 두 상수의 indexOf 를 비교하며, connectionsCloseLastOfAll 은 values 배열의 마지막 원소가 CLOSE_CONNECTIONS 인지 보고, 여섯째는 상수가 30초인지 본다. 각 시험의 as 설명문도 함께 실린다. 다음으로 두 이름이 나오는 파일이 pathspec 없이 셋으로 나오는데 설계 계획 문서와 인터페이스와 그 시험이다. implements MessagingLifecycle 를 가진 파일은 0 개, 없는 이름으로 건 자기시험도 0 개, 대조로 센 implements MessagingTransport 는 6 개이고 그 여섯이 나열되는데 넷은 main 어댑터이고 둘은 시험 클래스다. DEFAULT_DRAIN_DEADLINE 이라는 이름의 선언은 셋인데 OutboxRelayWorker 35번 줄과 DefaultMessagingRuntimeRegistry 28번 줄과 MessagingLifecycle 40번 줄이고, 앞 둘은 각각 129번과 36번 줄에서 자기 상수를 읽는다. 그 아래에 프로퍼티로 들어오는 드레인 마감이 나오는데 MessagingSettings 281번 줄의 기본값 30초를 MessagingCoreAutoConfiguration 이 192번·204번·243번 줄에서 세 곳에 넘긴다. 오늘 종료를 담당하는 구현으로 MessagingShutdownLifecycle 과 MessagingOutboxRelayLifecycle 이 나오는데 둘 다 SmartLifecycle 을 구현한다. 마지막으로 레포 사본을 /tmp 에서 컴파일한 결과가 나온다. 그대로면 통과 6 실패 0 이고, CLOSE_CONNECTIONS 를 첫 상수로 옮기면 통과 3 실패 3 이 되며 깨지는 시험은 connectionsCloseLastOfAll 과 outboxLeasesAreReleasedBeforeClosing 과 producerConfirmsAreAwaitedBeforeTheConnectionGoesAway 다. 레포 변경은 0 건이다." caption="인터페이스 67줄과 시험 59줄 전문 · implements 0 과 자기시험 0 과 대조 6 및 그 main/test 구분 · 이름이 같은 상수 셋과 실제로 주입되는 프로퍼티 값 · SmartLifecycle 로 구현된 종료 둘 · 사본에서 상수를 재배열했을 때 깨지는 시험 셋 — 181줄 · exit 0" zoom="true" -::: - -여덟은 선언 순서대로 `STOP_PUBLISH_ADMISSION`, `STOP_NEW_HANDLERS`, `PAUSE_CONSUMERS`, `DRAIN_HANDLERS`, `FLUSH_SETTLEMENTS`, `AWAIT_PRODUCER_CONFIRMS`, `RELEASE_OUTBOX_LEASES`, `CLOSE_CONNECTIONS` 다. - -클래스 자바독 `:8`\~`:11` 은 이 순서가 계약이지 구현 세부가 아니라고 적는다. 정착이 전송되기 전에 연결을 닫으면 그 정착을 잃고, 드레인 뒤에 소비자를 멈추면 이미 종료 중인 런타임으로 새 배달이 들어온다는 것이다. 단계를 구현하는 것은 각 어댑터이고 순서는 어느 어댑터도 고르지 않는다고 적는다. - -## MessagingLifecycle 을 구현한다고 선언한 클래스가 0 개다 - -두 이름이 나오는 파일을 pathspec 없이 저장소 전체에서 찾으면 셋이다 — 설계 계획 문서 하나, 인터페이스 자신, 그 시험. - -`implements MessagingLifecycle` 을 가진 파일은 0 개다. 같은 검색식에 없는 이름을 넣어도 0 이 나오므로, 0 만으로는 검색이 매치를 놓친 경우와 구별되지 않는다. 그래서 같은 식으로 `implements MessagingTransport` 를 세면 6 개가 나온다. 검색식은 매치를 찾을 수 있는 상태다. - -그 여섯 중 넷은 `KafkaMessagingTransport:41` · `NatsJetStreamTransport:54` · `PulsarMessagingTransport:47` · `RabbitMessagingTransport:39` 어댑터이고, 나머지 둘은 `DefaultMessagePublisherTest:426` 과 `MessagingRuntimeRegistryTest:230` 의 시험 더블이다. - -`implements` 선언이 없으면 컴파일러가 여덟 단계 중 무엇도 요구하지 않는다. 전송 SPI 에서 강제가 걸리는 이유는 그것을 구현한다고 적은 파일이 여섯 있기 때문이고, 그 여섯 각각이 선언한 메서드를 다 채우지 않으면 빌드가 실패한다. - -## MessagingLifecycleTest 의 여섯 시험이 비교하는 값 - -시험은 여섯이고 그중 다섯이 순서를 다룬다. 넷은 `List.of(ShutdownPhase.values())` 로 목록을 만들어 두 상수의 `indexOf` 를 비교한다. 나머지 하나인 `connectionsCloseLastOfAll:30` 은 `ShutdownPhase.values()` 를 배열로 받아 `order[order.length - 1]` 이 `CLOSE_CONNECTIONS` 인지 본다. 여섯째 `theDefaultDrainDeadlineMatchesTheDesign:56` 은 상수가 30 초인지 본다. - -다섯이 한 번도 언급하지 않는 상수는 `STOP_NEW_HANDLERS` 하나다. - -각 시험의 `.as()` 설명문은 시스템 동작을 적는다. `handlersDrainBeforeTheirSettlementsAreFlushed:25` 는 핸들러가 끝나기 전에 플러시하면 그 핸들러가 만들 정착을 잃는다고 적고, `producerConfirmsAreAwaitedBeforeTheConnectionGoesAway:43` 은 닫은 뒤에 도착한 확인은 관측할 수 없어 발행이 미결로 남는다고 적는다. 그 시험을 통과시키는 조건은 두 상수의 선언 위치뿐이다. - -## 상수를 재배열하면 시험 셋이 깨지고 그 밖에는 아무것도 깨지지 않는다 - -인터페이스와 시험 두 파일을 `/tmp` 로 복사해 저장소를 건드리지 않고 컴파일했다. 사본 그대로는 여섯이 모두 통과한다. - -`CLOSE_CONNECTIONS` 를 첫 상수로 옮기고 다시 컴파일하니 셋이 깨졌다 — `connectionsCloseLastOfAll`, `outboxLeasesAreReleasedBeforeClosing`, `producerConfirmsAreAwaitedBeforeTheConnectionGoesAway`. 나머지 셋은 그대로 통과한다. - -`ShutdownPhase` 를 자기 파일 밖에서 읽는 프로덕션 코드가 없으므로, 이 재배열이 바꾸는 것은 이 시험 파일의 통과 여부뿐이다. - -## DEFAULT_DRAIN_DEADLINE 이라는 이름이 세 곳에 따로 선언된다 - -`MessagingLifecycle:40` 의 공개 상수, `OutboxRelayWorker:35` 의 `public static final`, `DefaultMessagingRuntimeRegistry:28` 의 `private static final` 이 각각 `Duration.ofSeconds(30)` 을 적는다. 뒤 둘은 앞의 것을 참조하지 않고 `:129` 와 `:36` 에서 자기 상수를 읽는다. - -다만 실제로 주입되는 값은 넷째 자리에 있다. `MessagingSettings:281` 의 `drainDeadline` 기본값이 30 초이고, `MessagingCoreAutoConfiguration` 이 `:192` 와 `:204` 와 `:243` 에서 그 값을 세 군데로 넘긴다. `DefaultMessagingRuntimeRegistry:28` 의 복사본은 무인자 생성자(`:36`)로만 닿는다. - -인터페이스의 상수를 읽는 자리는 `MessagingLifecycleTest:57` 하나다. - -## 오늘 종료를 담당하는 것은 다른 인터페이스다 - -`MessagingShutdownLifecycle:30` 과 `MessagingOutboxRelayLifecycle:23` 이 Spring 의 `SmartLifecycle` 을 구현한다. 여덟 단계가 아니라 그쪽이 실제 종료 순서를 정한다. - -이 기록은 그 둘이 여덟 중 무엇을 밟는지까지 세지 않았다. - -## 확인하지 못한 것 - -`SmartLifecycle` 을 구현한 `MessagingShutdownLifecycle` 과 `MessagingOutboxRelayLifecycle` 이 여덟 단계 중 몇을 실제로 밟는지 세지 않았다. 계약을 구현한다고 선언한 타입이 없다는 데까지 확인했다. - -재배열은 두 파일만 복사한 사본에서 돌렸다. 전체 빌드로 확인하지는 않았다. - -`STOP_NEW_HANDLERS` 만 시험에 나오지 않는 이유는 찾지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-inbound-graphql-c08.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-inbound-graphql-c08.md deleted file mode 100644 index ce79452..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-inbound-graphql-c08.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-graphql-c08 -title: 보고되는 프로파일이 규정하는 동작을 수행하는 코드가 없다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-graphql-c08 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-graphql-c08 - file: ../../../final/evidence/rendered/adapter-inbound-graphql-c08.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-graphql-c08.txt -source: - - 원본 분석 절은 final/document.md#a16#L612 이다. -module: adapter-inbound-graphql ---- - -# 보고되는 프로파일이 규정하는 동작을 수행하는 코드가 없다 - -`GraphQlPlatformConfigurationReport`가 `GraphQlHttpProfile.V1.name()`을 배포 상태의 일부로 보고하는데, 그 프로파일이 규정하는 전송 동작을 수행하는 코드가 미배선이다. - -## 본문 - - - -`GraphQlPlatformConfigurationReport`(§8.1)가 `GraphQlHttpProfile.V1.name()`을 배포 상태의 일부로 보고한다. `GraphQlHttpProfile`은 autoconf=2로 참조되지만, 그 프로파일이 규정하는 전송 동작(상태 매핑 · Accept 협상 · 응답 형태)을 수행하는 코드는 미배선이다(§19.1). - -## GraphQlPlatformConfigurationReport 참조 위치 - -:::evidence key="adapter-inbound-graphql-c08" alt="코드베이스에서 GraphQlPlatformConfigurationReport 를 검색한 출력 4줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GraphQlPlatformConfigurationReport 코드베이스 검색 — 4줄 · exit 0" zoom="true" -::: - -## 보고서를 발행할 엔드포인트도 등록되지 않는다 - -§8.1. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-cache-redis-c06.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-cache-redis-c06.md deleted file mode 100644 index 8bbe5d2..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-cache-redis-c06.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-cache-redis-c06 -title: 모호한 실행을 재시도 가능으로 표시할 수 없다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-cache-redis-c06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-cache-redis-c06 - file: ../../../final/evidence/rendered/adapter-outbound-cache-redis-c06.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-cache-redis-c06.txt -source: - - 원본 분석 절은 final/document.md#a10#L332 이다. -module: adapter-outbound-cache-redis ---- - -# 모호한 실행을 재시도 가능으로 표시할 수 없다 - -`RedisFailureMetadata`의 불변식 하나가 이 SDK의 재시도 규칙 전체다 — 모호한 실행은 재시도 가능일 수 없다. - -## 본문 - - - -`RedisFailureMetadata`는 "Low-cardinality, payload-free description"이고, 불변식 하나가 이 SDK의 재시도 규칙 전체다. - -```java -if (retryable && ambiguousExecution) { - throw new IllegalArgumentException("an ambiguous execution must never be marked retryable"); -} -``` - -## RedisFailureMetadata 참조 위치 - -:::evidence key="adapter-outbound-cache-redis-c06" alt="코드베이스에서 RedisFailureMetadata 를 검색한 출력 30줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RedisFailureMetadata 코드베이스 검색 — 30줄 · exit 0" zoom="true" -::: - -## 두 팩토리가 그 규칙을 실제 상황에 적용한다 - -`notSent(...)`는 `retryable = readOperation`으로 유도한다 — 서버에 닿지 않은 읽기는 재시도해도 안전하다. `storedDataCorruption(...)`은 **일부러 `notSent`가 아니고**, javadoc이 그 이유를 적는다 — 같은 바이트를 다시 디코딩하면 같은 실패가 나오므로 값이 틀린 것이지 시도가 틀린 것이 아니다. 그 팩토리는 실제로 쓰인다 — `JsonEnvelopeFraming:202`, `VersionedJsonCodec:103` 두 곳이 디코딩 실패에서 호출한다. - -## 메시지가 담지 않는 것 - -예외 계층은 12종이고 전부 `RedisOperationException`을 상속한다. 메시지는 reason + `command=` 계열 + `mode=` + `ambiguous=`만 조립하고, javadoc이 경계를 적는다 — "keys, fields, members, values, arguments, and authentication material never appear." - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-fileserver-c05.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-fileserver-c05.md deleted file mode 100644 index cbc9b2b..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-fileserver-c05.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-fileserver-c05 -title: 길이 프레이밍이 구분자를 없애고, 잘린 토큰의 충돌을 컴파일 시점에 잡는다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-fileserver-c05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-fileserver-c05 - file: ../../../final/evidence/rendered/adapter-outbound-fileserver-c05.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-fileserver-c05.txt -source: - - 원본 분석 절은 final/document.md#a08#L300 이다. -module: adapter-outbound-fileserver ---- - -# 길이 프레이밍이 구분자를 없애고, 잘린 토큰의 충돌을 컴파일 시점에 잡는다 - -다이제스트는 값마다 길이를 앞세워 경계를 고정하고, 그 다이제스트를 잘라 만든 route token은 컴파일 시점에 충돌 검사를 받는다. - -## 본문 - - - -`FilePublicationCanonicalDigests.digestOrderedValues`는 값 개수를 먼저 넣고, 값마다 **길이(4바이트) + 엄격 UTF-8 바이트**를 넣는다. 구분자를 쓰지 않으므로 값 안에 어떤 문자가 있어도 경계가 흐려지지 않는다. `FilePublishRequestFingerprint`도 같은 방식이다. - -## FilePublicationCanonicalDigests 참조 위치 - -:::evidence key="adapter-outbound-fileserver-c05" alt="코드베이스에서 FilePublicationCanonicalDigests 를 검색한 출력 6줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="FilePublicationCanonicalDigests 코드베이스 검색 — 6줄 · exit 0" zoom="true" -::: - -## 잘린 토큰이 만들 수 있는 유일한 문제 - -`routeToken`은 정책 다이제스트의 앞 31자에 `r`을 붙인 것이라 **잘린 값**이다. 그래서 `FileserverBindingCompiler.deriveUniqueRouteTokens`가 컴파일 시점에 토큰 충돌을 검사하고, 충돌하면 두 destination 이름을 모두 담아 거부한다. 잘림이 만들 수 있는 유일한 문제를 그 자리에서 닫는다. - -## 값이 아니라 관계를 다시 계산해 대조한다 - -컴파일 후에도 `compiled.forEach`로 각 destination의 토큰이 레지스트리와 같은지 다시 확인한다. `CompiledFileDestination`의 compact 생성자는 넘겨받은 `effectivePolicyDigest`를 **다시 계산해 대조**하고, `routeToken`이 그 다이제스트에서 유도됐는지, `formatPolicyDigest`가 정본과 같은지도 확인한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-fileserver-c07.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-fileserver-c07.md deleted file mode 100644 index 83014d8..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-fileserver-c07.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-fileserver-c07 -title: 인식하지 못한 실패는 변경 연산이면 ambiguous로 떨어진다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-fileserver-c07 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-fileserver-c07 - file: ../../../final/evidence/rendered/adapter-outbound-fileserver-c07.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-fileserver-c07.txt -source: - - 원본 분석 절은 final/document.md#a08#L517 이다. -module: adapter-outbound-fileserver ---- - -# 인식하지 못한 실패는 변경 연산이면 ambiguous로 떨어진다 - -`AmbiguousFilesystemOperationDetector`의 기본값이 보수적이라, 메시지 텍스트 매칭이 빗나가도 안전한 방향으로 떨어진다. - -## 본문 - - - -`AmbiguousFilesystemOperationDetector`는 `IOException`을 네 결과로 나눈다(`NOT_SENT` / `DEFINITELY_REJECTED` / `AMBIGUOUS_COMPLETION` / `RECONCILIATION_REQUIRED`). 기본값이 보수적이다 — 인식하지 못한 실패는 **변경 연산이면 ambiguous**다. - -## AmbiguousFilesystemOperationDetector 참조 위치 - -:::evidence key="adapter-outbound-fileserver-c07" alt="코드베이스에서 AmbiguousFilesystemOperationDetector 를 검색한 출력 7줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="AmbiguousFilesystemOperationDetector 코드베이스 검색 — 7줄 · exit 0" zoom="true" -::: - -## 두 오분류의 값이 다르다 - -javadoc이 비대칭을 적는다: "the cost of a wrong 'safe to retry' is a corrupted object, while the cost of a wrong 'ambiguous' is one reconciliation entry." `mutating` 인자로 순수 읽기는 결코 ambiguous가 되지 않게 하고, stale handle은 변경 연산일 때 `RECONCILIATION_REQUIRED`로 격상한다 — 에러만으로는 결과를 알 수 없으므로 물리 증거를 다시 읽어야 한다. - -## 분류가 메시지 문구에 걸려 있다 - -`isStaleHandle`·`isLostResponse`와 `FilesystemFailureClassifier.isOutOfSpace`가 **메시지 텍스트 매칭**에 의존한다("stale file handle", "estale", "timed out", "No space left on device", "Disk quota exceeded"). 후자에는 주석이 붙어 있다 — "The JDK has no dedicated exception for this, so the reason text is the only available signal." 로케일이나 JDK 판본에 따라 문구가 달라지면 분류가 기본값으로 떨어지는데, 기본값이 보수적(변경 연산 → ambiguous)이므로 안전한 방향이다. 기록만 한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-messaging-c04.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-messaging-c04.md deleted file mode 100644 index f23b305..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-messaging-c04.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-messaging-c04 -title: 브로커가 받아들인 발행을 로거 실패가 실패로 만들지 못한다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-messaging-c04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-messaging-c04 - file: ../../../final/evidence/rendered/adapter-outbound-messaging-c04.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-messaging-c04.txt -source: - - 원본 분석 절은 final/document.md#a12#L284 이다. -module: adapter-outbound-messaging ---- - -# 브로커가 받아들인 발행을 로거 실패가 실패로 만들지 못한다 - -두 발행 포트의 실패 정책이 정반대이고, 발행과 관측이 분리된 이유가 수정 이력으로 남아 있다. - -## 본문 - - - -두 발행 포트의 실패 정책이 정반대다. - -| 포트 | 정책 | 근거 | -|---|---|---| -| `MessagePublisher` → `OutboundMessagePublisher` | **fail-open** | "a broker outage must never turn a core use case into a 5xx (durable delivery is delegated to the outbox/retry path)" | -| `OutboxMessagePublishPort` → `OutboxMessagePublishAdapter` | **fail-closed** | 실패가 그대로 전파되어 relay가 FAILED/DEAD 전이를 몰 수 있게 한다 | - -## OutboundMessagePublisher 참조 위치 - -:::evidence key="adapter-outbound-messaging-c04" alt="코드베이스에서 OutboundMessagePublisher 를 검색한 출력 13줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="OutboundMessagePublisher 코드베이스 검색 — 13줄 · exit 0" zoom="true" -::: - -## 같은 try 블록을 쓰던 시절 - -`OutboundMessagePublisher.publish`에 이 저장소에서 반복해 본 종류의 수정 이력이 있다. - -> "The send and the observation are separate steps because they used to share a try block: **a logger that threw after a successful send was caught by the same catch and reported as a publish failure.** The broker had accepted the message; the only thing that failed was the record of it, and the two must not be confusable." - -## 진단은 권위를 갖지 않는다 - -`observeQuietly`가 진단 예외를 흡수하며 "Diagnostics are non-authoritative. **An appender that is out of disk must not change what the caller believes about the broker.**" 비활성 sentinel 둘은 조용한 no-op이 아니라 `AdapterDisabledException`을 던지고, 서로 다른 클래스로 분리된 이유가 bean 조회 모호성이다(§2). - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-notification-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-notification-c03.md deleted file mode 100644 index e8e71b6..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-notification-c03.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-notification-c03 -title: provider가 요청한 지연은 계산값보다 길 때만 채택되고 max에서 잘린다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-notification-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-notification-c03 - file: ../../../final/evidence/rendered/adapter-outbound-notification-c03.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-notification-c03.txt -source: - - 원본 분석 절은 final/document.md#a13#L582 이다. -module: adapter-outbound-notification ---- - -# provider가 요청한 지연은 계산값보다 길 때만 채택되고 max에서 잘린다 - -`Retry-After` 힌트가 종단까지 도달하는 것을 확인했고, 그 힌트가 배달 기한을 늘릴 수 있는 경로는 없다. - -## 본문 - - - -`ProviderResults.retryAfter`가 파싱한 값이 종단까지 도달하는지 추적했다. 도달한다 — `NotificationDispatchService.java:381`이 `ProviderFailure::retryAfter`로 넘기고, `RetryBackoff.delay`(`:43-45`)가 소비한다. - -```java -Duration computed = Duration.ofMillis(Math.max(jittered, base.toMillis())); -Duration chosen = retryAfter.filter(hint -> hint.compareTo(computed) > 0).orElse(computed); -return chosen.compareTo(max) > 0 ? max : chosen; -``` - -힌트는 계산값보다 **길 때만** 채택되고, 그 뒤 설정된 `max`(기본 5분)로 **상한이 걸린다**. - -## ProviderResults 참조 위치 - -:::evidence key="adapter-outbound-notification-c03" alt="코드베이스에서 ProviderResults 를 검색한 출력 25줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ProviderResults 코드베이스 검색 — 25줄 · exit 0" zoom="true" -::: - -## javadoc의 주장이 코드와 일치한다 - -"A provider-supplied `Retry-After` always wins over the computed value, but never over the configured maximum: a provider asking for an hour must not silently extend a delivery deadline." 악의적 provider가 큰 `Retry-After` 값으로 배달을 수십 년 뒤로 미루는 경로는 **없다**. §17.1의 `AccessContext`와 대조되는, 회로가 닫힌 사례다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-persistence-jpa-c04.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-persistence-jpa-c04.md deleted file mode 100644 index 0b9d119..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-persistence-jpa-c04.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c04 -title: offset을 표현할 수 없는 타입과 count 질의 없는 hasNext -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c04 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c04.svg - - key: adapter-outbound-persistence-jpa-c04-diagram - file: ../../../final/assets/diagrams/adapter-outbound-persistence-jpa-c04.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c04.txt -source: - - 원본 분석 절은 final/document.md#a05#L302 이다. -module: adapter-outbound-persistence-jpa ---- - -# offset을 표현할 수 없는 타입과 count 질의 없는 hasNext - -질의 API가 offset과 page number를 타입으로 표현하지 않고, `fetchSize()`가 size + 1을 반환해 별도 count 질의 없이 `hasNext`를 정한다. - -## 본문 - - - -offset/page number를 아예 표현하지 않으므로 keyset API를 사용하는 consumer가 실수로 large offset pagination으로 회귀하기 어렵다. - -## 질의 API가 표현하지 않는 것 - -:::evidence key="adapter-outbound-persistence-jpa-c04-diagram" alt="정렬 키와 size 더하기 1과 서명된 커서가 질의 API 안에 놓이고 offset과 total count 질의가 바깥에 빗금으로 놓인다" caption="질의 API가 표현하지 않는 것" zoom="false" -::: - -## count 질의 없이 hasNext를 정한다 - -`fetchSize()`는 요청 size + 1을 반환한다. 즉 별도 count query 없이 한 row를 더 읽어 `hasNext`를 판단하는 계약이다. hasNext=true이면 nextCursor 필수, terminal slice이면 nextCursor 금지, items는 defensive copy. page number/total count가 없다는 것은 API omission이 아니라 의도된 성능 정책이다 — "keyset을 쓰면서 매번 count(*)도 수행"하는 모순을 contract shape에서 제거한다. - -## QueryName 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c04" alt="코드베이스에서 QueryName 를 검색한 출력 21줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="QueryName 코드베이스 검색 — 21줄 · exit 0" zoom="true" -::: - -## raw SQL이 metric identity가 될 수 없다 - -`QueryName`도 bounded registry key다. `QueryObservation.start(QueryName)` → `QueryScope` 구조에서 `QueryScope.failure` 문서가 "throwable message를 log하지 말 것"을 직접 계약한다. Micrometer implementation이 이를 실제로 지키는지는 observation sub-scope에서 확인한다. - -## backend가 없어도 흐름이 갈라지지 않는다 - -`NoopQueryObservation`은 backend가 없을 때도 caller control flow가 갈라지지 않게 singleton no-op scope를 제공한다. app-bootstrap `JpaObservabilityAutoConfiguration`에서 actual fallback consumer가 존재한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-persistence-jpa-c31.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-persistence-jpa-c31.md deleted file mode 100644 index abfac9b..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-persistence-jpa-c31.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c31 -title: grep refs=0은 finding의 시작점이지 결론이 아니다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c31 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c31 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c31.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c31.txt -source: - - 원본 분석 절은 final/document.md#a05#L2151 이다. -module: adapter-outbound-persistence-jpa ---- - -# grep refs=0은 finding의 시작점이지 결론이 아니다 - -production consumer가 확인되지 않은 열 개의 optimization helper를 곧바로 dead code로 읽으면 안 되는 이유를, 이 저장소의 기존 기록과 integration test가 갈라 준다. - -## 본문 - - - -negative-space search에서 다음 implementation roots는 repository production consumer가 확인되지 않았다 — `HibernateJpaBatchExecutor`, `JpaBatchProfileRegistry`, `HibernateBulkDmlExecutor`, `HibernateStatelessSessionRunner`, `FetchPlanApplier`, `JpaKeysetQuerySupport`, `JpaRepositoryFragmentSupport`, `JpaStreamExecutor`, `SpecificationPolicy`, `QuerydslJpaSupport`. - -## HibernateJpaBatchExecutor 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c31" alt="코드베이스에서 HibernateJpaBatchExecutor 를 검색한 출력 11줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="HibernateJpaBatchExecutor 코드베이스 검색 — 11줄 · exit 0" zoom="true" -::: - -## "dead code가 대량 존재한다"로 읽으면 안 되는 이유 - -이 repository의 기존 study/review 문서도 이미 JPA platform helper가 **구현/qualification되어 있지만 sample production path가 대부분 채택하지 않은 상태**라고 기록한다. 또한 batch/bulk/stateless helper는 real PostgreSQL integration tests에서 직접 실행된다. 따라서 현재 판단은 capability별로 나눈다 — 이들은 library capability로 유지할 수 있다. - -## 같은 refs=0이라도 성질이 다른 것 - -`NamedStatementInspector`처럼 global Hibernate hook이 필요한 기능은 "아무 use case가 안 쓴다"와 다르다 — feature를 사용하려면 composition이 먼저 존재해야 한다. transaction scope의 `TransactionProfileRegistry`처럼 history를 통해 실제 residue로 판정해야 하는 것도 있다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-persistence-jpa-c32.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-persistence-jpa-c32.md deleted file mode 100644 index 7d6d56c..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-persistence-jpa-c32.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c32 -title: 두 export 목록에 postgresql과 h2만큼의 차이가 있다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c32 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c32 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c32.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c32.txt -source: - - 원본 분석 절은 final/document.md#a05#L2217 이다. -module: adapter-outbound-persistence-jpa ---- - -# 두 export 목록에 postgresql과 h2만큼의 차이가 있다 - -app-bootstrap consumer rule이 leaf의 export 목록과 별개의 `EXPORTED` set을 다시 정의한다. - -## 본문 - - - -`CleanArchitectureTest.BOOTSTRAP_USES_ONLY_THE_PERSISTENCE_EXPORT_SURFACE`는 또 다른 `EXPORTED` set을 정의한다. 여기에는 root composition이 vendor entry point를 import해야 하므로 두 항목이 추가돼 있다. - -- postgresql -- h2 - -즉 두 목록은 이미 동일하지 않다. - -## 부트스트랩 쪽 EXPORTED 집합 - -:::evidence key="adapter-outbound-persistence-jpa-c32" alt="분석 문서 final/document.md#a05 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a05 발췌 — 15줄" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-persistence-jpa-c33.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-persistence-jpa-c33.md deleted file mode 100644 index 60ce2b7..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-persistence-jpa-c33.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c33 -title: leaf list 자체는 outside consumer를 검사하지 않는다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c33 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c33 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c33.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c33.txt -source: - - 원본 분석 절은 final/document.md#a05#L2230 이다. -module: adapter-outbound-persistence-jpa ---- - -# leaf list 자체는 outside consumer를 검사하지 않는다 - -leaf의 export test와 consumer restriction이 서로 다른 데이터를 읽으므로, 둘 다 통과한다는 것이 둘이 drift하지 않는다는 증명은 아니다. - -## 본문 - - - -`JpaModuleBoundaryTest`의 local export test는 export package가 실제 존재하는지, 새 top-level package가 governance 대상인지를 보지만 repository의 outside consumer import를 직접 스캔하지 않는다. 실제 consumer restriction은 app-bootstrap의 별도 ArchUnit rule이 담당한다. - -## JpaModuleBoundaryTest 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c33" alt="코드베이스에서 JpaModuleBoundaryTest 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="JpaModuleBoundaryTest 코드베이스 검색 — 2줄 · exit 0" zoom="true" -::: - -## 둘 다 통과한다는 것이 증명하지 않는 것 - -fresh architecture tests는 모두 통과했다. 이것은 현재 import graph가 각자의 rule을 만족한다는 뜻이지 **A와 B가 서로 drift하지 않는다는 증명은 아니다.** - -## 권장 방향 - -우선순위는 **P2/P3 architecture-governance hardening**이고, 권장 방향은 exported package registry를 한 곳으로 옮겨 leaf package DAG와 consumer ArchUnit rule이 같은 데이터를 읽게 하는 것이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-persistence-mongo-c07.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-persistence-mongo-c07.md deleted file mode 100644 index 0506dcd..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-adapter-outbound-persistence-mongo-c07.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-mongo-c07 -title: 투영 먼저 checkpoint 나중, 그리고 3-state claim -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-mongo-c07 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-mongo-c07 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-mongo-c07.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-mongo-c07.txt -source: - - 원본 분석 절은 final/document.md#a06#L1015 이다. -module: adapter-outbound-persistence-mongo ---- - -# 투영 먼저 checkpoint 나중, 그리고 3-state claim - -이 leaf에서 유일하게 조립까지 된 대형 서브시스템이고, 그 설계의 규칙 대부분이 과거 결함을 이름으로 적어 두고 있다. - -## 본문 - - - -앞선 sub-scope들과 다르다. `MongoPlatformAutoConfiguration`이 두 개의 bean을 실제로 만든다. - -- `mongoChangeStreamSource`(209행) — `SpringReactiveChangeStreamSource`, 무조건. -- `reactiveMongoChangeStreamConsumer`(235행) — fork만 공급할 수 있는 5종(`MongoChangeStreamSubscription`, `MongoResumeCheckpointStore`, `MongoResumeTokenCodec`, `MongoChangeProjector`, `MongoChangeDeduplicationStore`)에 `@ConditionalOnBean`. - -## MongoPlatformAutoConfiguration 참조 위치 - -:::evidence key="adapter-outbound-persistence-mongo-c07" alt="코드베이스에서 MongoPlatformAutoConfiguration 를 검색한 출력 14줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MongoPlatformAutoConfiguration 코드베이스 검색 — 14줄 · exit 0" zoom="true" -::: - -## fork가 다섯을 채우면 완성된 소비자가 돈다 - -pipeline·runner·recovery policy·invalidate recovery는 auto-configuration이 직접 `new`한다. 이 사실이 아래 §67의 심각도를 결정한다. - -## 순서가 계약이다 - -`MongoChangeStreamRunner`는 투영 먼저, checkpoint 나중이다 — "Checkpointing first would mean a crash between the two loses the event permanently, with no trace." 그래서 중복을 택하고 중복을 제거한다. - -## 읽고-쓰기가 동시성에서 살아남지 못했다 - -과거 `alreadyProjected` + `markProjected`는 동시에 `false`를 읽은 두 subscriber가 둘 다 투영했다 — "the deduplication that exists precisely because redelivery is guaranteed did not survive concurrency". 지금은 `CLAIMED`/`ALREADY_COMPLETED`/`BUSY`의 원자적 전이다. - -## 빈 완료는 프로토콜 위반이다 - -`Mono`이 empty로 완료되면 `flatMap`을 그냥 통과해 "투영도 checkpoint도 없이 아무도 문제를 보고하지 않는" 상태가 됐다. 이제 `switchIfEmpty(Mono.error(...))`로 잡는다. - -## 구분자를 0x1F로 고른 이유 - -identity는 SHA-256이고 구분자는 ASCII unit separator(0x1F)다 — namespace/clusterTime/operationType에 나타날 수 없으므로 필드 재배열로 다른 이벤트의 identity를 위조할 수 없다. 한 transaction이 같은 문서를 두 번 고치면 앞 네 필드가 모두 같아지므로 `txnNumber`+`lsid` discriminator를 추가로 넣는다 — 없으면 두 번째가 첫 번째의 재전달로 **버려진다**. - -## 로그에 찍히면 안 되는 것 - -`MongoResumeCheckpoint.toString()`은 길이만 보고한다. token은 clusterTime과 documentKey를 인코딩하므로 로그에 찍는 순간 production write의 모양과 타이밍이 샌다. - -## 기본 구현을 일부러 두지 않았다 - -`MongoResumeTokenCodec`에는 기본 구현이 없다 — "a built-in that merely encoded would be worse than none: it would satisfy the type and none of the reason for it." `HISTORY_LOST`는 자동 복구하지 않는다 — "resuming from now… the projection then looks healthy and is quietly wrong, which is worse than a stopped consumer somebody has to look at." `MongoClusterTime`은 숫자로 비교한다 — 텍스트 비교는 `1700000000.10`을 `1700000000.9`보다 앞에 놓는데, 그것은 바쁜 1초가 정확히 만드는 경우다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-application-core-c06.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-application-core-c06.md deleted file mode 100644 index 5b97b41..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-application-core-c06.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -kind: CONCEPT -slug: application-core-c06 -title: 계약에 Kafka topic도 WebSocket도 나오지 않는다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:application-core-c06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: application-core-c06 - file: ../../../final/evidence/rendered/application-core-c06.svg -evidence: - - ../../../final/evidence/raw/application-core-c06.txt -source: - - 원본 분석 절은 final/document.md#a03#L174 이다. -module: application-core ---- - -# 계약에 Kafka topic도 WebSocket도 나오지 않는다 - -messaging·realtime 계약이 semantic 정보만 담고 provider/transport 어휘를 밖으로 밀어내며, qualification은 evidence provenance property까지 요구한다. - -## 관계 - -- **legacy storage/notification compatibility surface의 제거 조건 추적** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -messaging application contract catalog는 contract id, logical destination, schema resource, ordering, payload/envelope bounds, sensitivity, retry/requeue horizon 등 semantic 정보만 가진다. Kafka topic/provider runtime type은 public contract에 없다. validated integration event는 partition key, schema/content hash, catalog/binding revision 같은 immutable evidence를 보존한다. - -## 이 기록이 다루는 범위 - -:::evidence key="application-core-c06" alt="코드베이스에서 파일 목록을 만든 출력 25줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 25줄 · exit 0" zoom="true" -::: - -## qualification이 테스트 이름만으로는 통과하지 않는다 - -strict `messagingApplicationContractQualificationTest`는 normal test source set의 세 required class를 no-skip 조건으로 실행한다. 처음 digest property 없이 실행했을 때 `prepareMessagingContractEvidence`가 fail-closed로 거부했다. current source/archive, current application-core JAR, exact profile file의 SHA-256을 공급한 재실행에서는 **15 tests, 0 skipped, BUILD SUCCESSFUL**이었다. 즉 qualification은 evidence provenance property까지 요구한다. - -## realtime 계약이 accepted와 delivered를 구분한다 - -durable fanout과 ephemeral fanout을 분리한다. durable은 accepted와 delivered를 동일시하지 않고 stream+position dedupe/replay를 모델링한다. stale cursor는 resnapshot 요구로 분리된다. presence는 non-authoritative이며 TTL/heartbeat failure 시 empty로 degrade할 뿐 security 판단에 사용하지 않는다. logical channel은 WebSocket/STOMP 같은 transport 명칭을 소유하지 않는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-application-core-c07.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-application-core-c07.md deleted file mode 100644 index 313a2ba..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-application-core-c07.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -kind: CONCEPT -slug: application-core-c07 -title: forRemoval이 붙었는데 production consumer가 남아 있다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:application-core-c07 -evidenceCapturedOn: 2026-09-01 -assets: - - key: application-core-c07 - file: ../../../final/evidence/rendered/application-core-c07.svg -evidence: - - ../../../final/evidence/raw/application-core-c07.txt -source: - - 원본 분석 절은 final/document.md#a03#L182 이다. -module: application-core ---- - -# forRemoval이 붙었는데 production consumer가 남아 있다 - -raw key/whole-byte legacy 계약과 provider-neutral semantic 계약이 같은 leaf에 공존하고, 제거 시점이 날짜가 아니라 실제 usage로 문서화돼 있다. - -## 관계 - -- **legacy storage/notification compatibility surface의 제거 조건 추적** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -`application.storage.ObjectStoragePort`는 raw object key/whole-byte 방식의 legacy contract이며 `forRemoval` 표시가 있지만 실제 production consumer가 남아 있다. sample poster upload, adapter/config, characterization test에서 사용되므로 dead code로 분류할 수 없다. - -## FilesystemCsvExportAdapter 참조 위치 - -:::evidence key="application-core-c07" alt="코드베이스에서 FilesystemCsvExportAdapter 를 검색한 출력 8줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="FilesystemCsvExportAdapter 코드베이스 검색 — 8줄 · exit 0" zoom="true" -::: - -## 제거 시점을 날짜로 적지 않았다 - -제거 시점은 날짜가 아니라 실제 migration/zero usage로 판단하도록 문서화돼 있다. `fileexport` 역시 raw filesystem path를 반환하는 opt-in legacy capability이며 `FilesystemCsvExportAdapter`/configuration을 통해 조건부 활성화된다. - -## receipt surface에 raw Path가 없는 쪽 - -반대로 `filepublication`은 logical destination, operation/reference/version, schema, row streaming/checkpoint, durability semantic을 provider-neutral 계약으로 만든다. CSV formula injection(`=`, `+`, `-`, `@`, tab, CR)을 reject하는 정책이 테스트로 고정돼 있고, raw Path/SFTP/fileserver 타입이 receipt surface에 나오지 않는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-application-core-c11.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-application-core-c11.md deleted file mode 100644 index 39956c0..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-application-core-c11.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -kind: CONCEPT -slug: application-core-c11 -title: adapter까지 있고 호출자가 없는 교정 경로 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:application-core-c11 -evidenceCapturedOn: 2026-09-01 -assets: - - key: application-core-c11 - file: ../../../final/evidence/rendered/application-core-c11.svg -evidence: - - ../../../final/evidence/raw/application-core-c11.txt -source: - - 원본 분석 절은 final/document.md#a03#L270 이다. -module: application-core ---- - -# adapter까지 있고 호출자가 없는 교정 경로 - -package-level reachability는 모두 확인됐고 legacy surface도 dead가 아니지만, notification admin의 원자적 `claim()`은 구현까지 있고 application service consumer가 없다. - -## 관계 - -- **legacy storage/notification compatibility surface의 제거 조건 추적** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -static production reference scan에서 주요 application package는 모두 외부 production consumer를 확인했다. 이 count는 "모든 type이 각각 호출된다"는 의미가 아니라 package-level runtime/repository reachability의 evidence다. 세부 파일은 `evidence/raw/013-application-core-reachability.txt`에 보존했다. - -## NotificationPort 참조 위치 - -:::evidence key="application-core-c11" alt="코드베이스에서 NotificationPort 를 검색한 출력 17줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="NotificationPort 코드베이스 검색 — 17줄 · exit 0" zoom="true" -::: - -## legacy surface를 무조건 dead로 분류하지 않았다 - -`application.storage.ObjectStoragePort`, root notification `NotificationPort`, `NotificationVariablesCodecPort`, old idempotency-related exception 등은 adapter/config/characterization path에서 실제 reference가 남아 있다. 현재 상태는 dead code가 아니라 migration/compatibility surface다. - -## 이번 scope에서 가장 중요한 reachability finding - -반대로 notification admin atomic `claim()`은 adapter 구현까지 존재하지만 application service consumer가 없는 **unwired corrective path**로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-domain-core-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-domain-core-c02.md deleted file mode 100644 index 2166727..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-domain-core-c02.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -kind: CONCEPT -slug: domain-core-c02 -title: bean은 없고 컴파일 시점 참조만 있다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:domain-core-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: domain-core-c02 - file: ../../../final/evidence/rendered/domain-core-c02.svg -evidence: - - ../../../final/evidence/raw/domain-core-c02.txt -source: - - 원본 분석 절은 final/document.md#a01#L146 이다. -module: domain-core ---- - -# bean은 없고 컴파일 시점 참조만 있다 - -`domain-core`는 Spring bean도 entry point도 없지만 두 runtime composition에 membership이 있고, 주요 public abstraction이 각각 실제 소비자를 갖는다. - -## 본문 - - - -`domain-core` 자체에는 Spring bean/configuration/entry point가 없다. Registry상 `app-bootstrap`, `sample-portfolio` 두 runtime composition에 membership이 있고, concrete consumers가 compile-time type/annotation으로 이 module을 참조한다. - -## ResourceId 참조 위치 - -:::evidence key="domain-core-c02" alt="코드베이스에서 ResourceId 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ResourceId 코드베이스 검색 — 2줄 · exit 0" zoom="true" -::: - -## 어떤 추상을 누가 참조하나 - -- `ResourceId` — application-core messaging contract 및 sample IDs -- `IdFactory` — sample factory/use-case/identifier adapter -- `AggregateRoot` — sample aggregate -- `DomainEvent` — sample events와 websocket broadcaster qualification -- `ValueObject` — sample IDs/value objects - -## 두 방향 모두 근거가 없다 - -major public abstraction이 완전히 dead/unwired인 상태는 아니다. 반대로 `domain-core`가 runtime service를 직접 수행한다는 근거도 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-grpc-admin-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-grpc-admin-c01.md deleted file mode 100644 index 4ffd05b..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-grpc-admin-c01.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -kind: CONCEPT -slug: grpc-admin-c01 -title: 건강 레지스트리 — 낙관에서 시작하지 않는다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:grpc-admin-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-admin-c01 - file: ../../../final/evidence/rendered/grpc-admin-c01.svg - - key: grpc-admin-c01-diagram - file: ../../../final/assets/diagrams/grpc-admin-c01.svg -evidence: - - ../../../final/evidence/raw/grpc-admin-c01.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-admin#L50 이다. -module: grpc-admin ---- - -# 건강 레지스트리 — 낙관에서 시작하지 않는다 - -모든 등록 서비스가 UNKNOWN 에서 시작한다. 그리고 배수 중에는 markServing·markNotServing 이 무시된다. - -## 본문 - - - -모든 등록 서비스가 `UNKNOWN` 에서 시작한다. - -> "A registry that starts optimistic reports ready during startup, receives traffic before the first dependency check has run, and fails the requests that arrive in that window — the window being exactly the moment a rollout is shifting traffic onto the instance." - -## 배수 중에는 표시가 무시된다 - -`markServing`·`markNotServing` 이 무시된다. - -> "a service that reports itself healthy after the drain has started would be routed traffic the instance has already promised not to take." - -## 전역 상태 판정 우선순위 - -:::evidence key="grpc-admin-c01-diagram" alt="임계 의존 불건강과 하나라도 NOT_SERVING 과 하나라도 SERVING 과 그 밖이 위에서 아래로 쌓여 있고 오른쪽에 판정 순서 화살표가 있다" caption="전역 상태 판정 우선순위" zoom="false" -::: - -임계 의존이 하나라도 불건강하면 `NOT_SERVING`, 아니면 하나라도 `NOT_SERVING` 이면 `NOT_SERVING`, 하나라도 `SERVING` 이면 `SERVING`, 그 밖에는 `UNKNOWN` 이다. - -## 이 기록이 다루는 범위 - -:::evidence key="grpc-admin-c01" alt="코드베이스에서 파일 목록을 만든 출력 13줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 13줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-grpc-admin-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-grpc-admin-c02.md deleted file mode 100644 index 27f98e4..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-grpc-admin-c02.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -kind: CONCEPT -slug: grpc-admin-c02 -title: 배수 순서 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:grpc-admin-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-admin-c02 - file: ../../../final/evidence/rendered/grpc-admin-c02.svg -evidence: - - ../../../final/evidence/raw/grpc-admin-c02.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-admin#L65 이다. -module: grpc-admin ---- - -# 배수 순서 - -beginDrain 이 앞의 둘을 한 번에 수행하고, 그 전에 rejectNewAdmission 을 부르면 던진다. 조정자는 잠들지 않는다. - -## 본문 - - - -배수 순서가 여섯 단계로 고정돼 있다. - -```text -READINESS_FALSE → HEALTH_DRAINING → REJECT_NEW_ADMISSION → DRAIN_UNARY → SIGNAL_STREAMS → FORCE_CANCEL -``` - -## beginDrain 이 앞의 둘을 한 번에 한다 - -그 전에 `rejectNewAdmission` 을 부르면 던진다. - -> "refusing calls before readiness has flipped produces errors for traffic that routing is still sending" - -## 이 기록이 다루는 범위 - -:::evidence key="grpc-admin-c02" alt="코드베이스에서 파일 목록을 만든 출력 13줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 13줄 · exit 0" zoom="true" -::: - -## 조정자는 잠들지 않는다 - -> "It is given the current moment and the counts, and returns whether the phase is done; the waiting belongs to the caller, which is what makes every branch of this testable without a clock." - -## 예산은 누적이다 - -스트림 신호 완료 판정이 `unaryDrainBudget + streamSignalBudget` 을 기준으로 한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-grpc-advanced-resilience-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-grpc-advanced-resilience-c02.md deleted file mode 100644 index d994691..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-grpc-advanced-resilience-c02.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -kind: CONCEPT -slug: grpc-advanced-resilience-c02 -title: xDS 시작 가드 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:grpc-advanced-resilience-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-advanced-resilience-c02 - file: ../../../final/evidence/rendered/grpc-advanced-resilience-c02.svg -evidence: - - ../../../final/evidence/raw/grpc-advanced-resilience-c02.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-advanced-resilience#L75 이다. -module: grpc-advanced-resilience ---- - -# xDS 시작 가드 - -두 거절이 있고 javadoc 이 둘째를 더 중요하다고 적는다. 시작 차단 사유는 둘 — 능력이 사용 가능하지 않음, 그리고 애플리케이션이 재시도 정책을 함께 정의함. - -## 본문 - - - -두 거절이 있고 javadoc 이 둘째를 더 중요하다고 적는다. - -> "xDS working in a deployment is not the same claim as the platform supporting it: it brings a control plane, its outage modes, its own security boundary and its own version skew, and the Stable support statement covers DNS and static targets. A support matrix that quietly widens is a support matrix nobody can rely on." - -## 시작을 막는 두 사유 - -능력이 사용 가능하지 않음, 그리고 애플리케이션이 재시도 정책을 함께 정의함이다. - -> "with xDS the control plane owns it, and defining it in both places makes the winner depend on resolution order" - -## 이 기록이 다루는 범위 - -:::evidence key="grpc-advanced-resilience-c02" alt="코드베이스에서 파일 목록을 만든 출력 16줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 16줄 · exit 0" zoom="true" -::: - -## 부트스트랩 대조가 보는 세 가지 - -`xds_servers` 선언, 통제 평면 채널의 TLS, 프로파일의 자원 이름공간이다. - -> "a client whose bootstrap names a namespace the deployment did not configure subscribes successfully and receives another team's routing. Nothing errors — the control plane answers, the resources parse, and traffic goes somewhere nobody chose." - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-admin-api-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-admin-api-c01.md deleted file mode 100644 index 50f0635..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-admin-api-c01.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-admin-api-c01 -title: 권한을 불리언이 아니라 타입으로 만든다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-admin-api-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-admin-api-c01 - file: ../../../final/evidence/rendered/messaging-admin-api-c01.svg - - key: messaging-admin-api-c01-diagram - file: ../../../final/assets/diagrams/messaging-admin-api-c01.svg -evidence: - - ../../../final/evidence/raw/messaging-admin-api-c01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-admin-api#L57 이다. -module: messaging-admin-api ---- - -# 권한을 불리언이 아니라 타입으로 만든다 - -되돌릴 수 없는 작업을 사람의 승인에 묶는 타입 집합이다. 실행 코드는 하나도 없다 — 브로커를 만지는 것도, 메시지를 옮기는 것도 전부 messaging-admin-runtime 과 어댑터가 한다. - -## 본문 - - - -**되돌릴 수 없는 작업을 사람의 승인에 묶는 타입 집합**이다. 실행 코드는 하나도 없다 — 브로커를 만지는 것도, 메시지를 옮기는 것도 전부 `messaging-admin-runtime` 과 어댑터가 한다. - -## 이 리프가 소유한 어휘 - -:::evidence key="messaging-admin-api-c01-diagram" alt="계획 타입과 승인 타입과 검증 타입이 messaging-admin-api 안에 놓이고 브로커 접촉과 저장소 스프링이 바깥에 빗금으로 놓인다" caption="이 리프가 소유한 어휘" zoom="false" -::: - -이 리프가 정의하는 것은 "무엇이 승인이고, 승인이 무엇을 인가하며, 인가되지 않은 것이 왜 컴파일되지 않는가" 다. 설계의 축은 하나다 — **권한을 불리언이 아니라 타입으로 만든다.** - -## ReplayPlan 참조 위치 - -:::evidence key="messaging-admin-api-c01" alt="코드베이스에서 ReplayPlan 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ReplayPlan 코드베이스 검색 — 5줄 · exit 0" zoom="true" -::: - -## 같은 기법이 한 층 더 쌓인다 - -`ReplayPlan` → `ApprovedReplayPlan` → 실행. 각 화살표가 타입 경계이고, 각 경계에서 검사가 **생성자 안에** 있어 우회 경로가 없다. - -## 이 리프가 모르는 것 - -브로커를 모른다 — `DestinationTopology` 는 브로커가 보고한 값을 담는 record 일 뿐 조회하지 않는다. 저장소를 모른다 — `AdminOperationJournal` 은 인터페이스다. 스프링도 모른다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-admin-runtime-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-admin-runtime-c01.md deleted file mode 100644 index b7d3bcc..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-admin-runtime-c01.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-admin-runtime-c01 -title: 부품은 정교하고 부품을 잇는 층은 실행된 적이 없다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-admin-runtime-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-admin-runtime-c01 - file: ../../../final/evidence/rendered/messaging-admin-runtime-c01.svg - - key: messaging-admin-runtime-c01-diagram - file: ../../../final/assets/diagrams/messaging-admin-runtime-c01.svg -evidence: - - ../../../final/evidence/raw/messaging-admin-runtime-c01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-admin-runtime#L62 이다. -module: messaging-admin-runtime ---- - -# 부품은 정교하고 부품을 잇는 층은 실행된 적이 없다 - -messaging-admin-api 가 정의한 타입들을 실제로 실행하는 계층이다. 계획을 세우고, 저널에 자리를 잡고, 옮기고, 결과를 보고한다. - -## 본문 - - - -`messaging-admin-api` 가 정의한 타입들을 **실제로 실행하는 계층**이다. 계획을 세우고, 저널에 자리를 잡고, 옮기고, 결과를 보고한다. - -## 실행 계층이 만지는 것 - -:::evidence key="messaging-admin-runtime-c01-diagram" alt="오케스트레이션과 실행 서비스와 토폴로지 저널이 messaging-admin-runtime 안에 놓이고 브로커 클라이언트와 스프링 배선이 바깥에 빗금으로 놓인다" caption="실행 계층이 만지는 것" zoom="false" -::: - -1. **오케스트레이션** — `MessagingAdminService` / `DefaultMessagingAdminService`. 계획·승인·저널·실행을 잇는다. -2. **실행** — `ReplayService`, `RedriveService`. 각각 하나의 작업을 수행하며, 브로커 접촉은 SPI(`ReplayExecutor`, `RedriveSource`, `RedrivePublisher`)로 밀어낸다. -3. **토폴로지·저널** — `CompositeTopologyValidator`+`TopologyValidator`, `TopologyValidationRuntime`, `InMemoryAdminOperationJournal`. - -## MessagingAdminService 참조 위치 - -:::evidence key="messaging-admin-runtime-c01" alt="코드베이스에서 MessagingAdminService 를 검색한 출력 4줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingAdminService 코드베이스 검색 — 4줄 · exit 0" zoom="true" -::: - -## 경계 밖 - -브로커 클라이언트가 없다. Kafka·Rabbit 어느 것도 import 하지 않고, 모든 브로커 접촉이 함수형 인터페이스 뒤에 있다. Spring 도 없다 — 배선은 전부 starter 몫이다. - -## 읽고 나서 남는 인상이 갈린다 - -**개별 부품은 대단히 정교하다** — 저널의 펜싱 프로토콜, 리드라이브 루프의 per-item 경계, 토폴로지 severity 판정은 각각 실패 사례를 겪고 나온 코드로 보이며 그 근거가 주석에 있다. 반면 **부품을 잇는 층은 실행된 적이 없다** — §12.1 에서 보듯 `DefaultMessagingAdminService` 는 프로덕션에서도 테스트에서도 인스턴스화되지 않는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-admin-runtime-c04.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-admin-runtime-c04.md deleted file mode 100644 index a276c65..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-admin-runtime-c04.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-admin-runtime-c04 -title: 파괴적 작업에는 실행 경로가 없다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-admin-runtime-c04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-admin-runtime-c04 - file: ../../../final/evidence/rendered/messaging-admin-runtime-c04.svg -evidence: - - ../../../final/evidence/raw/messaging-admin-runtime-c04.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-admin-runtime#L440 이다. -module: messaging-admin-runtime ---- - -# 파괴적 작업에는 실행 경로가 없다 - -경로 A — 리드라이브 (설계상 의도된 흐름) 경로 B — 토폴로지 검증 Stack A 는 validateTopology() 로 진입해 보고서를 돌려준다. 그 보고서로 requireAcceptable() 을 부르는 코드는 없다. - -## 본문 - - - -**경로 A — 리드라이브** 가 설계상 의도된 흐름이다. - -## 경로 B — 토폴로지 검증 - -Stack A 는 `validateTopology()` 로 진입해 보고서를 돌려준다. 그 보고서로 `requireAcceptable()` 을 부르는 코드는 없다. Stack B 는 `validate(...)` 안에서 직접 던진다. 둘 다 프로덕션 진입점이 없다(`EVD-307`). - -## DestructiveMessagingAdmin 참조 위치 - -:::evidence key="messaging-admin-runtime-c04" alt="코드베이스에서 DestructiveMessagingAdmin 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DestructiveMessagingAdmin 코드베이스 검색 — 3줄 · exit 0" zoom="true" -::: - -## 경로 C — 파괴적 작업 - -없다. `DestructiveMessagingAdmin` 구현체가 0건이므로 `PURGE`·`OFFSET_RESET`·`DELETE_DESTINATION` 은 이 저장소에 실행 경로가 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-cloudevents-c04.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-cloudevents-c04.md deleted file mode 100644 index 934a419..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-cloudevents-c04.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-cloudevents-c04 -title: 왕복 검증이 producedAt 을 비교하지 않는다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-cloudevents-c04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-cloudevents-c04 - file: ../../../final/evidence/rendered/messaging-cloudevents-c04.svg - - key: messaging-cloudevents-c04-diagram - file: ../../../final/assets/diagrams/messaging-cloudevents-c04.svg -evidence: - - ../../../final/evidence/raw/messaging-cloudevents-c04.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-cloudevents#L113 이다. -module: messaging-cloudevents ---- - -# 왕복 검증이 producedAt 을 비교하지 않는다 - -봉투 → CloudEvent CloudEvent → 봉투 두 번째는 messaging-core-api의 MessageEnvelope javadoc과 정확히 짝을 이룬다 — "A null Kafka value is a tombstone, which is a distinct broker-native operation with different retention semantics." 봉투가 payload를 non-null로 강제한 이유가 여기서 실제 매핑 규칙으로 나타난다. ProducerId가 "deployment-independent service name, not a host, pod, or connection identity, so that it stays a bounded value safe for metric tags"라고 선언한 것과 같은 관심사다. - -## 관계 - -- **왕복이라 부르는 테스트는 무엇을 보존하지 않는지도 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **문서가 범위를 제한하면 그 제한을 누가 강제하는지 같이 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -매핑 방향이 둘이고, 두 번째는 `messaging-core-api`의 `MessageEnvelope` javadoc과 정확히 짝을 이룬다 — "A null Kafka value is a tombstone, which is a distinct broker-native operation with different retention semantics." 봉투가 payload를 non-null로 강제한 이유가 여기서 실제 매핑 규칙으로 나타난다. - -## producerFrom 의 방어가 완전하지 않다 - -`ProducerId`가 "deployment-independent service name, not a host, pod, or connection identity, so that it stays a bounded value safe for metric tags"라고 선언한 것과 같은 관심사다. **다만 이 방어는 완전하지 않다** — 마지막 세그먼트가 여전히 120 UTF-8 바이트를 넘거나 제어문자를 담을 수 있고, 그 경우 `ProducerId` 생성자가 `IllegalArgumentException`을 던진다(§4.5). `urn:service:order-api` → `order-api`(테스트가 쓰는 형태), `https://a.example/very/long/path/x` → `x`. - -## MessageEnvelope 참조 위치 - -:::evidence key="messaging-cloudevents-c04" alt="코드베이스에서 MessageEnvelope 를 검색한 출력 18줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessageEnvelope 코드베이스 검색 — 18줄 · exit 0" zoom="true" -::: - -## 봉투가 왕복에서 잃는 것 - -:::evidence key="messaging-cloudevents-c04-diagram" alt="원래 봉투에 occurredAt 과 producedAt 이 있고 왕복 후 봉투에서 producedAt 만 빗금으로 놓인다" caption="봉투가 왕복에서 잃는 것" zoom="false" -::: - -CloudEvents에는 `time` 하나뿐이므로 봉투의 두 시각(플랫폼이 봉투를 만든 때 / 사실이 일어난 때)을 구분할 수 없다. 같은 값을 넣는 것은 합리적 선택이지만 **정보 손실이 기록되지 않았다** — 왕복 후 `producedAt`은 원래 값이 아니다. - -## 테스트가 비교하지 않는 필드 - -왕복 검증(`roundTripsBackToAnEnvelopeWithoutInventingATombstone`)이 `messageId`·`messageType`·`schemaVersion`·`correlationId`·`tenantContext`·`payload`만 비교하고 `producedAt`은 비교하지 않는다. fixture에서 `producedAt`은 `09:15:01Z`, `occurredAt`은 `09:15:00Z`로 **일부러 다르게** 설정돼 있으므로, 비교했다면 실패했을 것이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-core-api-c04.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-core-api-c04.md deleted file mode 100644 index 6ed3b8f..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-core-api-c04.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-core-api-c04 -title: 거절과 결과 모름을 하나로 합치면 중복 주문이 생긴다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-core-api-c04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-core-api-c04 - file: ../../../final/evidence/rendered/messaging-core-api-c04.svg - - key: messaging-core-api-c04-diagram - file: ../../../final/assets/diagrams/messaging-core-api-c04.svg -evidence: - - ../../../final/evidence/raw/messaging-core-api-c04.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-core-api#L180 이다. -module: messaging-core-api ---- - -# 거절과 결과 모름을 하나로 합치면 중복 주문이 생긴다 - -이 leaf의 실질은 여기 있다. 표현할 수 없는 상태를 생성자에서 거절하는 것이 설계의 축이다. - -## 본문 - - - -이 leaf의 실질은 여기 있다. **표현할 수 없는 상태를 생성자에서 거절하는 것**이 설계의 축이다. - -## 발행 결과 세 상태 - -:::evidence key="messaging-core-api-c04-diagram" alt="발행 시도에서 CONFIRMED 와 REJECTED 와 AMBIGUOUS 세 갈래가 나온다" caption="발행 결과 세 상태" zoom="false" -::: - -`PublishCompletion`은 boolean이 아니라 3상태다. enum javadoc이 왜 셋인지 적는다 — "Collapsing 'the broker refused this' and 'we never learned what the broker did' into one failure is what produces duplicate orders"(`publish/PublishCompletion.java:6-8`). `AMBIGUOUS`인 호출자는 **같은 `messageId`로만** 재발행할 수 있다. - -## PublishCompletion 참조 위치 - -:::evidence key="messaging-core-api-c04" alt="코드베이스에서 PublishCompletion 를 검색한 출력 29줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="PublishCompletion 코드베이스 검색 — 29줄 · exit 0" zoom="true" -::: - -## 생성자가 거절하는 열두 조합 - -`PublishResult` 생성자(`publish/PublishResult.java:39-101`)가 12가지를 거절한다. 11번과 14번에는 코드 주석이 직접 달려 있다. record가 public이고 모든 adapter가 이것을 만들기 때문에 호출부를 믿지 않고 여기서 검증한다는 것도 javadoc에 적혀 있다(`PublishResult.java:18-20`). - -## 증거가 결론보다 먼저 기록된다 - -`PublishEvidence`(`publish/PublishEvidence.java`)는 `queuedLocally`, `transmission`, `brokerAccepted`, `confirmationLevel` 넷을 갖고, javadoc이 순서를 못 박는다 — "Evidence is recorded before a completion is chosen, not derived from it. That ordering is what lets an operator answer 'could the broker be holding this message?' from a stored result." `TransmissionEvidence`가 3상태(`NOT_TRANSMITTED` / `MAY_HAVE_BEEN_TRANSMITTED` / `TRANSMITTED`)인 것이 그 순서를 가능하게 한다. - -## 정산 쪽도 같은 형태다 - -`SettlementResult`(`settlement/SettlementResult.java:23-36`)는 `SETTLED`인데 `!brokerConfirmed`이면 거절하고, `SETTLED`인데 `redeliveryPossible`이면 거절한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-core-api-c06.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-core-api-c06.md deleted file mode 100644 index 38ec0c4..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-core-api-c06.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-core-api-c06 -title: 예외 클래스로 분기하지 않아도 되게 만든 기반 타입 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-core-api-c06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-core-api-c06 - file: ../../../final/evidence/rendered/messaging-core-api-c06.svg -evidence: - - ../../../final/evidence/raw/messaging-core-api-c06.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-core-api#L419 이다. -module: messaging-core-api ---- - -# 예외 클래스로 분기하지 않아도 되게 만든 기반 타입 - -MessagingException(abstract) → 23개 구체 예외. 기반 타입이 FailureDescriptor를 갖고 category()·retryable()를 위임한다. - -## 본문 - - - -`MessagingException`(abstract) → 23개 구체 예외. 기반 타입이 `FailureDescriptor`를 갖고 `category()`·`retryable()`를 위임한다. javadoc이 목적을 적는다 — "a caller catching the base type can still classify and route the failure without matching on exception classes." - -## MessagingException 참조 위치 - -:::evidence key="messaging-core-api-c06" alt="코드베이스에서 MessagingException 를 검색한 출력 36줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingException 코드베이스 검색 — 36줄 · exit 0" zoom="true" -::: - -## 23개 중 12개가 leaf 밖에서 참조되지 않는다 - -`evidence/raw/269` §B — 12개 전부 `git grep` exit=1. §12.1에서 다룬다. - -## 조용한 강등을 막는 문장들 - -`MessagingCapabilityUnavailableException` javadoc — "Downgrading replication evidence to a bare ack, or ordered delivery to unordered, produces a system that looks healthy right up to the moment the guarantee actually mattered." `MessageBackpressureException` javadoc — "Blocking the caller until a slot frees turns producer-side saturation into thread exhaustion in the calling application, which is a far worse failure than a fast rejection." 그리고 "Nothing was transmitted when this is thrown, so the message has no ambiguity." - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-kafka-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-kafka-c01.md deleted file mode 100644 index 7029f79..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-kafka-c01.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-kafka-c01 -title: 소비자 런타임 — 스레드 규율이 설계다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-kafka-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-kafka-c01 - file: ../../../final/evidence/rendered/messaging-kafka-c01.svg - - key: messaging-kafka-c01-diagram - file: ../../../final/assets/diagrams/messaging-kafka-c01.svg -evidence: - - ../../../final/evidence/raw/messaging-kafka-c01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-kafka#L69 이다. -module: messaging-kafka ---- - -# 소비자 런타임 — 스레드 규율이 설계다 - -공개 API 인 pause/resume 도 제어 큐를 통해 폴 스레드로 넘어가고, 반환된 단계는 다음 폴 주기 에 완료된다. close() 만 예외이고 그 예외에 근거가 붙어 있다 — 이후 폴 루프가 멈추므로 큐에 넣으면 영원히 배수되지 않는다. - -## 본문 - - - -공개 API 인 `pause`/`resume` 도 제어 큐를 통해 폴 스레드로 넘어가고, 반환된 단계는 **다음 폴 주기** 에 완료된다. - -## 폴 스레드가 소유한 것 - -:::evidence key="messaging-kafka-c01-diagram" alt="consumer 객체와 제어 큐 배수가 폴 스레드 안에 놓이고 작업자 람다가 바깥에 빗금으로 놓인다" caption="폴 스레드가 소유한 것" zoom="false" -::: - -## close 만 예외인 이유 - -이후 폴 루프가 멈추므로 큐에 넣으면 영원히 배수되지 않는다. - -## 이 기록이 다루는 범위 - -:::evidence key="messaging-kafka-c01" alt="코드베이스에서 파일 목록을 만든 출력 25줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 25줄 · exit 0" zoom="true" -::: - -## 규율이 실제로 지켜진다 - -작업자 람다가 만지는 것은 `settlements`·`coordinator`·`shutdown`·`retries` 뿐이고 `consumer` 는 한 번도 없다. 통독으로 확인했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-kafka-share-experimental-c04.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-kafka-share-experimental-c04.md deleted file mode 100644 index 6296ee9..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-kafka-share-experimental-c04.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-kafka-share-experimental-c04 -title: 등록이 통과한 뒤에 아무 일도 일어나지 않는다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-kafka-share-experimental-c04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-kafka-share-experimental-c04 - file: ../../../final/evidence/rendered/messaging-kafka-share-experimental-c04.svg -evidence: - - ../../../final/evidence/raw/messaging-kafka-share-experimental-c04.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-kafka-share-experimental#L234 이다. -module: messaging-kafka-share-experimental ---- - -# 등록이 통과한 뒤에 아무 일도 일어나지 않는다 - -등록: registrar.register(profile, spec) → validator.validate(profile) → 통과하면 ShareRegistration(profile) 반환 → 이후 아무 일도 일어나지 않는다 pause: registration.pause(scope) → 즉시 실패 stage 이 leaf에 메시지가 흐르는 경로가 없다. - -## 관계 - -- **허용 의존 목록은 상한이므로 미사용을 잡지 않는다 — 그것을 잡으려면 별도 검사가 필요하다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **에러 메시지가 지시하는 설정은 그 설정을 읽는 코드와 함께 존재해야 한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **구성 오류는 한 예외 타입과 안정 코드로 보고한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -**등록:** `registrar.register(profile, spec)` → `validator.validate(profile)` → 통과하면 `ShareRegistration(profile)` 반환 → **이후 아무 일도 일어나지 않는다**. - -**pause:** `registration.pause(scope)` → 즉시 실패 stage. - -## 이 기록이 다루는 범위 - -:::evidence key="messaging-kafka-share-experimental-c04" alt="코드베이스에서 파일 목록을 만든 출력 4줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 4줄 · exit 0" zoom="true" -::: - -## 메시지가 흐르는 경로가 없다 - -이 leaf에 메시지가 흐르는 경로가 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-kafka-share-experimental-c05.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-kafka-share-experimental-c05.md deleted file mode 100644 index c5bd459..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-kafka-share-experimental-c05.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-kafka-share-experimental-c05 -title: 조용히 강등하지 않고 던진다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-kafka-share-experimental-c05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-kafka-share-experimental-c05 - file: ../../../final/evidence/rendered/messaging-kafka-share-experimental-c05.svg -evidence: - - ../../../final/evidence/raw/messaging-kafka-share-experimental-c05.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-kafka-share-experimental#L244 이다. -module: messaging-kafka-share-experimental ---- - -# 조용히 강등하지 않고 던진다 - -MessagingCapabilityUnavailableException의 javadoc이 이 leaf의 태도와 정확히 일치한다 — "Thrown instead of quietly degrading. Downgrading … ordered delivery to unordered, produces a system that looks healthy right up to the moment the guarantee actually mattered." - -## 관계 - -- **허용 의존 목록은 상한이므로 미사용을 잡지 않는다 — 그것을 잡으려면 별도 검사가 필요하다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **에러 메시지가 지시하는 설정은 그 설정을 읽는 코드와 함께 존재해야 한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **구성 오류는 한 예외 타입과 안정 코드로 보고한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -실패가 다섯 가지다. - -| 코드 | 예외 | 카테고리 | 조건 | -|---|---|---|---| -| `KAFKA_SHARE_DISABLED` | `MessagingCapabilityUnavailableException` | `CONFIGURATION` | `enabled == false` | -| (코드 없음) | `IllegalArgumentException` | — | `orderingScope != NONE` | -| `KAFKA_SHARE_NO_PAUSE` | `MessagingCapabilityUnavailableException` | `CONFIGURATION` | `pause(...)` | -| `KAFKA_SHARE_NO_RESUME` | `MessagingCapabilityUnavailableException` | `CONFIGURATION` | `resume(...)` | -| (코드 없음) | `IllegalArgumentException` | — | `shareGroup` 공백, `maxDeliveryCount < 1` | - -## MessagingCapabilityUnavailableException 참조 위치 - -:::evidence key="messaging-kafka-share-experimental-c05" alt="코드베이스에서 MessagingCapabilityUnavailableException 를 검색한 출력 37줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingCapabilityUnavailableException 코드베이스 검색 — 37줄 · exit 0" zoom="true" -::: - -## javadoc 이 이 leaf 의 태도와 일치한다 - -"Thrown instead of quietly degrading. Downgrading … ordered delivery to unordered, produces a system that looks healthy right up to the moment the guarantee actually mattered." - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-nats-experimental-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-nats-experimental-c01.md deleted file mode 100644 index 77adcaa..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-nats-experimental-c01.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-nats-experimental-c01 -title: 없는 큐를 찾아 나서게 만들지 않는다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-nats-experimental-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-nats-experimental-c01 - file: ../../../final/evidence/rendered/messaging-nats-experimental-c01.svg -evidence: - - ../../../final/evidence/raw/messaging-nats-experimental-c01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-nats-experimental#L82 이다. -module: messaging-nats-experimental ---- - -# 없는 큐를 찾아 나서게 만들지 않는다 - -nativeDeadLetter=false 의 근거가 클래스 javadoc 과 검증기 javadoc 양쪽에 있다 — 없는 큐를 찾아 나서게 만들지 않는다. keyedOrdering=false 도 검증기가 강제한다 — 키 순서를 요구하는 목적지를 거부한다. - -## 본문 - - - -능력 상수는 다음과 같다. - -```java -CAPABILITIES = (true, true, true, true, true, false, true, false, false, true, false, true); -``` - -## nativeDeadLetter 가 false 인 근거 - -클래스 javadoc 과 검증기 javadoc 양쪽에 있다 — 없는 큐를 찾아 나서게 만들지 않는다. - -## 이 기록이 다루는 범위 - -:::evidence key="messaging-nats-experimental-c01" alt="코드베이스에서 파일 목록을 만든 출력 7줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 7줄 · exit 0" zoom="true" -::: - -## keyedOrdering 도 검증기가 강제한다 - -키 순서를 요구하는 목적지를 거부한다. `deduplicatedPublish=true` 는 §17.1 이 다룬다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-observability-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-observability-c03.md deleted file mode 100644 index 28c2f38..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-observability-c03.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-observability-c03 -title: 차원이 닫혀 있고, 사전 확인과 커밋 사이에 lock이 없다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-observability-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-observability-c03 - file: ../../../final/evidence/rendered/messaging-observability-c03.svg - - key: messaging-observability-c03-diagram - file: ../../../final/assets/diagrams/messaging-observability-c03.svg -evidence: - - ../../../final/evidence/raw/messaging-observability-c03.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-observability#L113 이다. -module: messaging-observability ---- - -# 차원이 닫혀 있고, 사전 확인과 커밋 사이에 lock이 없다 - -여섯 차원: broker, destinationProfile, operation, outcome, failureCategory, retryStage. 없는 값은 NONE = "none"이다 — null도 빈 문자열도 아니고 명시적 sentinel이다. - -## 관계 - -- **타입이 문서화한 불변식은 타입이 강제한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -`MessagingTags`는 열린 map이 아니라 고정 record다 — "Every field here is bounded by configuration or by an enum, so the cardinality of the metric is known before it is ever scraped. Message ids, partition keys, tenant ids, and offsets are all deliberately absent: each of them is unbounded at runtime and would multiply every series by the message volume." - -## 태그에서 일부러 뺀 것 - -:::evidence key="messaging-observability-c03-diagram" alt="여섯 고정 차원과 없는 값은 none 이 MessagingTags 안에 놓이고 message id 와 partition key, tenant id 와 offset 이 바깥에 빗금으로 놓인다" caption="태그에서 일부러 뺀 것" zoom="false" -::: - -여섯 차원은 `broker`, `destinationProfile`, `operation`, `outcome`, `failureCategory`, `retryStage`다. 없는 값은 `NONE = "none"`이다 — null도 빈 문자열도 아니고 명시적 sentinel이다. - -## 두 factory의 차이 - -`new MessagingTags(6개 인자)`는 `failureCategory`와 `retryStage`를 호출자가 지정하고, `MessagingTags.of(4개 인자)`는 둘 다 `NONE` 고정이다. 이 차이가 §12.1의 핵심이 된다. - -## LinkedHashMap 참조 위치 - -:::evidence key="messaging-observability-c03" alt="코드베이스에서 LinkedHashMap 를 검색한 출력 10줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="LinkedHashMap 코드베이스 검색 — 10줄 · exit 0" zoom="true" -::: - -`asMap()`이 `LinkedHashMap`으로 순서를 고정하고 `Map.copyOf`로 불변화한다. - -## 호출자가 철자를 정하지 않는다 - -`MessagingObservation`은 네 메서드와 네 상수(`PUBLISH`, `CONSUME`, `SETTLE`, `DEAD_LETTER`)를 갖는다. `publish(...)`는 `PublishCompletion`과 `Optional`를 받아 **enum에서 문자열을 파생**한다. 이 클래스는 소비자가 0이다(§12.1). - -## 이전 결함 둘이 코드에 남아 있다 - -기본 상한은 200/차원이다. 먼저 lock 없이 `values.contains(value)`로 빠른 경로를 두고, 새 값일 때만 `synchronized`로 들어가 다시 확인한다 — double-checked 패턴이다. 테스트가 경합을 직접 재현한다(`MessagingSecretLeakTest.concurrentAdmissionNeverExceedsTheLimit`). - -## 사전 확인과 커밋 사이에 lock이 없다 - -`wouldAdmit`으로 전수 사전 확인 후 `admit`으로 커밋한다. 그 사이에 lock이 없으므로 두 스레드가 동시에 통과할 수 있고, 그 경우 두 번째 `admit`이 false를 반환해 `admitted &= ...`가 false가 된다 — 상한은 지켜지고 결과만 거절이 된다. 안전한 방향이다. - -## 같은 문제를 두 강도로 푼다 - -거부 목록은 27개 키이고 **두 범주**를 섞어 담는다. `isDenied`가 소문자 정규화 후 정확 일치다. **`messaging-core-api`의 `MessageHeaders.carriesACredential`은 세그먼트 매칭 + 인접 결합**(그쪽 §4.6)인데 이쪽은 정확 일치다(§12.3). `msg.id`가 목록에 리터럴로 들어 있다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-observability-c04.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-observability-c04.md deleted file mode 100644 index a2205a1..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-observability-c04.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-observability-c04 -title: 거절된 태그 집합도 세어서 남긴다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-observability-c04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-observability-c04 - file: ../../../final/evidence/rendered/messaging-observability-c04.svg -evidence: - - ../../../final/evidence/raw/messaging-observability-c04.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-observability#L334 이다. -module: messaging-observability ---- - -# 거절된 태그 집합도 세어서 남긴다 - -메트릭: 호출자가 MessagingTags를 만들어 MessagingObservation의 다섯 메서드 중 하나를 호출 → MessagingMetrics.admitted(tags) → guard.admit(tags) → 통과하면 Micrometer Tags로 변환 후 미터 기록, 거절되면 rejectedTagSets.increment() 추적(발행): tracer.inject(context, headers) → traceparent 없으면 그대로 반환 → 있으면 세 헤더를 platform factory로 추가 추적(수신): tracer.extract(headers) → traceparent 없으면 TraceContext.none() → 있으면 세 값으로 TraceContext 재구성(core-api의 W3C 검증을 통과해야 함) 감사: 호출자가 MessagingAuditEvent를 만들어 sink에 record - -## 관계 - -- **타입이 문서화한 불변식은 타입이 강제한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -**메트릭:** 호출자가 `MessagingTags`를 만들어 `MessagingObservation`의 다섯 메서드 중 하나를 호출 → `MessagingMetrics.admitted(tags)` → `guard.admit(tags)` → 통과하면 Micrometer `Tags`로 변환 후 미터 기록, 거절되면 `rejectedTagSets.increment()`. - -## MessagingTags 참조 위치 - -:::evidence key="messaging-observability-c04" alt="코드베이스에서 MessagingTags 를 검색한 출력 37줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingTags 코드베이스 검색 — 37줄 · exit 0" zoom="true" -::: - -## 추적 — 발행 쪽 - -`tracer.inject(context, headers)` → `traceparent` 없으면 그대로 반환 → 있으면 세 헤더를 `platform` factory로 추가. - -## 추적 — 수신 쪽 - -`tracer.extract(headers)` → `traceparent` 없으면 `TraceContext.none()` → 있으면 세 값으로 `TraceContext` 재구성(**core-api의 W3C 검증을 통과해야 함**). - -## 감사 - -호출자가 `MessagingAuditEvent`를 만들어 sink에 `record` 한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-observability-c05.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-observability-c05.md deleted file mode 100644 index e465c6e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-observability-c05.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-observability-c05 -title: 손상된 traceparent 는 이 leaf 의 실패 어휘 밖에서 터진다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-observability-c05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-observability-c05 - file: ../../../final/evidence/rendered/messaging-observability-c05.svg -evidence: - - ../../../final/evidence/raw/messaging-observability-c05.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-observability#L346 이다. -module: messaging-observability ---- - -# 손상된 traceparent 는 이 leaf 의 실패 어휘 밖에서 터진다 - -이 leaf는 MessagingException을 하나도 던지지 않는다. 실패를 값으로 표현한다. - -## 관계 - -- **타입이 문서화한 불변식은 타입이 강제한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -이 leaf는 `MessagingException`을 하나도 던지지 않는다. 실패를 **값으로 표현**한다. - -## MessagingException 참조 위치 - -:::evidence key="messaging-observability-c05" alt="코드베이스에서 MessagingException 를 검색한 출력 36줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingException 코드베이스 검색 — 36줄 · exit 0" zoom="true" -::: - -## 던지는 곳은 전부 호출자의 프로그래밍 오류다 - -`IllegalArgumentException`을 던지는 곳은 넷 — `CardinalityGuard` 생성자(`limitPerDimension < 1`), `MessagingMetrics.recordDelivery`(`attempt < 1`), `MessagingTracer.shouldLinkRatherThanContinue`(`batchSize < 1`), `MessagingAuditEvent` 생성자(빈 필드). - -## extract 가 W3C 검증에 걸릴 수 있다 - -`new TraceContext(traceparent, tracestate, baggage)`가 core-api의 정규식·바이트 상한·all-zero 검사를 돌리므로(그쪽 §4.11), 다른 시스템이 보낸 손상된 `traceparent`는 `IllegalArgumentException`이 된다. 그 예외는 `MessagingException`이 아니고 `extract`는 그것을 잡지 않는다. `messaging-cloudevents`의 id 파싱과 같은 형태다(`final/document.md#a19-messaging-cloudevents` §17). §17. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-outbox-jdbc-postgresql-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-outbox-jdbc-postgresql-c03.md deleted file mode 100644 index cdc36f3..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-outbox-jdbc-postgresql-c03.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-outbox-jdbc-postgresql-c03 -title: 확률을 낮추는 것과 데이터 제약으로 만드는 것은 다르다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-outbox-jdbc-postgresql-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-outbox-jdbc-postgresql-c03 - file: ../../../final/evidence/rendered/messaging-outbox-jdbc-postgresql-c03.svg -evidence: - - ../../../final/evidence/raw/messaging-outbox-jdbc-postgresql-c03.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-outbox-jdbc-postgresql#L177 이다. -module: messaging-outbox-jdbc-postgresql ---- - -# 확률을 낮추는 것과 데이터 제약으로 만드는 것은 다르다 - -V1 — message_id 를 대리키가 아니라 기본키로 삼는다. 인덱스도 근거가 있다. - -## 본문 - - - -마이그레이션 네 개가 이 리프의 이력을 담고 있다. - -## V1 — message_id 가 기본키인 이유 - -대리키가 아니라 기본키다 — 릴레이가 모든 재시도에서 보존해야 하는 논리적 정체이고, 그것을 키로 만들면 어떤 경로도 같은 행을 새 id로 발행할 수 없다. 인덱스도 근거가 있다 — 부분 인덱스인 이유("PUBLISHED rows accumulate until the retention job removes them"), `IN_FLIGHT` 를 포함하는 이유("A relay that dies mid-publish leaves rows in that state ... omitting them here would strand those messages"). - -## V2 — 펜싱 토큰 - -주석이 시나리오를 그대로 적는다 — relay A 가 청구하고 브로커를 부르는 사이 lease 가 만료되고, relay B 가 재청구해 발행하고 PUBLISHED 를 기록한다. "확률을 낮추는 것과 데이터 제약으로 만드는 것은 다르다" — 이 리프에서 가장 좋은 한 줄이다. `EXHAUSTED` 상태 추가와 `next_attempt_at` 인덱스도 여기서 들어온다. - -## 이 기록이 다루는 범위 - -:::evidence key="messaging-outbox-jdbc-postgresql-c03" alt="코드베이스에서 파일 목록을 만든 출력 13줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 13줄 · exit 0" zoom="true" -::: - -## V3 — admin 저널 - -복합 기본키 `(approval_ticket, plan_digest)` 의 근거가 `messaging-admin-api` 의 것과 동일하게 적혀 있다. - -## V4 — 정경 메타데이터 12컬럼 - -왜 봉투 blob 이 아니라 컬럼인지가 명확하다. 그리고 밀반입 문제를 명시한다 — "smuggled through the header map under the reserved `msg.*` names ... a row whose header map contains `msg.id` overwrites another message's identity on the wire". DB 레벨 제약을 Java 와 이중으로 거는 이유도 적혀 있다. 마지막으로 **생성 컬럼**이 두 릴레이의 합의를 하나로 만든다. - -## 이 수정이 properties 파일에는 도달하지 않았다 - -§12.4(a). - -## append 가 보는 세 가지 - -`:228-245` — 활성 트랜잭션이 있는가 / 읽기 전용이 아닌가 / **이 DataSource 에 바인딩되어 있는가**. 세 번째가 특히 좋다 — 다른 DataSource 의 트랜잭션 안에서 append 하면 둘이 독립적으로 커밋된다. `append(Connection, OutboxRecord)` 가 package-private 으로 내려간 이력도 적혀 있다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-policy-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-policy-c02.md deleted file mode 100644 index 4359125..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-policy-c02.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-policy-c02 -title: bean 정의는 전부 starter 쪽에 있다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-policy-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-policy-c02 - file: ../../../final/evidence/rendered/messaging-policy-c02.svg -evidence: - - ../../../final/evidence/raw/messaging-policy-c02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-policy#L84 이다. -module: messaging-policy ---- - -# bean 정의는 전부 starter 쪽에 있다 - -들어오는 것: messaging-core-api(api), messaging-schema-api(api). 둘 다 api인 이유는 DestinationProfile이 DeliveryGuarantee·OrderingScope·DestinationKind·DestinationName(core-api)와 SchemaCompatibility(schema-api)를 필드로 갖기 때문이다. - -## 관계 - -- **구성 오류는 한 예외 타입과 안정 코드로 보고한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **저장소 밖 문서를 절 번호로 인용하지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **부팅 경로의 알고리즘 복잡도는 문서화한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -들어오는 것은 `messaging-core-api`(api)와 `messaging-schema-api`(api)다. 둘 다 `api`인 이유는 `DestinationProfile`이 `DeliveryGuarantee`·`OrderingScope`·`DestinationKind`·`DestinationName`(core-api)와 `SchemaCompatibility`(schema-api)를 필드로 갖기 때문이다. - -## 이 기록이 다루는 범위 - -:::evidence key="messaging-policy-c02" alt="코드베이스에서 파일 목록을 만든 출력 25줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 25줄 · exit 0" zoom="true" -::: - -## 나가는 쪽 - -`messaging-transport-spi`, `messaging-runtime-core`, `messaging-kafka`, `messaging-kafka-share-experimental`, `messaging-rabbit`, `messaging-outbox-jdbc-postgresql`, `messaging-admin-api`, `messaging-admin-runtime`, `messaging-pulsar-experimental`, `messaging-nats-experimental`, `messaging-spring-cloud-stream-bridge`, `messaging-spring-boot-starter`, `messaging-testkit`. - -## 실제 배선 지점 넷이 전부 starter 안에 있다 - -전부 `messaging-spring-boot-starter/MessagingCoreAutoConfiguration`이다. 이 leaf 자체는 Spring 주석을 갖지 않는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-policy-c04.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-policy-c04.md deleted file mode 100644 index e5f7bd6..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-policy-c04.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-policy-c04 -title: 재시도 판단과 DLQ 경로는 출하 컨텍스트에서 호출되지 않는다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-policy-c04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-policy-c04 - file: ../../../final/evidence/rendered/messaging-policy-c04.svg -evidence: - - ../../../final/evidence/raw/messaging-policy-c04.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-policy#L411 이다. -module: messaging-policy ---- - -# 재시도 판단과 DLQ 경로는 출하 컨텍스트에서 호출되지 않는다 - -시작: MessagingCoreAutoConfiguration:134 → validateAll(registered) → 프로파일별 15검사 + 중복 이름 + 사이클 그래프 → 실패 시 IllegalArgumentException으로 부팅 중단 발행: DefaultMessagePublisher → admission.admit(destination, bytes) → 크기 → 종료 여부 → 목적지 슬롯 → 프로세스 permit → (발행) → admission.complete(destination) 재시도 판단: RetryContext(profile, deliveryMetadata, failure, capabilities, ...) → engine.decide(...) → RetryDecision 5종 중 하나 — 이 경로는 출하 컨텍스트에서 호출되지 않는다(§12.1) DLQ: orchestrator.deadLetter(profile, delivery, failure, settlement) → 헤더 6개 추가 → 발행 → CONFIRMED면 원본 정산 — 이 경로도 호출되지 않는다(§12.1) - -## 관계 - -- **구성 오류는 한 예외 타입과 안정 코드로 보고한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **저장소 밖 문서를 절 번호로 인용하지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **부팅 경로의 알고리즘 복잡도는 문서화한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -**시작:** `MessagingCoreAutoConfiguration:134` → `validateAll(registered)` → 프로파일별 15검사 + 중복 이름 + 사이클 그래프 → 실패 시 `IllegalArgumentException`으로 부팅 중단. - -**발행:** `DefaultMessagePublisher` → `admission.admit(destination, bytes)` → 크기 → 종료 여부 → 목적지 슬롯 → 프로세스 permit → (발행) → `admission.complete(destination)`. - -## 이 기록이 다루는 범위 - -:::evidence key="messaging-policy-c04" alt="코드베이스에서 파일 목록을 만든 출력 25줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 25줄 · exit 0" zoom="true" -::: - -## 호출되지 않는 두 경로 - -**재시도 판단:** `RetryContext(profile, deliveryMetadata, failure, capabilities, ...)` → `engine.decide(...)` → `RetryDecision` 5종 중 하나 — 이 경로는 출하 컨텍스트에서 호출되지 않는다(§12.1). - -**DLQ:** `orchestrator.deadLetter(profile, delivery, failure, settlement)` → 헤더 6개 추가 → 발행 → CONFIRMED면 원본 정산 — 이 경로도 호출되지 않는다(§12.1). - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-policy-c07.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-policy-c07.md deleted file mode 100644 index 7530f5f..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-policy-c07.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-policy-c07 -title: 발행 경로는 프로덕션에서 실제로 조립된다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-policy-c07 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-policy-c07 - file: ../../../final/evidence/rendered/messaging-policy-c07.svg -evidence: - - ../../../final/evidence/raw/messaging-policy-c07.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-policy#L603 이다. -module: messaging-policy ---- - -# 발행 경로는 프로덕션에서 실제로 조립된다 - -DefaultMessagePublisher MessagingCoreAutoConfiguration.java:446 TransportMessagingRuntime MessagingCoreAutoConfiguration.java:476 DefaultRetryDecisionEngine MessagingCoreAutoConfiguration.java:168 DeadLetterOrchestrator MessagingCoreAutoConfiguration.java:180 - -## 관계 - -- **구성 오류는 한 예외 타입과 안정 코드로 보고한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **저장소 밖 문서를 절 번호로 인용하지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **부팅 경로의 알고리즘 복잡도는 문서화한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -네 타입이 프로덕션 auto-configuration 안에서 생성된다. - -| 타입 | 생성 지점 | -|---|---| -| `DefaultMessagePublisher` | `MessagingCoreAutoConfiguration.java:446` | -| `TransportMessagingRuntime` | `MessagingCoreAutoConfiguration.java:476` | -| `DefaultRetryDecisionEngine` | `MessagingCoreAutoConfiguration.java:168` | -| `DeadLetterOrchestrator` | `MessagingCoreAutoConfiguration.java:180` | - -## 이 기록이 다루는 범위 - -:::evidence key="messaging-policy-c07" alt="코드베이스에서 파일 목록을 만든 출력 25줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 25줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-policy-c08.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-policy-c08.md deleted file mode 100644 index 0f68591..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-policy-c08.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-policy-c08 -title: RetryDecision 변형에 반응하는 파일 넷 중 셋이 선언과 테스트다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-policy-c08 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-policy-c08 - file: ../../../final/evidence/rendered/messaging-policy-c08.svg -evidence: - - ../../../final/evidence/raw/messaging-policy-c08.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-policy#L617 이다. -module: messaging-policy ---- - -# RetryDecision 변형에 반응하는 파일 넷 중 셋이 선언과 테스트다 - -messaging-kafka/.../KafkaRetryExecutor.java (생성되지 않음) messaging-policy/.../DefaultRetryDecisionEngine.java (생산자) messaging-policy/.../RetryDecision.java (선언) messaging-policy/.../RetryDecisionEngineTest.java (테스트) - -## 관계 - -- **구성 오류는 한 예외 타입과 안정 코드로 보고한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **저장소 밖 문서를 절 번호로 인용하지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **부팅 경로의 알고리즘 복잡도는 문서화한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -`RetryDecision` 변형에 실제로 작용하는 파일은 넷이다. - -| 파일 | 역할 | -|---|---| -| `messaging-kafka/.../KafkaRetryExecutor.java` | 생성되지 않음 | -| `messaging-policy/.../DefaultRetryDecisionEngine.java` | 생산자 | -| `messaging-policy/.../RetryDecision.java` | 선언 | -| `messaging-policy/.../RetryDecisionEngineTest.java` | 테스트 | - -## 이 기록이 다루는 범위 - -:::evidence key="messaging-policy-c08" alt="코드베이스에서 파일 목록을 만든 출력 25줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 25줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-rabbit-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-rabbit-c01.md deleted file mode 100644 index e4ef670..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-rabbit-c01.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-rabbit-c01 -title: 소비·정착·죽은 편지의 세 규율 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-rabbit-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-rabbit-c01 - file: ../../../final/evidence/rendered/messaging-rabbit-c01.svg - - key: messaging-rabbit-c01-diagram - file: ../../../final/assets/diagrams/messaging-rabbit-c01.svg -evidence: - - ../../../final/evidence/raw/messaging-rabbit-c01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-rabbit#L91 이다. -module: messaging-rabbit ---- - -# 소비·정착·죽은 편지의 세 규율 - -좁은 catch. RabbitConsumerRegistrar.onMessage 가 디코딩만 감싸는 안쪽 try 를 따로 둔다. - -## 본문 - - - -**좁은 catch.** `RabbitConsumerRegistrar.onMessage` 가 디코딩만 감싸는 안쪽 `try` 를 따로 둔다. - -## 핸들러가 끝난 뒤의 두 갈래 - -:::evidence key="messaging-rabbit-c01-diagram" alt="핸들러 완료에서 정착함 ack 와 정착 안 함 requeue 두 갈래가 나온다" caption="핸들러가 끝난 뒤의 두 갈래" zoom="false" -::: - -**정착하지 않은 핸들러.** 완료했는데 정착하지 않으면 대신 ack 하지 않고 requeue 한다 — "acknowledging on its behalf would silently drop it". - -## RabbitConsumerRegistrar 참조 위치 - -:::evidence key="messaging-rabbit-c01" alt="코드베이스에서 RabbitConsumerRegistrar 를 검색한 출력 13줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RabbitConsumerRegistrar 코드베이스 검색 — 13줄 · exit 0" zoom="true" -::: - -## 네이티브 죽은 편지 - -`RabbitNativeDeadLetterCapability` 가 두 조건을 모두 요구한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-runtime-core-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-runtime-core-c01.md deleted file mode 100644 index 7e5a1fd..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-runtime-core-c01.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-runtime-core-c01 -title: 컨텍스트 테스트를 통과하면서 아무것도 실행하지 않는 조립 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-runtime-core-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-runtime-core-c01 - file: ../../../final/evidence/rendered/messaging-runtime-core-c01.svg -evidence: - - ../../../final/evidence/raw/messaging-runtime-core-c01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-runtime-core#L55 이다. -module: messaging-runtime-core ---- - -# 컨텍스트 테스트를 통과하면서 아무것도 실행하지 않는 조립 - -이 leaf는 조립 결함 하나를 고치기 위해 만들어졌다. 여섯 파일 중 다섯의 javadoc이 "X was an interface with no implementation" 형태로 시작한다. - -## 관계 - -- **만들어 두고 흘리지 않는 진단값은 진단이 아니다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **증가한다고 문서화한 값이 리터럴이면 그 사실을 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **안정 코드는 운영자의 행동이 갈리는 지점마다 나눈다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **`CompletionStage`를 반환하는 메서드는 동기적으로 던지지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -**이 leaf는 조립 결함 하나를 고치기 위해 만들어졌다.** 여섯 파일 중 다섯의 javadoc이 "X was an interface with no implementation" 형태로 시작한다. `build.gradle`이 그 사정을 파일 맨 위에 적는다. - -## DefaultDeliveryProcessor 참조 위치 - -:::evidence key="messaging-runtime-core-c01" alt="코드베이스에서 DefaultDeliveryProcessor 를 검색한 출력 7줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DefaultDeliveryProcessor 코드베이스 검색 — 7줄 · exit 0" zoom="true" -::: - -## 진단의 마지막 문장 - -**컨텍스트 테스트를 통과하면서 아무것도 실행하지 않는 조립**이 가능했다는 것. 이 저장소가 반복해서 만나는 형태다. - -## 여섯 중 하나는 아직 배선되지 않았다 - -여섯 파일이 구멍을 메웠고, 그중 다섯은 배선됐다. 마지막 하나(`DefaultDeliveryProcessor`)는 배선되지 않았다(§12.1). - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-runtime-core-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-runtime-core-c02.md deleted file mode 100644 index 4a81cfa..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-runtime-core-c02.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-runtime-core-c02 -title: 인자가 여섯 개라는 것이 관측 지점이다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-runtime-core-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-runtime-core-c02 - file: ../../../final/evidence/rendered/messaging-runtime-core-c02.svg -evidence: - - ../../../final/evidence/raw/messaging-runtime-core-c02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-runtime-core#L86 이다. -module: messaging-runtime-core ---- - -# 인자가 여섯 개라는 것이 관측 지점이다 - -들어오는 것: 여섯 project 의존, 전부 api. DefaultMessagePublisher 한 클래스가 그중 다섯을 생성자로 받으므로 api가 맞다. - -## 관계 - -- **만들어 두고 흘리지 않는 진단값은 진단이 아니다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **증가한다고 문서화한 값이 리터럴이면 그 사실을 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **안정 코드는 운영자의 행동이 갈리는 지점마다 나눈다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **`CompletionStage`를 반환하는 메서드는 동기적으로 던지지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -들어오는 것은 여섯 project 의존이고 전부 `api`다. `DefaultMessagePublisher` 한 클래스가 그중 다섯을 생성자로 받으므로 `api`가 맞다. - -## DefaultMessagePublisher 참조 위치 - -:::evidence key="messaging-runtime-core-c02" alt="코드베이스에서 DefaultMessagePublisher 를 검색한 출력 22줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DefaultMessagePublisher 코드베이스 검색 — 22줄 · exit 0" zoom="true" -::: - -## 나가는 쪽과 배선 지점 - -나가는 것은 `messaging-spring-boot-starter`뿐이다. 배선 지점은 다섯이고 전부 `MessagingCoreAutoConfiguration`에 있다. 446의 인자가 **여섯 개**라는 것이 §12.1의 관측 지점이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-runtime-core-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-runtime-core-c03.md deleted file mode 100644 index 4053aed..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-runtime-core-c03.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-runtime-core-c03 -title: 바이트가 프로세스를 떠났는가가 REJECTED와 AMBIGUOUS를 가른다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-runtime-core-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-runtime-core-c03 - file: ../../../final/evidence/rendered/messaging-runtime-core-c03.svg - - key: messaging-runtime-core-c03-diagram - file: ../../../final/assets/diagrams/messaging-runtime-core-c03.svg -evidence: - - ../../../final/evidence/raw/messaging-runtime-core-c03.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-runtime-core#L128 이다. -module: messaging-runtime-core ---- - -# 바이트가 프로세스를 떠났는가가 REJECTED와 AMBIGUOUS를 가른다 - -실제 순서 여덟 단계: 1–7은 전부 REJECTED, 8만 AMBIGUOUS다. 그 경계가 정확히 "바이트가 프로세스를 떠났는가"다. - -## 관계 - -- **만들어 두고 흘리지 않는 진단값은 진단이 아니다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **증가한다고 문서화한 값이 리터럴이면 그 사실을 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **안정 코드는 운영자의 행동이 갈리는 지점마다 나눈다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **`CompletionStage`를 반환하는 메서드는 동기적으로 던지지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -순서가 인터셉터 map에서 조립되지 않고 고정돼 있으며, javadoc이 각 단계의 위치를 결정으로 적는다 — 목적지와 접근이 먼저라 인가되지 않은 발행이 payload를 인코딩하지 않고, 인코딩이 admission보다 먼저인 것은 admission 경계가 바이트에 걸려 있어 인코딩 전에는 바이트 수를 모르기 때문이며, 런타임 lease가 전송 직전 마지막인 것은 이미 in-flight 한도에 계상된 메시지 밑에서 rotation이 transport를 바꾸지 못하게 하기 위해서다. - -## 발행 단계가 고정된 순서 - -:::evidence key="messaging-runtime-core-c03-diagram" alt="목적지와 접근에서 인코딩으로 이름 확인이 건너가고 인코딩에서 admission 으로 바이트 수가 건너가고 admission 에서 브로커 전송으로 permit 이 건너간다" caption="발행 단계가 고정된 순서" zoom="false" -::: - -## 여덟 단계 중 여덟만 AMBIGUOUS다 - -**1–7은 전부 `REJECTED`, 8만 `AMBIGUOUS`다.** 그 경계가 정확히 "바이트가 프로세스를 떠났는가"다. `messaging-core-api`의 3상태(§4.1)가 여기서 실제 분기가 된다. 그리고 `rejected(...)`가 만드는 `PublishResult`는 `PublishEvidence.notTransmitted()`를 쓰므로 `PublishResult` 생성자의 14가지 금지 조합 검증을 자연히 통과한다. - -## PublishResult 참조 위치 - -:::evidence key="messaging-runtime-core-c03" alt="코드베이스에서 PublishResult 를 검색한 출력 25줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="PublishResult 코드베이스 검색 — 25줄 · exit 0" zoom="true" -::: - -## 이미 기다리기를 그만둔 메시지를 보내지 않는다 - -`remainingBudget`이 `timeout - elapsedSince(startedAt)`이고, 0 이하면 전송 전에 `REJECTED`로 끝낸다 — "Sending anyway would start a message the caller has already stopped waiting for." - -## copy 에 타임아웃을 거는 이유 - -`orTimeout`을 원본에 걸면 만료가 어댑터의 stage를 완료시켜 어댑터의 자기 정리가 깨진다. 복사본에 걸면 만료는 이쪽 경로만 끝내고 어댑터는 자기 in-flight를 계속 소유한다. 그 대가도 명시돼 있다 — permit과 lease는 **복사본이 완료될 때** 반납되므로, 브로커가 나중에 응답해도 이미 반납된 상태다. 그것이 의도다("holding them until a stalled broker answers is how a rotation waits forever"). - -## 두 경우를 한 블록에서 처리한다 - -`handle`은 `whenComplete`와 달리 실패를 삼키고 값을 반환한다. `lease.close()`는 `MessagingRuntimeLease` 계약상 멱등이고(`transport-spi` §4.1), `admission.complete`도 미보유 목적지에 대해 무해하다(`messaging-policy` §4.3). - -## 한 가지 비대칭 - -6번(`admit`)이 예외를 던지면 그 예외가 그대로 호출자에게 전파된다 — `try` 블록 밖이다. 다른 모든 실패는 `PublishResult`로 정규화되는데 admission 실패만 예외다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-runtime-core-c05.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-runtime-core-c05.md deleted file mode 100644 index bbc823e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-runtime-core-c05.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-runtime-core-c05 -title: 같은 코드가 두 completion에 쓰인다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-runtime-core-c05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-runtime-core-c05 - file: ../../../final/evidence/rendered/messaging-runtime-core-c05.svg -evidence: - - ../../../final/evidence/raw/messaging-runtime-core-c05.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-runtime-core#L397 이다. -module: messaging-runtime-core ---- - -# 같은 코드가 두 completion에 쓰인다 - -DefaultMessagePublisher가 만드는 결과: 같은 코드 PUBLISH_DEADLINE_EXCEEDED가 두 completion에 쓰인다. 전송 전이면 REJECTED, 후면 AMBIGUOUS다. - -## 관계 - -- **만들어 두고 흘리지 않는 진단값은 진단이 아니다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **증가한다고 문서화한 값이 리터럴이면 그 사실을 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **안정 코드는 운영자의 행동이 갈리는 지점마다 나눈다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **`CompletionStage`를 반환하는 메서드는 동기적으로 던지지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -`DefaultMessagePublisher`가 만드는 결과에서 같은 코드 `PUBLISH_DEADLINE_EXCEEDED`가 **두 completion에 쓰인다.** 전송 전이면 `REJECTED`, 후면 `AMBIGUOUS`다. 코드만 보는 대시보드는 두 경우를 구분할 수 없다 — completion을 함께 봐야 한다. §17. - -## DefaultMessagePublisher 참조 위치 - -:::evidence key="messaging-runtime-core-c05" alt="코드베이스에서 DefaultMessagePublisher 를 검색한 출력 22줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DefaultMessagePublisher 코드베이스 검색 — 22줄 · exit 0" zoom="true" -::: - -## sanitized 가 타입 이름만 남긴다 - -`sanitized(Throwable)`가 메시지가 아니라 **타입 이름만** 남긴다. `messaging-core-api`의 `FailureDescriptor` javadoc("no payload, no stack trace, no credential")과 같은 관심사다. `isDeadline`과 `sanitized` 둘 다 `CompletionException`을 한 겹 벗긴다 — 비동기 경로에서 원인이 감싸지기 때문이다. - -## 처리기 쪽은 던지지 않는다 - -`DefaultDeliveryProcessor`는 예외를 던지지 않는다. 이중 정산만 `failedFuture`로 보고한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-schema-json-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-schema-json-c01.md deleted file mode 100644 index 1d97814..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-schema-json-c01.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-schema-json-c01 -title: 크기 초과를 codec에서 잡으면 버려도 안전한 실패가 된다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-schema-json-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-schema-json-c01 - file: ../../../final/evidence/rendered/messaging-schema-json-c01.svg - - key: messaging-schema-json-c01-diagram - file: ../../../final/assets/diagrams/messaging-schema-json-c01.svg -evidence: - - ../../../final/evidence/raw/messaging-schema-json-c01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-schema-json#L46 이다. -module: messaging-schema-json ---- - -# 크기 초과를 codec에서 잡으면 버려도 안전한 실패가 된다 - -Stable JSON codec 하나. MessageCodec(schema-api)을 구현하고 Jackson 3(tools.jackson.* 네임스페이스)을 쓴다. - -## 관계 - -- **실패 코드는 운영자의 다음 행동이 갈리는 지점마다 나눈다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -Stable JSON codec 하나. `MessageCodec`(schema-api)을 구현하고 Jackson 3(`tools.jackson.*` 네임스페이스)을 쓴다. - -## 크기 초과를 어디서 잡나 - -:::evidence key="messaging-schema-json-c01-diagram" alt="크기 초과에서 codec 이 잡으면 REJECTED 이고 브로커가 잡으면 AMBIGUOUS 인 두 갈래가 나온다" caption="크기 초과를 어디서 잡나" zoom="false" -::: - -javadoc이 "기본 codec으로 노출해도 안전한 이유" 셋을 명시한다. 세 번째가 `messaging-core-api`의 3상태 발행 결과와 직접 연결된다 — 크기 초과를 브로커가 아니라 여기서 잡으면 `NOT_TRANSMITTED` 증거가 붙은 `REJECTED`가 되고, 브로커에서 잡히면 `AMBIGUOUS`가 된다. 전자는 버려도 안전하고 후자는 아니다. - -## MessageCodec 참조 위치 - -:::evidence key="messaging-schema-json-c01" alt="코드베이스에서 MessageCodec 를 검색한 출력 40줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessageCodec 코드베이스 검색 — 40줄 · exit 0" zoom="true" -::: - -## 이 leaf만 implementation인 이유 - -Jackson 의존성은 `implementation`이다 — public 시그니처에 Jackson 타입이 없기 때문이다. 형제 leaf(`schema-avro`, `schema-protobuf`, `cloudevents`)는 vendor 타입이 public 시그니처에 나오므로 `api`로 선언했고 build.gradle에 그 이유를 주석으로 적었다. `src/messaging/CLAUDE.md:40-43`의 게이트가 이 구분을 강제한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-schema-json-c05.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-schema-json-c05.md deleted file mode 100644 index b5a90a1..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-schema-json-c05.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-schema-json-c05 -title: 여섯 갈래 원인이 하나의 코드로 접힌다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-schema-json-c05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-schema-json-c05 - file: ../../../final/evidence/rendered/messaging-schema-json-c05.svg -evidence: - - ../../../final/evidence/raw/messaging-schema-json-c05.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-schema-json#L246 이다. -module: messaging-schema-json ---- - -# 여섯 갈래 원인이 하나의 코드로 접힌다 - -전부 retryable = false다 — PERMANENT_BUSINESS와 DESERIALIZATION 카테고리다. 같은 바이트를 다시 디코딩해도 같은 결과이므로 일관적이다. - -## 관계 - -- **실패 코드는 운영자의 다음 행동이 갈리는 지점마다 나눈다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -실패는 전부 `retryable = false`다 — `PERMANENT_BUSINESS`와 `DESERIALIZATION` 카테고리다. 같은 바이트를 다시 디코딩해도 같은 결과이므로 일관적이다. - -## 이 기록이 다루는 범위 - -:::evidence key="messaging-schema-json-c05" alt="코드베이스에서 파일 목록을 만든 출력 1줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 1줄 · exit 0" zoom="true" -::: - -## 진단 손실 하나 - -파서 강화가 잡는 여섯 가지(깊이, 중복 키, trailing token, 미지 필드, 문서 길이, 토큰 길이)가 전부 하나의 코드 `JSON_DECODE_FAILED`로 접힌다. 운영자는 "JSON 디코딩 실패"만 보고 원인 여섯 갈래를 구분할 수 없다. - -## 원인은 붙지만 분류에는 남지 않는다 - -원인 예외가 `cause`로 붙지만 `FailureDescriptor`는 `exceptionType`을 `Optional.empty()`로 둔다(`MessageSerializationException`의 3인자 생성자 경로). §17 참조. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-schema-protobuf-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-schema-protobuf-c03.md deleted file mode 100644 index 4b8f5d2..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-schema-protobuf-c03.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-schema-protobuf-c03 -title: 어긋난 짝을 표현할 수 없게 만든 값 하나 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-schema-protobuf-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-schema-protobuf-c03 - file: ../../../final/evidence/rendered/messaging-schema-protobuf-c03.svg - - key: messaging-schema-protobuf-c03-diagram - file: ../../../final/assets/diagrams/messaging-schema-protobuf-c03.svg -evidence: - - ../../../final/evidence/raw/messaging-schema-protobuf-c03.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-schema-protobuf#L112 이다. -module: messaging-schema-protobuf ---- - -# 어긋난 짝을 표현할 수 없게 만든 값 하나 - -이 leaf에서 가장 밀도 높은 결정이다. 증명 방법이 영리하다. - -## 관계 - -- **검증되지 않는 스키마 파일은 문서임을 파일 안에 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **신뢰할 수 없는 입력 쪽 경계를 먼저 테스트한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -이 leaf에서 가장 밀도 높은 결정이다. 예전에는 클래스와 parser가 두 개의 평행한 맵에 살았고 둘이 일치하는지 아무도 확인하지 않아, `OrderCreated.class`와 `OrderCancelled`의 parser를 짝지은 registry가 생성 시점에 받아들여진 뒤 디코딩 시점에 브로커 스레드에서 `ClassCastException`을 냈다. 둘을 한 값에 묶으면 어긋남을 표현할 수 없다. - -## 짝을 생성 시점에 증명하는 방법 - -:::evidence key="messaging-schema-protobuf-c03-diagram" alt="빈 바이트 파싱에서 default instance 로 proto3 유효가 건너가고 default instance 에서 선언 클래스 대조로 산출 타입이 건너간다" caption="짝을 생성 시점에 증명하는 방법" zoom="false" -::: - -증명 방법이 영리하다. proto3에서 모든 필드가 wire상 optional이므로 **빈 바이트는 항상 유효한 메시지**다. 그것을 파싱하면 default instance가 나오고 그 클래스가 곧 parser의 산출 타입이다. 별도 리플렉션 없이 짝을 확인한다. - -## 에러 메시지가 실패 지점을 명시한다 - -"a mismatched pairing fails at decode time on a broker thread, not here" — 즉 **여기서 실패하는 것이 목적**임을 메시지가 스스로 말한다. 두 코드가 다르다 — `PROTOBUF_CONTRACT_UNUSABLE`(파싱 자체 실패)과 `PROTOBUF_CONTRACT_MISMATCH`(파싱은 되는데 타입이 다름). 카테고리는 둘 다 `CONFIGURATION`이다. 테스트가 이 성질을 붙든다 — `aParserThatDoesNotProduceTheDeclaredClassIsRejectedAtConstruction`, `as("the mismatch used to surface as a ClassCastException on a broker thread")`. - -## BoundedByteSink 참조 위치 - -:::evidence key="messaging-schema-protobuf-c03" alt="코드베이스에서 BoundedByteSink 를 검색한 출력 15줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="BoundedByteSink 코드베이스 검색 — 15줄 · exit 0" zoom="true" -::: - -## 세 codec 중 유일하게 사전 거절이 가능하다 - -`BoundedByteSink.requireFits`가 이 leaf를 위해 존재하고, schema-api의 javadoc이 그것을 명시한다 — "Protobuf knows its serialized size exactly, so the whole encode can be refused before the first byte is written." 그리고 사전 검사가 사후 경계를 대체하지 않는다 — `writeTo(sink)`가 여전히 sink를 통과하므로 이중 방어다. schema-api javadoc: "this is a cheaper refusal, not a replacement for the bound." - -## 두 검사를 함께 보는 이유 - -`Message`인지와 등록된 클래스의 인스턴스인지를 함께 본다. 후자만으로 충분해 보이지만 전자가 `writeTo`를 부를 수 있음을 보장한다. JSON codec과 같은 비대칭이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-security-c05.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-security-c05.md deleted file mode 100644 index d580922..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-security-c05.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-security-c05 -title: 같은 두 검사를 두 예외 계층이 나눠 갖는다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-security-c05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-security-c05 - file: ../../../final/evidence/rendered/messaging-security-c05.svg -evidence: - - ../../../final/evidence/raw/messaging-security-c05.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-security#L339 이다. -module: messaging-security ---- - -# 같은 두 검사를 두 예외 계층이 나눠 갖는다 - -보안 판정이 두 예외 계층으로 나뉜다. BrokerTlsPolicy는 안정 코드가 붙은 MessagingConfigurationException을 쓰고, MessageSecurityValidator는 코드 없는 IllegalArgumentException을 쓴다. - -## 관계 - -- **배선된 게이트는 자기 leaf 레인에서 검증한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 술어가 두 타입에 있으면 하나가 다른 하나를 부른다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **가변 필드로 상태 전이를 표현하면 가시성을 함께 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **맵 갱신 함수 안에서 I/O를 하면 그 지연이 락 범위가 된다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -실패가 두 계층으로 나뉜다. - -| 코드 | 예외 | 위치 | -|---|---|---| -| `TLS_REQUIRED` | `MessagingConfigurationException` | `BrokerTlsPolicy` | -| `HOSTNAME_VERIFICATION_REQUIRED` | `MessagingConfigurationException` | 같음 | -| `TLS_PROTOCOL_NOT_ACCEPTED` | `MessagingConfigurationException` | 같음 | -| `TLS_PROTOCOL_UNSPECIFIED` | `MessagingConfigurationException` | 같음 | -| `APPLICATION_HOLDS_DESTRUCTIVE_GRANT` | `MessagingConfigurationException` | `BrokerAclManifest`(미사용) | -| `DESTINATION_PUBLISH_DENIED` | `MessageAuthorizationException` | `DestinationAccessValidator`(미사용) | -| `DESTINATION_CONSUME_DENIED` | `MessageAuthorizationException` | 같음(미사용) | -| `DESTINATION_ADMIN_DENIED` | `MessageAuthorizationException` | 같음(미사용) | -| (코드 없음) | `IllegalArgumentException` × 5 | `MessageSecurityValidator` | -| (코드 없음) | `IllegalArgumentException` | `CredentialIds`, 각 생성자 | -| (코드 없음) | `IllegalStateException` | `CredentialRuntime.material()` 소거 후 | - -## BrokerTlsPolicy 참조 위치 - -:::evidence key="messaging-security-c05" alt="코드베이스에서 BrokerTlsPolicy 를 검색한 출력 28줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="BrokerTlsPolicy 코드베이스 검색 — 28줄 · exit 0" zoom="true" -::: - -## 나뉘는 지점 - -`BrokerTlsPolicy`는 안정 코드가 붙은 `MessagingConfigurationException`을 쓰고, `MessageSecurityValidator`는 코드 없는 `IllegalArgumentException`을 쓴다. 둘이 같은 두 검사(TLS·hostname)를 공유하는데도 그렇다 — §12.3, §17. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-spring-boot-starter-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-spring-boot-starter-c02.md deleted file mode 100644 index adc20f3..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-spring-boot-starter-c02.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-spring-boot-starter-c02 -title: 모든 거부가 타입이 아니라 키를 부른다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-spring-boot-starter-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-spring-boot-starter-c02 - file: ../../../final/evidence/rendered/messaging-spring-boot-starter-c02.svg -evidence: - - ../../../final/evidence/raw/messaging-spring-boot-starter-c02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-spring-boot-starter#L111 이다. -module: messaging-spring-boot-starter ---- - -# 모든 거부가 타입이 아니라 키를 부른다 - -MessagingConfigurationCompiler 가 닫는 것은 기능이 아니라 바인더의 부재다. 컴파일과 검증을 나눈 이유도 적혀 있다. - -## 관계 - -- **검증기는 발행이 아니라 주입이 강제다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -`MessagingConfigurationCompiler` 가 닫는 것은 기능이 아니라 바인더의 부재다. 컴파일과 검증을 나눈 이유도 적혀 있다. - -## MessagingConfigurationCompiler 참조 위치 - -:::evidence key="messaging-spring-boot-starter-c02" alt="코드베이스에서 MessagingConfigurationCompiler 를 검색한 출력 4줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingConfigurationCompiler 코드베이스 검색 — 4줄 · exit 0" zoom="true" -::: - -## 컴파일이 보는 것 - -객체 모델이 표현할 수 없는 것만 본다 — 목적지의 브로커가 존재하는지, 사후 처리 목적지가 선언되었는지, 보안 항목이 실재하는 브로커를 지키는지. 프로파일이 자체로 정합한지는 `DestinationProfileValidator` 의 질문이고 레지스트리 전체에 대해 던져진다. 그래서 설정으로 만든 프로파일과 빈으로 선언한 프로파일이 **같은 규칙**을 받는다. - -## 거부가 키를 부르는 이유 - -타입을 부르는 오류는 운영자가 고칠 줄을 알려 주지 않기 때문이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-spring-boot-starter-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-spring-boot-starter-c03.md deleted file mode 100644 index 3fe7210..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-spring-boot-starter-c03.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-spring-boot-starter-c03 -title: 종료 순서가 두 수명 주기의 phase 로 표현된다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-spring-boot-starter-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-spring-boot-starter-c03 - file: ../../../final/evidence/rendered/messaging-spring-boot-starter-c03.svg - - key: messaging-spring-boot-starter-c03-diagram - file: ../../../final/assets/diagrams/messaging-spring-boot-starter-c03.svg -evidence: - - ../../../final/evidence/raw/messaging-spring-boot-starter-c03.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-spring-boot-starter#L155 이다. -module: messaging-spring-boot-starter ---- - -# 종료 순서가 두 수명 주기의 phase 로 표현된다 - -SmartLifecycle 은 내림차순으로 멈추므로 중계가 먼저, 승인 차단과 배수가 다음이다. 두 클래스의 javadoc 이 서로를 근거로 든다 — 중계가 발행 중일 때 승인을 닫으면 그 회차의 행이 모호해지고, 그 모호함이야말로 배수가 없애려는 것이다. - -## 관계 - -- **검증기는 발행이 아니라 주입이 강제다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -`SmartLifecycle` 은 내림차순으로 멈추므로 중계가 먼저, 승인 차단과 배수가 다음이다. - -## 두 수명 주기로 나뉜 종료 - -:::evidence key="messaging-spring-boot-starter-c03-diagram" alt="중계 정지에서 승인 차단과 배수로 내림차순 phase 가 건너가고 승인 차단과 배수에서 빈 소멸로 두 수명 주기 종료가 건너간다" caption="두 수명 주기로 나뉜 종료" zoom="false" -::: - -두 클래스의 javadoc 이 서로를 근거로 든다 — 중계가 발행 중일 때 승인을 닫으면 그 회차의 행이 모호해지고, 그 모호함이야말로 배수가 없애려는 것이다. - -## SmartLifecycle 참조 위치 - -:::evidence key="messaging-spring-boot-starter-c03" alt="코드베이스에서 SmartLifecycle 를 검색한 출력 14줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="SmartLifecycle 코드베이스 검색 — 14줄 · exit 0" zoom="true" -::: - -## 브로커 연결을 쥔 빈이 마지막이다 - -`@Bean(destroyMethod = "close")` 인 생산자는 `Lifecycle` 이 아니므로 컨텍스트가 `destroyBeans()` 에 도달할 때, 즉 두 수명 주기가 모두 끝난 뒤에 닫힌다. 순서가 맞는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-spring-cloud-stream-bridge-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-spring-cloud-stream-bridge-c02.md deleted file mode 100644 index 6f11667..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-spring-cloud-stream-bridge-c02.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-spring-cloud-stream-bridge-c02 -title: Spring Cloud Stream 브리지가 Spring을 import하지 않는다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-spring-cloud-stream-bridge-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-spring-cloud-stream-bridge-c02 - file: ../../../final/evidence/rendered/messaging-spring-cloud-stream-bridge-c02.svg -evidence: - - ../../../final/evidence/raw/messaging-spring-cloud-stream-bridge-c02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-spring-cloud-stream-bridge#L86 이다. -module: messaging-spring-cloud-stream-bridge ---- - -# Spring Cloud Stream 브리지가 Spring을 import하지 않는다 - -선언된 것과 쓰이는 것이 다르다. grep -rn 'import dev.caskeleton.messaging.transport\|import org.springframework' → exit 1. - -## 관계 - -- **허용 의존 목록은 상한이므로 미사용을 잡지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **등록을 받는 컴포넌트는 해제도 제공한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **함께 읽히는 두 맵은 한 값으로 묶는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **에러 메시지가 지시하는 설정은 그 설정을 읽는 코드와 함께 존재해야 한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **한 개념의 등록 상태를 두 객체가 나눠 갖지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -**선언된 것과 쓰이는 것이 다르다.** `grep -rn 'import dev.caskeleton.messaging.transport\|import org.springframework'` → exit 1. - -## SpringCloudStreamPublisherBridge 참조 위치 - -:::evidence key="messaging-spring-cloud-stream-bridge-c02" alt="코드베이스에서 SpringCloudStreamPublisherBridge 를 검색한 출력 10줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="SpringCloudStreamPublisherBridge 코드베이스 검색 — 10줄 · exit 0" zoom="true" -::: - -## 바인더 접촉면이 두 함수형 인터페이스뿐이다 - -`SpringCloudStreamPublisherBridge.ChannelSend`와 `SpringCloudStreamConsumerBridge.BridgedHandler`. javadoc이 그 목적을 적는다 — "isolated so the bridge is testable without a binder". 즉 **`spring-context` 의존은 실제 통합 코드가 있어야 필요했을 것**인데 그 코드가 없다. §12.4. - -## 삼중 정합 - -나가는 것이 없다 — 어떤 leaf의 `allowed_dependencies`에도 이 leaf가 없고 starter 목록에도 없다. 런타임 배선 없음, `runtime_memberships: []`, bean 없음. **소비자 0 · membership `[]` · 조립 0의 삼중 정합** — `messaging-kafka-share-experimental`·`messaging-schema-avro`와 같은 상태다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-spring-cloud-stream-bridge-c04.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-spring-cloud-stream-bridge-c04.md deleted file mode 100644 index 09114c1..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-spring-cloud-stream-bridge-c04.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-spring-cloud-stream-bridge-c04 -title: send가 true를 반환해도 AMBIGUOUS다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-spring-cloud-stream-bridge-c04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-spring-cloud-stream-bridge-c04 - file: ../../../final/evidence/rendered/messaging-spring-cloud-stream-bridge-c04.svg -evidence: - - ../../../final/evidence/raw/messaging-spring-cloud-stream-bridge-c04.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-spring-cloud-stream-bridge#L309 이다. -module: messaging-spring-cloud-stream-bridge ---- - -# send가 true를 반환해도 AMBIGUOUS다 - -검증: validator.validate(profile, bindingName, extendedProperties, enabled) → guard 4검사 → 이름 → 속성 8개 → production → BindingCapabilityReport.bridged(...) 발행: bridge.bindPublisher(dest, binding) → bridge.publish(dest, payload, headers) → send.send(...) → true면 AMBIGUOUS, false면 REJECTED 수신: consumerBridge.register(dest, binding, handler) → 바인더가 dispatch(binding, payload, headers) → handler.handle(...) (예외 그대로 전파) - -## 관계 - -- **허용 의존 목록은 상한이므로 미사용을 잡지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **등록을 받는 컴포넌트는 해제도 제공한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **함께 읽히는 두 맵은 한 값으로 묶는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **에러 메시지가 지시하는 설정은 그 설정을 읽는 코드와 함께 존재해야 한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **한 개념의 등록 상태를 두 객체가 나눠 갖지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -세 경로가 있다. - -**검증:** `validator.validate(profile, bindingName, extendedProperties, enabled)` → guard 4검사 → 이름 → 속성 8개 → production → `BindingCapabilityReport.bridged(...)` - -**발행:** `bridge.bindPublisher(dest, binding)` → `bridge.publish(dest, payload, headers)` → `send.send(...)` → true면 `AMBIGUOUS`, false면 `REJECTED` - -**수신:** `consumerBridge.register(dest, binding, handler)` → 바인더가 `dispatch(binding, payload, headers)` → `handler.handle(...)` (예외 그대로 전파) - -## 이 기록이 다루는 범위 - -:::evidence key="messaging-spring-cloud-stream-bridge-c04" alt="코드베이스에서 파일 목록을 만든 출력 6줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 6줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-spring-cloud-stream-bridge-c05.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-spring-cloud-stream-bridge-c05.md deleted file mode 100644 index 10d6df4..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-spring-cloud-stream-bridge-c05.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-spring-cloud-stream-bridge-c05 -title: messaging family에서 예외 어휘가 가장 일관된 leaf -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-spring-cloud-stream-bridge-c05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-spring-cloud-stream-bridge-c05 - file: ../../../final/evidence/rendered/messaging-spring-cloud-stream-bridge-c05.svg -evidence: - - ../../../final/evidence/raw/messaging-spring-cloud-stream-bridge-c05.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-spring-cloud-stream-bridge#L319 이다. -module: messaging-spring-cloud-stream-bridge ---- - -# messaging family에서 예외 어휘가 가장 일관된 leaf - -아홉 개의 구성 실패가 전부 MessagingConfigurationException + 안정 코드다. 이 저장소 messaging family에서 예외 어휘가 가장 일관된 leaf다 — messaging-security(두 계층 혼용)·messaging-kafka-share-experimental(두 계층 혼용)·messaging-policy(검증기가 IllegalArgumentException)와 대비된다. - -## 관계 - -- **허용 의존 목록은 상한이므로 미사용을 잡지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **등록을 받는 컴포넌트는 해제도 제공한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **함께 읽히는 두 맵은 한 값으로 묶는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **에러 메시지가 지시하는 설정은 그 설정을 읽는 코드와 함께 존재해야 한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **한 개념의 등록 상태를 두 객체가 나눠 갖지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -**아홉 개의 구성 실패가 전부 `MessagingConfigurationException` + 안정 코드다.** `messaging-security`(두 계층 혼용)·`messaging-kafka-share-experimental`(두 계층 혼용)·`messaging-policy`(검증기가 `IllegalArgumentException`)와 대비된다. - -## MessagingConfigurationException 참조 위치 - -:::evidence key="messaging-spring-cloud-stream-bridge-c05" alt="코드베이스에서 MessagingConfigurationException 를 검색한 출력 24줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingConfigurationException 코드베이스 검색 — 24줄 · exit 0" zoom="true" -::: - -## 발행 결과는 예외가 아니라 값이다 - -`messaging-core-api`의 설계를 그대로 따른다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-testkit-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-testkit-c01.md deleted file mode 100644 index dfe3200..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-testkit-c01.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-testkit-c01 -title: 지원한다는 단어의 정의를 코드로 못 박는 곳 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-testkit-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-testkit-c01 - file: ../../../final/evidence/rendered/messaging-testkit-c01.svg - - key: messaging-testkit-c01-diagram - file: ../../../final/assets/diagrams/messaging-testkit-c01.svg -evidence: - - ../../../final/evidence/raw/messaging-testkit-c01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-testkit#L68 이다. -module: messaging-testkit ---- - -# 지원한다는 단어의 정의를 코드로 못 박는 곳 - -이 리프는 "지원한다(supported)"라는 단어의 정의를 코드로 못 박는 곳이다. 플랫폼의 다른 어떤 리프도 "Kafka 는 Stable 이다" 를 주장하지 않는다. - -## 본문 - - - -이 리프는 **"지원한다(supported)"라는 단어의 정의를 코드로 못 박는 곳**이다. 플랫폼의 다른 어떤 리프도 "Kafka 는 Stable 이다" 를 주장하지 않는다. 그 주장은 여기에만 있고, 여기서만 검증된다. - -## 이 리프가 소유한 세 층 - -:::evidence key="messaging-testkit-c01-diagram" alt="공유 계약과 결함 시나리오 증거와 지원 등급이 messaging-testkit 안에 놓이고 어댑터 구현과 어댑터 실행이 바깥에 빗금으로 놓인다" caption="이 리프가 소유한 세 층" zoom="false" -::: - -1. **공유 계약** (`MessagingAdapterContract` + `MessagingAdapterHarness` + `ContractMessage`/`ContractAssertions`/`ObservedDelivery`/`HandleOutcome`/`FaultController`) — 브로커가 무엇이든 똑같이 답해야 하는 7가지 행동. -2. **결함 시나리오와 그 증거** (`NetworkFaultScenario` + `BrokerCertificationEvidence` + `CertifiedEvidence` + `BrokerFailureMatrix`) — 어떤 장애를 실제로 돌려 봤는가. -3. **지원 등급** (`CompatibilityMatrix`) — 위 두 층의 결과로 어댑터가 얻는 등급. - -## MessagingAdapterContract 참조 위치 - -:::evidence key="messaging-testkit-c01" alt="코드베이스에서 MessagingAdapterContract 를 검색한 출력 8줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingAdapterContract 코드베이스 검색 — 8줄 · exit 0" zoom="true" -::: - -## 구현하지도 실행하지도 않는다 - -하니스 구현은 각 어댑터 리프의 `src/test` 에 있다(`KafkaContractHarness`, `RabbitContractHarness`). 이 리프가 가진 유일한 하니스는 `InMemoryMessagingHarness` 이며 `src/test` 에 있고, 그 javadoc 이 스스로 선을 긋는다. - -## 접근 제어자로도 강제되는 문장 - -`final` + package-private + `private` 생성자 + 정적 팩토리. "프로덕션 어댑터가 되어서는 안 된다" 는 문장이 접근 제어자로도 강제되어 있다. `src/main` 이 아니라 `src/test` 에 둔 것도 같은 결정이다 — 다른 리프의 test 클래스패스에 올라가는 것은 `src/main` 뿐이므로, 이 하니스는 물리적으로 이 리프 밖으로 나갈 수 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-testkit-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-testkit-c02.md deleted file mode 100644 index cf59ef5..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-testkit-c02.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-testkit-c02 -title: 상속하는 쪽이 컴파일되려면 전부 전이되어야 한다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-testkit-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-testkit-c02 - file: ../../../final/evidence/rendered/messaging-testkit-c02.svg -evidence: - - ../../../final/evidence/raw/messaging-testkit-c02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-testkit#L99 이다. -module: messaging-testkit ---- - -# 상속하는 쪽이 컴파일되려면 전부 전이되어야 한다 - -여섯 개가 전부 api 다. implementation 이 하나도 없다. - -## 본문 - - - -여섯 개가 전부 `api` 다. `implementation` 이 하나도 없다. - -## MessagingAdapterContract 참조 위치 - -:::evidence key="messaging-testkit-c02" alt="코드베이스에서 MessagingAdapterContract 를 검색한 출력 8줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingAdapterContract 코드베이스 검색 — 8줄 · exit 0" zoom="true" -::: - -## 이 리프에서는 그것이 옳은 선택이다 - -`MessagingAdapterContract` 는 `@Test` 를 **자기 시그니처에** 달고 있고(`MessagingAdapterContract.java:28`), `ContractAssertions` 는 AssertJ 를 반환 타입 없이 쓰지만 상속받는 쪽이 같은 AssertJ 를 봐야 하며, `MessagingAdapterHarness.publish` 는 `PublishResult`(core-api)를, `ContractMessage` 는 `EncodedMessage`(schema-api)를 **공개 시그니처에** 노출한다. - -## 빈 membership 의 뜻이 다른 리프들과 정반대다 - -`runtime_memberships: []` 이지만 §12.1 의 판정은 다른 `[]` 리프들과 정반대다. 추가로 `messaging-kafka/build.gradle:91` 이 이 리프의 **리소스 파일 경로를 문자열로 참조**한다(§4.3). - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-transport-spi-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-transport-spi-c02.md deleted file mode 100644 index e9dc68c..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-messaging-transport-spi-c02.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-transport-spi-c02 -title: 세 leaf의 타입이 public 시그니처에 직접 등장한다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-transport-spi-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-transport-spi-c02 - file: ../../../final/evidence/rendered/messaging-transport-spi-c02.svg -evidence: - - ../../../final/evidence/raw/messaging-transport-spi-c02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-transport-spi#L90 이다. -module: messaging-transport-spi ---- - -# 세 leaf의 타입이 public 시그니처에 직접 등장한다 - -들어오는 것: messaging-core-api(api), messaging-schema-api(api), messaging-policy(api). 셋 다 api인 이유는 세 leaf의 타입이 이 leaf의 public 시그니처에 직접 등장하기 때문이다 — TransportPublishRequest가 DestinationProfile(policy)·MessageEnvelope(core-api)·EncodedMessage(schema-api)를 필드로 갖는다. - -## 관계 - -- **멱등 종료를 보장하는 컴포넌트는 종료 이후의 등록도 정의한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 개념의 sentinel은 계층을 넘어 하나로 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -들어오는 것은 `messaging-core-api`(api), `messaging-schema-api`(api), `messaging-policy`(api)다. 셋 다 `api`인 이유는 세 leaf의 타입이 이 leaf의 public 시그니처에 직접 등장하기 때문이다 — `TransportPublishRequest`가 `DestinationProfile`(policy)·`MessageEnvelope`(core-api)·`EncodedMessage`(schema-api)를 필드로 갖는다. - -## TransportPublishRequest 참조 위치 - -:::evidence key="messaging-transport-spi-c02" alt="코드베이스에서 TransportPublishRequest 를 검색한 출력 4줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="TransportPublishRequest 코드베이스 검색 — 4줄 · exit 0" zoom="true" -::: - -## 나가는 쪽 - -`messaging-runtime-core`, `messaging-kafka`, `messaging-kafka-share-experimental`, `messaging-rabbit`, `messaging-pulsar-experimental`, `messaging-nats-experimental`, `messaging-admin-runtime`, `messaging-spring-cloud-stream-bridge`, `messaging-spring-boot-starter`, `messaging-testkit`. 런타임 편입은 starter closure를 통해서다. 이 leaf 자체는 bean을 만들지 않는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-shared-contract-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-shared-contract-c01.md deleted file mode 100644 index 314db1e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/delivery-and-settlement-models/concept/concept-shared-contract-c01.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -kind: CONCEPT -slug: shared-contract-c01 -title: factory는 검증하고 raw 생성자는 검증하지 않는다 -topic: delivery-and-settlement-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:shared-contract-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: shared-contract-c01 - file: ../../../final/evidence/rendered/shared-contract-c01.svg - - key: shared-contract-c01-diagram - file: ../../../final/assets/diagrams/shared-contract-c01.svg -evidence: - - ../../../final/evidence/raw/shared-contract-c01.txt -source: - - 원본 분석 절은 final/document.md#a02#L51 이다. -module: shared-contract ---- - -# factory는 검증하고 raw 생성자는 검증하지 않는다 - -ApiErrorCode는 code/category/httpStatus/retryable의 최소 표면을 제공하고 OperationalError가 registry mirror 역할을 한다. Category는 VALIDATION, AUTH, AUTHZ, NOT_FOUND, CONFLICT, RATE_LIMIT, TRANSIENT_DEPENDENCY, PERMANENT_DEPENDENCY, DATA_INTEGRITY, INTERNAL의 10개 값으로 고정되어 있으며 테스트가 정확한 vocabulary를 pin 한다. - -## 본문 - - - -`ApiErrorCode`는 code/category/httpStatus/retryable의 최소 표면을 제공하고 `OperationalError`가 registry mirror 역할을 한다. `Category`는 VALIDATION, AUTH, AUTHZ, NOT_FOUND, CONFLICT, RATE_LIMIT, TRANSIENT_DEPENDENCY, PERMANENT_DEPENDENCY, DATA_INTEGRITY, INTERNAL의 10개 값으로 고정되어 있으며 테스트가 정확한 vocabulary를 pin 한다. - -## ApiErrorCode 참조 위치 - -:::evidence key="shared-contract-c01" alt="코드베이스에서 ApiErrorCode 를 검색한 출력 16줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ApiErrorCode 코드베이스 검색 — 16줄 · exit 0" zoom="true" -::: - -## retryable 이 category 에서 자동으로 나오지 않는다 - -`OperationalErrorTest`는 단순 enum 존재보다 category × retryable 의미를 강하게 검증한다. deterministic VALIDATION/AUTHZ/NOT_FOUND는 retryable=false이고, transient INTERNAL은 기본적으로 retryable=true이되 deploy-time/configuration/terminal 상태인 일부 code는 명시적 예외로 false다. `AUTH_KID_UNKNOWN`은 key rotation 중 JWKS refresh 가능성을 이유로 AUTH 중 유일한 retryable case로 pin 되어 있다. upstream 4xx 전체를 permanent/non-retryable로 분류하면서 408/429의 의미 차이가 남는다는 점은 source comment와 README가 이미 known edge로 기록한다. - -## 어떤 예외가 코드를 나르는가 - -`DependencyFailureException`과 `PersistenceFailureException`은 `ApiErrorCarrier`를 통해 transport adapter에 stable error code를 전달하면서 raw cause/diagnostic message를 server-side 정보로 남긴다. `AdapterDisabledException`은 carrier를 구현하지 않고 별도 mapping 대상이다. - -## 검증이 걸리는 자리 - -:::evidence key="shared-contract-c01-diagram" alt="factory 경로에 배타성 검증과 문서의 정상 shape 가 놓이고 raw 생성자에 배타성 미검증과 invalid shape 가능이 빗금으로 놓인다" caption="검증이 걸리는 자리" zoom="false" -::: - -`Envelope`, `BulkEnvelope`, `ResponseMeta`, `PageMeta`, `Operation`은 framework-neutral record/factory로 API shape를 전달한다. `Envelope.ok/failure`, `BulkEnvelope.allOk/partial`, `Operation.pending/succeeded/failed` factory는 문서의 정상 shape를 생성하고 테스트도 이 factory path를 검증한다. 그러나 canonical record constructor 자체는 success/data/error의 배타성, operation status와 result/error의 조합, pagination 범위 등을 검증하지 않는다. - -## 그래서 이 규칙의 성격이 다르다 - -rate-limit value object처럼 intrinsic constructor invariant가 아니라 factory/adapter usage contract다. 현재 source와 test가 일치하므로 즉시 결함으로 분류하지 않지만, raw constructor가 외부 module에 public인 만큼 invalid shape 생성 가능성은 P1 hardening 후보로 남는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/drain-and-shutdown-ordering/case/case-reject-new-admission-that-rejects-nothing.md b/docs/clean-architecture-backend-template/tech-log-studio/drain-and-shutdown-ordering/case/case-reject-new-admission-that-rejects-nothing.md deleted file mode 100644 index f4dd492..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/drain-and-shutdown-ordering/case/case-reject-new-admission-that-rejects-nothing.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -kind: CASE -slug: reject-new-admission-that-rejects-nothing -title: 새 승인을 거절한다는 메서드가 단계만 기록하고 아무것도 거절하지 않는다 -topic: drain-and-shutdown-ordering -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:reject-new-admission-that-rejects-nothing -evidenceCapturedOn: 2026-09-01 -assets: - - key: reject-new-admission-that-rejects-nothing - file: ../../../final/evidence/rendered/reject-new-admission-that-rejects-nothing.svg -evidence: - - ../../../final/evidence/raw/reject-new-admission-that-rejects-nothing.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-admin §17.1 이다. ---- - -# 새 승인을 거절한다는 메서드가 단계만 기록하고 아무것도 거절하지 않는다 - -배수 조정자의 새 승인 거절 메서드가 현재 단계를 기록하는 것이 전부다. 승인을 실제로 판정하는 제어기는 다른 리프에 있고 이 단계를 읽지 않는다. - -## 관계 - -- **8단계 종료 순서 계약과 실제 종료 경로** - 이 사례가 속한 구조다. -- **배수를 시작한 뒤에도 한 서비스가 다시 SERVING 이 될 수 있다** - 같은 리프에서 배수를 무력하게 만드는 다른 절반이다. -- **검증기는 발행이 아니라 주입이 강제다** - 같은 계열의 규칙이다. - -## 문제 - -배수의 첫 단계는 새 요청을 받지 않는 것이다. 그래야 남은 작업이 줄어들고 드레인이 끝난다. - -배수 조정자가 그 단계를 메서드로 갖는다. 이름이 새 승인을 거절한다고 말한다. - -## 결론 - -본문이 현재 단계를 기록하는 것이 전부다. - -승인을 실제로 판정하는 것은 다른 리프의 승인 제어기이고, 그 제어기는 이 조정자의 단계를 읽지 않는다. 두 리프 사이에 참조가 없다. - -그래서 배수를 시작해도 새 요청은 계속 승인된다. 드레인이 기다리는 작업 수가 줄지 않고, 드레인 마감이 지나면 아직 처리 중인 요청을 두고 종료한다. - -같은 리프의 헬스 레지스트리가 배수 중에 서비스를 다시 서빙으로 돌릴 수 있다는 것과 겹치면 결론이 하나로 모인다. 배수라는 절차가 상태 기록으로만 존재하고 트래픽에 대해서는 아무 효과가 없다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Gradle : 9.0.0 -확인 방식 : 메서드 본문 확인과 두 리프 사이의 타입 참조 검색 -소스 수정 : x - -## 재현 조건 - -1. 배수 조정자의 새 승인 거절 메서드 본문을 읽는다. -2. 그 메서드가 바꾸는 상태를 읽는 코드를 저장소에서 찾는다. -3. 승인 제어기가 배수 단계를 참조하는지 확인한다. - -## 본문 - - - -배수의 첫 단계는 새 요청을 받지 않는 것이다. `rejectNewAdmission()` 은 이름이 그것을 말하고, 본문은 현재 단계를 기록하는 것이 전부다. - -## 메서드 본문이 하는 일 - -:::evidence key="reject-new-admission-that-rejects-nothing" alt="분석 문서 final/document.md#a20-grpc-admin 에서 이 기록의 근거 절을 그대로 잘라낸 18줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-admin 발췌 — 18줄" zoom="true" -::: - -## 승인을 판정하는 곳이 이 단계를 읽지 않는다 - -승인 제어기는 다른 리프에 있고 이 조정자의 단계를 읽지 않는다. 그래서 배수를 시작해도 새 요청은 계속 승인된다. - -## 헬스 레지스트리 쪽과 겹치면 - -배수 중에 서비스를 다시 SERVING 으로 돌릴 수 있다는 것과 합쳐져, 배수라는 절차 전체가 상태 기록으로만 존재하고 트래픽에 대해서는 아무 효과가 없다. - -## 확인하지 못한 것 - -배수 중 승인 시도를 실행으로 재현하지 않았다. 두 리프 사이에 참조가 없다는 것으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/drain-and-shutdown-ordering/case/case-the-last-step-of-secret-erasure-is-not-wired.md b/docs/clean-architecture-backend-template/tech-log-studio/drain-and-shutdown-ordering/case/case-the-last-step-of-secret-erasure-is-not-wired.md deleted file mode 100644 index 6ebfb04..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/drain-and-shutdown-ordering/case/case-the-last-step-of-secret-erasure-is-not-wired.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -kind: CASE -slug: the-last-step-of-secret-erasure-is-not-wired -title: 비밀을 힙에서 지우는 마지막 단계가 종료 경로에 연결되지 않았다 -topic: drain-and-shutdown-ordering -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:the-last-step-of-secret-erasure-is-not-wired -evidenceCapturedOn: 2026-09-01 -assets: - - key: the-last-step-of-secret-erasure-is-not-wired - file: ../../../final/evidence/rendered/the-last-step-of-secret-erasure-is-not-wired.svg -evidence: - - ../../../final/evidence/raw/the-last-step-of-secret-erasure-is-not-wired.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-security §17 이다. ---- - -# 비밀을 힙에서 지우는 마지막 단계가 종료 경로에 연결되지 않았다 - -자격증명 레지스트리의 전체 소거 메서드가 종료용이라고 javadoc 에 적혀 있고 호출자가 저장소에 없다. 리프 전체가 비밀을 힙에 남기지 않기 위해 설계됐다. - -## 관계 - -- **8단계 종료 순서 계약과 실제 종료 경로** - 이 사례가 속한 구조다. -- **순서 계약을 구현하는 것이 없고, 그것을 검증한다는 테스트는 enum 선언 순서만 본다** - 같은 계약이 구현되지 않았다는 사실의 구체적 결과 하나다. -- **검증기는 발행이 아니라 주입이 강제다** - 같은 계열의 규칙이다. - -## 문제 - -이 리프는 자격증명을 다루는 방식 전체가 하나의 목적을 향한다. 비밀을 문자열이 아니라 문자 배열로 들고, 쓰고 나면 그 배열을 지우고, 회전할 때 옛 값을 즉시 소거한다. - -그 규율의 마지막 단계가 종료다. 프로세스가 끝날 때 아직 들고 있는 자격증명을 전부 지우는 메서드가 있다. - -## 결론 - -그 메서드를 부르는 곳이 없다. - -javadoc 은 용도를 명확히 적는다. - -> Clears every held credential, for shutdown - -호출자를 저장소 전역에서 검색하면 선언과 테스트만 나온다. - -프로세스가 끝나면 힙도 사라지므로 사소해 보인다. 그러나 이 통제가 겨냥하는 상황이 정확히 그 전제가 성립하지 않는 경우다. 종료가 느려서 힙이 오래 남거나, 종료 중에 덤프가 뜨거나, 컨테이너가 정지 상태로 유지되는 경우다. - -8단계 종료 계약에 자격증명 소거에 해당하는 단계가 있고 그 단계를 수행하는 코드가 없다. 그러므로 이 사례는 계약이 구현되지 않았다는 사실의 구체적 결과 하나다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Gradle : 9.0.0 -확인 방식 : 전체 소거 메서드의 호출자 전수 검색 -소스 수정 : x - -## 재현 조건 - -1. 자격증명 레지스트리에서 전체 소거 메서드와 그 javadoc 을 확인한다. -2. 그 메서드 이름으로 저장소를 검색해 호출자를 센다. -3. 종료 수명주기 클래스가 그것을 부르는지 확인한다. - -## 본문 - - - -이 리프 전체가 "비밀이 힙에 남지 않게 한다" 를 목적으로 설계됐다 — 자격증명을 `char[]` 로 들고, 사용 후 `clear()` 하고, 회전 시 즉시 소거한다. - -## 리프가 설계된 목적 - -:::evidence key="the-last-step-of-secret-erasure-is-not-wired" alt="분석 문서 final/document.md#a19-messaging-security 에서 이 기록의 근거 절을 그대로 잘라낸 18줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a19-messaging-security 발췌 — 18줄" zoom="true" -::: - -## 마지막 단계에 호출자가 없다 - -`clearAll()` 의 javadoc 이 "Clears every held credential, for shutdown" 이라고 적고, 호출자가 저장소에 없다. - -## 사소해 보이지만 겨냥한 상황이 그것이다 - -프로세스가 끝나면 힙도 사라지지만, 종료가 느리거나 힙 덤프가 뜨는 경우가 정확히 이 통제가 겨냥하는 상황이다. 8단계 종료 계약에 자격증명 소거 단계가 있고 그 단계를 수행하는 코드가 없다는 점에서, 이 사례는 계약이 구현되지 않았다는 사실의 구체적 결과 하나다. - -## 확인하지 못한 것 - -힙 덤프로 잔존을 확인하지 않았다. 호출자 부재로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/drain-and-shutdown-ordering/concept/concept-the-eight-phase-shutdown-contract.md b/docs/clean-architecture-backend-template/tech-log-studio/drain-and-shutdown-ordering/concept/concept-the-eight-phase-shutdown-contract.md deleted file mode 100644 index 919b4ac..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/drain-and-shutdown-ordering/concept/concept-the-eight-phase-shutdown-contract.md +++ /dev/null @@ -1,95 +0,0 @@ ---- -kind: CONCEPT -slug: the-eight-phase-shutdown-contract -title: 8단계 종료 순서 계약과 실제 종료 경로 -topic: drain-and-shutdown-ordering -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:the-eight-phase-shutdown-contract -evidenceCapturedOn: 2026-09-01 -assets: - - key: the-eight-phase-shutdown-contract - file: ../../../final/evidence/rendered/the-eight-phase-shutdown-contract.svg - - key: the-eight-phase-shutdown-contract-diagram - file: ../../../final/assets/diagrams/the-eight-phase-shutdown-contract.svg -evidence: - - ../../../final/evidence/raw/the-eight-phase-shutdown-contract.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-transport-spi §17 이다. ---- - -# 8단계 종료 순서 계약과 실제 종료 경로 - -전송 SPI 가 종료를 8단계로 선언하고 그 순서를 계약이라고 못 박는다. 실제 종료는 스타터의 다른 클래스가 하고 8단계 중 셋만 명시적으로 수행한다. - -## 관계 - -- **순서 계약을 구현하는 것이 없고, 그것을 검증한다는 테스트는 enum 선언 순서만 본다** - 이 계약이 코드에 닿지 않는다는 사례다. -- **새 승인을 거절한다는 메서드가 단계만 기록하고 아무것도 거절하지 않는다** - 단계 하나가 이름만 남은 사례다. -- **비밀을 힙에서 지우는 마지막 단계가 종료 경로에 연결되지 않았다** - 단계 하나가 통째로 빠진 사례다. - -## 본문 - - - -`MessagingLifecycle` 이 종료를 8단계로 선언하고 javadoc 이 그 순서를 계약이라고 못 박는다 — "The order in ShutdownPhase is the contract, not an implementation detail. Each adapter implements the phases; none of them chooses the order." - -## 순서가 계약인 이유 - -각 단계가 앞 단계의 결과 위에 서기 때문이다. 새 승인을 멈추기 전에 드레인하면 드레인이 끝나지 않고, 핸들러가 끝나기 전에 플러시하면 그 핸들러가 만들 정착을 잃는다. - -## MessagingLifecycle 참조 위치 - -:::evidence key="the-eight-phase-shutdown-contract" alt="코드베이스에서 MessagingLifecycle 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingLifecycle 코드베이스 검색 — 2줄 · exit 0" zoom="true" -::: - -## 선언과 실제의 거리 - -:::evidence key="the-eight-phase-shutdown-contract-diagram" alt="선언된 단계에 승인 정지와 드레인과 플러시 및 나머지가 놓이고 실제 수행에서 마지막 칸만 빗금이다" caption="선언과 실제의 거리" zoom="false" -::: - -실제 종료는 스타터의 `MessagingShutdownLifecycle` 이 하고 8단계 중 셋만 명시적으로 수행한다 — 승인 정지 · 새 핸들러 정지 · 드레인. 나머지는 Spring 의 `getPhase()` 정수와 빈 소멸 순서에 위임되거나 명시 단계가 없다. - -## 순서를 결정하는 것 - -`ShutdownPhase` 가 아니다. - -:::note - -실제 종료 시퀀스를 부팅해 관측하지 않았다. 두 클래스의 코드로 판정했다 - -::: - -## 왜 순서가 계약인가 - -각 단계가 앞 단계의 결과 위에 선다. - -새 승인을 멈추기 전에 드레인을 시작하면 드레인이 끝나지 않는다. 계속 들어오는 요청이 남은 작업 수를 다시 늘린다. - -핸들러가 끝나기 전에 플러시하면 그 핸들러가 만들 정착을 잃는다. 아직 만들어지지 않은 것은 플러시할 수 없다. - -인터페이스 javadoc 이 그 성질을 직접 적는다. - -> The order in ShutdownPhase is the contract, not an implementation detail. Each adapter implements the phases; none of them chooses the order. - -어댑터가 각 단계를 구현하고, 순서는 어느 어댑터도 고르지 않는다는 뜻이다. - -## 실제로 무엇이 도는가 - -종료를 수행하는 것은 스타터의 종료 수명주기 클래스다. 그 클래스가 명시적으로 하는 것은 셋이다. 승인 정지, 새 핸들러 정지, 드레인. - -나머지는 Spring 의 단계 정수와 빈 소멸 순서에 위임되거나, 대응하는 명시 단계가 없다. - -즉 순서를 결정하는 것은 선언된 단계 열거형이 아니라 프레임워크의 소멸 순서다. - -## 두 이름의 거리 - -선언된 계약과 실행되는 절차가 이름을 공유하지 않는다. 계약 쪽은 여덟 단계를 갖고 구현체가 없으며, 실행 쪽은 세 동작을 갖고 그 동작을 단계 이름으로 부르지 않는다. - -그래서 계약을 읽은 사람과 실행을 읽은 사람이 같은 시스템에 대해 다른 그림을 갖는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/drain-and-shutdown-ordering/reference/reference-a-declaration-order-test-is-a-gate-only-if-something-reads-that-order.md b/docs/clean-architecture-backend-template/tech-log-studio/drain-and-shutdown-ordering/reference/reference-a-declaration-order-test-is-a-gate-only-if-something-reads-that-order.md deleted file mode 100644 index d3665df..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/drain-and-shutdown-ordering/reference/reference-a-declaration-order-test-is-a-gate-only-if-something-reads-that-order.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -kind: REFERENCE -slug: a-declaration-order-test-is-a-gate-only-if-something-reads-that-order -title: 선언 순서를 단언하는 테스트는 그 순서를 읽는 코드가 있을 때만 게이트다 -topic: drain-and-shutdown-ordering -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:a-declaration-order-test-is-a-gate-only-if-something-reads-that-order -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# 선언 순서를 단언하는 테스트는 그 순서를 읽는 코드가 있을 때만 게이트다 - -## 목적 - -열거형이나 목록의 선언 순서를 단언하는 테스트가 무엇을 지키는지 정확히 판정한다. - -## 규칙 - -1. 이 열거형의 값 목록이나 서수를 읽는 production 코드를 찾는다 - 없으면 그 테스트가 지키는 것은 타이핑 순서다. - -2. 테스트 이름과 설명 메시지가 무엇을 주장하는지 읽는다 - 시스템 동작을 서술하는데 단언이 선언 순서면 그 둘이 어긋나 있다. - -3. 순서를 실제로 결정하는 것이 무엇인지 찾는다 - 프레임워크의 소멸 순서나 다른 클래스의 절차일 수 있다. 그것이 진짜 계약이다. - -4. 순서 자체가 문서인 경우를 구분한다 - 그때는 테스트 이름이 문서와의 일치를 말해야 하고 시스템 동작을 주장하면 안 된다. - -## 적용 조건 - -순서가 의미를 갖는 모든 열거형과 목록. 종료 단계, 필터 체인, 실패 번역 사슬, 마이그레이션 순서. - -## 예외 - -선언 순서가 곧 실행 순서인 구조가 있다. 그 구조를 만드는 코드가 명시적으로 값 목록을 순회하면 이 규칙은 만족된다. - -## 예시 - -종료 단계 열거형을 순서대로 단언하는 다섯 테스트가 있다. 그 열거형의 외부 소비자가 없고, 실제 종료 순서는 스타터의 다른 클래스와 프레임워크 소멸 순서가 정한다. - -테스트를 통과시키려면 상수를 그 순서대로 적으면 되고, 시스템 동작은 그것과 무관하게 유지된다. - -## 관계 - -- **순서 계약을 구현하는 것이 없고, 그것을 검증한다는 테스트는 enum 선언 순서만 본다** - 이 규칙을 만든 사례다. -- **빠뜨림이 통과가 되는 게이트는 게이트가 아니다** - 같은 계열의 규칙이다. -- **이름이 검사한다고 말하는 것을 본문이 검사하지 않는 테스트 다섯** - 이름과 본문이 갈리는 다른 형태들이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/drift-direction/reference/reference-numbers-in-docs-should-be-derived.md b/docs/clean-architecture-backend-template/tech-log-studio/drift-direction/reference/reference-numbers-in-docs-should-be-derived.md deleted file mode 100644 index 7d9b3a9..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/drift-direction/reference/reference-numbers-in-docs-should-be-derived.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -kind: REFERENCE -slug: numbers-in-docs-should-be-derived -title: 문서의 수치는 세지 말고 파생하거나 게이트로 붙든다 -topic: drift-direction -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:numbers-in-docs-should-be-derived -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# 문서의 수치는 세지 말고 파생하거나 게이트로 붙든다 - -## 목적 - -문서에 손으로 적은 수가 코드보다 뒤처져, 그 수를 근거로 한 서술 전체가 언제 것인지 알 수 없게 되는 것을 막는다. - -## 규칙 - -1. 셀 수 있는 것은 세지 말고 파생한다 - 모듈 수 어댑터 수 규칙 수는 레지스트리나 소스 트리에서 계산할 수 있다. - -2. 파생할 수 없으면 게이트로 붙든다 - 문서의 수와 실제의 수를 비교하는 검사를 만든다. 검사가 없으면 그 수는 작성 시점의 스냅숏이다. - -3. 같은 수가 여러 문서에 있으면 출처를 하나로 만든다 - 흩어진 수는 한 번에 갱신되지 않는다. - -4. 수를 근거로 한 서술을 함께 표시한다 - 그 수가 틀리면 그 서술도 틀린다. 어느 서술이 그 수에 의존하는지 알 수 있어야 한다. - -## 적용 조건 - -모듈 수 리프 수 규칙 수 지원 버전 수처럼 코드에서 셀 수 있는 모든 수치 - -지원 매트릭스와 아키텍처 개요 문서 - -## 예외 - -운영상의 가정으로 정한 임계값은 세는 수가 아니다. 그런 값은 출처가 판단이므로 근거를 적는 것으로 충분하다. - -## 예시 - -여러 문서가 리프 수를 19 로 적고 레지스트리는 62 다. 빌드 설정에 그 수를 검사하는 코드가 없다. - -반대 사례로, 오류 코드 태그의 카디널리티 상한은 오류 코드 정의 파일의 행 수와 동기화된다고 상수 주석에 적혀 있다. - -## 관계 - -- **여러 문서가 19개 리프라고 적고 레지스트리는 62다** - 이 규칙을 만든 사례다. -- **빠뜨림이 통과가 되는 게이트는 게이트가 아니다** - 두 번째 규칙이 기대는 상위 규칙이다. -- **과대 진술 문서를 과소보다 먼저 고친다** - 드리프트가 이미 생겼을 때의 우선순위다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/duplicate-mechanisms/case/case-the-same-repository-bound-a-decision-once-and-not-the-other-time.md b/docs/clean-architecture-backend-template/tech-log-studio/duplicate-mechanisms/case/case-the-same-repository-bound-a-decision-once-and-not-the-other-time.md deleted file mode 100644 index 40bb70d..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/duplicate-mechanisms/case/case-the-same-repository-bound-a-decision-once-and-not-the-other-time.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -kind: CASE -slug: the-same-repository-bound-a-decision-once-and-not-the-other-time -title: 같은 저장소가 "결정을 그 결정이 판정한 대상에 묶는 것"을 한 번은 맞게, 한 번은 틀리게 썼다 -topic: duplicate-mechanisms -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:the-same-repository-bound-a-decision-once-and-not-the-other-time -evidenceCapturedOn: 2026-09-01 -assets: - - key: the-same-repository-bound-a-decision-once-and-not-the-other-time - file: ../../../final/evidence/rendered/the-same-repository-bound-a-decision-once-and-not-the-other-time.svg -evidence: - - ../../../final/evidence/raw/the-same-repository-bound-a-decision-once-and-not-the-other-time.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-codegen §17.4 · final/document.md#a20-grpc-advanced-bootstrap(확인된 설계) 이다. ---- - -# 같은 저장소가 "결정을 그 결정이 판정한 대상에 묶는 것"을 한 번은 맞게, 한 번은 틀리게 썼다 - -두 리프가 판정과 기록을 두 호출로 나눈다. 한쪽은 결정이 자기가 밟고 선 상태를 들고 있어 적용 시점에 대조하고, 다른 쪽은 결정이 무엇을 판정했는지 들고 있지 않아 짝이 어긋날 수 있다. - -## 관계 - -- **중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다** - 같은 형태의 장치가 둘일 때 쓰는 규칙이다. -- **같은 자격 증명 회전 결함이 한 가족에서 닫히고 다른 가족에서 재현됐다** - 같은 저장소가 같은 형태를 두 번 다르게 쓴 다른 사례다. -- **검증기는 발행이 아니라 주입이 강제다** - 판정이 실제로 걸리는 지점을 묻는 계열의 규칙이다. - -## 문제 - -판정과 기록을 나누면 그 사이를 무엇이 묶는지가 문제가 된다. - -스키마 산출물 발행자는 후보와 소비자 픽스처 목록을 받아 발행 가능 여부를 결정으로 돌려준다. 그리고 그 결정을 들고 다시 오면 이력에 기록한다. 발행 결정은 허용 여부와 차단 사유 목록으로 되어 있다. - -승격 지원 매트릭스도 같은 형태다. 승격 게이트가 증거와 시작 등급과 목표 등급을 받아 결정을 돌려주고, 매트릭스가 그 결정을 받아 적용한다. - -## 결론 - -한쪽만 짝을 확인한다. - -승격 매트릭스는 결정의 시작 등급이 현재 등급과 다르면 던진다. 예외 메시지가 그 이유를 적는다. 두 승격이 경합했거나 하나가 재생된 경우라는 것이다. 결정이 자기가 밟고 선 상태를 들고 있고 적용 시점에 그것을 대조한다. - -스키마 발행자는 결정이 허용인지만 본다. 그 결정이 지금 발행하려는 후보를 판정한 것인지 확인하지 않는다. 발행 결정 객체가 허용 여부와 차단 사유만 갖고 있어서 확인할 재료도 없다. - -그래서 무해한 후보를 평가한 결정으로 다른 후보를 발행할 수 있다. 그 후보는 어떤 소비자 픽스처와도 대조되지 않고, 이미 발행된 버전인지도 확인되지 않은 채 이력에 들어간다. 이 클래스가 존재하는 이유인 두 규칙, 즉 소비자 컴파일 게이트와 릴리스 버전 불변성이 인자 짝 하나로 무력해진다. - -테스트의 차이도 같다. 발행자 테스트는 평가와 발행을 한 줄에 겹쳐 써서 규율을 지킨다. 그 규율을 코드가 강제하지 않는다. 매트릭스 쪽에는 어긋난 짝을 넣어 거부를 확인하는 테스트가 따로 있다. - -이 저장소는 올바른 형태를 알고 있고 한 곳에서 쓰지 않았다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Gradle : 9.0.0 -확인 방식 : 두 적용 메서드의 본문 대조와 결정 객체의 성분 확인 -소스 수정 : x - -## 재현 조건 - -1. 스키마 발행자의 평가 메서드와 발행 메서드가 각각 무엇을 검사하는지 적는다. -2. 발행 결정 객체의 성분을 나열한다. -3. 승격 매트릭스의 적용 메서드가 결정의 어떤 성분을 현재 상태와 대조하는지 확인한다. -4. 두 테스트가 각각 짝을 어떻게 맞추는지 확인한다. - -## 본문 - - - -두 리프가 같은 문제를 푼다 — 판정과 기록이 두 호출로 나뉠 때 그 사이를 무엇이 묶는가. - -## PublishDecision 참조 위치 - -:::evidence key="the-same-repository-bound-a-decision-once-and-not-the-other-time" alt="코드베이스에서 PublishDecision 를 검색한 출력 4줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="PublishDecision 코드베이스 검색 — 4줄 · exit 0" zoom="true" -::: - -## 묶지 않은 쪽 - -`GrpcSchemaArtifactPublisher.publish(candidate, decision)` 는 `decision.allowed()` 만 보고 기록한다. `PublishDecision` 은 `(boolean, List)` 뿐이라 자기가 무엇을 판정했는지 들고 있지 않으므로, A 를 평가한 결정으로 B 를 발행할 수 있고 그러면 소비자 컴파일 게이트와 릴리스 버전 불변성을 둘 다 우회한다. 이 클래스가 존재하는 이유인 두 규칙이 인자 짝 하나로 무력해진다. - -## 묶은 쪽 - -`GrpcAdvancedSupportMatrix.apply(decision)` 는 결정의 `from` 이 현재 등급과 다르면 던지고, 그 이유를 "두 승격이 경합했거나 하나가 재생된 경우" 라고 적는다. 결정이 자기가 밟고 선 상태를 들고 있고 적용 시점에 대조하는 형태다. - -## 두 테스트의 차이도 같다 - -전자의 테스트는 `publish(artifact, evaluate(artifact, …))` 로 한 줄에서 짝을 맞춰 규율을 지키지만 코드가 그것을 강제하지 않고, 후자는 어긋난 짝을 넣는 테스트가 따로 있다. - -## 확인하지 못한 것 - -어긋난 짝을 실제로 실행해 보지 않았다. 판정은 발행 메서드 본문에 대조 코드가 없다는 것에 근거한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/duplicate-mechanisms/decision/decision-one-audit-mechanism-per-entity.md b/docs/clean-architecture-backend-template/tech-log-studio/duplicate-mechanisms/decision/decision-one-audit-mechanism-per-entity.md deleted file mode 100644 index 1471ed7..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/duplicate-mechanisms/decision/decision-one-audit-mechanism-per-entity.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -kind: PROJECT_DECISION -slug: one-audit-mechanism-per-entity -title: 감사 메커니즘은 엔티티당 정확히 하나여야 한다 -topic: duplicate-mechanisms -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: decision:one-audit-mechanism-per-entity -decisionStatus: ADOPTED -decidedOn: 2026-08-30 -source: - - src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/audit - - src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/envers - - final/document.md#a05 ---- - -# 감사 메커니즘은 엔티티당 정확히 하나여야 한다 - -## 결정문 - -한 엔티티의 감사 기록은 하나의 메커니즘이 소유하고, 둘 이상이 같은 엔티티를 기록하지 않는다. - -## 판단 이유 - -감사 메커니즘이 둘이면 같은 변경이 두 번 기록되거나, 두 기록이 서로 다른 내용을 담거나, 둘 중 하나만 도는데 어느 쪽인지 알 수 없게 된다. - -세 결과 모두 감사의 목적을 무너뜨린다. 감사 기록은 나중에 사람이 판단의 근거로 쓰는 것이고, 근거가 둘이면 판단이 서지 않는다. - -그리고 감사는 조용히 실패하는 계열이다. 기록이 남지 않아도 업무 트랜잭션은 성공하므로, 두 메커니즘 중 하나가 꺼져 있어도 증상이 없다. - -그래서 소유권을 엔티티 단위로 정한다. 어떤 엔티티가 어떤 메커니즘의 소유인지가 한 곳에서 결정되고, 그 결정이 코드로 확인 가능해야 한다. - -## 영향 - -감수하는 것 - -메커니즘마다 다른 능력을 갖는데 엔티티는 하나만 고를 수 있다. 이력 조회가 필요한 엔티티와 변경 시각만 필요한 엔티티가 같은 선택지를 공유하지 않는다. - -메커니즘을 바꾸면 그 엔티티의 과거 기록과 새 기록이 다른 형태가 된다. - -얻는 것 - -한 변경에 대한 감사 기록이 정확히 하나다. - -메커니즘 하나가 배선되지 않았을 때 그 엔티티의 기록이 통째로 비므로, 부분적으로만 기록되는 상태보다 발견하기 쉽다. - -## 근거 - -- **같은 개념의 두 어휘가 공존하면 하나를 죽은 것으로 표시한다** - 이 결정이 속한 계열의 규칙이다. -- **중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다** - 중복이 이미 있을 때의 확인 절차다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/duplicate-mechanisms/reference/reference-two-vocabularies-for-one-concept.md b/docs/clean-architecture-backend-template/tech-log-studio/duplicate-mechanisms/reference/reference-two-vocabularies-for-one-concept.md deleted file mode 100644 index fa743f4..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/duplicate-mechanisms/reference/reference-two-vocabularies-for-one-concept.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -kind: REFERENCE -slug: two-vocabularies-for-one-concept -title: 같은 개념의 두 어휘가 공존하면 하나를 죽은 것으로 표시한다 -topic: duplicate-mechanisms -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:two-vocabularies-for-one-concept -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# 같은 개념의 두 어휘가 공존하면 하나를 죽은 것으로 표시한다 - -## 목적 - -같은 개념을 가리키는 이름이 둘 남아 있어, 다음 사람이 어느 쪽이 정본인지 모른 채 둘 다 유지하거나 잘못된 쪽을 고치는 것을 막는다. - -## 규칙 - -1. 정본을 명시한다 - 두 어휘 중 어느 쪽이 앞으로 쓰일 이름인지 코드나 문서에 적는다. - -2. 죽은 쪽에 표시를 남긴다 - 지울 수 없다면 그것이 유지되지 않는 이름이라는 것과 언제 지울 수 있는지를 적는다. - -3. 표시가 없으면 둘 다 살아 있는 것으로 읽힌다 - 이름이 남아 있는 것 자체가 의도로 읽힌다. - -4. 이름이 다른 계층에 걸쳐 있으면 매핑을 한 곳에 둔다 - 변환이 여러 곳에 흩어지면 그중 하나가 뒤처진다. - -## 적용 조건 - -리팩터링이나 계층 재배치 뒤 남은 옛 이름 - -같은 도메인 개념을 어댑터와 애플리케이션이 다르게 부르는 경우 - -## 예외 - -외부 계약이 옛 이름을 요구하면 그 이름은 죽은 것이 아니라 경계 어휘다. 그 사실이 경계 지점에 적혀 있어야 한다. - -## 예시 - -이 저장소에는 미완성 상태에 이름을 붙이고 기동에서 거절하며 그 이름이 언제 목록에서 빠지는지까지 적은 선례가 있다. 그 항목은 자기 전송이 존재하는 날 이 맵을 떠난다고 적혀 있다. - -반대로 구현 계획에만 남은 클래스 이름이 ADR 의 강제 절에 그대로 인용되어, 그 절을 읽으면 존재하지 않는 것이 강제하고 있다고 읽힌다. - -## 관계 - -- **재시도 코디네이터는 빈이지만 그것을 어디에도 적용하지 않는다** - 문서에만 남은 이름이 강제 수단으로 인용된 사례다. -- **중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다** - 같은 계열의 확인 규칙이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/failure-category-across-adapters/case/case-a-closing-transport-reported-as-a-permanent-business-failure.md b/docs/clean-architecture-backend-template/tech-log-studio/failure-category-across-adapters/case/case-a-closing-transport-reported-as-a-permanent-business-failure.md deleted file mode 100644 index 81185e1..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/failure-category-across-adapters/case/case-a-closing-transport-reported-as-a-permanent-business-failure.md +++ /dev/null @@ -1,113 +0,0 @@ ---- -kind: CASE -slug: a-closing-transport-reported-as-a-permanent-business-failure -title: 종료 중이라는 사정이 업무의 영구 실패로 분류된다 -topic: failure-category-across-adapters -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a-closing-transport-reported-as-a-permanent-business-failure -evidenceCapturedOn: 2026-09-02 -assets: - - key: a-closing-transport-reported-as-a-permanent-business-failure - file: ../../../final/evidence/rendered/a-closing-transport-reported-as-a-permanent-business-failure.svg -evidence: - - ../../../final/evidence/raw/a-closing-transport-reported-as-a-permanent-business-failure.txt -source: - - 원본 분석은 NATS 와 Pulsar 어댑터 문서의 §17.2 다. 네 어댑터의 범주와 호출자, 승인 게이트의 처리, 전송 빈 조립 지점은 이 기록에 붙은 자산에서 확인할 수 있다. ---- - -# 종료 중이라는 사정이 업무의 영구 실패로 분류된다 - -전송 어댑터가 브로커에 보내기 전에 스스로 거절할 때 쓰는 헬퍼가 영구 업무 실패 범주를 고정으로 붙인다. 호출자가 둘인데 하나는 적재물 초과이고 다른 하나는 전송 종료다. 첫째에는 맞는 범주이고 둘째에는 아니다. - -## 관계 - -- **실패 범주는 재시도·DLQ·대시보드 사이의 계약이다** -- **이 세대의 사정은 업무의 영구 실패가 아니다** -- **재시도 가능성은 멱등성과 실패 범주를 함께 봐야 정해진다** - -## 문제 - -실패 범주는 재시도 엔진과 데드레터 라우터가 읽는 값이다. 영구 업무 실패는 "다시 보내도 같은 결과" 를 뜻하고, 그 값을 받으면 재시도가 일어나지 않는다. - -전송이 종료 중이라는 것은 그런 성질이 아니다. 다음 세대나 다른 인스턴스에서는 같은 메시지가 정상 발행된다. - -## 결론 - -원본 분석은 이 형태를 배선되지 않은 두 실험 어댑터에서 P3 으로 판정했다. - -같은 헬퍼를 네 전송 어댑터의 production 코드에서 찾으면 넷 다 같은 범주를 고정으로 붙인다. 다만 배선된 경로에서는 종료 중 발행이 전송에 닿기 전에 승인 게이트가 다른 범주로 거절한다. - -어느 어댑터가 실제로 조립되는지, 그 게이트가 무엇을 막는지는 본문이 다룬다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Gradle : 9.0.0 -확인 방식 : 네 전송 어댑터의 헬퍼 범주와 호출자 확인, 승인 게이트의 종료 처리 확인, 전송 빈 조립 지점 검색 -소스 수정 : x - -## 재현 조건 - -1. 각 전송 어댑터의 로컬 거절 헬퍼가 붙이는 범주를 확인한다. -2. 그 헬퍼의 호출자 둘이 각각 어떤 상황인지 확인한다. -3. 승인 게이트가 종료 중 발행에 어떤 예외와 범주를 붙이는지 확인한다. -4. 각 전송 어댑터를 조립하는 production 파일을 검색한다. - -## 본문 - - - -전송 어댑터가 브로커에 보내기 전에 스스로 거절할 때 쓰는 헬퍼가 `FailureCategory.PERMANENT_BUSINESS` 를 고정으로 붙인다. 호출자는 어느 어댑터에서든 둘이다 — 적재물이 상한을 넘은 경우와 전송이 종료 중인 경우. - -## 첫째는 맞고 둘째는 아니다 - -적재물 초과는 영구 업무 실패가 맞다. 같은 메시지를 같은 목적지로 다시 보내면 같은 상한에 걸린다. - -종료 중은 다르다. 그것은 이 전송 세대의 사정이지 메시지의 성질이 아니다. - -## 두 실험 어댑터에서는 같은 파일의 분류기가 대비를 만든다 - -NATS 와 Pulsar 의 전송 파일에는 브로커에서 돌아온 실패를 다루는 `classify` 가 같이 들어 있고, 그것은 범주를 신중히 나눈다 — 사전 거절은 구성 오류로, 결과를 알 수 없는 실패는 일시적 인프라로. - -같은 파일 안에서 한 경로는 범주를 나누고 다른 경로는 하나로 고정한다. 원본 분석이 이 대비를 근거로 두 리프에 P3 을 매겼고, 두 리프가 같은 형태를 공유하므로 수정도 함께 하는 편이 낫다고 적는다. - -## 같은 형태가 출하 어댑터 둘에도 있다 - -:::evidence key="a-closing-transport-reported-as-a-permanent-business-failure" alt="코드베이스에서 네 전송 어댑터의 로컬 거절 범주, 종료 중 발행을 거절하는 승인 게이트, 두 전송 빈의 조립 지점을 뽑은 출력 14줄. 네 어댑터가 같은 범주를 고정으로 붙이는 것과, 승인 게이트가 종료 중 발행에 다른 범주를 붙인다는 것과, Rabbit 전송을 조립하는 production 파일이 0 이라는 것이 그 출력에 그대로 보인다." caption="네 어댑터의 거절 범주 · 승인 게이트 · 전송 빈 조립 지점 — 14줄 · exit 0" zoom="true" -::: - -Kafka 와 Rabbit 의 전송 어댑터도 같은 이름의 헬퍼를 갖고, 같은 범주를 고정으로 붙이고, 같은 형태의 호출자 둘을 갖는다. 원본 분석의 두 리프 문서에는 이 항목이 없다 — 두 문서 모두 자기 §17 목록을 갖고 있지만 거기에 이 형태가 올라 있지 않다. - -다만 코드 형태가 같다는 것과 같은 결과가 난다는 것은 다르다. - -## 배선된 경로에서는 승인 게이트가 먼저 걸린다 - -발행 경로는 전송에 닿기 전에 승인 게이트를 지난다. 종료가 시작되면 그 게이트가 닫히고, 이후 발행은 `SHUTTING_DOWN` 코드를 단 배압 예외로 거절된다. - -그 예외의 범주는 `TRANSIENT_INFRASTRUCTURE` 다. 재시도 가능한 값이다. - -닫는 순서도 그렇게 짜여 있다. 종료 수명주기가 브로커 연결을 소유한 빈들보다 **먼저** 승인을 멈춘다. 그래서 배선된 배포에서 종료 중에 도착한 발행은 전송의 닫힘 분기에 도달하기 전에 다른 범주를 받는다. - -## Rabbit 전송은 조립되지 않는다 - -`messaging-rabbit` 은 배포 아티팩트에 실린다. 그런데 그 전송 클래스를 조립하는 production 파일이 0 이다. - -Kafka 는 자동 설정이 전송 빈을 만든다. Rabbit 의 자동 설정은 프로파일 검증기와 실패 분류기와 보안 설정기를 만들고 전송 빈은 만들지 않는다. 그래서 `RABBIT_TRANSPORT_CLOSED` 는 현재 조립에서 발생할 수 없다. - -편입돼 있다는 것과 인스턴스화된다는 것이 다르다. - -## 남는 것 - -네 어댑터에 같은 코드 형태가 있다. 그중 둘은 배선되지 않았고, 하나는 배선됐지만 전송 빈이 없고, 하나는 배선됐지만 그 분기에 닿기 전에 다른 게이트가 걸린다. - -원본 분석이 두 실험 어댑터에 P3 을 매긴 판단은 그대로 성립한다. 이 기록이 더하는 것은 같은 형태가 출하 코드에도 있다는 사실과, 지금 그것이 발현하지 않는 이유가 어댑터 자신이 아니라 그 위의 두 장치라는 사실이다. - -그 두 장치 중 하나라도 바뀌면 — Rabbit 전송이 조립되거나 승인 게이트의 순서가 달라지면 — 같은 코드가 다른 결과를 낸다. - -## 확인하지 못한 것 - -종료 중 발행을 실행해 재시도 엔진의 판정을 관측하지 않았다. 승인 게이트가 먼저 걸린다는 것은 호출 순서와 종료 수명주기 코드로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/failure-category-across-adapters/case/case-an-authorization-denial-recorded-as-a-configuration-error.md b/docs/clean-architecture-backend-template/tech-log-studio/failure-category-across-adapters/case/case-an-authorization-denial-recorded-as-a-configuration-error.md deleted file mode 100644 index cceb641..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/failure-category-across-adapters/case/case-an-authorization-denial-recorded-as-a-configuration-error.md +++ /dev/null @@ -1,142 +0,0 @@ ---- -kind: CASE -slug: an-authorization-denial-recorded-as-a-configuration-error -title: 브로커가 막으면 AUTHORIZATION, 플랫폼이 막으면 CONFIGURATION 이다 -topic: failure-category-across-adapters -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:an-authorization-denial-recorded-as-a-configuration-error -evidenceCapturedOn: 2026-09-04 -body: case-an-authorization-denial-recorded-as-a-configuration-error.body.md -assets: - - key: an-authorization-denial-recorded-as-a-configuration-error - file: ../../../final/evidence/rendered/an-authorization-denial-recorded-as-a-configuration-error.svg -evidence: - - ../../../final/evidence/raw/an-authorization-denial-recorded-as-a-configuration-error.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-security §17.2 이다. ---- - -# 브로커가 막으면 AUTHORIZATION, 플랫폼이 막으면 CONFIGURATION 이다 - -한 번의 `publish` 가 두 가지 이유로 거절될 수 있는데, 브로커가 거부하면 `KafkaPublishFailureClassifier:83` 이 `AUTHORIZATION` 을 붙이고, 플랫폼이 `DefaultMessagePublisher:170` 에서 스스로 막으면 `:289` 가 `CONFIGURATION` 을 붙인다. 두 번째 질문을 `AUTHORIZATION` 으로 답하도록 쓰인 `DestinationAccessValidator` 는 인스턴스를 만드는 자리가 저장소에 없다. 그런데 보안 문서는 그 검사가 오늘 일어난다고 서술한다. - -## 관계 - -- **실패 범주는 재시도·DLQ·대시보드 사이의 계약이다** - 이 사례가 만든 규칙이다. 다만 `FailureDescriptor.defaultRetryable:67`~`:79` 은 두 범주를 모두 `false` 로 두므로 재시도 판정은 같다. 세 소비자 중 답이 달라지는 것은 데드레터 헤더와 호출자 쪽이다. -- **중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다** - 이 사례가 속한 구조다. 목적지 권한을 보는 코드가 둘인데 한쪽만 발행 경로에 있다. -- **권한 거부가 AUTHORIZATION이 아니라 CONFIGURATION으로 기록된다** - 같은 두 경로를 먼저 적은 기록이다. 여기서는 브로커 쪽 분류기가 살아 있다는 것과 기본 정책이 그렇게 정해진 이유까지 확인했다. - -## 문제 - -저장소에는 FailureCategory 라는 이름의 열거형이 넷 있다. 이 사례가 다루는 것은 messaging-core-api 에 있는 것이고, 그 자바독 :4 가 세 소비자의 합의 분류라고 밝혀 둔 열거형이다. - -거부 하나가 어느 상수를 받는지, 그리고 그 상수를 고르는 코드가 실제로 조립되는지 확인했다. - -## 결론 - -그 열거형은 상수 열을 가진다. :32 자바독이 AUTHORIZATION 을 브로커가 거부한 경우로 한정하고, :38 이 CONFIGURATION 을 플랫폼이나 목적지의 설정 오류로 적는다. - -브로커 쪽은 배선돼 있다. KafkaPublishFailureClassifier:83 과 RabbitPublishFailureClassifier:102 가 AUTHORIZATION 을 반환하고, 두 클래스는 KafkaMessagingTransport:112 와 RabbitMessagingTransport:130 이 직접 만들며 스타터의 :86 과 :71 도 빈으로 등록한다. - -플랫폼 쪽은 다르다. DefaultMessagePublisher:170 이 access.mayPublish 가 거짓이면 :173 에서 rejected("PUBLISH_FORBIDDEN", ...) 를 돌려주고, 그 헬퍼가 :289 에서 FailureCategory.CONFIGURATION 을 상수로 박는다. 발행 경로 전체를 통틀어 이 파일에 나오는 범주 상수는 두 종류다. - -rejected 를 부르는 자리는 넷이다. 목적지 권한(:173) 말고도 준비 실패(:182), 데드라인 소진(:190), 런타임 세대 부재(:235) 가 같은 상수를 받는다. - -같은 검사를 AUTHORIZATION 으로 올리는 코드도 저장소에 있다. DestinationAccessValidator:33 이 같은 mayPublish 를 묻고 :34 에서 MessageAuthorizationException 을 던진다. 그 이름이 나오는 파일은 자기 자신과 docs/messaging/security.md 둘뿐이다. 같은 검색으로 센 DestinationAccessPolicy 는 main 여섯 파일에 나온다. - -보안 문서 :69 는 이 검사가 broker ACL 보다 먼저 일어난다고 서술한다. 앞으로 그렇게 하겠다는 표현이 아니라 현재 동작을 적은 문장이다. - -기본 정책이 CONFIGURATION 을 받는 데는 코드가 밝힌 이유가 있다. MessagingCoreAutoConfiguration:396 이 넣는 DeclaredDestinationAccess 는 자기 자바독에서 미선언 목적지 발행을 권한 문제로 보지 않겠다고 선언해 둔다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Gradle : 9.0.0 -확인 방식 : 같은 이름의 열거형 파일 전수, messaging 쪽 열거형 40 줄 전문과 상수 계수, 재시도 기본값 스위치 전문, 발행 경로의 권한 분기와 거절 헬퍼와 그 호출자 넷을 코드째 인용, 이 파일이 쓰는 범주 상수 계수, 브로커 쪽 분류기와 그것을 만드는 자리, 검증기 이름의 저장소 전수 검색과 대조군, 없는 이름으로 같은 검색을 거는 자기시험, 보안 문서 창, 기본 정책을 넣는 자리와 그 자바독 -소스 수정 : x - -## 재현 조건 - -1. FailureCategory 라는 이름의 파일을 저장소에서 모두 찾는다. -2. messaging-core-api 것의 전문을 싣고 상수를 센다. -3. 재시도 기본값이 두 범주를 어떻게 가르는지 스위치를 읽는다. -4. 발행 경로에서 권한 검사가 거절 헬퍼로 이어지는 줄들을 인용하고, 그 헬퍼를 부르는 자리를 전부 코드째 싣는다. -5. AUTHORIZATION 을 반환하는 messaging main 코드를 찾고, 그 클래스를 만드는 자리가 있는지 본다. -6. DestinationAccessValidator 라는 이름이 나오는 파일을 pathspec 없이 전부 찾고, 대조군과 없는 이름으로 같은 검색을 건다. -7. 보안 문서의 해당 절과 기본 정책을 넣는 자동설정 줄, 그 정책 자바독을 싣는다. - -## 본문 - - - -`messaging-core-api` 의 `FailureCategory` 는 자바독 `:4` 가 "재시도 엔진과 DLQ 라우터와 대시보드가 함께 합의하는 안정 분류" 라고 적는 열거형이다. 저장소에 같은 이름의 파일이 넷 있으므로 어느 것인지 먼저 갈라야 한다. - -## 열 개의 상수와 그중 둘의 자바독 - -:::evidence key="an-authorization-denial-recorded-as-a-configuration-error" alt="저장소 루트에서 돌린 정적 검색 출력 179줄. 먼저 FailureCategory 라는 이름의 파일 넷이 나오는데 httpclient 어댑터, persistence-jpa 어댑터, application-core 알림, messaging-core-api 하나씩이다. 이어서 messaging 쪽 열거형 40줄이 전문으로 실리고 상수 개수가 10 개로 세어진다. 32번 줄 자바독은 AUTHORIZATION 을 브로커가 거부한 경우로 적고 38번 줄은 CONFIGURATION 을 플랫폼이나 목적지의 설정 오류로 적는다. 다음으로 FailureDescriptor 67~79번 줄의 재시도 기본값 스위치가 나오는데 AUTHORIZATION 과 CONFIGURATION 이 같은 false 갈래에 있다. 범주 이름을 DLQ 헤더로 쓰는 자리는 KafkaRetryMetadataMapper 53번 줄과 DeadLetterEnvelopeFactory 40번 줄이다. 그 아래에 DefaultMessagePublisher 166~178번 줄이 실려 170번 줄의 mayPublish 검사가 173번 줄의 rejected 호출로 이어지는 것이 보이고, 278~291번 줄의 그 헬퍼 본문이 289번 줄에서 FailureCategory.CONFIGURATION 을 박는 것이 보인다. 헬퍼를 부르는 자리 넷이 각각 네 줄씩 실리는데 173번 PUBLISH_FORBIDDEN, 182번 PUBLISH_PREPARATION_FAILED, 190번 PUBLISH_DEADLINE_EXCEEDED, 235번 PUBLISH_RUNTIME_UNAVAILABLE 이다. 이 파일이 쓰는 범주 상수는 AMBIGUOUS 하나와 CONFIGURATION 하나뿐이고 import 는 5번 줄이다. 브로커 쪽에서는 MessageAuthorizationException 11번 줄과 KafkaPublishFailureClassifier 83번 줄과 RabbitPublishFailureClassifier 102번 줄이 AUTHORIZATION 을 붙이고, 그 두 분류기를 만드는 자리가 네 곳 나온다. DestinationAccessValidator 28~40번 줄은 같은 mayPublish 를 묻고 MessageAuthorizationException 을 던지는데, 그 이름이 나오는 파일은 자기 자신과 docs/messaging/security.md 둘뿐이다. 대조군 DestinationAccessPolicy 는 main 여섯 파일에 나오고, 없는 이름으로 같은 검색을 건 자기시험은 0 개다. 마지막으로 security.md 65~73번 줄과, 기본 정책을 넣는 MessagingCoreAutoConfiguration 378번·396번 줄, DeclaredDestinationAccess 25~30번 줄의 자바독이 나온다." caption="같은 이름의 열거형 넷 · messaging 열거형 40줄 전문과 상수 열 · 재시도 기본값 스위치 · 권한 분기에서 거절 헬퍼까지와 그 호출자 넷 · 브로커 쪽 분류기와 그것을 만드는 자리 · 검증기 이름의 전수 검색과 대조군과 자기시험 · 보안 문서와 기본 정책의 자바독 — 179줄 · exit 0" zoom="true" -::: - -상수는 열이다. 이 사례에 걸리는 둘은 `:33` 의 `AUTHORIZATION` 과 `:39` 의 `CONFIGURATION` 인데, 각각의 자바독이 범위를 좁혀 놓았다. `:32` 는 "Broker authorization denied the operation", `:38` 은 "The platform or destination is misconfigured" 다. - -범주가 갈라 놓는 것 중 재시도는 아니다. `FailureDescriptor.defaultRetryable:70`\~`:77` 이 `AUTHORIZATION` 과 `CONFIGURATION` 을 같은 `false` 갈래에 넣는다. 갈리는 것은 `DeadLetterEnvelopeFactory:40` 과 `KafkaRetryMetadataMapper:53` 이 헤더에 적는 이름, 그리고 호출자가 `PublishResult.failure().category()` 로 읽는 값이다. - -## 브로커가 막은 거부는 AUTHORIZATION 을 받는다 - -`KafkaPublishFailureClassifier:83` 과 `RabbitPublishFailureClassifier:102` 가 `FailureCategory.AUTHORIZATION` 을 반환한다. - -두 클래스는 조립돼 있다. `KafkaMessagingTransport:112` 가 생성자에서 직접 만들고, `RabbitMessagingTransport:130` 이 실패 처리 자리에서 만들며, 스타터의 `KafkaMessagingAutoConfiguration:86` 과 `RabbitMessagingAutoConfiguration:71` 이 빈으로도 등록한다. - -## 플랫폼이 막은 거부는 CONFIGURATION 을 받는다 - -`DefaultMessagePublisher:170` 이 `access.mayPublish(destination.name())` 를 부른다. 거짓이면 `:173` 이 `rejected("PUBLISH_FORBIDDEN", ...)` 를 돌려준다. `:171`\~`:172` 주석은 인코딩 전에 막는 이유를 적는다 — 인가되지 않은 발행이 payload 를 직렬화하면 그 바이트를 claim-check 나 로그가 들고 있게 된다. - -그 헬퍼는 `:278` 에서 시작해 `:289` 에서 `new FailureDescriptor(FailureCategory.CONFIGURATION, code, false, reason, ...)` 를 만든다. 상수가 인자가 아니라 본문에 박혀 있다. - -같은 헬퍼를 부르는 자리는 넷이다. `:173` 의 권한 거부 말고도 `:182` 의 `PUBLISH_PREPARATION_FAILED`, `:190` 의 `PUBLISH_DEADLINE_EXCEEDED`, `:235` 의 `PUBLISH_RUNTIME_UNAVAILABLE` 이 같은 상수를 받는다. 마지막 것은 런타임 세대가 설치되지 않은 상태이므로 설정 오류와 성격이 다르다. - -이 파일이 쓰는 범주 상수를 세면 `CONFIGURATION` 하나와 `AMBIGUOUS` 하나다. 발행 경로 안에서 `AUTHORIZATION` 이 붙는 자리는 없다. - -## 같은 검사를 AUTHORIZATION 으로 올리는 클래스는 아무도 만들지 않는다 - -`DestinationAccessValidator:33` 이 `policy.mayPublish(destination)` 를 묻는다. `:170` 과 같은 질문이다. 거짓이면 `:34` 가 `MessageAuthorizationException("DESTINATION_PUBLISH_DENIED", ...)` 를 던지고, 그 예외는 `MessageAuthorizationException:11` 에서 `AUTHORIZATION` 을 고정으로 들고 있다. - -이 이름이 나오는 파일을 pathspec 없이 저장소 전체에서 찾으면 둘이다 — 자기 자신과 `docs/messaging/security.md`. 같은 검색을 `DestinationAccessPolicy` 에 걸면 `BrokerSecurityProfile` · `DeclaredDestinationAccess` · `DefaultMessagePublisher` · `MessagingConfigurationCompiler` · `MessagingCoreAutoConfiguration` 과 이 검증기 자신까지 main 여섯 파일이 나온다. 없는 이름으로 같은 검색을 걸면 0 이 나온다. - -## 보안 문서는 그 클래스가 지금 검사한다고 적는다 - -`docs/messaging/security.md:69` 는 `DestinationAccessValidator` 가 broker ACL 이전에 검사한다고 적는다. `:70`\~`:71` 은 그 이유를 덧붙인다 — broker ACL 거부는 애플리케이션 컨텍스트 없는 연결 수준 오류로 도착하므로, 어느 모듈이 어디에 publish 하려 했는가가 조사 대상이 된다. - -문장에 조건절도 미래 시제도 붙어 있지 않다. - -## 기본 정책이 그렇게 정해진 이유는 코드에 적혀 있다 - -`MessagingCoreAutoConfiguration:396` 이 `DeclaredDestinationAccess.of(destinations.all())` 를 넣는다. `:378` 이 목적지 프로파일 레지스트리를 만들 때 쓰는 것과 같은 맵이다. - -그 클래스의 자바독 `:27`\~`:29` 는 선언하지 않은 목적지로 가는 메시지를 접근 제어의 경계 사례가 아니라 오타이거나 모듈이 자기 계약을 넘은 것으로 규정한다. 이 규정을 받아들이면 `:173` 의 거절에 `CONFIGURATION` 이 붙는 것은 일관된 선택이다. - -남는 것은 문서와 코드의 어긋남이다. `security.md:69` 가 서술하는 검사는 `AUTHORIZATION` 을 올리는 쪽인데, 발행 경로에 조립된 것은 `CONFIGURATION` 을 붙이는 쪽이다. - -## 원문과 갈리는 자리 - -원문은 `AUTHORIZATION` 범주를 쓰는 유일한 코드가 미사용 클래스에 있다고 적었다. 저장소 검색은 그것을 반박한다. messaging main 에서 `AUTHORIZATION` 을 붙이는 코드는 셋이고 그중 둘은 조립된 브로커 분류기다. - -원문은 권한 거부가 구성 오류로 분류되면 보안 대시보드가 그것을 보지 못한다고 적었다. 이 기록은 대시보드를 확인하지 않았으므로 그 결과를 다시 적지 않는다. 코드로 확인되는 것은 DLQ 헤더에 실리는 이름과 호출자가 읽는 값이다. - -`:170` 에서 `:173` 을 거쳐 `:289` 에 닿는다는 것과 검증기의 소비자가 0 이라는 것은 원문대로다. - -## 확인하지 못한 것 - -런타임을 띄워 반환 값을 관측하지 않았다. `:170` 에서 `:173` 을 거쳐 `:289` 에 닿는 것은 코드를 따라 읽었다. - -기본 설정에서 그 분기에 몇 갈래로 도달할 수 있는지는 세지 않았다. 목적지 프로파일과 접근 정책이 같은 맵에서 만들어진다는 두 줄을 나란히 놓은 데까지다. - -이 값을 소비하는 관측 도구가 저장소 밖에 있는지는 찾아보지 않았다. - -거절을 실제로 일으켜 반환된 범주를 읽지는 않았다. 세 줄을 코드로 따라 읽은 데까지다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/failure-category-across-adapters/reference/reference-a-category-is-a-contract-between-retry-dlq-and-dashboard.md b/docs/clean-architecture-backend-template/tech-log-studio/failure-category-across-adapters/reference/reference-a-category-is-a-contract-between-retry-dlq-and-dashboard.md deleted file mode 100644 index 7fd1c3d..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/failure-category-across-adapters/reference/reference-a-category-is-a-contract-between-retry-dlq-and-dashboard.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -kind: REFERENCE -slug: a-category-is-a-contract-between-retry-dlq-and-dashboard -title: 실패 범주는 재시도·DLQ·대시보드 사이의 계약이다 -topic: failure-category-across-adapters -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:a-category-is-a-contract-between-retry-dlq-and-dashboard -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# 실패 범주는 재시도·DLQ·대시보드 사이의 계약이다 - -## 목적 - -실패 범주를 로그 라벨이 아니라 세 소비자에 대한 지시로 다룬다. - -## 규칙 - -1. 범주를 붙이는 자리마다 세 질문에 답한다 - 재시도 엔진은 재시도하는가, 데드레터 라우터는 최종 실패로 두는가, 대시보드는 누구에게 보이는가. - -2. 세 답이 이 실패에 맞는지 본다 - 하나라도 어긋나면 범주가 틀렸다. - -3. 헬퍼가 범주를 고정으로 들고 있으면 호출자를 전부 센다 - 호출자가 둘 이상이면 대개 같은 답이 성립하지 않는다. - -4. 올바른 범주를 아는 코드가 따로 있는지 찾는다 - 있는데 실행되지 않으면 그 자체가 별개의 결함이다. - -## 적용 조건 - -어댑터가 만드는 모든 실패 서술자와 그것을 만드는 헬퍼. - -## 예외 - -분류할 수 없는 실패는 억지로 좁히지 않는다. 이 저장소의 기본값이 일시적 인프라 실패인 것이 그 선택이고, 재시도를 허용하는 쪽이 보수적인 방향이다. - -## 예시 - -권한 거부가 구성 오류로 기록된다. 보안 대시보드가 그것을 보지 못하고 구성 오류 알림이 오염된다. - -로컬 거절 헬퍼 하나가 적재물 초과와 전송 종료 둘 다에 영구 업무 실패를 붙인다. 앞의 것은 맞고 뒤의 것은 틀렸다. - -## 관계 - -- **권한 거부가 보안이 아니라 구성 오류로 기록된다** - 이 규칙을 만든 사례다. -- **종료 중이라는 사정이 업무의 영구 실패로 분류된다** - 헬퍼의 호출자를 세는 규칙을 만든 사례다. -- **전송 실패의 단계와 범주 — `AttemptStage`와 `FailureCategory`** - 범주 체계 자체의 설명이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/failure-category-across-adapters/reference/reference-this-generations-circumstance-is-not-a-permanent-failure.md b/docs/clean-architecture-backend-template/tech-log-studio/failure-category-across-adapters/reference/reference-this-generations-circumstance-is-not-a-permanent-failure.md deleted file mode 100644 index 9826a12..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/failure-category-across-adapters/reference/reference-this-generations-circumstance-is-not-a-permanent-failure.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -kind: REFERENCE -slug: this-generations-circumstance-is-not-a-permanent-failure -title: 이 세대의 사정은 업무의 영구 실패가 아니다 -topic: failure-category-across-adapters -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:this-generations-circumstance-is-not-a-permanent-failure -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# 이 세대의 사정은 업무의 영구 실패가 아니다 - -## 목적 - -프로세스나 연결 세대의 상태 때문에 생긴 거절을 요청 자체의 성질과 구분한다. - -## 규칙 - -1. 다음 세대에서 같은 요청이 성공하는지 묻는다 - 성공하면 그것은 일시적 실패다. - -2. 영구 실패는 요청 자체의 성질이어야 한다 - 적재물 초과, 스키마 불일치, 업무 규칙 거절이 그렇다. - -3. 거절 헬퍼가 범주를 고정으로 들면 호출자마다 이 질문을 다시 한다 - 세대 사정과 요청 성질이 한 헬퍼를 공유하는 경우가 흔하다. - -4. 전송 증거는 범주와 독립으로 유지한다 - 범주가 틀렸다고 전송되지 않았다는 증거까지 바꾸면 안 된다. - -## 적용 조건 - -로컬에서 만들어지는 모든 거절. 전송 종료, 승인 거부, 자격증명 회전 중 거절. - -## 예외 - -세대가 다시 만들어지지 않는 구조라면 그 사정은 영구적이다. 다만 그런 구조인지는 배포가 정하므로 코드에서 단정할 수 없다. - -## 예시 - -두 실험 어댑터의 로컬 거절 헬퍼가 전송 종료에 영구 업무 실패를 붙인다. 종료 중이라는 것은 이 세대의 사정이고, 다음 세대에서 같은 메시지는 정상 발행된다. - -같은 헬퍼의 다른 호출자인 적재물 초과는 영구 업무 실패가 맞다. 같은 메시지를 다시 보내도 같은 상한에 걸린다. - -## 관계 - -- **종료 중이라는 사정이 업무의 영구 실패로 분류된다** - 이 규칙을 만든 사례다. -- **실패 범주는 재시도·DLQ·대시보드 사이의 계약이다** - 범주 선택의 상위 규칙이다. -- **전송·업무·스트림 증거는 세 축이고 서로를 함의하지 않는다** - 범주와 증거를 독립으로 두는 근거다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/file-transfer-and-storage/case/case-a08-f005-transferbufferpool-maxborrowedbytes.md b/docs/clean-architecture-backend-template/tech-log-studio/file-transfer-and-storage/case/case-a08-f005-transferbufferpool-maxborrowedbytes.md deleted file mode 100644 index dd612ba..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/file-transfer-and-storage/case/case-a08-f005-transferbufferpool-maxborrowedbytes.md +++ /dev/null @@ -1,190 +0,0 @@ ---- -kind: CASE -slug: a08-f005-transferbufferpool-maxborrowedbytes -title: 부재로 판정된 결합이 원본 증거의 출력 안에 있다 -topic: file-transfer-and-storage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a08-f005-transferbufferpool-maxborrowedbytes -evidenceCapturedOn: 2026-09-02 -assets: - - key: a08-f005-transferbufferpool-maxborrowedbytes - file: ../../../final/evidence/rendered/a08-f005-transferbufferpool-maxborrowedbytes.svg - - key: a08-f005-transferbufferpool-maxborrowedbytes-peak - file: ../../../final/evidence/rendered/a08-f005-transferbufferpool-maxborrowedbytes-peak.svg -evidence: - - ../../../final/evidence/raw/a08-f005-transferbufferpool-maxborrowedbytes.txt - - ../../../final/evidence/raw/a08-f005-transferbufferpool-maxborrowedbytes-peak.txt -source: - - 원본 분석 절은 final/document.md#a08#L410 이다. 등급은 P3 이다. 자바독의 문장, 계측이 옳게 동작한다는 확인, 그리고 경계 성질이 다른 방식으로도 검증된다는 서술이 그 절에 있다. - - 그 절은 그 이름이 구현 파일 세 줄에만 나타나고 지목된 시험에 없다고 적는다. 구현 파일은 네 줄이고 시험에 한 줄이 있으며, 그 절이 근거로 댄 증거 파일이 다섯 줄을 전부 출력했다. - - 페이로드를 바꿔도 그 값이 상수라는 것, 반납 없는 반복 대여에서는 단언이 깨진다는 것, 그리고 두 시험의 워크플로 지위가 다르다는 것은 이 기록에서 확인했다. ---- - -# 부재로 판정된 결합이 원본 증거의 출력 안에 있다 - -버퍼 풀의 접근자가 자기 자바독에 유계 메모리 회귀 시험이 이 값을 단언한다고 적는다. 원본 절은 그 결합이 없다고 판정했는데, 원본이 근거로 댄 증거 파일이 그 단언 줄을 화면에 출력하고 있다. - -## 관계 - -- **소비자가 없는 fixture 셋** - 이쪽도 프로덕션 호출자가 0 이라는 것을 세어 확인했다. -- **그 속성을 보일 수 없는 대역 위에서 통과한 테스트는 증인이 아니다** - 단언의 변별력을 묻는 규칙이고, 이 기록의 뒷부분이 그것을 자기 대상에 적용한다. -- **커버리지 gate 둘이 나란히 있고, 하나는 발화할 수 없다** - 두 사례 모두 검증이 있는 것처럼 보이지만 그 코드가 실행되지 않는다. - -## 문제 - -존재 이유는 접근자 자바독에 한 줄로 적혀 있다. 동시 대여 바이트의 정점이며 유계 메모리 회귀 시험이 이 값을 단언한다는 것이다. - -원본 분석의 판정은 그 결합이 없다는 것이었다. 그 이름이 구현 파일 세 줄에만 나타나고 지목된 시험에는 없다는 것이 근거다. - -## 결론 - -결합은 실재한다. 그 이름은 다섯 줄에 나오고 그중 하나가 시험의 단언이다. 그 시험의 클래스 주석이 자기를 유계 메모리 회귀라고 부르므로, 자바독이 가리킨 대상도 분명하다. - -원본의 계수는 두 군데에서 어긋난다. 구현 파일은 세 줄이 아니라 네 줄이고, 시험 한 줄이 통째로 빠졌다. - -빠진 이유는 검색 범위가 아니다. 원본이 근거로 댄 증거 파일의 해당 절이 구현 파일과 시험 파일을 함께 인자로 넘겨 검색했고, 다섯 줄을 전부 출력했다. 출력된 줄이 요약 단계에서 없는 것으로 적혔다. - -시험은 실제로 돈다. 격리 태그 없이 야간 워크플로가 이름을 적어 돌린다. 풀 리퀘스트 쪽 경계 메모리 잡이 고르는 것은 두 번째 시험 하나다. - -여기까지가 원본 판정의 정정이다. 그 뒤에 남는 것은 그 단언이 무엇을 변별하느냐다. - -대여 계수가 실제 버퍼 용량이 아니라 고정 버퍼 크기 단위로 누적하므로, 최대 대여 바이트는 언제나 버퍼 크기의 배수다. 그리고 이 경로에서 대여는 한 번뿐이다. 오프셋이 0 이면 접두 재해시가 빌리기 전에 반환하고, 복사와 다이제스트가 한 번 빌려 종료 블록에서 반납한다. - -실행으로 확인했다. 페이로드를 1메가, 16메가, 256메가로 바꿔도 최대 대여 바이트는 131072 로 고정된다. 이름은 최대 관측 버퍼가 페이로드를 따라 늘지 않는다고 말하는데, 실제로는 페이로드를 바꿔도 값이 그대로다. - -그 단언이 잡는 것은 반납 없는 반복 대여다. 같은 풀에서 반납 없이 두 번 빌리면 값이 두 배가 되고 단언이 깨진다. 풀을 거치지 않는 할당은 세지 못한다. - -그 성질을 실제로 재는 것은 두 번째 시험이다. 생성기 채널이 저장소가 채운 가장 큰 단일 버퍼를 세고, 전송 비용이 파일 크기를 따라 늘지 않는다는 것을 그 수로 보인다. 정작 그 시험은 접근자를 건드리지 않는다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : src 자바 트리 식별자 전수 검색, 원본 증거 파일 대조, 실행 탐침 -소스 수정 : x - -## 재현 조건 - -1. 접근자의 자바독을 읽는다. -2. 그 이름을 src 아래 자바 파일에서 시험 트리를 포함해 검색하고 줄을 센다. -3. 원본 절이 근거로 댄 증거 파일의 해당 절을 열어 무엇이 출력됐는지 읽는다. -4. 그 시험에 격리 태그가 있는지, 어느 워크플로가 이름으로 지목하는지 확인한다. -5. 대여와 반납이 어떤 단위로 누적하는지 읽는다. -6. 같은 추가 경로를 페이로드 크기를 바꿔 가며 돌리고 최대 대여 바이트를 읽는다. -7. 같은 풀에서 반납 없이 두 번 빌려 그 값을 읽는다. -8. 같은 성질을 재는 두 번째 시험이 이 접근자를 부르는지 확인한다. - -## 본문 - - - -접근자의 자바독이 존재 이유를 한 줄로 적는다. - -## 자바독이 적은 결합 - -:::evidence key="a08-f005-transferbufferpool-maxborrowedbytes" alt="버퍼 풀 접근자의 자바독과 구현. 그 이름이 src 아래 자바 파일에 나오는 곳 전수. 원본 절이 근거로 댄 증거 파일의 해당 절이 실행한 검색 명령과 그 출력. 그리고 그 시험의 애너테이션과 두 시험을 이름으로 지목하는 워크플로 줄을 출력한 터미널 기록." caption="자바독은 회귀 시험이 이 값을 단언한다고 적음 · 그 이름은 다섯 줄에 나오고 그중 하나가 시험 · 원본 증거는 구현과 시험 두 파일을 함께 검색해 다섯 줄을 전부 출력함 · 시험에 격리 태그 없음, 야간 워크플로가 이름으로 지목 — 28줄 · exit 0" zoom="true" -::: - -```java -/** Peak simultaneously-borrowed bytes; the bounded-memory regression asserts on this. */ -long maxBorrowedBytes() { - return maxBorrowedBytes.get(); -} -``` - -원본 분석은 그 결합이 없다고 판정했다. 그 이름이 구현 파일 세 줄에만 나타난다는 것이 근거다. - -## 구현 네 줄과 시험 한 줄 - -```text - test/.../LocalAppendMemoryTest.java:54: assertThat(bufferPool.maxBorrowedBytes()).isLessThanOrEqualTo(BUFFER_BYTES); - main/.../TransferBufferPool.java:20: private final AtomicLong maxBorrowedBytes = new AtomicLong(); - main/.../TransferBufferPool.java:38: maxBorrowedBytes.accumulateAndGet(outstanding, Math::max); - main/.../TransferBufferPool.java:56: long maxBorrowedBytes() { - main/.../TransferBufferPool.java:57: return maxBorrowedBytes.get(); -``` - -경로 앞부분을 줄여 옮겼다. 구현 파일도 세 줄이 아니라 네 줄이고, 그 위에 시험 한 줄이 있다. - -## 원본 증거는 그 줄을 출력했다 - -```text -=== 8.4 doc/behaviour: bounded memory claim and its regression test === -$ grep -n 'maxBorrowedBytes' …/TransferBufferPool.java …/LocalAppendMemoryTest.java - …/TransferBufferPool.java:20: private final AtomicLong maxBorrowedBytes = new AtomicLong(); - …/TransferBufferPool.java:38: maxBorrowedBytes.accumulateAndGet(outstanding, Math::max); - …/TransferBufferPool.java:56: long maxBorrowedBytes() { - …/TransferBufferPool.java:57: return maxBorrowedBytes.get(); - …/LocalAppendMemoryTest.java:54: assertThat(bufferPool.maxBorrowedBytes()).isLessThanOrEqualTo(BUFFER_BYTES); -``` - -검색 명령이 구현 파일과 시험 파일을 함께 인자로 받았고, 다섯 줄을 전부 찍었다. 범위 밖이어서 못 본 것이 아니라, 출력된 줄이 요약 단계에서 없는 것으로 적혔다. - -시험은 실제로 돈다. - -```text - 31: @Test - .github/workflows/fileserver-nightly.yml:103: … --tests '*LocalAppendMemoryTest' - .github/workflows/fileserver-pr.yml:161: … --tests '*LargeFileBoundedMemoryTest' -``` - -격리 태그가 없고 야간 워크플로가 이름으로 지목한다. 다만 풀 리퀘스트 쪽 경계 메모리 잡은 두 번째 시험만 돌린다. - -## 그 단언이 변별하는 것 - -:::evidence key="a08-f005-transferbufferpool-maxborrowedbytes-peak" alt="자바독이 지목한 시험과 같은 추가 경로를 페이로드 1메가·16메가·256메가로 각각 돌려 최대 대여 바이트와 그것이 버퍼 크기 이하인지, 버퍼 크기의 몇 배인지 읽은 결과. 그리고 같은 풀에서 반납 없이 두 번 빌렸을 때의 값과 그 단언의 성립 여부를 출력한 터미널 기록." caption="페이로드를 256배로 늘려도 최대 대여 바이트는 131072 로 고정, 버퍼 크기의 1배 · 반납 없이 두 번 빌리면 262144 가 되어 단언이 깨짐 — 9줄 · exit 0" zoom="true" -::: - -대여 계수는 실제 버퍼 용량이 아니라 고정 버퍼 크기 단위로 누적한다. - -```java -long outstanding = borrowedNow.addAndGet(bufferSize); -maxBorrowedBytes.accumulateAndGet(outstanding, Math::max); -``` - -그래서 이 값은 언제나 버퍼 크기의 배수다. 그리고 이 경로에서 대여는 한 번뿐이다 — 오프셋이 0 이면 접두 재해시가 빌리기 전에 반환하고, 복사와 다이제스트가 한 번 빌려 종료 블록에서 반납한다. - -같은 추가 경로를 페이로드만 바꿔 돌렸다. - -```text -payload maxBorrowedBytes BUFFER_BYTES 이하 버퍼 크기의 배수 -1MiB 131072 예 1 -16MiB 131072 예 1 -256MiB 131072 예 1 -``` - -페이로드를 256배로 늘려도 값이 움직이지 않는다. 시험 메서드의 이름은 최대 관측 버퍼가 페이로드를 따라 늘지 않는다는 것인데, 그 값은 페이로드에 대해 상수다. - -자명한 단언은 아니다. 반납 없이 두 번 빌리면 깨진다. - -```text -[대조] 반납 없이 두 번 빌리면 그 단언이 깨진다 - maxBorrowedBytes = 262144 BUFFER_BYTES 이하 = false -``` - -잡는 것은 정점 동시 대여가 버퍼 하나를 넘는 경우다. 잡지 못하는 것은 풀을 우회한 페이로드 비례 할당이다. - -## 그 성질을 재는 것은 두 번째 시험이다 - -```java - * Proves that transfer cost does not scale with file size. - * - *

The property is measured, not asserted by inspection: a generator channel counts the largest - * single buffer the store ever asked it to fill. -``` - -생성기 채널이 실제로 채운 단일 버퍼의 최댓값을 기록한다. 이쪽은 접근자를 부르지 않는다. - -## 정정 범위 - -자바독이 서술한 결합은 실재하고, 그 계측은 시험에 연결돼 있다. 정정할 것은 원본 판정이다. 그 단언의 변별 범위가 좁다는 것은 별개의 관찰이고, 원본 절은 그것을 다루지 않았다. - -## 확인하지 못한 것 - -원본 절이 왜 자기 증거가 출력한 줄을 부재로 요약했는지, 그 경위는 확인하지 못했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/file-transfer-and-storage/case/case-analysis-finding-a08-f002.md b/docs/clean-architecture-backend-template/tech-log-studio/file-transfer-and-storage/case/case-analysis-finding-a08-f002.md deleted file mode 100644 index 734532e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/file-transfer-and-storage/case/case-analysis-finding-a08-f002.md +++ /dev/null @@ -1,97 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a08-f002 -title: R1과 R2의 설정 취급이 비대칭이고, 검증된 쪽은 하나뿐이다 -topic: file-transfer-and-storage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a08-f002 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a08-f002.body.md -assets: - - key: analysis-finding-a08-f002 - file: ../../../final/evidence/rendered/analysis-finding-a08-f002.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a08-f002.txt -source: - - 원본 분석 절은 final/document.md#a08#L108 이다. ---- - -# R1과 R2의 설정 취급이 비대칭이고, 검증된 쪽은 하나뿐이다 - -같은 리프의 두 설정 묶음이 미지의 키를 다르게 다룬다. 한쪽은 거부하고 그것을 지키는 테스트가 있다. 다른 쪽은 조용히 무시하고 그 테스트가 없다. 오타 난 상한이 적용되지 않은 채 큰 기본값이 쓰인다. - -## 관계 - -- **설정 오타는 실패해야 하고 무시되면 안 된다** - 이 사례가 그 규칙의 형태다. -- **문서가 지목한 기본값 위치와 test 목록이 실제와 다르다** - 같은 리프의 다른 문서 오류다. -- **빠뜨림이 통과가 되는 게이트는 게이트가 아니다** - 같은 계열의 상위 규칙이다. - -## 문제 - -같은 리프 안에 설정 묶음이 둘 있다. 하나는 현재 경로이고 하나는 호환용이다. - -두 묶음이 설정을 어떻게 다루는지 대조했다. - -## 결론 - -다섯 축에서 다르다. - -바인딩 타입이 다르다. 현재 경로는 불변 레코드에 미지 필드 거부를 켠다. 호환 경로는 가변 자바빈이고 기본값이라 미지 키를 무시한다. - -루트 경로 취급이 다르다. 현재 경로는 이미 절대이고 정규화된 경로를 요구한다. 호환 경로는 상대 경로를 받아 현재 작업 디렉터리 기준으로 절대화한다. - -기본 루트가 다르다. 현재 경로는 필수라 기본값이 없다. 호환 경로는 상대 기본값 둘을 가진다. - -디렉터리 생성이 다르다. 현재 경로는 만들지 않고 증명을 별도로 요구한다. 호환 경로는 만든다. - -그리고 미지 키 테스트가 현재 경로에만 있다. - -결과의 차이가 구체적이다. - -현재 경로에서는 설정 키의 오타가 컨텍스트를 실패시킨다. 호환 경로에서는 상한 키의 오타가 조용히 무시되고, 설정했다고 믿는 상한 대신 백만이라는 기본값이 쓰인다. - -두 묶음이 같은 리프의 같은 성격 설정인데 한쪽만 닫힌 실패다. - -판정은 P3 다. 호환 경로는 문서상 호환 전용이므로 우선순위를 낮춘다. - -## 검증 환경 - -Spring Boot : 4.0.8 -확인 방식 : 두 설정 클래스와 각각의 테스트 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/142 계열에 있다. - -1. 두 설정 묶음의 바인딩 타입을 확인한다. -2. 미지 필드 거부 설정이 켜져 있는지 각각 확인한다. -3. 루트 경로 정규화 방식을 대조한다. -4. 기본값과 디렉터리 생성 여부를 대조한다. -5. 미지 키 거부 테스트가 어느 쪽에 있는지 확인한다. - -## 본문 - - - -R2에서는 `strict-path-securty` 같은 오타가 컨텍스트를 실패시킨다. R1에서는 `app.file-export.maximum-rowz=10` 같은 오타가 조용히 무시되고, 설정했다고 믿는 상한이 적용되지 않은 채 기본값 1,000,000이 쓰인다. - -## 같은 오타가 두 등급에서 갈리는 결과 - -:::evidence key="analysis-finding-a08-f002" alt="분석 문서 final/document.md#a08 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a08 발췌 — 15줄" zoom="true" -::: - -## 같은 성격의 설정인데 한쪽만 fail-closed다 - -두 selector가 같은 leaf에 있다. P3 — R1은 문서상 "compatibility only"이므로 우선순위를 낮춘다. - -## 확인하지 못한 것 - -호환 경로의 상한 키에 오타를 넣고 실제로 백만이 쓰이는지 실행하지 않았다. 바인딩 설정상 그 결과가 나온다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/file-transfer-and-storage/case/case-analysis-finding-a08-f003.md b/docs/clean-architecture-backend-template/tech-log-studio/file-transfer-and-storage/case/case-analysis-finding-a08-f003.md deleted file mode 100644 index 1f1f51a..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/file-transfer-and-storage/case/case-analysis-finding-a08-f003.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a08-f003 -title: 문서가 지목한 기본값 위치와 test 목록이 실제와 다르다 -topic: file-transfer-and-storage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a08-f003 -evidenceCapturedOn: 2026-09-01 -assets: - - key: analysis-finding-a08-f003 - file: ../../../final/evidence/rendered/analysis-finding-a08-f003.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a08-f003.txt -source: - - 원본 분석 절은 final/document.md#a08#L122 이다. ---- - -# 문서가 지목한 기본값 위치와 test 목록이 실제와 다르다 - -README 가 선택 스위치의 기본값이 부트스트랩 설정 파일에 꺼짐으로 들어 있다고 적는다. 그 파일에 해당 키가 없다. 그리고 테스트 목록의 첫 항목은 이 리프가 아니라 애플리케이션 코어에 있다. - -## 관계 - -- **두 selector의 설정 취급이 비대칭이고 검증된 쪽은 하나뿐이다** - 같은 리프의 설정 사례다. -- **README의 세 가지 사실 오류** - 같은 계열의 문서 오류다. -- **동작이 옳아도 문서가 가리킨 자리는 틀릴 수 있다** - 이 사례가 그 규칙의 형태다. - -## 문제 - -README 가 두 가지를 적는다. 선택 스위치의 기본값 위치와 이 리프를 검증하는 테스트 목록이다. - -## 결론 - -첫째가 어긋난다. - -README 는 선택 스위치가 부트스트랩 설정 파일에서 꺼짐으로 기본값을 갖는다고 적는다. - -그 파일에는 현재 경로의 활성화 키도 호환 경로의 활성화 키도 없다. 비슷한 접두사로 걸리는 두 줄은 주석이다. - -실효 기본값은 다른 경로로 만들어진다. 속성이 없으면 조건부 애너테이션이 맞지 않고 빈이 만들어지지 않는다. - -그러므로 동작 자체는 옳다. 꺼진 것이 기본이다. - -틀린 것은 그 결과가 어디서 오는지에 대한 서술이다. 문서가 가리킨 자리에 그 키가 없다. - -둘째도 어긋난다. - -README 의 테스트 목록 첫 항목이 이 리프에 없다. 애플리케이션 코어에 있는 클래스다. - -판정은 P3 다. 둘 다 동작이 아니라 서술의 문제다. - -## 검증 환경 - -Spring Boot : 4.0.8 -확인 방식 : 설정 파일 키 검색과 테스트 클래스 위치 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/142 계열에 있다. - -1. README 의 기본값 서술 줄을 읽는다. -2. 지목된 설정 파일에서 두 활성화 키를 검색한다. -3. 걸리는 줄이 주석인지 확인한다. -4. 조건부 애너테이션이 속성 부재에서 어떻게 동작하는지 확인한다. -5. 테스트 목록의 첫 항목을 저장소 전체에서 찾는다. - -## 본문 - - - -README는 R2 selector가 "`app-bootstrap/application.yml`에서 `false`로 기본값을 갖는다"고 적는다. 그 파일에 `app.fileserver.enabled`도 `app.file-export.enabled`도 없다(`142-...` §8.4e; `app.fileserver`로 걸리는 두 줄은 주석이다). - -## FilePublicationContractTest 참조 위치 - -:::evidence key="analysis-finding-a08-f003" alt="코드베이스에서 FilePublicationContractTest 를 검색한 출력 1줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="FilePublicationContractTest 코드베이스 검색 — 1줄 · exit 0" zoom="true" -::: - -## 동작은 옳고 문서가 가리킨 자리가 비어 있다 - -실효 기본값은 "속성 부재 → `@ConditionalOnProperty` 미매치 → bean 없음"이다. - -## test 목록의 첫 항목도 다른 곳에 있다 - -README의 Tests 목록 첫 항목 `FilePublicationContractTest`는 이 leaf가 아니라 `application-core`에 있다. - -## 확인하지 못한 것 - -그 키가 과거에 설정 파일에 있었는지 확인하지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/file-transfer-and-storage/case/case-analysis-finding-a08-f004.md b/docs/clean-architecture-backend-template/tech-log-studio/file-transfer-and-storage/case/case-analysis-finding-a08-f004.md deleted file mode 100644 index c508026..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/file-transfer-and-storage/case/case-analysis-finding-a08-f004.md +++ /dev/null @@ -1,113 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a08-f004 -title: 발행 rename만 경로 기반이고, 그것을 지키는 것은 이 모듈이 "근사에 불과하다"고 적은 사전검사다 -topic: file-transfer-and-storage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a08-f004 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a08-f004.body.md -assets: - - key: analysis-finding-a08-f004 - file: ../../../final/evidence/rendered/analysis-finding-a08-f004.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a08-f004.txt -source: - - 원본 분석 절은 final/document.md#a08#L376 이다. ---- - -# 발행 rename만 경로 기반이고, 그것을 지키는 것은 이 모듈이 "근사에 불과하다"고 적은 사전검사다 - -이 모듈은 디스크립터를 따라 내려가는 방식으로 심볼릭 링크 공격을 구조적으로 막는다. 콘텐츠를 보이게 만드는 마지막 단계만 경로 기반으로 남아 있고, 그 단계를 지키는 사전검사는 모듈 자신이 근사에 불과하다고 적어 둔 메서드다. - -## 관계 - -- **검사를 더 하는 것은 창을 좁힐 뿐 닫지 않는다** - 이 사례가 그 규칙의 형태다. -- **두 selector의 설정 취급이 비대칭이고 검증된 쪽은 하나뿐이다** - 같은 리프의 다른 사례다. -- **계측 필드가 자기 회귀 test를 지목하는데 그 test가 읽지 않는다** - 같은 리프의 문서 결합 사례다. - -## 문제 - -이 모듈의 지역 플랫폼에 남은 파일 시스템 정적 호출을 전수 조사했다. - -대부분은 정당하다. 루트 열기는 문서가 피할 수 없는 하나의 경로 해석이라고 적은 것이고, 시작 시 능력 탐침은 격리된 영역에서 돌며, 고아 검사와 상태 조회와 사용량 탐침은 다른 하위 범위다. - -## 결론 - -문제는 쓰기 경로에 남은 다섯 호출이다. - -원자적 이동 발행자의 발행 이동과 그 실패 분류를 위한 존재 검사 둘, 발행 검증의 크기 조회, 메타데이터 포인터 발행자의 준비 파일 삭제다. - -그중 발행 이동이 핵심이다. - -발행자는 그 이동 직전에 루트와 대상 부모 사이에 심볼릭 링크가 없는지 검사한다. 경로 기반 사전검사다. - -그런데 그 메서드의 자바독이 스스로를 이렇게 설명한다. - -능력 탐침을 위해 남겨 두었으며 탐침은 여전히 경로명으로 추론한다. production 접근은 더 이상 여기에 의존하지 않는다. 디스크립터를 하나씩 따라 내려가며 링크를 따르지 않는 방식이 심볼릭 링크 성분을 구조적으로 거부하고, 사전검사는 그것을 근사할 수 있을 뿐이라는 것이다. - -즉 production 이 더 이상 의존하지 않는다고 적힌 메서드를, 콘텐츠를 보이게 만드는 바로 그 단계가 유일한 보호로 쓴다. - -같은 모듈의 다른 문서가 겨냥한 패턴 그대로다. 검사를 더 하는 것은 창을 좁힐 뿐 닫지 않는다는 문장이다. - -같은 불일치가 파일 길이 조회에서도 보인다. - -판정은 P3 다. - -실제 악용에는 저장소 루트 안쪽 쓰기 권한이 필요하다. 이 리프가 그 루트의 소유자와 권한을 증명하는 것은 현재 경로뿐이고, 플랫폼 저장소 루트의 증명은 부트스트랩의 시작 검증기 몫이다. - -그래서 도달성은 배포 형상에 달려 있다. - -그럼에도 기록하는 이유는 모듈 자신의 문서가 이 패턴을 명시적으로 불충분하다고 선언했다는 점이다. - -수정은 발행 이동을 디스크립터 기반 도우미로 옮겨 부모 서술자 상대 이동을 쓰고, 크기 조회를 채널의 속성 읽기로 바꾸는 것이다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 정적 호출 전수 조사와 자바독 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/145 계열에 있다. - -1. 지역 플랫폼의 파일 시스템 정적 호출을 전수 조사한다. -2. 각 호출이 어느 경로에 속하는지 분류한다. -3. 쓰기 경로에 남은 호출을 추린다. -4. 발행 이동 직전의 사전검사가 무엇인지 확인한다. -5. 그 사전검사 메서드의 자바독을 읽는다. - -## 본문 - - - -발행 rename만 경로 기반이고, 그것을 지키는 것은 이 모듈이 "a precheck could only ever approximate"라고 적은 사전검사다. - -## LocalPersistentRootAttestor 참조 위치 - -:::evidence key="analysis-finding-a08-f004" alt="코드베이스에서 LocalPersistentRootAttestor 를 검색한 출력 9줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="LocalPersistentRootAttestor 코드베이스 검색 — 9줄 · exit 0" zoom="true" -::: - -## 도달성이 배포 형상에 달려 있다 - -실제 악용에는 스토리지 루트 안쪽 쓰기 권한이 필요하고, 이 leaf가 그 루트의 소유자·권한을 증명하는 것은 R2 경로(`LocalPersistentRootAttestor`)뿐이며, 플랫폼 저장소 루트의 증명은 `app-bootstrap`의 startup validator 몫이다. 심각도를 P3로 두는 이유가 그것이다. - -## 그럼에도 기록하는 이유 - -모듈 자신의 문서가 이 패턴을 명시적으로 불충분하다고 선언했다. - -## 수정 - -발행 rename을 `SecureDirectoryWalk.inParentOf`로 옮겨 부모 서술자 상대 `move`를 쓰고, `sizeOf`를 `channels.readAttributes`로 바꾸는 것이다. - -## 확인하지 못한 것 - -심볼릭 링크를 실제로 심어 발행 단계를 통과시키는 재현을 하지 않았다. 그것은 저장소 루트 안쪽 쓰기 권한을 전제한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/file-transfer-and-storage/case/case-analysis-finding-a09-f001.md b/docs/clean-architecture-backend-template/tech-log-studio/file-transfer-and-storage/case/case-analysis-finding-a09-f001.md deleted file mode 100644 index 38e1113..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/file-transfer-and-storage/case/case-analysis-finding-a09-f001.md +++ /dev/null @@ -1,117 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a09-f001 -title: production 판정이 두 개의 리터럴 프로파일 이름에 걸려 있다 -topic: file-transfer-and-storage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a09-f001 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a09-f001.body.md -assets: - - key: analysis-finding-a09-f001 - file: ../../../final/evidence/rendered/analysis-finding-a09-f001.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a09-f001.txt -source: - - 원본 분석 절은 final/document.md#a09#L106 이다. ---- - -# production 판정이 두 개의 리터럴 프로파일 이름에 걸려 있다 - -개발용 제공자를 운영에서 쓰지 말라는 금지가 문서에 있고 그것을 강제하는 코드는 한 곳이다. 그 코드는 거부 검사인데 판정 근거가 이름 두 개의 허용 목록이다. 다른 이름을 쓰는 분기는 검사를 통과한다. - -## 관계 - -- **거부 검사의 근거를 허용 목록으로 두면 목록 밖이 통과한다** - 이 사례가 그 규칙의 형태다. -- **사라진 가드가 문서에만 남았고 그 부재는 이미 승인된 결정이다** - 같은 계열의 강제 수단 사례다. -- **발행 rename만 경로 기반이고 그것을 지키는 것은 모듈 자신이 근사에 불과하다고 적은 사전검사다** - 도달성이 형상에 달린 다른 사례다. - -## 문제 - -리프 지침 문서의 금지 목록에 개발용 제공자를 운영에서 쓰는 것이 있다. - -그것을 강제하는 코드가 어디 있는지 찾았다. - -## 결론 - -한 곳이다. - -활성 프로파일 목록을 소문자로 만들고, 그중 하나가 짧은 운영 이름이거나 긴 운영 이름과 같은지 본다. - -이 저장소가 짧은 이름의 프로파일 설정 파일을 싣고 있으므로 현재 형상에서는 맞는다. - -그리고 같은 방식으로 운영을 판정하는 리프는 이것 하나뿐이다. 저장소 전체가 공유하는 운영 판별 장치가 없다. - -문제는 방향이다. - -이것은 거부 검사인데 판정 근거가 허용 목록 두 개다. - -지역이나 환경을 이름에 붙이거나 축약형이나 다른 낱말을 쓰는 분기는 이 검사를 통과한다. 그리고 개발용 제공자가 운영에서 조용히 선택된다. - -그 제공자는 README 가 현재 경로 전용 개발 제공자라고 적은 것이다. - -실패는 시작 시점이 아니라 데이터가 로컬 디스크에 쌓인 뒤에 드러난다. - -판정은 P3 다. 이 저장소 형상에서는 도달하지 않는다. - -기록하는 이유는 셋이다. 지침 문서가 금지 항목으로 명시했고, 강제 수단이 문자열 둘이며, 분기가 프로파일 이름을 바꾸는 것은 평범한 일이다. - -수정은 운영 판별을 뒤집는 것이다. 이름을 묻는 대신 개발용 제공자를 허용한다는 명시적 설정을 요구하는 형태다. 이름이 아니라 의도를 묻는 것이다. - -## 검증 환경 - -Spring Boot : 4.0.8 -확인 방식 : 강제 코드 전수 검색과 저장소 전체 운영 판별 장치 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/150 계열에 있다. - -1. 지침 문서의 금지 목록을 읽는다. -2. 그 금지를 강제하는 코드를 저장소에서 찾는다. -3. 판정 조건이 무엇과 비교하는지 확인한다. -4. 같은 방식으로 운영을 판정하는 다른 리프가 있는지 센다. -5. 개발용 제공자의 README 서술을 확인한다. - -## 본문 - - - -CLAUDE.md의 Forbidden 목록에 "local-dev in production"이 있고, 그것을 강제하는 코드는 이것 하나다. - -```java -private boolean productionProfileActive() { - return activeProfiles.stream() - .map(profile -> profile.toLowerCase(Locale.ROOT)) - .anyMatch(profile -> profile.equals("prod") || profile.equals("production")); -} -``` - -## 금지를 강제하는 코드 한 곳 - -:::evidence key="analysis-finding-a09-f001" alt="분석 문서 final/document.md#a09 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a09 발췌 — 15줄" zoom="true" -::: - -## 현재 형상에서는 맞는다 - -이 저장소가 `application-prod.yml`을 싣고 있다. 그리고 `150-...` §8.2c에서 확인했듯 같은 방식으로 production을 판정하는 leaf는 이것 하나뿐이다 — 저장소 전체가 공유하는 production 판별 장치가 없다. - -## 문제는 방향이다 - -이것은 **거부** 검사인데 판정 근거가 **허용 목록 두 개**다. `prd`, `production-eu`, `live`, `prod-apac` 같은 이름을 쓰는 fork는 이 검사를 통과하고, `filesystem-local-dev`가 production에서 조용히 선택된다 — 그 provider는 README가 "R1-only development provider"라고 적은 것이다. 실패는 startup이 아니라 데이터가 로컬 디스크에 쌓인 뒤에 드러난다. - -## 기록하는 세 이유 - -(a) CLAUDE.md가 금지 항목으로 명시했고 (b) 강제 수단이 두 문자열이며 (c) fork가 프로파일 이름을 바꾸는 것은 평범한 일이다. 수정은 production 판별을 명시적 설정(예: `app.object-storage.allow-local-dev=true`를 요구)으로 뒤집는 것 — 이름이 아니라 의도를 묻는 형태다. P3. - -## 확인하지 못한 것 - -목록 밖 이름의 프로파일로 실제로 띄워 개발용 제공자가 선택되는지 재현하지 않았다. 조건식상 그 결과가 나온다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/file-transfer-and-storage/case/case-analysis-finding-a09-f004.md b/docs/clean-architecture-backend-template/tech-log-studio/file-transfer-and-storage/case/case-analysis-finding-a09-f004.md deleted file mode 100644 index d8871e6..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/file-transfer-and-storage/case/case-analysis-finding-a09-f004.md +++ /dev/null @@ -1,112 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a09-f004 -title: 그러나 R0 경계가 문서에만 있고 compile 경로에서 닫히지 않는다 -topic: file-transfer-and-storage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a09-f004 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a09-f004.body.md -assets: - - key: analysis-finding-a09-f004 - file: ../../../final/evidence/rendered/analysis-finding-a09-f004.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a09-f004.txt -source: - - 원본 분석 절은 final/document.md#a09#L485 이다. ---- - -# 그러나 R0 경계가 문서에만 있고 compile 경로에서 닫히지 않는다 - -직접 전송 능력이 아직 배선되지 않았다고 README 가 선언한다. 그런데 설정은 그 능력을 계속 받아들이고 런타임은 자격증명을 쥔 서명자를 실제로 만든다. 만들어진 제공자를 꺼내 가는 코드는 없다. - -## 관계 - -- **두 개의 outbox 중 하나만 조립되어 있다** - 같은 형태의 미조립 사례다. -- **production 판정이 두 개의 리터럴 프로파일 이름에 걸려 있다** - 같은 리프의 다른 사례이자 반대 사례다. -- **문서가 선언한 경계는 코드가 닫아야 경계다** - 이 사례가 그 규칙의 형태다. - -## 문제 - -README 가 직접 업로드와 직접 다중 파트가 아직 배선되지 않았다고 적는다. - -그 선언이 코드로도 강제되는지 확인했다. - -## 결론 - -강제되지 않는다. 네 단계로 확인된다. - -첫째, 두 능력은 목적지 요구사항으로 선언할 수 있고 바인딩 컴파일러가 그것을 제공자 능력으로 번역한다. - -둘째, 제공자 바인딩의 프로파일 컴파일은 자체 호스팅 구현에 대해서만 이 두 주장을 거부한다. 정확한 판본이 조건부 관리 변형을 기본 지원한다고 주장할 수 없다는 이유다. 클라우드 프로파일에는 대응하는 거부가 없다. - -셋째, 그러면 제공자 기여자가 두 스위치 중 하나라도 켜져 있을 때 서명자를 할당하고 직접 전송 제공자와 직접 다중 파트 제공자를 만들어 선택 팩토리에 넣는다. - -넷째, 그 둘을 팩토리에서 꺼내 가는 코드가 없다. 전수 검색 결과가 0 이다. - -즉 클라우드 바인딩에서 직접 업로드를 요구하는 배포는 시작을 통과하고 서명자를 할당하고 두 제공자를 조립하고, 직접 전송 포트는 여전히 하나도 얻지 못한다. - -README 가 말한 미배선은 사실이다. 그 사실을 강제하는 것이 문서뿐이다. - -이 리프 안에 정반대의 사례가 있어서 대비가 분명하다. - -개발용 제공자는 운영 프로파일에서 컴파일 시점에 거부된다. 자체 호스팅 구현의 능력 과대 주장도 컴파일 시점에 거부된다. - -같은 파일이 같은 종류의 자격 없음 판정을 클라우드와 직접 전송 조합에 대해서만 하지 않는다. - -판정은 P2 다. - -데이터 위험은 없다. 없는 포트는 호출될 수 없다. - -위험은 둘이다. 운영자가 켰다고 믿는 기능이 없다는 것과, 아무도 쓰지 않는 서명 자격증명 핸들이 프로세스 수명 동안 살아 있다는 것이다. - -수정은 셋 중 하나다. 조정자를 조건부로 조립하거나, 미배선 동안 두 능력 요구를 제공자 종류와 무관하게 컴파일 단계에서 거부하거나, 능력이 켜져도 서명자를 만들지 않도록 조립을 뒤로 미루는 것이다. - -## 검증 환경 - -확인 방식 : 바인딩 컴파일러와 제공자 기여자 코드 확인, 소비자 전수 검색 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/154 계열에 있다. - -1. README 의 미배선 선언을 읽는다. -2. 두 능력이 목적지 요구사항으로 선언 가능한지 확인한다. -3. 프로파일 컴파일에서 어느 제공자에 대해 거부가 있는지 확인한다. -4. 제공자 기여자가 어떤 조건에서 서명자를 만드는지 확인한다. -5. 만들어진 두 제공자를 팩토리에서 꺼내는 코드를 검색한다. - -## 본문 - - - -R0 경계가 문서에만 있고 compile 경로에서 닫히지 않는다. - -## 문서에만 있는 R0 경계 - -:::evidence key="analysis-finding-a09-f004" alt="분석 문서 final/document.md#a09 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a09 발췌 — 15줄" zoom="true" -::: - -## 데이터 위험은 없다 - -없는 port는 호출될 수 없다. P2. - -## 위험 둘 - -(a) 운영자가 켰다고 믿는 기능이 없다는 것, (b) 아무도 쓰지 않는 서명 자격증명 핸들이 프로세스 수명 동안 살아 있다는 것. - -## 수정 방향 셋 - -coordinator를 조건부로 조립하거나, R0인 동안 `DIRECT_UPLOAD`/`DIRECT_MULTIPART` 요구를 compile 단계에서 provider 종류와 무관하게 거부하거나, capability가 켜져도 presigner를 만들지 않도록 조립을 뒤로 미루는 것. - -## 확인하지 못한 것 - -클라우드 바인딩으로 실제로 띄워 서명자가 살아 있는 것을 확인하지 않았다. 조립 조건상 그 결과가 나온다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/file-transfer-and-storage/case/case-analysis-finding-a09-f006.md b/docs/clean-architecture-backend-template/tech-log-studio/file-transfer-and-storage/case/case-analysis-finding-a09-f006.md deleted file mode 100644 index 739ebf9..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/file-transfer-and-storage/case/case-analysis-finding-a09-f006.md +++ /dev/null @@ -1,98 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a09-f006 -title: nonce replay 경계가 결과를 읽고 버린다 -topic: file-transfer-and-storage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a09-f006 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a09-f006.body.md -assets: - - key: analysis-finding-a09-f006 - file: ../../../final/evidence/rendered/analysis-finding-a09-f006.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a09-f006.txt -source: - - 원본 분석 절은 final/document.md#a09#L589 이다. ---- - -# nonce replay 경계가 결과를 읽고 버린다 - -인터페이스가 스스로를 내구적 비교 후 교체 재생 경계라고 부른다. 호출부는 청구 결과를 받아 변수에 담고, 어느 값이든 발행을 그대로 진행한다. 결과가 바꾸는 것은 종결 기록을 쓸지 여부뿐이다. - -## 관계 - -- **검사 결과를 읽고 버리면 검사가 아니다** - 이 사례가 그 규칙의 형태다. -- **미배선 경계가 문서에만 있고 compile 경로에서 닫히지 않는다** - 같은 리프의 다른 경계 사례다. -- **멱등은 결과를 같게 만들지만 경계를 대신하지는 않는다** - 피해가 제한된 이유이자 그것이 변명이 되지 않는 이유다. - -## 문제 - -승인 문서의 논스가 한 번만 쓰인다는 성질을 지키는 저장소가 있다. - -그 저장소의 청구 메서드를 호출부가 어떻게 쓰는지 확인했다. - -## 결론 - -청구 결과를 변수에 담고, 다음 줄에서 발행을 부른다. - -그다음에야 결과를 본다. 종결 재생이 아니면 종결 기록을 남긴다. - -결과 값은 셋이다. 청구됨과 정확한 재생과 종결 재생이다. - -어느 값이든 발행은 그대로 진행된다. - -즉 청구 결과가 바꾸는 것은 종결 기록을 쓸지 여부뿐이다. 인터페이스 자바독은 자신을 내구적 비교 후 교체 재생 경계라고 부르는데, 경계로서 무엇도 막지 않는다. - -실제 피해는 제한적이다. - -발행이 연산 키 기반 멱등이다. 이미 소진된 논스로 다시 들어와도 결과는 재생됨이고 두 번째 객체가 생기지 않는다. - -그래서 판정은 P3 다. - -그럼에도 기록하는 이유는 셋이다. - -이름이 약속하는 것과 다르다. 종결 기록에 넘기는 기대 개정 번호가 항상 0 이라 비교 후 교체의 인자로서도 고정값이다. 그리고 승인 문서의 논스가 한 번만 쓰인다는 성질이 이 코드로는 보장되지 않는다. - -수정은 종결 재생에서 발행 전에 거부하는 것이다. - -## 검증 환경 - -확인 방식 : 호출부와 인터페이스 자바독 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/154 계열에 있다. - -1. 재생 저장소 인터페이스의 자바독을 읽는다. -2. 청구 결과 열거값 셋을 확인한다. -3. 호출부에서 결과가 어떻게 쓰이는지 확인한다. -4. 발행 호출이 결과보다 앞인지 뒤인지 본다. -5. 종결 기록에 넘기는 기대 개정 번호가 무엇인지 확인한다. - -## 본문 - - - -`ClaimResult`는 `CLAIMED` / `EXACT_REPLAY` / `TERMINAL_REPLAY` 셋인데, 어느 값이든 발행은 그대로 진행된다. claim 결과가 바꾸는 것은 terminal 기록을 쓸지 여부뿐이다. - -## ClaimResult 참조 위치 - -:::evidence key="analysis-finding-a09-f006" alt="코드베이스에서 ClaimResult 를 검색한 출력 10줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ClaimResult 코드베이스 검색 — 10줄 · exit 0" zoom="true" -::: - -## 경계라고 부르지만 아무것도 막지 않는다 - -인터페이스 javadoc은 자신을 "Durable compare-and-set nonce replay boundary"라고 부른다. - -## 확인하지 못한 것 - -소진된 논스로 두 번 들어와 결과가 재생됨이 되는지 실행하지 않았다. 멱등 키 구현상 그 결과가 나온다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/fileserver-state-and-fencing/case/case-a-cleanup-claim-without-fencing.md b/docs/clean-architecture-backend-template/tech-log-studio/fileserver-state-and-fencing/case/case-a-cleanup-claim-without-fencing.md deleted file mode 100644 index 4d180fc..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/fileserver-state-and-fencing/case/case-a-cleanup-claim-without-fencing.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -kind: CASE -slug: a-cleanup-claim-without-fencing -title: claim이 소유자·토큰·만료를 기록하지 않아 죽은 worker의 항목이 영영 남았다 -topic: fileserver-state-and-fencing -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a-cleanup-claim-without-fencing -evidenceCapturedOn: 2026-09-02 -assets: - - key: a-cleanup-claim-without-fencing - file: ../../../final/evidence/rendered/a-cleanup-claim-without-fencing.svg -evidence: - - ../../../final/evidence/raw/a-cleanup-claim-without-fencing.txt -source: - - 컬럼 정의와 두 결과의 사후 기록은 `V3__fileserver_fenced_cleanup_lease.sql` 의 헤더와 두 COMMENT 에 있다. 토큰 대조와 널 만료 제외의 이유는 `FileserverCleanupRepository` 의 두 메서드 javadoc 에 있다. 원본 분석은 `final/document.md#a05` §79 가 이 스키마를 다룬다. ---- - -# claim이 소유자·토큰·만료를 기록하지 않아 죽은 worker의 항목이 영영 남았다 - -정리 항목의 청구가 상태만 바꾸고 소유자도 토큰도 리스 만료도 기록하지 않았다. 하나의 누락에서 회수되지 않는 항목과 덮어쓰기라는 두 결과가 나왔다. - -## 관계 - -- **fenced lease — 만료 시각만으로는 부족한 이유** - 이 사례가 같은 문제의 파일서버 판이다. -- **리스가 만료 시각만 기록하고 소유자를 기록하지 않아 최종 상태를 되돌릴 수 있었다** - 같은 형태가 메시징 어댑터에서 나타난 사례다. -- **cleanup이 읽은 lease와 삭제 사이에 writer가 그 lease를 얻을 수 있었다** - 같은 저장소의 다음 마이그레이션이 다룬 문제다. - -## 문제 - -정리 작업은 파일을 물리적으로 지운 뒤 데이터베이스를 정산한다. 그 사이에 작업자가 죽을 수 있고, 여러 작업자가 같은 항목을 두고 겹칠 수 있다. - -청구가 그 두 상황을 구별할 정보를 남기지 않았다. 상태를 진행 중으로 옮기는 것이 전부였다. - -## 결론 - -수정은 컬럼 넷과 질의 둘이다. - -컬럼은 청구 소유자, 청구 토큰, 리스 만료, 그리고 청구 세대 계수기다. 질의는 완료 갱신이 토큰으로 행을 찾게 만든 것과, 회수기가 만료된 청구를 오래된 것부터 가져오되 널 만료는 제외하게 만든 것이다. - -두 결과가 어떻게 생겼고 각 컬럼이 무엇을 맡는지는 본문이 다룬다. - -## 검증 환경 - -데이터베이스 : PostgreSQL -마이그레이션 도구 : Flyway -확인 방식 : V3 마이그레이션의 컬럼 정의와 정리 저장소 질의 두 개 확인, claim_fence 참조 전수 검색 -소스 수정 : x - -## 재현 조건 - -1. fileserver 의 V3 마이그레이션 헤더를 읽는다. 두 결과가 나란히 적혀 있다. -2. 추가된 컬럼 넷과 각각의 널 허용 여부를 확인한다. -3. 완료 갱신 질의가 무엇으로 행을 찾는지 확인한다. -4. 회수기 질의가 널 만료를 어떻게 다루는지 확인한다. -5. claim_fence 를 읽는 코드가 있는지 검색한다. - -## 본문 - - - -정리 항목의 청구가 상태를 `IN_PROGRESS` 로 옮기고 그 외에는 아무것도 기록하지 않았다. 마이그레이션 헤더가 그 누락에서 나온 결과를 둘로 적는다. - -## 하나는 아무도 손대지 않아서, 하나는 두 손이 겹쳐서 - -첫째는 회수되지 않는 항목이었다. 물리 삭제를 수행하고 데이터베이스를 정산하기 전에 죽은 작업자가 행을 진행 중 상태로 영원히 남겼다. 어떤 질의도 그 항목을 살아 있는 작업자가 지금 지우고 있는 항목과 구별할 수 없었다. 파일은 이미 사라졌는데 쿼터와 수명주기는 정산되지 않은 채 남았다. - -둘째는 덮어쓰기였다. 완료 갱신이 정리 식별자만으로 행을 찾았다. 어떤 합리적 리스보다 오래 멈춰 있던 작업자가 깨어나, 그 사이 다른 작업자가 청구해 반쯤 진행한 항목 위에 완료를 쓸 수 있었다. - -같은 누락에서 나왔지만 방향이 반대다. - -## 컬럼 넷과 질의 둘 - -:::evidence key="a-cleanup-claim-without-fencing" alt="코드베이스에서 V3 마이그레이션의 컬럼 넷과 정리 저장소의 질의, 그리고 claim_fence 참조를 뽑은 출력 21줄. 완료 갱신이 토큰으로 행을 찾고 회수기가 널 만료를 제외한다는 것, claimFence 는 증가만 하고 그 엔티티의 getter 열셋 안에 없다는 것이 그 출력에 그대로 보인다." caption="V3 컬럼 넷 · 토큰 대조 · 널 만료 제외 · claimFence 참조 — 21줄 · exit 0" zoom="true" -::: - -컬럼은 `claim_owner` · `claim_token` · `lease_until` · `claim_fence` 다. 앞의 셋은 널을 허용하고 넷째만 `NOT NULL DEFAULT 0` 이다. 헤더가 그 이유를 하나로 적는다 — 이 마이그레이션 이전에 청구된 항목이 계속 동작해야 하기 때문이다. - -스키마만 바뀐 것이 아니다. 저장소의 질의 둘이 그 컬럼을 실제로 쓴다. - -## 토큰이 덮어쓰기를 막는다 - -완료 갱신의 `where` 절에 `and c.claimToken = :token` 이 붙었다. 그 메서드의 javadoc 이 이전 상태를 적는다 — 예전에는 정리 식별자만으로 대조했고, 그래서 오래 멈춰 있던 작업자가 다른 작업자의 진행 중 항목 위에 완료를 쓸 수 있었다. - -반환 타입이 `int` 다. 교체된 작업자는 예외를 받는 것이 아니라 **갱신 행 수 0** 을 받는다. 행을 못 찾는 것이 실패가 아니라 답이 되는 형태다. - -## 널 만료를 회수기가 건너뛴다 - -회수기 질의는 진행 중이면서 만료가 지난 항목을 만료 순으로 가져온다. 조건에 `and c.leaseUntil is not null` 이 들어 있다. - -그 메서드의 javadoc 이 이유를 적는다 — 널 리스는 펜싱이 생기기 전에 청구됐다는 뜻이고, 자동으로 넘겨받는 것은 아무도 상태를 기록하지 않은 작업에 대해 추측하는 일이다. 그 항목이 물리 삭제를 마쳤는지 시작도 안 했는지 알 방법이 없다. 그래서 운영자를 필요로 한다. - -부분 인덱스가 그 질의 모양 그대로 만들어져 있다 — `lease_until` 에 걸리고 조건이 `status = 'IN_PROGRESS'` 다. - -## 넷째 컬럼은 아직 소비자가 없다 - -`claim_fence` 는 청구할 때마다 1 씩 오른다. 저장소에서 그 이름이 나오는 곳은 그 증가 한 줄과 엔티티의 필드 선언뿐이다. - -엔티티는 getter 열셋을 갖는다. `getClaimToken` 과 `getLeaseUntil` 은 있고 `claimFence` 의 getter 는 없다. 즉 값이 올라가기만 하고 어디에서도 읽히거나 비교되지 않는다. - -마이그레이션은 이 컬럼에만 COMMENT 를 붙이지 않았다. 다른 둘에는 무엇을 위한 값인지 적혀 있다. - -## 확인하지 못한 것 - -작업자를 죽여 진행 중 항목이 남는 것을 재현하지 않았다. 컨테이너 레인을 돌리지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/fileserver-state-and-fencing/case/case-a-read-then-delete-race-on-the-upload-lease.md b/docs/clean-architecture-backend-template/tech-log-studio/fileserver-state-and-fencing/case/case-a-read-then-delete-race-on-the-upload-lease.md deleted file mode 100644 index 214ff55..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/fileserver-state-and-fencing/case/case-a-read-then-delete-race-on-the-upload-lease.md +++ /dev/null @@ -1,114 +0,0 @@ ---- -kind: CASE -slug: a-read-then-delete-race-on-the-upload-lease -title: cleanup이 읽은 lease와 삭제 사이에 writer가 그 lease를 얻을 수 있었다 -topic: fileserver-state-and-fencing -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a-read-then-delete-race-on-the-upload-lease -evidenceCapturedOn: 2026-09-02 -assets: - - key: a-read-then-delete-race-on-the-upload-lease - file: ../../../final/evidence/rendered/a-read-then-delete-race-on-the-upload-lease.svg -evidence: - - ../../../final/evidence/raw/a-read-then-delete-race-on-the-upload-lease.txt -source: - - 이 경합의 1차 기록은 `V4__fileserver_upload_terminal_state.sql` 의 헤더다. 분석 문서는 두 곳에서 이 사건을 다룬다. application-core 편 §11.2 가 취소와 정리의 경합을 청구가 만드는 경계로 정리하고, persistence-jpa 편 §83.3 이 이전 리뷰가 올린 소유자·토큰·최종 상태 부재를 V3·V4 와 현재 저장소 코드가 해소했다고 적는다. 같은 편 §79 의 마이그레이션 목록도 이 컬럼을 언급한다. ---- - -# cleanup이 읽은 lease와 삭제 사이에 writer가 그 lease를 얻을 수 있었다 - -정리 작업이 쓰기 리스를 읽어 없음을 확인하고 스테이징 바이트를 지웠다. 읽기와 삭제 사이에 쓰기 작업자가 바로 그 리스를 얻을 수 있었고, 정리가 지운 것은 업로드가 이어 쓰고 있던 객체였다. - -## 관계 - -- **조건부 update로 행을 claim하고 읽은 값으로 판단하지 않는다** - 이 사례에서 끌어낸 규칙이다. -- **CAS 튜플을 where 절에 전부 반복하고 update count를 답으로 쓴다** - 같은 형태를 다른 저장소에서 다룬 규칙이다. -- **claim이 소유자·토큰·만료를 기록하지 않아 죽은 worker의 항목이 영영 남았다** - 같은 어댑터의 앞선 마이그레이션이 다룬 문제다. - -## 문제 - -정리 작업은 스테이징 바이트를 지우기 전에 그 업로드에 쓰기 리스가 걸려 있는지 확인했다. 리스가 없으면 아무도 쓰고 있지 않다고 판단하고 지웠다. - -읽기와 삭제 사이에 쓰기 작업자가 그 리스를 얻을 수 있었다. 업로드가 끝났다는 표시가 데이터베이스에 없었기 때문이다. - -쓰기 작업자의 획득 문장도 그것을 막지 못했다. 그 문장은 업로드의 만료와 리스만 확인했고, 취소되었는지 검증에 실패했는지는 확인하지 않았다. - -## 결론 - -수정은 두 쪽이 함께 볼 상태 컬럼을 만드는 것이었다. 취소와 검증 실패 마무리가 정리를 큐에 넣는 같은 트랜잭션 안에서 세션을 최종 상태로 옮기고, 획득과 갱신과 오프셋 커밋은 활성 상태를 요구한다. - -정리는 조금 전에 읽은 값으로 판단하는 대신 조건부 갱신으로 행을 청구한다. 0행이 돌아오면 삭제를 다음 주기로 넘긴다. - -기본값이 활성으로 잡혀 있어 이 컬럼이 생기기 전 세션도 영향을 받지 않는다. - -리스 반납 문장에는 이 조건이 없다. 클래스 javadoc 은 모든 쓰기 문장이 활성을 요구한다고 적는데, 그 문장 하나는 그렇지 않다. - -## 검증 환경 - -데이터베이스 : PostgreSQL -마이그레이션 도구 : Flyway -확인 방식 : 마이그레이션 헤더의 사후 기록과 현재 구현의 조건절·트랜잭션 경계 확인 -소스 수정 : x - -## 재현 조건 - -1. 업로드 최종 상태 마이그레이션의 헤더를 읽는다. 경합 순서와 양쪽의 판단 근거가 적혀 있다. -2. 추가된 상태 컬럼과 CHECK 제약, 부분 인덱스를 확인한다. -3. 저장소의 쓰기·전이 문장을 전부 세고, 그중 몇 개가 활성 상태를 조건으로 거는지 확인한다. -4. 클래스 javadoc 의 주장과 각 문장의 실제 조건을 대조한다. -5. 최종화와 정리 큐잉을 감싸는 트랜잭션 람다를 두 경로에서 시작부터 끝까지 읽는다. -6. 정리가 청구 전에 무엇을 하는지, 청구에 실패하면 무엇을 하는지 확인한다. - -## 본문 - - - -마이그레이션 헤더가 이 경합을 사후에 기록한다. 정리는 리스의 부재를 봤고, 쓰기의 획득 문장은 업로드의 만료와 리스를 봤다. 취소되었는지 검증에 실패했는지를 묻는 쪽은 없었다. - -그래서 이미 삭제 예정인 바이트에 대해 획득 문장이 리스를 내주었다. 그 요청을 거를 근거를 갖고 있지 않았다. - -## 상태 하나를 두 쪽이 같이 본다 - -:::evidence key="a-read-then-delete-race-on-the-upload-lease" alt="코드베이스에서 V4 마이그레이션이 더한 상태 컬럼과 CHECK 제약과 부분 인덱스, 이 저장소의 쓰기·전이 문장 여섯과 그중 활성 상태를 조건으로 거는 네 줄, 모든 쓰기 문장이 활성을 요구한다고 적은 클래스 javadoc 과 그 조건이 없는 리스 반납 문장, 최종화와 정리 청구가 요구하는 상태, 취소 경로와 검증 실패 경로의 트랜잭션 람다 전체, 그리고 정리가 세션을 먼저 읽고 있을 때만 청구하는 조건을 뽑은 출력. javadoc 의 주장과 리스 반납 문장의 조건이 어긋난다는 것이 그 출력에 나란히 보인다." caption="상태 컬럼과 제약 · 문장 여섯과 활성 조건 넷 · javadoc 과 어긋나는 반납 문장 · 두 경로의 트랜잭션 전체 · 정리의 읽기와 청구" zoom="true" -::: - -V4 가 세션 테이블에 상태 컬럼을 더한다. 값은 활성과 최종 둘이고 CHECK 로 묶여 있다. 기본값이 활성이라 이 컬럼 이전의 세션은 그대로 동작한다. - -같은 마이그레이션이 부분 인덱스도 만든다. 최종 상태인 행만 담고 리스 만료로 정렬한다. 청구 문장의 술어와 같은 조건이지만, 청구 자체는 업로드 아이디 단건 갱신이라 이 인덱스를 타지 않는다. 최종 상태 집합을 만료 순으로 훑을 때를 위한 모양이다. - -## 쓰기 문장 넷 중 셋이 활성을 요구한다 - -리스 획득과 갱신과 오프셋 커밋의 조건절에 활성 상태가 들어갔다. 최종 상태로 옮겨진 세션에는 이 셋이 걸리지 않는다. - -리스 반납 문장에는 없다. 업로드 아이디와 리스 토큰만 본다. 최종 상태로 옮겨진 뒤에도 진행 중이던 쓰기 작업자가 자기 리스를 놓을 수 있어야 하고, 반납은 리스 컬럼을 비울 뿐 바이트를 건드리지 않는다. - -클래스 javadoc 은 모든 쓰기 문장이 활성을 요구한다고 적는다. 넷 중 셋이다. - -최종화 문장 자체도 활성일 때만 성립한다. 이미 최종인 행을 다시 최종으로 옮기는 시도는 0행을 갱신한다. - -## 최종화와 정리 큐잉이 한 트랜잭션 안에 있다 - -취소 경로와 검증 실패 경로가 각각 하나의 트랜잭션 안에서 세 가지를 함께 한다. 기록을 도달 불가로 옮기고, 세션을 최종 상태로 옮기고, 정리 요청을 큐에 넣는다. - -셋이 함께 커밋되므로 큐에 항목이 있는데 세션은 아직 활성인 순간이 없다. 취소 경로의 주석이 그 이유를 적는다 — 큐잉만 하면 세션이 활성으로 남아, 정리가 지우려는 바로 그 바이트에 쓰기가 리스를 얻을 수 있었다는 것이다. - -첫 번째 쓰기에도 이유가 붙어 있다. 기록이 먼저 도달 불가가 되어야 물리 작업이 예약되고, 둘이 함께 커밋되므로 파일이 도달 불가인데 회수할 것이 큐에 없는 상태가 남지 않는다. - -## 정리는 읽은 값 대신 청구로 판단한다 - -정리 청구 문장은 최종 상태이고 리스가 없거나 만료된 행만 갱신한다. 그 한 문장이 리스가 걸려 있지 않다는 것을 증명하는 동시에 리스 컬럼을 비운다. - -청구가 0행이면 지우지 않는다. 항목을 실패로 표시하고 사유를 남기고 결과를 건너뜀으로 돌려준다. - -다만 청구는 세션 행이 있을 때만 걸린다. 조건이 존재 확인과 청구의 논리곱이라, 세션이 이미 사라진 항목은 청구를 거치지 않고 바로 지운다 — 지킬 리스를 가진 쓰기 작업자가 있을 수 없는 경우다. - -## 확인하지 못한 것 - -읽기와 삭제 사이에서 리스를 가로채는 경합을 재현해 보지는 않았다. 경합 순서는 마이그레이션 헤더의 사후 기록이고, 수정의 현재 형태는 코드로 확인했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/fileserver-state-and-fencing/decision/decision-no-physical-paths-in-metadata.md b/docs/clean-architecture-backend-template/tech-log-studio/fileserver-state-and-fencing/decision/decision-no-physical-paths-in-metadata.md deleted file mode 100644 index e1a30b9..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/fileserver-state-and-fencing/decision/decision-no-physical-paths-in-metadata.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -kind: PROJECT_DECISION -slug: no-physical-paths-in-metadata -title: 물리 경로와 원본 파일명을 저장하지 않는다 -topic: fileserver-state-and-fencing -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: decision:no-physical-paths-in-metadata -decisionStatus: ADOPTED -decidedOn: 2026-08-30 -source: - - src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/fileserver/V1__create_fileserver_metadata.sql - - final/document.md#a08 ---- - -# 물리 경로와 원본 파일명을 저장하지 않는다 - -## 결정문 - -파일 메타데이터에 물리 경로도 마운트도 원본 물리 파일명도 저장하지 않는다. 콘텐츠 키는 서버가 만든 불투명 키이고 원본 이름은 표시용 텍스트일 뿐이다. - -## 판단 이유 - -물리 경로를 저장하면 그 값이 언젠가 경로 조작에 쓰인다. 저장소에서 읽은 값이라는 이유로 신뢰되기 쉽고, 그것을 만든 것은 결국 업로드한 쪽이다. - -원본 파일명도 같다. 사용자가 정하는 값이고, 그것으로 파일을 찾거나 열면 경로 순회와 확장자 기반 오판의 입구가 된다. - -그래서 두 값의 역할을 분리한다. 서버가 만든 불투명 키가 자원을 지목하고, 원본 이름은 화면에 보여 줄 때만 쓴다. - -마운트를 저장하지 않는 것도 같은 계열이다. 저장 위치가 메타데이터에 박히면 저장소를 옮길 때 레코드를 고쳐야 하고, 그 값이 코드 경로로 흘러들 수 있다. - -## 영향 - -감수하는 것 - -원본 이름으로 파일을 찾을 수 없다. 검색이 필요하면 별도 색인이 필요하다. - -저장 위치를 메타데이터에서 알 수 없으므로, 키에서 위치를 유도하는 규칙이 어딘가에 있어야 한다. - -얻는 것 - -경로 순회와 확장자 기반 오판의 입구가 메타데이터에 없다. - -저장소를 옮겨도 메타데이터를 고치지 않는다. - -## 근거 - -- **파일 상태 기계와 READY가 뜻하는 것** - 이 결정이 속한 메타데이터 설계다. -- **sanitize가 아니라 reject가 기본이다** - 신뢰할 수 없는 값을 다루는 같은 계열의 규칙이다. -- **이름은 값이 아니라 registry key다** - 식별자와 표시용 값을 구별하는 같은 원칙이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/fileserver-state-and-fencing/reference/reference-a-publicly-readable-state-must-be-complete-by-constraint.md b/docs/clean-architecture-backend-template/tech-log-studio/fileserver-state-and-fencing/reference/reference-a-publicly-readable-state-must-be-complete-by-constraint.md deleted file mode 100644 index c4d2691..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/fileserver-state-and-fencing/reference/reference-a-publicly-readable-state-must-be-complete-by-constraint.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -kind: REFERENCE -slug: a-publicly-readable-state-must-be-complete-by-constraint -title: 공개 읽기 가능한 상태는 완전한 identity를 DB 제약으로 요구한다 -topic: fileserver-state-and-fencing -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:a-publicly-readable-state-must-be-complete-by-constraint -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# 공개 읽기 가능한 상태는 완전한 identity를 DB 제약으로 요구한다 - -## 목적 - -공개 상태에 도달한 레코드가 필수 값을 빠뜨린 채 존재해, 그 값을 전제한 코드가 나중에 실패하는 것을 막는다. - -## 규칙 - -1. 공개 상태의 요구를 데이터베이스 제약으로 표현한다 - 애플리케이션 검사로 두면 그 검사를 지나지 않는 경로가 언젠가 생긴다. - -2. 상태별로 다른 요구를 조건부 제약으로 쓴다 - 모든 상태에 같은 요구를 걸면 중간 상태를 만들 수 없다. - -3. 상태와 버전을 함께 가드한다 - 상태만 조건에 넣으면 같은 상태에서 출발한 두 전이가 모두 성공한다. - -4. 진실의 출처를 하나로 정한다 - 파일시스템에 바이트가 있다는 것과 공개 가능하다는 것은 다른 사실이다. 어느 쪽이 정본인지 정하고 그것을 문서에 적는다. - -## 적용 조건 - -외부에 노출되는 상태를 갖는 모든 레코드 - -파일이나 객체처럼 저장소와 메타데이터가 따로 있는 자원 - -## 예외 - -내부 처리 단계의 중간 상태는 완전성을 요구하지 않는다. 그 상태가 외부로 새지 않는 것이 보장되어야 한다. - -## 예시 - -파일 메타데이터의 헤더가 관계형 레코드가 공개 가능 여부를 정한다고 명시하고, 모든 상태 전이가 상태와 버전 양쪽으로 가드된다고 적는다. - -## 관계 - -- **파일 상태 기계와 READY가 뜻하는 것** - 이 규칙이 나온 개념이다. -- **CAS 튜플을 where 절에 전부 반복하고 update count를 답으로 쓴다** - 세 번째 규칙의 일반형이다. -- **위험한 조합은 정책이 아니라 생성자가 거부하게 만든다** - 같은 원칙의 값 타입 판이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/fileserver-state-and-fencing/reference/reference-claim-with-a-conditional-update-not-a-read.md b/docs/clean-architecture-backend-template/tech-log-studio/fileserver-state-and-fencing/reference/reference-claim-with-a-conditional-update-not-a-read.md deleted file mode 100644 index d20b694..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/fileserver-state-and-fencing/reference/reference-claim-with-a-conditional-update-not-a-read.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -kind: REFERENCE -slug: claim-with-a-conditional-update-not-a-read -title: 조건부 update로 행을 claim하고 읽은 값으로 판단하지 않는다 -topic: fileserver-state-and-fencing -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:claim-with-a-conditional-update-not-a-read -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# 조건부 update로 행을 claim하고 읽은 값으로 판단하지 않는다 - -## 목적 - -읽기와 행동 사이에 상태가 바뀌어, 이미 유효하지 않은 판단으로 되돌릴 수 없는 작업을 수행하는 것을 막는다. - -## 규칙 - -1. 읽고 나서 판단하지 않는다 - 조건을 갱신문의 where 절에 넣고 갱신 건수로 판단한다. - -2. 되돌릴 수 없는 작업 앞에서는 특히 그렇다 - 물리 삭제나 외부 호출은 되돌릴 수 없다. 그 앞의 판단은 원자적이어야 한다. - -3. 양쪽이 같은 사실을 본다 - 한쪽은 리스의 부재를 보고 다른 쪽은 만료만 보면, 둘 다 자기 기준으로 옳으면서 서로 어긋난다. - -4. 상태를 명시적으로 만든다 - 끝났다는 사실이 값으로 없으면 각 참여자가 그것을 추론하고, 추론의 근거가 서로 다르다. - -5. 청구하지 못하면 미룬다 - 갱신 건수가 0 이면 다른 참여자가 그 행을 들고 있다는 뜻이다. 강제하지 않고 다음 주기로 넘긴다. - -## 적용 조건 - -여러 참여자가 같은 자원을 놓고 경합하는 정리 작업과 배치 - -물리 삭제나 외부 호출이 뒤따르는 판정 - -## 예외 - -읽기와 행동이 같은 트랜잭션 안에서 행 잠금과 함께 일어나면 조건부 갱신 없이도 안전하다. 그 잠금이 실제로 걸리는지 확인해야 한다. - -## 예시 - -정리가 쓰기 리스를 읽어 없음을 확인하고 스테이징 바이트를 지웠다. 읽기와 삭제 사이에 쓰기 작업자가 그 리스를 얻었고, 지워진 것은 업로드가 이어 쓰고 있던 객체였다. - -수정 후 정리는 조건부 갱신으로 청구하고, 리스가 실제로 걸려 있으면 아무것도 청구하지 않고 미룬다. - -## 관계 - -- **cleanup이 읽은 lease와 삭제 사이에 writer가 그 lease를 얻을 수 있었다** - 이 규칙을 만든 사례다. -- **CAS 튜플을 where 절에 전부 반복하고 update count를 답으로 쓴다** - 같은 규칙의 상태 기계 판이다. -- **시간은 DB에서, 그리고 행을 잠근 다음에 읽는다** - 같은 계열의 짝 규칙이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/fileserver-state-and-fencing/reference/reference-prefix-matching-fits-signatures-not-sniffing.md b/docs/clean-architecture-backend-template/tech-log-studio/fileserver-state-and-fencing/reference/reference-prefix-matching-fits-signatures-not-sniffing.md deleted file mode 100644 index 447b95e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/fileserver-state-and-fencing/reference/reference-prefix-matching-fits-signatures-not-sniffing.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -kind: REFERENCE -slug: prefix-matching-fits-signatures-not-sniffing -title: 접두사 시작 매칭은 시그니처에는 맞고 스니핑 패턴에는 맞지 않는다 -topic: fileserver-state-and-fencing -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:prefix-matching-fits-signatures-not-sniffing -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# 접두사 시작 매칭은 시그니처에는 맞고 스니핑 패턴에는 맞지 않는다 - -## 목적 - -파일 형식 시그니처를 찾는 방법으로 브라우저 스니핑 대상을 찾아, 앞에 바이트를 붙이는 것만으로 우회되는 것을 막는다. - -## 규칙 - -1. 두 문제를 구별한다 - 형식 시그니처는 정의상 시작 바이트다. 브라우저 스니핑은 관용적 해석이므로 시작이 아니어도 된다. - -2. 스니핑 대상은 포함으로 찾는다 - 앞의 일정 구간 안에 마커가 있으면 탐지한다. 시작이어야 한다는 조건을 걸지 않는다. - -3. 앞에 붙는 것들을 목록으로 갖는다 - 바이트 순서 표시와 널 바이트와 공백과 주석이 흔하다. - -4. 탐지 대상은 소비자의 관용도에 맞춘다 - 무엇을 실행할지 정하는 것은 브라우저다. 우리 파서가 아니다. - -5. 같은 함수를 두 목적에 쓰지 않는다 - 한쪽에 맞추면 다른 쪽이 틀린다. - -## 적용 조건 - -업로드 콘텐츠 검증 - -인라인으로 제공될 수 있는 모든 콘텐츠의 분류 - -## 예외 - -형식 시그니처를 확인해 파일 타입을 판정하는 목적이면 시작 매칭이 옳다. 그 경우 그 판정이 보안 결정으로 쓰이지 않아야 한다. - -## 예시 - -실행 가능 콘텐츠 정책이 앞의 1024 바이트에서 마커를 찾되 시작 매칭을 쓴다. 바이트 순서 표시나 널 바이트나 주석을 앞에 붙이면 탐지되지 않고, 브라우저는 그런 파일도 실행한다. - -## 관계 - -- **scriptable 콘텐츠 탐지가 BOM과 NUL과 주석으로 우회된다** - 이 규칙을 만든 사례다. -- **sanitize가 아니라 reject가 기본이다** - 탐지된 콘텐츠를 어떻게 다룰지 정한 규칙이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/graphql-surface/case/case-a16-f002-oneof.md b/docs/clean-architecture-backend-template/tech-log-studio/graphql-surface/case/case-a16-f002-oneof.md deleted file mode 100644 index 1bf0c9c..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/graphql-surface/case/case-a16-f002-oneof.md +++ /dev/null @@ -1,132 +0,0 @@ ---- -kind: CASE -slug: a16-f002-oneof -title: 플랫폼이 강제한다고 적었지만 그 규칙을 실제로 거는 것은 라이브러리다 -topic: graphql-surface -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -evidenceCapturedOn: 2026-09-03 -rootTreeNode: case:a16-f002-oneof -body: case-a16-f002-oneof.body.md -assets: - - key: a16-f002-oneof - file: ../../../final/evidence/rendered/a16-f002-oneof.svg - - key: a16-f002-oneof-run - file: ../../../final/evidence/rendered/a16-f002-oneof-run.svg -evidence: - - ../../../final/evidence/raw/a16-f002-oneof.txt - - ../../../final/evidence/raw/a16-f002-oneof-run.txt -source: - - 원본 분석 절은 final/document.md#a16#L281 이다. ---- - -# 플랫폼이 강제한다고 적었지만 그 규칙을 실제로 거는 것은 라이브러리다 - -단일 선택 입력 정책의 자바독이 플랫폼이 강제하는 규칙이라고 적었다. 강제하는 두 코드에 프로덕션 호출자가 없다. 그리고 스키마 게이트가 거부하는 잘못된 선언은 graphql-java 25.0 이 이미 스키마를 만들 때 거부한다. - -## 관계 - -- **크로스 스택 게이트가 검증하는 조립은 픽스처의 조립이고, 플랫폼의 조립이 아니다** - 둘 다 게이트가 도는 자리가 시험 쪽이라서 배포에 대해 아무 말도 하지 않는다. 저쪽은 게이트가 보는 조립이 픽스처의 것이고, 여기는 게이트가 보는 규칙을 라이브러리가 이미 본다. -- **검증기는 발행이 아니라 주입이 강제다** - 저쪽 규칙의 예외 절에 걸리는 사례다. 순수 함수형 규칙 객체는 호출자가 어디인지를 자바독이 대야 한다. 정책 자바독은 스키마 게이트와 실행 시점 검증기를 호출자로 대지만 그 둘의 자바독은 자기 호출자를 대지 않고, 그 둘을 실제로 부르는 자리는 시험과 픽스처뿐이다. 자바독이 댄 사슬이 프로덕션에서 끊긴다. -- **설정 바인딩이 마스터 스위치 밖에서 일어난다. 컴포지션 루트의 자기 규칙과 어긋난다** - 둘 다 서술과 실제 적용 지점이 어긋난다. 저쪽은 스위치 밖이고 여기는 계층 밖이다. - -## 문제 - -정책 자바독이 이 규칙을 플랫폼이 강제한다고 적었다. 강제하는 코드가 실제로 불리는지, 그리고 그 코드가 없으면 무엇이 달라지는지 확인했다. - -## 결론 - -프로덕션에서는 불리지 않는다. - -강제하는 코드는 둘이다. 선언을 보는 스키마 게이트와 값을 보는 실행 시점 검증기다. 실행 시점 검증기는 부르는 자리가 시험뿐이다. 스키마 게이트는 시험 다섯 자리와 계약 스위트 한 자리에서 불린다. 그 스위트는 testFixtures 에 있어 채택자도 쓸 수 있지만, 이 저장소에서도 GraphQlCrossModuleContractSuiteTest 가 세 자리에서 부르고, 그 시험의 태그를 기본 test 레인이 배제하지 않는다. 애플리케이션 기동 경로에는 둘 다 없다. - -노출은 없다. 현재 세 스키마 파일 어디에도 그 지시자 선언이 없고, 라이브러리가 값 검사를 스스로 한다. 규칙에 맞게 선언한 스키마에 구성원 둘을 채워 보내면 라이브러리가 거부하고, 아무것도 채우지 않아도 거부한다. - -여기서 원문과 갈린다. - -원문은 게이트가 라이브러리의 사각을 메운다고 적고, 그래서 잘못된 타입이 첫 요청에서 드러난다고 했다. 돌려 보니 둘 다 아니다. graphql-java 25.0 이 같은 선언을 스키마 만들 때 거부하고, 게이트와 거의 같은 문장으로 같은 두 구성원을 지목한다. 드러나는 시점은 첫 요청이 아니라 스키마 조립이다. - -남는 것은 하나다. 강제하는 계층이 자바독이 적은 계층과 다르다. 게이트가 계약 스위트에서 도는 것은 사실이지만 그것은 시험을 돌릴 때이지 배포가 뜰 때가 아니다. - -판정은 P3 이고 원문과 같다. 노출이 없다는 결론도, 그 두 근거인 스키마에 지시자가 없다는 것과 라이브러리가 값을 본다는 것도 원문과 같다. 갈리는 것은 원문이 따로 적어 둔 선언 시점 주장뿐이다. - -## 검증 환경 - -OpenJDK : 21.0.12 -graphql-java : 25.0 -확인 방식 : 두 강제 코드의 호출자 전수, 스키마 파일의 지시자 선언 검색, graphql-java 25.0 에 규칙에 맞는 선언과 어긴 선언을 각각 넣어 스키마 생성, 같은 두 선언을 스키마 게이트에 투입, 규칙에 맞는 스키마에 값 세 가지를 실어 실행 -소스 수정 : x - -## 재현 조건 - -원문 근거는 분석 문서의 #L281 절이다. - -1. 정책 자바독 첫 문장이 강제 주체를 무엇으로 적었는지 읽는다. -2. 스키마 게이트와 실행 시점 검증기를 자기 파일 밖에서 부르는 자리를 main·test·testFixtures 로 갈라 세고, 두 클래스의 생성자 접근 지정자를 읽는다. -3. 잠금 파일에서 graphql-java 판본을 확인한다. -4. 저장소의 스키마 파일을 찾아 그 지시자 선언이 있는지 본다. -5. 규칙에 맞는 선언과 어긴 선언을 각각 graphql-java 에 넣어 스키마가 만들어지는지 본다. -6. 같은 두 선언을 스키마 게이트에 넣어 결과를 대조한다. -7. 규칙에 맞는 스키마에 구성원 하나, 구성원 둘, 아무것도 없는 값을 차례로 실어 보낸다. -8. 이 계열 타입을 API 표면 목록에 적어 둔 문서 줄을 찾는다. - -## 본문 - - - -정책 클래스 자바독의 첫 문장이 강제 주체를 밝힌다. 2025년 9월 판 단일 선택 입력 규칙을 플랫폼이 강제한다는 것이다. - -## 강제하는 두 코드를 부르는 자리 - -:::evidence key="a16-f002-oneof" alt="저장소 루트에서 돌린 정적 검색 출력 69줄. 정책 클래스 자바독 전체와 지시자 상수 선언, 스키마 게이트와 실행 시점 검증기를 자기 파일 밖에서 부르는 자리가 각각 나오고 게이트 쪽에는 testFixtures 의 계약 스위트가 한 줄 섞여 있다. 이어서 정책을 참조하는 main 네 자리와 test 다섯 자리, 잠금 파일의 graphql-java 판본, 저장소의 스키마 파일 셋과 그중 지시자를 선언한 파일이 없다는 확인, 그 계약 스위트를 부르는 시험 세 자리와 그 시험의 태그와 기본 레인이 배제하는 태그, 두 강제 코드가 각각 비공개 생성자를 두어 인스턴스를 갖지 않는다는 줄, 그리고 이 계열 타입 넷을 API 표면 목록에 적어 둔 문서 네 줄이 보인다." caption="두 강제 코드의 호출자와 계약 스위트 경로·스키마·판본 대조 — 69줄 · exit 0" zoom="true" -::: - -두 강제 코드는 비공개 생성자만 두고 정적 메서드로 되어 있다. 컨테이너가 발행할 대상이 아니라 호출자가 직접 부르는 규칙 객체다. 그래서 어디에서 부르는지가 전부다. - -실행 시점 검증기는 자기 시험에서만 불린다. 스키마 게이트는 같은 시험 파일 다섯 자리와, `GraphQlSchemaContractSuite:41` 한 자리에서 불린다. - -```java -// GraphQlSchemaContractSuite.java:38-44 - GraphQlSchemaAssemblyResult assembled = GraphQlSchemaAssembler.defaults().assemble(resources); - - try { - GraphQlOneOfSchemaGate.verify(assembled.registry()); - } catch (RuntimeException ex) { - violations.add("@oneOf declaration is invalid: " + ex.getMessage()); - } -``` - -그 스위트는 `testFixtures` 소스 세트에 있어 채택자가 자기 스키마를 검사할 때 쓸 수 있고, 이 저장소에서는 `GraphQlCrossModuleContractSuiteTest` 가 세 자리에서 부른다. 그 시험은 `@Tag("graphql-contract")` 를 달았고 기본 `test` 레인이 그 태그를 배제하지 않는다. 시험을 돌리면 여기서도 돈다. 어느 쪽이든 시험이지 배포가 뜰 때 도는 코드가 아니다. - -스키마 파일은 셋이고 어디에도 그 지시자 선언이 없다. - -## 라이브러리가 어디까지 하는가 - -:::evidence key="a16-f002-oneof-run" alt="JVM 프로브 출력 16줄. 맨 첫 줄에 OpenJDK 판이 찍히고, graphql-java 25.0 에 규칙에 맞는 선언과 어긴 선언을 각각 넣은 결과가 먼저 나오고, 어긴 쪽은 스키마 생성 단계에서 예외가 나며 비널 구성원과 기본값 구성원을 각각 지목한다. 이어서 같은 두 선언을 스키마 게이트에 넣은 결과가 같은 모양으로 갈린다. 끝으로 규칙에 맞는 스키마에 구성원 하나만 채운 값, 구성원 둘을 채운 값, 아무것도 채우지 않은 값을 실어 보낸 세 결과가 보인다." caption="같은 두 선언에 대한 라이브러리와 게이트의 판정, 그리고 값 세 가지의 실행 결과 — 16줄 · exit 0" zoom="true" -::: - -규칙을 어긴 선언을 라이브러리에 넣으면 스키마가 만들어지지 않는다. `id` 는 널 허용이어야 한다고, `number` 는 기본값을 둘 수 없다고 따로 지목한다. 같은 선언을 스키마 게이트에 넣으면 같은 두 구성원을 같은 순서로 지목한다. - -값 쪽도 라이브러리가 본다. 구성원 하나만 채우면 통과하고, 둘을 채우거나 아무것도 채우지 않으면 정확히 하나여야 한다며 거부한다. - -## 원문과 갈리는 자리 - -원문은 게이트가 확인하는 선언 시점 규칙을 라이브러리가 확인하지 않는 부분이라고 적었고, 그래서 잘못된 선언이 첫 요청에서 드러난다고 했다. 위 실행이 둘 다 뒤집는다. 라이브러리가 같은 규칙을 스키마 만들 때 건다. - -규칙 자체는 지켜진다. 지키는 주체가 자바독이 적은 주체와 다를 뿐이다. - -## 확인하지 못한 것 - -계약 스위트를 실제로 돌려 보지 않았다. 확인한 것은 스위트가 게이트를 부르는 자리와, 이 저장소의 어떤 시험이 그 스위트를 부르며 어느 레인이 그 시험을 배제하지 않는지까지다. - -애플리케이션 기동 경로 전체를 띄워 두 코드가 불리지 않는 것을 확인하지 않았다. 호출자를 전수로 세었을 뿐이다. - -라이브러리가 선언을 거부하는 것은 스키마를 만드는 시점이다. 이 저장소의 배포가 스키마를 언제 만드는지, 그 실패가 기동 실패로 이어지는지는 보지 않았다. - -프로브가 쓴 스키마는 두 구성원짜리 최소 예제다. 라이브러리가 더 복잡한 선언에서도 같은 검사를 하는지는 보지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/graphql-surface/case/case-a16-f004-graphqloperationnamepolicy.md b/docs/clean-architecture-backend-template/tech-log-studio/graphql-surface/case/case-a16-f004-graphqloperationnamepolicy.md deleted file mode 100644 index c3afab9..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/graphql-surface/case/case-a16-f004-graphqloperationnamepolicy.md +++ /dev/null @@ -1,167 +0,0 @@ ---- -kind: CASE -slug: a16-f004-graphqloperationnamepolicy -title: 연산 이름 정책의 두 구현 중 하나만 배선되고, 미배선 쪽만 GraphQlOperationNamePolicy를 쓴다 -topic: graphql-surface -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a16-f004-graphqloperationnamepolicy -evidenceCapturedOn: 2026-09-03 -assets: - - key: a16-f004-graphqloperationnamepolicy - file: ../../../final/evidence/rendered/a16-f004-graphqloperationnamepolicy.svg - - key: a16-f004-graphqloperationnamepolicy-run - file: ../../../final/evidence/rendered/a16-f004-graphqloperationnamepolicy-run.svg -evidence: - - ../../../final/evidence/raw/a16-f004-graphqloperationnamepolicy.txt - - ../../../final/evidence/raw/a16-f004-graphqloperationnamepolicy-run.txt -source: - - 원본 분석 절은 final/document.md#a16#L403 이다. 세 구현의 참조 계수는 정적 검색으로, 두 구현의 판정 차이는 프로브 실행으로 확인했다. ---- - -# 연산 이름 정책의 두 구현 중 하나만 배선되고, 미배선 쪽만 GraphQlOperationNamePolicy를 쓴다 - -이름 없는 연산을 운영에서 거부하는 규칙이 `GraphQlOperationNamePolicy` 와 `GraphQlOperationSelectionHandler` 와 `GraphQlRequestEnvelopeValidator` 셋에 나뉘어 있고, 자동설정이 조립하는 것은 `GraphQlOperationSelectionHandler` 하나다. 조립되는 `GraphQlClientPolicy` 는 `defaults(...)` 가 만드는 것 하나뿐이고 그것이 `namedOperationRequired` 에 상수 `false` 를 넣으므로, `GraphQlOperationSelectionHandler` 의 이름 요구 분기는 출하 상태에서 실행되지 않는다. - -## 관계 - -- **프로파일별 정책 매니페스트가 미배선이라, 자격에서 해석된 프로파일이 아무 예산도 선택하지 않는다** - 둘 다 프로파일마다 다른 값을 고르도록 타입을 만들어 두고, 조립은 기본값 하나만 넣는다. -- **플랫폼이 강제한다고 적었지만 그 규칙을 실제로 거는 것은 라이브러리다** - 둘 다 자바독이 적은 강제 주체와 그 검사를 실제로 도는 코드가 어긋난다. -- **중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다** - 이름을 요구하는 코드가 셋인데, 어느 것이 조립되는지 보기 전에는 규칙이 걸린다고 읽게 된다. - -## 문제 - -익명 연산을 거부하는 규칙이 자바독에 적혀 있다. - -그 규칙을 거는 코드가 몇 개이고 그중 무엇이 조립되는지, 조립된 것이 실제로 그 검사를 도는지 확인했다. - -## 결론 - -거는 코드는 셋이고, 조립되는 것은 하나다. - -GraphQlOperationNamePolicy 는 미배선 인터셉터의 필드와 생성자에서만 참조되고, 그 인터셉터를 만드는 코드는 GraphQlOperationNamePolicyTest:94 한 줄이다. GraphQlRequestEnvelopeValidator 도 같은 불리언을 읽지만 생성 지점 아홉이 전부 test 와 testFixtures 다. 남는 GraphQlOperationSelectionHandler 는 GraphQlExecutionChain 과 GraphQlPlatformInstrumentation 을 거쳐 요청마다 돈다. - -그런데 그 핸들러가 읽는 namedOperationRequired 가 조립되는 정책에서 거짓이다. - -GraphQlClientPolicy 를 만드는 프로덕션 코드가 defaults(...) 하나이고, 열여섯 번째 성분에 상수 false 가 들어간다. 프로브로 돌려 보니 익명 단일 연산이 통과하고 operationId 가 anonymous 로 찍힌다. 같은 핸들러에 그 성분만 참으로 바꾸면 같은 문서가 거부된다. - -배선된 경로가 언제나 거는 것은 따로 있다. 문서에 연산이 하나도 없을 때, 요청한 이름과 맞는 연산이 문서에 없을 때, 그리고 연산이 여럿인데 어느 것을 돌릴지 지정하지 않았을 때다. 이름 요구와 달리 이 셋은 클라이언트 프로파일을 보지 않는다. - -두 구현은 같은 입력에 반대로 답하기도 한다. 두 글자짜리 이름을 미배선 정책에 넣으면 IllegalArgumentException 이 나오고, 배선된 핸들러는 통과시킨 뒤 식별자를 anonymous 로 정규화한다. ADMIN 프로파일을 요구에서 빼는 예외도 미배선 쪽에만 있다. - -판정은 원문과 같은 P3 이고, 노출이 없다는 것이 근거다. 근거는 셋으로 갈린다. 미배선 정책에는 실행 경로가 없다. 어댑터 자체가 backend.graphql.enabled 로 꺼져 있는 것이 출하 기본값이다. 그리고 배선된 핸들러가 심는 operationId 를 읽는 프로덕션 코드가 이 모듈에 없어서, anonymous 로 뭉개진 식별자가 지금 아무 데로도 흘러가지 않는다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 세 구현의 바깥 참조 전수 계수, 조립되는 정책 빈과 defaults 의 성분 확인, 배선 사슬과 어댑터 프로퍼티 조건 확인, GraphQlOperationSelectionHandler.handle 과 GraphQlOperationNamePolicy.verify 를 문서 넷과 프로파일 둘로 직접 실행 -소스 수정 : x - -## 재현 조건 - -1. GraphQlOperationNamePolicy 의 자바독에서 이름을 요구하는 이유를 읽는다. -2. GraphQlOperationNamePolicy 를 언급하는 파일을 소스 세트별로 모아 센다. -3. 그것을 쓰는 인터셉터를 생성하는 자리가 어느 소스 세트에 있는지 확인한다. -4. namedOperationRequired 를 읽는 프로덕션 코드를 전부 찾고, 그 클래스들을 만드는 자리도 함께 센다. -5. GraphQlClientPolicy 를 만드는 프로덕션 코드와 그 빈에 붙은 조건 애너테이션을 확인한다. -6. defaults 가 마지막 세 성분에 넣는 값과 그 성분의 이름을 대조한다. -7. 배선된 핸들러가 실행 사슬과 Instrumentation 을 거쳐 요청 경로에 오르는 자리와, 어댑터 자체의 프로퍼티 조건을 읽는다. -8. 조립되는 값 그대로 GraphQlOperationSelectionHandler.handle 에 익명 단일 연산, 이름이 Ab 인 연산, 이름이 HealthQuery 인 연산, 연산 둘짜리 문서를 넣는다. -9. 같은 핸들러에 namedOperationRequired 만 참으로 바꿔 익명 단일 연산을 다시 넣는다. -10. GraphQlOperationNamePolicy.production().verify 에 PUBLIC 의 이름 없는 선택과 ADMIN 의 이름 없는 선택, 그리고 PUBLIC 의 Ab 와 HealthQuery 를 넣는다. -11. GraphQlOperationName.parse 의 본문에서 빈 Optional 이 나오는 조건을 읽는다. -12. GraphQlRequestContext 의 operationId 를 읽는 프로덕션 코드를 센다. - -## 본문 - - - -`GraphQlOperationNamePolicy` 의 자바독에는 운영에서 연산에 이름을 요구하는 이유가 적혀 있다. 규격은 익명 연산 하나를 허용하고 로컬에서는 그게 편하지만, 운영에서는 추적과 비용 예외와 persisted operation 레지스트리와 사용량 분석이 공통으로 키에 쓰는 연산 이름이 없어진다. 이름이 없으면 대신 쓸 수 있는 키는 원본 문서 문자열뿐인데, 그 문자열은 길이 상한이 없고 변수까지 들어 있다. - -## 이름 요구를 거는 코드 셋과 각각의 바깥 참조 - -:::evidence key="a16-f004-graphqloperationnamepolicy" alt="저장소 루트에서 돌린 정적 검색 출력 89줄. 같은 불리언을 읽는 세 클래스의 줄 수가 먼저 나오고, GraphQlOperationNamePolicy 를 자기 파일 밖에서 쓰는 자리가 main 두 줄과 test 여덟 줄로 갈려 보인다. GraphQlOperationNameInterceptor 의 바깥 참조는 시험 한 줄뿐이고, GraphQlRequestEnvelopeValidator 를 실제로 만드는 아홉 자리는 전부 test 와 testFixtures 다. 이어서 namedOperationRequired 를 읽는 프로덕션 세 줄, GraphQlClientPolicy 인스턴스를 만드는 프로덕션 코드 전부와 그 빈을 내놓는 메서드의 조건 애너테이션, defaults 가 마지막 세 성분에 넣는 값 셋과 그 성분의 이름 셋이 나란히 나온다. 그다음 배선된 핸들러가 실행 사슬과 Instrumentation 을 거쳐 요청 경로에 오르는 자리들과 어댑터 자체의 프로퍼티 조건, GraphQlRequestContext 의 operationId 를 읽는 프로덕션 코드가 0 건이라는 확인, 그리고 GraphQlOperationNamePolicy 54행부터 61행까지와 GraphQlOperationName.parse 의 본문이 보인다." caption="세 구현의 바깥 참조 · namedOperationRequired 소비자 셋 · defaults 의 마지막 세 값 · 배선 사슬과 어댑터 조건 · operationId 소비자 0 — 89줄 · exit 0" zoom="true" -::: - -`GraphQlOperationNamePolicy`(85줄)를 자기 파일 밖에서 쓰는 프로덕션 코드는 `GraphQlOperationNameInterceptor` 의 필드 선언(\:17)과 생성자(\:24) 두 줄이다. 그 인터셉터를 만드는 자리는 저장소 전체에서 `GraphQlOperationNamePolicyTest:94` 하나이고, 자동설정에는 없다. - -`GraphQlRequestEnvelopeValidator`(152줄)의 `validateEnvelope` 도 같은 불리언을 읽어 이름 없는 요청을 거부한다(\:134). 이 클래스를 만드는 자리는 아홉인데 여덟은 자기 시험이고 나머지 하나가 `GraphQlContractFixture:84` 다. 프로덕션 조립에는 없다. - -남는 것이 `GraphQlOperationSelectionHandler`(125줄)다. `GraphQlPlatformAutoConfiguration:404` 가 `new GraphQlOperationSelectionHandler(clientPolicy)` 로 만들어 `:403` 의 `GraphQlExecutionChain.stable` 에 넣고, `:420` 이 그 사슬을 `GraphQlPlatformInstrumentation` 으로 감싼다. Spring for GraphQL 이 `Instrumentation` 빈을 집어가므로 `GraphQlPlatformInstrumentation:81` 의 `chain.run` 이 실제 요청마다 돈다. 이 핸들러의 생성자가 받는 것은 `GraphQlOperationNamePolicy` 가 아니라 `GraphQlClientPolicy` 다. - -어댑터 자체는 기본으로 꺼져 있다. `GraphQlRootAutoConfiguration:21` 이 `@ConditionalOnProperty(prefix = "backend.graphql", name = "enabled", havingValue = "true")` 를 달고 있어서, 아래의 통과와 거부는 그 프로퍼티를 켠 배포에서만 일어난다. - -## 조립되는 GraphQlClientPolicy 의 namedOperationRequired 는 false 다 - -`GraphQlClientPolicy` 인스턴스를 만드는 프로덕션 코드는 `GraphQlClientPolicy.defaults` 와 그 안의 생성자 호출뿐이고, 그 `defaults` 를 부르는 자리는 `GraphQlPlatformAutoConfiguration:303` 하나다. 그 메서드에 `@ConditionalOnMissingBean` 이 붙어 있어 채택자가 자기 빈을 내놓지 않으면 이것이 쓰인다. - -```java -// GraphQlClientPolicy.java:85-104 (마지막 세 성분) - public static GraphQlClientPolicy defaults( - int maximumPageSize, long maximumComplexity, boolean introspectionAllowed) { - return new GraphQlClientPolicy( - …, - introspectionAllowed, // :101 → :49 introspectionAllowed - false, // :102 → :50 persistedOperationOnly - false); // :103 → :51 namedOperationRequired - } -``` - -레코드 성분 순서에서 `namedOperationRequired` 는 열여섯 번째이자 마지막(\:51)이고, `defaults` 가 그 자리에 넣는 값은 상수 `false` 다. 이 값을 프로퍼티로 바꿀 수 있는 경로는 없다. - -## 같은 문서를 배선된 핸들러는 통과시키고 미배선 정책은 거부한다 - -:::evidence key="a16-f004-graphqloperationnamepolicy-run" alt="JVM 프로브 출력 19줄. 맨 앞에 OpenJDK 판이 찍히고, 조립되는 defaults 가 namedOperationRequired 에 false 를 넣는다는 것이 먼저 나온다. 이어서 배선된 GraphQlOperationSelectionHandler 에 그 값을 그대로 넣었을 때 익명 단일 연산과 이름이 Ab 인 연산이 모두 통과하고 둘 다 operationId 가 anonymous 로 찍히며, 이름이 HealthQuery 인 연산은 통과하면서 operationId 가 healthquery 로 정규화되고, 연산 둘에 operationName 이 없는 문서만 거부된다. 같은 핸들러에 namedOperationRequired 만 true 로 바꾸면 익명 단일 연산이 거부된다. 끝으로 미배선 GraphQlOperationNamePolicy.production().verify 가 PUBLIC 의 이름 없는 연산은 GraphQlAnonymousOperationException 으로 거부하고, ADMIN 의 이름 없는 연산은 통과시키며, 이름이 Ab 인 연산에서는 IllegalArgumentException 을 던지고, 이름이 HealthQuery 인 연산은 통과시키는 것이 보인다." caption="조립되는 값과 두 구현의 판정 · 같은 이름에 통과와 예외가 갈린다 — 19줄 · exit 0" zoom="true" -::: - -조립되는 값 그대로 배선된 핸들러에 문서를 넣으면 익명 단일 연산이 통과한다. `:74` 의 `policy.namedOperationRequired() && selected.getName() == null` 에서 앞쪽이 거짓이라 뒤를 보지 않는다. 같은 핸들러에 그 성분만 `true` 로 바꾸면 같은 문서가 `GraphQlAnonymousOperationException` 으로 거부된다. - -거부가 남아 있는 것은 셋이다. 연산이 하나도 없는 문서(\:53), `operationName` 이 가리키는 연산이 문서에 없는 경우(\:65), 그리고 연산이 둘 이상인데 `operationName` 이 없는 경우(\:68)다. 마지막 것을 거절이 아니라 기본 선택으로 두면 클라이언트가 문서 순서를 바꿔 실행 대상을 바꿀 수 있고 그것이 인가 우회가 된다고 이 핸들러의 클래스 자바독이 적는다. 이 셋은 클라이언트 프로파일과 무관하게 항상 건다. - -두 구현이 반대로 답하는 입력도 있다. 연산 이름이 `Ab` 인 문서에서 배선된 핸들러는 통과시키고 `operationId` 를 `anonymous` 로 정규화한다. `MINIMUM_OPERATION_ID_LENGTH` 가 3 이라 두 글자는 식별자가 되지 못한다. 정규화 메서드의 자바독에는 이름 규칙 때문에 유효한 요청을 거절하지는 않는다고 적혀 있다. 미배선 정책에 같은 이름을 넣으면 `IllegalArgumentException : invalid GraphQL operation name` 이 나온다. - -프로파일 예외도 한쪽에만 있다. `GraphQlOperationNamePolicy:49` 는 `production && !ADMIN.equals(client.value())` 로 ADMIN 을 요구에서 뺀다. 배선된 경로에는 프로파일별 분기가 없고 불리언 하나를 읽는다. - -## GraphQlOperationNamePolicy\:57 은 실행되지 않는다 - -이름이 규칙을 어겼을 때 `GraphQlAnonymousOperationException` 을 던지려고 쓴 검사인데, 그 예외는 나오지 않는다. - -```java -// GraphQlOperationNamePolicy.java:54-61 - if (namedRequired && !selection.named()) { - throw new GraphQlAnonymousOperationException("named operation required"); - } - if (selection.named() && GraphQlOperationName.parse(selection.operationName()).isEmpty()) { - // Validates the bounded naming pattern; an unbounded name would defeat the reason for - // requiring one at all. - throw new GraphQlAnonymousOperationException("named operation required"); - } -``` - -`selection.named()` 가 참이면 `operationName` 은 널도 공백도 아니다. 그 값을 받은 `GraphQlOperationName.parse` 는 빈 `Optional` 을 돌려주는 분기(널이거나 공백)에 들어가지 않고 `new GraphQlOperationName(candidate)` 로 간다. 그 생성자는 `[A-Za-z][_0-9A-Za-z]{2,127}` 에 맞지 않으면 `IllegalArgumentException` 을 던진다. 그래서 `isEmpty()` 는 이 자리에서 참이 될 수 없고, 60행의 `throw` 는 실행되지 않는다. - -`GraphQlRequestEnvelopeValidator:134` 에도 `parse(...).isEmpty()` 를 보는 같은 검사가 있지만 결과가 다르다. `envelope.operationName()` 은 클라이언트가 보내지 않으면 널이므로 `parse` 가 빈 `Optional` 을 돌려주고, 의도한 `GraphQlRequestFormatException` 이 나온다. - -## 원문과 갈리는 자리 - -원문 §12.2 는 익명 연산 거부를 배선된 `GraphQlOperationSelectionHandler` 가 수행하므로 강제 자체는 존재한다고 적고, 기록할 것은 정책 객체의 이원화라고 했다. 위 실행은 그중 강제가 존재한다는 부분을 뒤집는다. 조립되는 `GraphQlClientPolicy` 가 `namedOperationRequired` 에 `false` 를 넣으므로, 이름을 요구하는 강제는 출하 상태에서 어느 코드도 걸지 않는다. 배선된 핸들러가 항상 거는 것은 연산이 없는 문서, 지정한 이름의 연산이 없는 문서, 연산이 여럿인데 `operationName` 이 없는 문서 세 가지이지 이름 요구가 아니다. - -원문 §11.2 가 미배선 인터셉터를 누락이 아니라 중복이라고 판정한 것도 함께 흔들린다. `GraphQlOperationNamePolicy.production()` 은 ADMIN 이 아닌 프로파일에 이름을 강제하는데 배선된 핸들러는 지금 그것을 강제하지 않으므로, 두 구현은 같은 일을 하지 않는다. - -원문의 인용에도 어긋난 곳이 둘 있다. 배선된 검사를 `:75` 로 적었는데 실제는 `:74` 다. 그리고 `GraphQlOperationNamePolicy` 의 참조자를 미배선 인터셉터와 자기 자신뿐이라고 적었는데 자기 시험 여덟 줄이 빠졌다. - -이원화 판정과 P3 등급은 원문과 같다. - -## 확인하지 못한 것 - -프로브는 실행 사슬의 한 단계만 직접 불렀다. 요청을 엔드포인트로 쏘아 응답을 받아 본 것은 아니다. - -채택자가 `GraphQlClientPolicy` 빈을 직접 등록하면 `@ConditionalOnMissingBean` 이라 `namedOperationRequired` 가 참이 될 수 있다. 그런 배포를 띄워 보지는 않았다. - -브라우저나 HTTP 클라이언트로 실제 GraphQL 요청을 보내지 않았다. 프로브는 `GraphQlOperationSelectionHandler.handle` 을 직접 불렀고, 그 앞뒤에 있는 전송 계층과 인증 계층은 거치지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/graphql-surface/case/case-analysis-finding-a16-f001.md b/docs/clean-architecture-backend-template/tech-log-studio/graphql-surface/case/case-analysis-finding-a16-f001.md deleted file mode 100644 index cfdb85f..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/graphql-surface/case/case-analysis-finding-a16-f001.md +++ /dev/null @@ -1,128 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a16-f001 -title: 스키마 조립·계약 정체성·해시 사슬이 통째로 미배선이고, 그것을 발행할 액추에이터 엔드포인트도 등록되지 않는다 -topic: graphql-surface -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a16-f001 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a16-f001.body.md -assets: - - key: analysis-finding-a16-f001 - file: ../../../final/evidence/rendered/analysis-finding-a16-f001.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a16-f001.txt -source: - - 원본 분석 절은 final/document.md#a16#L258 이다. ---- - -# 스키마 조립·계약 정체성·해시 사슬이 통째로 미배선이고, 그것을 발행할 액추에이터 엔드포인트도 등록되지 않는다 - -조립기부터 보고서까지 사슬 전체가 끊겨 있다. 각 단계의 호출자와 생산자와 빈 선언이 모두 0 이다. 그 결과 결정적 병합 순서와 네 부분 계약 정체성과 운영 가시성이 함께 사라진다. - -## 관계 - -- **시작 검증기가 시작 시 실행되지 않는다** - 다른 리프의 같은 형태다. -- **5계층 예산 모델에서 요청 계층만 강제되고 나머지 파생이 전부 미배선이다** - 같은 리프의 다른 미배선 사슬이다. -- **만들어졌으나 아무도 만들지 않는 타입은 계약이 아니다** - 이 사례가 그 규칙의 형태다. - -## 문제 - -이 리프는 스키마 조각을 병합해 스키마를 만들고, 그 결과에서 해시를 얻고, 해시를 포함한 계약 정체성을 만들고, 그것을 운영 보고서로 발행하도록 설계되어 있다. - -각 단계가 실제로 불리는지 확인했다. - -## 결론 - -전 단계가 끊겨 있다. - -조립 메서드의 호출자가 0 이고, 조립 결과 타입의 생산자가 0 이고, 해시 타입의 생산자가 0 이다. - -보고 엔드포인트의 빈 선언이 0 이고, 그 보고서의 발행 경로가 0 이다. - -네 부분 계약 정체성 타입의 생성자 호출도 0 이다. - -세 가지가 함께 사라진다. - -첫째는 결정적 병합 순서다. - -스키마 조각의 정렬을 조립기가 강제하도록 설계되어 있는데, 실제로는 프레임워크의 탐색 순서를 그대로 쓴다. - -조각이 하나뿐인 지금은 무해하다. 그러나 채택자가 자기 조각을 추가하는 순간 달라진다. 그것이 이 리프의 문서화된 확장 방식이다. - -충돌 선언의 승자와 스키마 해시가 포장 방식에 따라 달라질 수 있다. - -둘째는 네 부분 계약 정체성이다. - -해시만으로는 호환성을 판정할 수 없다는 판단을 타입으로 만들었는데, 그 타입을 만드는 코드가 없다. - -호환성 판정이 필요한 릴리스 게이트는 별도의 비교기를 직접 쓴다. - -셋째는 운영 가시성이다. - -보고 메서드가 배포된 스키마 해시와 실행 프로파일과 배포 모드와 활성 능력과 등록된 연산 및 페치 프로파일 수를 하나의 보고서로 낸다. - -등록되지 않으므로 운영자가 이 배포가 무엇을 켜고 있는가를 물을 표면이 없다. - -다른 리프의 시작 검증기 사례와 같은 형태다. - -거기서는 시작 검증기가 시작 시 실행되지 않았고, 여기서는 보고 엔드포인트가 등록되지 않는다. - -두 모듈 모두 파일 서버 하위 트리와 대조된다. 그쪽의 증명 메서드는 부트스트랩에서 실제로 호출된다. - -권고는 이렇다. - -플랫폼 자동 설정이 이미 서른아홉 빈을 만들고 해시 타입을 세 곳에서 참조하므로 자리는 있다. - -스키마 원본이 확정된 뒤 그 정의로 조립기를 돌려 해시를 얻고, 그것으로 엔드포인트를 등록하면 된다. - -판정은 P2 다. - -## 검증 환경 - -확인 방식 : 사슬 각 단계의 호출자와 생산자 계수 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/182 계열에 있다. - -1. 조립 메서드의 호출자를 센다. -2. 조립 결과와 해시 타입의 생산자를 센다. -3. 보고 엔드포인트의 빈 선언을 검색한다. -4. 계약 정체성 타입의 생성자 호출을 센다. -5. 릴리스 게이트가 호환성을 어떻게 판정하는지 확인한다. - -## 본문 - - - -세 가지가 통째로 미배선이다. - -## GraphQlSchemaContract 참조 위치 - -:::evidence key="analysis-finding-a16-f001" alt="코드베이스에서 GraphQlSchemaContract 를 검색한 출력 4줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GraphQlSchemaContract 코드베이스 검색 — 4줄 · exit 0" zoom="true" -::: - -## 1. 결정적 병합 순서 - -SDL 조각의 정렬을 조립기가 강제하도록 설계돼 있고(§7.4), 실제로는 Spring GraphQL의 탐색 순서를 그대로 쓴다. 조각이 하나(`skeleton.graphqls`)뿐인 지금은 무해하지만, adopter가 자기 `.graphqls`를 추가하는 순간 — 그것이 이 leaf의 문서화된 확장 방식이다 — 충돌 선언의 승자와 스키마 해시가 패키징 방식에 따라 달라질 수 있다. - -## 2. 네 부분 계약 정체성 - -`GraphQlSchemaContract`가 "해시만으로는 호환성을 판정할 수 없다"는 판단을 타입으로 만들었는데, 그 타입을 만드는 코드가 없다. 호환성 판정이 필요한 곳(릴리스 게이트)은 `compat`의 비교기를 직접 쓴다. - -## 3. 운영 가시성 - -`GraphQlPlatformActuatorEndpoint.report()`가 배포된 스키마 해시 · 실행 프로파일 · 배포 모드 · 활성 능력 · 등록된 연산/페치 프로파일 수를 하나의 보고서로 낸다. 등록되지 않으므로 운영자가 "이 배포가 무엇을 켜고 있는가"를 물을 표면이 없다. - -## 확인하지 못한 것 - -조각을 추가해 병합 순서가 달라지는 것을 재현하지 않았다. 조각이 하나뿐이라 현재 형상에서는 드러나지 않는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/graphql-surface/case/case-analysis-finding-a16-f003.md b/docs/clean-architecture-backend-template/tech-log-studio/graphql-surface/case/case-analysis-finding-a16-f003.md deleted file mode 100644 index ff1b8c7..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/graphql-surface/case/case-analysis-finding-a16-f003.md +++ /dev/null @@ -1,128 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a16-f003 -title: 5계층 예산 모델에서 요청 계층만 강제되고, 나머지 파생이 전부 미배선이다 -topic: graphql-surface -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a16-f003 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a16-f003.body.md -assets: - - key: analysis-finding-a16-f003 - file: ../../../final/evidence/rendered/analysis-finding-a16-f003.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a16-f003.txt -source: - - 원본 분석 절은 final/document.md#a16#L382 이다. ---- - -# 5계층 예산 모델에서 요청 계층만 강제되고, 나머지 파생이 전부 미배선이다 - -마감 전파기의 자바독이 다섯 계층이 왜 분리되는지 정확히 적는다. 그 파생을 수행하는 다섯 메서드 전부 프로덕션 호출자가 0 이다. 요청 전체 마감만 작동한다. - -## 관계 - -- **스키마 조립과 계약 정체성과 해시 사슬이 통째로 미배선이고 그것을 발행할 엔드포인트도 등록되지 않는다** - 같은 리프의 다른 미배선 사슬이다. -- **용량 보호 계층 전체가 자기 테스트 픽스처 안에서만 실행된다** - 같은 형태의 미등록 사례다. -- **계층을 나눈 이유가 코드에 적혀 있어도 파생이 없으면 계층은 하나다** - 이 사례가 그 규칙의 형태다. - -## 문제 - -이 리프는 마감을 다섯 계층으로 나눈다. - -마감 전파기의 자바독이 그 이유를 적는다. - -계층이 서로 다른 것을 뜻하고 서로 다른 시점에 만료되기 때문이라는 것이다. 전송 악수와 요청 실행과 하나의 해석기와 하나의 적재 배치, 그리고 구독의 경우 연결 자체다. - -마지막 것은 의도적으로 요청 예산에서 파생하지 않는다는 것이다. 구독은 오래 사는 흐름이고 오 초 요청 시간 제한을 적용하면 모든 구독이 시작 오 초 뒤에 끝나기 때문이라는 것이다. - -## 결론 - -그 파생을 수행하는 메서드 다섯 개 전부 프로덕션 호출자가 0 이다. - -실제로 강제되는 것과 아닌 것이 갈린다. - -요청 전체 마감은 작동한다. 플랫폼 인터셉터가 만들고 취소 장치가 끊는다. - -개별 해석기가 남은 요청 예산으로 잘리는 것은 없다. - -적재 배치 시간 제한이 남은 요청 예산으로 잘리는 것도 없다. - -하류 호출에 남은 예산이 전달되는 것도 없다. - -실패 시나리오는 이렇다. - -요청 예산이 오 초이고 해석기 하나가 하류 호출을 부른다. - -그 호출에 전달되는 마감은 나가는 어댑터 자신의 기본값이고, 남은 요청 예산이 일 초라는 사실은 전달되지 않는다. - -요청은 오 초에 취소되지만 하류 호출은 계속 진행되어 연결과 스레드를 사 초 더 붙잡는다. - -전파기의 하류 마감 메서드가 정확히 그 죄기를 위해 존재한다. - -적재 쪽은 더 직접적이다. - -배치 문맥이 마감을 레코드 성분으로 갖지만, 그 값을 남은 요청 예산으로 잘라 넣는 코드가 배치 시간 제한 메서드이고 호출자가 없다. - -권고는 전파기를 빈으로 등록하고 세 지점에 연결하는 것이다. - -배치 등록기가 배치 시간 제한을, 해석기 실행 경로가 해석기 예산을, 나가는 포트 호출 지점이 하류 마감을 쓰게 한다. - -구독 계층은 별도 하위 범위에서 확인한다. - -판정은 P2 다. - -## 검증 환경 - -확인 방식 : 파생 메서드 호출자 계수와 강제 지점 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/182 계열에 있다. - -1. 마감 전파기의 자바독을 읽는다. -2. 파생 메서드 다섯을 나열한다. -3. 각 메서드의 프로덕션 호출자를 센다. -4. 요청 전체 마감이 어디서 만들어지고 어디서 끊기는지 확인한다. -5. 배치 문맥이 마감을 어떻게 받는지 확인한다. - -## 본문 - - - -`GraphQlDeadlinePropagator`의 javadoc이 계층 분리의 이유를 정확히 적는다. - -> "The layers are separate because they mean different things and expire at different points: a transport handshake, the request execution, one resolver, one DataLoader batch, and — for a subscription — the connection itself. The last one is deliberately not derived from the request budget: **a subscription is a long-lived stream, and applying a five-second request timeout to it would terminate every subscription five seconds after it started.**" - -## GraphQlDeadlinePropagator 참조 위치 - -:::evidence key="analysis-finding-a16-f003" alt="코드베이스에서 GraphQlDeadlinePropagator 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GraphQlDeadlinePropagator 코드베이스 검색 — 2줄 · exit 0" zoom="true" -::: - -## 파생 메서드 다섯 전부 호출자가 0이다 - -§11.1·§11.3. - -## 강제되는 것과 아닌 것 - -요청 전체 데드라인은 `GraphQlPlatformWebInterceptor`가 만들고 `GraphQlCancellation`이 끊는다 — 작동한다. 개별 리졸버가 남은 요청 예산으로 잘리는 것, DataLoader 배치 타임아웃이 남은 요청 예산으로 잘리는 것, DB/HTTP 다운스트림 호출에 남은 예산이 전달되는 것은 없다. - -## 실패 시나리오 - -요청 예산이 5초이고 리졸버 하나가 다운스트림 HTTP를 부른다. 그 호출에 전달되는 데드라인은 아웃바운드 어댑터 자신의 기본값(예: 10초)이고, 남은 요청 예산이 1초라는 사실은 전달되지 않는다. 요청은 5초에 취소되지만 다운스트림 호출은 계속 진행되어 연결과 스레드를 4초 더 붙잡는다. `GraphQlDeadlinePropagator.downstreamDeadline`이 정확히 그 clamping을 위해 존재한다. DataLoader 쪽은 더 직접적이다 — `dataloader/GraphQlBatchContext`가 `GraphQlDeadline`을 레코드 컴포넌트로 갖지만, 그 값을 남은 요청 예산으로 잘라 넣는 코드가 `dataLoaderBatchTimeout`이고 호출자가 없다. P2. - -## 권고 - -`GraphQlDeadlinePropagator`를 빈으로 등록하고 세 지점에 연결한다 — `GraphQlBatchLoaderRegistrar`(autoconf=4)가 배치 타임아웃을, 리졸버 실행 경로가 `resolverBudget`을, 아웃바운드 포트 호출 지점이 `downstreamDeadline`을 쓰게 한다. - -## 확인하지 못한 것 - -요청 취소 후 하류 호출이 계속되는 것을 실행으로 재현하지 않았다. 호출자 부재상 그 결과가 나온다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/graphql-surface/case/case-analysis-finding-a16-f005.md b/docs/clean-architecture-backend-template/tech-log-studio/graphql-surface/case/case-analysis-finding-a16-f005.md deleted file mode 100644 index f887d8c..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/graphql-surface/case/case-analysis-finding-a16-f005.md +++ /dev/null @@ -1,122 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a16-f005 -title: 설정으로 정한 파서 한계가 graphql-java에 설치되지 않는다 -topic: graphql-surface -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a16-f005 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a16-f005.body.md -assets: - - key: analysis-finding-a16-f005 - file: ../../../final/evidence/rendered/analysis-finding-a16-f005.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a16-f005.txt -source: - - 원본 분석 절은 final/document.md#a16#L511 이다. ---- - -# 설정으로 정한 파서 한계가 graphql-java에 설치되지 않는다 - -파서 계층은 이 방어의 첫 번째 관문이다. 리프가 한계를 값으로 갖고 설치 함수도 갖는데 호출하는 코드가 없다. 파서 옵션이 정적 전역이라 시작 시 한 번 설치하지 않으면 라이브러리 기본값이 유지된다. - -## 관계 - -- **배열 원소 상한이 선언만 되고 강제되지 않으며 백스톱도 없다** - 같은 형태의 선언과 강제 어긋남이다. -- **5계층 예산 모델에서 요청 계층만 강제되고 나머지 파생이 전부 미배선이다** - 같은 리프의 다른 미배선 사례다. -- **설정은 받아들여지고 검증되며 효과가 없다** - 이 사례가 그 규칙의 형태다. - -## 문제 - -파서 계층은 이 프로토콜의 서비스 거부 방어에서 첫 번째 관문이다. - -복잡도 계산도 구조 분석도 문서를 파싱한 뒤에 일어나므로, 파싱 자체를 폭발시키는 문서는 그 앞에서 막아야 한다. - -이 리프가 그 한계를 실제로 거는지 확인했다. - -## 결론 - -걸지 않는다. - -한계를 값으로 갖는 타입이 있고 클라이언트 정책에서 파생된다. - -설치 함수도 있다. 연산 기본값 설치라는 이름이다. - -호출하는 코드가 없다. - -이름이 가리키듯 이 라이브러리의 파서 옵션은 정적 전역이다. 시작 시 한 번 설치하지 않으면 라이브러리 기본값이 유지된다. - -실패 시나리오는 이렇다. - -운영자가 설정으로 파서 한계를 조인다. - -그 값은 플랫폼 설정을 거쳐 클라이언트 정책까지 도달한다. 그러나 한계 타입을 만드는 팩토리를 부르는 코드가 없어 파서에 닿지 않는다. - -실제로 적용되는 것은 라이브러리의 기본값이다. - -설정은 받아들여지고 검증되며 효과가 없다. - -노출의 크기는 라이브러리 기본값이 정한다. - -이 판본은 토큰 수와 공백 토큰 수와 규칙 깊이에 자체 기본 상한을 두므로 무제한은 아니다. - -그리고 구조 한계와 복잡도 계산은 배선되어 있어 파싱 이후 계층은 작동한다. - -그래서 P1 이 아니라 P2 다. 침묵하는 설정 표면이자 방어 계층 하나의 부재다. - -권고는 플랫폼 자동 설정에 시작 시 한 번 설치를 부르는 초기화 지점을 두는 것이다. - -정적 전역이므로 빈 메서드보다 초기화 콜백이 적절하다. - -## 검증 환경 - -확인 방식 : 설치 함수 호출자 검색과 파서 옵션 전역성 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/182 계열에 있다. - -1. 파서 한계 타입과 설치 함수를 확인한다. -2. 설치 함수의 호출자를 센다. -3. 라이브러리의 파서 옵션이 정적 전역인지 확인한다. -4. 설정 값이 어디까지 도달하는지 추적한다. -5. 구조 한계와 복잡도 계산이 배선되는지 확인한다. - -## 본문 - - - -파서 계층은 GraphQL DoS 방어의 **첫 번째** 관문이다 — 복잡도 계산도 구조 분석도 문서를 파싱한 뒤에 일어나므로, 파싱 자체를 폭발시키는 문서는 그 앞에서 막아야 한다. - -## 파서 계층이 첫 관문인 이유 - -:::evidence key="analysis-finding-a16-f005" alt="분석 문서 final/document.md#a16 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a16 발췌 — 15줄" zoom="true" -::: - -## 값도 있고 설치 함수도 있고 호출하는 코드가 없다 - -이 leaf는 그 한계를 값으로 갖고(`GraphQlParserLimits`, 클라이언트 정책에서 파생), 설치 함수를 갖는다(`GraphQlParserOptionsFactory.installOperationDefaults`). `installOperationDefaults`라는 이름이 가리키듯 graphql-java의 파서 옵션은 정적 전역(`ParserOptions.setDefaultOperationParserOptions`)이고, 시작 시 한 번 설치하지 않으면 라이브러리 기본값이 유지된다. - -## 실패 시나리오 - -운영자가 `backend.graphql.limits.*`로 파서 한계를 조인다. 그 값은 `GraphQlPlatformSettings` → `GraphQlClientPolicy`까지 도달하지만 `GraphQlParserLimits.from(...)`을 부르는 코드가 없어 파서에 닿지 않는다. 실제로 적용되는 것은 graphql-java 25.0의 기본값이다. 설정은 받아들여지고 검증되며 효과가 없다. - -## 노출의 크기는 라이브러리 기본값이 정한다 - -graphql-java 25.0은 토큰 수·공백 토큰 수·규칙 깊이에 자체 기본 상한을 두므로 무제한은 아니다. 그리고 구조 한계(`GraphQlDocumentShapeAnalyzer`, autoconf=4)와 복잡도 계산(autoconf=4)은 배선돼 있어 파싱 이후 계층은 작동한다. 그래서 P1이 아니라 P2다 — 침묵하는 설정 표면이자 방어 계층 하나의 부재다. - -## 권고 - -`GraphQlPlatformAutoConfiguration`에 시작 시 `GraphQlParserOptionsFactory.installOperationDefaults(GraphQlParserLimits.from(clientPolicy))`를 한 번 호출하는 초기화 지점을 둔다. 정적 전역이므로 `@Bean` 메서드보다 `InitializingBean`/`SmartInitializingSingleton`이 적절하다. - -## 확인하지 못한 것 - -파서를 폭발시키는 문서를 보내 라이브러리 기본값이 적용되는지 재현하지 않았다. 호출자 부재상 그 결과가 나온다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/graphql-surface/case/case-analysis-finding-a16-f006.md b/docs/clean-architecture-backend-template/tech-log-studio/graphql-surface/case/case-analysis-finding-a16-f006.md deleted file mode 100644 index d52d435..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/graphql-surface/case/case-analysis-finding-a16-f006.md +++ /dev/null @@ -1,116 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a16-f006 -title: 프로파일별 정책 매니페스트가 미배선이라, 자격에서 해석된 프로파일이 아무 예산도 선택하지 않는다 -topic: graphql-surface -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a16-f006 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a16-f006.body.md -assets: - - key: analysis-finding-a16-f006 - file: ../../../final/evidence/rendered/analysis-finding-a16-f006.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a16-f006.txt -source: - - 원본 분석 절은 final/document.md#a16#L525 이다. ---- - -# 프로파일별 정책 매니페스트가 미배선이라, 자격에서 해석된 프로파일이 아무 예산도 선택하지 않는다 - -클라이언트 프로파일은 검증된 주체에서 정확히 해석되고 요청 문맥에 실린다. 그 값이 선택하는 것은 캐시 키와 지표 태그뿐이다. 정책은 프로파일과 무관하게 단일 빈이다. - -## 관계 - -- **연산 이름 정책의 두 구현 중 하나만 배선되고 미배선 쪽만 정책 객체를 쓴다** - 같은 리프의 정책 이원화 사례다. -- **설정으로 정한 파서 한계가 라이브러리에 설치되지 않는다** - 같은 리프의 다른 미배선 사례다. -- **값이 정확히 해석되어도 그것을 쓰는 조회가 없으면 효과가 없다** - 이 사례가 그 규칙의 형태다. - -## 문제 - -이 리프는 클라이언트 프로파일별로 다른 예산을 줄 수 있도록 설계되어 있다. - -프로파일이 실제로 예산을 선택하는지 확인했다. - -## 결론 - -선택하지 않는다. - -클라이언트 프로파일은 검증된 주체에서 정확히 해석되고 요청 문맥에 실린다. - -그리고 그 값이 선택하는 것은 캐시 키와 지표 태그뿐이다. - -정책은 프로파일과 무관하게 단일 빈이다. - -설계가 이 구조를 명시적으로 거부한다. - -성능 측정된 한계를 애플리케이션 코드가 아니라 환경 매니페스트에 둔다는 것이다. - -지금은 코드 안의 기본 정책 하나다. - -실패 시나리오는 이렇다. - -배포가 내부 배치 클라이언트에는 큰 복잡도 예산을, 공개 이동 클라이언트에는 작은 예산을 주려 한다. - -두 프로파일이 자격에서 정확히 구분되고, 두 요청 모두 같은 정책으로 평가된다. - -프로파일을 나눈 목적이 달성되지 않으며, 그 사실은 어떤 오류로도 드러나지 않는다. - -매니페스트가 약속한 성질도 성립할 기회가 없다. - -알 수 없는 프로파일은 관대한 기본값으로 조용히 떨어지는 대신 시작이나 요청 실패가 된다는 것인데, 조회가 일어나지 않기 때문이다. - -권고는 단일 정책 빈을 매니페스트 빈으로 바꾸고, 정책을 요구하는 여덟 지점이 요청 문맥의 프로파일로 조회하게 하는 것이다. - -매니페스트는 중복 프로파일을 생성자에서 거부하므로 설정 오류가 부팅에서 드러난다. - -판정은 P2 다. - -## 검증 환경 - -확인 방식 : 프로파일 해석 경로와 정책 빈 구성 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/182 계열에 있다. - -1. 클라이언트 프로파일이 어디서 해석되는지 확인한다. -2. 그 값이 요청 문맥에 실리는지 확인한다. -3. 그 값을 읽는 지점을 모두 센다. -4. 정책 빈이 프로파일별인지 단일인지 확인한다. -5. 매니페스트 타입의 생성자와 조회 메서드를 읽는다. - -## 본문 - - - -클라이언트 프로파일은 검증된 principal에서 정확히 해석되고 요청 컨텍스트에 실린다(§15.2). 그리고 그 값이 선택하는 것은 **캐시 키와 지표 태그뿐**이다 — 정책은 프로파일과 무관하게 단일 빈이다. - -## 프로파일이 실제로 고르는 것 - -:::evidence key="analysis-finding-a16-f006" alt="분석 문서 final/document.md#a16 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a16 발췌 — 15줄" zoom="true" -::: - -## 설계가 이 구조를 명시적으로 거부한다 - -"The design keeps benchmarked limits in an environment manifest rather than in application code." 지금은 코드 안의 `GraphQlClientPolicy.defaults(properties)` 하나다. - -## 실패 시나리오 - -배포가 내부 배치 클라이언트에는 큰 복잡도 예산을, 공개 모바일 클라이언트에는 작은 예산을 주려 한다. 두 프로파일이 자격에서 정확히 구분되고, 두 요청 모두 같은 `GraphQlClientPolicy`로 평가된다. 프로파일을 나눈 목적이 달성되지 않으며, 그 사실은 어떤 오류로도 드러나지 않는다 — `GraphQlClientPolicyManifest`가 약속한 "An unknown profile is a startup or request failure rather than a silent fallback to a permissive default"는 조회가 일어나지 않으므로 성립할 기회가 없다. P2. - -## 권고 - -`GraphQlClientPolicy` 단일 빈을 `GraphQlClientPolicyManifest` 빈으로 바꾸고, 정책을 요구하는 여덟 지점이 요청 컨텍스트의 프로파일로 조회하게 한다. 매니페스트는 중복 프로파일을 생성자에서 거부하므로 설정 오류가 부팅에서 드러난다. - -## 확인하지 못한 것 - -두 프로파일로 요청을 보내 같은 예산이 적용되는 것을 재현하지 않았다. 단일 빈 구성상 그 결과가 나온다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/graphql-surface/case/case-analysis-finding-a16-f008.md b/docs/clean-architecture-backend-template/tech-log-studio/graphql-surface/case/case-analysis-finding-a16-f008.md deleted file mode 100644 index 7bf5f8b..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/graphql-surface/case/case-analysis-finding-a16-f008.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a16-f008 -title: 파싱·검증 실패에 플랫폼 매퍼가 없다 -topic: graphql-surface -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a16-f008 -evidenceCapturedOn: 2026-09-01 -assets: - - key: analysis-finding-a16-f008 - file: ../../../final/evidence/rendered/analysis-finding-a16-f008.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a16-f008.txt -source: - - 원본 분석 절은 final/document.md#a16#L680 이다. ---- - -# 파싱·검증 실패에 플랫폼 매퍼가 없다 - -요청 오류 매퍼가 그 목적으로 존재하고 미배선이다. 배선된 두 매퍼는 각각 해석기 예외와 플랫폼 거부를 덮는다. 구문 오류 응답에는 이 플랫폼의 안정 확장이 붙지 않는다. - -## 관계 - -- **설정으로 정한 파서 한계가 라이브러리에 설치되지 않는다** - 같은 리프의 다른 파서 계층 사례다. -- **5계층 예산 모델에서 요청 계층만 강제되고 나머지 파생이 전부 미배선이다** - 같은 리프의 다른 미배선 사례다. -- **오류 코드로 분기하는 클라이언트는 형식이 갈리는 지점에서 빗나간다** - 이 사례가 그 규칙의 형태다. - -## 문제 - -이 리프는 오류 응답에 안정 코드와 분류 확장을 붙인다. - -모든 실패 종류가 그것을 받는지 확인했다. - -## 결론 - -두 종류만 받는다. - -배선된 매퍼가 둘이다. 하나는 해석기 예외를 덮고 하나는 플랫폼 거부를 덮는다. - -파싱과 검증 실패를 덮을 매퍼가 그 목적으로 존재하는데 미배선이다. - -결과적으로 구문 오류나 검증 실패의 응답에는 이 플랫폼의 안정 코드와 분류 확장이 붙지 않고 라이브러리의 기본 형식이 나간다. - -클라이언트가 오류 코드로 분기한다면 그 분기가 파싱 오류에서만 빗나간다. - -판정은 P3 다. - -## 검증 환경 - -확인 방식 : 매퍼별 배선 여부와 담당 범위 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/182 계열에 있다. - -1. 오류 매퍼 타입을 모두 나열한다. -2. 각각이 어떤 실패를 덮는지 확인한다. -3. 각각이 배선되는지 확인한다. -4. 파싱 실패 응답의 형식을 확인한다. - -## 본문 - - - -`GraphQlRequestErrorMapper`(57)가 그 목적으로 존재하고 미배선이다(§19.3). - -## GraphQlRequestErrorMapper 참조 위치 - -:::evidence key="analysis-finding-a16-f008" alt="코드베이스에서 GraphQlRequestErrorMapper 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GraphQlRequestErrorMapper 코드베이스 검색 — 2줄 · exit 0" zoom="true" -::: - -## 배선된 두 매퍼가 덮는 범위 - -`GraphQlExceptionResolver`는 리졸버 예외를, `GraphQlWireErrorMapper`는 플랫폼 거부를 덮는다. - -## 그래서 파싱 오류만 형식이 다르다 - -구문 오류나 검증 실패의 응답에는 이 플랫폼의 안정 `code`/`category` 확장이 붙지 않고 graphql-java의 기본 형식이 나간다. 클라이언트가 오류 코드로 분기한다면 그 분기가 파싱 오류에서만 빗나간다. P3. - -## 확인하지 못한 것 - -구문 오류 요청을 보내 응답 형식을 확인하지 않았다. 배선 부재상 그 결과가 나온다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/graphql-surface/case/case-analysis-finding-a16-f011.md b/docs/clean-architecture-backend-template/tech-log-studio/graphql-surface/case/case-analysis-finding-a16-f011.md deleted file mode 100644 index 1a9a157..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/graphql-surface/case/case-analysis-finding-a16-f011.md +++ /dev/null @@ -1,114 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a16-f011 -title: 기계가 읽는 능력 매니페스트와 사람이 읽는 등급표가 커서 서명에 대해 다르게 답한다 -topic: graphql-surface -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a16-f011 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a16-f011.body.md -assets: - - key: analysis-finding-a16-f011 - file: ../../../final/evidence/rendered/analysis-finding-a16-f011.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a16-f011.txt -source: - - 원본 분석 절은 final/document.md#a16#L889 이다. ---- - -# 기계가 읽는 능력 매니페스트와 사람이 읽는 등급표가 커서 서명에 대해 다르게 답한다 - -안정 능력 매니페스트가 커서 서명을 지원 목록에 넣는다. 그 목록의 용도는 안정 스타터에서 켜도 되는가를 판정하는 것이다. 그 능력은 켤 수 있는 것으로 판정되고, 켜는 코드는 없다. - -## 관계 - -- **기본 비활성은 존재하지 않는 스위치의 기본값을 서술한다** - 같은 리프의 같은 계열 사례다. -- **프로파일별 정책 매니페스트가 미배선이라 자격에서 해석된 프로파일이 아무 예산도 선택하지 않는다** - 같은 리프의 다른 매니페스트 사례다. -- **두 목록이 같은 사실을 말하게 하려면 한 test로 묶는다** - 권고의 형태다. - -## 문제 - -이 리프에 능력 등급을 말하는 원천이 둘 있다. - -기계가 읽는 매니페스트와 사람이 읽는 등급표다. - -두 원천이 같은 답을 내는지 확인했다. - -## 결론 - -한 항목에서 갈린다. - -안정 능력 매니페스트가 커서 서명을 지원 목록에 넣는다. - -그 목록의 용도가 자바독에 적혀 있다. 어떤 능력이 안정 스타터에서 활성화되어도 되는지 확인하며, 고급이거나 실험적이거나 미지원이면 예외를 던진다는 것이다. - -즉 이 매니페스트는 안정 스타터에서 켜도 되는가를 판정한다. - -커서 서명은 켤 수 있는 것으로 판정되고, 켜는 코드는 없다. - -실패 시나리오는 이렇다. - -채택자가 릴리스 게이트를 돌려 그 능력이 안정 등급에서 승인되는 것을 확인한다. - -그것을 근거로 커서 쪽매김을 안정 계약의 일부로 문서화한다. - -같은 리프의 다른 사례가 든 세 긍정 신호에 네 번째가 더해진다. 부팅 거부와 설정 수용과 상태 보고에 이어서다. - -권고는 둘 중 하나다. - -매니페스트에 모형 집합을 추가하거나, 그 능력을 실험 등급으로 옮기는 것이다. - -등급표와 매니페스트가 같은 사실을 말하도록 두 목록을 한 테스트로 묶는 것이 더 낫다. 이 리프는 이미 개수 고정 형태를 두 번 쓰고 있다. - -판정은 P3 다. - -## 검증 환경 - -확인 방식 : 매니페스트 목록과 등급표 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/182 계열에 있다. - -1. 안정 능력 매니페스트의 지원 목록을 확인한다. -2. 그 목록을 소비하는 메서드의 자바독을 읽는다. -3. 등급표에서 같은 능력의 등급을 확인한다. -4. 그 능력을 켜는 코드가 있는지 검색한다. -5. 이 리프의 다른 개수 고정 테스트 형태를 확인한다. - -## 본문 - - - -`GraphQlStableCapabilityManifest.STABLE`이 `SIGNED_CURSOR_CONNECTION`을 지원 목록에 넣는다. 그 목록의 용도는 `requireStable(...)`이고, javadoc은 이렇게 말한다. - -> "Verifies a capability may be activated on the Stable starter. @throws GraphQlReleaseException when it is Advanced, Experimental or unsupported" - -## 매니페스트가 지원 목록에 넣은 항목 - -:::evidence key="analysis-finding-a16-f011" alt="분석 문서 final/document.md#a16 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a16 발췌 — 15줄" zoom="true" -::: - -## 켤 수 있는 것으로 판정되고 켜는 코드가 없다 - -즉 이 매니페스트는 "Stable 스타터에서 켜도 되는가"를 판정한다. - -## 실패 시나리오 - -adopter가 릴리스 게이트를 돌려 `SIGNED_CURSOR_CONNECTION`이 Stable에서 승인되는 것을 확인하고, 그것을 근거로 커서 페이지네이션을 Stable 계약의 일부로 문서화한다. §24.1의 세 긍정 신호(부팅 거부 · 설정 수용 · 액추에이터 보고)에 네 번째가 더해진다. P3. - -## 권고 - -매니페스트에 `MODELLED` 집합을 추가하거나, `SIGNED_CURSOR_CONNECTION`을 `EXPERIMENTAL`로 옮긴다. 등급표와 매니페스트가 같은 사실을 말하도록 두 목록을 한 테스트로 묶는 것이 더 낫다 — 이 leaf는 이미 `WebArchitectureRulesTest` 형태의 개수 고정을 두 번 쓰고 있다(§3.4). - -## 확인하지 못한 것 - -릴리스 게이트를 돌려 승인이 나오는 것을 실행하지 않았다. 목록 구성상 그 결과가 나온다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/graphql-surface/case/case-analysis-finding-a16-f012.md b/docs/clean-architecture-backend-template/tech-log-studio/graphql-surface/case/case-analysis-finding-a16-f012.md deleted file mode 100644 index 491e09f..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/graphql-surface/case/case-analysis-finding-a16-f012.md +++ /dev/null @@ -1,112 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a16-f012 -title: '"기본 비활성"은 존재하지 않는 스위치의 기본값을 서술한다' -topic: graphql-surface -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a16-f012 -evidenceCapturedOn: 2026-09-01 -assets: - - key: analysis-finding-a16-f012 - file: ../../../final/evidence/rendered/analysis-finding-a16-f012.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a16-f012.txt -source: - - 원본 분석 절은 final/document.md#a16#L1002 이다. ---- - -# "기본 비활성"은 존재하지 않는 스위치의 기본값을 서술한다 - -고급 능력을 켤 설정 표면이 없다. 등급표는 이 상태를 정확히 말한다. 어긋나는 것은 지침 문서 산문의 활성화 서술뿐이고, 그 두 문장은 활성화 경로의 존재를 전제한다. - -## 관계 - -- **기계가 읽는 능력 매니페스트와 사람이 읽는 등급표가 같은 능력에 대해 다르게 답한다** - 같은 리프의 같은 계열 사례다. -- **선언된 Advanced 능력 11개 중 9개는 켜는 방법이 없다** - 다른 리프의 같은 형태이고 심각도가 다르다. -- **등급표가 정확하면 산문만 고치면 된다** - 판정이 낮은 이유다. - -## 문제 - -지침 문서가 고급 능력의 활성화를 서술한다. - -기본이 비활성이며 명시적 승인 없이는 운영 활성화를 거부한다는 것이다. - -그 서술이 성립하는지 확인했다. - -## 결론 - -성립하지 않는다. - -고급 능력을 켤 설정 표면이 없다. - -깃발 레코드는 코드에서만 만들어지고, 그 활성화 메서드를 부르는 것은 테스트다. - -그것을 결속하거나 소비하는 자동 설정이 없다. - -등급표는 이 상태를 정확히 말한다. - -고급 항목이 전부 모형 등급이다. 요청 경로에는 없다는 뜻이다. 그러므로 능력이 동작한다고 주장하지 않는다. - -어긋나는 것은 지침 문서 산문의 활성화 서술뿐이다. - -기본 비활성이라는 문장과 명시적 승인 없이는 거부한다는 문장이 둘 다 활성화 경로의 존재를 전제한다. - -다른 리프의 같은 형태와 비교하면 심각도가 다르다. - -거기서는 열한 능력이 선언되고 둘만 켤 수 있으면서 그 사실이 어디에도 없었다. - -여기서는 켤 수 없다는 사실이 등급표에 모형으로 적혀 있고, 산문 한 문단만 그보다 앞서 나간다. - -권고는 그 문단을 등급에 맞추는 것이다. - -고급 능력은 현재 모형 등급이며 활성화 경로가 없고, 관련 깃발과 가드는 그 경로가 생길 때 쓸 판정 모델이라고 적으면 된다. - -판정은 P3 다. - -## 검증 환경 - -확인 방식 : 활성화 경로 검색과 등급표, 산문 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/182 계열에 있다. - -1. 지침 문서의 활성화 서술 두 문장을 읽는다. -2. 고급 깃발 레코드를 만드는 코드를 검색한다. -3. 그것을 결속하거나 소비하는 자동 설정을 검색한다. -4. 등급표에서 고급 항목의 등급을 확인한다. - -## 본문 - - - -Advanced 능력을 켤 설정 표면이 없다 — 플래그 record는 코드에서만 만들어지고(`enabling(...)`은 테스트가 부른다), 그것을 바인딩하거나 소비하는 자동설정이 없다(§38.2). - -## GraphQlAdvancedFeatureFlags 참조 위치 - -:::evidence key="analysis-finding-a16-f012" alt="코드베이스에서 GraphQlAdvancedFeatureFlags 를 검색한 출력 13줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GraphQlAdvancedFeatureFlags 코드베이스 검색 — 13줄 · exit 0" zoom="true" -::: - -## 등급표는 이 상태를 정확히 말한다 - -Advanced 항목이 전부 `modelled`("요청 경로에는 없다")이므로 능력이 동작한다고 주장하지 않는다. 어긋나는 것은 CLAUDE.md 산문의 활성화 서술뿐이다 — "기본 비활성"과 "명시적 승인 없이는 production 활성화를 거부한다"는 둘 다 활성화 경로의 존재를 전제한다. - -## inbound-web §36.1과 같은 형태이되 심각도가 다르다 - -거기서는 11개 능력이 선언되고 2개만 켤 수 있으면서 그 사실이 어디에도 없었다. 여기서는 켤 수 없다는 사실이 등급표에 `modelled`로 적혀 있고, 산문 한 문단만 그보다 앞서 나간다. P3. - -## 권고 - -그 문단을 등급에 맞춘다 — "Advanced capability 는 현재 `modelled` 등급이며 활성화 경로가 없다. `GraphQlAdvancedFeatureFlags`·`GraphQlAdvancedModuleGuard`는 그 경로가 생길 때 쓸 판정 모델이다." - -## 확인하지 못한 것 - -설정을 넣고 띄워 아무 일도 일어나지 않는 것을 재현하지 않았다. 소비자 부재상 그 결과가 나온다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-a20-f003-claude.md b/docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-a20-f003-claude.md deleted file mode 100644 index 7f273d7..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-a20-f003-claude.md +++ /dev/null @@ -1,147 +0,0 @@ ---- -kind: CASE -slug: a20-f003-claude -title: grpc 레인 시험 25개는 check 로 돌고, 레인만 거는 실행 0 검사는 돌지 않는다 -topic: grpc-and-streaming -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a20-f003-claude -evidenceCapturedOn: 2026-09-04 -body: case-a20-f003-claude.body.md -assets: - - key: a20-f003-claude - file: ../../../final/evidence/rendered/a20-f003-claude.svg -evidence: - - ../../../final/evidence/raw/a20-f003-claude.txt -source: - - 원본 분석 절은 final/document.md#a20#L260 이다. ---- - -# grpc 레인 시험 25개는 check 로 돌고, 레인만 거는 실행 0 검사는 돌지 않는다 - -원문은 증거 레인의 시험 스물다섯 개에 자동 실행 경로가 없다고 적었다. `:grpc:grpc-testkit` 의 두 태스크를 돌려 시험 이름을 집합으로 비교하니 기본 `test` 의 56 개가 그 스물다섯을 전부 포함한다. 자동 경로에 없는 것은 시험이 아니라 레인이 거는 실행 0 검사다. - -## 관계 - -- **아무도 돌리지 않는 레인의 게이트는 마지막으로 돌린 사람이 본 것을 보고한다** - 원문이 이 사례를 그 규칙으로 읽었다. 시험 쪽에는 붙지 않고, 레인만 거는 실행 0 검사 쪽에 붙는다. -- **네 레인이 check 에 붙지 않고, 이 가족을 이름으로 부르는 워크플로가 없다** - 그 기록이 25개 시험을 직접 입력할 때만 도는 것으로 적었는데, 기본 `test` 가 그 25개를 포함한다. -- **release gate가 실제로 차단하는 것은 hermetic test 3개이고, mongo용 CI workflow는 없다** - mongo 가족에서도 레인을 이름으로 부르는 워크플로가 없어서, 게이트가 실제로 차단하는 것은 기본 `test` 에 들어 있는 밀폐 클래스뿐이다. - -## 문제 - -src/grpc/CLAUDE.md 가 세 레인의 현재 상태를 통과로 서술한다. - -그 문장의 주어가 무엇이고 어느 경로로 실행되는지 확인했다. - -## 결론 - -앞 절반은 사실이다. grpcInProcessContractTest·grpcNettyContractTest·grpcFaultTest 를 이름으로 실행하니 모두 BUILD SUCCESSFUL 이고 시험 수는 7·9·9 다. - -이어서 grpc-testkit 의 기본 test 를 실행했다. 7 클래스 56 개가 통과하는데, 시험 이름을 하나씩 맞춰 보면 레인 쪽 25 개가 빠짐없이 그 안에 들어 있다. - -겹치는 이유는 제외 태그 목록에 있다. 레인은 태그로 시험을 고르고 그 시험들은 공유 test 소스 세트에 있는데, 기본 test 가 제외하는 것은 루트 규약의 quarantine 과 grpc-testkit/build.gradle:42 의 grpc-performance 뿐이다. ci-quality-gates.yml:50 이 ./gradlew check 를 돌리므로 이 25 개에는 사람이 부르지 않아도 도는 경로가 있다. - -넷째 레인만 다르다. GrpcPerformanceLaneTest 는 @Tag("grpc-performance") 를 달아 기본 test 결과에 0 건이다. - -자동 경로에 없는 것은 따로 있다. ca.strict-test-lane.gradle:293 의 최신 재사용 거부와, :246·:257 이 실행한 시험 수를 세어 0 이면 GradleException 을 던지는 검사다. failOnNoDiscoveredTests 는 이 둘에 들어가지 않는다. 레인을 쓰지 않는 leaf 의 테스트 태스크에서도 같은 값이라 빌드 도구 기본값이다. - -@Tag 가 지워지면 기본 test 는 같은 수를 계속 돌리고, 클래스가 지워지면 더 적은 수를 돌리며 그대로 통과한다. :grpc:grpc-testkit:check 의 dry-run 그래프에 레인 태스크는 0 건이고, grpc-testkit/build.gradle 에 check 가 0 번 나온다. - -.github/workflows 의 파일 어디에도 grpc 라는 문자열이 없다. 다만 ci-quality-gates.yml:61 의 conditionalTransportQualification 이 src/build.gradle:606 을 통해 :adapter:inbound:grpc:grpcTransportQualificationTest 를 요구하므로, 자동으로 도는 grpc 레인이 다른 프로젝트에 하나 있다. - -원본 분석의 등급은 P2 이고 이 기록은 새로 매기지 않는다. 그 등급을 떠받치던 근거 — 스물다섯 개 시험이 자동 실행 밖에 있다 — 는 성립하지 않는다. 남는 것은 레인만 거는 두 보증이 자동 경로에 없다는 더 좁은 결함이다. - -## 검증 환경 - -Gradle : 9.0.0 -확인 방식 : 레인 등록 블록과 태그 확인, 기본 test 에 걸리는 제외 태그를 루트 규약과 leaf 양쪽에서 확인, 태그가 클래스 단위인지 확인, :grpc:grpc-testkit:check dry-run 으로 태스크 그래프 확인, 결과 디렉터리를 비우고 실행 전 XML 0 건 확인 후 세 레인과 기본 test 를 각각 실행하고 종료 코드로 집계를 막음, 두 실행의 시험 이름 집합 비교, 성능 레인 클래스의 포함 여부 확인, 레인 규약의 보증 구현 확인, failOnNoDiscoveredTests 를 레인 없는 leaf 와 대조, check 언급과 워크플로 검색, 지속 통합이 부르는 다른 grpc 레인 확인 -소스 수정 : x - -## 재현 조건 - -1. 레인을 등록하는 블록에서 이름과 태그를 모은다. -2. 기본 테스트 태스크에 걸리는 제외 태그를 루트 규약과 leaf 빌드 파일 양쪽에서 찾는다. -3. 레인 태그가 클래스 선언에 붙는지 확인한다. -4. 검사 태스크를 dry-run 해 그래프에 기본 테스트가 있고 레인 태스크가 없는 것을 본다. -5. 결과 디렉터리를 비우고 남은 XML 이 0 인지 찍는다. -6. 세 레인을 이름으로 실행하고 종료 코드를 확인한 뒤에만 결과를 집계한다. -7. 같은 leaf 의 기본 테스트 태스크를 같은 방식으로 실행하고 집계한다. -8. 두 실행이 남긴 시험 이름을 모아 한쪽이 다른 쪽에 포함되는지 판정한다. -9. 제외 태그를 단 클래스가 기본 실행 결과에 있는지 센다. -10. 레인 규약에서 기본 태스크에 없는 보증을 찾고, failOnNoDiscoveredTests 를 레인이 없는 leaf 와 대조한다. -11. 이 leaf 의 빌드 파일이 검사 태스크를 언급하는지, 워크플로가 이 가족을 이름으로 부르는지 세고, 다른 프로젝트의 grpc 레인이 자동으로 도는지 확인한다. - -## 본문 - - - -`src/grpc/CLAUDE.md` 는 in-process·Netty·fault 레인이 실제로 실행되어 통과한다고 적는다. 원본 분석은 앞 절반을 사실로 확인한 뒤, 그 레인들이 `check` 에 없고 어떤 워크플로도 이름으로 부르지 않으므로 스물다섯 개 시험이 누군가 명령을 입력할 때만 돈다고 적었다. - -## 레인 넷과 기본 test 가 제외하는 태그 - -:::evidence key="a20-f003-claude" alt="src 디렉터리에서 돌린 gradle 실행과 정적 검색을 합친 출력 134줄. 먼저 gradle 판이 나오고, grpc-testkit 의 build.gradle 이 등록하는 레인 넷과 태그, 그리고 기본 test 에 걸리는 제외 태그가 루트 규약의 quarantine 과 leaf 의 grpc-performance 두 곳에서 온다는 것이 원문과 함께 실린다. 세 레인 태그가 클래스 선언 바로 위에 붙어 있는 것이 보인다. 이어서 check 를 dry-run 한 태스크 그래프가 나오는데 test 와 check 는 있고 레인 태스크는 0 건이다. 그 다음 결과 디렉터리를 지우고 실행 전 남은 XML 이 0 건임을 찍은 뒤 세 레인을 이름으로 돌려 GRADLE_EXIT=0 과 클래스별 7·9·9 가 실패 0 으로 나오고, 이어서 기본 test 를 돌려 GRADLE_EXIT=0 과 7 클래스 56 개가 나온다. 두 실행의 시험 이름을 집합으로 비교해 레인 25 개가 기본 test 56 개 안에 전부 있다는 판정이 True 로 찍히고 기본 test 에만 있는 시험이 31 개라는 것, 성능 레인 클래스가 기본 test 결과에 0 건이라는 것이 나온다. 마지막으로 레인 규약에서 보증에 해당하는 줄들이 인용되고, grpc-testkit 의 build.gradle 이 check 를 0 번 언급하며 워크플로 28 개 중 grpc 문자열을 담은 것이 0 건인데 ci-quality-gates 의 48번부터 61번 줄에는 check 가 모든 레인을 덮지 않는다는 주석과 별도 스텝 둘이 있고, 그중 마지막 스텝이 부르는 태스크가 grpc 이름의 레인을 요구한다는 것이 나온다." caption="레인 넷과 태그 · 기본 test 의 제외 태그 두 곳 · check 그래프에 레인 0 건 · 실행 전 XML 0 건과 GRADLE_EXIT · 레인 25 개가 기본 test 56 개의 부분집합 True · 레인 규약의 보증 · check 가 레인을 덮지 않는다는 주석과 grpc 레인을 부르는 별도 스텝 — 134줄 · exit 0" zoom="true" -::: - -`grpc-testkit/build.gradle:14`\~`:35` 가 레인 넷을 등록한다. `grpcInProcessContractTest`, `grpcNettyContractTest`, `grpcFaultTest`, `grpcPerformanceTest` 이고 태그는 `grpc-inprocess`, `grpc-netty`, `grpc-fault`, `grpc-performance` 다. 세 태그는 클래스 선언 바로 위에 붙어 클래스 전체를 덮는다. - -기본 `test` 에 걸리는 제외 태그는 두 곳에서 온다. 루트 규약 `src/build.gradle:551` 의 `quarantine` 과 `grpc-testkit/build.gradle:42` 의 `grpc-performance` 다. `grpc-testkit` 의 시험에 `quarantine` 태그는 0 건이므로 이 leaf 에서 실효 제외는 성능 태그 하나다. 나머지 세 태그는 어느 쪽에도 없다. - -## 레인 25 개는 기본 test 56 개 안에 있다 - -`:grpc:grpc-testkit:check` 를 dry-run 했다. 그래프에 `:grpc:grpc-testkit:test` 가 있고 레인 태스크는 0 건이다. 시험을 돌리지 않고 얻은 결과다. - -그다음 결과 디렉터리를 지우고 돌렸다. 실행 전 남은 XML 이 0 건임을 찍은 뒤, 세 레인을 이름으로 돌려 `GRADLE_EXIT=0` 과 `GrpcInProcessContractFixtureTest` 7, `GrpcNettyContractProfileTest` 9, `GrpcTransportEvidenceClassifierTest` 9 를 얻었다. 이어서 기본 `test` 를 돌려 `GRADLE_EXIT=0` 과 7 클래스 56 개를 얻었다. - -두 실행의 시험 이름을 집합으로 비교했다. 레인이 돌린 25 개가 기본 `test` 의 56 개 안에 전부 있다. 기본 `test` 에만 있는 시험은 31 개다. - -`GrpcPerformanceLaneTest` 는 기본 `test` 결과에 0 건이다. 제외 태그가 그 클래스를 걸러낸다. - -`ci-quality-gates.yml:50` 이 `./gradlew check` 를 돌린다. 그러므로 이 25 개에는 사람이 이름을 입력하지 않아도 도는 경로가 있다. - -## 레인만 거는 보증 둘은 check 에 없다 - -레인 태스크가 기본 `test` 와 다른 점은 둘이다. - -하나는 `ca.strict-test-lane.gradle:293` 의 `outputs.upToDateWhen { false }` 다. 레인은 결과를 최신으로 재사용하지 않는다. - -다른 하나는 실행한 시험 수를 세어 0 을 거부하는 것이다. `:246` 이 `executedTests` 를 두고 `:250` 이 시험마다 증가시키며, `:257` 이 그 값이 0 이면 `GradleException` 을 던진다. `:239`\~`:245` 는 왜 그것이 필요한지 적는다 — `failOnNoDiscoveredTests` 는 발견 단계에 적용되는데 태그 필터는 발견 이후에 시험을 걸러 내므로, 태그가 아무것도 맞히지 못한 레인은 그것만으로는 성공으로 끝난다는 것이다. - -`failOnNoDiscoveredTests` 자체는 레인만의 것이 아니다. 레인이 없는 `:grpc:grpc-core-api:test` 에서도 `true` 이므로 Gradle 9.0.0 의 기본값이다. `:23` 이 적는 레인의 몫은 그 값을 갖는 것이 아니라 leaf 가 그것을 끄지 못하게 하는 것이다. - -`@Tag("grpc-netty")` 가 지워지면 그 클래스는 여전히 test 소스 세트에 있으므로 기본 `test` 는 같은 수를 돌린다. 클래스 자체가 지워지면 기본 `test` 는 더 적은 수를 돌리고 그대로 통과한다. 두 경우 모두 레인은 실행 0 으로 실패하는데, 레인을 부르는 자동 경로가 없다. - -`grpc-testkit/build.gradle` 은 `check` 를 한 번도 언급하지 않는다. 워크플로 스물여덟 개 중 `grpc` 문자열을 담은 것은 0 건이다. - -다만 그 0 건이 이 가족 전체를 뜻하지는 않는다. `ci-quality-gates.yml:61` 이 `conditionalTransportQualification` 을 돌리고, `src/build.gradle:606` 이 그 태스크에 `:adapter:inbound:grpc:grpcTransportQualificationTest` 를 건다. 지속 통합이 자동으로 부르는 grpc 이름의 레인이 하나 있고, 그것은 `:grpc:grpc-testkit` 의 레인 넷이 아니다. - -## 원문과 갈리는 자리 - -원문은 `:286` 에서 `ci-quality-gates.yml` 이 `check` 를 돌리므로 각 leaf 의 기본 `test` 는 지속 통합에서 실행된다고 스스로 적었다. 그리고 바로 다음 문단에서 그 25 개가 누군가 명령을 직접 입력할 때만 돈다고 적었다. 두 문장이 함께 설 수 없다. - -성립하는 쪽은 앞 문장이다. 세 레인의 시험은 태그가 붙은 채 공유 test 소스 세트에 있고, 기본 `test` 가 제외하는 것은 성능 태그와 `quarantine` 뿐이다. 집합 비교가 25 개 전부의 포함을 보인다. - -원문이 인용한 문장 — 아무도 지역에서 돌리지 않는 레인의 붉은 게이트는 마지막으로 돌린 사람이 본 것을 보고한다 — 은 시험 자체에는 붙지 않는다. 붙는 것은 레인이 거는 실행 0 검사 쪽이다. - -원문은 레인을 `:286` 에서 "네 증거 레인"으로, `:8` 과 `:465` 에서 "증거 레인 3종"으로 부른다. 등록된 것은 넷이고 돌린 것은 셋이며 넷째는 기본 `test` 에서 의도적으로 제외된 성능 레인이므로 두 표기 모두 대상이 다를 뿐 틀리지 않았다. - -## 등급에 대해 - -원본 분석의 등급은 P2 다. 제목이 든 근거 — 증거 등급 모델 전체가 자동 실행 경로 밖에 있다 — 는 이 리비전에서 성립하지 않는다. - -남는 것은 더 좁은 결함이다. 레인이 존재하는 이유인 실행 0 검사와 최신 재사용 거부가 자동 경로에 없다. 이 기록은 등급을 새로 매기지 않고 상류가 P2 로 둔 근거와 여기서 확인한 반대 근거를 함께 남긴다. - -## 확인하지 못한 것 - -태그 삭제와 클래스 삭제를 실제로 해 보고 두 태스크가 어떻게 갈리는지 관측하지는 않았다. 필터 구성과 규약 코드를 읽었고, 겹치는 시험은 이름 대조로 확인했다. - -GitHub 러너 위에서 검사 태스크를 돌리지 않았다. 워크플로에 적힌 명령과 로컬 dry-run 그래프를 이어 붙여 판단했다. - -지속 부하와 성능 기준선은 확인 범위 밖이다. 지침 문서도 그것이 없다고 적는다. - -`@Tag` 를 지우거나 클래스를 지운 상태를 만들어 두 태스크의 결과가 갈리는 것을 실행으로 보이지 않았다. 두 태스크의 필터 구성과 레인 규약의 실행 0 검사를 읽고, 같은 시험이 양쪽에서 도는 것을 집합 비교로 확인한 데까지다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-a20-f006-grpcadmissioncontroller-tryadmit.md b/docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-a20-f006-grpcadmissioncontroller-tryadmit.md deleted file mode 100644 index 232cfa2..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-a20-f006-grpcadmissioncontroller-tryadmit.md +++ /dev/null @@ -1,147 +0,0 @@ ---- -kind: CASE -slug: a20-f006-grpcadmissioncontroller-tryadmit -title: 승격이 상한 필드를 읽지 않고, 세 승인 메서드의 main 호출자가 0 이다 -topic: grpc-and-streaming -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a20-f006-grpcadmissioncontroller-tryadmit -evidenceCapturedOn: 2026-09-04 -body: case-a20-f006-grpcadmissioncontroller-tryadmit.body.md -assets: - - key: a20-f006-grpcadmissioncontroller-tryadmit - file: ../../../final/evidence/rendered/a20-f006-grpcadmissioncontroller-tryadmit.svg -evidence: - - ../../../final/evidence/raw/a20-f006-grpcadmissioncontroller-tryadmit.txt -source: - - 원본 분석 절은 final/document.md#a20#L518 이다. ---- - -# 승격이 상한 필드를 읽지 않고, 세 승인 메서드의 main 호출자가 0 이다 - -승인 메서드가 `AtomicInteger` 를 읽고 비교한 뒤 따로 증가시킨다. `promoteFromQueue` 는 `maxConcurrentCalls` 를 읽는 줄이 아예 없다. 다만 그 결과가 보이려면 호출 순서가 어긋나야 하고, 이 셋 중 어느 것도 프로덕션 코드가 부르지 않는다. - -## 관계 - -- **스트림 승인의 경계가 동시성 아래에서 새고, caller별 맵이 줄지 않는다** - 같은 검사 후 사용 틈을 가진 짝이다. 두 타입 모두 승인 API 를 부르는 main 코드가 없다. -- **원자 타입 위의 검사 후 실행과 비교 후 교체 루프** - 그 개념이 두 형태를 가른다. 같은 가족의 `GrpcRetryBudget` 이 뒤쪽을 쓰는데 두 승인 클래스에는 `compareAndSet` 이 한 건도 없다. -- **테스트에서는 드물고 부하에서는 일상인 것** - 검사와 증가 사이의 틈은 시험에서 거의 걸리지 않고 동시 요청에서는 흔하다. 승격 쪽은 부하가 아니라 호출 순서에 달려 있다. - -## 문제 - -GrpcAdmissionController 가 두 축에 상한을 건다 — 동시에 실행 중인 호출 수와 대기열 길이다. - -그 상한이 코드에서 어떻게 지켜지고 어느 경로가 그것을 실제로 부르는지 확인했다. - -## 결론 - -tryAdmit:52~:54 가 값을 읽고 상한과 비교한 뒤 별도로 증가시킨다. :57~:59 의 대기열 쪽도, release:84~:85 도 같은 모양이다. 계수기 셋이 모두 AtomicInteger 인데 이 파일에 compareAndSet 은 0 건이다. - -promoteFromQueue:76~:78 은 대기 수가 0 보다 큰지만 확인하고 진행 중 수를 늘린다. 상한 필드를 읽지 않는다. - -초과가 드러나는 조건은 호출 순서다. 두 순서를 각각 돌려 값을 얻었다. 해제와 승격을 번갈아 다섯 쌍 부르면 inFlight 는 1 을 유지하고, 해제를 건너뛰고 승격만 다섯 번 부르면 6 까지 오른다. 저장소의 유일한 승격 시험(GrpcServerProfileTest:126~:127)은 앞의 순서를 쓴다. - -부르는 자리 자체가 적다. 호출 아홉 줄이 모두 GrpcServerProfileTest 한 파일에 있고 프로덕션 쪽에는 하나도 없다. 승격을 부르는 :127 은 바로 앞 :126 의 release() 와 짝을 이룬다. - -빈 정의를 담은 자동설정도 켜지지 않는다. GrpcPlatformAutoConfiguration:30~:34 가 matchIfMissing = false 인 프로퍼티 조건을 걸어 두었는데 그것을 켜는 설정 파일이 0 개이고, src/grpc 밖에서 그 starter 에 의존하는 모듈도 0 개다. main 소비자로 잡히던 GrpcDrainCoordinator 마저 자기 시험에서만 생성되고 제어기에는 inFlight() 읽기 한 줄만 건다. 빈은 GrpcPlatformAutoConfiguration:56 이 만들고 GrpcDrainCoordinator 가 들고 있지만, 그 조정자가 제어기에 하는 호출은 :148 의 inFlight() 읽기 하나다. - -같은 가족에 올바른 형태가 있다. GrpcRetryBudget:50~:58 이 읽고 검사한 뒤 compareAndSet 으로 교체하고 실패하면 되돌아간다. - -판정은 P2 이고 원문과 같다. 코드를 읽어 얻은 근거는 유지되고 관측된 초과 폭은 상류가 적은 것보다 크다. 반대편에는 이 코드를 오늘 도는 경로가 어디에도 없다는 사실이 있다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 승인·승격·해제 메서드 본문과 계수기 필드 확인, 빈 정의를 담은 자동설정의 조건과 그 프로퍼티를 켜는 설정 파일 계수와 starter 의존 모듈 계수, 그 소비자 클래스의 생성 지점 확인, 클래스 자바독의 설계 근거 확인, 타입 이름과 세 메서드 호출을 각각 소스 세트별로 계수, GrpcDrainCoordinator 가 제어기에 거는 호출 전수, 저장소의 승격 시험 본문 확인, compareAndSet 사용 계수와 같은 가족의 대조 구현 확인, 해제와 승격을 짝지은 순서와 짝짓지 않은 순서를 각각 단일 스레드로 실행해 값 관측 -소스 수정 : x - -## 재현 조건 - -1. 승인 메서드 본문에서 읽기와 비교와 증가가 각각 어느 줄인지 적는다. -2. 대기열 분기와 해제 메서드도 같은 방식으로 읽는다. -3. 계수기 필드의 타입과 이 파일의 compareAndSet 사용 수를 센다. -4. 승격 메서드에 상한 필드를 읽는 줄이 있는지 본다. -5. 세 메서드를 호출하는 자리를 소스 세트별로 나눠 센다. -6. main 에서 이 타입을 들고 있는 클래스가 제어기에 어떤 호출을 거는지 전부 찾는다. -7. 저장소의 승격 시험이 해제와 승격을 어떤 순서로 부르는지 읽는다. -8. 같은 상한으로 두 순서를 각각 실행하고 진행 중 수를 읽는다. -9. 같은 가족에서 compareAndSet 을 쓰는 main 파일을 찾고 그 루프를 읽는다. - -## 본문 - - - -`GrpcAdmissionController` 는 동시 호출 수와 대기열 길이를 상한으로 지킨다. 클래스 자바독(`:6`\~`:11`)은 `RESOURCE_EXHAUSTED` 로 거부하는 것이 대기열에 넣는 것보다 나은 이유를 둘 적는데, 둘 다 부하 아래에서 성립하는 이유다. - -## 승인 메서드 셋의 본문 - -:::evidence key="a20-f006-grpcadmissioncontroller-tryadmit" alt="저장소 루트에서 돌린 정적 검색과 /tmp/probe5 에서 컴파일해 돌린 프로브를 합친 출력 151줄. GrpcAdmissionController 의 tryAdmit 과 promoteFromQueue 와 release 본문이 50번부터 87번 줄까지 원문 그대로 실리고 계수기 필드 셋이 이어진다. 클래스 자바독이 거부가 대기열보다 나은 이유를 적는 대목이 나오고, src/grpc 아래에서 이 타입 이름이 나오는 자리가 소스 세트별로 나열된 뒤, 세 승인 메서드를 호출하는 자리만 따로 뽑히는데 전부 test 이고 main 소스 세트는 0 개 파일이다. main 에서 이 타입을 들고 있는 GrpcDrainCoordinator 가 하는 일은 inFlight 를 읽어 돌려주는 한 줄뿐이고, 그 빈을 만드는 자동설정이 기본값 꺼짐인 프로퍼티 조건 아래 있으며 그 프로퍼티를 켜는 설정 파일도 그 starter 에 의존하는 모듈도 0 개라는 것과, GrpcDrainCoordinator 를 생성하는 자리가 자기 시험 하나라는 것이 이어진다. 이어서 저장소의 유일한 승격 시험이 해제와 승격을 한 쌍으로 부르고 진행 중이 1 임을 단언하는 본문이 실린다. 같은 가족의 GrpcRetryBudget 이 읽고 검사한 뒤 compareAndSet 으로 교체하고 실패하면 되돌아가는 루프가 나오고, compareAndSet 을 쓰는 grpc main 파일 셋과 두 승인 클래스의 0 건 계수가 붙는다. 마지막으로 프로브 소스의 호출 순서가 그대로 실리고 두 실행 결과가 나오는데, 해제와 승격을 짝지으면 진행 중이 1 로 유지되고 해제 없이 승격만 다섯 번 부르면 6 이 된다." caption="세 메서드의 본문과 계수기 · 자바독의 설계 근거 · 승인 API 를 부르는 main 파일 0 개와 GrpcDrainCoordinator 의 읽기 한 줄 · 저장소의 짝지은 승격 시험 · 같은 가족의 compareAndSet 루프 · 프로브의 호출 순서와 짝지은 경우 1 · 짝짓지 않은 경우 6 — 151줄 · exit 0" zoom="true" -::: - -`tryAdmit:52` 가 `inFlight.get()` 으로 값을 읽고 `:53` 이 상한과 비교한 뒤 `:54` 가 따로 `incrementAndGet()` 한다. 읽기와 증가가 한 연산이 아니다. `:57`\~`:59` 의 대기열 쪽도 같은 모양이다. - -`release:84` 는 `inFlight.get() > 0` 을 본 뒤 `:85` 가 감소시킨다. 검사와 감소 사이가 열려 있다. - -세 필드 모두 `AtomicInteger` 인데(`:17`\~`:19`) `compareAndSet` 은 이 파일에 0 건이다. - -## promoteFromQueue 는 상한 필드를 읽지 않는다 - -`promoteFromQueue:76` 은 `queued.get() > 0` 만 확인한다. `:77` 이 대기 수를 줄이고 `:78` 이 진행 중 수를 늘리는데, 그 사이에 `maxConcurrentCalls` 를 읽는 줄이 없다. 이것은 소스만 읽어도 확정되는 사실이다. - -그 결과가 상한 초과로 나타나려면 조건이 하나 더 필요하다. 승격이 해제를 앞질러야 한다. - -프로브로 두 순서를 나란히 돌렸다. 저장소 시험과 같이 해제 하나에 승격 하나를 짝지으면 진행 중은 1 에 머문다. 해제 없이 승격만 다섯 번 부르면 진행 중이 6 이 된다. 상한은 1 이다. - -저장소가 쓰는 순서는 앞쪽이다. `GrpcServerProfileTest:126`\~`:127` 이 `release()` 다음에 `promoteFromQueue()` 를 부르고 `:129` 가 진행 중 1 을 단언한다. 뒤쪽 순서를 쓰는 자리는 저장소에 없다. - -## 세 메서드를 부르는 main 코드가 없다 - -`tryAdmit` 과 `promoteFromQueue` 와 `release` 를 호출하는 자리는 `src/grpc` 아래에서 아홉 줄인데 전부 `GrpcServerProfileTest` 다. main 소스 세트는 0 개 파일이다. - -그중 승격을 부르는 줄은 `:127` 하나이고, 그 시험의 이름(`:120`)은 해제가 자리를 비우고 대기 중인 호출이 그 자리로 올라간다는 것이다. `:126` 이 `release()` 를 먼저 부른다. 저장소에 있는 유일한 호출자가 둘을 짝지어 부른다. - -빈 정의는 있다. `GrpcPlatformAutoConfiguration:56`\~`:57` 이 `grpcAdmissionController` 를 만든다. - -그 정의가 컨텍스트에 올라오는 조건은 좁다. 그 자동설정은 `:30`\~`:34` 에서 `ca-skeleton.grpc.platform.enabled` 가 `true` 일 때만 켜지고 `matchIfMissing` 이 `false` 다. 그 프로퍼티를 켜는 설정 파일은 저장소에 0 개이고, `src/grpc` 밖에서 이 starter 에 의존하는 모듈도 0 개이며, `src/grpc` 안에 `ApplicationContextRunner` 를 쓰는 파일도 0 개다. - -main 소비자로 잡히는 `GrpcDrainCoordinator` 도 `:25` 의 필드 선언과 `:42` 의 생성자 인자일 뿐이다. 그 클래스를 생성하는 자리는 `GrpcDrainCoordinatorTest:26` 하나이고, 제어기에 거는 호출은 `:148` 의 `inFlight()` 읽기 한 줄이다. - -## 같은 가족이 올바른 형태를 갖고 있다 - -`GrpcRetryBudget:50`\~`:58` 의 `tryConsume` 은 `while (true)` 안에서 `tokens.get()` 으로 읽고, 부족하면 `false` 를 돌려주고, 충분하면 `compareAndSet(observed, observed - tokensPerRetry)` 로 교체한다. 교체가 실패하면 루프가 다시 읽는다. - -`compareAndSet` 을 쓰는 grpc main 파일은 `GrpcRetryBudget`, `GrpcChannelRuntimeRegistry`, `GrpcCancellationToken` 셋이다. 두 승인 클래스는 그 목록에 없다. - -## 원문과 갈리는 자리 - -원문은 원자 정수를 쓰지만 원자적 연산은 하나도 하지 않는다고 적었고, 읽고 비교한 뒤 별도로 증가시킨다고 했다. 그 서술은 맞다. - -원문은 대기열 승격을 "한 단계 더 나아간다"고 적으면서 대기열에서 승격되는 호출이 동시성 한도를 무조건 통과한다고 했다. 상한 필드를 읽지 않는다는 것까지는 맞지만, 진행 중 수가 상한을 넘으려면 승격이 해제를 앞질러야 한다. 저장소의 유일한 승격 시험은 둘을 짝지어 부른다. - -원문은 이 클래스가 조립된다는 것을 근거의 하나로 들었다. 빈으로 만들어지는 것은 맞다. 세 승인 메서드를 부르는 main 코드는 0 건이다. - -## 등급에 대해 - -원본 분석의 등급은 P2 다. 이 기록은 새로 매기지 않는다. - -상류가 P2 로 둔 근거 — 부하 아래에서 지키라고 만든 상한이 부하 아래에서 샌다 — 는 코드 수준에서 성립하고, 관측된 초과 폭은 원본 분석이 적은 것보다 크다. - -반대 근거는 오늘 이 코드를 도는 경로가 없다는 것이다. 세 메서드의 main 호출자가 0 이고, 빈 정의를 담은 자동설정은 아무도 켜지 않는 프로퍼티 뒤에 있으며, 그 starter 에 의존하는 모듈도 없다. - -## 확인하지 못한 것 - -경합 쪽 수치는 자료에 없다. 초과 폭이 스레드를 맞춰 출발시키는 방식에 크게 좌우돼 안정된 값을 얻지 못했다. - -실제 gRPC 서버를 띄워 요청을 흘리거나 부하를 걸지 않았다. 이 타입의 메서드를 직접 부른 결과까지다. - -드레인 조정자가 그 값으로 무엇을 결정하는지는 조사하지 않았다. - -동시 호출에서 상한이 넘어가는 것은 자료로 싣지 않은 실행에서 봤다. 관측되는 폭은 스레드를 어떻게 맞춰 출발시키느냐에 크게 달렸다. `CountDownLatch` 로 맞추면 이천 시행에서 위반이 0 회인 실행도 나오는데, 스핀 배리어로 바꾸면 스레드 열둘로 천 시행에 사백여든일곱 회가 상한을 넘고 `inFlight` 가 아홉까지, 예순넷으로는 구백쉰아홉 회에 열하나까지 올라간다. 어느 쪽도 안정된 값이 아니라 자료에는 실행마다 같은 값이 나오는 순서 비교만 실었다. - -`GrpcDrainCoordinator` 가 `inFlight()` 를 읽어 무엇을 판단하는지는 읽지 않았다. 그것이 제어기에 하는 유일한 호출이라는 데까지 확인했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-a20-f007-grpcstreamadmission.md b/docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-a20-f007-grpcstreamadmission.md deleted file mode 100644 index 9799fe5..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-a20-f007-grpcstreamadmission.md +++ /dev/null @@ -1,148 +0,0 @@ ---- -kind: CASE -slug: a20-f007-grpcstreamadmission -title: GrpcStreamAdmission 의 호출자 맵이 줄지 않고 main 소비자는 0 이다 -topic: grpc-and-streaming -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a20-f007-grpcstreamadmission -evidenceCapturedOn: 2026-09-04 -body: case-a20-f007-grpcstreamadmission.body.md -assets: - - key: a20-f007-grpcstreamadmission - file: ../../../final/evidence/rendered/a20-f007-grpcstreamadmission.svg -evidence: - - ../../../final/evidence/raw/a20-f007-grpcstreamadmission.txt -source: - - 원본 분석 절은 final/document.md#a20#L572 이다. ---- - -# GrpcStreamAdmission 의 호출자 맵이 줄지 않고 main 소비자는 0 이다 - -`GrpcStreamAdmission.tryAdmit` 이 호출자별 계수기와 전체 계수기를 각각 읽어 상한과 비교한 뒤 따로 늘린다. `release` 는 값만 줄이고 `perCaller` 항목은 지우지 않아 맵이 지금까지 본 지문 수만큼 남는다. 거기에 이 타입을 세우는 프로덕션 코드가 저장소에 하나도 없다. - -## 관계 - -- **스트림 승인의 경계가 동시성 아래에서 새고, caller별 맵이 줄지 않는다** - 같은 클래스의 같은 두 결함을 적은 기록이다. 그 기록은 원자성 분석으로 판정했고, 여기서는 줄 번호를 세고 단일 스레드 프로브로 맵이 두 규모에서 줄지 않는 것과 main 참조가 0 인 것을 확인했다. -- **queued 를 줄이는 유일한 연산이 상한을 읽지 않는 승격 안에 있다** - 두 타입 모두 계수기를 읽어 상한과 비교한 뒤 별개 연산으로 늘린다. 다만 이 타입의 `release` 는 호출자별 계수와 전체 계수를 모두 내리는데, 그 기록의 승인 제어기는 `inFlight` 만 내린다. -- **카디널리티 경계를 타입으로 표현하기** - 그 개념은 메트릭 태그의 값 공간이 트래픽과 함께 자라지 않도록 금지 목록과 태그별 상한과 폐쇄 집합으로 닫는 방법을 적는다. `perCaller` 는 호출자 지문을 키로 쓰는데 그 셋에 해당하는 장치가 하나도 없다. - -## 문제 - -GrpcStreamAdmission 이 두 축에 상한을 건다. - -그 두 상한이 어느 줄에서 검사되는지, perCaller 항목이 언제 지워지는지, 그리고 이 타입을 main 에서 만드는 코드가 있는지 확인했다. - -## 결론 - -읽는 줄 둘(:44, :47)과 늘리는 줄 둘(:50, :51)이 각각 별개의 연산이다. 그 사이에 다른 스레드가 끼어들 수 있다. release:58~:62 도 검사와 감소가 나뉘어 있다. - -계수기를 지우는 코드는 없다. 이 맵이 나오는 네 줄은 선언(:17), computeIfAbsent(:43), get(:57, :73) 이고 제거 계열 호출은 0 건이다. - -두 규모로 확인했다. 서로 다른 지문 셋으로 열고 전부 닫으면 맵이 3 으로 남고, 오만으로 하면 50,000 으로 남는다. - -닫는 것과 무관한 경로도 있다. 항목을 만드는 줄이 상한 검사 두 줄보다 위에 있어, 거절로 끝나는 시도도 자기 항목을 남긴다. 상한을 (1, 1) 로 두고 하나만 승인한 뒤 오만 번 더 시도하면 전부 거절되는데 맵은 50,001 이 된다. 상한 두 개가 맵 크기는 전혀 제한하지 못한다. - -같은 가족의 카디널리티 정책은 :24~:33 에 허용 태그 여덟을 열거하고 그 밖을 전부 거부한다. perCaller 쪽에는 그런 장치가 하나도 없다. - -이 타입은 라이브러리 안에서도 쓰이지 않는다. 검색에 잡히는 파일이 셋뿐이고 프로덕션 쪽 사용처가 하나도 없다. 승인 제어기 쪽은 프로덕션 파일 둘이 쓴다. 다만 그 차이는 라이브러리 안에 머문다 — modules.json 의 grpc leaf 열여덟이 전부 빈 runtime_memberships 를 갖는다. - -판정은 P2 이고 원문과 같다. 다만 상류가 든 근거에 반대 근거가 하나 있다. 세우는 코드가 없으니 이 두 상한은 오늘 어떤 프로세스에서도 계산되지 않는다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 승인·해제 메서드 본문과 필드 넷 확인, 자바독의 실패 시나리오 확인, perCaller 가 나오는 줄 전부와 소속 메서드 확인, 제거 계열 호출 일곱 이름 검색, 경로 제한 없는 참조 검색과 대조군 비교, 자원 파일·자동설정·리플렉션 경로 검색, 카디널리티 정책의 자바독과 허용 목록 전수, 서로 다른 지문 셋과 오만 두 규모로 열고 닫은 뒤 맵 크기를 단일 스레드로 관측 -소스 수정 : x - -## 재현 조건 - -1. 승인 메서드에서 값을 읽는 줄과 늘리는 줄을 각각 적는다. -2. 해제 메서드에서 검사와 감소가 어느 줄인지 적고, 널 검사가 무엇을 막는지 읽는다. -3. 호출자별 맵이 나오는 줄을 전부 찾고 각각 어느 메서드에 속하는지 적는다. -4. 제거 계열 호출을 여러 이름으로 검색한다. -5. 서로 다른 지문 셋으로 열고 전부 닫은 뒤 열린 수와 맵 크기를 읽는다. -6. 같은 절차를 오만으로 반복해 규모가 따라 커지는지 본다. -7. 같은 지문들로 한 번 더, 그리고 새 지문 하나로 맵 크기를 읽는다. -8. 이 타입의 참조를 경로 제한 없이 검색하고 대조군과 비교한다. -9. 자원 파일과 자동설정 목록과 리플렉션 경로도 검색한다. -10. 같은 가족에서 값 공간 증가를 막는 정책을 찾아 그 근거와 목록을 읽는다. - -## 본문 - - - -`GrpcStreamAdmission` 은 호출자별 스트림 수와 전체 동시 스트림 수를 상한으로 지킨다. 자바독(`:8`\~`:10`)은 스트림이 요청과 달라 수명 내내 연결과 큐와 생산자를 차지하므로 그 개수가 용량 숫자라고 적고, 상한이 없으면 오류마다 재접속하는 클라이언트가 예전 스트림이 닫히는 것보다 빠르게 새 스트림을 연다고 적는다. - -## tryAdmit 과 release 에서 읽기와 쓰기가 나뉜 줄 - -:::evidence key="a20-f007-grpcstreamadmission" alt="저장소 루트에서 돌린 정적 검색과 /tmp/probe5 에서 컴파일해 돌린 프로브를 합친 출력 112줄. GrpcStreamAdmission 의 tryAdmit 과 release 본문이 38번부터 64번 줄까지 원문 그대로 실리고 필드 넷이 14번부터 18번 줄로 이어진다. 자바독의 실패 시나리오가 나오고, perCaller 가 나오는 네 줄이 각각 어느 메서드에 속하는지 주석과 함께 실리며 제거 계열 호출 검색이 0 건으로 나온다. 이어서 경로 제한 없는 git grep 이 이 타입을 언급하는 파일 셋과 대조군인 GrpcAdmissionController 의 일곱 파일을 나란히 내고, 자원 파일과 자동설정과 리플렉션 경로 검색이 각각 0 건이다. 같은 가족의 GrpcMetricCardinalityPolicy 자바독 네 줄과 ALLOWED_TAGS 선언 전체가 원소 여덟과 함께 나온다. 마지막으로 프로브 소스의 호출 줄이 실리고, 호출자 셋으로 돌린 결과와 오만으로 돌린 결과가 나란히 나오는데 둘 다 스트림을 모두 닫아도 맵 크기가 그대로이고 새 지문 하나에 하나씩 는다." caption="tryAdmit·release 의 본문과 필드 넷 · 자바독의 재접속 시나리오 · perCaller 네 줄과 제거 호출 0 건 · 경로 제한 없는 참조 목록과 대조군 · 카디널리티 정책의 자바독과 허용 태그 여덟 · 호출자 3 과 50,000 두 규모의 맵 크기 — 112줄 · exit 0" zoom="true" -::: - -`tryAdmit:43` 이 `computeIfAbsent` 로 호출자별 계수기를 얻는다. 값을 읽는 줄은 `:44` 와 `:47` 둘이고, 값을 늘리는 줄은 `:50` 과 `:51` 둘이다. 읽은 뒤 늘리기 전에 다른 스레드가 같은 값을 읽을 수 있다. - -`release:57` 이 계수기를 꺼내고 `:58` 이 널이 아닌지와 0 보다 큰지를 함께 확인한 뒤 `:59` 가 줄인다. `:61` 이 전체 값을 확인하고 `:62` 가 줄인다. `:58` 의 널 검사가 `release` 를 맵에 항목을 새로 만들지 않는 메서드로 만든다. - -## 스트림 오만 개를 모두 닫아도 perCaller 는 오만이다 - -`perCaller` 가 나오는 줄은 넷이다. 선언(`:17`), `tryAdmit` 안의 `computeIfAbsent`(`:43`), `release` 안의 `get`(`:57`), `openStreamsFor` 안의 `get`(`:73`)이다. `remove`·`clear`·`removeIf`·`compute`·`merge`·`keySet`·`entrySet` 을 통틀어 제거 계열 호출은 0 건이다. - -맵 크기는 단일 스레드 프로브로 읽었다. 서로 다른 호출자 지문 셋으로 스트림을 열고 전부 닫으면 `openStreams()` 는 0 인데 맵 크기는 3 이다. 같은 지문들로 한 번 더 열고 닫아도 3 이고, 새 지문 하나를 더하면 4 가 된다. 오만으로 돌리면 같은 모양이 규모로 나타나 50,000 · 50,000 · 50,001 이 된다. - -닫는 것과 무관하게 자라는 경로가 하나 더 있다. `computeIfAbsent`(`:43`)가 두 상한 검사(`:44`, `:47`)보다 앞에 있어, 상한에 걸려 거절되는 승인도 항목을 먼저 만든다. - -상한을 `(1, 1)` 로 두고 확인했다. 스트림 하나를 승인한 뒤 서로 다른 지문으로 오만 번 더 시도하면 오만 번 모두 거절된다. `openStreams()` 는 1 로 상한을 지키는데 맵은 50,001 이다. - -그러므로 두 상한은 맵 크기에 아무 상한도 주지 않는다. 승인된 스트림이 하나뿐인 동안에도 맵은 시도한 지문 수만큼 자란다. - -자바독이 실패 시나리오로 적은 재접속 클라이언트가 재접속마다 다른 지문을 쓰는지는 확인하지 않았다. 다르다면 재접속 한 번마다 항목이 하나씩 늘어난다. - -## 같은 가족이 값 공간의 증가를 막는 자리 - -`GrpcMetricCardinalityPolicy:12`\~`:15` 의 자바독은 문제가 나쁜 태그가 아니라 상한 없는 태그이고, 값 공간이 트래픽과 함께 자라는 태그 하나가 시계열 수를 그 공간만큼 곱한다고 적는다. 테넌트 식별자 하나가 시계열 백 개를 십만 개로 만든다는 것이다. - -그래서 그 정책은 허용 목록으로 동작한다. `ALLOWED_TAGS` 는 `:24`\~`:33` 에 여덟 개가 열거돼 있고 그 밖은 전부 거부된다. - -`perCaller` 에는 허용 목록도 크기 상한도 없다. - -## GrpcStreamAdmission 을 쓰는 main 코드가 없다 - -경로 제한 없이 저장소 전체를 검색했다. 이 이름이 나오는 파일은 셋이다 — 자기 파일, `GrpcFlowControlPolicyTest`, 그리고 구현 계획 문서 하나다. main 소스 세트에서 자기 파일 밖 참조는 0 이다. - -자원 파일과 자동설정 목록과 `Class.forName` 경로도 각각 0 건이다. - -대조군은 다르다. `GrpcAdmissionController` 는 같은 검색에서 일곱 파일에 나오고 그중 `GrpcDrainCoordinator` 와 `GrpcPlatformAutoConfiguration` 이 main 이다. - -다만 그 차이는 라이브러리 안에서의 차이다. `modules.json` 의 grpc leaf 열여덟 개는 `runtime_memberships` 가 모두 비어 있다. 승인 제어기도 배포 아티팩트에 실려 있지 않다. - -## 원문과 갈리는 자리 - -원문은 `GrpcStreamAdmission` 을 승인 제어기와 "같은 형태"로 적었다. 읽기와 쓰기가 나뉜 것과 해제가 0 아래로 갈 수 있는 것까지는 그렇다. - -원문이 적지 않은 것이 소비자다. 승인 제어기는 자동 설정과 드레인 조정자가 main 에서 쓰는데 이 타입은 그런 코드가 없다. 두 타입 다 배포 아티팩트에는 실려 있지 않으므로, 갈리는 것은 라이브러리 안의 사용처다. - -원문은 맵이 "호출자 수만큼 자라고 줄지 않는다"고 적었다. 그 서술은 맞고, 자라는 경로가 하나 더 있다. 거절된 승인도 `computeIfAbsent` 를 먼저 지나므로 승인이 하나도 나지 않는 동안에도 맵이 자란다. - -## 등급에 대해 - -원본 분석의 등급은 P2 다. 이 기록은 새로 매기지 않는다. - -상류가 P2 로 둔 근거는 상한이 부하 아래에서 새는 것과 맵이 줄지 않는 것이다. 뒤쪽은 단일 스레드로 확인했고, 앞쪽은 재현 가능한 값을 얻지 못했다. - -반대 근거는 이 타입을 만드는 main 코드가 0 이라는 것이다. 두 상한은 지금 어느 런타임에서도 검사되지 않는다. 채택자가 스트림 어댑터를 붙여 이 타입을 쓰기 시작하면 두 결함이 나타날 수 있는데, 그 배포를 띄워 확인하지는 않았다. - -## 확인하지 못한 것 - -동시 부하에서 위반 시행이 나오는 것만 확인했다. 최댓값은 실행마다 달라 수치를 적지 않는다. - -실제 클라이언트의 재접속을 일으킨 것이 아니다. 지문 문자열을 손으로 만들어 넣었다. - -지문의 생성 규칙과, 문자열로 조립하는 참조는 좁히지 않았다. - -호출자 지문이 무엇으로 만들어지는지 확인하지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-a20-f008-grpcserializedstreamwriter-drop-oldest.md b/docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-a20-f008-grpcserializedstreamwriter-drop-oldest.md deleted file mode 100644 index a68ab5f..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-a20-f008-grpcserializedstreamwriter-drop-oldest.md +++ /dev/null @@ -1,153 +0,0 @@ ---- -kind: CASE -slug: a20-f008-grpcserializedstreamwriter-drop-oldest -title: GrpcSerializedStreamWriter 가 버린 봉투 대신 들어오는 크기를 뺀다 -topic: grpc-and-streaming -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a20-f008-grpcserializedstreamwriter-drop-oldest -evidenceCapturedOn: 2026-09-04 -body: case-a20-f008-grpcserializedstreamwriter-drop-oldest.body.md -assets: - - key: a20-f008-grpcserializedstreamwriter-drop-oldest - file: ../../../final/evidence/rendered/a20-f008-grpcserializedstreamwriter-drop-oldest.svg -evidence: - - ../../../final/evidence/raw/a20-f008-grpcserializedstreamwriter-drop-oldest.txt -source: - - 원본 분석 절은 final/document.md#a20#L595 이다. ---- - -# GrpcSerializedStreamWriter 가 버린 봉투 대신 들어오는 크기를 뺀다 - -넘침 분기가 앞에서 봉투를 꺼내 버리면서 누적 바이트에서는 새로 들어오는 메시지의 크기를 뺀다. 봉투가 크기를 담지 않아 버린 봉투의 크기를 알 방법이 없다. 메시지 상한 3 으로 돌리면 누적이 1,000 에 머무는 동안 실제 큐는 3,000 바이트다. - -## 관계 - -- **queued 를 줄이는 유일한 연산이 상한을 읽지 않는 승격 안에 있다** - 그 기록에서는 큐 계수기를 내리는 유일한 줄이 승격 안에 묶여 있어 대기 값을 되돌릴 수단이 없다. 여기서는 넘침 분기가 버린 봉투 대신 들어오는 메시지 크기를 빼서 누적이 실제 큐 합계와 어긋난다. -- **배치 상한이 개수와 바이트 두 축인 이유** - 그 개념은 messaging 의 배치 발행에서 개수와 바이트를 함께 두는 이유를 다룬다. 여기서는 같은 이유를 자기 자바독에 적은 gRPC 흐름 제어 정책이 실제와 다른 누적값으로 바이트 축을 판정한다. -- **테스트에서는 드물고 부하에서는 일상인 것** - 대응 시험이 크기 공급자를 상수로 고정한다. 크기가 하나로 통일되면 두 값이 우연히 일치해 오차가 0 이 된다. 다만 이 결함은 동시 호출이 아니라 크기가 섞이는 것만으로 단일 스레드에서 재현된다. - -## 문제 - -직렬 스트림 기록기가 넘침 정책 중 하나로 가장 오래된 것을 버린다. - -그 분기가 누적 바이트를 어떻게 갱신하는지, 그리고 두 방향에서 결과가 무엇인지 확인했다. - -## 결론 - -GrpcSerializedStreamWriter:82 가 queue.pollFirst() 로 봉투를 꺼내고 :84 가 queuedBytes 에서 nextBytes 를 뺀다. nextBytes 는 :73 에서 payloadSizer.getAsLong() 로 얻은 들어오는 메시지의 크기다. - -올바른 값을 쓸 방법도 없다. GrpcStreamEnvelope 의 성분 일곱에 크기가 없고, 크기 공급자는 인자를 받지 않는다. - -이 누적값은 흐름 제어 정책의 판정 입력이다. GrpcFlowControlPolicy:57 이 누적과 다음 메시지 크기의 합을 바이트 상한과 비교한다. - -두 방향을 돌렸다. 작은 것을 버리며 큰 것을 넣으면 누적이 1,000 에 고정되는데 실제 큐는 3,000 이다. 순서를 뒤집으면 반대로 부풀어, 실제 큐가 상한의 4분의 1도 차지 않았는데 5,000 바이트짜리가 버려진다. - -원문은 뒤쪽에서 조기 TERMINATE 가 된다고 적었는데 그렇게 되지 않는다. 이 정책값으로 decide 를 156,282 조합 불러도 그 결정은 0 회다. 두 분기는 같은 필드의 서로 다른 값에 매달려 있어 한 writer 안에서 함께 도달할 수 없다. - -시험이 이 오차를 드러낼 수 없다. DROP_OLDEST 를 쓰는 유일한 자리가 크기 공급자에 상수를 주고, 단언 대상도 결과와 버린 수와 남은 수에 그친다. - -배선은 없다. 프로덕션 쪽에 이 타입을 세우는 파일이 하나도 없다. - -판정은 P2 이고 원문과 같다. 오차 자체는 양쪽 방향에서 실행으로 잡았다. 반대편에는 이 코드를 도는 인스턴스가 오늘 없다는 것과, 기본 프로파일이 아예 다른 분기를 탄다는 것이 있다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 넘침 분기와 enqueue 를 한 창에서 확인, 크기 획득 지점 추적, 봉투 타입의 성분 전수, 흐름 제어 정책의 판정식과 네 갈래 결정과 기본 팩토리 확인, 경로 제한 없는 참조 검색, 대응 시험의 크기 상수와 단언 확인, 두 방향으로 크기를 바꿔 넣어 회차별로 내부 누적값과 리플렉션으로 읽은 실제 큐 합계 비교, 이 정책값으로 decide 가 낼 수 있는 결정 전수 -소스 수정 : x - -## 재현 조건 - -1. 넘침 정책 분기의 본문을 읽고 빼는 값이 어디서 왔는지 거슬러 올라간다. -2. 같은 창에서 enqueue 가 그 값을 다시 더하는지 확인한다. -3. 봉투 타입의 성분을 전부 나열해 크기가 있는지 본다. -4. 크기 공급자의 시그니처를 읽어 버린 봉투로 되물을 수 있는지 본다. -5. 흐름 제어 정책의 판정식과 자바독의 두 축 근거를 읽는다. -6. 메시지 상한을 작게 두고 작은 것에서 큰 것으로 크기를 올려 가며 넣는다. -7. 반대로 큰 것에서 작은 것으로 내려 가며 넣는다. -8. 회차마다 내부 누적값과 큐를 직접 읽어 얻은 실제 합계를 비교한다. -9. 같은 정책값으로 판정 함수를 조합 전수로 불러 나오는 결정을 센다. -10. 대응 시험이 크기 공급자에 주는 값과 단언 대상을 읽는다. -11. 이 타입의 참조를 경로 제한 없이 검색한다. - -## 본문 - - - -`GrpcSerializedStreamWriter` 는 봉투를 큐에 쌓고 하나의 기록기가 빼내며, 큐가 넘칠 때 무엇을 할지는 흐름 제어 정책의 느린 소비자 설정이 정한다. 그중 `DROP_OLDEST` 는 가장 오래된 봉투를 버리고 새 메시지를 받는 손실 허용 프로파일이다. - -## 넘침 분기가 빼는 값 - -:::evidence key="a20-f008-grpcserializedstreamwriter-drop-oldest" alt="저장소 루트에서 돌린 정적 검색과 /tmp/probe5 에서 컴파일해 돌린 프로브를 합친 출력 174줄. GrpcSerializedStreamWriter 의 넘침 분기가 70번부터 107번 줄까지 실려 DROP_OLDEST 가 pollFirst 로 봉투를 꺼내고 누적에서 nextBytes 를 뺀 뒤 enqueue 가 같은 값을 다시 더하는 것이 한 화면에 보이고, 크기를 얻는 payloadSizer 줄들이 이어진다. GrpcStreamEnvelope 의 성분 일곱에 크기가 없다. GrpcFlowControlPolicy 는 40번부터 87번 줄까지 실려 stable 팩토리와 decide 의 네 갈래가 모두 나오는데 TERMINATE 분기와 DROP_OLDEST 분기와 PAUSE 와 PROCEED 가 각각 보이고, 개수와 바이트를 함께 두는 이유를 적은 자바독이 따라온다. 경로 제한 없는 검색이 이 타입을 언급하는 파일을 전부 내는데 자기 파일 밖 main 참조는 0 개다. 대응 시험의 헬퍼와 각 호출이 크기 공급자에 주는 상수들이 나오고, DROP_OLDEST 를 쓰는 시험이 무엇을 단언하는지 본문째 실린다. 마지막으로 프로브가 두 방향을 각각 표로 낸다. 작은 것을 버리며 큰 것을 넣으면 누적이 1000 에 머무는데 실제 큐는 3000 이고, 큰 것을 버리며 작은 것을 넣으면 누적이 20010 에 고정되는데 실제 큐는 30 까지 줄어 오차가 플러스 19980 이 된다. 그 아래에서 이 정책값으로 decide 를 156282 조합 불렀을 때 TERMINATE 가 0 회이고 나올 수 있는 결정이 DROP_OLDEST 와 PAUSE 와 PROCEED 셋뿐임이 나온다." caption="넘침 분기와 enqueue 가 같은 화면에 · 크기를 담지 않는 봉투 · decide 의 네 갈래와 stable 팩토리 · 자기 파일 밖 main 참조 0 · 시험이 크기 공급자에 주는 상수 · 두 방향의 회차별 표 · 이 정책값으로 TERMINATE 0/156,282 — 174줄 · exit 0" zoom="true" -::: - -`:81`\~`:89` 의 `DROP_OLDEST` 분기는 `queue.pollFirst()` 로 봉투를 꺼내고(`:82`), 널이 아니면 `queuedBytes = Math.max(0L, queuedBytes - nextBytes)` 를 한다(`:84`). `nextBytes` 는 `:73` 에서 `payloadSizer.getAsLong()` 로 얻은 값, 곧 지금 들어오는 메시지의 크기다. 이어서 `:87` 의 `enqueue` 가 `:106` 에서 같은 `nextBytes` 를 다시 더한다. - -꺼낸 봉투의 크기를 쓸 수도 없다. `GrpcStreamEnvelope` 는 성분 일곱을 담는데 — `streamId`, `sequence`, `kind`, `snapshotVersion`, `resumeToken`, `terminationReason`, `payload` — 크기가 없다. `payloadSizer` 는 인자를 받지 않는 `LongSupplier` 라 버린 봉투를 넘겨 되물을 수도 없다. - -## GrpcFlowControlPolicy 가 누적 바이트를 상한과 비교한다 - -`:57` 이 `queuedBytes + nextMessageBytes > maxQueuedBytes` 로 바이트 넘침을 판정하고, `:56` 의 개수 넘침과 함께 `:58` 이 둘 중 하나라도 참이면 느린 소비자 정책으로 넘어간다. - -자바독 `:6`\~`:8` 에는 개수와 바이트 중 하나만 두면 다른 축에 상한이 없어진다고 적혀 있다. 바이트 상한 없이 메시지 천 개만 제한하면 큐가 쓸 수 있는 메모리는 누군가 보낸 가장 큰 메시지가 정하고, 반대로 개수 상한 없이 바이트만 제한하면 큐 자체의 부대 비용에 상한이 없다. - -## 두 방향을 돌린 결과 - -메시지 상한 3, 바이트 상한 1,000,000 으로 100 바이트 셋을 넣고 1000 바이트를 여섯 번 넣었다. 네 번째에서 개수 상한에 걸려 100 바이트짜리가 버려지는데 누적에서는 1000 이 빠진다. 첫 절단에서 `Math.max` 가 음수를 0 으로 잘라 그때까지의 누적이 사라지고, 그 뒤로는 빼는 값과 더하는 값이 같아 누적이 1000 에 고정된다. 실제 큐는 3,000 바이트다. - -이 구성에서는 개수 상한 3 이 먼저 걸리므로 바이트 상한 1,000,000 은 도달하지 않는다. 바이트 경계가 늦게 발화하는 것을 직접 본 것은 아니고, 본 것은 큐가 3,000 바이트를 들고 있는 동안 판정에 들어가는 값이 1,000 이라는 것이다. 바이트 상한을 그 사이 어딘가로 잡은 배포에서는 큐가 개수 경계까지 임의 크기 메시지로 채워지고, 자바독이 막으려던 것이 그 상태다. - -크기를 반대로 넣으면 누적이 실제보다 높아진다. 메시지 상한 3, 바이트 상한 25,000 으로 10,000 바이트 둘과 10 바이트 하나를 넣은 뒤 10 바이트를 계속 넣으면 누적이 20,010 에 고정되는데 실제 큐는 30 까지 줄어 오차가 19,980 이 된다. 그 상태에서 5,000 바이트를 넣으면 실제 큐가 5,020 뿐인데도 버려진다. - -## 과대 계상 쪽은 원문과 결과가 다르다 - -원문은 이 방향에서 조기 `TERMINATE` 가 된다고 적었다. 그렇게 되지 않는다. - -`GrpcFlowControlPolicy.decide` 를 이 정책값으로 156,282 조합 불러도 `TERMINATE` 는 0 회이고, 나오는 결정은 `DROP_OLDEST`·`PAUSE`·`PROCEED` 셋뿐이다. 넘침 판정이 `TERMINATE` 로 가는 분기(`:60`)는 느린 소비자 정책이 `TERMINATE` 일 때만 들어가는데, 잘못된 뺄셈이 있는 분기를 도는 writer 의 정책은 `DROP_OLDEST` 다. 두 값은 같은 record 의 같은 필드라 한 writer 안에서 바뀌지 않는다. - -과대 계상은 종료가 아니라 아직 여유가 있는 큐에서 가장 오래된 봉투를 버리게 만든다. - -## 원문과 갈리는 자리 - -원문은 과소 계상 쪽에서 누적이 `Math.max(0, ...)` 로 0 에서 멈춘다고 적었다. 관측된 것은 0 이 아니라 마지막에 들어온 메시지 하나의 크기다. 절단은 첫 회에만 일어나고 그 뒤로는 뺀 값과 더한 값이 같다. - -원문은 과대 계상 쪽에서 조기 `TERMINATE` 가 된다고 적었다. 그 정책값으로는 `decide` 가 그 결정을 낼 수 없다. - -분기가 빼는 값, 봉투의 성분 일곱, 판정식, 고정 크기 시험은 원문대로다. - -## 대응 시험이 이것을 볼 수 없다 - -`GrpcSerializedStreamWriterTest:104` 가 `DROP_OLDEST` 정책을 쓰는 유일한 자리인데 크기 공급자를 `8L` 로 고정한다. 그 시험(`:101`\~`:111`)이 단언하는 것은 결과가 `DROPPED` 인 것과 `droppedMessages()` 가 1 인 것과 `queuedMessages()` 가 1 인 것이다. 누적 바이트를 보는 단언은 없다. - -이 시험 파일의 다른 자리도 `1L`, `8L`, `16L` 로 고정한다. 모든 메시지가 같은 크기이면 잘못된 뺄셈이 옳은 값과 같아진다. - -## GrpcSerializedStreamWriter 를 만드는 main 코드가 없다 - -경로 제한 없이 검색하면 이 이름은 자기 파일과 `GrpcSerializedStreamWriterTest` 와 문서 둘에 나온다. 자기 파일 밖에서 이 타입을 쓰는 main 파일은 0 개다. - -## 등급에 대해 - -원본 분석의 등급은 P2 다. 이 기록은 새로 매기지 않는다. - -상류가 P2 로 둔 근거는 판정 입력이 틀어진다는 것이고 그것은 두 방향 모두 실행으로 확인됐다. - -반대 근거는 둘이다. 이 타입을 만드는 main 코드가 0 이라 오늘 이 계산을 도는 인스턴스가 없다. 그리고 원문이 적은 대로 `GrpcFlowControlPolicy.stable()` 의 느린 소비자 정책은 `TERMINATE` 이므로, 이 분기는 배포가 손실 허용 프로파일을 직접 고를 때만 들어간다. - -## 확인하지 못한 것 - -gRPC 스트림을 실제로 열지 않았다. 클래스를 직접 인스턴스화해 호출했다. - -필요한 값 둘 다 공개 접근자가 없어 관측에 리플렉션을 썼다. 실제 큐 합계도 같은 방식으로 봉투를 읽어 더했다. - -배포에서 크기를 어떻게 재는지는 조사하지 않았다. - -스트림을 열어 큰 메시지를 흘린 것이 아니다. 타입의 메서드를 직접 호출하고 내부 상태를 읽었다. - -크기 공급자의 실제 구현은 범위 밖으로 두었다. 이 타입을 만드는 main 코드가 없으므로 그 자리도 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-a20-f010-grpcoutcomereplay.md b/docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-a20-f010-grpcoutcomereplay.md deleted file mode 100644 index 0333700..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-a20-f010-grpcoutcomereplay.md +++ /dev/null @@ -1,152 +0,0 @@ ---- -kind: CASE -slug: a20-f010-grpcoutcomereplay -title: GrpcOutcomeReplay 의 맵에 put·get·size 뿐이고 부르는 코드도 없다 -topic: grpc-and-streaming -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a20-f010-grpcoutcomereplay -evidenceCapturedOn: 2026-09-04 -body: case-a20-f010-grpcoutcomereplay.body.md -assets: - - key: a20-f010-grpcoutcomereplay - file: ../../../final/evidence/rendered/a20-f010-grpcoutcomereplay.svg -evidence: - - ../../../final/evidence/raw/a20-f010-grpcoutcomereplay.txt -source: - - 원본 분석 절은 final/document.md#a20#L664 이다. ---- - -# GrpcOutcomeReplay 의 맵에 put·get·size 뿐이고 부르는 코드도 없다 - -`maxInlineBytes` 는 응답 하나의 바이트 길이만 제한한다. `storedOutcomes` 에 가해지는 연산이 `put`·`get`·`size` 셋뿐이라 수를 줄이는 경로가 없다. 다만 `store` 를 부르는 프로덕션 코드가 없다. 자바독은 이 구현을 배포가 골라 쓰는 여러 방식 가운데 하나로 소개한다. - -## 관계 - -- **GrpcStreamAdmission 의 호출자 맵이 줄지 않고 main 소비자는 0 이다** - 같은 `grpc-policy` 리프에 지우는 경로가 없는 맵이 하나 더 있다. `GrpcStreamAdmission` 은 거절된 승인도 `computeIfAbsent` 를 먼저 지나 항목을 남기고, `GrpcOutcomeReplay` 는 넣는 코드 자체가 없다. -- **체크포인트 전진이 ConcurrentMap 위의 확인 후 쓰기다** - 그 기록은 `GrpcClientMessageDeduplicator` 의 체크포인트 맵을 다루는데 크기가 아니라 갱신의 원자성을 본다. 크기 쪽에서는 같은 클래스가 `endSession` 에 정리 메서드를 두고 있어 이 저장소와 갈린다. -- **조립 결함을 판정하려면 조립하는 쪽을 먼저 읽어야 한다** - 자료구조만으로는 유계인지까지만 말할 수 있고, 이 저장소가 실제로 자라는지는 `store` 를 부르는 코드를 찾은 뒤에야 판정된다. - -## 문제 - -멱등 연산의 응답을 담는 저장소가 ConcurrentMap 하나로 되어 있다. - -그 맵의 크기를 무엇이 제한하고, 무엇이 그것을 채우는지 확인했다. - -## 결론 - -제한하는 값은 항목 하나의 크기뿐이다. :41 이 maxInlineBytes 를 넘는 응답을 IllegalArgumentException 으로 돌려보내면서 객체 참조 뒤에 두라고 안내한다. 통과한 것은 clone() 사본으로 들어간다(:49). - -수를 줄이는 코드는 없다. storedOutcomes 가 나오는 네 줄이 선언과 put(:49)과 get(:54)과 size(:60)이고, 클래스가 final 이며 필드가 private final 이고 맵을 돌려주는 접근자가 없어 밖에서도 줄일 수 없다. 줄이는 연산 이름 열여덟도 0 건이다. - -프로브로 확인했다. 서로 다른 키 천 개를 넣으면 size 가 1001 이고, 그 천 개를 모두 읽어도 같은 키로 다시 써도 1001 이다. - -그 값을 보고 판단하는 프로덕션 코드도 없다. size() 를 읽는 자리는 GrpcIdempotencyInterceptorTest:166 의 단언 하나다. - -넣는 코드도 없다. 이 이름이 나오는 파일은 자기 파일과 그 시험과 구현 계획 문서 하나이고, 자기 파일 밖 main 참조가 0 이다. 같은 패키지의 GrpcIdempotencyInterceptor:99 조차 참조만 돌려주고 이 저장소를 부르지 않는다. - -대조 구현에 같은 잣대를 대면 GrpcClientMessageDeduplicator 도 main 참조가 0 이고 endSession 을 부르는 코드도 없다. 두 클래스의 차이는 도는지 여부가 아니라 자바독과 API 설계에 있다. - -두 클래스가 속한 family 도 다르다. src/grpc-advanced/CLAUDE.md:15 와 :74 가 grpc:* 에서 grpc-advanced:* 를 참조하는 것을 금지하고 registry 가 거부한다고 적는다. - -원본 분석의 등급은 P2 인데 그것을 떠받치던 인과가 이 리비전에서 성립하지 않는다. 이 기록은 새로 매기지 않고 양쪽 근거를 함께 남긴다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 클래스 62 줄 전수 확인, storedOutcomes 가 나오는 줄 전수와 클래스·필드 한정자 확인, 줄이는 연산 이름 열여덟 검색과 대조 파일 검색, 크기 상한의 거부와 저장·조회·덮어쓰기 뒤의 크기를 프로브로 관측, 이 이름이 나오는 파일 전수와 소스 세트 분리, 같은 패키지 인터셉터의 재생 판정 확인, 대조 클래스의 자바독과 정리 메서드와 그 클래스의 main 참조 계수, modules.json 의 family 키 검색과 두 권위 문서의 family 선언 및 참조 금지 확인 -소스 수정 : x - -## 재현 조건 - -1. 클래스를 처음부터 끝까지 읽고 필드와 공개 메서드를 적는다. -2. 맵 필드 이름이 나오는 줄을 전부 찾아 어떤 연산이 가해지는지 센다. -3. 클래스와 필드의 한정자를 읽고 맵을 돌려주는 접근자가 있는지 본다. -4. 줄이는 연산 이름을 여러 가지로 검색하고, 같은 검색을 다른 파일에 걸어 검색이 도는지 확인한다. -5. 크기 상한을 넘는 응답을 넣어 거부 메시지를 받고, 그 뒤 크기가 그대로인지 본다. -6. 서로 다른 키로 여럿 넣은 뒤 읽기와 덮어쓰기가 크기를 줄이는지 확인한다. -7. 이 이름이 나오는 파일을 경로 제한 없이 찾고 소스 세트로 나눈다. -8. 같은 패키지에서 이 저장소를 쓸 법한 클래스를 열어 실제로 부르는지 본다. -9. 대조 구현을 찾아 자바독과 정리 메서드를 읽고, 같은 방식으로 그 클래스의 참조도 센다. -10. 두 클래스가 같은 family 인지 registry 와 각 디렉터리의 권위 문서에서 확인한다. - -## 본문 - - - -`GrpcOutcomeReplay` 는 62 줄짜리 클래스이고 내부는 `ConcurrentMap storedOutcomes` 하나(`:18`)다. 클래스 자바독 `:10`\~`:14` 는 원장과 이 저장소를 나눈 이유를 적는다. 원장 행은 작아야 하고 업무 트랜잭션 안에서 쓰이는데 응답은 클 수 있고 누가 실제로 재시도할 때만 필요하다는 것이다. 그래서 원장은 참조만 저장하고, 그 참조를 배포가 고른 대상 — 작은 인라인 저장소, 객체 저장소, 커밋된 자원의 재조회 — 에 대해 이 클래스가 해석한다. 여기 있는 구현이 그중 인라인 저장소 쪽이다. - -## storedOutcomes 에 가해지는 연산 넷 - -:::evidence key="a20-f010-grpcoutcomereplay" alt="저장소 루트에서 돌린 정적 검색과 /tmp/probe5 에서 컴파일해 돌린 프로브를 합친 출력 166줄. GrpcOutcomeReplay.java 62 줄이 통째로 실린다. 이어서 storedOutcomes 라는 이름이 나오는 네 줄이 선언과 put 과 get 과 size 로 나오고, 클래스가 final 이며 필드가 private final 이라는 두 줄이 붙는다. 줄이는 연산 이름 열여덟 가지를 통틀어 0 건이고, 같은 검색을 대조 파일에 걸면 0 이 아니다. 프로브가 16 바이트를 넣고 17 바이트를 거부당한 메시지를 그대로 내며, 서로 다른 키 천 개를 넣은 뒤 size 가 1001 이고 모두 replay 해도 같은 키로 덮어써도 그대로임을 보인다. 이 이름이 나오는 파일이 셋으로 나열되고 자기 파일 밖 main 참조가 0 이며 추적되지 않는 변경도 없고 java 밖 파일이 하나다. 같은 패키지의 인터셉터가 재생을 판정하는 열여섯 줄이 실리는데 그 파일은 이 타입을 0 번 부른다. 대조 클래스는 grpc-advanced-streaming 리프에 있고 자바독 두 문단과 endSession 세 줄이 나오며 그 클래스의 자기 파일 밖 main 참조도 0 이다. 마지막으로 modules.json 에 family 키가 0 건이라는 것과 두 CLAUDE.md 가 각각 다른 family 의 권위 문서임을 밝히는 대목, 그리고 Stable 이 advanced 를 참조하는 것이 registry 에서 거부된다는 두 줄이 나온다." caption="클래스 62줄 전체 · 맵에 가해지는 연산 넷과 final·private final · 줄이는 이름 열여덟 0 과 대조 검색 · 거부 메시지와 단조 증가하는 size · 이름이 나오는 파일 셋과 main 참조 0 · 같은 패키지 인터셉터의 재생 판정 · 대조 클래스도 main 참조 0 · 두 family 의 경계와 참조 금지 — 166줄 · exit 0" zoom="true" -::: - -`storedOutcomes` 라는 이름은 네 줄에 나온다. 선언(`:18`), `store` 안의 `put`(`:49`), `replay` 안의 `get`(`:54`), `size()` 의 `return`(`:60`)이다. 이 맵에 가해지는 연산은 그 셋이 전부다. - -밖에서 줄일 수도 없다. 클래스가 `final`(`:16`)이고 필드가 `private final` 이며 맵을 돌려주는 접근자가 없다. - -이름으로도 확인했다. `remove`·`clear`·`removeIf`·`computeIfPresent`·`merge`·`entrySet`·`keySet`·`evict`·`expire`·`TTL`·`Duration`·`Instant`·`maximumSize`·`Caffeine`·`LinkedHashMap`·`WeakReference`·`SoftReference`·`Cleaner` 열여덟을 통틀어 0 건이고, 같은 검색을 대조 파일에 걸면 0 이 아니다. - -`maxInlineBytes`(`:19`)는 `store` 안에서 `serializedResponse.length` 와 비교되고(`:41`), 그보다 큰 응답은 `IllegalArgumentException` 으로 거부되며 메시지가 객체 참조 뒤에 두라고 적는다. 통과한 응답은 `serializedResponse.clone()`(`:49`)으로 복사돼 들어간다. - -## 넣으면 줄지 않는다 - -프로브로 확인했다. 16 바이트를 넣으면 `size` 가 1 이고, 17 바이트는 거부되며 거부 뒤에도 `size` 는 그대로다. - -서로 다른 키 천 개를 넣으면 1001 이 된다. 그 천 개를 모두 `replay` 해도 1001 이고, 같은 키로 천 번 덮어써도 1001 이다. 읽기도 덮어쓰기도 수를 줄이지 않고, 늘어나는 축은 서로 다른 참조 문자열의 개수다. - -`size()` 의 반환값을 읽는 프로덕션 코드는 없다. 읽는 자리는 `GrpcIdempotencyInterceptorTest:166` 의 `assertThat(replay.size()).isEqualTo(1)` 하나다. - -## store 를 부르는 main 코드가 없다 - -이 이름이 나오는 파일은 셋이다. 자기 파일, `GrpcIdempotencyInterceptorTest`, 그리고 구현 계획 문서 하나(두 줄)다. 자기 파일 밖 main 참조는 0 이고, 추적되지 않는 변경도 0 이다. - -같은 패키지에 인터셉터가 있는데 그것도 이 타입을 부르지 않는다. `GrpcIdempotencyInterceptor:98`\~`:107` 은 원장 기록이 `COMMITTED` 이면 `GrpcIdempotencyDecision.replay(record.outcomeReference())` 로 **참조만** 돌려준다. 참조를 만들고 소비하는 쪽은 main 에 있고, 그 참조를 이 저장소에 넣는 코드만 없다. - -자바독이 적은 세 선택지 가운데 어느 것을 고를지는 배포의 몫이므로, 이 리비전에서 인라인 저장소가 배선되지 않은 것 자체는 설계와 어긋나지 않는다. - -## 대조 구현에 같은 잣대를 대면 - -`GrpcClientMessageDeduplicator` 의 자바독 `:11`\~`:14` 는 본 키를 집합으로 들면 세션 수명 동안 경계 없이 자라고, 그것이 답하는 "본 적이 있는가" 는 필요한 질문이 아니며, 필요한 질문인 "적용됐는가" 는 단조 증가하는 적용 순번이 상수 공간으로 답하고 집합과 달리 프로세스 재시작도 견딘다고 적는다. `endSession:110`\~`:112` 가 체크포인트를 지우고 그 세션 접두사를 가진 재생 가능 결과를 지운다. - -그런데 그 클래스도 자기 파일 밖 main 참조가 0 이고, `endSession` 을 부르는 main 코드도 없다. 이 기록이 이 저장소에 댄 잣대 — 부르는 코드가 없으면 오늘 일어나지 않는다 — 를 그대로 대면 두 클래스는 같은 상태다. - -그러므로 이것은 도는 코드 둘의 대비가 아니라 자바독과 API 설계의 대비다. 한쪽은 무한 증가를 설계 문제로 이름 붙이고 정리 메서드를 두었고, 한쪽은 그런 자바독도 그런 메서드도 없다. - -## 두 클래스는 다른 family 다 - -`modules.json` 에 `family` 키는 0 건이다. family 를 정하는 것은 각 디렉터리의 권위 문서다. - -`src/grpc/CLAUDE.md:1`·`:3` 이 `grpc:*` family 의 local authority 라고 적고, `src/grpc-advanced/CLAUDE.md:3` 이 `grpc-advanced:*` family 의 local authority 라고 적는다. `:7`\~`:9` 는 후자가 Stable 플랫폼이 의도적으로 제외한 능력을 담으며 디렉터리와 Gradle 접두사를 나눈 이유가 "Stable starter 가 advanced module 을 참조하면 build 가 실패한다" 는 불변 조건을 기계로 검증하기 위해서라고 적는다. - -`:15` 와 `:74` 가 `grpc:*` 에서 `grpc-advanced:*` 로 가는 참조를 금지로 못박는다. registry 가 거부한다는 것이다. - -그러므로 대조 구현을 이쪽으로 옮겨 오는 것은 지금 구조에서 허용되지 않는다. - -## 원문과 갈리는 자리 - -원문은 `store()` 가 멱등 키를 요구하는 메서드가 커밋될 때마다 호출되므로 프로세스 수명 동안 커밋한 멱등 연산 수만큼 항목이 쌓인다고 적었다. main 에는 `store` 를 부르는 줄이 없고 같은 패키지의 인터셉터도 참조 반환에서 멈춘다. - -원문은 비교 대상이 "같은 리프 안에" 있다고 적었다. `GrpcClientMessageDeduplicator` 는 `grpc-advanced-streaming` 이고 이 클래스는 `grpc-policy` 이며, 두 디렉터리는 서로 다른 family 이고 이쪽에서 저쪽을 참조하는 것이 금지돼 있다. - -크기 상한이 항목 하나만 제한한다는 것과 줄이는 경로가 없다는 것은 원문대로다. - -## 등급에 대해 - -원본 분석의 등급은 P2 다. 그 등급을 떠받치던 인과 — 커밋마다 `store` 가 불려 쌓인다 — 는 이 리비전에서 성립하지 않는다. 부르는 코드가 없고, 같은 가족의 자매 클래스도 같은 상태이며, 자바독은 이 구현을 배포가 고를 수 있는 세 선택지 중 하나로 적는다. - -이 기록은 등급을 새로 매기지 않는다. 상류가 P2 로 둔 근거와 여기서 확인한 반대 근거를 함께 남기고, 재감정은 상류의 몫으로 둔다. - -## 확인하지 못한 것 - -프로세스를 오래 띄워 증가를 계측하지 않았다. 줄이는 코드의 부재와, 넣고 읽는 동안 수가 유지되는 것을 확인했다. - -참조 계수는 이름 일치와 추적된 파일만 본다. 문자열로 조립하는 사용은 잡히지 않는다. - -이 저장소를 인터셉터에 물린 채택자 코드는 보지 못했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-a20-f011-grpccompletionreconciler-arraylist.md b/docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-a20-f011-grpccompletionreconciler-arraylist.md deleted file mode 100644 index 193c3c6..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-a20-f011-grpccompletionreconciler-arraylist.md +++ /dev/null @@ -1,153 +0,0 @@ ---- -kind: CASE -slug: a20-f011-grpccompletionreconciler-arraylist -title: GrpcCompletionReconciler 의 pending 만 잠금 없는 ArrayList 다 -topic: grpc-and-streaming -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a20-f011-grpccompletionreconciler-arraylist -evidenceCapturedOn: 2026-09-04 -body: case-a20-f011-grpccompletionreconciler-arraylist.body.md -assets: - - key: a20-f011-grpccompletionreconciler-arraylist - file: ../../../final/evidence/rendered/a20-f011-grpccompletionreconciler-arraylist.svg -evidence: - - ../../../final/evidence/raw/a20-f011-grpccompletionreconciler-arraylist.txt -source: - - 원본 분석 절은 final/document.md#a20#L678 이다. ---- - -# GrpcCompletionReconciler 의 pending 만 잠금 없는 ArrayList 다 - -`GrpcCompletionReconciler.pending` 은 `ArrayList` 이고, `reconcile` 과 `clearPending` 이 잠금 없이 구조를 바꾸는 동안 `pendingCases()` 가 잠금 없이 그것을 순회한다. 같은 리프의 `GrpcCancellationCoordinator` 는 같은 모양의 맵을 들고 공개 메서드 다섯을 모두 `synchronized` 로 닫았다. 다만 이 타입은 오늘 어디에서도 만들어지지 않는다. - -## 관계 - -- **queued 를 줄이는 유일한 연산이 상한을 읽지 않는 승격 안에 있다** - 그 기록은 원자 타입 위의 검사 후 실행이고 여기는 잠금 없는 컬렉션이다. 둘 다 타입이 주는 보증과 코드가 필요로 하는 보증이 다른 자리다. -- **의도적으로 스레드 안전하지 않은 타입이 하나 있다** - 그 기록의 `BoundedByteSink` 는 안전하지 않다는 것을 자바독에 적고 호출마다 새로 만들어 쓴다. 여기에는 그런 표기도 그런 사용 규약도 없다. -- **실행 코드가 없어서 동시성 계약이 전부 문서다** - 그 리프는 실행 코드가 없어 계약을 문서로만 표현한다. 여기는 실행 코드가 있고 그것을 부르는 코드가 없으며, 운영 문서가 그 동작을 미리 적어 두었다. - -## 문제 - -GrpcCompletionReconciler 가 원장이 답을 주지 못한 연산을 목록에 모은다. - -그 목록이 어떤 자료구조이고 어느 메서드가 그것을 바꾸거나 읽는지, 무엇이 언제 넣는지, 같은 리프가 같은 모양을 어떻게 다루는지 확인했다. - -## 결론 - -pending 은 new ArrayList<>()(:25)다. 닿는 자리는 셋인데 하는 일이 다르다 — reconcile:79 의 add 와 clearPending:91 의 remove 는 구조를 바꾸고, pendingCases:86 의 List.copyOf 는 순회해 복사한다. 어느 쪽에도 잠금이 없고 동기화 마커 일곱 가지가 모두 0 이다. - -들어가는 조건은 좁다. :64~:77 이 COMMITTED·FAILED_TERMINAL·업무 조회가 있는 NOT_FOUND 를 모두 먼저 반환하므로, :78 에 닿는 것은 GrpcCompletionResolution:50 이 정의한 UNKNOWN 과 IN_PROGRESS 뿐이다. 앞쪽은 원장 장애 시점이다. - -같은 리프에 같은 모양이 하나 더 있다. GrpcCancellationCoordinator:27 도 동시 자료구조가 아닌 LinkedHashMap 을 오래 들고 있는데, 공개 메서드 register·cancel·markCommitBoundaryCrossed·businessEffectAborted·reason 이 모두 synchronized 다. grpc-policy 안에서 그런 파일은 여덟이다. - -위험은 리스트 구조에 갇혀 있다. pending 은 밖으로 나가지 않고 원소인 PendingCase 는 record 다. - -프로덕션 호출자는 없다. 자기 파일 밖 main 참조가 0 개이고, 같은 검색이 짝 클래스에서는 0 이 아니다. - -운영 런북이 이 클래스의 동작과 에스컬레이션 조건을 적어 두었지만, 같은 문서 :3~:6 이 이 가족 전부가 build-only 라 프로덕션에서 도는 것이 없다고 먼저 밝힌다. 요구하는 스위치는 코드에 정의가 0 개다. - -판정은 P2 이고 원문과 같다. 코드 쪽 사실은 확인됐고, 요청 경로라는 근거는 그것을 부르는 자리가 없어 오늘 서지 않는다. 대신 그 상황을 다룰 절차가 이미 문서에 있고 리스트는 지금 모양 그대로라는 사실이 남는다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 클래스 자바독과 목록 선언 확인, pending 이 나오는 줄 전수와 구조 변경·순회 지점 구분, 동기화 마커 일곱을 이 파일·짝 파일·리프 전체 세 열로 계수, 같은 모양의 필드를 가진 클래스 검색과 그 공개 메서드 확인, reconcile 의 분기 전수와 requiresReconciliation 정의 확인, 경로 제한 없는 참조 검색과 대조 타입 계수, 운영 런북의 앞머리와 해당 절차 확인, 그 런북이 요구하는 프로퍼티의 정의 검색 -소스 수정 : x - -## 재현 조건 - -1. 목록 필드의 선언과 클래스 자바독을 읽는다. -2. 그 이름이 나오는 줄을 전부 찾고 구조를 바꾸는 것과 아닌 것을 가른다. -3. 동기화 마커를 여러 이름으로 세되, 같은 검색을 같은 리프의 다른 파일에도 걸어 검색이 도는지 확인한다. -4. 그 검색을 리프 전체에 걸어 마커를 가진 파일을 센다. -5. 같은 모양의 필드를 가진 클래스를 찾고 그 공개 메서드의 잠금 여부를 읽는다. -6. 목록에 넣는 줄까지 오는 분기를 처음부터 따라가고, 그 조건 메서드의 정의를 읽는다. -7. 이 타입의 참조를 경로 제한 없이 검색하고, 같은 방식으로 대조 타입도 센다. -8. 문서 참조가 있으면 그 문서의 앞머리부터 읽어 절차에 조건이 걸려 있는지 본다. -9. 그 문서가 요구하는 설정 프로퍼티가 코드에 정의돼 있는지 찾는다. - -## 본문 - - - -`GrpcCompletionReconciler` 는 원장이 답을 주지 못한 gRPC 호출을 목록에 모아 두는 93 줄짜리 클래스다. 그 목록은 `new ArrayList<>()`(`:25`)이고 감싸는 것이 없다. - -## pending 을 바꾸는 두 자리와 순회하는 한 자리 - -:::evidence key="a20-f011-grpccompletionreconciler-arraylist" alt="저장소 루트에서 돌린 정적 검색 출력 137줄. GrpcCompletionReconciler 의 클래스 자바독과 ArrayList 선언이 8번부터 26번 줄까지 실리고, pending 이라는 이름이 나오는 줄이 전부 나오는데 선언과 add 와 copyOf 와 remove 넷에 예외 메시지 둘이다. 동기화 마커 일곱 가지를 이 파일과 같은 리프의 GrpcCancellationCoordinator 와 리프 전체에 각각 걸어 세 열로 비교한 표가 나오는데 이 파일은 전부 0 이고 짝 파일은 synchronized 다섯이다. 이어서 같은 검색을 리프 62 파일에 걸어 표시자를 가진 여덟 파일이 나열된다. pending.add 가 실행되는 조건이 reconcile 본문 전체와 함께 실리고, requiresReconciliation 이 UNKNOWN 이나 IN_PROGRESS 일 때만 참이라는 정의가 따라온다. 같은 모양의 필드를 가진 클래스 다섯이 나오고 그중 GrpcCancellationCoordinator 의 공개 메서드가 전부 synchronized 인 것이 보인다. 참조 검색은 자기 파일 밖 main 이 0 개이고 대조 타입은 0 이 아니다. 마지막으로 운영 런북의 첫 아홉 줄이 실려 이 가족 전부가 build-only 라 여기 있는 것 중 프로덕션에서 도는 것이 없다고 문서가 스스로 밝히는 대목이 나오고, 그 런북이 요구하는 스위치가 코드에 정의된 파일 수와 지원 매트릭스의 같은 서술, 그리고 이 클래스에 기대하는 절차가 이어진다." caption="ArrayList 선언과 pending 이 나오는 줄 전부 · 표시자 일곱을 이 파일·짝·리프에 건 세 열 비교 · 리프 62 중 여덟 · add 의 실행 조건과 requiresReconciliation 정의 · 같은 모양 필드 다섯과 짝의 synchronized · 자기 파일 밖 main 0 · 런북의 build-only 자기 규정 — 137줄 · exit 0" zoom="true" -::: - -`pending` 이라는 이름은 여섯 줄에 나온다. 선언(`:25`), `reconcile` 안의 `add`(`:79`), `pendingCases()` 의 `List.copyOf`(`:86`), `clearPending` 의 `remove`(`:91`), 그리고 `PendingCase` 생성자의 예외 메시지 둘(`:34`, `:37`)이다. - -셋이 잠금 없이 같은 리스트에 닿는데 하는 일이 다르다. `add`(`:79`)와 `remove`(`:91`)는 구조를 바꾸고, `List.copyOf(pending)`(`:86`)은 그 목록을 순회해 복사한다. - -원문은 두 실패 모드를 따로 적었다. 동시 `add` 는 원소 유실이나 `ArrayIndexOutOfBoundsException` 이고, `add` 가 도는 중의 `List.copyOf` 는 `ConcurrentModificationException` 이나 널 원소로 인한 `NullPointerException` 이다. `pending` 이 담는 것은 사람이 조정해야 하는 연산이므로 유실은 조정되지 않은 채 잊히는 연산이 된다. 이 기록은 그 실행을 재현하지 않았다. - -위험이 리스트 구조에 갇혀 있기는 하다. `pending` 은 `private final` 이고 밖으로 나가는 것은 `List.copyOf` 로 만든 불변 복사뿐이라 가변 참조가 새지 않는다. `PendingCase` 는 record 라 원소 자체도 불변이다. - -## 항목이 들어가는 조건 - -`reconcile:62`\~`:82` 를 따라가면 `add` 에 닿는 길이 좁다. - -`:64`\~`:67` 이 `COMMITTED` 와 `FAILED_TERMINAL` 을 먼저 돌려보낸다. `:68`\~`:77` 은 `NOT_FOUND` 이고 업무 조회가 주어진 경우인데, 조회가 결과를 보이면 `:71` 에서, 아니면 `:76` 에서 반환한다. 어느 쪽이든 `:78` 에 닿지 않는다. - -`:78` 의 `requiresReconciliation()` 은 `GrpcCompletionResolution:50` 에서 `status == UNKNOWN || status == IN_PROGRESS` 다. - -즉 목록에 쌓이는 것은 원장을 조회하지 못했거나 청구가 아직 진행 중인 경우다. 앞쪽은 원장 장애 시점이고, 그때가 호출이 가장 몰리는 때다. - -## 같은 리프의 짝은 같은 모양을 잠근다 - -동기화 마커 일곱 가지를 이 파일과 `GrpcCancellationCoordinator` 와 리프 전체에 각각 걸었다. 이 파일은 전부 0 이고, 짝 파일은 `synchronized` 다섯이다. 같은 검색이 한쪽에서 0 이 아닌 값을 내므로 검색이 헛돌지 않는다. - -두 클래스는 모양이 같다. `GrpcCancellationCoordinator:27` 이 `Map operations = new LinkedHashMap<>()` 를 들고, 이 클래스가 `List pending = new ArrayList<>()` 를 든다. 둘 다 동시 자료구조가 아니고 둘 다 오래 사는 객체다. - -갈리는 것은 그다음이다. 짝의 공개 메서드는 `register`(`:45`), `cancel`(`:69`), `markCommitBoundaryCrossed`(`:90`), `businessEffectAborted`(`:100`), `reason`(`:110`)이 모두 `synchronized` 다. 이 클래스에는 하나도 없다. - -이 짝만 그런 것도 아니다. 리프를 전부 훑으면 여덟 파일이 잠금이나 동시 자료구조를 쓴다. 이 리프가 동시성을 다루지 않는 것이 아니다. - -## 런북은 자기가 아직 안 도는 것을 알고 쓰였다 - -운영 런북이 이 클래스를 안내한다는 것은 원문에 없다. `docs/runbooks/grpc-platform-operations.md:36`\~`:37` 은 `UNKNOWN` 이 원장을 조회할 수 없었다는 뜻이고 아무것도 결론지을 수 없으며 그 사례는 이 클래스가 큐에 넣고 나중에 재시도한다고 적는다. `:41`\~`:42` 는 그 목록이 여러 차례에 걸쳐 자라면 에스컬레이션하라고 적는다. - -그러나 같은 문서 `:3`\~`:6` 이 먼저 조건을 건다. 범위가 `:grpc:*` 가족이고 오늘 전부 build-only 이며 모든 leaf 의 `runtime_memberships` 가 비어 있어 여기 있는 것 중 프로덕션에서 도는 것이 없다는 것이다. 그렇게 미리 쓰는 이유도 적는다 — 이 문서가 다루는 상태들은 당직자가 새벽 세 시에 원리부터 풀어낼 수 있는 것이 아니고, 동작을 런북보다 먼저 출하하면 그것을 처음 만나는 사람이 그 일을 하게 된다는 것이다. - -`:9` 는 `ca-skeleton.grpc.platform.enabled` 가 `true` 여야 플랫폼이 시작된다고 적는데, 그 프로퍼티를 정의하는 파일이 저장소에 0 개다. `docs/compatibility/grpc-support-matrix.md:66`\~`:67` 도 배포 아티팩트가 이 가족을 싣지 않는다고 따로 적는다. - -그러므로 오늘 이 절차를 밟는 운영자는 없다. 남는 것은 그 절차가 이미 적혀 있고, 배선되는 순간 그것이 읽는 대상이 이 잠금 없는 리스트라는 것이다. - -## 이 타입을 만드는 프로덕션 코드가 없다 - -자기 파일 밖에서 이 타입을 쓰는 main 파일은 0 개다. 참조는 자기 파일 둘, `GrpcCompletionReconcilerTest` 다섯 줄, 그리고 문서 셋이다. 같은 검색을 `GrpcCancellationCoordinator` 에 걸면 0 이 아니다. - -## 원문과 갈리는 자리 - -원문은 이 리프에서 스레드 안전성을 명시적으로 다루는 유일한 클래스가 `GrpcSerializedStreamWriter` 라고 적었다. 62 파일 중 여덟이 동시성 구성을 쓴다. - -원문은 `reconcile` 이 완료 결과가 불확실한 호출마다 불린다고 적었다. 목록에 쌓이는 조건은 그보다 좁아서 `UNKNOWN` 과 `IN_PROGRESS` 둘뿐이고, `NOT_FOUND` 는 업무 조회가 있는 경로에서 `:78` 에 닿지도 않는다. - -`pending` 이 `ArrayList` 라는 것, 두 자리가 잠금 없이 구조를 바꾸고 한 자리가 잠금 없이 순회한다는 것, 마커가 0 이라는 것은 원문대로다. - -## 등급에 대해 - -원본 분석의 등급은 P2 다. 이 기록은 새로 매기지 않는다. - -상류가 P2 로 둔 근거는 요청 경로에서 동기화 없는 리스트가 변경된다는 것이다. 자료구조와 표시자 부재는 확인했고, 요청 경로라는 부분은 부르는 코드가 없어 오늘 성립하지 않는다. - -같은 무게로 반대편에 놓이지 않는 사실이 하나 있다. 이 코드가 오늘 도는 것은 아니지만, 그것이 다룰 상황과 그때의 절차는 이미 문서에 적혀 있다. 배선은 남은 작업이고 리스트는 지금 모양 그대로다. - -## 확인하지 못한 것 - -스레드를 여럿 띄워 동시 호출로 원소가 사라지는 것을 재현하지 않았다. 리스트 타입과 마커 부재와 세 자리가 하는 일을 읽은 데까지다. - -`UNKNOWN` 상태를 만들어 넣지 않았다. 분기와 조건 정의를 코드로 따라갔다. - -다른 브랜치에 이 클래스를 물리는 코드가 있는지 확인하지 않았다. 이 리비전의 main 에 없다는 것까지다. - -다른 브랜치에 이 클래스를 물리는 코드가 있는지 확인하지 않았다. 이 리비전의 main 에 없다는 것까지다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-analysis-finding-a15-f001.md b/docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-analysis-finding-a15-f001.md deleted file mode 100644 index 86a382c..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-analysis-finding-a15-f001.md +++ /dev/null @@ -1,129 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a15-f001 -title: 원인 사슬 순회가 2-순환에서 무한 루프에 빠지고, 저장소는 이미 그 사례를 이름으로 적어 두었다 -topic: grpc-and-streaming -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a15-f001 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a15-f001.body.md -assets: - - key: analysis-finding-a15-f001 - file: ../../../final/evidence/rendered/analysis-finding-a15-f001.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a15-f001.txt -source: - - 원본 분석 절은 final/document.md#a15#L171 이다. ---- - -# 원인 사슬 순회가 2-순환에서 무한 루프에 빠지고, 저장소는 이미 그 사례를 이름으로 적어 두었다 - -오류 코드 판정의 종료 조건은 자기참조 하나다. 서로를 원인으로 갖는 두 예외에서는 그 조건이 참이 되지 않는다. 같은 저장소의 다른 모듈이 정확히 그 경우를 이름으로 적고 깊이 제한을 채택했다. - -## 관계 - -- **원인 사슬은 바깥부터 안쪽까지 걸어야 한다** - 같은 계열의 순회 규칙이다. -- **자기참조 검사는 2-순환을 잡지 못한다** - 이 사례가 그 규칙의 형태다. -- **같은 문제를 다른 모듈에서는 닫았다** - 기록하는 이유다. - -## 문제 - -원격 호출 오류를 코드로 바꾸는 메서드가 원인 사슬을 순회한다. - -종료 조건이 무엇인지 확인했다. - -## 결론 - -자기참조 하나뿐이다. 현재 예외의 원인이 자기 자신이면 멈춘다. - -서로를 원인으로 갖는 두 예외에서는 이 조건이 참이 되지 않는다. - -현재 지점이 두 예외 사이를 무한히 순환한다. - -이 사슬은 평범한 자바로 구성 가능하다. 하나를 만들고 그것을 원인으로 삼는 둘째를 만든 뒤, 첫째의 원인을 둘째로 지정하면 된다. - -같은 저장소가 이 정확한 위험을 다른 모듈에서 이름으로 서술하고 다른 관용구를 택했다. - -깊이 제한이지 순환 탐지가 아니라는 것이다. 원인 사슬은 순환일 수 있고 두 예외가 서로를 원인으로 지정한 경우가 그것이며, 제한 없는 순회 하나가 발송 스레드를 멈추게 한다는 것이다. 열이면 실제 전송 감싸기보다 훨씬 깊다는 것이다. - -즉 그 주석은 자기참조 검사가 놓치는 바로 그 경우를 지목하고 깊이 제한을 그 이유로 채택한다. - -저장소의 아홉 순회 지점 중 다섯이 깊이 제한이고 넷이 자기참조 검사다. - -실패 시나리오는 이렇다. - -기능 서비스가 순환 원인 사슬을 가진 라이브러리 예외를 전파한다. 일부 연결 풀과 재시도 감싸개가 실패 원인을 상호 참조하는 형태로 만든다. - -오류 종료 경로가 그 예외를 상태에 실어 정제 종료로 보내고, 오류 코드 판정이 진입해 돌아오지 않는다. - -처리기 스레드 하나가 처리기를 태우며 멈추고, 클라이언트는 응답도 상태도 받지 못한 채 마감까지 기다린다. - -같은 예외가 반복되면 서버 스레드가 하나씩 소진된다. - -나머지 세 지점의 영향도 비슷하다. - -두 연결 끊김 탐지기는 요청 처리 중 클라이언트 끊김을 판정하는 곳이고, 트랜잭션 재시도 분류기는 재시도 여부를 판정하는 곳이다. 셋 다 요청 스레드 위에서 실행된다. - -권고는 네 지점을 깊이 제한으로 통일하는 것이다. - -다른 모듈의 형태가 이미 정본이고 그 근거까지 코드에 있다. - -이 리프에서는 순회 반복문을 깊이 상한이 있는 형태로 바꾸면 닫힌다. - -판정은 P2 다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 종료 조건 분석과 저장소 전체 순회 지점 계수 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/179 계열에 있다. - -1. 오류 코드 판정의 순회 반복문을 읽는다. -2. 종료 조건을 확인한다. -3. 두 예외가 서로를 원인으로 갖는 사슬을 만든다. -4. 그 사슬에서 종료 조건이 참이 되는지 확인한다. -5. 저장소의 다른 순회 지점을 세고 관용구를 분류한다. - -## 본문 - - - -`errorCodeOf`의 종료 조건은 `current.getCause() == current` 하나다. 서로를 원인으로 갖는 두 예외(`a.cause = b`, `b.cause = a`)에서는 이 조건이 참이 되지 않고 `current`가 a→b→a→b로 무한히 순환한다. 이 사슬은 평범한 자바로 구성 가능하다 — `a = new RuntimeException(); b = new RuntimeException(a); a.initCause(b);`. - -## MvcDisconnectDetector 참조 위치 - -:::evidence key="analysis-finding-a15-f001" alt="코드베이스에서 MvcDisconnectDetector 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MvcDisconnectDetector 코드베이스 검색 — 2줄 · exit 0" zoom="true" -::: - -## 같은 저장소가 이 위험을 이름으로 적고 다른 관용구를 택했다 - -> `JdkNotificationHttpGateway:93-97` — "Depth-bounded rather than cycle-detecting: **a cause chain can be circular (two exceptions each `initCause`'d to the other)**, and an unbounded walk over one hangs the dispatch thread. Ten is far deeper than any real transport wrapping." - -저장소의 아홉 개 순회 지점 중 다섯이 깊이 제한이고 넷이 자기참조 검사다(§3.2). - -## 실패 시나리오 - -feature gRPC 서비스가 순환 원인 사슬을 가진 라이브러리 예외를 전파한다. `closeWithError`가 그 예외를 `Status.withCause`에 실어 sanitizing `close`로 보내고, `errorCodeOf`가 진입해 돌아오지 않는다. gRPC 핸들러 스레드 하나가 CPU를 태우며 멈추고, 클라이언트는 응답도 상태도 받지 못한 채 데드라인까지 기다린다. 같은 예외가 반복되면 서버 스레드가 하나씩 소진된다. - -## 나머지 세 지점의 영향도 - -`MvcDisconnectDetector`와 `WebFluxDisconnectDetector`는 요청 처리 중 클라이언트 연결 끊김을 판정하는 곳이고, `TransactionRetryClassifier`는 트랜잭션 재시도 여부를 판정하는 곳이다. 셋 다 요청 스레드 위에서 실행된다. P2. - -## 권고 - -네 지점을 깊이 제한으로 통일한다. `JdkNotificationHttpGateway`의 형태가 이미 정본이고 그 근거까지 코드에 있다. 이 leaf에서는 `errorCodeOf`의 `while`을 `for (int depth = 0; current != null && depth < 16; depth++, current = current.getCause())`로 바꾸면 닫힌다. - -## 확인하지 못한 것 - -순환 사슬을 실제로 흘려 스레드가 멈추는 것을 재현하지 않았다. 종료 조건상 그 결과가 나온다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-analysis-finding-a15-f002.md b/docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-analysis-finding-a15-f002.md deleted file mode 100644 index 1427542..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-analysis-finding-a15-f002.md +++ /dev/null @@ -1,115 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a15-f002 -title: 설정 바인딩이 마스터 스위치 밖에서 일어난다. 컴포지션 루트의 자기 규칙과 어긋난다 -topic: grpc-and-streaming -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a15-f002 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a15-f002.body.md -assets: - - key: analysis-finding-a15-f002 - file: ../../../final/evidence/rendered/analysis-finding-a15-f002.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a15-f002.txt -source: - - 원본 분석 절은 final/document.md#a15#L187 이다. ---- - -# 설정 바인딩이 마스터 스위치 밖에서 일어난다. 컴포지션 루트의 자기 규칙과 어긋난다 - -설정 객체가 두 경로로 등록된다. 게이트 안쪽의 명시적 활성화와 게이트 바깥의 전역 훑기다. 후자가 있으면 마스터 스위치와 무관하게 결속이 일어난다. 조립 루트의 자바독이 과거에 같은 비대칭이 만든 사고를 기록한다. - -## 관계 - -- **원인 사슬 순회가 2-순환에서 무한 루프에 빠지고 저장소는 이미 그 사례를 이름으로 적어 두었다** - 같은 리프의 다른 사례다. -- **지금 안전한 것은 규칙이 지켜져서가 아니라 기본값이 유효해서다** - 이 사례가 그 규칙의 형태다. -- **문서가 선언한 경계는 코드가 닫아야 경계다** - 같은 계열의 규칙이다. - -## 문제 - -이 리프의 설정 객체가 마스터 스위치 뒤에 있어야 한다. - -등록 경로를 확인했다. - -## 결론 - -두 경로가 있다. - -하나는 리프 설정 클래스의 명시적 활성화다. 게이트 안쪽이다. - -다른 하나는 조립 루트의 전역 설정 훑기다. 게이트 바깥이다. - -후자가 있으면 마스터 스위치와 무관하게 결속이 일어난다. - -조립 루트의 자바독이 이 구조가 과거에 만든 사고를 기록한다. - -이전에 있던 비대칭이 문제였다는 것이다. 빈은 게이트를 받고 설정은 받지 않았기 때문에, 알림 마스터가 꺼진 배포에서 알림 설정 객체가 스스로 결속했다는 것이다. - -그리고 그 교훈을 다섯 선택 어댑터에 적용하면서 이 리프를 포함한 셋은 목록에 남겼다. - -지금 이 리프에서는 무해하다. - -검증이 전부 게이트를 존중하거나 안전한 기본값을 갖는다. - -지역 비보안 설정 검증은 비활성일 때 즉시 참이 된다. 포트 범위 검증과 결속 주소 검증과 종료 유예 검증은 모두 유효한 기본값을 가진다. - -부작용은 두 가지뿐이다. - -비활성 배포에서도 속성 빈이 만들어진다. 그리고 미지 필드 거부가 켜져 있으므로 이 이름공간 아래 오타 하나가 이 기능을 쓰지 않는 배포의 부팅을 실패시킨다. - -둘째는 오히려 바람직한 쪽에 가깝다. - -기록하는 이유는 규칙과 적용이 갈린다는 점이다. - -같은 자바독이 이 목록에 패키지를 되돌리면 결속이 복원되어 게이트를 조용히 무효화한다고 경고하고, 이 패키지가 그 목록에 있다. - -지금 이 리프가 안전한 것은 규칙이 지켜져서가 아니라 기본값이 전부 유효하기 때문이고, 새 검증이 하나 추가되면 그 보호막이 사라진다. - -판정은 P3 다. - -## 검증 환경 - -Spring Boot : 4.0.8 -확인 방식 : 등록 경로 대조와 검증 애너테이션 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/179 계열에 있다. - -1. 설정 객체의 등록 경로를 모두 찾는다. -2. 각 경로가 게이트 안쪽인지 바깥인지 확인한다. -3. 조립 루트의 자바독을 읽는다. -4. 그 자바독이 든 목록에 이 패키지가 있는지 확인한다. -5. 설정 객체의 검증 애너테이션과 기본값을 확인한다. - -## 본문 - - - -`GrpcServerProperties`는 두 경로로 등록된다 — `GrpcServerConfig`의 `@EnableConfigurationProperties`(게이트 안쪽)와 `CaSkeletonApplication`의 `@ConfigurationPropertiesScan`(게이트 바깥). 후자가 있으면 `ca-skeleton.grpc.enabled`와 무관하게 바인딩이 일어난다(§3.4). - -## GrpcServerProperties 참조 위치 - -:::evidence key="analysis-finding-a15-f002" alt="코드베이스에서 GrpcServerProperties 를 검색한 출력 4줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcServerProperties 코드베이스 검색 — 4줄 · exit 0" zoom="true" -::: - -## 컴포지션 루트가 이 형태를 사고로 기록했다 - -`CaSkeletonApplication`의 javadoc — "The asymmetry that existed before — beans gated, settings not — is why a notification settings object bound itself in a deployment whose notification master was off." 그 교훈을 다섯 optional 어댑터에 적용하면서 grpc·web·websocket은 목록에 남겼다. - -## 지금 이 leaf에서는 무해하다 - -검증이 전부 게이트를 존중하거나 안전한 기본값을 갖는다. P3. - -## 확인하지 못한 것 - -마스터 스위치를 끄고 띄워 속성 빈이 만들어지는 것을 확인하지 않았다. 등록 경로상 그 결과가 나온다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-analysis-finding-a20-f004.md b/docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-analysis-finding-a20-f004.md deleted file mode 100644 index 7b1f509..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-analysis-finding-a20-f004.md +++ /dev/null @@ -1,112 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a20-f004 -title: 조립 경계가 정책 객체 9개를 만들고 서버를 만들지 않는다 -topic: grpc-and-streaming -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a20-f004 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a20-f004.body.md -assets: - - key: analysis-finding-a20-f004 - file: ../../../final/evidence/rendered/analysis-finding-a20-f004.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a20-f004.txt -source: - - 원본 분석 절은 final/document.md#a20#L298 이다. ---- - -# 조립 경계가 정책 객체 9개를 만들고 서버를 만들지 않는다 - -플랫폼 자동 설정이 등록하는 아홉 빈은 전부 프로파일과 정책과 레지스트리다. 서버도 인터셉터 사슬도 서비스 어댑터 등록도 없다. 그것을 담당하는 타입들이 주 참조 0 이다. - -## 관계 - -- **크로스 스택 게이트가 검증하는 조립은 픽스처의 조립이고 플랫폼의 조립이 아니다** - 다른 가족의 같은 형태다. -- **저장소 어디에도 참조가 없는 타입 3개** - 같은 리프의 더 나아간 사례다. -- **손으로 만드는 목록은 적어도 한 번은 거꾸로 만든다** - 인터셉터 사슬 클래스가 존재하는 이유다. - -## 문제 - -이 플랫폼의 자동 설정이 무엇을 등록하는지 확인했다. - -## 결론 - -아홉 빈이 전부 프로파일과 정책과 레지스트리다. - -서버도, 인터셉터 사슬도, 서비스 어댑터 등록도 없다. - -그리고 그것을 담당하는 타입들이 주 참조 0 이다. 테스트 참조는 각각 하나씩 있다. - -서버 인터셉터 사슬과 두 인터셉터, 서비스 어댑터 규격, 재시도 조정자와 소유권 검증자, 스트림 승인과 직렬 기록기와 간극 탐지기와 수명 조정자, 배수 조정자와 스냅숏 서비스, 형 있는 스텁 공장과 호출 문맥이다. - -인터셉터 사슬 클래스의 자바독이 자기 존재 이유를 적는다. - -그 뒤집힘이 호출 지점의 목록 리터럴이 아니라 이 클래스가 존재하는 이유라는 것이다. - -프레임워크의 감싸기 함수가 각 인터셉터를 이전 것 위에 감싸므로 마지막에 넘긴 것이 실행 시점에 가장 바깥이 되며, 그것은 순서가 읽히는 방식과 반대라는 것이다. - -이 목록을 손으로 만드는 모든 코드베이스가 적어도 한 번은 거꾸로 만든다는 것이다. - -그 클래스가 주 참조 0 이다. - -판정은 P2 다. - -## 검증 환경 - -Spring Boot : 4.0.8 -확인 방식 : 자동 설정 빈 목록과 타입별 참조 계수 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/255 계열에 있다. - -1. 플랫폼 자동 설정의 빈 목록을 확인한다. -2. 각 빈의 성격을 분류한다. -3. 서버와 인터셉터 사슬과 어댑터 등록을 검색한다. -4. 그 역할의 타입들을 나열하고 참조를 센다. -5. 인터셉터 사슬 클래스의 자바독을 읽는다. - -## 본문 - - - -`GrpcPlatformAutoConfiguration`이 등록하는 9개는 전부 **프로파일·정책·레지스트리**다. 서버도, 인터셉터 체인도, 서비스 어댑터 등록도 없다. - -## GrpcPlatformAutoConfiguration 참조 위치 - -:::evidence key="analysis-finding-a20-f004" alt="코드베이스에서 GrpcPlatformAutoConfiguration 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcPlatformAutoConfiguration 코드베이스 검색 — 2줄 · exit 0" zoom="true" -::: - -## 그것을 담당하는 타입들이 main 참조 0이다 - -| 타입 | leaf | 역할 (javadoc) | main | test | -|---|---|---|---|---| -| `GrpcServerInterceptorChain` | server | "Builds the server interceptor chain in the Stable order" | **0** | 1 | -| `ProtovalidateGrpcInterceptor` | policy | 요청 검증 인터셉터 | **0** | 1 | -| `GrpcIdempotencyInterceptor` | policy | 멱등성 인터셉터 | **0** | 1 | -| `GrpcServiceAdapter` | server | typed service adapter SPI | **0** | 1 | -| `GrpcRetryCoordinator` · `GrpcRetryOwnershipValidator` | policy | 재시도 소유권 | **0** | 1 | -| `GrpcStreamAdmission` · `GrpcSerializedStreamWriter` · `GrpcStreamGapDetector` · `GrpcStreamLifecycleCoordinator` | policy | 단일 writer·갭 탐지 | **0** | 1 | -| `GrpcDrainCoordinator` · `GrpcPlatformSnapshotService` | admin | drain·정책 스냅샷 | **0** | 1 | -| `GrpcTypedStubFactory` · `GrpcClientCallContext` | client | typed stub·호출 컨텍스트 | **0** | 1 | - -## 그 클래스가 존재하는 이유가 그대로 위험이 된다 - -`GrpcServerInterceptorChain`의 javadoc — "`ServerInterceptors.intercept` wraps each interceptor around the previous one, so the last one passed is the outermost at runtime — the opposite of how the order reads. **Every codebase that builds this list by hand gets it backwards at least once, and the symptom is an exception boundary that catches nothing.**" 그 클래스를 조립에서 쓰는 곳이 없으므로, 채택자가 인터셉터 목록을 직접 만들면 그 javadoc이 서술한 실수를 그대로 하게 된다. - -## 73이라는 숫자를 그대로 결함 수로 읽으면 안 된다 - -260개 main 타입 중 73개가 main 참조 0이지만, build-only 라이브러리 가족에서 공개 API 표면이 내부 참조를 갖지 않는 것은 정상이다. 위 표는 그중 **가족 내부의 다른 코드가 불러야 하는 조립·기계 타입**만 골라낸 것이다. P2. - -## 확인하지 못한 것 - -이 플랫폼을 켜고 서버가 뜨지 않는 것을 재현하지 않았다. 빈 목록상 그 결과가 나온다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-analysis-finding-a20-f005.md b/docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-analysis-finding-a20-f005.md deleted file mode 100644 index edb428e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/grpc-and-streaming/case/case-analysis-finding-a20-f005.md +++ /dev/null @@ -1,110 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a20-f005 -title: 저장소 어디에도 참조가 없는 타입 3개 -topic: grpc-and-streaming -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a20-f005 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a20-f005.body.md -assets: - - key: analysis-finding-a20-f005 - file: ../../../final/evidence/rendered/analysis-finding-a20-f005.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a20-f005.txt -source: - - 원본 분석 절은 final/document.md#a20#L321 이다. ---- - -# 저장소 어디에도 참조가 없는 타입 3개 - -주 참조와 테스트 참조가 모두 0 인 타입이 셋이다. 앞의 둘은 채택자가 부를 표면이라 참조 0 이 설계와 모순되지는 않는다. 셋째는 안정 계약에 있고 자바독이 호출자가 이것으로 분기한다고 단정한다. - -## 관계 - -- **조립 경계가 정책 객체 9개를 만들고 서버를 만들지 않는다** - 같은 리프의 상위 사실이다. -- **소비자가 없는 fixture 셋** - 같은 계수 방식의 사례다. -- **던지는 코드도 잡는 코드도 없으면 분기할 것이 없다** - 이 사례가 그 규칙의 형태다. - -## 문제 - -이 가족의 타입별 참조를 셌다. - -주 참조도 테스트 참조도 0 인 것, 즉 선언 파일 외에 아무 곳에서도 이름이 등장하지 않는 타입을 가려냈다. - -## 결론 - -셋이다. - -앞의 둘은 고급 호환 리프의 반응형 표면이다. - -하나는 단항 호출을 단일 값 흐름으로, 서버 스트림을 다중 값 흐름으로 노출한다. - -다른 하나는 반응형 사용 사례를 서비스 어댑터 뒤에서 돌린다. - -둘 다 채택자가 부를 타입이므로 참조 0 이 설계와 모순되지는 않는다. - -다만 테스트도 0 이라 다른 고급 타입들과 다르다. 나머지 고급 미참조 타입은 전부 테스트 참조가 하나씩 있다. - -셋째가 더 구체적이다. - -마감 초과 예외가 안정 핵심 계약에 있다. - -자바독이 일반 플랫폼 예외가 아니라 자기 타입인 이유를 호출자가 이것으로 분기하기 때문이라고 단정한다. - -그런데 던지는 코드도 잡는 코드도 테스트도 없다. - -그리고 그 타입의 조정 필요 여부 메서드가 상태 코드가 답할 수 없는 질문에 답한다고 적혀 있는데, 그 메서드를 부르는 곳이 없다. - -판정은 P3 다. - -## 검증 환경 - -확인 방식 : 타입별 주 참조와 테스트 참조 전수 계수 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/255 계열에 있다. - -1. 가족의 타입 목록을 만든다. -2. 각 타입의 주 참조와 테스트 참조를 센다. -3. 둘 다 0 인 타입을 가려낸다. -4. 각 타입의 자바독 용도를 읽는다. -5. 셋째 타입을 던지거나 잡는 코드를 검색한다. - -## 본문 - - - -`main = 0`이면서 `test = 0`인 것, 즉 선언 파일 외에 아무 곳에서도 이름이 등장하지 않는 타입이 셋이다. - -| 타입 | leaf | javadoc이 말하는 용도 | -|---|---|---| -| `ReactiveGrpcClient` | advanced-compat | "Exposes a unary call as a `Mono` and a server stream as a `Flux`" | -| `ReactiveGrpcServerAdapter` | advanced-compat | "Runs a reactive use case behind a gRPC service adapter" | -| `GrpcDeadlineExceededException` | **core-api** | "Its own type rather than a generic platform exception **because callers branch on it**" | - -## main 과 test 양쪽에서 0 인 타입 - -:::evidence key="analysis-finding-a20-f005" alt="분석 문서 final/document.md#a20 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20 발췌 — 15줄" zoom="true" -::: - -## 앞의 둘은 설계와 모순되지 않는다 - -advanced 가족의 Reactor 표면이고 채택자가 부를 타입이다 — 다만 **테스트도 0**이라 다른 advanced 타입들과 다르다(나머지 advanced 미참조 타입은 전부 `test=1`). - -## 세 번째가 더 구체적이다 - -`GrpcDeadlineExceededException`은 Stable core-api에 있고, javadoc이 "callers branch on it"이라고 단정하는데 **던지는 코드도 잡는 코드도 테스트도 없다.** `requiresReconciliation()`이 "status code가 답할 수 없는 질문에 답한다"고 적혀 있고, 그 메서드를 부르는 곳이 없다. P3. - -## 확인하지 못한 것 - -채택자가 앞의 두 타입을 실제로 쓰는지 저장소 밖에서 확인할 수 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/http-client-and-resilience/case/case-a11-f001-close.md b/docs/clean-architecture-backend-template/tech-log-studio/http-client-and-resilience/case/case-a11-f001-close.md deleted file mode 100644 index f7abed2..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/http-client-and-resilience/case/case-a11-f001-close.md +++ /dev/null @@ -1,241 +0,0 @@ ---- -kind: CASE -slug: a11-f001-close -title: 누수 하나는 회수되고 하나는 회수되지 않는다 -topic: http-client-and-resilience -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a11-f001-close -evidenceCapturedOn: 2026-09-02 -body: case-a11-f001-close.body.md -assets: - - key: a11-f001-close - file: ../../../final/evidence/rendered/a11-f001-close.svg - - key: a11-f001-close-leak - file: ../../../final/evidence/rendered/a11-f001-close-leak.svg -evidence: - - ../../../final/evidence/raw/a11-f001-close.txt - - ../../../final/evidence/raw/a11-f001-close-leak.txt -source: - - 원본 분석 절은 final/document.md#a11#L121 이다. 등급은 P3 이다. 다시 던지기가 스케줄러 종료 블록보다 앞에 있어 실패 경로에서 그 블록에 닿지 않는다는 관찰과, 그래서 javadoc 이 적은 스레드 수명 성질이 깨진다는 판정이 그 절에 있다. 바로 위 루프에는 예외를 모으는 수정이 적용됐는데 스케줄러에는 오지 않았다는 지적, 대응 test 가 실패 없는 경로만 본다는 사실도 있다. 판정 근거 셋과 그럼에도 기록해야 하는 사유, 그리고 수정이 한 줄이라는 서술까지 그 절이 적는다. - - 이 기록이 더한 것은 넷이다. 그 경로를 실제로 만들어 배수 스레드가 남는 것을 관측했고, 한 번 더 닫으면 회수된다는 것도 확인했다. 강제 닫기가 하나도 던지지 않는데 스레드가 남고 회수되지 않는 두 번째 경로를 찾았다. 닫기를 거부한 런타임이 자원을 쥔 채 닫힘으로 표시된다는 것을 자원 닫기 호출 수로 확인했다. 그리고 주석이 감시자로 지목한 묶음이 그 스레드를 만든 적조차 없다는 것을 같은 순서로 돌려 확인했다. ---- - -# 누수 하나는 회수되고 하나는 회수되지 않는다 - -레지스트리 닫기가 첫 실패를 다시 던지는 자리가 스케줄러 종료 블록보다 앞에 있다. 그 순서에서 배수 스레드가 남는데, 한 번 더 닫으면 회수된다. 종료 대기가 실패하는 다른 경로에서는 참조가 이미 비워진 뒤라 회수되지 않는다. 그리고 닫기를 거부한 런타임은 자원이 열린 채 닫힘으로 표시된다. - -## 관계 - -- **회전이 틈으로 관측되지 않게 만든 순서와 두 누수 이력** - 이 결함이 그 두 수정 중 뒤엣것의 남은 절반이다. -- **타입이 문서화한 불변식은 타입이 강제한다** - javadoc 이 적은 스레드 수명을 강제하는 장치가 없다. -- **그 속성을 보일 수 없는 대역 위에서 통과한 테스트는 증인이 아니다** - 주석이 감시자로 지목한 묶음이 그 스레드를 본 적이 없다. - -## 문제 - -레지스트리의 닫기는 모든 런타임을 닫고 배수 스케줄러를 종료해야 한다. - -클래스 자바독이 그 성질을 적는다. 하나뿐인 예약 실행기가 레지스트리와 함께 종료되므로 어떤 스레드도 레지스트리보다 오래 살지 않는다는 것이다. - -## 결론 - -수정이 절반만 적용되어 있다. - -닫기의 마지막 부분에서 은퇴 목록과 런타임 목록을 비우고, 첫 실패가 있으면 다시 던진다. 그 던지기 다음에 스케줄러 종료 블록이 온다. - -강제 닫기 하나라도 던지면 144 에서 끝나 146 이후에 닿지 않는다. 실행해서 확인했다. 회전 하나를 만들고 닫으면 배수 스레드가 1 로 남고 살아 있다. - -이 누수는 회수된다. 두 목록이 이미 비워진 뒤라 두 번째 닫기에는 실패할 것이 없고, 그때 146 에 닿아 스레드가 정리된다. - -같은 메서드에 회수되지 않는 경로가 하나 더 있다. 종료 블록 안의 대기가 5 초를 넘기면 153 이 던지는데, 그 시점에는 146 이 이미 참조를 비운 뒤다. 강제 닫기가 하나도 던지지 않아도 일어난다. 마감 작업이 인터럽트를 무시하고 도는 동안 닫으면 그렇게 된다. 실행해서 확인했다. 첫 닫기가 5000 밀리초 뒤에 던지고 스레드가 남으며, 두 번째 닫기는 정상 반환하는데 스레드는 그대로다. - -새는 것이 스레드만도 아니다. 런타임 닫기는 자원 닫기를 부르기 전에 상태를 닫힘으로 바꾼다. 그래서 닫기를 거부한 런타임은 자원이 해제되지 않은 채 닫힘으로 표시되고, 이후의 강제 닫기는 상태 검사에 걸려 아무 일도 하지 않는다. 자원 닫기 호출 수가 1 에서 늘지 않는 것으로 확인했다. 바로 위 루프가 막으려던 것이 하나가 거부해도 나머지가 새지 않게 하는 것이었는데, 거부한 그 하나는 되돌릴 수 없다. - -종료 블록 안의 주석은 배수 스레드가 살아 있는 채 레지스트리가 반환하면 회전 주기마다 스레드 하나가 샌다고 적고, 자원 경계 묶음을 그 감시자로 지목한다. - -그 묶음은 이 스레드를 본 적이 없다. 그 묶음이 보는 회전은 임차를 쥐지 않아 은퇴 세대가 곧바로 닫히고, 스케줄러를 만드는 조건이 거짓이 된다. 회전 49 회 동안 배수 스레드가 0 이다. 마지막 줄의 단언은 성공 경로에서도 빈 검사다. - -같은 성질을 보는 단위 test 는 실패 없는 경로만 만든다. - -판정은 P3 다. 스레드가 데몬이라 가상 머신 종료를 막지 않고, 레지스트리당 하나이며, 닫기 실패라는 조건이 필요하다. 원본이 든 근거 그대로다. - -그럼에도 기록하는 것은 주석이 지목한 감시자가 그 누수를 본 적이 없기 때문이다. - -수정은 한 줄이 아니다. 종료 블록을 finally 로 옮기려면 그 앞의 루프까지 감싸는 try 를 먼저 만들어야 하고, 만들어도 그 블록 안에 던지는 줄이 있어 원래 실패가 밀려난다. 던지기를 뒤로 미루는 편이 손이 덜 가지만 두 번째 누수를 못 막는다. 어느 쪽이든 스케줄러 실패를 원래 실패에 억제 예외로 붙이는 처리가 필요하다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 닫기 메서드의 제어 흐름 확인, 실행 탐침 -소스 수정 : x - -## 재현 조건 - -1. 레지스트리 닫기 메서드를 끝까지 읽는다. -2. 첫 실패를 다시 던지는 줄과 스케줄러 종료 블록의 앞뒤를 확인한다. -3. 런타임 닫기가 상태와 자원 닫기 중 무엇을 먼저 하는지 읽는다. -4. 배수 스레드를 만드는 조건과 이름과 데몬 여부를 확인한다. -5. 주석이 지목한 묶음의 회전이 임차를 쥐는지 확인한다. -6. 그 묶음과 같은 순서로 49 회 회전시켜 배수 스레드를 센다. -7. 임차를 쥔 채 회전한 뒤 자원 닫기가 던지게 하고 닫는다. 두 번 닫아 스레드와 자원 닫기 호출 수를 본다. -8. 마감 작업이 인터럽트를 무시하고 도는 동안 닫아, 종료 대기가 실패할 때를 본다. 다시 닫아 회수되는지 본다. - -## 본문 - - - -레지스트리 클래스의 javadoc 이 스레드 수명을 못박는다. - -## 적어 둔 성질 - -:::evidence key="a11-f001-close" alt="레지스트리 클래스의 javadoc 이 적은 스레드 수명 서술, 닫기 메서드 전체를 줄 번호와 함께, 런타임 닫기가 상태를 먼저 바꾸는 네 줄, 배수 스레드를 만드는 조건과 그 스레드의 이름과 데몬 설정, 주석이 감시자로 지목한 묶음의 회전 루프와 마지막 단언, 그리고 같은 성질을 보는 단위 test 본문을 출력한 터미널 기록." caption="javadoc 은 어떤 스레드도 레지스트리보다 오래 살지 않는다고 적는다 · close() 는 143~145 에서 첫 실패를 다시 던지고 스케줄러 종료는 146 부터이며 그 안에도 던지는 줄이 있다 · 런타임 닫기는 자원 닫기 전에 상태를 CLOSED 로 바꾼다 · 감시자로 지목된 묶음의 회전은 임차를 쥐지 않는다 — 124줄 · exit 0" zoom="true" -::: - -```text - *

A swap publishes the replacement first and drains the predecessor afterwards, so a rotation is - * never observable as a gap. The single scheduled executor exists only to enforce drain deadlines - * and is created lazily; it is shut down with the registry so no thread outlives it. -``` - -## 던지기가 종료보다 앞에 있다 - -```text -141: retired.clear(); -142: runtimes.clear(); -143: if (firstFailure != null) { -144: throw firstFailure; -145: } -146: ScheduledExecutorService scheduler = drainScheduler.getAndSet(null); -147: if (scheduler != null) { -148: // Await termination: a registry that returns while its drain thread is still alive would -149: // leak a thread per rotation cycle, which the resource-bound suite exists to catch. -150: scheduler.shutdownNow(); -``` - -바로 위 루프는 하나가 거부해도 나머지를 닫도록 예외를 모으게 고쳐진 자리다. - -```text -126: // Every runtime is closed even when one refuses. forEach stopped at the first exception, so a -127: // single misbehaving pool left every remaining connection, thread and socket open — shutdown -128: // leaked more the worse the failure was. -``` - -같은 논리가 스케줄러에는 오지 않았다. - -## 회수되는 쪽 - -:::evidence key="a11-f001-close-leak" alt="주석이 지목한 묶음과 같은 모양으로 49 회 회전시켰을 때의 배수 스레드 최대치, 임차를 쥔 채 회전한 뒤 자원 닫기가 던지게 하고 두 번 닫았을 때의 결과와 거부한 런타임의 상태와 자원 닫기 호출 수, 그리고 마감 작업이 인터럽트를 무시하고 도는 동안 닫았을 때의 결과와 다시 닫아도 회수되지 않는 것을 출력한 터미널 기록. 픽스처는 고정 리비전 소스에서 직접 컴파일한다." caption="감시자와 같은 모양의 회전 49 회 동안 배수 스레드 0 · 강제 닫기가 던지면 스레드 1 이 남고 두 번째 닫기로 회수되지만 거부한 런타임은 CLOSED 로 굳는다 · 종료 대기가 5000 밀리초에 실패하면 두 번째 닫기로도 회수되지 않는다 — 20줄 · exit 0" zoom="true" -::: - -```text -[강제 닫기 하나가 던지는 경로] - swap 직후 drain 스레드 : 1 - 1회차 close() : IllegalStateException: pool refused to close / drain 스레드 1 - 거부한 런타임 state : CLOSED / 자원 닫기 호출 1회 - 다시 forceClose 후 자원 닫기 호출 : 1회 - 2회차 close() : 정상 반환 / drain 스레드 0 -``` - -두 번째 닫기가 스레드를 회수한다. 두 목록이 이미 비워져 있어 실패할 것이 없고, 그래서 146 에 닿는다. - -회수되지 않는 것이 그 줄 사이에 있다. 거부한 런타임의 상태가 `CLOSED` 인데 자원 닫기는 한 번뿐이고, 다시 불러도 늘지 않는다. - -```text -96: public final void close() { -97: ClientRuntimeState previous = state.getAndSet(ClientRuntimeState.CLOSED); -98: if (previous != ClientRuntimeState.CLOSED) { -99: resourceCloser.run(); -100: } -101: } -``` - -상태를 먼저 바꾼다. 자원 닫기가 던지면 그 런타임은 자원을 쥔 채 닫힘으로 굳고, 이후의 강제 닫기는 98 에서 되돌아간다. 126~128 이 막으려던 것이 하나가 거부해도 나머지가 새지 않는 것이었는데, 거부한 그 하나는 열린 채 남는다. - -## 회수되지 않는 쪽 - -```text -[강제 닫기가 하나도 던지지 않는데 스레드가 남는 경로] - 마감 작업이 도는 중 drain 스레드 : 1 - 강제 닫기가 던진 것 : 없음 - 1회차 close() : IllegalStateException: http client drain scheduler did not terminate (5000ms) - 1회차 후 drain 스레드 : 1 - 2회차 close() : 정상 반환 / drain 스레드 1 -``` - -종료 블록 자체가 던지는 경로다. - -```text -152: if (!scheduler.awaitTermination(SHUTDOWN_AWAIT_MILLIS, TimeUnit.MILLISECONDS)) { -153: throw new IllegalStateException("http client drain scheduler did not terminate"); -``` - -146 이 참조를 이미 비웠으므로 두 번째 닫기는 종료할 대상을 찾지 못한다. 여기 필요한 것은 강제 닫기 실패가 아니라, 마감 작업이 인터럽트를 넘기지 못하는 것뿐이다. `shutdownNow()` 가 보낼 수 있는 것은 인터럽트 하나다. - -## 감시자가 그 스레드를 본 적이 없다 - -148~149 의 주석이 자원 경계 묶음을 감시자로 지목한다. 그 묶음의 회전은 이렇게 생겼다. - -```text -31: for (int generation = 2; generation <= 50; generation++) { -32: ClientRuntime replacement = -33: new ClientRuntime( -34: ClientProfiles.builder("rotating").build(), -35: new RuntimeGeneration(generation), -36: closed::incrementAndGet); -37: registry.swap(first.name(), replacement, Duration.ofSeconds(1)); -``` - -임차를 쥐지 않는다. 그러면 `beginDrain` 이 그 자리에서 세대를 닫고, 스케줄러를 만드는 조건이 거짓이 된다. - -```text -99: previous.beginDrain(drainTimeout); -100: if (previous.state() != ClientRuntimeState.CLOSED && !drainTimeout.isZero()) { -``` - -그 묶음과 같은 순서로 돌려 보면 그렇다. - -```text -[자원 경계 묶음이 도는 모양] 임차를 쥐지 않고 회전한다 - 회전 49회 동안 관측된 drain 스레드 최대치 : 0 - 닫힌 세대 : 50 / close() 후 drain 스레드 : 0 -``` - -마지막 줄의 `noneMatch` 단언은 만들어진 적 없는 스레드를 찾는다. 실패 경로 이전에, 성공 경로에서도 빈 검사다. - -같은 성질을 보는 단위 test 도 실패 경로를 만들지 않는다. - -```java -ClientRuntimeRegistry registry = new ClientRuntimeRegistry(Map.of(first.name(), first)); -ClientRuntimeLease lease = registry.acquire(first.name()); -registry.swap(first.name(), second, Duration.ofSeconds(30)); -registry.close(); -``` - -이쪽은 임차를 쥐어 스케줄러가 만들어지지만, `running(1)` 과 `running(2)` 의 자원 닫기가 둘 다 빈 람다다. - -## 스레드는 데몬이다 - -```text -177: Thread thread = new Thread(runnable, "httpclient-runtime-drain"); -178: thread.setDaemon(true); -``` - -가상 머신 종료를 막지는 않는다. P3 을 유지하는 세 근거 중 하나다. - -## 수정의 모양 - -종료 블록을 `finally` 로 옮기려면 121~145 를 감싸는 `try` 를 먼저 만들어야 한다. 만들어도 그 블록 안에 153 이 있어서, `finally` 에서 나온 예외가 원래 던지던 예외를 밀어낸다. 호출자가 받는 것이 어느 풀이 닫기를 거부했는지에서 종료 대기 실패로 바뀐다. - -던지기를 종료 블록 뒤로 내리는 쪽이 더 작은 수정이지만 153 문제는 그대로다. 어느 쪽이든 스케줄러 실패를 원래 실패에 억제 예외로 붙여야 한다. - -## 확인하지 못한 것 - -회전을 여러 번 돌려 스레드가 누적되는지 재지 않았다. 스케줄러는 레지스트리당 하나이므로 한 레지스트리에서는 최대 하나다. - -큐에 남은 마감 작업이 은퇴 세대와 레지스트리를 얼마나 오래 붙잡는지 측정하지 않았다. 그 참조는 마감이 지나면 풀린다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/http-client-and-resilience/case/case-a11-f002-pool-route-exceeds-total.md b/docs/clean-architecture-backend-template/tech-log-studio/http-client-and-resilience/case/case-a11-f002-pool-route-exceeds-total.md deleted file mode 100644 index 4a11a22..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/http-client-and-resilience/case/case-a11-f002-pool-route-exceeds-total.md +++ /dev/null @@ -1,226 +0,0 @@ ---- -kind: CASE -slug: a11-f002-pool-route-exceeds-total -title: 설정 참고 문서가 약속한 코드를 운영자는 받지 못한다 -topic: http-client-and-resilience -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a11-f002-pool-route-exceeds-total -evidenceCapturedOn: 2026-09-02 -body: case-a11-f002-pool-route-exceeds-total.body.md -assets: - - key: a11-f002-pool-route-exceeds-total - file: ../../../final/evidence/rendered/a11-f002-pool-route-exceeds-total.svg - - key: a11-f002-pool-route-exceeds-total-shape - file: ../../../final/evidence/rendered/a11-f002-pool-route-exceeds-total-shape.svg -evidence: - - ../../../final/evidence/raw/a11-f002-pool-route-exceeds-total.txt - - ../../../final/evidence/raw/a11-f002-pool-route-exceeds-total-shape.txt -source: - - 원본 분석 절은 final/document.md#a11#L146 이다. 등급은 P3 이다. 레코드 정규 생성자와 검증기 분기가 같은 조건을 본다는 대조, 프로파일이 이미 구성된 풀 설정을 들고 있어 그 상태로 존재할 수 없다는 판정, 프로덕션의 생성 지점이 같은 생성자를 지난다는 확인, 이 코드의 test 참조가 0 이라는 사실이 그 절에 있다. 결정적 진단 형식을 이 한 조합만 받지 못한다는 지적도 원본의 것이다. - - 이 기록이 더한 것은 넷이다. 두 실패 모양을 프로덕션 판정으로 나란히 실행해, 정렬까지 포함한 약속이 지켜지는 쪽과 문장 하나만 오는 쪽을 보였다. 문법과 정책의 분업이 이 계층 전반의 방식이고 이 규칙만 양쪽에 적혀 있다는 것을 확인했다. 같은 목록의 `DUPLICATE_CLIENT_NAME` 이 같은 성질이라는 것을 찾았다. 그리고 검증기의 코드가 서른다섯 종이며 원본의 서른넷은 여러 줄에 걸친 호출 하나를 지나친 수라는 것을 확인했다. ---- - -# 설정 참고 문서가 약속한 코드를 운영자는 받지 못한다 - -풀 상한 규칙이 레코드 정규 생성자와 프로파일 검증기 양쪽에 적혀 있다. 앞의 검증에서 먼저 예외가 발생하므로 뒤의 위반 코드는 실행되지 않는다. 설정 참고 문서는 그 코드를 발화하는 코드들과 같은 줄에 적어 둔다. 같은 목록에 같은 성질의 코드가 하나 더 있다. - -## 관계 - -- **지워도 test가 초록인 검사가 셋이다** - 두 사례 모두 앞의 검증에서 먼저 예외가 발생해 뒤의 검증 코드에 도달하지 못한다. -- **위반 코드 34종 중 22종이 어떤 test에서도 이름으로 확인되지 않는다** - 이 코드의 test 참조가 0 인 이유가 그 표에 있다. 그 표의 34 는 한 줄 grep 이 만든 수다. -- **문서가 지목하는 조정 레코드를 쓰는 코드가 없다** - 두 사례 모두 문서가 약속한 것을 만드는 코드가 없다. - -## 문제 - -풀 설정 레코드의 정규 생성자가 경로당 상한이 전체 상한을 넘으면 던진다. - -프로파일 검증기가 같은 조건을 검사하고 위반 코드를 목록에 넣는다. - -두 검사가 겹치는지, 겹친다면 운영자가 받는 것이 무엇인지 확인했다. - -## 결론 - -겹치고, 뒤가 도달하지 않는다. - -프로파일은 이미 구성된 풀 설정을 들고 있다. 그 레코드는 경로당 상한이 전체를 넘는 상태로 존재할 수 없다. 정규 생성자를 지나지 않고 레코드를 만들 방법이 없으므로 생성 지점 수와 무관하게 그렇다. src/main 의 생성 지점이 부트스트랩 팩토리 한 군데이고 설정에서 읽은 값도 같은 생성자를 지난다는 것은 프로덕션 값도 예외가 아니라는 확인이다. - -여기까지는 이 계층의 일반적인 분업이다. 레코드가 막는 것은 문법이 틀린 값이고, 검증기가 잡는 것은 문법은 맞는데 정책이 금하는 값이다. 리다이렉트 홉 수와 재시도 백오프도 같은 식으로 나뉘어 있다. - -이 규칙만 양쪽에 적혀 있고, 실제로 걸리는 것은 앞쪽이다. - -그래서 운영자가 받는 것이 달라진다. 프로덕션에서 전송을 잘못 잡으면 코드와 프로파일 이름과 설정 경로를 담은 위반이 돌아오고, 여럿이면 정렬된 목록으로 온다. 풀 상한을 잘못 잡으면 코드도 이름도 경로도 없는 인자 예외 하나가 온다. 부트스트랩을 거치면 빈 생성 실패가 그 인자 예외를 감싼다. - -같은 목록에 같은 성질의 코드가 하나 더 있다. 이름 중복을 세는 자리는 프로파일 맵을 순회하는데, 맵의 키가 프로파일 이름이라 같은 이름이 두 번 들어갈 수 없다. 이름이 겹치면 설정 레코드가 먼저 그 이름을 두 번 선언했다고 던진다. 여기서도 코드가 비어 나간다. - -경로당 상한과 전체 상한이 같은 값이면 양쪽 다 통과한다. 검사가 초과만 보기 때문이다. - -주석이 적은 실제 위험은 막혀 있다. 어떤 반응형 전송에서는 경로당 손잡이가 유일하게 존재하는 것이라 그것이 조용히 실효 상한이 된다는 것인데, 그 조합은 만들어지지 않는다. - -검증기가 내는 코드는 서른다섯 종이다. 원본이 적은 서른넷은 한 줄 grep 이 만든 수다. 한 코드가 세 줄에 걸쳐 있어 그 검색에 잡히지 않는다. - -판정은 P3 다. 잘못된 설정은 어느 쪽이든 기동을 세운다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 레코드 생성자와 검증기 분기 대조, 생성 지점 전수 확인, 실행 탐침 -소스 수정 : x - -## 재현 조건 - -1. 풀 설정 레코드의 정규 생성자를 읽는다. -2. 프로파일 검증기의 같은 조건 분기를 읽는다. -3. 검증기 클래스 javadoc 이 약속하는 보고 형태를 읽는다. -4. 검증기가 내는 코드 수를 여러 줄에 걸친 호출까지 세어 확인한다. -5. src/main 전체에서 그 레코드를 만드는 곳과 값의 출처를 본다. -6. 이름 중복을 세는 자리가 무엇을 순회하는지, 그 앞에 무엇이 있는지 읽는다. -7. 두 코드의 이름을 참조하는 곳을 저장소 전체에서 센다. -8. 프로덕션 판정으로 정상 프로파일과 위반 하나짜리와 둘짜리를 검증기에 넣어 결과 모양을 본다. -9. 경로당 상한이 전체를 넘는 풀 설정을 만들어 보고, 같은 이름을 두 번 넣은 맵의 크기를 본다. - -## 본문 - - - -풀 상한을 잘못 잡는 실수를 두 곳이 본다. - -## 같은 규칙이 두 곳에 있다 - -:::evidence key="a11-f002-pool-route-exceeds-total" alt="풀 설정 레코드의 정규 생성자가 막는 두 조건, 프로파일 검증기가 같은 조건을 다시 보는 분기와 그 주석, 검증기 클래스 javadoc 이 약속하는 보고 형태, 검증기가 내는 코드 수를 여러 줄에 걸친 호출까지 세어 한 줄 검색과 대조한 결과와 그 걸리지 않는 호출, src/main 전체에서 그 레코드를 만드는 곳, 이름 중복을 세는 자리와 그 앞에서 먼저 던지는 설정 레코드, 그리고 두 코드의 이름을 참조하는 곳을 저장소 전체에서 확장자 제한 없이 센 터미널 기록." caption="레코드 생성자가 경로당 초과를 먼저 막고 검증기가 같은 조건을 다시 본다 · javadoc 은 코드마다 정렬된 위반을 약속한다 · 코드는 35종이고 한 줄 검색은 34종만 본다 · 이름 중복도 맵 앞에서 설정 레코드가 먼저 던진다 · 두 코드를 이름으로 참조하는 곳은 그 분기들과 설정 참고 문서뿐 — 54줄 · exit 0" zoom="true" -::: - -```java -if (maxConnectionsPerRoute > maxTotalConnections) { - throw new IllegalArgumentException("per-route pool must not exceed the total pool"); -} -``` - -레코드의 정규 생성자다. 검증기에는 같은 조건이 이렇게 있다. - -```java -if (profile.pool().maxConnectionsPerRoute() > profile.pool().maxTotalConnections()) { - // A per-route ceiling above the total is incoherent, and on Reactor — where the per-route - // knob is the only one that exists — it silently becomes the effective limit. - out.add(violation("POOL_ROUTE_EXCEEDS_TOTAL", profile, "pool.max-connections-per-route")); -} -``` - -프로파일은 이미 만들어진 풀 설정을 들고 있고, 레코드는 정규 생성자를 지나지 않고 만들 수 없다. 이 조건이 참인 프로파일은 존재하지 않는다. - -```text - bootstrap/autoconfigure/httpclient/HttpClientProfileFactory.java:83: return new PoolSettings( -``` - -`src/main` 의 생성 지점은 여기 하나이고, 설정에서 읽은 값을 같은 생성자에 넘긴다. 프로덕션 값도 예외가 아니라는 확인이다. - -## 문법과 정책은 원래 나뉘어 있다 - -문법이 틀린 값은 레코드가 막고, 문법은 맞는데 정책이 금하는 값은 검증기가 잡는다. 음수 홉 수는 리다이렉트 설정 레코드가 던지고, 홉 수 0 에 리다이렉트를 켠 조합은 검증기가 코드로 보고한다. 재시도 백오프도 같은 식이다. - -풀 상한 규칙만 양쪽에 적혀 있다. 그리고 걸리는 쪽은 앞이다. - -## 그래서 운영자가 무엇을 받는가 - -:::evidence key="a11-f002-pool-route-exceeds-total-shape" alt="프로덕션 판정으로 정상 프로파일과, 경로당 상한이 전체와 같은 프로파일과, 위반이 하나인 프로파일과 둘인 프로파일을 각각 검증기에 넣어 돌아온 위반 목록. 경로당 상한이 전체를 넘는 풀 설정을 만들어 봤을 때의 결과. 그리고 같은 이름을 두 번 넣은 맵의 크기를 출력한 터미널 기록. 픽스처는 고정 리비전 소스에서 직접 컴파일한다." caption="전송 오설정은 코드와 프로파일 이름과 설정 경로를 담은 위반으로 돌아오고 둘이면 정렬되어 온다 · 경로당과 전체가 같으면 통과 · 경로당이 전체를 넘는 풀 설정은 인자 예외로 끝난다 · 같은 이름을 두 번 넣은 맵의 크기는 1 — 20줄 · exit 0" zoom="true" -::: - -검증기의 클래스 javadoc 이 보고 형태를 약속한다. - -```text - *

Every guard in the design has exactly one stable violation code here. The result is sorted so - * a configuration error reports deterministically across runs and machines. -``` - -프로덕션 판정에서 전송을 잘못 잡으면 그 약속대로 온다. - -```text - 전송만 SIMPLE 로 바꾼 프로파일 - ClientProfileViolation[code=PRODUCTION_SIMPLE_FACTORY_FORBIDDEN, detail=profile=payment setting=transport] - 전송 JDK + 경로 풀 요구 + HTTP/3 미승인 - ClientProfileViolation[code=HTTP3_STABLE_FORBIDDEN, detail=profile=payment setting=protocols] - ClientProfileViolation[code=JDK_FINE_GRAINED_POOL_UNSUPPORTED, detail=profile=payment setting=transport] -``` - -둘이면 코드 순으로 정렬되어 온다. 풀 상한은 다르다. - -```text - 전체 10 / 경로당 20 -> IllegalArgumentException: per-route pool must not exceed the total pool -``` - -코드도, 프로파일 이름도, 설정 경로도 없다. 부트스트랩에서는 이것이 빈 생성 실패에 감싸여 나온다. - -경계값은 양쪽 다 통과한다. - -```text - 전체 20 / 경로당 20 (같음) - 위반 없음 -``` - -검사가 보는 것이 초과뿐이라 같은 값은 걸리지 않는다. - -## 같은 목록의 다른 코드 - -```text - profiles.forEach( - (name, profile) -> { - if (!seenNames.add(name.value())) { - violations.add("DUPLICATE_CLIENT_NAME profile=" + name.value()); - } -``` - -순회 대상이 프로파일 맵이고 키가 프로파일 이름이다. 같은 이름이 두 번 들어갈 수 없다. - -```text - 같은 이름을 두 번 넣은 맵의 크기 : 1 -``` - -이름이 겹치는 설정은 그 앞에서 이미 죽는다. - -```text - if (seen.contains(name)) { - throw new IllegalStateException(where + " declares '" + name + "' more than once"); - } -``` - -여기도 코드가 붙지 않는다. - -## 문서는 두 코드를 다른 코드와 같이 적어 둔다 - -```text -docs/httpclient/configuration-reference.md:191:`RETRY_BACKOFF_REQUIRED`, `MISSING_PRODUCTION_SETTING`, `DUPLICATE_CLIENT_NAME`, -docs/httpclient/configuration-reference.md:194:`HTTP2_REQUIRED_TRANSPORT_UNSUPPORTED`, `POOL_ROUTE_EXCEEDS_TOTAL`, -docs/httpclient/configuration-reference.md:209:- `POOL_ROUTE_EXCEEDS_TOTAL` — a per-route ceiling above the total is incoherent, and on Reactor, -``` - -두 코드가 저장소에 나오는 곳은 만드는 분기 각각 한 줄과 이 세 줄뿐이다. - -## 코드는 몇 개인가 - -```text - violation( 호출 40개, 서로 다른 코드 35개 - 한 줄 grep 이 보는 코드 34개, 놓치는 것 ['PROXY_AMBIENT_NO_PROXY_UNSUPPORTED'] -``` - -한 코드가 세 줄에 걸쳐 있다. - -```text - if (profile.proxy().importAmbientNoProxy()) { - out.add( - violation( - "PROXY_AMBIENT_NO_PROXY_UNSUPPORTED", profile, "proxy.import-ambient-no-proxy")); -``` - -원본이 적은 서른넷은 이 호출을 지나친 수다. - -## 확인하지 못한 것 - -두 위반 코드가 과거에 도달 가능했던 시점이 있었는지 이력에서 확인하지 않았다. - -부트스트랩을 실제로 띄워 잘못된 풀 설정이 어떤 예외로 감싸여 보고되는지 관측하지 않았다. 확인한 것은 레코드 생성자가 던지는 예외 자체까지다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/http-client-and-resilience/case/case-a11-f004-number.md b/docs/clean-architecture-backend-template/tech-log-studio/http-client-and-resilience/case/case-a11-f004-number.md deleted file mode 100644 index a2b00dc..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/http-client-and-resilience/case/case-a11-f004-number.md +++ /dev/null @@ -1,214 +0,0 @@ ---- -kind: CASE -slug: a11-f004-number -title: 재생 가능으로 인증된 본문이 다른 바이트를 낸다 -topic: http-client-and-resilience -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a11-f004-number -evidenceCapturedOn: 2026-09-02 -body: case-a11-f004-number.body.md -assets: - - key: a11-f004-number - file: ../../../final/evidence/rendered/a11-f004-number.svg - - key: a11-f004-number-bytes - file: ../../../final/evidence/rendered/a11-f004-number-bytes.svg -evidence: - - ../../../final/evidence/raw/a11-f004-number.txt - - ../../../final/evidence/raw/a11-f004-number-bytes.txt -source: - - 원본 분석 절은 final/document.md#a11#L249 이다. 등급은 P3 이다. 첫 분기가 `Number` 를 무조건 통과시킨다는 지적, 원자 패키지 여섯 타입이 그것을 상속하는 가변 타입이라는 열거, 그래서 성분에 원자 정수를 가진 레코드가 재생 가능으로 인증되고 호출자가 값을 올리면 같은 멱등 키로 다른 바이트가 나간다는 판정, 시간 상위 타입에도 같은 구멍이 있다는 관찰, 자바독 선언과 코드 범위가 어긋난다는 이유가 그 절에 있다. 수정 방향 둘 — 상자 원시형 여덟 종과 큰 정수·큰 십진수를 명시하거나 원자 패키지를 제외하는 것 — 도 그 절이 제시한 것이다. - - 이 기록이 더한 것은 셋이다. 여섯 타입의 판정과, 요청 경로가 실제로 쓰는 변환기로 인코딩한 결과를 받아, 판정이 재생 가능인 채 바이트가 달라지는 것을 그대로 보였다. 선언에 없는 분기가 셋이고 그중 실제 구멍은 하나라는 것, 그리고 상자 원시형과 원자 계열 사이에 남는 두 타입이 자기를 바꾸지 않는다는 것을 확인했다. 그리고 그 test 넷의 입력을 전수 확인해 숫자는 상자 원시형만 들어간다는 것을 보였다 — 원본은 test 넷의 이름과 일반적 가변 객체를 잡는다는 한계까지만 적는다. ---- - -# 재생 가능으로 인증된 본문이 다른 바이트를 낸다 - -깊은 불변성 검사의 첫 분기가 숫자 상위 타입을 무조건 통과시킨다. 원자 계열 여섯 타입이 그 상위 타입을 상속하는 가변 타입이다. 성분에 그런 카운터를 둔 요청을 요청 경로의 실제 변환기로 두 번 인코딩해, 인증은 유지되고 결과만 갈리는 것을 받았다. - -## 관계 - -- **재시도 안전성은 증거에 기반해 판정한다** - 이 검사가 존재하는 이유다. -- **지워도 test가 초록인 검사가 셋이다** - 저쪽은 실행되지 않는 분기이고, 이쪽은 실행되지만 선언보다 넓은 분기다. -- **타입이 문서화한 불변식은 타입이 강제한다** - 자바독이 선언한 범위를 코드가 지키지 않는다. - -## 문제 - -요청 본문이 재시도에도 같은 바이트를 내는지 판정하는 검사가 있다. - -그 검사의 자바독이 범위를 선언한다. 레코드와 열거형과 문자열과 상자 원시형과 불변 컬렉션 뷰는 재생 가능하고, 그 밖의 모든 것은 일회성으로 취급한다는 것이다. 값을 확실히 하려는 호출자에게는 바이트 배열 본문으로 굳히는 길을 안내한다. - -## 결론 - -첫 분기가 그 선언보다 넓다. - -선언에 없는 것이 셋 들어 있다. 숫자 상위 타입과 UUID 와 시간 상위 타입이다. UUID 는 불변이고 시간 쪽은 표준 구현체가 전부 불변이라, 남는 것은 숫자 상위 타입 한 줄이다. - -상자 원시형과 원자 계열 사이에 남는 표준 타입은 큰 정수와 큰 십진수 둘뿐이고, 둘 다 자기 값을 바꾸지 않는다. 더하기를 불러도 원래 객체가 그대로인 것을 확인했다. 그래서 표준 라이브러리 안의 실제 구멍은 원자 계열 여섯이다. - -원자 정수와 원자 긴 정수와 긴 덧셈기와 실수 덧셈기와 긴 누산기와 실수 누산기다. 여섯 전부 재생 가능으로 인증되는 것을 실행으로 확인했다. - -성분에 원자 정수를 둔 레코드도 마찬가지다. 요청 경로가 실제로 쓰는 변환기로 그 본문을 두 번 인코딩하면 첫 번째는 attempt 가 1 이고 두 번째는 2 인데, 사이에서 판정은 재생 가능 그대로다. 재시도가 같은 멱등 키로 다른 바이트를 보낸다는 뜻이다. - -그것이 이 검사가 존재하는 이유로 인용된 결과다. 검사를 고정하는 test 클래스의 자바독이 바로 그 문장을 적어 뒀다. - -test 넷이 보는 갈래는 따로 있다. 불변 레코드, 호출자가 쥔 컬렉션, 그것을 감싼 레코드, 들여다볼 수 없는 빈이다. 숫자는 상자 원시형만 들어간다. - -같은 형태의 좁은 구멍이 시간 상위 타입에도 있다. 사용자 정의 구현은 불변이 아닐 수 있다. 다만 숫자 쪽이 훨씬 현실적이다. 요청 객체에 카운터를 두는 것은 드물지 않다. - -판정은 P3 다. 도달하려면 원자 카운터를 요청 객체에 넣어야 하고, 검사 전체의 방향은 보수적이다. 호출자가 쥔 컬렉션은 그것을 감싼 레코드까지 일회성으로 떨어진다. - -자바독은 이 검사의 계약이고, 계약과 구현이 어긋난 쪽은 구현이다. - -수정은 둘 중 하나다. 상자 원시형 여덟 종과 큰 정수와 큰 십진수를 명시하거나, 원자 패키지를 제외하는 것이다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 검사 분기와 타입 계층 대조, 실행 탐침 -소스 수정 : x - -## 재현 조건 - -1. 깊은 불변성 검사의 자바독 선언을 읽는다. -2. 첫 분기의 허용 타입 목록을 확인하고 선언에 없는 것을 고른다. -3. 검사를 고정하는 test 넷이 무엇을 보는지 확인한다. -4. 기본 설정의 REST 클라이언트가 JSON 본문을 어느 변환기로 쓰는지 확인한다. -5. 큰 정수와 큰 십진수가 자기 값을 바꾸는지, 원자 정수는 바꾸는지 대조한다. -6. 원자 계열 여섯 타입을 각각 본문 값으로 넣어 판정을 받는다. -7. 성분에 원자 정수를 둔 레코드의 판정을 받고, 그 변환기로 인코딩한 뒤 카운터를 올려 다시 인코딩해 바이트를 비교한다. -8. 호출자가 쥔 컬렉션과 그것을 감싼 레코드의 판정을 대조한다. - -## 본문 - - - -이 모듈은 본문을 인코딩하기 전에, 그 본문이 재시도에도 같은 바이트를 낼지 먼저 판정한다. - -## 선언과 분기 - -:::evidence key="a11-f004-number" alt="본문 재생 가능성 검사의 자바독이 선언하는 범위, 깊은 불변성 판정의 첫 분기 전체, 그 검사를 고정하는 test 클래스의 javadoc, 그리고 그 test 넷의 이름과 표시 이름을 출력한 터미널 기록." caption="자바독은 레코드·열거형·문자열·상자 원시형·불변 컬렉션 뷰만 재생한다고 적고, 확실한 호출자에게는 값을 바이트 배열로 굳히라고 덧붙인다 · 첫 분기는 Number 를 무조건 통과시킨다 · test 클래스 javadoc 이 다른 바이트에 같은 멱등 키를 이 검사의 존재 이유로 적는다 — 41줄 · exit 0" zoom="true" -::: - -```text - *

The check is structural and conservative: records, enums, strings, boxed primitives and - * immutable collection views replay; anything else is treated as one-shot, so the retry engine - * refuses rather than gambling. A caller who knows better can freeze the value itself — serialize - * it to a {@code byte[]} body — which states the guarantee instead of asserting it. -``` - -확실한 호출자에게는 값을 바이트 배열 본문으로 굳히라고 적어 뒀다. 아래는 그 권고를 따르지 않은 값에 대한 이야기다. - -판정의 첫 분기는 이렇다. - -```java -if (candidate instanceof String - || candidate instanceof Number - || candidate instanceof Boolean - || candidate instanceof Character - || candidate instanceof Enum - || candidate instanceof java.util.UUID - || candidate instanceof java.time.temporal.Temporal) { - return true; -} -``` - -선언에 없는 분기가 셋이다. `Number` 와 `UUID` 와 `Temporal`. `UUID` 는 불변이고 `Temporal` 은 표준 구현체가 전부 불변이라, 남는 것은 `Number` 한 줄이다. 상자 원시형만이 아니라 `Number` 를 상속하는 모든 타입이 들어온다. - -## 넣어 보면 - -:::evidence key="a11-f004-number-bytes" alt="기본 설정의 REST 클라이언트가 JSON 본문을 쓸 때 고르는 변환기, 원자 계열 여섯 타입을 각각 본문 값으로 넣어 받은 판정, 큰 정수와 큰 십진수가 더하기 뒤에도 자기 값을 유지하는 것과 원자 정수가 바뀌는 것의 대조, 성분에 카운터를 둔 레코드의 판정과 그 변환기로 두 번 인코딩한 결과와 바이트 동일성 비교, 그리고 호출자가 쥔 컬렉션과 그것을 감싼 레코드의 판정을 출력한 터미널 기록." caption="요청 경로의 JSON 변환기는 Spring 의 JacksonJsonHttpMessageConverter · 원자 계열 여섯 타입이 전부 REPLAYABLE · 큰 정수와 큰 십진수는 더하기 뒤에도 자기 값 그대로 · 카운터를 둔 레코드는 판정이 그대로인 채 인코딩이 attempt 1 에서 2 로 바뀐다 — 35줄 · exit 0" zoom="true" -::: - -```text -[java.util.concurrent.atomic 의 Number 하위 타입] - AtomicInteger Number 상속 true 판정 REPLAYABLE - AtomicLong Number 상속 true 판정 REPLAYABLE - LongAdder Number 상속 true 판정 REPLAYABLE - DoubleAdder Number 상속 true 판정 REPLAYABLE - LongAccumulator Number 상속 true 판정 REPLAYABLE - DoubleAccumulator Number 상속 true 판정 REPLAYABLE -``` - -여섯 다 값을 바꿀 수 있는 타입이고, 여섯 다 통과한다. `Number` 를 상속했다는 것 말고 공통점이 없다. - -상자 원시형과 이 여섯 사이에 남는 표준 타입은 둘뿐이다. - -```text -[상자 원시형과 원자 계열 사이에 남는 둘] - BigInteger add 뒤 자기 값 7 판정 REPLAYABLE - BigDecimal add 뒤 자기 값 7.5 판정 REPLAYABLE - AtomicInteger increment 뒤 자기 값 8 판정 REPLAYABLE -``` - -둘은 더하기를 불러도 자기 값이 그대로다. 원자 정수는 7 이 8 이 된다. 같은 판정을 받는 세 타입 중 마지막 하나만 자기를 바꾼다. - -## 판정은 그대로, 바이트는 다르다 - -요청 경로가 JSON 본문을 쓸 때 고르는 변환기부터 확인했다. - -```text -[요청 본문을 인코딩하는 변환기] RestClient 기본 목록 - org.springframework.http.converter.json.JacksonJsonHttpMessageConverter -``` - -그 변환기로 두 번 인코딩한 결과다. - -```text -[성분에 카운터를 둔 record] - 판정 : REPLAYABLE - 첫 인코딩 : {"id":"A-1","attempt":1} - 다시 인코딩 : {"id":"A-1","attempt":2} - 판정(그대로) : REPLAYABLE - 같은 바이트인가 : false -``` - -사이에 한 것은 카운터를 올린 것뿐이다. 재시도 엔진이 읽는 값은 두 번 다 재생 가능이고, 같은 값을 다시 인코딩한 결과는 다르다. - -## 이 검사가 존재하는 이유 - -```text - *

Every {@code ObjectBody} used to report {@code REPLAYABLE}. A caller who reused a builder or - * kept a reference to a list therefore got a retry that re-encoded the value as it was at retry - * time — different bytes, same idempotency key, which is precisely what a replay must never - * be. -``` - -이 문장은 test 클래스 javadoc 에 있다. - -그 test 넷은 다른 갈래를 본다. - -```text - 28: @DisplayName("a record of immutable components replays") - 29: void anImmutableRecordReplays() { - 41: @DisplayName("a value the caller can still mutate does not replay") - 42: void aMutableValueIsOneShot() { - 51: @DisplayName("a record wrapping a mutable component does not replay") - 52: void aRecordWrappingMutableStateIsOneShot() { - 66: @DisplayName("an uninspectable value is one-shot") - 67: void anArbitraryBeanIsOneShot() { -``` - -입력에는 컬렉션과 맵과 빈이 들어 있고, 숫자는 상자 원시형뿐이다. `Map.of("k", 1)` 의 1 과 `ImmutableOrder` 의 int 가 그것이다. `Number` 분기 자체는 밟히지만 가변 숫자 타입은 한 번도 들어가지 않는다. - -## 검사가 잡는 쪽 - -```text -[검사가 잡는 쪽] - ArrayList 자체 판정 ONE_SHOT - ArrayList 를 감싼 record 판정 ONE_SHOT - List.of 를 감싼 record 판정 REPLAYABLE -``` - -컬렉션 쪽은 이름으로 불변 뷰인지 확인하고 감싼 레코드까지 따라간다. 원자 카운터를 요청 객체에 넣지 않는 한 이 구멍에 닿지 않는다. - -## 확인하지 못한 것 - -재시도 엔진을 실제로 돌려 두 번째 시도가 그 바이트를 보내는지 관측하지 않았다. 확인한 것은 판정과 인코딩까지다. - -사용자 정의 시간 구현으로 같은 구멍을 재현하지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/http-client-and-resilience/case/case-a11-f006-boundeddatabufferflux.md b/docs/clean-architecture-backend-template/tech-log-studio/http-client-and-resilience/case/case-a11-f006-boundeddatabufferflux.md deleted file mode 100644 index 9c95679..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/http-client-and-resilience/case/case-a11-f006-boundeddatabufferflux.md +++ /dev/null @@ -1,216 +0,0 @@ ---- -kind: CASE -slug: a11-f006-boundeddatabufferflux -title: 부르는 쪽이 이미 걸어 둔 것을 한 번 더 건다 -topic: http-client-and-resilience -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a11-f006-boundeddatabufferflux -evidenceCapturedOn: 2026-09-02 -body: case-a11-f006-boundeddatabufferflux.body.md -assets: - - key: a11-f006-boundeddatabufferflux - file: ../../../final/evidence/rendered/a11-f006-boundeddatabufferflux.svg - - key: a11-f006-boundeddatabufferflux-release - file: ../../../final/evidence/rendered/a11-f006-boundeddatabufferflux-release.svg -evidence: - - ../../../final/evidence/raw/a11-f006-boundeddatabufferflux.txt - - ../../../final/evidence/raw/a11-f006-boundeddatabufferflux-release.txt -source: - - 원본 분석 절은 final/document.md#a11#L424 이다. 등급은 P3 이다. 자바독이 취소와 오류를 실제로 새는 경로로 적는다는 인용, 두 연산자가 각각 정의상 항등이라는 판정, 그래서 버퍼 해제가 전적으로 폐기 연산자와 드라이버의 해제 동작에 의존한다는 결론, 누수가 실재한다고 주장하지 않는다는 단서, 취소 경로 test 가 확인하는 것은 연결 반환이라는 지적, 실질적 안전망이 모든 레인에 켜진 누수 탐지기라는 지적, 그리고 수정 방향 둘이 그 절에 있다. - - 이 기록이 더한 것은 셋이다. 이 사슬을 부르는 유일한 지점이 바로 뒤에서 같은 폐기 연산자를 한 번 더 걸고, 폐기 처리기가 상류로 전파되므로 호출 지점에서는 사슬 자신의 것까지 잉여라는 것을 실행으로 보였다. 오류 경로에서는 있는 그대로의 사슬도 상류가 쥔 버퍼를 돌려주지 않는다는 것 — 34~35 주석의 서술과 다르다. 그리고 폐기 연산자가 도는 것은 상류가 규약을 지킬 때뿐이라는 것이다. ---- - -# 부르는 쪽이 이미 걸어 둔 것을 한 번 더 건다 - -클래스 자바독이 취소와 오류를 실제로 새는 경로로 지목한다. 그 두 이름을 딴 연산자는 신호에 대해 항등이다. 남은 폐기 연산자도 이 사슬을 부르는 유일한 지점이 바로 뒤에서 한 번 더 걸어 둔 것이라, 호출 지점에서는 셋 다 빼도 결과가 같다. - -## 관계 - -- **타입이 문서화한 불변식은 타입이 강제한다** - 자바독이 지목한 경로를 그 이름의 연산자가 지키지 않는다. -- **누수 하나는 회수되고 하나는 회수되지 않는다** - 같은 리프의 자원 해제 사례다. -- **그 속성을 보일 수 없는 대역 위에서 통과한 테스트는 증인이 아니다** - 취소 경로 test 본문이 확인하는 것은 버퍼 해제가 아니다. - -## 문제 - -반응형 본문을 제한하고 넘기지 않은 버퍼를 해제하는 클래스가 있다. - -클래스 자바독이 목적을 적는다. 취소와 오류가 실제로 새는 경로라는 것이다. 구독자가 요구를 멈추고, 상류가 이미 만든 것을 버리고, 그 버퍼는 아무도 반환하지 않는 직접 메모리라는 것이다. - -그 두 경로에 붙은 연산자가 실제로 무엇을 하는지, 그리고 이 사슬이 불리는 자리에서는 어떻게 되는지 확인했다. - -## 결론 - -취소 연산자의 본문은 비어 있고, 오류 대체 연산자는 받은 오류를 그대로 다시 방출한다. 둘 다 신호에 대해 항등이다. 사슬의 모양은 바꾼다. 두 연산자가 붙으면 반환 타입이 달라지고 융합이 끊긴다. - -버퍼 다섯 개 중 하나만 받고 취소하면 있는 그대로에서 미회수가 0 이고, 두 연산자를 빼도 0 이다. 폐기 연산자까지 빼면 넷이 남는다. 이 사슬만 구독했을 때는 폐기 연산자가 일한다. - -그런데 이 사슬을 부르는 곳은 하나이고, 그 자리가 바로 뒤에서 같은 폐기 연산자를 한 번 더 건다. 같은 조건에서 사슬 자신의 폐기 연산자를 빼도 미회수가 0 이다. 폐기 처리기는 문맥으로 상류에 전파되므로 바깥의 것이 안쪽까지 덮는다. - -오류 경로는 다르다. 상류가 셋을 쥔 채 던지면 있는 그대로에서 셋이 남고, 폐기 연산자를 빼도 셋이다. 자바독이 지목한 두 경로 중 오류 쪽에서 이 사슬이 하는 일은 없다. - -오류 대체 연산자 안의 주석은 흐르던 것이 위의 폐기 연산자로 처리된다고 적는데, 오류 신호에서 그 폐기는 일어나지 않는다. - -폐기 연산자가 도는 조건도 좁다. 상류가 폐기 규약을 지킬 때만이다. 규약을 지키지 않는 상류로 바꾸면 폐기 연산자가 있으나 없으나 넷이 남는다. - -실제 누수가 있다는 주장은 아니다. 취소 시 미방출 버퍼는 드라이버의 바이트 흐름이 스스로 돌려준다. - -문제는 코드가 하지 않는 일을 하는 것처럼 읽힌다는 것이다. 자바독이 지목한 두 누수 경로의 이름을 딴 연산자가 나란히 있고 둘 다 비어 있으므로, 이 클래스를 읽는 사람은 취소와 오류 해제가 여기서 명시적으로 처리된다고 결론짓게 된다. - -취소 경로 test 본문이 단언하는 것은 다음 호출이 성공한다는 것, 즉 연결 반환이다. 다만 그 test 클래스에는 누수 탐지기가 최고 수준으로 살아 있다는 것과 누수 보고가 하나도 없다는 것을 단언하는 확장이 붙어 있다. - -판정은 P3 다. - -수정은 둘 중 하나다. 세 연산자를 지우고 자바독이 부르는 쪽의 폐기 연산자와 드라이버의 역할을 정확히 적게 하거나, 취소와 오류 경로에서 실제로 해제해야 할 것이 있다면 그것을 구현하는 것이다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 연산자 본문 확인과 자바독 대조, 호출 지점 추적, 실행 탐침 -소스 수정 : x - -## 재현 조건 - -1. 클래스 자바독의 누수 경로 서술을 읽는다. -2. 조립된 연산자 사슬을 순서대로 읽는다. -3. 이 사슬을 부르는 곳을 세고, 그 호출 뒤에 무엇이 이어지는지 읽는다. -4. 취소 경로 test 본문과 그 클래스에 붙은 확장이 무엇을 단언하는지 확인한다. -5. 참조 계수를 볼 수 있는 버퍼 다섯 개를 만들어, 사슬만 구독하고 하나만 받은 뒤 취소한다. -6. 같은 조건을 호출 지점의 조립 모양으로 바꿔 반복한다. -7. 상류가 셋을 쥔 채 던지는 오류 경로로 반복한다. -8. 폐기 규약을 지키지 않는 상류로 반복한다. - -## 본문 - - - -클래스 자바독이 두 경로를 이름으로 지목한다. - -## 지목된 두 경로 - -:::evidence key="a11-f006-boundeddatabufferflux" alt="클래스 자바독이 지목한 두 누수 경로, 조립되는 연산자 사슬 전체를 줄 번호와 함께, 이 사슬을 부르는 유일한 지점과 그 호출을 감싼 표현식과 바로 뒤에 이어지는 연산자, 취소 경로 test 두 요청 전체와 그 클래스에 붙은 확장, 그리고 그 확장이 단언하는 두 가지를 출력한 터미널 기록." caption="자바독은 취소와 오류를 실제로 새는 경로로 지목한다 · 사슬은 doOnNext · doOnDiscard · 비어 있는 doOnCancel · 오류를 그대로 재방출하는 onErrorResume 순 · 부르는 곳은 한 군데이고 그 자리가 105 에서 같은 doOnDiscard 를 한 번 더 건다 · test 클래스에는 누수 탐지기를 단언하는 확장이 붙어 있다 — 104줄 · exit 0" zoom="true" -::: - -```text - *

Cancellation and error are the paths that leak in practice: the subscriber stops asking, the - * upstream drops what it already produced, and those buffers are direct memory nobody returns. -``` - -사슬에 그 두 이름을 딴 연산자가 나란히 있다. - -```java -24: return source -25: .doOnNext( -26: buffer -> { -27: limiter.recordWireBytes(buffer.readableByteCount()); -28: guard.markDelivered(); -29: }) -30: .doOnDiscard(DataBuffer.class, DataBufferUtils::release) -31: .doOnCancel(() -> {}) -32: .onErrorResume( -33: failure -> { -34: // Buffers already emitted belong to the subscriber; anything still in flight is -35: // discarded through doOnDiscard above. -36: return Flux.error(failure); -37: }); -``` - -31 은 본문이 비어 있어 정의상 항등이다. 32~37 은 받은 오류를 그대로 다시 방출하므로 오류 신호에 대해 항등이다. - -## 사슬만 구독하면 - -:::evidence key="a11-f006-boundeddatabufferflux-release" alt="참조 계수를 볼 수 있는 버퍼 다섯 개를 만들어 네 가지 조건으로 돌린 결과. 사슬만 구독해 하나만 받고 취소했을 때, 호출 지점의 조립 모양으로 같은 것을 했을 때, 상류가 셋을 쥔 채 던졌을 때, 그리고 상류가 폐기 규약을 지키지 않을 때다. 각 조건에서 있는 그대로와 연산자를 뺀 사슬을 나란히 세었다." caption="사슬만 구독하면 두 연산자를 빼도 미회수 0 이고 폐기 연산자까지 빼면 넷 · 호출 지점 모양에서는 폐기 연산자를 빼도 0 · 오류 경로는 있는 그대로에서도 셋이 남고 규약을 안 지키는 상류에서는 넷이 남는다 — 17줄 · exit 0" zoom="true" -::: - -```text -[취소] bound 만 구독한다 — 다섯 중 하나만 받고 취소 - 있는 그대로 전달 1, 상류가 쥐고 있던 것 중 미회수 0 / 4 - 두 연산자 제거 전달 1, 상류가 쥐고 있던 것 중 미회수 0 / 4 - doOnDiscard 까지 제거 전달 1, 상류가 쥐고 있던 것 중 미회수 4 / 4 -``` - -31 과 32~37 을 빼도 결과가 같고, 30 을 빼면 달라진다. 여기까지는 30 이 일한다. - -## 부르는 자리에서는 다르다 - -이 사슬을 부르는 곳은 하나다. - -```java - 87: return request - 88: .exchangeToFlux( - 89: response -> { -... -102: return BoundedDataBufferFlux.bound( -103: response.bodyToFlux(DataBuffer.class), limiter, guard); -104: }) -105: .doOnDiscard(DataBuffer.class, DataBufferUtils::release); -``` - -105 가 같은 폐기 연산자를 한 번 더 건다. 폐기 처리기는 구독자 문맥에 쓰여 상류로 전파되므로, 바깥의 것이 `bound` 안쪽까지 덮는다. - -```text -[취소] 부르는 쪽이 105 에서 같은 폐기 연산자를 한 번 더 건다 - 있는 그대로 전달 1, 상류가 쥐고 있던 것 중 미회수 0 / 4 - doOnDiscard 까지 제거 전달 1, 상류가 쥐고 있던 것 중 미회수 0 / 4 -``` - -30 을 빼도 0 이다. 실제로 불리는 모양에서는 `doOnNext` 뒤의 세 연산자가 모두 잉여다. - -## 오류 경로에서는 아무것도 하지 않는다 - -```text -[오류] 상류가 셋을 쥔 채 던진다 - 있는 그대로 전달 2, 상류가 쥐고 있던 것 중 미회수 3 / 3 - doOnDiscard 까지 제거 전달 2, 상류가 쥐고 있던 것 중 미회수 3 / 3 -``` - -34~35 의 주석은 흐르던 것이 위의 폐기 연산자로 처리된다고 적는다. 오류 신호에서는 그 폐기가 일어나지 않는다. - -## 폐기 연산자가 도는 조건 - -```text -[취소] 상류가 폐기 규약을 지키지 않는다 - 있는 그대로 전달 1, 상류가 쥐고 있던 것 중 미회수 4 / 4 - doOnDiscard 까지 제거 전달 1, 상류가 쥐고 있던 것 중 미회수 4 / 4 -``` - -취소에서 상류가 쥔 것을 돌려주는 것은 상류가 폐기 규약을 지킬 때뿐이다. - -## 취소 경로 test 가 보는 것 - -```java -StepVerifier.create( - gateway - .download( - profile.name(), - HttpOperation.get(new OperationName("download"), "/small", Map.of())) - .doOnNext(DataBufferUtils::release)) - .expectNextCount(1) - .verifyComplete(); -``` - -앞의 요청이 하나만 받고 취소한 뒤, 두 번째 요청이 성공하는지 본다. 단일 연결 풀이라 그 성공이 연결 반환의 증거다. 버퍼는 test 가 직접 해제한다. - -버퍼 해제를 이 본문이 단언하지는 않는다. 대신 클래스에 확장이 붙어 있다. - -```text - assertThat(ResourceLeakDetector.getLevel()) -``` - -그 확장이 탐지기가 최고 수준으로 살아 있다는 것과, 실행 뒤 누수 보고가 하나도 없다는 것을 단언한다. - -## 남는 것 - -누수가 실재한다고 주장하지 않는다. 취소 시 미방출 버퍼는 드라이버의 바이트 흐름이 스스로 돌려주고, 누수 탐지기가 모든 레인에서 확인한다. - -문제는 읽는 방식이다. 자바독이 두 누수 경로를 이름으로 지목하고, 그 두 이름을 딴 연산자가 사슬에 나란히 있고, 둘 다 비어 있다. 세 번째는 부르는 쪽이 이미 걸어 둔 것과 같다. - -## 확인하지 못한 것 - -실제 드라이버의 바이트 흐름 위에서 같은 대조를 하지 않았다. 탐침은 참조 계수를 볼 수 있는 힙 버퍼를 직접 만들어 흘렸고, 자바독이 말하는 직접 메모리는 탐침 범위 밖이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/http-client-and-resilience/case/case-a11-f007-dns.md b/docs/clean-architecture-backend-template/tech-log-studio/http-client-and-resilience/case/case-a11-f007-dns.md deleted file mode 100644 index cd79721..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/http-client-and-resilience/case/case-a11-f007-dns.md +++ /dev/null @@ -1,223 +0,0 @@ ---- -kind: CASE -slug: a11-f007-dns -title: 혼동이라 적어 둔 이름이 아직 검사 문구에 있다 -topic: http-client-and-resilience -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a11-f007-dns -evidenceCapturedOn: 2026-09-02 -body: case-a11-f007-dns.body.md -assets: - - key: a11-f007-dns - file: ../../../final/evidence/rendered/a11-f007-dns.svg - - key: a11-f007-dns-overloads - file: ../../../final/evidence/rendered/a11-f007-dns-overloads.svg -evidence: - - ../../../final/evidence/raw/a11-f007-dns.txt - - ../../../final/evidence/raw/a11-f007-dns-overloads.txt -source: - - 원본 분석 절은 final/document.md#a11#L583 이다. 등급은 P3 이다. 블로킹 오버로드에만 그 검사와 주석이 있다는 대조, 반응형 오버로드는 동적 대상 안정만 본다는 지적, 그래서 주석이 혼동이라 적은 상태가 반응형 경로에 남아 있다는 판정, 출하된 두 반응형 전송의 두 플래그가 같은 값이라 지금 노출이 없다는 확인, 프로파일 검증기가 동적 모드와 실험적 전송 조합을 이미 거부한다는 사실, 위험이 열리는 조합과 수정 방향이 그 절에 있다. - - 이 기록이 더한 것은 넷이다. 36 의 문구가 자기가 읽지 않는 플래그의 이름을 쓴다는 것 — 혼동은 과거형이 아니다. 그래서 이 두 검사에 걸린 유일한 test 가 36 의 문구만으로 통과하므로 43 을 지워도 빨개지지 않는다는 것. 두 플래그 조합 넷을 실제로 넣어, 갈리는 것이 한 조합뿐이고 나머지 거부에서 반응형이 내놓는 문구가 검사하지 않은 능력의 이름이라는 것. 그리고 오늘 막고 있는 것이 프로파일 검증기가 아니라 반응형 제공자 맵에 전송이 하나뿐이라는 사실이고, 그 맵은 빈 부재 조건이라 배포가 갈아 끼울 수 있다는 것이다. ---- - -# 혼동이라 적어 둔 이름이 아직 검사 문구에 있다 - -전송 능력 검증기의 블로킹 오버로드에 동적 모드 검사가 둘 있다. 앞엣것은 안정 플래그를 읽으면서 뒤엣것이 지키는 능력의 이름을 문구에 쓴다. 반응형 오버로드에는 앞엣것만 있고, 그래서 그 문구가 반응형 거부에도 그대로 나온다. - -## 관계 - -- **두 오버로드가 있었고 호출되는 쪽이 틀린 쪽이었다** - 두 사례 모두 오버로드가 둘인데 검사가 한쪽에만 있다. -- **재생 가능으로 인증된 본문이 다른 바이트를 낸다** - 같은 리프의 다른 검사 사례다. -- **가드의 존재가 곧 테넌트 격리 보장은 아니다** - 지금 노출이 없는 것과 검사가 있는 것은 다르다. - -## 문제 - -능력 검증기는 블로킹용과 반응형용 오버로드를 따로 갖는다. - -두 오버로드가 같은 검사를 하는지 대조했다. - -## 결론 - -블로킹 쪽에만 있는 검사가 하나 있다. - -동적 모드인데 호출 범위 검증된 DNS 고정 능력이 없으면 누락 목록에 넣는 검사다. 그 앞에는 같은 조건에서 동적 대상 안정 능력을 보는 검사가 따로 있다. - -뒤엣것의 주석이 이유를 적는다. 그 능력 플래그가 모든 능력 레코드에 선언되어 있었지만 아무도 읽지 않았다는 것이다. 서버 측 요청 위조 방어의 주소 검증이 소켓까지 살아남는지를 결정하는 것이 그 능력이므로, 그것이 없는 전송은 동적 대상 안정 플래그가 무엇을 말하든 동적 대상을 서비스할 수 없다는 것이다. 둘이 혼동되고 있었다는 것이다. - -그런데 앞엣것의 문구가 자기가 읽지 않는 플래그의 이름을 쓴다. 그 검사는 안정 플래그를 보면서 검증된 DNS 고정이 없다고 적고, 고정 플래그를 실제로 읽는 것은 뒤엣것이다. 주석이 지나간 일로 적은 혼동이 이 문구에는 아직 남아 있다. - -그래서 하나뿐인 그 test 가 둘째 줄을 놓친다. 그 test 는 두 플래그가 모두 거짓인 능력을 넣고 메시지에 검증된 DNS 고정이 들어 있는지만 본다. 앞엣것의 문구만으로 맞는 조건이다. 뒤엣것을 통째로 지워도 초록이다. - -반응형 오버로드에는 앞엣것만 있다. - -실행으로 확인했다. 동적 프로파일 하나에 두 플래그 조합 넷을 넣었다. 둘 다 참이면 양쪽 통과다. 안정이 거짓인 두 조합은 양쪽 다 거부하는데, 그때 반응형이 내놓는 문구가 검증된 DNS 고정이 없다는 말이다. 고정이 거짓이고 안정이 참인 조합에서만 갈린다. 블로킹은 호출 범위 항목으로 거부하고 반응형은 통과한다. - -지금 그 조합은 만들어지지 않는다. 다섯 능력 모두 두 플래그에 같은 값을 넣어 둔다. 셋은 둘 다 참이고 둘은 둘 다 거짓이다. - -막고 있는 것은 프로파일 검증기가 아니다. 프로파일 검증기는 동적 모드와 두 전송 상수의 조합을 이름으로 막는데, 전송이 하나 늘면 그 목록에 없다. 오늘 이 오버로드에 닿는 능력이 하나뿐인 이유는 기본 자동 구성이 반응형 제공자 맵에 전송 하나만 넣기 때문이다. - -그 맵은 빈 부재 조건이 붙어 있다. 자기 맵을 올리는 배포는 소스를 건드리지 않고 그 자리의 능력을 갈아 끼울 수 있다. - -판정은 P3 다. - -위험은 그렇게 올린 능력이 동적 대상 안정을 참으로, 호출 범위 검증된 DNS 고정을 거짓으로 선언하는 경우다. 주석이 못박은 정확히 그 조합이 반응형 쪽에서는 통과한다. - -수정은 38~44 를 반응형 오버로드에 복사하는 것이다. 다만 출하된 다섯이 두 플래그를 같은 값으로 두고 있으므로 오늘 바뀌는 판정은 없다. - -반응형 레코드에는 어느 검사도 읽지 않는 플래그가 둘 더 있다. 주석이 적은 상태가 그 둘에는 그대로다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 두 오버로드 대조, 플래그 참조 전수 확인, 실행 탐침 -소스 수정 : x - -## 재현 조건 - -1. 전송 능력 검증기의 두 오버로드를 나란히 읽는다. -2. 블로킹 쪽 마지막 두 검사가 각각 어떤 플래그를 읽고 어떤 문구를 넣는지 대조한다. -3. 그 두 검사에 걸린 test 를 찾아 무엇을 단언하는지 읽는다. -4. 세 능력 플래그를 읽는 곳을 저장소 전체에서 센다. -5. 출하된 능력 다섯이 두 플래그에 넣는 값을 뽑는다. -6. 반응형 제공자 맵에 어떤 전송이 들어가는지, 그 빈에 어떤 조건이 붙어 있는지 읽는다. -7. 프로파일 검증기가 동적 모드에 대해 이름으로 막는 전송을 확인한다. -8. 동적 프로파일 하나에 두 플래그 조합 넷을 넣어 양쪽 오버로드에 통과시킨다. - -## 본문 - - - -전송 능력 검증기에 오버로드가 둘 있다. - -## 한쪽에만 있는 검사 - -:::evidence key="a11-f007-dns" alt="전송 능력 검증기의 블로킹 오버로드에 있는 마지막 두 검사와 그 사이의 주석, 반응형 오버로드의 마지막 검사, 그 두 검사에 걸린 유일한 test 가 단언하는 것, 세 능력 플래그를 읽는 곳 전부, 출하된 능력 다섯이 두 플래그에 넣는 값, 반응형 제공자 맵에 들어가는 전송과 그 빈에 붙은 조건, 그리고 프로파일 검증기가 동적 모드에 대해 이름으로 막는 전송을 출력한 터미널 기록." caption="36 은 안정 플래그를 읽으면서 검증된 DNS 고정이라는 문구를 넣고, 고정 플래그를 읽는 것은 43 이다 · test 는 검증된 DNS 고정이라는 문자열만 확인한다 · 출하된 능력 다섯이 두 플래그를 같은 값으로 선언한다 · 반응형 제공자 맵에는 REACTOR_NETTY 하나가 들어가고 그 빈은 빈 부재 조건이다 — 68줄 · exit 0" zoom="true" -::: - -블로킹 쪽의 마지막 두 검사다. - -```java -35: if (profile.mode() == ClientMode.DYNAMIC && !capabilities.dynamicTargetStable()) { -36: missing.add("validated DNS pinning for dynamic targets"); -37: } -38: if (profile.mode() == ClientMode.DYNAMIC && !capabilities.validatedDnsPinning()) { -39: // `validatedDnsPinning` was declared on every capability record and read by nothing. It is -40: // the capability that decides whether the SSRF address validation survives to the socket, so -41: // a transport that does not have it cannot serve a dynamic target no matter what its -42: // `dynamicTargetStable` flag says — the two were being conflated. -43: missing.add("call-scoped validated DNS pinning"); -44: } -``` - -반응형 쪽은 여기서 끝난다. - -```java -64: if (profile.mode() == ClientMode.DYNAMIC && !capabilities.dynamicTargetStable()) { -65: missing.add("validated DNS pinning for dynamic targets"); -66: } -67: reject(profile, missing); -68: } -``` - -## 36 의 문구가 가리키는 플래그 - -36 은 자기가 읽는 플래그를 가리키지 않는다. 그 검사가 보는 것은 `dynamicTargetStable` 인데 적히는 말은 검증된 DNS 고정이고, `validatedDnsPinning` 을 실제로 읽는 검사는 43 이다. 주석이 과거형으로 적은 혼동이 이 문구에 남아 있다. - -그래서 이 두 검사에 걸린 유일한 test 가 43 을 붙들지 못한다. - -```java -39: void rejectsDynamicModeWithoutValidatedPinning() { -40- ClientProfile dynamic = ClientProfiles.builder("webhook").mode(ClientMode.DYNAMIC).build(); -41- assertThatThrownBy( -42- () -> -43- validator.validate( -44- dynamic, BlockingTransportCapabilities.lightweightHttp11AndHttp2())) -45- .isInstanceOf(HttpConfigurationException.class) -46- .hasMessageContaining("validated DNS pinning"); -``` - -넣는 능력은 두 플래그가 모두 거짓이고, 단언은 메시지에 그 문자열이 있는지다. 36 의 문구만으로 맞는다. 38~44 를 통째로 지워도 이 test 는 초록이다. - -## 넣어 보면 - -:::evidence key="a11-f007-dns-overloads" alt="동적 대상 프로파일 하나에 두 플래그 조합 넷을 넣어, 블로킹 오버로드와 반응형 오버로드가 각각 통과시키는지 거부하는지와 거부할 때 적히는 누락 항목을 출력한 터미널 기록. 출하된 두 반응형 전송의 두 플래그 값도 함께 적는다. 픽스처는 고정 리비전 소스에서 직접 컴파일한다." caption="안정이 거짓인 두 조합은 양쪽 다 거부하고 반응형 문구는 검증된 DNS 고정이 없다는 말이다 · 고정이 거짓이고 안정이 참인 조합에서만 블로킹이 거부하고 반응형이 통과한다 — 20줄 · exit 0" zoom="true" -::: - -```text -pinning=true stable=true - 블로킹 : 통과 - 반응형 : 통과 -pinning=false stable=false - 블로킹 : 거부 — validated DNS pinning for dynamic targets, call-scoped validated DNS pinning - 반응형 : 거부 — validated DNS pinning for dynamic targets -pinning=true stable=false - 블로킹 : 거부 — validated DNS pinning for dynamic targets - 반응형 : 거부 — validated DNS pinning for dynamic targets -pinning=false stable=true - 블로킹 : 거부 — call-scoped validated DNS pinning - 반응형 : 통과 -``` - -안정이 거짓인 두 조합은 양쪽 다 거부한다. 그때 반응형이 운영자에게 내놓는 말이 검증된 DNS 고정이 없다는 문구다. 검사한 것은 다른 플래그다. - -갈리는 것은 마지막 줄뿐이다. - -## 지금 그 조합은 만들어지지 않는다 - -```text - apacheClassic validatedDnsPinning=true dynamicTargetStable=true - http11AndHttp2 validatedDnsPinning=true dynamicTargetStable=true - lightweightHttp11AndHttp2 validatedDnsPinning=false dynamicTargetStable=false - reactorNetty validatedDnsPinning=true dynamicTargetStable=true - jettyHttp3Experimental validatedDnsPinning=false dynamicTargetStable=false -``` - -출하된 능력 다섯이 전부 두 플래그를 같은 값으로 선언한다. 38~44 를 복사해도 오늘 바뀌는 판정은 없다. - -## 막고 있는 것은 프로파일 검증기가 아니다 - -```text -173- if (profile.mode() == ClientMode.DYNAMIC -174- && (profile.transport() == TransportType.JDK -175- || profile.transport() == TransportType.JETTY)) { -176: out.add(violation("DYNAMIC_TARGET_TRANSPORT_UNSUPPORTED", profile, "transport")); -``` - -이 검사가 이름으로 막는 것은 두 상수다. 전송이 하나 늘면 여기에는 걸리지 않는다. - -오늘 이 오버로드에 닿는 능력이 하나뿐인 이유는 다른 데 있다. - -```java - @Bean - @ConditionalOnMissingBean(name = "httpClientReactiveTransportProviders") - Map httpClientReactiveTransportProviders( - ObjectProvider meterRegistry, TlsMaterialProvider tlsMaterialProvider) { - Map providers = new EnumMap<>(TransportType.class); - providers.put( - TransportType.REACTOR_NETTY, -``` - -맵에 들어가는 전송이 하나다. 그리고 그 빈에는 빈 부재 조건이 붙어 있다. 배포가 자기 맵을 올리면 소스를 갈라내지 않고 다른 능력을 그 자리에 넣을 수 있다. - -## 아직 아무도 읽지 않는 플래그 - -```text - TransportCapabilityValidator.java:38: if (profile.mode() == ClientMode.DYNAMIC && !capabilities.validatedDnsPinning()) { - ReactiveTransportCapabilities.java:14: boolean validatedDnsPinning, - ReactiveTransportCapabilities.java:16: boolean serverSentEvents, - ReactiveTransportCapabilities.java:17: boolean cancellationReleasesConnection) { - BlockingTransportCapabilities.java:19: boolean validatedDnsPinning, -``` - -`serverSentEvents` 와 `cancellationReleasesConnection` 은 선언 줄 말고 나오는 곳이 없다. 39 의 주석이 `validatedDnsPinning` 에 대해 적은 상태가 이 둘에는 손대지 않은 채로 있다. - -## 확인하지 못한 것 - -배포가 제공자 맵을 실제로 갈아 끼워 그 조합을 기동시키지 않았다. 확인한 것은 기동 검증기의 판정까지다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/http-client-and-resilience/case/case-analysis-finding-a11-f003.md b/docs/clean-architecture-backend-template/tech-log-studio/http-client-and-resilience/case/case-analysis-finding-a11-f003.md deleted file mode 100644 index e62cde6..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/http-client-and-resilience/case/case-analysis-finding-a11-f003.md +++ /dev/null @@ -1,103 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a11-f003 -title: 위반 코드 34종 중 22종이 어떤 test에서도 이름으로 확인되지 않는다 -topic: http-client-and-resilience -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a11-f003 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a11-f003.body.md -assets: - - key: analysis-finding-a11-f003 - file: ../../../final/evidence/rendered/analysis-finding-a11-f003.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a11-f003.txt -source: - - 원본 분석 절은 final/document.md#a11#L164 이다. ---- - -# 위반 코드 34종 중 22종이 어떤 test에서도 이름으로 확인되지 않는다 - -프로파일 검증기가 내는 위반 코드는 서른네 종이다. 그중 스물두 종은 어느 테스트에서도 이름으로 참조되지 않는다. 그리고 확인되지 않는 쪽에 사고에서 유래한 가드가 거의 전부 들어 있다. - -## 관계 - -- **풀 상한 위반 코드는 발화할 수 없다** - 이 계수에서 참조 0 으로 나온 코드 중 하나다. -- **코드 자체가 옳아도 회귀를 막는 것은 test다** - 이 사례가 그 규칙의 형태다. -- **커버리지 gate 둘이 나란히 있고 하나는 발화할 수 없다** - 같은 계열의 검증 지형 사례다. - -## 문제 - -프로파일 검증기가 위반 코드를 문자열로 낸다. 종류가 서른네 개다. - -각 코드가 테스트에서 이름으로 확인되는지 셌다. - -## 결론 - -열두 종은 한 건 이상 참조된다. 모두 신뢰나 이름 검증이나 평문이나 실험 프로토콜 계열이다. - -스물두 종은 0 건이다. - -문제는 개수가 아니라 어느 쪽이 비어 있는가다. - -확인되지 않는 스물두 종에 사고에서 유래한 가드가 거의 전부 들어 있다. - -서버 주소 위조 우회와 기록의 개인정보와 조용한 프로토콜 강등과 아무 일도 하지 않는 설정과 재지향을 조용히 무시하는 경우다. - -테스트 두 개가 그룹으로 몇 개를 묶어 확인한다. 하나의 메서드가 세 위반을 한 번에 본다. - -나머지 스물두 종은 분기를 지워도 초록으로 남는다. - -코드 자체는 현재 옳다. 위험은 회귀다. - -판정은 P3 다. - -수정은 코드별 최소 경우를 매개변수 테스트 한 벌로 놓는 것이다. 서른네 종이 모두 결정적으로 정렬된 목록을 내므로 그 형태가 자연스럽다. - -## 검증 환경 - -확인 방식 : 위반 코드 문자열의 테스트 소스 집합 참조 계수 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/168 계열에 있다. - -1. 검증기가 내는 위반 코드 목록을 만든다. -2. 저장소의 테스트와 테스트킷 소스 집합에서 각 문자열을 검색한다. -3. 참조 건수로 두 무리를 나눈다. -4. 참조 0 인 무리에 어떤 성격의 가드가 들어 있는지 본다. -5. 그룹으로 확인하는 테스트가 몇 개를 덮는지 확인한다. - -## 본문 - - - -`ClientProfileValidator`가 내는 코드는 **34종**이다. 저장소 전체의 `test`/`testkit` source set에서 그 문자열을 참조하는 파일 수를 세면(`168-...` §8.4b) 다음과 같다. - -| test 참조 | 코드 수 | 예 | -|---|---|---| -| 1건 이상 | **12** | `TRUST_ALL_FORBIDDEN`(3) · `HOSTNAME_VERIFICATION_REQUIRED`(2) · `PLAINTEXT_*`(2) · `HTTP3_STABLE_FORBIDDEN`(2) … | -| **0건** | **22** | `DYNAMIC_TARGET_PROXY_UNSUPPORTED` · `FULL_URL_RECORDING_FORBIDDEN` · `BODY_LOGGING_FORBIDDEN` · `REACTIVE_REDIRECT_UNSUPPORTED` · `HTTP2_REQUIRED_TRANSPORT_UNSUPPORTED` · `TLS_PROTOCOL_SET_REQUIRED` · `DNS_TIMEOUT_UNSUPPORTED` · `PROXY_CREDENTIAL_UNSUPPORTED` · `RETRY_POLICY_CONTRADICTS_ATTEMPTS` · `MISSING_PRODUCTION_SETTING` · `ALLOWED_HOST_MISMATCH` · `ALLOWED_PORT_MISMATCH` … | - -## ClientProfileValidator 참조 위치 - -:::evidence key="analysis-finding-a11-f003" alt="코드베이스에서 ClientProfileValidator 를 검색한 출력 6줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ClientProfileValidator 코드베이스 검색 — 6줄 · exit 0" zoom="true" -::: - -## 문제는 개수가 아니라 어느 쪽이 비어 있는가다 - -확인되지 않는 22종에는 §2가 인용한 사고 유래 가드가 거의 전부 들어 있다 — SSRF 우회(`DYNAMIC_TARGET_PROXY_UNSUPPORTED`), 로그의 PII(`FULL_URL_RECORDING_FORBIDDEN`·`BODY_LOGGING_FORBIDDEN`), 조용한 프로토콜 다운그레이드(`HTTP2_REQUIRED_TRANSPORT_UNSUPPORTED`·`TLS_PROTOCOL_SET_REQUIRED`), 아무 일도 하지 않는 설정(`DNS_TIMEOUT_UNSUPPORTED`·`PROXY_CREDENTIAL_UNSUPPORTED`), 그리고 리다이렉트를 조용히 무시하는 경우(`REACTIVE_REDIRECT_UNSUPPORTED`). - -## 확인하지 못한 것 - -각 분기를 실제로 지우고 테스트가 초록으로 남는지 확인하지 않았다. 참조 계수상 그 결과가 나온다. - -test 두 개(ClientProfileValidatorTest 106줄)가 그룹으로 몇 개를 묶어 확인하지만(rejectsSimpleFactoryAndUnacknowledgedHttp3AndJdkRoutePool), 나머지 22종은 분기를 지워도 초록으로 남는다. 코드 자체는 현재 옳다 — 위험은 회귀다. P3. 수정은 @ParameterizedTest로 코드별 최소 케이스를 한 벌 놓는 것이고, 34종이 모두 결정적으로 정렬된 목록을 내므로 그 형태가 자연스럽다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/http-failure-classification/case/case-a-circuit-breaker-permit-that-leaks-on-local-rejection.md b/docs/clean-architecture-backend-template/tech-log-studio/http-failure-classification/case/case-a-circuit-breaker-permit-that-leaks-on-local-rejection.md deleted file mode 100644 index d3acf9c..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/http-failure-classification/case/case-a-circuit-breaker-permit-that-leaks-on-local-rejection.md +++ /dev/null @@ -1,118 +0,0 @@ ---- -kind: CASE -slug: a-circuit-breaker-permit-that-leaks-on-local-rejection -title: 로컬 거부 경로에서 회로 브레이커 permission이 반환되지 않는다 -topic: http-failure-classification -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a-circuit-breaker-permit-that-leaks-on-local-rejection -evidenceCapturedOn: 2026-09-02 -body: case-a-circuit-breaker-permit-that-leaks-on-local-rejection.body.md -assets: - - key: a-circuit-breaker-permit-that-leaks-on-local-rejection - file: ../../../final/evidence/rendered/a-circuit-breaker-permit-that-leaks-on-local-rejection.svg -evidence: - - ../../../final/evidence/raw/a-circuit-breaker-permit-that-leaks-on-local-rejection.txt -source: - - 원본 분석 절은 final/document.md#a11 §22 이다. permission 획득·반환 경로의 전수 추적은 그 분석이 인용하는 원시 증거 170 번 §8.1 에 있다. ---- - -# 로컬 거부 경로에서 회로 브레이커 permission이 반환되지 않는다 - -회로 브레이커 permission을 얻은 뒤 rate limiter나 bulkhead가 요청을 거부하면 그 permission이 반환되지 않는다. HALF_OPEN 상태에서는 시험 슬롯이 영구히 소비되어, 회복한 업스트림에 대해 회로가 닫히지 않을 수 있다. - -## 관계 - -- **전송 실패의 단계와 범주 — AttemptStage와 FailureCategory** - 이 파이프라인이 그 분류를 만들기 전에 지나는 승인 계층이다. - -## 문제 - -요청 하나가 실행되기 전에 세 가드를 차례로 지난다. 회로 브레이커가 permission 을 주고, rate limiter 가 토큰을 주고, bulkhead 가 슬롯을 준다. - -뒤의 두 가드가 거부하면 그 경로는 예외를 던지고 끝난다. 그 사이에 이미 받아 둔 회로 permission 을 돌려주는 호출이 없다. - -## 결론 - -경로 확인으로 확정한 결함이다. 회로가 반쯤 열린 상태에서만 발생한다. - -업스트림 장애로 회로가 열린다. 대기 후 반쯤 열린 상태로 바뀌고, 트래픽이 돌아온다. 그 순간 평상시 부하에 맞춰 사이징된 로컬 rate limiter 나 bulkhead 가 거부하기 시작하고, 거부마다 시험 슬롯 하나가 사라진다. - -거부가 허용된 시험 호출 수만큼 쌓이면 브레이커는 성공도 실패도 못 본 채 그 상태에 머문다. 회복한 업스트림에 대해 회로가 닫히지 않는다. - -수정하려면 인터페이스에 permission 반환 연산을 추가하고, rate limiter 와 bulkhead 가 요청을 거부하는 두 경로에서 그 연산을 호출해야 한다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Gradle : 9.0.0 -Spring Boot : 4.0.8 -Resilience4j : 2.2.0 -확인 방식 : 파이프라인 진입부의 예외 경로 추적, 인터페이스 연산 전수 확인, 반환 연산 이름 검색 -소스 수정 : x - -## 재현 조건 - -1. AttemptResiliencePipeline.execute 의 83행부터 97행까지를 읽고, 세 가드의 거부 경로에서 회로 브레이커 연산이 호출되는지 확인한다. -2. AttemptCircuitBreaker 의 추상 연산을 전수 확인한다. 넷이며 반환 연산이 없다. -3. releasePermission 을 코드베이스에서 검색한다. Java 매치가 0 이다. -4. AttemptResiliencePipelineTest 에서 회로 permission 반환을 단언하는 테스트가 있는지 확인한다. - -## 본문 - - - -`AttemptResiliencePipeline.execute` 진입부에서 세 가드가 차례로 실행된다. 83행이 회로 permission 을 얻고, 88행이 rate limiter 를, 93행이 bulkhead 를 본다. - -83행을 통과한 뒤 88행과 93행이 거부하면 그 두 경로는 예외를 던지고 끝난다. 회로 permission 이 돌아오지 않는다. - -## 반환할 연산 자체가 인터페이스에 없다 - -:::evidence key="a-circuit-breaker-permit-that-leaks-on-local-rejection" alt="코드베이스에서 AttemptResiliencePipeline 의 가드와 반납 지점, AttemptCircuitBreaker 의 추상 연산 전수, releasePermission 검색 결과를 뽑은 출력 26줄. 인터페이스의 연산 넷과 Java 코드 매치 0, 그리고 그 이름이 설계 문서에만 남아 있다는 것이 그 출력에 그대로 보인다." caption="세 가드 · 정상 경로 반납 · AttemptCircuitBreaker 연산 넷 · releasePermission 검색 — 26줄 · exit 0" zoom="true" -::: - -`AttemptCircuitBreaker` 의 추상 연산은 넷이다 — `tryAcquirePermission` · `onSuccess` · `onError` · `state`. 획득은 있고 반환은 없다. - -`releasePermission` 은 `src` 아래 Java 파일에서 매치가 0 이다. 저장소 전체로 넓히면 한 곳에 나오는데, 그것은 이 능력의 설계 문서다. 즉 이름이 설계 단계에서는 존재했고 구현에는 들어오지 않았다. - -로컬 거부 경로가 부르는 것을 잊은 것이 아니라, 부를 수 있는 연산이 없다. - -## 같은 거부 경로에서 rate 토큰은 돌려준다 - -94행이 이 판정을 뒷받침한다. bulkhead 가 거부하는 경로는 `rateLimiter.onCompleted()` 를 불러 rate 토큰을 명시적으로 반환한다. 저자가 permit 반환을 의식하고 있었다는 증거다. - -정상 경로에도 같은 의식이 보인다. `releaseAttemptPermits()` 가 `bulkhead.release()` 와 `rateLimiter.onCompleted()` 를 함께 부른다 — 자료의 120행이 그 두 번째 호출이다. 세 가드 중 둘은 정상 경로에서도 거부 경로에서도 반납되고, 회로만 어느 쪽에서도 반납되지 않는다. - -## 이 결함은 HALF_OPEN 에서만 값을 갖는다 - -Resilience4j 의 `tryAcquirePermission()` 은 반쯤 열린 상태에서 허용된 시험 호출 수 중 하나를 소비한다. 그 슬롯은 `onSuccess` · `onError` · `releasePermission` 중 하나로만 돌아온다. 아무것도 부르지 않으면 슬롯은 영구히 소비된다. - -닫힌 상태에서는 permission 이 계수를 소비하지 않으므로 같은 코드가 무해하다. 그래서 이 결함은 코드가 아니라 상태에 걸려 있고, 평상시 테스트로는 드러나지 않는다. - -## 조건들이 우연히 겹치지 않는다 - -시험 슬롯이 열리는 시점은 업스트림이 회복을 시작한 시점이고, 트래픽이 돌아오는 시점도 같다. 평상시 부하에 맞춰 사이징된 로컬 가드는 그 순간에 거부하기 시작한다. - -거부마다 슬롯 하나가 사라진다. 허용된 시험 호출 수만큼 거부가 나면 브레이커는 성공도 실패도 관측하지 못한 채 그 상태에 머문다. `maxWaitDurationInHalfOpenState` 기본값이 0 — 무한 대기 — 이므로 시간이 그것을 풀어 주지도 않는다. - -## 테스트가 그 공백을 그대로 보여 준다 - -인접한 두 성질에는 테스트가 있다. - -- `openCircuitDoesNotConsumeRateOrBulkheadPermit` — 회로가 거부할 때 뒤의 둘을 소비하지 않는다 -- `bulkheadRejectionReleasesTheRateLimiterAndIsNotACircuitError` — bulkhead 거부가 rate 를 돌려준다 - -둘째 테스트가 단언하는 이벤트 순서는 `circuit-enter` · `rate-enter` · `bulkhead-reject` · `rate-exit` 다. 회로를 돌려주는 이벤트가 그 목록에 없다. 88행의 rate limiter 거부 경로에는 테스트가 아예 없다. - -## 수정 - -`AttemptCircuitBreaker` 에 `releasePermission()` 을 더해 Resilience4j 의 같은 이름 연산에 위임하고, `alwaysClosed()` 구현에서는 아무것도 하지 않게 둔다. 그리고 두 로컬 거부 경로에서 그것을 부른다. - -## 확인하지 못한 것 - -시험 슬롯 고갈을 반복 호출로 재현해 보지는 않았다. 회로가 닫히지 않는 상태를 런타임에서 관측한 것은 아니다. - -Resilience4j 의 상태별 permission 회계와 무한 대기 기본값은 그 라이브러리의 문서화된 동작을 근거로 삼았고, 이 회차에 라이브러리 코드를 실행해 확인하지는 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/http-failure-classification/reference/reference-a-classifier-sees-only-what-the-engine-kept.md b/docs/clean-architecture-backend-template/tech-log-studio/http-failure-classification/reference/reference-a-classifier-sees-only-what-the-engine-kept.md deleted file mode 100644 index ed1e72d..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/http-failure-classification/reference/reference-a-classifier-sees-only-what-the-engine-kept.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -kind: REFERENCE -slug: a-classifier-sees-only-what-the-engine-kept -title: 분류기는 엔진이 남긴 것만 볼 수 있다 -topic: http-failure-classification -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:a-classifier-sees-only-what-the-engine-kept -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# 분류기는 엔진이 남긴 것만 볼 수 있다 - -## 목적 - -예외를 자기 범주로 번역하는 계층이 무엇을 분류할 수 있는지에 상한이 있다는 것을 잊고, 분기 순서를 고쳐서 해결하려 드는 것을 막는다. - -## 규칙 - -1. 분류 가능한 것의 상한은 사슬에 남은 것이다 - 번역 계층은 그 아래 엔진이 버리지 않고 남긴 원인만 볼 수 있다. 사슬에 없는 원인은 어떤 순회 순서로도 분류되지 않는다. - -2. 이름 하나가 여러 엔드포인트로 풀리면 마지막 것만 남을 수 있다 - Apache HttpClient 5 의 다중 주소 연결 루프는 마지막이 아닌 주소의 실패를 삼킨다. 호스트명이 여러 주소로 풀리면 호출자에게 도달하는 것은 마지막 주소의 오류뿐이다. - -3. 분류를 고치기 전에 입력을 먼저 확인한다 - 잘못된 분류를 보면 분기 순서부터 의심하게 된다. 순서를 바꾸기 전에 사슬을 출력해서 원인이 실제로 거기 있는지 본다. - -4. 테스트는 주소를 고정한다 - 실패 분류를 검증하는 테스트가 호스트명을 쓰면, 그 테스트는 이름 해석이라는 통제되지 않은 변수를 함께 검증한다. 루프백 주소를 직접 쓰거나 주소 패밀리를 고정한다. - -## 적용 조건 - -이름 하나가 여러 엔드포인트로 풀리는 모든 클라이언트 : DNS A 와 AAAA, 서비스 디스커버리, 다중 브로커 부트스트랩 - -실패 분류가 재시도 안전성이나 보안 판정으로 이어지는 곳 : 특히 중요 - -## 예외 - -모든 주소가 같은 이유로 실패하면 마지막 오류가 대표성을 가지므로 문제가 되지 않는다. 강등은 패밀리별 또는 엔드포인트별 실패 양상이 다를 때만 일어난다. - -## 예시 - -인증서가 신뢰 불가면 두 주소 패밀리 모두 TLS 에서 실패하고, 마지막 오류도 핸드셰이크 예외라 분류가 맞는다. - -한 패밀리는 TLS 를 거절하고 다른 패밀리는 연결이 거부되면, 영구 실패가 일시적 연결 실패로 보고된다. - -## 관계 - -- **붉은 테스트를 제품 결함으로 읽은 오진** - 이 규칙을 끌어낸 사례다. 사슬을 출력하기 전까지는 분기 순서가 원인으로 보였다. -- **원인 사슬은 가장 구체적인 분류가 이기도록 순회한다** - 이 규칙과 짝을 이룬다. 그 규칙은 사슬 안에서의 선택을, 이 규칙은 사슬 자체의 한계를 다룬다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/http-failure-classification/reference/reference-retryability-needs-both-idempotency-and-category.md b/docs/clean-architecture-backend-template/tech-log-studio/http-failure-classification/reference/reference-retryability-needs-both-idempotency-and-category.md deleted file mode 100644 index fe4d5de..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/http-failure-classification/reference/reference-retryability-needs-both-idempotency-and-category.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -kind: REFERENCE -slug: retryability-needs-both-idempotency-and-category -title: 재시도 가능성은 멱등성과 실패 범주를 함께 봐야 정해진다 -topic: http-failure-classification -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:retryability-needs-both-idempotency-and-category -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# 재시도 가능성은 멱등성과 실패 범주를 함께 봐야 정해진다 - -## 목적 - -한 축만 보고 재시도를 정해서, 비멱등 요청을 반복하거나 영구 실패를 무한히 되풀이하는 것을 막는다. - -## 규칙 - -1. 두 입력의 곱이다 - 범주가 재시도 가능하고 동시에 요청이 재시도 안전할 때만 재시도한다. 범주만 보면 비멱등 요청을 재시도하고, 멱등성만 보면 영구 실패를 반복한다. - -2. 전송되지 않았다는 증거는 멱등성 요구를 완화한다 - 아무것도 서버에 닿지 않았음이 증명되면 그 시도는 없던 일이므로 멱등성을 묻지 않아도 된다. 다만 그 증거는 증명일 때만 쓴다. - -3. 증거는 승격하지 않는다 - 일반적인 엔진 입출력 실패를 전송되지 않음으로 올리지 않는다. 모르는 것은 모르는 채로 둔다. 추측해서 올린 판정이 중복 결제를 만든다. - -4. 응답이 전달되기 시작했으면 재시도하지 않는다 - 첫 바이트가 호출자에게 전달된 뒤에는 범주와 무관하게 재시도가 막힌다. - -5. 정책의 화이트리스트로 terminal 판정을 되살리지 않는다 - 화이트리스트는 어떤 범주가 재시도될 수 있는지를 넓히지, 이 실패에 대한 판정을 뒤집지 않는다. - -## 적용 조건 - -HTTP gRPC 메시징 클라이언트의 재시도 결정 - -멱등성 여부가 요청마다 다른 경로 - -## 예외 - -분류기가 terminal 로 표시한 실패는 정책으로 되살릴 수 없다. - -## 예시 - -이 저장소는 전송되지 않음 증거를 별도 축으로 두어 완화를 표현한다. 재시도 컨텍스트에 HTTP 메서드가 없고, 멱등성 키가 전송 여부까지 요구하며, 첫 바이트 전달은 되돌릴 수 없는 래치다. - -부분 응답은 안전하게 멱등인 요청에 대해서만 재시도된다. 그 외에는 원격 결과 불명으로 남긴다. - -## 관계 - -- **전송 실패를 단계와 범주 두 축으로 모델링한다** - 이 규칙의 입력을 만드는 모델이다. -- **재시도 안전성은 증거에 기반해 판정한다** - 이 규칙을 채택한 프로젝트 결정이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/http-failure-classification/reference/reference-verify-runtime-shape-at-runtime.md b/docs/clean-architecture-backend-template/tech-log-studio/http-failure-classification/reference/reference-verify-runtime-shape-at-runtime.md deleted file mode 100644 index 835db1f..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/http-failure-classification/reference/reference-verify-runtime-shape-at-runtime.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -kind: REFERENCE -slug: verify-runtime-shape-at-runtime -title: 런타임의 모양에 대한 주장은 런타임에서 확인한다 -topic: http-failure-classification -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:verify-runtime-shape-at-runtime -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# 런타임의 모양에 대한 주장은 런타임에서 확인한다 - -## 목적 - -코드를 읽어 얻은 추론이 관측으로 굳어지는 것을 막는다. 특히 실패하는 테스트를 결함의 증거로 읽는 순간을 막는다. - -## 규칙 - -1. 벤더 라이브러리의 런타임 동작은 읽어서 알 수 없다 - 이 라이브러리는 예외를 이렇게 감쌀 것이다, 이 게이트가 이 값을 읽을 것이다, 이 경로가 프로덕션 기본값이다. 이런 문장은 전부 가설이며 확인 전까지 판정의 근거가 될 수 없다. - -2. 붉은 테스트는 조사의 시작점이지 결론이 아니다 - 실패하는 테스트는 무언가 어긋났다는 사실만 말한다. 무엇이 어긋났는지는 별도 측정이다. 테스트 이름이 가리키는 대상이 곧 원인인 경우가 오히려 드물다. - -3. 대조는 변수 하나만 바꾼다 - 같은 픽스처에서 호스트 문자열만 바꾸는 식의 대조가 가장 값싸고 결정적이다. 두 가지를 함께 바꾸면 결과를 해석할 수 없다. - -4. 소스를 고치지 않고 확인할 방법이 대개 있다 - 테스트 런타임 클래스패스를 얻어 jshell 로 재현하고, 리플렉션으로 private 메서드를 부르고, 조건만 바꿔 두 번 돌린다. 애플리케이션 소스를 건드리지 않고도 관측이 된다. - -5. 확인 비용은 분 단위다 - 이 프로젝트에서 판정 번복 한 건과 자기 교정 세 건이 전부 이 형태였고, 넷 다 측정 하나로 갈렸다. - -## 적용 조건 - -프레임워크 드라이버 클라이언트 라이브러리의 런타임 동작에 의존하는 모든 판정 - -실패하는 테스트를 근거로 결함을 보고하려는 순간 - -관측 없이 심각도를 P1 으로 올리려는 순간 - -## 예외 - -소스가 저장소 안에 있고 그 경로가 테스트로 고정돼 있으면 읽기로 충분하다. 이 규칙은 벤더 코드의 동작에 대한 것이다. - -## 예시 - -분기 순서를 읽고 예외 사슬의 모양을 단정했다. 사슬을 출력하니 달랐다. - -레지스트리에 검사가 없다고 단정했다. 빌드 파일의 주석이 가리키는 세 곳을 따라가니 있었다. - -전역 승인 게이트가 임계값을 읽을 것이라고 가정했다. 그 가드는 해당 필드를 읽지 않았다. - -## 관계 - -- **붉은 테스트를 제품 결함으로 읽은 오진** - 이 규칙을 끌어낸 사례다. 네 건 중 가장 비쌌던 것이다. -- **분류기는 엔진이 남긴 것만 볼 수 있다** - 같은 사례에서 나온 짝 규칙이다. 이쪽은 방법을, 저쪽은 대상을 다룬다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/http-failure-classification/reference/reference-walk-the-cause-chain-most-specific-wins.md b/docs/clean-architecture-backend-template/tech-log-studio/http-failure-classification/reference/reference-walk-the-cause-chain-most-specific-wins.md deleted file mode 100644 index a4c4e26..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/http-failure-classification/reference/reference-walk-the-cause-chain-most-specific-wins.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -kind: REFERENCE -slug: walk-the-cause-chain-most-specific-wins -title: 원인 사슬은 가장 구체적인 분류가 이기도록 순회한다 -topic: http-failure-classification -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:walk-the-cause-chain-most-specific-wins -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -note: 이 규칙을 처음 끌어낸 사례는 나중에 철회되었다. httpclient 의 mTLS 테스트 3건이 실패한 원인은 분기 순서가 아니라 테스트 픽스처의 호스트명이었고, 그 저장소의 분류기는 이 실패의 사례가 아니다. 규칙 자체는 다른 근거로 유효하다. ---- - -# 원인 사슬은 가장 구체적인 분류가 이기도록 순회한다 - -## 목적 - -라이브러리가 구체적 원인을 일반적 예외로 감쌌을 때, 바깥 타입만 보고 분류해서 정확한 판정을 잃는 것을 막는다. - -## 규칙 - -1. 바깥 타입 하나로 분류하지 않는다 - Spring 이 엔진 예외를 감싸고 드라이버가 자기 예외를 다시 감싼다. 바깥 타입만 매칭하면 증명 가능한 판정이 모호한 판정으로 내려간다. - -2. 기준은 순서가 아니라 구체성이다 - 이 사슬에서 가장 구체적인 분류가 이기는가를 묻는다. 구현은 두 가지다. 구체적 분기를 앞으로 옮기거나, 사슬 전체를 훑어 최선의 매치를 고른다. - -3. 사이클 안전을 확보한다 - 이미 본 예외를 IdentityHashMap 으로 표시하거나 최대 깊이를 둔다. 순환 참조는 실제로 나타난다. - -4. 벤더별 곁가지를 따라간다 - getCause 만으로는 부족하다. SQLException 의 getNextException 처럼 벤더가 따로 두는 연결 고리가 있다. - -5. 이 규칙이 실패의 원인이 아닐 수도 있다 - 잘못된 분류를 봤을 때 순서를 의심하기 전에, 그 원인이 사슬에 실제로 있는지 먼저 확인한다. - -## 적용 조건 - -드라이버나 클라이언트 예외를 자기 범주로 번역하는 모든 분류기 - -특히 전송 계층 : 감싸기가 흔하다 - -## 예외 - -바깥 예외가 실제로 더 구체적인 경우가 있다. 그때는 순서가 아니라 우선순위 표가 필요하고, 그 표를 테스트로 고정해야 한다. - -## 예시 - -이 저장소의 mongo 실패 추출기와 bulk 실패 추출기는 IdentityHashMap 으로 방문 표시를 두고 사슬을 훑는다. - -jpa 의 SQL 상태 해석기는 getNextException 을 따라간다. - -httpclient 의 분류기는 사슬 전체를 훑는다. 주석이 이유를 적는다. 감싸인 ConnectException 도 요청이 전송되지 않았음을 증명하므로, 바깥 타입만 매칭하면 증명 가능한 NOT_SENT 를 모호한 SENT_NO_RESPONSE 로 낮추게 된다. - -## 관계 - -- **분류기는 엔진이 남긴 것만 볼 수 있다** - 이 규칙의 상한을 정한다. 사슬에 없는 원인은 어떤 순회로도 분류되지 않는다. -- **붉은 테스트를 제품 결함으로 읽은 오진** - 이 규칙을 잘못 적용한 기록이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/identity-and-identifier/case/case-a07-f001-uuidcodec.md b/docs/clean-architecture-backend-template/tech-log-studio/identity-and-identifier/case/case-a07-f001-uuidcodec.md deleted file mode 100644 index 2966e29..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/identity-and-identifier/case/case-a07-f001-uuidcodec.md +++ /dev/null @@ -1,176 +0,0 @@ ---- -kind: CASE -slug: a07-f001-uuidcodec -title: UuidCodec 의 메서드를 부르는 줄은 자기 명세 다섯뿐이다 -topic: identity-and-identifier -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a07-f001-uuidcodec -evidenceCapturedOn: 2026-09-02 -body: case-a07-f001-uuidcodec.body.md -assets: - - key: a07-f001-uuidcodec - file: ../../../final/evidence/rendered/a07-f001-uuidcodec.svg - - key: a07-f001-uuidcodec-elsewhere - file: ../../../final/evidence/rendered/a07-f001-uuidcodec-elsewhere.svg -evidence: - - ../../../final/evidence/raw/a07-f001-uuidcodec.txt - - ../../../final/evidence/raw/a07-f001-uuidcodec-elsewhere.txt -source: - - 원본 분석 절은 final/document.md#a07#L73 이다. 등급은 P2 다. 세 타입의 리프 밖 소비자 계수, 메서드 호출이 자기 명세뿐이라는 사실, 문자열에서 직접 만드는 경로가 여럿이라는 지적, 컬럼 변환을 하이버네이트가 처리한다는 서술, 그리고 수정 두 가지가 그 절에 있다. - - 그 절은 직접 만드는 파일이 스무 개 이상이라 적는데, 그 절이 예시로 든 다섯이 모두 프로덕션 소스이므로 같은 정의에서 맞는 수는 열다섯 파일 스물한 줄이다. 소스 세트를 가리지 않고 세면 서른넷이고 그중 열아홉이 테스트다. - - 애너테이션이 잇는 구간이 UUID 값과 컬럼 사이라는 것, GraphQL 스칼라가 표준 파서보다 좁게 거른다는 것, 그리고 모듈 의존 규칙이 스물한 줄 중 열세 줄을 막는다는 것은 이 기록에서 확인했다. ---- - -# UuidCodec 의 메서드를 부르는 줄은 자기 명세 다섯뿐이다 - -이 리프를 아웃바운드 어댑터 모듈 밖에 둔 근거로 UUID 식별자와 코덱 능력이 제시된다. 그 능력을 구현한 타입을 부르는 프로덕션 코드가 없다. - -## 관계 - -- **@Bean이 있다는 것은 조립 증거가 아니다** - 타입의 존재와 사용을 나눠 세는 규칙이다. -- **소비자가 없는 fixture 셋** - 선언된 타입 수와 프로덕션 호출자 수를 각각 세어 배선되지 않았다는 것을 확인한 다른 리프다. -- **normalize는 canonical이 아닌 입력을 받아 다른 UUID로 조용히 바꾼다** - 이 타입을 단일 경로로 올릴 때 먼저 고쳐야 하는 것이다. -- **문서가 선언됐다는 둘은 없고, 선언된 하나는 그 문장에 없다** - 같은 리프의 문서 정합성 사례다. - -## 문제 - -모듈 문서가 배치 근거를 한 문장으로 적는다. UUID 식별자와 코덱 능력은 인프라이지 아웃바운드 연동 지점이 아니므로 그 모듈 밖에 둔다는 것이다. - -리프의 공개 타입은 셋이다. 가명화기와 업로드 식별자 팩토리와 UUID 코덱이다. - -## 결론 - -앞의 둘은 리프 밖에서 생성된다. 가명화기는 부트스트랩과 예제 애플리케이션에서, 업로드 식별자 팩토리는 부트스트랩에서다. - -셋째는 없다. 그 이름이 리프 밖에 나오는 자리는 넷인데 둘은 몽고 테스트킷이 임포트하는 드라이버의 동명 타입이고 하나는 그 드라이버 설정을 적은 ADR 문서다. 이 리프의 타입을 가리키는 것은 하나뿐이고, 그것은 호출이 아니라 예제 애플리케이션 README 의 산문이다. 그 산문은 코덱을 쓰지 않는 이유를 적는다. - -메서드를 부르는 줄은 저장소 전체에서 다섯이고 다섯 다 자기 명세다. - -리프 밖 프로덕션에서 문자열을 UUID 로 바꾸는 자리를 세면 파일 열다섯에 줄 스물하나다. 그 열다섯이 하는 일이 다 같지는 않다. GraphQL 의 UUID 스칼라는 표준 파서가 관대하다는 것을 자바독에 적고 정규형 정규식으로 먼저 거른 뒤에야 파서를 부른다. 코덱으로 갈아 끼우면 검사 폭이 줄어든다. - -문서가 코덱의 다른 목적으로 든 컬럼 변환은 하이버네이트의 타입 코드 애너테이션이 맡는다. 다만 그 애너테이션이 붙은 서른여덟 자리의 필드 타입은 전부 UUID 다. 애너테이션이 잇는 것은 UUID 값과 컬럼 사이이고, 문자열과 UUID 사이는 여전히 손으로 짜여 있다. 예제의 영속 매퍼가 그 두 겹을 한 파일에서 보여 준다. - -이 분산은 배치 규칙의 결과다. 모듈 레지스트리에서 이 리프에 의존해도 되는 모듈은 예순둘 중 둘, 부트스트랩과 예제 애플리케이션뿐이다. 그중 열세 줄은 규칙을 손대기 전에는 그 타입에 닿을 수 없는 자리다. 남는 여덟 줄 중 둘은 예제 README 가 쓰지 않는 이유를 이미 적어 두었다. - -코드 자체에는 결함이 없다. 서른 줄짜리 유틸이고 자기 시험은 다 통과한다. 어긋난 것은 논거다. - -그래서 선택지는 좁다. 능력을 논거에서 지우거나 배치 규칙을 손보는 것뿐이다. 그 타입을 실제 단일 경로로 올리려면 규칙을 먼저 바꿔야 하고, 그 다음에는 같은 문서 다음 절이 다루는 정규화 문제를 고쳐야 한다. - -이 기록이 세는 것은 소비자의 유무이지 단일 경로 여부가 아니다. 단일 경로를 물으면 앞의 둘도 통과하지 못한다. 리프 밖 프로덕션 스물다섯 파일이 표준 라이브러리 생성기를 직접 부른다. - -## 검증 환경 - -확인 방식 : 저장소 전수 참조 계수, 모듈 의존 레지스트리 조회 -소스 수정 : x - -## 재현 조건 - -1. 모듈 문서가 적은 배치 근거를 읽는다. -2. 리프의 공개 타입을 나열한다. -3. 각 타입 이름이 리프 밖에 나오는 곳을 확장자 제한 없이 전수로 센다. -4. 그중 이 리프의 타입을 가리키는 것과 동명의 다른 타입을 가른다. -5. 그 타입의 메서드를 부르는 줄을 저장소 전체에서 센다. -6. 문자열에서 UUID 를 직접 만드는 리프 밖 프로덕션 파일과 줄을 전부 나열한다. -7. 그중 표준 파서를 그대로 쓰지 않는 것이 있는지 확인한다. -8. 컬럼 변환 애너테이션이 붙은 필드의 타입을 센다. -9. 레지스트리에서 이 리프의 소비자 후보를 센다. - -## 본문 - - - -모듈 문서가 이 리프를 아웃바운드 어댑터 모듈 밖에 둔 이유를 세 줄로 적는다. - -## 논거와 공개 타입 - -:::evidence key="a07-f001-uuidcodec" alt="모듈 문서가 적은 배치 근거 세 줄과 리프의 공개 타입 셋. 각 타입 이름이 리프 밖에 나오는 곳을 확장자 제한 없이 전수로 검색한 결과. 그리고 UUID 코덱의 메서드를 부르는 줄을 저장소 전체에서 검색한 결과를 출력한 터미널 기록." caption="가명화기는 리프 밖 생성 둘, 업로드 식별자 팩토리는 하나 · 코덱 이름이 나오는 넷 중 둘은 드라이버의 동명 타입, 하나는 ADR 문서, 하나는 예제 README 의 산문 · 메서드 호출은 자기 명세 다섯 줄 — 40줄 · exit 0" zoom="true" -::: - -```text -- Kept out of `adapter-outbound` on purpose: a UUID id/codec capability is - infrastructure, not an outbound integration point, so `adapter-outbound` keeps its - documented meaning (external HTTP / messaging / cache / notifications). -``` - -능력은 하나로 적혀 있고, 공개 타입은 셋이다. 셋 중 둘은 리프 밖에서 생성된다 — 가명화기는 부트스트랩과 예제 애플리케이션에서, 업로드 식별자 팩토리는 부트스트랩에서. - -## 이름이 겹친 참조들 - -코덱 이름이 리프 밖에 나오는 자리는 넷이다. - -```text - docs/adr/ADR-MONGO-002-bson-representation.md:44:pins `UuidCodec(STANDARD)` explicitly … - src/sample-portfolio/README.md:333:- … `UuidCodec` 같은 공용 - src/adapter/outbound/persistence-mongo/src/testkit/.../MongoBsonSnapshot.java:16:import org.bson.codecs.UuidCodec; - src/adapter/outbound/persistence-mongo/src/testkit/.../MongoBsonSnapshot.java:59: … new UuidCodec(UuidRepresentation.STANDARD)), -``` - -뒤 둘은 몽고 드라이버의 동명 타입이고, ADR 은 그 드라이버 설정을 적은 문서다. 이 리프의 타입을 가리키는 것은 예제 README 한 줄뿐인데, 그 줄은 호출이 아니라 쓰지 않는 이유를 적는 산문이다. - -저장소 전체에서 그 타입의 메서드를 부르는 줄은 다섯이고, 다섯 다 자기 명세 파일이다. - -## 같은 변환을 하는 다른 자리들 - -:::evidence key="a07-f001-uuidcodec-elsewhere" alt="문자열에서 UUID 를 직접 만드는 리프 밖 프로덕션 파일 수와 호출 줄 수와 그 파일 전부의 목록. 그중 GraphQL 스칼라가 표준 파서의 관대함을 적고 정규형 정규식으로 먼저 거르는 구간. 컬럼 변환 애너테이션을 단 프로덕션 파일 수와 애너테이션 수와 그 애너테이션이 붙은 필드의 타입 분포. 예제의 영속 매퍼가 문자열 구간을 따로 처리하는 줄. 그리고 모듈 레지스트리에서 이 리프에 의존해도 되는 모듈 수와 예제 README 가 코덱을 쓰지 않는 이유를 적은 세 줄을 출력한 터미널 기록." caption="직접 만드는 곳은 열다섯 파일 스물한 줄 · GraphQL 스칼라는 정규형 정규식으로 먼저 거름 · 컬럼 변환 애너테이션은 스물세 파일 서른여덟 개이고 붙은 필드는 전부 UUID · 리프에 의존해도 되는 모듈은 예순둘 중 둘 — 43줄 · exit 0" zoom="true" -::: - -문자열에서 UUID 를 만드는 일은 리프 밖 프로덕션 열다섯 파일에서 스물한 줄이 한다. 애플리케이션 코어의 파일·업로드 식별자, 세 메시징 리프의 매퍼, GraphQL 의 UUID 스칼라, 몽고의 커서 코덱, 알림의 라우팅 계획 코덱, JPA 의 멱등 청구 저장소, 예제의 웹 컨트롤러 넷과 영속 매퍼 둘이다. - -열다섯이 모두 같은 일을 하지는 않는다. - -```java - *

{@link UUID#fromString} is lenient — it happily accepts {@code "1-1-1-1-1"} — so accepting - * whatever it parses would make the wire contract depend on a JDK quirk and let two different - * strings denote the same identifier. The canonical form is enforced explicitly instead. -``` - -GraphQL 스칼라는 표준 파서의 관대함을 알고 정규형 정규식으로 먼저 거른다. 이쪽을 코덱으로 바꾸면 검사가 느슨해진다. - -## 컬럼 변환은 다른 구간을 잇는다 - -```text -# @JdbcTypeCode(SqlTypes.UUID) 를 단 프로덕션 파일 : 23 -# 그 애너테이션 개수 : 38 -# 그 애너테이션이 붙은 필드의 타입 : {'UUID': 38} -``` - -애너테이션이 잇는 것은 UUID 값과 PostgreSQL 컬럼 사이다. 문서가 `toUuid`/`fromUuid` 의 목적으로 적은 두 구간 중 문자열과 UUID 사이는 여기에 없고, 위의 스물한 줄이 각자 처리한다. 예제의 영속 매퍼가 두 겹을 한 파일에서 보여 준다 — 엔티티는 애너테이션으로 컬럼을 잇고, 매퍼가 표준 파서로 문자열을 잇는다. - -```java -public static UUID toUuid(WorkLogId id) { - return UUID.fromString(id.value()); -``` - -## 이 분산은 배치 규칙의 결과다 - -```text -# 전체 62 중 2 : app-bootstrap, sample-portfolio -``` - -모듈 레지스트리에서 이 리프에 의존해도 되는 모듈은 둘뿐이다. 스물한 줄 중 열세 줄은 규칙을 먼저 바꾸지 않으면 그 타입을 부를 수 없다. 남는 여덟 줄 중 둘에 대해서는 예제 README 가 이유를 적어 두었다. - -```text -- 36자 canonical UUID 와 PostgreSQL native `uuid`(128비트)를 서로 변환합니다. `UuidCodec` 같은 공용 - 코덱이 아니라 JDK `java.util.UUID` 를 **직접** 쓰는 이유: 영속 어댑터는 경계 규칙상 - `adapter-outbound`(코덱이 있는 곳)에 의존하면 안 되기 때문입니다(stdlib 이라 의존 문제 자체가 없음). -``` - -모듈을 그 자리에 둔 규칙이 그 모듈의 능력을 부를 수 없게 만든다. - -## 남는 선택지 - -이 타입은 서른 줄짜리 유틸이고 자기 명세를 통과한다. 논거에서 그 능력을 빼거나, 배치 규칙을 다시 여는 것이 남는다. 단일 경로로 올리는 쪽은 규칙 변경이 선행이고, 그 다음에는 같은 문서 다음 절이 다루는 정규화 문제가 온다. - -이 기록이 센 것은 소비자의 유무다. 단일 경로 여부를 물으면 앞의 두 타입도 통과하지 못한다 — 리프 밖 프로덕션 스물다섯 파일이 표준 라이브러리 생성기를 직접 부른다. - -## 확인하지 못한 것 - -스물한 줄 중 열세 줄은 모듈 의존 규칙상 이 타입을 부를 수 없어 동작 비교를 물을 단계가 아니다. 규칙이 허용하는 여덟 줄에 대해서만 대체 시 동작이 같은지 확인하지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/identity-and-identifier/case/case-a07-f002-normalize.md b/docs/clean-architecture-backend-template/tech-log-studio/identity-and-identifier/case/case-a07-f002-normalize.md deleted file mode 100644 index 47276a8..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/identity-and-identifier/case/case-a07-f002-normalize.md +++ /dev/null @@ -1,208 +0,0 @@ ---- -kind: CASE -slug: a07-f002-normalize -title: 정규형에서 한 글자를 지운 입력이 거부되지 않고 정규형으로 채워진다 -topic: identity-and-identifier -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a07-f002-normalize -evidenceCapturedOn: 2026-09-02 -body: case-a07-f002-normalize.body.md -assets: - - key: a07-f002-normalize - file: ../../../final/evidence/rendered/a07-f002-normalize.svg - - key: a07-f002-normalize-probe - file: ../../../final/evidence/rendered/a07-f002-normalize-probe.svg -evidence: - - ../../../final/evidence/raw/a07-f002-normalize.txt - - ../../../final/evidence/raw/a07-f002-normalize-probe.txt -source: - - 원본 분석 절은 final/document.md#a07#L89 이다. 등급은 P2 다. 계약과 구현의 어긋남, 표준 파서가 다섯 그룹을 길이 검사 없이 받는다는 사실, 탐침이 보인 재작성, 명세의 거부 케이스가 하나뿐인 이유, 관대함이 남은 자리가 신뢰 경계라는 판정, 도달성이 0 이라는 사실, 그리고 수정 두 가지가 그 절에 있다. - - 어느 글자를 지우느냐에 따라 같은 값이 되기도 하고 다른 값이 되기도 한다는 것, 길이가 36 이어도 대시 위치가 다르면 관대한 경로로 떨어진다는 것, 초과 자릿수가 상위 비트를 버린 채 통과한다는 것, 부호와 비라틴 숫자가 받아들여진다는 것, 그리고 UUID 스칼라의 관문이 세 진입점을 모두 지난다는 것은 이 기록에서 확인했다. - - 그 절의 수정안 첫 절은 길이 36 검사다. 위의 36자 반례가 그것만으로는 부족함을 보인다. ---- - -# 정규형에서 한 글자를 지운 입력이 거부되지 않고 정규형으로 채워진다 - -정규화 메서드의 계약은 정규형만 받고 형식 오류는 예외로 거부한다고 적는다. 구현이 위임하는 표준 파서는 대시로 나뉜 다섯 그룹이면 각 그룹의 자릿수를 보지 않는다. 거부됐어야 할 입력이 앞을 0 으로 채운 정규형 식별자로 돌아온다. - -## 관계 - -- **UuidCodec 의 메서드를 부르는 줄은 자기 명세 다섯뿐이다** - 이 결함이 아직 노출되지 않은 이유다. -- **sanitize가 아니라 reject가 기본이다** - 이 계약이 따르겠다고 적은 규칙이다. -- **그 속성을 보일 수 없는 대역 위에서 통과한 테스트는 증인이 아니다** - 기존 명세가 이것을 놓친 이유다. - -## 문제 - -계약은 자바독과 README 양쪽에 적혀 있다. 대소문자를 가리지 않는 정규형 문자열을 받아 36자 소문자형을 돌려주고, 형식이 잘못된 값에는 예외를 던진다는 것이다. - -구현은 한 줄이다. 표준 라이브러리의 문자열 파서를 부르고 결과를 다시 문자열로 만든다. - -## 결론 - -빠른 경로 조건은 길이 36 과 대시 위치 8·13·18·23 이고, 거기서 벗어난 값은 다섯 그룹으로 잘라 각각 수로 읽힌다. 자릿수는 검사하지 않고, 규정 자릿수를 넘으면 상위 비트를 버린 채 통과시킨다. - -실행으로 확인했다. 정규형 36자에서 마지막 그룹의 한 글자를 지운 입력이 예외 없이 통과하고, 지운 자리 앞을 0 으로 채운 정규형 문자열이 돌아온다. - -어느 글자를 지우느냐가 결과를 가른다. 앞자리 0 은 지워도 수가 같아서 결과가 입력과 일치한다. 그 외 자리를 지우면 형식이 올바른 다른 식별자가 돌아온다. 대시를 지운 값은 예외로 떨어진다. - -두 번째가 이 결함의 실질이다. 잘린 식별자가 거부되지 않고 다른 대상을 가리키는 식별자가 된다. - -길이만으로는 막히지 않는다. 길이가 36 이어도 대시 위치가 다르면 같은 관대한 경로로 떨어진다. - -받아들이는 범위가 십육진으로 한정되지도 않는다. 부호가 붙은 값과 아라비아·인도 숫자가 통과한다. - -거부 자체는 작동한다. 대시를 뺀 32자나 UUID 형태가 아닌 값은 예외로 떨어진다. 계약과 어긋나는 지점은 거부 기준이다. - -관대함이 이 메서드에 갇혀 있지도 않다. 같은 파서를 쓰는 형제 메서드가 재작성된 128비트 값을 그대로 호출자에게 넘긴다. - -기존 명세가 이것을 놓친 이유도 코드에 있다. 거부 단언이 하나뿐이고, 그 문자열은 대시 그룹이 다섯이 아니라 관대한 경로에 닿지 않는다. - -호출자가 준 텍스트를 저장 형태로 바꾸는 지점이 이 메서드다. 관대함이 남은 자리가 하필 신뢰 경계다. 다만 지금 이 메서드를 부르는 프로덕션 코드가 없어 노출은 0 이고, 그 타입을 단일 경로로 올리는 순간 결함이 된다. - -수정은 파서에 넘기기 전에 정규형 정규식으로 거르는 것이다. 길이 검사만으로는 부족하다. 또는 계약 문구를 실제 동작에 맞추는 것이다. 앞의 것이 문서가 말하는 바다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 실행 탐침 -소스 수정 : x - -## 재현 조건 - -1. 클래스 산출물이 현재 소스보다 새로운지 확인한다. -2. 자바독과 README 의 계약 문구를 읽는다. -3. 정규형 36자에서 마지막 그룹의 선행 0, 마지막 글자, 중간 글자, 그리고 대시를 각각 하나씩 지워 넣는다. -4. 각 결과가 원본과 같은지 비교한다. -5. 길이가 36 이면서 대시 위치가 다른 문자열을 넣는다. -6. 그룹이 규정 자릿수를 넘는 문자열과 부호·비라틴 숫자를 넣는다. -7. 대시가 없는 32자와 UUID 가 아닌 문자열을 넣는다. -8. 같은 파서를 쓰는 형제 메서드에 같은 입력을 넣는다. -9. 명세의 거부 단언 수를 센다. -10. 같은 저장소에서 이 파서를 쓰면서 먼저 거르는 코드를 찾는다. - -## 본문 - - - -계약은 자바독과 README 양쪽에 적혀 있고, 구현은 한 줄이다. - -## 계약과 한 줄 - -:::evidence key="a07-f002-normalize" alt="정규화 메서드의 자바독 계약과 한 줄짜리 구현, 같은 계약이 적힌 README 두 줄. 같은 파서를 쓰는 형제 메서드. 명세의 거부 케이스와 그 수. 그리고 같은 저장소의 GraphQL UUID 스칼라가 쓰는 정규형 정규식 상수와 파서보다 먼저 그것을 돌리는 관문, 그 관문을 지나는 호출 지점들을 출력한 터미널 기록." caption="계약은 형식 오류에 예외를 던진다고 적음 · 구현은 표준 파서 호출 한 줄 · 명세의 거부 단언은 하나 · UUID 스칼라는 정규식 상수를 두고 파서보다 먼저 돌린다 — 47줄 · exit 0" zoom="true" -::: - -```java -/** - * Accepts a case-insensitive canonical UUID string and returns the canonical 36-character - * lowercase form; {@code null} input returns {@code null}. - * - * @throws IllegalArgumentException on a malformed UUID - */ -public static String normalize(String input) { - if (input == null) { - return null; - } - return UUID.fromString(input).toString(); -} -``` - -표준 파서는 길이 36 이면서 대시가 8·13·18·23 에 있는 빠른 경로를 벗어나면, 대시로 나뉜 다섯 그룹을 각각 수로 읽는다. 자릿수는 검사하지 않는다. - -## 마지막 그룹에서 한 글자를 지운 입력 - -:::evidence key="a07-f002-normalize-probe" alt="클래스 산출물이 소스보다 새로운지 확인한 결과와 JVM 판본. 계약이 말하는 두 갈래의 입력을 넣은 결과. 정규형 36자에서 선행 0·마지막 글자·중간 글자·대시를 각각 하나씩 지운 입력의 결과와 원본과의 동일 여부. 길이가 36 이면서 대시 위치가 다른 문자열, 그룹이 규정 자릿수를 넘는 문자열, 부호와 비라틴 숫자를 넣은 결과. 서로 다른 다섯 입력이 어떤 식별자들로 모이는지. 그리고 같은 파서를 쓰는 형제 메서드의 결과를 출력한 터미널 기록." caption="선행 0 을 지우면 원본과 같은 값, 다른 자리를 지우면 다른 값, 대시를 지우면 예외 · 길이 36 이어도 대시 위치가 다르면 통과 · 초과 자릿수는 상위 비트를 버림 · 부호와 아라비아·인도 숫자도 통과 · 다섯 입력이 두 식별자로 모임 — 36줄 · exit 0" zoom="true" -::: - -정규형 36자에서 자리를 바꿔 가며 한 글자씩 지워 넣었다. - -```text - normalize("0190bd6e-7c3e-7abc-8def-123456789ab") -> "0190bd6e-7c3e-7abc-8def-0123456789ab" - 원본과 같은가 : true - normalize("0190bd6e-7c3e-7abc-8def-0123456789a") -> "0190bd6e-7c3e-7abc-8def-00123456789a" - 원본과 같은가 : false - normalize("0190bd6e7c3e-7abc-8def-0123456789ab") -> IllegalArgumentException: Invalid UUID string: … - 원본과 같은가 : false -``` - -세 결과가 다르다. 선행 0 을 지우면 값이 같아 원본이 돌아온다. 마지막 글자를 지우면 앞을 0 으로 채운 **다른** 식별자가 돌아온다. 대시를 지우면 예외가 난다. - -가운데가 이 결함의 실질이다. 잘린 문자열이 거부되지 않고, 형식이 올바르면서 다른 대상을 가리키는 식별자가 된다. - -## 길이 검사로는 막히지 않는다 - -```text - normalize("0000001-00001-0001-0001-000000000001") -> "00000001-0001-0001-0001-000000000001" -``` - -이 입력은 36자다. 대시 위치가 다를 뿐인데 빠른 경로를 벗어나 관대한 경로로 떨어진다. `length() != 36` 만 검사하는 수정은 이것을 통과시킨다. - -받는 것이 십육진에 한정되지도 않는다. - -```text - normalize("100000001-1-1-1-1") -> "00000001-0001-0001-0001-000000000001" - normalize("+1-1-1-1-1") -> "00000001-0001-0001-0001-000000000001" - normalize("١-1-1-1-1") -> "00000001-0001-0001-0001-000000000001" -``` - -첫 줄은 아홉 자리 그룹이고, 상위 비트가 버려진 채 통과한다. 나머지 둘은 부호와 아라비아·인도 숫자다. - -거부가 고장 난 것은 아니다. 대시 없는 32자와 UUID 가 아닌 문자열은 예외를 던진다. 계약과 다른 것은 거부의 기준이다. - -## 서로 다른 입력이 모이는 자리 - -```text - 00000001-0001-0001-0001-000000000001 <- "1-1-1-1-1", "01-01-01-01-01", "+1-1-1-1-1", "00000001-0001-0001-0001-000000000001" - 00000001-0001-0001-0001-000000000002 <- "1-1-1-1-2" -``` - -넷이 하나로 모이고, 다섯째는 따로 선다. 결과만 보고는 앞의 셋이 넷째와 다른 문자열이었다는 것을 알 수 없다. - -같은 파서를 쓰는 형제 메서드도 같은 값을 낸다. 관대함은 정규화 메서드에 갇혀 있지 않고, 재작성된 128비트 값이 그대로 호출자에게 간다. - -```text - toUuid("1-1-1-1-1") = 00000001-0001-0001-0001-000000000001 -``` - -## 명세의 거부 케이스 - -```groovy -def "normalize 는 형식이 잘못된 UUID 를 거부한다"() { - when: - UuidCodec.normalize("not-a-uuid") - - then: - thrown(IllegalArgumentException) -} -``` - -명세 전체의 거부 단언은 이 하나다. 그 문자열은 대시 그룹이 다섯이 아니라 관대한 경로에 닿지 않는다. - -## UUID 스칼라는 파서보다 정규식을 먼저 돌린다 - -같은 저장소의 다른 리프가 같은 파서를 쓰면서 계약을 문구가 아니라 코드로 지킨다. - -```java -public static UUID parse(String value) { - if (value == null || !CANONICAL.matcher(value).matches()) { - throw new CoercingParseValueException("invalid UUID"); - } -``` - -`CANONICAL` 은 정규형 정규식 상수이고, 값을 받는 세 진입점이 모두 이 한 메서드를 지난다. 수정에 필요한 정규식이 이미 저장소 안에 있다. - -## 지금은 아무도 부르지 않는다 - -이 타입의 메서드를 부르는 줄은 저장소 전체에서 자기 명세 다섯뿐이다. 프로덕션 호출자가 없으므로 현재 노출은 0 이다. - -이 메서드는 호출자가 준 텍스트를 저장 형태로 바꾸는 지점이다. 관대함이 남은 자리가 신뢰 경계라는 것이 이 어긋남의 무게이고, 그 무게는 이 타입을 문자열에서 UUID 로 가는 단일 경로로 올리는 순간 실현된다. - -## 확인하지 못한 것 - -표준 라이브러리 판본에 따라 관대한 경로가 달라지는지 확인하지 않았다. 확인한 것은 이 프로젝트가 쓰는 판본이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/identity-and-identifier/case/case-a07-f004-claude.md b/docs/clean-architecture-backend-template/tech-log-studio/identity-and-identifier/case/case-a07-f004-claude.md deleted file mode 100644 index 1692a62..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/identity-and-identifier/case/case-a07-f004-claude.md +++ /dev/null @@ -1,148 +0,0 @@ ---- -kind: CASE -slug: a07-f004-claude -title: 문서가 선언됐다는 둘은 없고, 선언된 하나는 그 문장에 없다 -topic: identity-and-identifier -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a07-f004-claude -evidenceCapturedOn: 2026-09-02 -assets: - - key: a07-f004-claude - file: ../../../final/evidence/rendered/a07-f004-claude.svg -evidence: - - ../../../final/evidence/raw/a07-f004-claude.txt -source: - - 원본 분석 절은 final/document.md#a07#L131 이다. 등급은 P3 이다. 선언 블록이 두 줄이라는 사실, 도메인 코어와 외부 라이브러리가 선언돼 있지 않다는 판정, 애플리케이션 코어가 언급되지 않았다는 지적, 그리고 실제 선언이 허용 목록의 부분집합이라는 결론이 그 절에 있다. - - 그 절은 문서 인용에서 `:application-core` 를 `:application-code` 로 적었다. 원문은 `core` 다. - - 앞 문장의 공유 계약이 강제되는 허용 목록에 없다는 것은 이 기록이 새로 확인했다. 그 라이브러리가 이 리프의 클래스패스에 없다는 판정은 그 절에 있고, 여기서는 잠금 파일의 빈 구성 표기로 그것을 다시 확인했다. ---- - -# 문서가 선언됐다는 둘은 없고, 선언된 하나는 그 문장에 없다 - -리프 지침 문서의 의존성 문단은 두 문장이다. 앞 문장이 든 허용 목록 셋 중 하나는 강제되는 레지스트리에 없다. 뒤 문장이 선언돼 있다고 적은 둘은 어느 쪽도 없고, 실제 선언 하나는 그 문장에 없다. - -## 관계 - -- **사라진 가드가 문서에만 남았고 그 부재는 이미 승인된 결정이다** - 같은 문서의 다른 오류다. -- **README의 세 가지 사실 오류** - 같은 리프의 문서 오류다. -- **허용 의존 목록은 상한이므로 미사용을 잡지 않는다** - 실제 선언이 그 상한의 부분집합인 이유다. - -## 문제 - -지침 문서에서 허용을 적은 절은 두 문장이다. 앞 문장은 이 리프가 의존할 수 있는 것을 셋으로 적고, 뒤 문장은 지금 빌드 파일에 실제로 선언돼 있다는 것을 적는다. 뒤 문장이 드는 둘 중 하나는 앞 목록의 항목이고 다른 하나는 그 목록에 없는 외부 라이브러리다. - -## 결론 - -빌드 파일의 선언 블록은 두 줄이다. 애플리케이션 코어 프로젝트와 Spock 시험 라이브러리다. - -뒤 문장이 든 둘은 어느 쪽도 거기 없다. 도메인 코어는 표기를 가리지 않고 세어도 빌드 파일에 0 건이다. 외부 UUID 생성 라이브러리는 이 리프의 잠금 파일에 없고, 그 잠금 파일은 컴파일과 런타임 클래스패스가 비어 있다고 적는다. 이 저장소는 모든 구성을 엄격 모드로 잠그므로, 그 라이브러리가 시험 경로에만 없는 것이 아니라 프로덕션 경로 자체에 없다. 실제로 그것을 선언하는 곳은 예제 애플리케이션 한 줄이다. - -실제로 선언된 애플리케이션 코어는 그 문장에 나오지 않는다. only 라고 쓴 이상 하나를 빠뜨린 것도 위반이다. - -앞 문장은 셋 중 둘이 맞고 하나가 어긋난다. 강제되는 목록은 모듈 레지스트리에 있고 거기 적힌 것은 도메인 코어와 애플리케이션 코어다. 문서가 든 셋 중 공유 계약은 그 목록에 없다. - -목록의 성격은 서술이 아니라 게이트다. 루트 빌드 파일이 레지스트리에서 허용 간선 표를 파생하고, 허용 밖 간선을 만나면 빌드를 실패시키며, 모든 리프의 검사가 그 태스크에 의존한다. 공유 계약을 실제로 선언하면 빌드가 멈춘다. - -빌드 자체는 정합하다. 실제 선언이 허용 목록의 부분집합이다. 어긋난 범위는 문단 하나이고, 그 안의 진술 넷이 사실과 다르다. 판정은 P3 다. - -## 검증 환경 - -Gradle : 9.0.0 -확인 방식 : 지침 문서와 빌드 파일·잠금 파일·모듈 레지스트리 대조 -소스 수정 : x - -## 재현 조건 - -1. 리프 지침 문서의 허용 절 두 문장을 읽는다. -2. 빌드 파일의 선언 블록을 연다. -3. 도메인 코어를 표기를 가리지 않고 빌드 파일에서 센다. -4. 외부 라이브러리를 리프 잠금 파일에서 세고, 그 잠금 파일이 덮는 구성과 비어 있는 구성을 읽는다. -5. 잠금 모드 설정을 확인한다. -6. 그 라이브러리를 실제로 선언하는 곳을 찾는다. -7. 모듈 레지스트리에서 이 리프의 허용 목록을 읽고 앞 문장의 셋과 대조한다. -8. 그 목록이 어떻게 강제되는지 사슬을 따라간다. - -## 본문 - - - -지침 문서의 허용 절은 두 문장이다. - -## 허용 절과 선언 블록 - -:::evidence key="a07-f004-claude" alt="리프 지침 문서의 허용 절과 빌드 파일의 선언 블록. 도메인 코어를 빌드 파일에서, 외부 UUID 생성 라이브러리를 리프 잠금 파일에서 각각 센 결과와 그 잠금 파일이 덮는 구성 목록, 비어 있는 구성, 잠금 모드 설정. 그 라이브러리를 실제로 선언하는 곳. 그리고 모듈 레지스트리의 허용 목록과 문서가 든 셋의 차이, 그 목록을 강제하는 코드 사슬을 출력한 터미널 기록." caption="문서는 도메인 코어와 uuid-creator 가 선언됐다고 적음 · 선언 블록은 애플리케이션 코어와 Spock 두 줄 · 빌드 파일의 도메인 코어 언급 0, 잠금 파일의 uuid-creator 0이고 컴파일·런타임 클래스패스는 비어 있음 · 레지스트리 허용 목록에 공유 계약 없음 · 허용 밖 간선은 빌드를 실패시킴 — 39줄 · exit 0" zoom="true" -::: - -```text -- `:application-core`, `:domain-core`, `:shared-contract` (Gradle matrix). Currently - only `:domain-core` + `com.github.f4b6a3:uuid-creator` are declared in - [build.gradle](build.gradle). -``` - -선언 블록은 두 줄이다. - -```groovy -dependencies { - implementation project(':application-core') - - testImplementation 'org.spockframework:spock-core:2.4-groovy-5.0' -} -``` - -## 뒤 문장의 두 이름 - -```text -# 빌드 파일의 domain-core 언급(모든 표기) : 0 -# 잠금 파일 줄 수 / uuid-creator·f4b6a3 : 154 / 0 -# 프로덕션 클래스패스의 잠금 상태 : empty=compileClasspath,runtimeClasspath - 336: lockAllConfigurations() - 337: lockMode = LockMode.STRICT -``` - -도메인 코어는 어떤 표기로도 빌드 파일에 없다. 외부 라이브러리 쪽은 잠금 파일이 더 강하게 말한다 — 이 저장소는 모든 구성을 엄격 모드로 잠그고, 이 리프의 컴파일·런타임 클래스패스는 잠긴 채 비어 있다. 시험 경로에만 없는 것이 아니라 프로덕션 경로 자체에 아무것도 없다. - -그 라이브러리를 선언하는 곳은 따로 있다. - -```text - sample-portfolio/build.gradle:52: implementation 'com.github.f4b6a3:uuid-creator:6.1.1' -``` - -선언 블록의 유일한 프로젝트 의존은 그 문장에 나오지 않는다. 문장이 `only` 라고 적어 선언 전체를 배타적으로 주장했으므로, 빠뜨린 것도 어긋남이다. - -## 앞 문장의 세 이름 - -```text -# 레지스트리 allowed_dependencies : ['domain-core', 'application-core'] -# 문서가 든 셋 중 목록에 없는 것 : ['shared-contract'] -``` - -레지스트리가 이 리프에 허용한 것은 둘이다. 셋째 항목은 여기 없다. - -이 목록은 서술이 아니라 게이트다. - -```groovy -1419: Map> allowedProjectDependencies = registry.modules.collectEntries { module -> -... - Set forbidden = actual - allowed - if (!forbidden.isEmpty()) { - throw new GradleException( -... - 577: dependsOn rootProject.tasks.named('verifyCleanArchitectureDependencies') -``` - -루트 빌드 파일이 레지스트리에서 허용 간선 표를 파생하고, 허용 밖 간선을 만나면 빌드를 실패시킨다. 모든 리프의 검사가 그 태스크에 의존하므로 이 리프도 지난다. 문서가 매트릭스라고 적은 셋째 항목을 실제로 선언하면 빌드가 멈춘다. - -## 실제 선언과 허용 목록 - -실제 선언은 허용 목록 안에 있다. 간선 쪽에서 고칠 것은 없다. 어긋난 것은 문단 하나이고, 그 문단이 담은 진술 넷이 사실과 다르다. - -## 확인하지 못한 것 - -문서가 든 두 이름이 과거 어느 시점에 실제로 선언되어 있었는지 확인하지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/identity-and-identifier/case/case-a07-f006-claude.md b/docs/clean-architecture-backend-template/tech-log-studio/identity-and-identifier/case/case-a07-f006-claude.md deleted file mode 100644 index dc94c6d..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/identity-and-identifier/case/case-a07-f006-claude.md +++ /dev/null @@ -1,157 +0,0 @@ ---- -kind: CASE -slug: a07-f006-claude -title: 사라진 가드가 문서에만 남았고 그 부재는 이미 승인된 결정이다 -topic: identity-and-identifier -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a07-f006-claude -evidenceCapturedOn: 2026-09-02 -assets: - - key: a07-f006-claude - file: ../../../final/evidence/rendered/a07-f006-claude.svg -evidence: - - ../../../final/evidence/raw/a07-f006-claude.txt -source: - - 원본 분석 절은 final/document.md#a07#L160 이다. 등급은 P3 이다. 아키텍처 규칙이 실재한다는 확인, 훅 파일과 그 디렉터리가 추적되지 않는다는 판정, `.claude/` 에 로컬 설정 파일 하나뿐이라는 관측, 그리고 복제본에서는 그 서술이 성립하지 않는다는 결론이 그 절에 있다. - - 이 기록이 더한 것은 넷이다. 무시 목록이 그 디렉터리를 덮고 전 이력에서 추적된 적이 없다는 것, 문서가 특정한 규칙 번호가 해석되지 않는다는 것, 남은 규칙이 같은 줄의 Spring Web 을 덮지 않는다는 것, 그리고 그 훅을 고치겠다던 계획이 폐기되고 대체 문서가 하네스 부재를 목표에 적었다는 것이다. ---- - -# 사라진 가드가 문서에만 남았고 그 부재는 이미 승인된 결정이다 - -리프 지침 문서가 금지 사항의 근거로 가드 둘을 든다. 아키텍처 규칙 쪽은 실재하고 CI 에서 돈다. 훅 스크립트 쪽은 이 저장소에 없고, 없는 이유가 다른 문서에 결정으로 적혀 있다. - -## 관계 - -- **문서가 선언됐다는 둘은 없고, 선언된 하나는 그 문장에 없다** - 같은 문서의 다른 오류다. -- **산문이 선언한 게이트는 빌드에 있는 게이트가 아니다** - 이 사례가 그 규칙의 형태다. -- **release gate가 실제로 차단하는 것은 hermetic test 3개이고, mongo용 CI workflow는 없다** - 릴리스 게이트가 실제로 무엇을 막는지 센 문서다. - -## 문제 - -지침 문서가 이 리프에 금지된 것을 나열한다. 첫 항목은 퍼시스턴스와 웹 기술이고, 거기에 강제 근거가 둘 붙는다. 아키텍처 규칙 이름 하나와, 훅 스크립트의 규칙 번호 하나다. - -## 결론 - -아키텍처 규칙은 실재하고 실제로 돈다. 시험 상수에 애너테이션이 붙어 있고 클래스 단위 분석 대상이 저장소 전체이며, 조립 루트가 이 리프를 물고 있어 검사 대상이 비어 있지 않다. CI 품질 게이트가 그 검사를 포함한 태스크를 돌린다. - -다만 그 규칙이 막는 이름 목록에 Spring Web 이 없다. 같은 줄이 함께 금지한 기술이고, 사라진 쪽 가드가 덮기로 돼 있던 것이다. - -그 기술들을 이 리프 밖에 두는 첫 방어선은 규칙이 아니다. 잠금 파일이 컴파일과 런타임 클래스패스가 비어 있다고 적는다. 컴파일에서 이미 불가능하고, 아키텍처 규칙은 그 뒤에 선다. - -훅 스크립트 쪽은 저장소에 없다. 그 디렉터리 아래 추적되는 파일이 0 이고, 전 이력에서도 추적된 적이 없다. 무시 목록 셋째 줄이 그 디렉터리를 덮는다. 작업 트리에 남아 있는 것은 로컬 설정 파일 하나이고 훅 디렉터리는 만들어진 적이 없다. - -규칙 번호 쪽도 해석되는 대상이 없다. 그 번호를 쓰는 곳은 자기 자신 말고 없다. - -없는 이유는 무시 목록이 아니라 다른 문서에 있다. 그 스크립트를 고치겠다던 계획은 첫 줄에 폐기가 적혀 있고, 그것을 대체한 개정안의 목표 문장이 부재한 개발 하네스를 되살리지 않은 채 Gradle 과 아키텍처 규칙으로만 의존성 강제를 복구하겠다고 적는다. 계획이 약속한 디렉터리 역시 존재하지 않는다. - -그러니 이것은 빠뜨린 파일이 아니라 이미 내려진 결정이다. 그 결정 이전의 문장을 그대로 들고 있는 것이 리프 지침 문서다. 판정은 P3 다. - -## 검증 환경 - -확인 방식 : 지침 문서와 저장소 추적 목록·전 이력·무시 규칙·계획 문서 대조 -소스 수정 : x - -## 재현 조건 - -1. 리프 지침 문서의 금지 절과 첫 항목에 붙은 근거 둘을 읽는다. -2. 아키텍처 규칙의 선언과 그것이 막는 이름 목록을 읽는다. -3. 그 규칙이 실행되는 경로와 검사 대상이 비어 있지 않은지 확인한다. -4. 리프 잠금 파일의 빈 구성 표기를 읽는다. -5. 훅이 있다는 디렉터리의 추적 파일 수와 전 이력 추적 여부와 무시 규칙을 확인한다. -6. 문서가 특정한 규칙 번호가 저장소에서 해석되는지 센다. -7. 그 훅을 고치겠다던 계획 문서의 첫 줄과 그것을 대체한 문서의 목표를 읽는다. - -## 본문 - - - -지침 문서가 금지 사항 첫 항목에 강제 근거를 둘 붙인다. - -## 금지 절과 근거 두 줄 - -:::evidence key="a07-f006-claude" alt="리프 지침 문서의 금지 절과 첫 항목에 붙은 근거 둘. 아키텍처 규칙의 선언과 그것이 막는 이름 목록, 그 규칙의 실행 경로와 검사 대상, 리프 잠금 파일의 빈 구성 표기. 훅이 있다는 디렉터리의 추적 파일 수와 전 이력 추적 여부, 작업 트리 내용, 무시 규칙 확인 결과. 문서가 특정한 규칙 번호가 저장소에 나오는 곳. 그리고 그 훅을 고치겠다던 계획 문서의 폐기 배너와 그것을 대체한 문서의 목표 문장을 출력한 터미널 기록." caption="아키텍처 규칙은 실재하고 CI 의 check 로 돌지만 막는 목록에 Spring Web 이 없음 · 리프의 컴파일·런타임 클래스패스는 잠긴 채 비어 있음 · 훅 디렉터리는 추적 0, 전 이력 0, 무시 목록에 포함 · 규칙 번호는 이 문장에만 나옴 · 그 훅을 고치겠다던 계획은 폐기됐고 대체 문서가 하네스 부재를 목표에 적음 — 54줄 · exit 0" zoom="true" -::: - -```text -## Forbidden - -- Persistence or web technology (JPA/Hibernate/Spring Data/Spring Web) — ArchUnit - `identifier_adapter_does_not_depend_on_other_adapters_or_bootstrap`; - `.claude/hooks/ca_import_gate.py` G4 가 쓰기 시점에 차단. -``` - -## 아키텍처 규칙은 있고 CI 에서 돈다 - -```java -@ArchTest -static final ArchRule IDENTIFIER_ADAPTER_DOES_NOT_DEPEND_ON_OTHER_ADAPTERS_OR_BOOTSTRAP = -``` - -선언만으로는 돈다고 말할 수 없어 실행 경로도 봤다. 클래스에 `@AnalyzeClasses(packages = "dev.caskeleton")` 가 붙어 있고, 조립 루트가 이 리프를 `implementation` 으로 물고 있어 검사 대상이 비어 있지 않으며, CI 품질 게이트가 `./gradlew check` 를 돌린다. - -## 남은 규칙이 덮지 않는 것 - -```java -"..adapter.inbound.web..", -"..adapter.outbound.persistence..", -"..bootstrap..", -"org.springframework.data.repository..", -"org.springframework.data.jpa.repository..", -"jakarta.persistence..", -"javax.persistence..", -"org.hibernate.." -``` - -금지 줄이 함께 든 Spring Web 이 이 목록에 없다. 사라진 가드가 덮기로 돼 있던 것 중 하나를 남은 가드가 덮지 않는다. - -그리고 이 리프에 그 기술들이 들어오지 못하게 하는 첫 줄은 규칙이 아니다. - -```text - 154:empty=compileClasspath,runtimeClasspath -``` - -프로덕션 클래스패스가 잠긴 채 비어 있으므로 컴파일에서 이미 불가능하다. 아키텍처 규칙은 그 뒤에 선 둘째 줄이다. - -## 훅이 있다는 디렉터리 - -```text -# git ls-files .claude : 0 -# 전 이력에서 .claude 아래 추적된 적 : 0 -# 작업 트리 : settings.local.json - .gitignore:3:.claude/ .claude/hooks/ca_import_gate.py -``` - -추적된 적이 한 번도 없고, 무시 목록이 그 디렉터리를 덮는다. 작업 트리에도 로컬 설정 파일 하나뿐이고 훅 하위 디렉터리 자체가 없다. - -문서가 특정한 규칙 번호도 해석되지 않는다. - -```text - src/adapter/outbound/identifier/CLAUDE.md:39: `.claude/hooks/ca_import_gate.py` G4 가 쓰기 시점에 차단. -``` - -`G4` 가 임포트 게이트의 규칙 번호로 쓰인 곳은 이 문장 자신뿐이다. 없는 파일 안의, 정의되지 않은 번호를 특정한다. - -## 없는 이유는 이미 적혀 있다 - -```text -> **SUPERSEDED — HISTORICAL PROVENANCE ONLY (2026-07-25):** The user-approved harness-free -> Mode B amendment supersedes this plan. … - -**Goal:** Restore Gradle bootstrap and Clean Architecture dependency enforcement without recreating -the absent development harness. -``` - -그 훅을 고치겠다던 계획은 폐기됐고, 대체한 개정안의 목표 문장이 부재한 개발 하네스를 되살리지 않겠다고 적는다. 그 계획이 만들겠다던 디렉터리도 없다. - -무시 목록은 그 파일이 배포되지 않는 경로를 말할 뿐이다. 왜 없는지는 이 개정안이 말한다. 빠뜨린 파일이 아니라 승인된 결정이고, 그 결정 이전의 문장을 그대로 들고 있는 것이 리프 지침 문서다. - -## 확인하지 못한 것 - -다른 개발자 머신에 그 스크립트가 있는지는 확인하지 않았다. 다만 여기서 본 것은 새 복제본이 아니라 로컬 브랜치와 작업 트리 이력을 가진 실제 작업 저장소이고, 그 한 대에도 훅은 없었다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/identity-and-identifier/case/case-analysis-finding-a07-f005.md b/docs/clean-architecture-backend-template/tech-log-studio/identity-and-identifier/case/case-analysis-finding-a07-f005.md deleted file mode 100644 index 1aad18e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/identity-and-identifier/case/case-analysis-finding-a07-f005.md +++ /dev/null @@ -1,99 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a07-f005 -title: README의 세 가지 사실 오류 -topic: identity-and-identifier -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a07-f005 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a07-f005.body.md -assets: - - key: analysis-finding-a07-f005 - file: ../../../final/evidence/rendered/analysis-finding-a07-f005.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a07-f005.txt -source: - - 원본 분석 절은 final/document.md#a07#L150 이다. ---- - -# README의 세 가지 사실 오류 - -리프 README 의 세 서술이 각각 패키지 루트와 테스트 라이브러리 판본과 의존성 허용 경로에서 실제와 다르다. 셋 다 메커니즘 자체는 실재하고 동작한다. 틀린 것은 이름과 판본이다. - -## 관계 - -- **문서가 선언됐다는 둘은 없고, 선언된 하나는 그 문장에 없다** - 같은 리프의 다른 문서 오류다. -- **사라진 가드가 문서에만 남았고 그 부재는 이미 승인된 결정이다** - 같은 계열이다. -- **문서가 아니라 빌드 파일이 의존성의 진실이다** - 셋째 오류가 그 규칙의 형태다. - -## 문제 - -리프 README 가 패키지 루트와 테스트 스택과 의존성 허용 경로를 적는다. 그 셋을 실제와 대조했다. - -## 결론 - -세 서술이 모두 다르다. - -패키지 루트는 어댑터 다음에 바로 리프 이름이 오는 형태로 적혀 있는데, 실제는 그 사이에 방향 구획이 하나 더 있다. 같은 저장소의 리프 지침 문서 쪽은 정확하다. - -테스트 스택은 Spock 판본에 붙는 그루비 변형을 4 계열로 적는데, 실제 선언은 5 계열이다. - -셋째가 가장 구조적이다. - -README 는 이 리프의 프로젝트 간 의존성 간선이 루트 빌드 파일의 허용 맵으로 허용된다고 적고, 그 맵의 키를 방향 구획이 빠진 이름으로 든다. - -실제 그 허용 맵은 리터럴 맵이 아니다. 레지스트리의 모듈 목록을 순회해 파생된다. 그리고 이 모듈의 키는 방향 구획이 들어간 이름이다. - -즉 키 이름도 틀렸고 그 맵이 어디서 오는지도 틀렸다. - -셋 다 메커니즘 자체는 실재하고 동작한다. 패키지는 있고 테스트는 돌고 간선은 허용된다. 틀린 것은 이름과 판본이다. - -그래서 판정은 P3 다. - -## 검증 환경 - -Gradle : 9.0.0 -확인 방식 : README 와 소스 트리, 빌드 파일 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/140 계열에 있다. - -1. README 의 패키지 루트 줄을 읽고 실제 소스 트리와 대조한다. -2. 테스트 스택 줄을 읽고 빌드 파일의 선언과 대조한다. -3. 의존성 허용 경로 줄을 읽는다. -4. 루트 빌드 파일에서 허용 맵이 어떻게 만들어지는지 확인한다. -5. 레지스트리에서 이 모듈의 키 이름을 확인한다. - -## 본문 - - - -README에 세 가지 사실 오류가 있다. - -| README | 실제 | -|---|---| -| \:3 패키지 루트 `dev.caskeleton.adapter.identifier` | `dev.caskeleton.adapter.outbound.identifier` (CLAUDE.md\:11은 정확) | -| \:59 "Spock 2.4 / **Groovy 4.0** variant" | `spock-core:2.4-groovy-**5.0**` | -| \:64 edge는 `src/build.gradle`의 `allowedProjectDependencies['**adapter-identifier**']`로 허용 | `build.gradle:1416`의 `allowedProjectDependencies`는 리터럴 맵이 아니라 `registry.modules.collectEntries { … }`로 **레지스트리에서 파생**되며, 이 모듈의 키는 `adapter-outbound-identifier`다 | - -## README 의 세 문장 - -:::evidence key="analysis-finding-a07-f005" alt="분석 문서 final/document.md#a07 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a07 발췌 — 15줄" zoom="true" -::: - -## 메커니즘은 실재하고 동작한다 - -틀린 것은 이름과 버전이다. P3. - -## 확인하지 못한 것 - -README 의 서술이 언제부터 어긋났는지 확인하지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/identity-and-value-contracts/case/case-a01-f002-idfactory-newid.md b/docs/clean-architecture-backend-template/tech-log-studio/identity-and-value-contracts/case/case-a01-f002-idfactory-newid.md deleted file mode 100644 index c8aa8dc..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/identity-and-value-contracts/case/case-a01-f002-idfactory-newid.md +++ /dev/null @@ -1,128 +0,0 @@ ---- -kind: CASE -slug: a01-f002-idfactory-newid -title: never-before-used 는 시그니처가 줄 수 없는 보장이다 -topic: identity-and-value-contracts -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a01-f002-idfactory-newid -evidenceCapturedOn: 2026-09-02 -assets: - - key: a01-f002-idfactory-newid - file: ../../../final/evidence/rendered/a01-f002-idfactory-newid.svg -evidence: - - ../../../final/evidence/raw/a01-f002-idfactory-newid.txt -source: - - 분석 문서는 도메인 코어 편 §11 P3 이다. 인터페이스가 저장소 충돌 검사를 요구하지 않고 샘플 테스트도 전역 유일성을 증명하지 않는다는 판정이 그 항목에 있다. 포트와 어댑터의 배선은 §4 에, 어댑터 테스트가 실제로 무엇을 검증하는지는 §9 에 있다. ---- - -# never-before-used 는 시그니처가 줄 수 없는 보장이다 - -식별자 생성 포트의 메서드 문서가 한 번도 쓰인 적 없는 값이라고 적는다. 그 메서드는 인자도 확인 예외도 없어 저장소를 물어볼 수도 충돌을 알릴 수도 없다. 저장소 수준 유일성을 실제로 강제하는 것은 스키마의 기본 키다. - -## 관계 - -- **이름은 값이 아니라 registry key다** - 식별자가 무엇을 보장하는지를 타입 계약으로 적으라는 규칙이다. -- **문서가 UUIDv7이라 말하고 생성되는 것은 v4다** - 다른 리프에서 같은 형태로 나타난 문서와 구현의 거리다. - -## 문제 - -포트 파일은 열다섯 줄이고, 메서드 문서는 한 줄이다. 그 한 줄이 충돌 없는 값을 약속한다. - -그 문장은 두 가지로 읽힌다. 확률적 유일성이거나 저장소 수준 유일성이다. - -## 결론 - -시그니처가 답을 정한다. T newId() 는 인자를 받지 않고 확인 예외도 선언하지 않는다. 질의할 저장소도 실패를 알릴 반환 통로도 시그니처에 없다. - -값 계약도 같은 쪽을 가리킨다. 포트 문서는 그 값을 36자 canonical 형식으로, 규격은 RFC 9562 의 UUIDv7 로 못박는다. UUID 가 주는 유일성은 확률에 기댄 것이다. - -포트를 직접 구현하는 클래스는 없다. 구현은 샘플이 정의한 하위 포트를 거친다. 작업 로그와 포스터의 팩토리가 각각 포트를 확장하고 타입 파라미터만 채우며, 스프링 컴포넌트인 어댑터 둘이 그것을 구현한다. - -어댑터 쪽 구현 자체는 규격대로다. 밀리초 안에서도 단조 증가하는 생성기를 부른다. 그렇다고 저장소를 확인할 자리가 생기지는 않는다. 오버라이드할 시그니처가 바뀌지 않기 때문이다. - -샘플의 다른 팩토리 둘은 이 포트를 확장하지 않는다. 반환형이 포트의 바운드를 만족하지 못한다. 하나는 문자열을 돌려주고, 다른 하나는 두 값을 한 번에 예약하는 레코드를 돌려준다. - -값 객체가 검사하는 것은 8-4-4-4-12 열여섯진수 모양뿐이다. 버전 자리는 보지 않는다. 계약이 지정한 UUIDv7 조차 타입이 강제하지 않는다. - -저장소 수준 유일성이 없는 것은 아니다. 작업 로그와 포스터에서 식별자 열은 기본 키로 잡혀 있다. 중복 삽입을 막는 것은 이 포트가 아니라 스키마다. 포트만 읽고 유일성이 확보됐다고 보면 틀린다. 그 보장은 데이터베이스에서 빌려 온 것이다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 포트 시그니처와 값 계약, 구현 경로 추적, 값 객체의 검사 범위, 마이그레이션의 제약 확인 -소스 수정 : x - -## 재현 조건 - -1. 포트 파일 전문을 읽고 메서드의 인자와 예외 선언을 확인한다. -2. 그 포트가 만드는 값의 계약 문장을 읽는다. -3. 포트를 직접 구현하는 클래스를 센다. 0 이면 하위 포트를 거치는 구현을 찾는다. -4. 그 구현이 무엇을 부르는지, 그리고 시그니처가 바뀌었는지 확인한다. -5. 값 객체의 정규식이 무엇을 검사하는지 읽는다. -6. 그 식별자가 들어가는 테이블의 제약을 마이그레이션에서 찾는다. - -## 본문 - - - -식별자 생성 포트는 열다섯 줄이다. 그중 13행의 메서드 문서 한 줄이 이 사례의 대상이다. - -## 포트가 줄 수 있는 것은 시그니처가 정한다 - -:::evidence key="a01-f002-idfactory-newid" alt="식별자 생성 포트의 파일 전문과 그것이 만드는 값의 계약, 포트를 직접 구현하는 클래스 수와 샘플이 정의한 팩토리 인터페이스 넷, 그중 포트를 확장한 둘을 구현하는 어댑터, 확장하지 않는 둘의 반환형, 실제 구현이 부르는 생성기, 값 객체가 검사하는 정규식, 모듈 README 의 책임 분리, 그리고 식별자 열의 기본 키 제약을 차례로 출력한 터미널 기록." caption="포트 전문 · 값 계약은 36자 canonical UUIDv7 · 직접 구현 0 과 하위 포트 경유 어댑터 둘 · 값 객체는 모양만 검사 · 유일성은 기본 키가 강제 — 73줄 · exit 0" zoom="true" -::: - -메서드 문서가 새 식별자를 신선하고 한 번도 쓰인 적 없는 값이라고 적는다. - -그 메서드는 인자를 받지 않고 확인 예외도 선언하지 않는다. 이미 쓰인 값인지 물어볼 대상이 없고, 충돌을 알릴 통로도 없다. 저장소 수준 유일성을 약속하려면 둘 중 하나는 있어야 한다. - -## 값 계약이 어느 쪽인지 정한다 - -이 포트가 만드는 값의 문서는 36자 canonical UUID 이고, RFC 9562 의 UUIDv7 이라고 버전까지 적는다. - -UUID 의 유일성은 확률적이다. 생성기가 같은 값을 두 번 낼 확률이 무시할 만큼 작다는 뜻이고, 저장소에 그 값이 없다는 확인은 아니다. - -값 객체 쪽은 그보다 더 느슨하다. 정규식이 보는 것은 8-4-4-4-12 열여섯진수 모양뿐이고, 버전 자리는 검사하지 않는다. 생성 어댑터를 거치지 않는 입력 경로에서는 계약이 적은 UUIDv7 조차 강제되지 않는다. - -## 직접 구현은 없고, 구현은 하위 포트를 거친다 - -이 포트를 `implements IdFactory<…>` 로 직접 구현하는 클래스는 없다. - -샘플이 팩토리 인터페이스 넷을 정의한다. 그중 둘이 포트를 확장하고, 메서드를 더하지 않은 채 타입 파라미터만 채운다. 나머지 둘은 확장하지 않는데, 반환형이 포트의 바운드 `T extends ResourceId` 를 만족하지 못하기 때문이다. 하나는 문자열을 돌려주고, 다른 하나는 의도 식별자와 연산 식별자를 한 번에 예약하는 레코드를 돌려준다. - -확장한 둘을 스프링 컴포넌트 어댑터가 구현한다. 그 구현은 진짜 UUIDv7 을 만든다. - -```java -return WorkLogId.of(UuidCreator.getTimeOrderedEpochPlus1().toString()); -``` - -밀리초 안에서도 단조 증가하고 보안 난수를 쓴다고 어댑터 javadoc 이 적는다. 그래도 저장소를 볼 자리는 생기지 않는다. 오버라이드하는 시그니처가 그대로이기 때문이다. - -## README 는 왜 그렇게 나눴는지를 적는다 - -발급할 책임은 도메인이 소유하고, 실제로 만드는 행위는 인프라 어댑터가 수행하며, 애플리케이션 유스케이스가 둘을 조율한다. 그렇게 나누면 도메인이 난수나 시계 같은 구체적 소스를 알지 못한 채 계약만 갖는다는 것이다. 괄호 안의 예시가 UUIDv7 생성기다. - -README 는 책임의 경계를 정할 뿐 보장의 범위를 정하지 않는다. 그 범위를 정하는 것은 시그니처다. - -## 유일성은 있다. 다른 데서 온다 - -작업 로그와 포스터의 식별자 열은 기본 키다. - -```text -CONSTRAINT pk_work_log PRIMARY KEY (id) -CONSTRAINT pk_poster PRIMARY KEY (id) -``` - -중복 삽입을 거절하는 것은 이 제약이다. 식별자를 자연 키로 쓰거나 중복을 막는 근거로 삼는 코드는 저장소 수준 유일성을 전제하는데, 그 전제를 주는 것은 포트가 아니라 스키마다. 포트를 읽고 유일성을 얻었다고 생각하면, 실제로는 그 보장을 데이터베이스에서 빌려 오고 있다. - -문구를 어떻게 할지는 의도를 먼저 확인해야 정해진다. 확률적 유일성을 뜻한 것이라면 문장을 그 범위로 좁히면 되고, 저장소 수준을 뜻한 것이라면 시그니처가 바뀌어야 한다. - -## 확인하지 못한 것 - -저자가 그 문구로 무엇을 의도했는지는 이 저장소가 말하지 않는다. 여기서 확인한 것은 계약이 무엇을 보장할 수 있는가다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/identity-and-value-contracts/concept/concept-adapter-inbound-web-c19.md b/docs/clean-architecture-backend-template/tech-log-studio/identity-and-value-contracts/concept/concept-adapter-inbound-web-c19.md deleted file mode 100644 index 5ef039a..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/identity-and-value-contracts/concept/concept-adapter-inbound-web-c19.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-web-c19 -title: 상속으로 강제하는 계약과 태스크 그래프로 강제하는 계약 -topic: identity-and-value-contracts -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-web-c19 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-web-c19 - file: ../../../final/evidence/rendered/adapter-inbound-web-c19.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-web-c19.txt -source: - - 원본 분석 절은 final/document.md#a14#L1555 이다. -module: adapter-inbound-web ---- - -# 상속으로 강제하는 계약과 태스크 그래프로 강제하는 계약 - -같은 "계약 강제"라는 이름 아래 두 가지 다른 장치가 있다. notification 쪽은 상속에 의존해 강제가 없고, web 쪽은 소스셋과 `dependsOn` 태스크 그래프로 강제하며 빠진 것을 잡는 장치까지 갖는다. - -## 본문 - - - -같은 "계약 강제"라는 이름 아래 두 가지 다른 장치가 있다. - -| | notification `ProviderAdapterContract` | web `WebBudgetContract` 외 6종 | -|---|---|---| -| 강제 수단 | 상속(강제 없음) | 소스셋 + `dependsOn` 태스크 그래프 | -| 실제 적용 | 8종 중 3종 | 세 런타임 전부 | -| 빠진 것을 잡는 장치 | 없음 | `webCrossStackParityTest`가 세 기록을 비교하고, 하나라도 없으면 실패 | - -## 빠진 것을 잡는 장치가 왜 필요한가 - -web 쪽이 구조적으로 우월하다. `build.gradle`이 그 이유를 적는다 — "a parity check that compares whatever happens to be present would report agreement across a matrix with a hole in it." - -## 분석 원문의 비교 표 - -:::evidence key="adapter-inbound-web-c19" alt="분석 문서 final/document.md#a14 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a14 발췌 — 15줄" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/identity-and-value-contracts/concept/concept-adapter-outbound-cache-redis-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/identity-and-value-contracts/concept/concept-adapter-outbound-cache-redis-c03.md deleted file mode 100644 index 30dbdf0..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/identity-and-value-contracts/concept/concept-adapter-outbound-cache-redis-c03.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-cache-redis-c03 -title: 부재 주장을 지키는 세 자리 — 선언·구현·정책 -topic: identity-and-value-contracts -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-cache-redis-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-cache-redis-c03 - file: ../../../final/evidence/rendered/adapter-outbound-cache-redis-c03.svg - - key: adapter-outbound-cache-redis-c03-diagram - file: ../../../final/assets/diagrams/adapter-outbound-cache-redis-c03.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-cache-redis-c03.txt -source: - - 원본 분석 절은 final/document.md#a10#L235 이다. -module: adapter-outbound-cache-redis ---- - -# 부재 주장을 지키는 세 자리 — 선언·구현·정책 - -"설계상 부재"라는 주장이 API 문서에만 있는지 확인했다. `sdk/api`의 선언, Lettuce 구현 계층, 명령 정책 파일 세 자리가 같은 부재를 각각 지킨다. - -## 본문 - - - -주장이 API 문서에만 있는지 확인했다(`160-...` §8.4). - -- `sdk/api` 전체에서 `SETNX`·`SETEX`·`PSETEX`·`ZREVRANGE`·`RPOPLPUSH`·`BRPOPLPUSH`·`GEORADIUS`가 등장하는 곳은 **"없다"고 적는 javadoc 네 줄뿐**이다. -- Lettuce 구현 계층에서 걸린 둘은 무해하다 — `HashOperationRequests:128`의 `HSETNX`(다른 명령이다), `WritePresence:6`의 주석("This is what replaces `SETNX` and `SETEX`"). -- 명령 정책 SSOT(`redis-command-policy.yml`, 1,406줄)에서 `KEYS`는 **`risk: R4`, `support: BLOCKED`**이고, 파일 머리의 표에 따르면 `BLOCKED`의 access는 `NONE`이다. 같은 자리에 `RANDOMKEY`(R2 BLOCKED)·`DUMP`·`RESTORE`·`MIGRATE`·`SELECT`·`SWAPDB`도 BLOCKED다. 즉 raw gateway로도 `KEYS`에 닿을 수 없다. - -## 부재를 지키는 세 자리 - -:::evidence key="adapter-outbound-cache-redis-c03-diagram" alt="sdk/api 선언과 Lettuce 구현 계층과 명령 정책 파일이 위에서 아래로 쌓이고 강제 방향 화살표가 아래로 그려진 구조" caption="부재를 지키는 세 자리" zoom="false" -::: - -## 분석 원문의 확인 절차 - -:::evidence key="adapter-outbound-cache-redis-c03" alt="분석 문서 final/document.md#a10 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a10 발췌 — 15줄" zoom="true" -::: - -## 정책 파일이 스스로 적는 권위 분리 - -정책 파일 자체의 구조는 sub-scope 05에서 다룬다 — 머리 주석이 "Official server metadata … decides what a command *is*. This file decides what this SDK is willing to *do* with it. **The catalog drift gate compares the two and fails the build when the server grows a command this file has not judged.**"라고 적는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/identity-and-value-contracts/concept/concept-adapter-outbound-notification-c05.md b/docs/clean-architecture-backend-template/tech-log-studio/identity-and-value-contracts/concept/concept-adapter-outbound-notification-c05.md deleted file mode 100644 index 61b022b..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/identity-and-value-contracts/concept/concept-adapter-outbound-notification-c05.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-notification-c05 -title: SSRF 가드의 호출처 둘과 javadoc이 지목하는 둘이 다르다 -topic: identity-and-value-contracts -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-notification-c05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-notification-c05 - file: ../../../final/evidence/rendered/adapter-outbound-notification-c05.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-notification-c05.txt -source: - - 원본 분석 절은 final/document.md#a13#L703 이다. -module: adapter-outbound-notification ---- - -# SSRF 가드의 호출처 둘과 javadoc이 지목하는 둘이 다르다 - -`NotificationEndpoints.requireExternallyRoutable`의 프로덕션 호출처는 SES endpoint와 webhook target 둘이다. 가드 자신의 javadoc이 위험 대상으로 지목하는 것은 Web Push endpoint와 webhook target이고, Web Push는 호출처 목록에 없다. - -## 본문 - - - -`NotificationEndpoints.requireExternallyRoutable`의 프로덕션 호출처는 **둘**뿐이다. - -```text -.../ses/SesProviderProperties.java:27: NotificationEndpoints.requireExternallyRoutable(endpoint, "SES endpoint", true); -.../webhook/WebhookSubscription.java:49: NotificationEndpoints.requireExternallyRoutable(target, "webhook target", trusted); -``` - -## javadoc이 위험 대상으로 지목하는 둘 - -> "**Web Push endpoints and webhook targets are supplied by clients**, which makes this a server-side request forgery primitive." - -Web Push는 호출처 목록에 없다. §25.1. - -## NotificationEndpoints 참조 위치 - -:::evidence key="adapter-outbound-notification-c05" alt="코드베이스에서 NotificationEndpoints 를 검색한 출력 17줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="NotificationEndpoints 코드베이스 검색 — 17줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/identity-and-value-contracts/concept/concept-adapter-outbound-notification-c06.md b/docs/clean-architecture-backend-template/tech-log-studio/identity-and-value-contracts/concept/concept-adapter-outbound-notification-c06.md deleted file mode 100644 index a331512..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/identity-and-value-contracts/concept/concept-adapter-outbound-notification-c06.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-notification-c06 -title: 상속을 강제하는 장치가 없어 8종 중 3종만 계약을 탄다 -topic: identity-and-value-contracts -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-notification-c06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-notification-c06 - file: ../../../final/evidence/rendered/adapter-outbound-notification-c06.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-notification-c06.txt -source: - - 원본 분석 절은 final/document.md#a13#L913 이다. -module: adapter-outbound-notification ---- - -# 상속을 강제하는 장치가 없어 8종 중 3종만 계약을 탄다 - -공유 계약 `ProviderAdapterContract`를 실제로 상속하는 어댑터는 8종 중 3종이다. 클래스 javadoc은 새 프로바이더가 같은 질문에 답하지 않고는 추가될 수 없다고 쓰지만, 상속을 강제하는 장치는 없다. - -## 본문 - - - -공유 계약을 실제로 상속하는 어댑터는 이 셋이다. - -```text -.../ses/SesNotificationProviderAdapterTest.java:50 -.../twilio/TwilioSmsProviderAdapterTest.java:24 -.../apns/ApnsNotificationProviderAdapterTest.java:31 -``` - -**8종 중 3종.** 클래스 javadoc은 "Subclasses supply an adapter and a fault harness; the assertions are here **so that a new provider cannot be added without answering the same three questions**"라고 쓰지만, 상속을 강제하는 장치는 없다. - -## 같은 종류의 강제가 다른 곳에는 있다 - -이 저장소는 같은 종류의 강제를 다른 곳에서는 만들어 두었다 — `EndpointGuardCallSiteTest`(가드가 호출처에서 실제로 도달하는가), `verifyNotificationApiSurface`(공개 타입 586개 스냅샷 고정). 여기에는 없다. - -## EndpointGuardCallSiteTest 참조 위치 - -:::evidence key="adapter-outbound-notification-c06" alt="코드베이스에서 EndpointGuardCallSiteTest 를 검색한 출력 1줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="EndpointGuardCallSiteTest 코드베이스 검색 — 1줄 · exit 0" zoom="true" -::: - -## 크로스-프로바이더 스위트에 등록된 다섯 - -`ContractAdapters`가 크로스-프로바이더 스위트에 등록하는 것은 **5종**(ses · twilio · apns · webpush · webhook)이다. 빠진 둘은 fcm과 smtp이고, 그것은 harness의 구조적 한계로 설명된다 — 스위트는 HTTP 루프백 서버 위에서 돌고, SMTP는 JavaMail 릴레이로, FCM은 `FcmGateway` 심으로 나간다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/identity-and-value-contracts/concept/concept-adapter-outbound-objectstorage-c09.md b/docs/clean-architecture-backend-template/tech-log-studio/identity-and-value-contracts/concept/concept-adapter-outbound-objectstorage-c09.md deleted file mode 100644 index ae983c8..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/identity-and-value-contracts/concept/concept-adapter-outbound-objectstorage-c09.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-objectstorage-c09 -title: 경로 방어는 세그먼트마다, 발행은 배타적 하드링크로 -topic: identity-and-value-contracts -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-objectstorage-c09 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-objectstorage-c09 - file: ../../../final/evidence/rendered/adapter-outbound-objectstorage-c09.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-objectstorage-c09.txt -source: - - 원본 분석 절은 final/document.md#a09#L601 이다. -module: adapter-outbound-objectstorage ---- - -# 경로 방어는 세그먼트마다, 발행은 배타적 하드링크로 - -`LocalObjectPathGuard`는 부모 경로를 root부터 한 세그먼트씩 내려가며 심링크와 비디렉터리를 거부한다. 발행은 `Files.createLink`가 주는 배타성에 기대고, 하드링크를 지원하지 않는 파일시스템에서 조용히 약한 방식으로 내려가지 않는다. - -## 본문 - - - -`LocalObjectPathGuard`는 이 저장소에서 반복해 본 강한 형태다 — root 정규화 + `startsWith` 봉쇄 + root 자신 거부에 더해, 부모 경로를 **root부터 한 세그먼트씩 내려가며** 심링크와 비디렉터리를 거부하고(`createParentsWithoutLinks` / `rejectExistingLinks`), 대상 자신도 심링크면 거부한다. control key는 `control/v1/` 접두사 + `[a-z0-9._/-]+` + `//`·`/./`·`/../` 금지 + 세그먼트별 재검사다. - -그리고 control 레코드는 물리 파일명에 `.record`를 붙인다 — 객체 저장소가 허용하는 `reference`와 `reference/lifecycle` 쌍이 파일시스템에서 파일/디렉터리 충돌을 일으키지 않도록. 논리 키는 그대로 유지된다. - -## LocalObjectPathGuard 참조 위치 - -:::evidence key="adapter-outbound-objectstorage-c09" alt="코드베이스에서 LocalObjectPathGuard 를 검색한 출력 14줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="LocalObjectPathGuard 코드베이스 검색 — 14줄 · exit 0" zoom="true" -::: - -## 발행이 배타적 하드링크인 이유 - -`LocalDevObjectDataStore.create`가 임시 파일에 쓰고 `channel.force(true)` 후 `Files.createLink(target, temporary)`를 하며, `FileAlreadyExistsException`을 `CONFLICT`로, `UnsupportedOperationException`을 "local filesystem cannot prove immutable create"로 번역한다 — 하드링크를 지원하지 않는 파일시스템에서 조용히 약한 방식으로 내려가지 않는다. 앞선 `Files.exists(NOFOLLOW)` 검사는 빠른 경로일 뿐이고 배타성은 `createLink`가 준다. POSIX면 소유자 읽기 전용 권한을 씌운다. - -## 테스트 이름이 잡는 여섯 가지 - -`traversalAbsoluteUnicodePercentAndSymlinkEscapesAreRejected`, `exclusiveCreateRaceHasOneWinner`, `injectedDiskFailureLeavesNoFinalOrTemporaryData`, `restartInspectsCommittedDataWithoutReplayingProducer`, `corruptControlRecordRemainsPresentAndNeverAppearsAbsent`, `createsRestrictivePermissionsWherePosixIsSupported`. 마지막에서 두 번째가 특히 이 leaf의 규칙이다 — 손상된 control 레코드는 **부재로 보이지 않는다**. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/identity-and-value-contracts/concept/concept-app-bootstrap-c04.md b/docs/clean-architecture-backend-template/tech-log-studio/identity-and-value-contracts/concept/concept-app-bootstrap-c04.md deleted file mode 100644 index 9da0a21..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/identity-and-value-contracts/concept/concept-app-bootstrap-c04.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: app-bootstrap-c04 -title: 자동설정 안의 @ConditionalOnBean은 컴포넌트 스캔과 다르다 -topic: identity-and-value-contracts -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:app-bootstrap-c04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: app-bootstrap-c04 - file: ../../../final/evidence/rendered/app-bootstrap-c04.svg -evidence: - - ../../../final/evidence/raw/app-bootstrap-c04.txt -source: - - 원본 분석 절은 final/document.md#a18#L386 이다. -module: app-bootstrap ---- - -# 자동설정 안의 @ConditionalOnBean은 컴포넌트 스캔과 다르다 - -7개 main 파일 전부 `@Configuration`이고 `@ConditionalOnProperty`/`@ConditionalOnBean`으로 게이트된다. 자동설정 안에서의 `@ConditionalOnBean`은 Boot가 평가 순서를 통제하므로 모듈 14 §7.1이 경고한 위험이 없다. - -## 본문 - - - -7개 main 파일 전부 `@Configuration`이고 `@ConditionalOnProperty`/`@ConditionalOnBean`으로 게이트된다. - -## 세 조건을 함께 쓰는 자리 - -`MongoPlatformHealthConfig`가 `@ConditionalOnBean` + `@ConditionalOnMissingBean` + `@ConditionalOnProperty` 셋을 함께 쓰는데, 자동설정 안에서의 `@ConditionalOnBean`은 Boot가 평가 순서를 통제하므로 모듈 14 §7.1이 경고한 컴포넌트 스캔 상의 위험이 없다. - -## MongoPlatformHealthConfig 참조 위치 - -:::evidence key="app-bootstrap-c04" alt="코드베이스에서 MongoPlatformHealthConfig 를 검색한 출력 1줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MongoPlatformHealthConfig 코드베이스 검색 — 1줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/identity-and-value-contracts/concept/concept-grpc-core-api-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/identity-and-value-contracts/concept/concept-grpc-core-api-c01.md deleted file mode 100644 index ede7ca5..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/identity-and-value-contracts/concept/concept-grpc-core-api-c01.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -kind: CONCEPT -slug: grpc-core-api-c01 -title: 게이트가 인자를 받지 않고 집합을 돌려주는 이유 -topic: identity-and-value-contracts -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:grpc-core-api-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-core-api-c01 - file: ../../../final/evidence/rendered/grpc-core-api-c01.svg -evidence: - - ../../../final/evidence/raw/grpc-core-api-c01.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-core-api#L105 이다. -module: grpc-core-api ---- - -# 게이트가 인자를 받지 않고 집합을 돌려주는 이유 - -`GrpcStableModuleCatalog`이 Stable 12와 Advanced 6을 상수로 든다. 그 위의 `GrpcStableBuildInvariant`는 인자를 받지 않는 판정 메서드와, 첫 위반이 아니라 위반 집합을 돌려주는 반환 형태를 갖는다. 둘 다 이유가 코드에 적혀 있다. - -## 본문 - - - -`GrpcStableModuleCatalog` 이 Stable 12 와 Advanced 6 을 상수로 든다. 그 위에 놓인 판정 메서드 `GrpcStableBuildInvariant.advancedDependencyAllowed()` 는 인자를 받지 않는다. - -> "the answer does not vary by module, by capability or by environment. A method that could return true for some input would be the seam through which 'just this one Advanced type in the starter' arrives." - -## GrpcStableModuleCatalog 참조 위치 - -:::evidence key="grpc-core-api-c01" alt="코드베이스에서 GrpcStableModuleCatalog 를 검색한 출력 14줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcStableModuleCatalog 코드베이스 검색 — 14줄 · exit 0" zoom="true" -::: - -## 첫 위반이 아니라 위반 집합을 돌려준다 - -누출을 던지지 않고 집합으로 돌려주는 이유도 적혀 있다 — 첫 하나만 보고하는 게이트는 넷을 지우는 일을 네 번의 대화로 만든다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/identity-and-value-contracts/concept/concept-messaging-testkit-c04.md b/docs/clean-architecture-backend-template/tech-log-studio/identity-and-value-contracts/concept/concept-messaging-testkit-c04.md deleted file mode 100644 index 976c375..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/identity-and-value-contracts/concept/concept-messaging-testkit-c04.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-testkit-c04 -title: 계약 테스트 일곱 개를 어댑터가 지울 수 없게 만든 방식 -topic: identity-and-value-contracts -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-testkit-c04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-testkit-c04 - file: ../../../final/evidence/rendered/messaging-testkit-c04.svg - - key: messaging-testkit-c04-diagram - file: ../../../final/assets/diagrams/messaging-testkit-c04.svg -evidence: - - ../../../final/evidence/raw/messaging-testkit-c04.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-testkit#L169 이다. -module: messaging-testkit ---- - -# 계약 테스트 일곱 개를 어댑터가 지울 수 없게 만든 방식 - -계약을 `abstract class` + `@Test` 로 만들었기 때문에 어댑터가 `@Test` 를 삭제하는 방법이 없다. 그 위에 리플렉션으로 메서드 이름 집합을 상수와 대조하는 자물쇠가 하나 더 있어 계약의 크기 자체가 잠겨 있다. - -## 본문 - - - -계약을 `abstract class` + `@Test` 로 만든 결정의 효과는 `InMemoryHarnessContractTest` 의 javadoc 에 있다. - -> "Every broker adapter adds the same nested class over its own harness, so a guarantee can only be weakened by editing the contract, where the change is visible, rather than by an adapter quietly not implementing it." - -즉 어댑터가 `@Test` 를 **삭제하는 방법이 없다**. 상속받는 순간 7개가 전부 실행된다. - -## 계약을 상속하면 일어나는 것 - -:::evidence key="messaging-testkit-c04-diagram" alt="계약 클래스에서 어댑터 중첩 클래스로 상속 화살표가 가고 거기서 계약 테스트 실행으로 이어지는 왼쪽에서 오른쪽 흐름" caption="계약을 상속하면 일어나는 것" zoom="false" -::: - -어댑터 쪽에서 하나를 빼려면 이 파일을 고쳐야 하고, 그것은 리뷰에 보인다. - -## 계약의 크기를 잠그는 두 번째 자물쇠 - -`CompatibilityMatrixTest` 가 리플렉션으로 `@Test` 가 붙은 메서드 이름 집합을 `REQUIRED_CONTRACT_TESTS` 상수와 정확히 대조한다. 계약에서 테스트 하나를 지우면 이 테스트가 깨진다. 추가해도 깨진다. 계약의 크기 자체가 잠겨 있다. - -## InMemoryHarnessContractTest 참조 위치 - -:::evidence key="messaging-testkit-c04" alt="코드베이스에서 InMemoryHarnessContractTest 를 검색한 출력 1줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="InMemoryHarnessContractTest 코드베이스 검색 — 1줄 · exit 0" zoom="true" -::: - -## 시나리오가 기대 결과를 소유한다 - -5개 시나리오, 그리고 각각이 `rationale` 을 **비어 있으면 생성 자체가 실패하도록** 강제한다. `REJECTED` 가 `BEFORE_TRANSMISSION` 하나뿐이라는 사실이 테스트로 잠겨 있다(`CrossBrokerContractSuite.aFailureBeforeTransmissionIsTheOnlyOneReportedAsRejected`). `byName` 은 알 수 없는 이름을 건너뛰지 않고 거절한다. - -## 증거 파일을 커밋하고 손으로 못 쓰게 한다 - -두 javadoc 이 **자기가 고친 결함을 이름 붙여** 남겼고, 네 가지 결정이 한 문단에 압축되어 있다. - -**(a) 커밋한다.** `src/main/resources/messaging/broker-certification-evidence.jsonl`. `build/` 를 읽으면 깨끗한 체크아웃에서 답이 달라진다는 이유가 명시되어 있다. 확인: `src` 와 `build/resources` 사본이 diff 로 동일(`EVD-300`). - -**(b) 손으로 못 쓰게 하는 게이트.** `messaging-kafka/build.gradle:82` 의 `verifyMessagingCertificationEvidence`. `gitCommit` 과 `observedAt` 을 정규식으로 지우고 나머지 집합을 비교한다. 그 둘은 매 실행마다 달라지므로 비교 대상이 아니라는 주석이 붙어 있다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a12-f001-check-verifyjsonschemaruntimegraph.md b/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a12-f001-check-verifyjsonschemaruntimegraph.md deleted file mode 100644 index eee6012..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a12-f001-check-verifyjsonschemaruntimegraph.md +++ /dev/null @@ -1,253 +0,0 @@ ---- -kind: CASE -slug: a12-f001-check-verifyjsonschemaruntimegraph -title: 매 PR 을 막는 게이트가 초록일 수 없다 -topic: messaging-and-outbox -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a12-f001-check-verifyjsonschemaruntimegraph -evidenceCapturedOn: 2026-09-02 -body: case-a12-f001-check-verifyjsonschemaruntimegraph.body.md -assets: - - key: a12-f001-check-verifyjsonschemaruntimegraph - file: ../../../final/evidence/rendered/a12-f001-check-verifyjsonschemaruntimegraph.svg - - key: a12-f001-check-verifyjsonschemaruntimegraph-run - file: ../../../final/evidence/rendered/a12-f001-check-verifyjsonschemaruntimegraph-run.svg -evidence: - - ../../../final/evidence/raw/a12-f001-check-verifyjsonschemaruntimegraph.txt - - ../../../final/evidence/raw/a12-f001-check-verifyjsonschemaruntimegraph-run.txt -source: - - 원본 분석 절은 final/document.md#a12#L82 이다. 등급은 P2 이다. 이 리프의 중심 규율을 강제하는 태스크가 검사 단계에 붙어 있다는 지형, 실행하면 실패한다는 관측, 필수 좌표에 정확한 패치 판본이 하드코딩되어 있고 잠긴 좌표는 다르다는 대조, BOM 이 올라가면서 두 좌표가 어긋났다는 원인 지목, 금지 조건 쪽은 여전히 옳다는 확인, 그리고 수정 방향 둘이 그 절에 있다. - - 이 기록이 더한 것은 넷이다. 하드코딩은 처음부터 틀린 것이 아니었고, 목록을 쓴 커밋 시점에는 잠금 파일과 일치했다. 883 파일을 건드린 커밋이 잠금 파일만 올리고 이 빌드 파일을 지나갔다. 이 태스크는 상위 자격 태스크 둘의 선행이기도 하지만 그 둘은 어떤 워크플로도 부르지 않고, 매 PR 마다 실제로 도는 것은 품질 게이트 잡의 검사 명령이며 그 잡은 릴리스 게이트의 필수 선행이다. 그리고 잠금 파일이 움직이기 여드레 전 같은 검사가 성공한 기록이 저장소에 남아 있다. - - 그래서 등급을 P1 로 올린다. 원본이 판단한 시점의 관찰은 게이트가 통과할 수 없다는 것까지였고, 그 게이트가 차단하는 범위와 그 상태가 이어진 기간은 이 기록에서 확인했다. ---- - -# 매 PR 을 막는 게이트가 초록일 수 없다 - -이 리프의 중심 규율을 강제하는 태스크가 검사 단계에 붙어 있고, CI 의 품질 게이트 잡이 매 PR 마다 그 검사를 부른다. 실행하면 종료 코드 1 이다. 필수 좌표에 패치 판본이 하드코딩되어 있는데, 그 판본은 태스크를 쓸 당시에는 맞았고 그 뒤 잠금 파일만 올라갔다. - -## 관계 - -- **release gate가 실제로 차단하는 것은 hermetic test 3개이고, mongo용 CI workflow는 없다** - 릴리스 게이트가 실제로 차단하는 대상을 센 문서다. -- **durable-operation 게이트는 켤 수 없고 켜면 부팅이 실패한다** - 두 게이트 모두 조건이 성립할 수 없어 통과 상태가 될 수 없다. -- **없다고 적은 라이브러리로 옆 파일이 봉투를 만든다** - 같은 리프에서 같은 커밋이 만든 사례다. - -## 문제 - -이 리프의 중심 규율은 JSON 검증 런타임을 닫는 것이다. - -이 규율을 지키는 태스크는 검사 단계에만 붙어 있는 것이 아니라 CI 가 매 PR 마다 부르는 검사 안에 들어 있다. - -그래서 실행했다. - -## 결론 - -실패한다. 필수로 잠긴 모듈 하나가 없다는 이유로 종료 코드 1 을 낸다. - -태스크가 요구하는 좌표는 셋이고 전부 판본이 3.0.2 다. 잠금 파일이 고정한 것은 검증기 라이브러리만 3.0.2 이고 직렬화 계열 셋은 3.1.5 다. - -하드코딩이 처음부터 틀렸던 것은 아니다. 이 목록을 쓴 커밋 시점의 잠금 파일에는 검증기와 직렬화 계열이 모두 3.0.2 로 잡혀 있었다. - -그 뒤 883 파일을 건드린 커밋이 잠금 파일의 직렬화 계열 세 좌표를 3.1.5 로 올렸다. 태스크가 이름으로 요구하는 둘이 그 안에 있다. 그 커밋은 같은 판본을 적어 둔 빌드 파일을 건드리지 않았다. - -닿는 범위가 검사 단계 하나가 아니다. 상위 자격 태스크 둘도 이 태스크를 선행으로 걸지만, 그 둘을 부르는 워크플로가 없다. 실제로 매 PR 과 main 푸시마다 이 태스크를 돌리는 것은 품질 게이트 잡의 검사 명령이고, 그 잡은 릴리스 게이트의 필수 선행이다. - -여드레 전에는 초록이었다. 저장소에 남은 실행 기록에 같은 검사 명령이 이 태스크를 돌고 성공으로 끝난 로그가 있다. - -금지 조건 쪽은 걸리는 것이 0 이다. 다만 그 0 은 이 검사가 잡아낸 결과라기보다 설정 블록이 형식 계열 세 모듈을 미리 빼 둔 결과다. 그 위에 구세대 이름공간이 통째로 빠진 것도 아니다. 애너테이션 아티팩트가 남아 있고, 태스크는 그것을 금지 목록에서 의도적으로 뺀다. - -판정을 P2 에서 올린다. - -원본은 게이트 전체가 통과할 수 없다는 관찰로 P2 를 매겼다. 그 게이트가 매 PR 을 막는 필수 잡 안에 있고, 그 상태가 특정 날짜부터 이어지고 있으며, 그 전에 초록이던 기록이 저장소에 남아 있다. 상시 빨간 차단 게이트는 P1 이다. - -수정은 필수 좌표에서 판본을 떼고 그룹과 이름만 확인하거나, 잠금 파일에서 판본을 읽어 비교하는 것이다. 닫힘 조건은 무엇이 없는가이지 어느 패치인가가 아니다. - -## 검증 환경 - -Gradle : 9.0.0 -확인 방식 : 태스크 실행, 검사 단계 예행 실행, 잠금 파일과 커밋 이력 조회 -소스 수정 : x -비고 : 해결이 STRICT 의존성 잠금에 고정되어 있어 오프라인 여부가 판본을 바꾸지 않는다 - -## 재현 조건 - -1. 검증 태스크를 단독으로 실행하고 종료 코드와 메시지를 본다. -2. 검사 단계를 예행 실행해 그 태스크에 닿는지 확인한다. -3. 태스크가 요구하는 좌표 셋을 빌드 파일에서 읽는다. -4. 잠금 파일이 고정한 같은 그룹의 좌표를 뽑는다. -5. 이 목록을 쓴 커밋을 찾아 그 시점의 잠금 파일 값을 확인한다. -6. 잠금 파일의 판본을 올린 커밋을 찾아, 그 커밋의 잠금 파일 차이와 이 빌드 파일 포함 여부를 본다. -7. 이 태스크 이름이 나오는 곳을 확장자 제한 없이 세고, 검사 단계를 부르는 워크플로와 상위 자격 태스크를 부르는 워크플로를 각각 센다. -8. 그 전에 이 태스크가 초록이던 실행 기록이 저장소에 있는지 찾는다. - -## 본문 - - - -이 리프의 규율을 지키는 태스크 하나가 검사 단계에 붙어 있고, CI 의 품질 게이트 잡은 그 검사를 매 PR 마다 부른다. - -## 태스크가 하는 일 - -:::evidence key="a12-f001-check-verifyjsonschemaruntimegraph" alt="검증 태스크의 정의 전체와 그 앞의 설정 제외 블록을 줄 번호와 함께, 이 태스크 이름이 저장소에서 나오는 곳 전부, CI 가 검사 단계를 부르는 줄과 릴리스 게이트의 필수 선행 목록, 상위 자격 태스크를 부르는 워크플로 수, 그리고 금지 목록에서 의도적으로 빠진 구세대 이름공간 아티팩트를 출력한 터미널 기록." caption="설정 블록이 형식 계열 세 모듈을 미리 빼고, 태스크는 그 뒤 필수 좌표 셋을 판본까지 확인한다 · CI 는 매 PR 과 main 푸시마다 검사 단계를 부르고 그 잡은 릴리스 게이트의 필수 선행이다 · 상위 자격 태스크를 부르는 워크플로는 0 · 구세대 이름공간의 애너테이션 아티팩트는 그래프에 남아 있다 — 71줄 · exit 0" zoom="true" -::: - -```groovy -50: [ -51: 'com.networknt:json-schema-validator:3.0.2', -52: 'tools.jackson.core:jackson-core:3.0.2', -53: 'tools.jackson.core:jackson-databind:3.0.2' -54: ].each { String required -> -55: if (!modules.contains(required)) { -56: throw new GradleException( -57: "Messaging JSON runtime is missing required locked module ${required}") -58: } -59: } -60: // Jackson 3 intentionally retains the 2.x-namespace annotations artifact. It is not a -61: // Jackson 2 databind/runtime engine and is part of the official Jackson 3 BOM graph. -62: } -63:} -64: -65:tasks.named('check') { -66: dependsOn tasks.named('verifyJsonSchemaRuntimeGraph') -67:} -``` - -## 돌려 보면 - -:::evidence key="a12-f001-check-verifyjsonschemaruntimegraph-run" alt="검증 태스크를 그대로 실행한 결과와 종료 코드, 검사 단계 예행 실행이 그 태스크에 닿는지, 태스크가 요구하는 좌표와 잠금 파일이 고정한 좌표, 이 목록을 쓴 커밋 시점의 잠금 값, 잠금 파일의 판본을 올린 커밋과 그 커밋의 잠금 파일 차이 및 이 빌드 파일 포함 여부, 그리고 그 전에 이 태스크가 초록이던 실행 기록을 출력한 터미널 기록." caption="태스크는 필수 모듈 하나가 없다며 종료 코드 1 로 끝나고, 검사 단계 예행 실행이 그 태스크에 닿는다 · 요구 판본은 셋 다 3.0.2 인데 잠금 파일의 직렬화 계열 셋은 3.1.5 · 883 파일 커밋이 그 셋을 올리면서 빌드 파일은 건드리지 않았다 · 여드레 전 실행 기록에는 같은 검사가 성공으로 끝나 있다 — 49줄 · exit 0" zoom="true" -::: - -```text -[check 에 붙은 태스크를 그대로 실행] - Execution failed for task ':adapter:outbound:messaging:verifyJsonSchemaRuntimeGraph'. - > Messaging JSON runtime is missing required locked module tools.jackson.core:jackson-core:3.0.2 - EXIT=1 -``` - -요구 판본 셋 중 둘이 어긋난다. - -```text -[태스크가 요구하는 좌표] -com.networknt:json-schema-validator:3.0.2 -tools.jackson.core:jackson-core:3.0.2 -tools.jackson.core:jackson-databind:3.0.2 - -[잠금 파일이 고정한 좌표] - com.networknt:json-schema-validator:3.0.2 - tools.jackson.core:jackson-core:3.1.5 - tools.jackson.core:jackson-databind:3.1.5 - tools.jackson:jackson-bom:3.1.5 -``` - -검증기 라이브러리만 맞는다. 직렬화 계열은 BOM 을 포함해 셋이 3.1.5 로 올라가 있다. - -## 처음부터 틀렸던 것은 아니다 - -```text -[태스크를 쓴 시점의 잠금 파일] - e5af2912 2026-07-31 feat: add messaging R2 polling producer - com.networknt:json-schema-validator:3.0.2 - tools.jackson.core:jackson-core:3.0.2 - tools.jackson.core:jackson-databind:3.0.2 - tools.jackson:jackson-bom:3.0.2 -``` - -이 목록을 쓴 커밋 시점에는 넷이 모두 3.0.2 였다. - -```text -[잠금 파일을 움직인 커밋] - a24ece9c 2026-08-28 feat: web, websocket 어댑터 추가 구현 - -com.fasterxml.jackson.core:jackson-annotations:2.20 - +com.fasterxml.jackson.core:jackson-annotations:2.21 - -tools.jackson.core:jackson-core:3.0.2 - -tools.jackson.core:jackson-databind:3.0.2 - -tools.jackson:jackson-bom:3.0.2 - +tools.jackson.core:jackson-core:3.1.5 - +tools.jackson.core:jackson-databind:3.1.5 - +tools.jackson:jackson-bom:3.1.5 - 그 커밋이 건드린 파일 수 : 883 - 그중 messaging/build.gradle : 0 건 -``` - -883 파일을 건드린 커밋이 BOM 을 3.1.5 로 올리면서 그것이 끌고 오는 두 좌표를 함께 올렸고, 같은 판본을 적어 둔 이 빌드 파일은 지나갔다. - -## 어디까지 막는가 - -```text - src/build.gradle:850: dependsOn ':adapter:outbound:messaging:verifyJsonSchemaRuntimeGraph' - src/build.gradle:892: dependsOn ':adapter:outbound:messaging:verifyJsonSchemaRuntimeGraph' - src/adapter/outbound/messaging/README.md:139:Gradle dependency lock과 `verifyJsonSchemaRuntimeGraph`가 담당한다. 이 검증은 business schema - src/adapter/outbound/messaging/build.gradle:28:tasks.register('verifyJsonSchemaRuntimeGraph') { - src/adapter/outbound/messaging/build.gradle:66: dependsOn tasks.named('verifyJsonSchemaRuntimeGraph') - src/adapter/outbound/messaging/CLAUDE.md:55: `CodeSource` is a regular JAR. Strict dependency locks and `verifyJsonSchemaRuntimeGraph` own the -``` - -850 과 892 가 상위 자격 태스크 둘이다. 그런데 그 둘을 부르는 워크플로가 없다. - -```text - verifyMessagingJsonSchemaV1 / verifyMessagingContracts 가 .github 아래 나오는 줄 : 0 -``` - -실제로 도는 것은 66 쪽이다. - -```text - 50: run: ./gradlew check verifyPublicPathSnapshot verifyDependencyLocks --warning-mode=fail --no-daemon --stacktrace - release-gate: - needs: - - quality-gates - - sample-off -``` - -품질 게이트 잡이 매 PR 과 main 푸시마다 검사 단계를 부르고, 릴리스 게이트가 그 잡을 필수 선행으로 건다. 예행 실행이 그 경로를 보여 준다. - -```text -[CI 가 부르는 check 가 이 태스크에 닿는가] - :adapter:outbound:messaging:verifyJsonSchemaRuntimeGraph SKIPPED - :adapter:outbound:messaging:check SKIPPED -``` - -## 여드레 전에는 초록이었다 - -```text -[그 전에 초록이던 기록] - run-at: 2026-08-20T01:53:16Z - 775:> Task :adapter:outbound:messaging:verifyJsonSchemaRuntimeGraph - 899:BUILD SUCCESSFUL in 5m 42s - 901:exit=0 -``` - -저장소에 커밋된 실행 기록이다. 잠금 파일이 움직이기 여드레 전, 같은 검사 명령이 이 태스크를 돌고 성공으로 끝났다. - -## 금지 조건 쪽 - -걸리는 것은 0 이다. 다만 그 0 을 만드는 것의 상당 부분은 검사가 아니라 그 앞의 제외 블록이다. - -```groovy -22:configurations.configureEach { -23: exclude group: 'tools.jackson.dataformat', module: 'jackson-dataformat-yaml' -24: exclude group: 'org.yaml', module: 'snakeyaml' -25: exclude group: 'org.snakeyaml', module: 'snakeyaml-engine' -26:} -``` - -금지 조건의 네 갈래 중 둘이 여기서 미리 제거된 모듈을 찾는다. - -구세대 이름공간이 그래프에서 사라진 것도 아니다. - -```text - 8:com.fasterxml.jackson.core:jackson-annotations:2.21=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -``` - -60~61 줄 주석이 이 아티팩트를 금지 목록에서 뺀 이유를 적는다. 닫힌 그래프 안에 구세대 이름공간 하나가 설계대로 남아 있다. - -## 확인하지 못한 것 - -판본을 떼는 수정을 적용해 태스크가 초록이 되는지 확인하지 않았다. 문서 작업 범위에서 소스를 고치지 않는다. - -두 커밋 사이에 같은 두 파일을 건드린 커밋이 둘 더 있다. 그 둘은 필수 목록과 직렬화 계열 잠금 줄을 손대지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a12-f002-jackson-databind.md b/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a12-f002-jackson-databind.md deleted file mode 100644 index d890a88..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a12-f002-jackson-databind.md +++ /dev/null @@ -1,223 +0,0 @@ ---- -kind: CASE -slug: a12-f002-jackson-databind -title: 근거를 없앤 커밋이 README 를 열고 그 줄만 두었다 -topic: messaging-and-outbox -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a12-f002-jackson-databind -body: case-a12-f002-jackson-databind.body.md -evidenceCapturedOn: 2026-09-02 -assets: - - key: a12-f002-jackson-databind - file: ../../../final/evidence/rendered/a12-f002-jackson-databind.svg - - key: a12-f002-jackson-databind-history - file: ../../../final/evidence/rendered/a12-f002-jackson-databind-history.svg -evidence: - - ../../../final/evidence/raw/a12-f002-jackson-databind.txt - - ../../../final/evidence/raw/a12-f002-jackson-databind-history.txt -source: - - 원본 분석 절은 final/document.md#a12#L118 이다. 등급은 P3 이다. README 문장 인용, 잠금 파일에서 그 좌표가 컴파일과 실행 양쪽에 있다는 대조, 같은 리프의 검증 태스크가 그 모듈을 필수로 요구한다는 지적, 구세대 이름공간 쪽은 실제로 금지되어 있으므로 서술이 그 이름공간을 뜻했다면 맞지만 문장이 한정하지 않는다는 판단, 코드 결함이 아니라 근거로 적힌 사실이 무너진 것이라는 결론이 그 절에 있다. - - 이 기록이 더한 것은 넷이다. 같은 주장이 클래스 자바독에도 있어 고칠 자리가 둘이라는 것. 잠금 파일이 아니라 이 리프의 빌드 선언이 그 좌표를 끌어온다는 것. 구성을 바꾼 커밋이 2026-07-31 이고, 그 커밋이 같은 자리에서 README 에 절 셋을 붙이면서 이 줄만 두고 갔으며 그 뒤로 이 파일을 연 커밋이 없다는 것. 그리고 그 라이브러리로 봉투를 만드는 옆 파일이 있지만 그것을 부르는 어댑터를 조립하는 곳은 없다는 것이다. ---- - -# 근거를 없앤 커밋이 README 를 열고 그 줄만 두었다 - -README 가 손수 짠 직렬화의 근거로 결속 라이브러리가 클래스패스에 없다는 사실을 들고, 그 클래스의 자바독이 같은 문장을 다시 적는다. 그 좌표를 배포되는 클래스패스로 올린 커밋이 같은 자리에서 README 에 절 셋을 붙이면서 이 줄만 두고 갔다. - -## 관계 - -- **매 PR 을 막는 게이트가 초록일 수 없다** - 같은 리프의 같은 잠금 좌표에서 갈라진 사례다. 저쪽은 판본이 올라간 커밋, 이쪽은 구성이 바뀐 커밋이다. -- **README가 "노출된 setting도 bean도 없다"고 적은 능력에 production bean 여덟이 있다** - 두 사례 모두 README 가 적은 것과 실제 코드가 다르다. -- **문서가 지목하는 조정 레코드를 쓰는 코드가 없다** - 문서가 가리킨 대상이 코드에 없다는 점이 같다. - -## 문제 - -봉투 직렬화를 손으로 짠 이유가 README 에 적혀 있다. - -이 모듈은 결속 라이브러리를 클래스패스에 두지 않아 뼈대를 가볍게 유지하며, 그래서 봉투 직렬화는 의존성 없는 손수 짠 JSON 이라는 것이다. - -## 결론 - -손수 짠 클래스는 그 서술과 어긋나지 않는다. 바깥에서 끌어오는 타입이 없다. - -다만 그 클래스의 자바독이 README 와 같은 문장을 다시 적는다. 모듈이 그 라이브러리를 클래스패스 밖에 둔다는 것이다. 고칠 자리가 하나가 아니라 둘이다. - -잠금 파일만의 문제도 아니다. 이 리프의 빌드 파일이 스프링 JSON 스타터와 스키마 검증기를 직접 선언한다. 그 좌표가 컴파일과 실행 클래스패스에 올라 있는 것은 그 선언의 결과다. - -같은 빌드 파일의 검증 태스크는 그 좌표가 런타임 그래프에 있어야 한다고 요구한다. 문서가 부재를 근거로 삼는 동안 게이트는 존재를 조건으로 건다. - -문장이 쓰일 때는 맞았다. 초기 커밋 시점에는 그 좌표가 배포되는 클래스패스 어디에도 없었다. - -바꾼 것은 2026-07-31 의 메시징 R2 커밋이다. 그 커밋이 빌드 파일에 JSON 스택 둘을 선언하면서 좌표를 컴파일과 실행으로 올렸다. 같은 커밋이 README 를 열어 절 셋을 새로 붙였고, 근거가 무너진 그 문장만 그대로 두었다. 그 뒤로 이 README 를 건드린 커밋은 없다. - -실시간 팬아웃 봉투는 석 주 뒤에 들어왔다. 883 파일을 건드린 커밋이 이 리프에서 만진 것은 잠금 파일과 팬아웃 어댑터와 그 봉투 클래스 셋이고, 잠금 파일에서 바뀐 것은 판본뿐이다. - -문장이 근거로 삼은 것은 모듈 전체의 부재이고, 그것이 깨진 뒤 옆 파일이 봉투 JSON 조립을 그 라이브러리로 처리한다. - -다만 그 봉투를 부르는 어댑터를 조립하는 곳이 저장소 어디에도 없다. 선언은 그대로 있고 그것을 부르는 실행 경로만 없다. - -구세대 이름공간의 결속과 코어를 세우는 것은 검증 태스크 안의 단언 한 줄이고, 구성 단위 제외 목록이 빼는 것은 형식 계열뿐이다. 애너테이션 쪽 2.21 은 아직 두 클래스패스에 다 남아 있다. 서술이 그 이름공간을 뜻했다면 맞지만, 문장에 이름공간이 없다. - -판정은 P3 다. 코드 결함은 아니다. - -README 를 읽고 이 리프의 의존성 정책을 판단하는 사람은 지금 없는 사실을 근거로 삼게 된다. - -## 검증 환경 - -Gradle : 9.0.0 -확인 방식 : README 와 자바독 대조, 빌드 선언과 잠금 파일 확인, 리프 main 의 참조 전수 확인, 커밋 이력 조회 -소스 수정 : x - -## 재현 조건 - -1. README 의 직렬화 근거 문장과 손수 짠 클래스의 자바독을 나란히 읽는다. -2. 빌드 파일에서 그 라이브러리를 끌어오는 선언을 찾는다. -3. 같은 빌드 파일의 검증 태스크가 요구하는 좌표를 읽는다. -4. 리프 main 에서 그 라이브러리를 쓰는 곳을 임포트와 인라인 표기까지 전수로 찾는다. -5. 그중 봉투를 만드는 쪽의 본문을 읽고, 그 봉투와 어댑터를 자기 파일 밖에서 부르는 곳을 센다. -6. 잠금 파일에서 관련 좌표를 확인한다. -7. 그 좌표의 구성이 바뀐 이력을 커밋별로 뽑는다. -8. README 를 건드린 커밋 전부를 나열하고, 구성을 바꾼 커밋이 README 에 무엇을 했는지 그 앞뒤 36 행으로 확인한다. -9. 봉투 클래스가 들어온 커밋과 그 커밋이 이 리프에서 건드린 파일을 확인한다. - -## 본문 - - - -봉투 직렬화를 손으로 짠 이유가 README 에 적혀 있고, 그 클래스의 자바독도 같은 말을 한다. - -## 같은 주장을 하는 두 자리 - -:::evidence key="a12-f002-jackson-databind" alt="README 의 근거 문단과 손수 짠 클래스의 자바독이 같은 주장을 하는 것, 빌드 파일이 그 라이브러리를 끌어오는 선언과 같은 파일의 검증 태스크가 요구하는 좌표, 리프 main 에서 그 라이브러리를 쓰는 곳을 임포트와 인라인 표기까지 전수로 찾은 목록, 그중 봉투를 만드는 쪽의 본문, 그 봉투와 어댑터를 자기 파일 밖에서 부르는 곳의 수, 그리고 잠금 파일의 관련 좌표를 출력한 터미널 기록." caption="README 와 클래스 자바독이 같은 문장을 적는다 · 빌드 파일이 스프링 JSON 스타터와 스키마 검증기를 선언하고, 같은 파일의 검증 태스크는 그 좌표를 필수로 든다 · 리프 main 의 두 파일이 그 라이브러리를 쓰고 한쪽은 봉투를 만든다 · 그 봉투를 부르는 어댑터를 조립하는 곳은 0 — 44줄 · exit 0" zoom="true" -::: - -```text -## OutboxEnvelopeJson — 손수 짠 JSON - -이 모듈은 `jackson-databind` 를 classpath 에 두지 않아(스켈레톤을 가볍게 유지) outbox envelope -직렬화는 의존성 없는 손수 짠 JSON 이다. -``` - -```java -/** - * Hand-rolled, dependency-free JSON serialiser for the outbox envelope (no Jackson — the module - * deliberately keeps {@code jackson-databind} off its classpath). -``` - -손수 짠 코드 자체는 서술대로다. 임포트가 도메인 타입 하나뿐이다. 고쳐야 할 것은 그 코드가 아니라 두 자리에 적힌 근거다. - -## 빌드 파일이 그것을 끌어온다 - -```text - 8: implementation 'org.springframework.boot:spring-boot-starter-json' - 9: implementation('com.networknt:json-schema-validator:3.0.2') { -``` - -잠금 파일이 스스로 그렇게 된 것이 아니다. 이 리프가 직접 선언한다. 그리고 같은 파일 아래쪽의 검증 태스크가 그 좌표를 요구한다. - -```groovy - 'com.networknt:json-schema-validator:3.0.2', - 'tools.jackson.core:jackson-core:3.0.2', - 'tools.jackson.core:jackson-databind:3.0.2' -``` - -README 가 없다고 적은 모듈을 이 리프의 게이트가 필수로 든다. - -## 문장이 무너진 자리 - -:::evidence key="a12-f002-jackson-databind-history" alt="이 좌표의 구성이 바뀐 이력을 커밋별로 뽑은 목록, README 를 건드린 커밋 전부, 구성을 바꾼 커밋이 README 에 절 셋을 붙였다는 것과 그 커밋 앞뒤의 36 행, 그리고 봉투 클래스가 들어온 커밋과 그 커밋이 이 리프에서 건드린 파일을 출력한 터미널 기록." caption="초기 커밋에서는 시험 클래스패스에만 있었고, 2026-07-31 커밋이 컴파일과 실행으로 올렸다 · 그 커밋이 README 에 절 셋을 붙였는데 36 행은 앞뒤가 같다 · 그 뒤 README 를 건드린 커밋은 없다 · 봉투 클래스는 석 주 뒤 커밋이 들여왔고 그 커밋의 잠금 변경은 판본뿐 — 29줄 · exit 0" zoom="true" -::: - -```text -# 이 좌표의 구성이 바뀐 이력 - a24ece9c 2026-08-28 feat: web, websocket 어댑터 추가 구현 --tools.jackson.core:jackson-databind:3.0.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -+tools.jackson.core:jackson-databind:3.1.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath - 5f10b791 2026-08-11 chore: record pre-existing uncommitted repository state - e5af2912 2026-07-31 feat: add messaging R2 polling producer --tools.jackson.core:jackson-databind:3.0.2=testCompileClasspath,testRuntimeClasspath -+tools.jackson.core:jackson-databind:3.0.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath - 821fe00c 2026-07-24 init: 클린 아키텍처 백엔드 -+tools.jackson.core:jackson-databind:3.0.2=testCompileClasspath,testRuntimeClasspath -``` - -구성을 바꾼 것은 2026-07-31 커밋이다. 8월 커밋이 한 일은 판본을 올린 것뿐이다. - -문장의 classpath 를 배포되는 쪽으로 읽으면 초기 커밋 때는 성립했다. 이름공간을 두고는 같은 호의를 주지 않는다는 것이 아래의 문제다. - -## 그 커밋이 README 에 한 일 - -```text -# 구성을 바꾼 커밋이 README 에 한 일 - README 를 건드렸는가 : 1 - 그 커밋이 README 에 새로 붙인 절 : 3 개 - 그 커밋 앞뒤의 36 행 - before 이 모듈은 `jackson-databind` 를 classpath 에 두지 않아(스켈레톤을 가볍게 유지) outbox envelope - after 이 모듈은 `jackson-databind` 를 classpath 에 두지 않아(스켈레톤을 가볍게 유지) outbox envelope -``` - -같은 커밋이 이 README 를 열어 절 셋을 새로 붙였다. 방금 자기가 무너뜨린 그 한 줄만 그대로 두었다. - -```text -# README 를 건드린 커밋 전부 - e5af2912 2026-07-31 feat: add messaging R2 polling producer - b3add016 2026-07-28 feat: redis, fileserver, httpclient 런타임 시점 구현 추가 - 821fe00c 2026-07-24 init: 클린 아키텍처 백엔드 -``` - -그 뒤로 이 파일을 연 커밋은 없다. - -## 같은 리프에서 그 라이브러리를 쓰는 곳 - -```text - realtime/RealtimeFanoutEnvelopeJson.java:5:import tools.jackson.databind.ObjectMapper; - realtime/RealtimeFanoutEnvelopeJson.java:6:import tools.jackson.databind.json.JsonMapper; - realtime/RealtimeFanoutEnvelopeJson.java:7:import tools.jackson.databind.node.ObjectNode; - realtime/RealtimeFanoutEnvelopeJson.java:71: private static String text(tools.jackson.databind.JsonNode root, String field) { - envelope/LocalJsonSchemaRegistry.java:33:import tools.jackson.databind.DeserializationFeature; - envelope/LocalJsonSchemaRegistry.java:34:import tools.jackson.databind.JsonNode; - envelope/LocalJsonSchemaRegistry.java:35:import tools.jackson.databind.ObjectMapper; - envelope/LocalJsonSchemaRegistry.java:36:import tools.jackson.databind.json.JsonMapper; -``` - -README 가 근거로 든 것은 모듈 차원의 부재다. 그 부재가 깨진 자리에서 같은 리프의 옆 파일이 같은 종류의 일을 그 라이브러리로 한다. - -```java -24: private static final String VERSION = "1"; -25: private static final ObjectMapper MAPPER = JsonMapper.builder().build(); -30: public static String toJson(DurableFanoutRecord record) { -31: Objects.requireNonNull(record, "record must not be null"); -32: ObjectNode root = MAPPER.createObjectNode(); -``` - -봉투 JSON 을 조립하는 일이다. - -## 다만 그 경로는 아직 돌지 않는다 - -```text - RealtimeFanoutEnvelopeJson 을 자기 파일 밖에서 부르는 곳 : 1 - MessagingDurableFanoutAdapter 를 자기 파일 밖에서 부르는 곳 : 0 -``` - -부르는 하나는 팬아웃 어댑터이고, 그 어댑터를 조립하는 곳은 없다. 클래스패스 사실은 그대로이고, 도는 경로만 아직 없다. - -## 이름공간을 한정하지 않는다 - -구세대 이름공간의 결속과 코어를 막는 것은 빌드 파일 검증 태스크의 단언이다. 구성 단위의 제외 목록은 형식 계열만 뺀다. - -```text - 8:com.fasterxml.jackson.core:jackson-annotations:2.21=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -``` - -같은 이름공간의 애너테이션은 지금도 컴파일과 실행에 있다. 서술이 결속과 코어만 뜻했다면 맞지만, 문장에 이름공간이 없다. - -## 확인하지 못한 것 - -실시간 팬아웃 봉투가 손수 짠 쪽과 같은 규율을 따라야 하는지는 판단하지 않았다. 여기서 확인한 것은 문장이 서술하는 사실 관계까지다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a19-f003-messaging-reliability-api.md b/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a19-f003-messaging-reliability-api.md deleted file mode 100644 index 284dfdd..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a19-f003-messaging-reliability-api.md +++ /dev/null @@ -1,134 +0,0 @@ ---- -kind: CASE -slug: a19-f003-messaging-reliability-api -title: messaging-reliability-api는 main 13파일 · 817 LOC에 테스트가 0개다 -topic: messaging-and-outbox -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a19-f003-messaging-reliability-api -evidenceCapturedOn: 2026-09-04 -body: case-a19-f003-messaging-reliability-api.body.md -assets: - - key: a19-f003-messaging-reliability-api - file: ../../../final/evidence/rendered/a19-f003-messaging-reliability-api.svg -evidence: - - ../../../final/evidence/raw/a19-f003-messaging-reliability-api.txt -source: - - 원본 분석 절은 final/document.md#a19#L325 이다. ---- - -# messaging-reliability-api는 main 13파일 · 817 LOC에 테스트가 0개다 - -네 리프 중 `messaging-reliability-api` 만 `src` 아래에 `main` 디렉터리만 있다. 담긴 것은 outbox 와 inbox 계약 타입 열셋인데, 그중 넷은 어느 시험도 이름을 대지 않는다. - -## 관계 - -- **outbox 가 둘이고, 출하되는 것은 messaging 플랫폼 쪽이 아니다** - 이 리프가 선언한 `OutboxRepository` 를 구현하는 것은 저쪽이 다루는 스택이고, 출하되는 outbox 는 `application-core` 의 별도 포트를 쓴다. -- **소비자가 없는 fixture 셋** - 저쪽은 세 타입을 참조하는 소스가 test 와 testFixtures 양쪽에서 0 이고, 여기는 리프에 test 소스 세트 자체가 없다. -- **계약만 담는 leaf도 계약의 거절 조건은 자기 레인에서 검증한다** - 이 리프에 `test` 디렉터리가 없어서, 레코드 생성자가 거는 검증과 열거형이 든 술어가 이 리프의 레인에서는 한 번도 실행되지 않는다. - -## 문제 - -이 하위 범위에 리프가 넷 있다. - -각 리프에 어떤 소스 세트가 있고 파일이 몇인지, 그리고 시험이 없는 리프의 타입을 무엇이 참조하는지 셌다. - -## 결론 - -테스트 디렉터리가 있는 리프가 셋, 없는 리프가 하나다. - -형제 셋은 각각 test 파일을 8, 4, 4 개 갖는다. messaging-reliability-api 만 0 이고, 그 리프에는 test 디렉터리 자체가 없다. - -제목의 817 은 원시 줄 수다. 그중 415 줄이 자바독이고 남는 코드가 323 줄이다. - -열세 타입은 레코드 다섯, 인터페이스 다섯, 열거형 셋이다. 큰 것은 OutboxCanonicalMetadata(158줄)와 OutboxRepository(152줄)와 OutboxRecord(140줄)다. - -상태 전이를 담는다고 알려진 두 열거형은 메서드가 0 개다. OutboxStatus 와 OutboxTransitionResult 는 전이의 어휘를 정의할 뿐이고, 규칙은 OutboxRepository 의 자바독 계약과 그것을 강제하는 JDBC 구현에 있다. 규칙을 값으로 든 것은 InboxResult 뿐이다. - -참조는 다른 리프에 있다. 자바독과 문자열 리터럴을 걷어낸 뒤 세면 OutboxStatus 쪽이 여섯 파일, OutboxTransitionResult 쪽이 네 파일이다. - -그렇다고 열세 타입이 모두 시험된다고 읽으면 안 된다. 넷은 test 참조가 0 이고, InboxRecord 와 ReliableMessagePublisher 는 자기 파일 밖 main 참조도 0 이다. - -판정은 P3 이고 원문과 같다. - -## 검증 환경 - -확인 방식 : 리프별 소스 세트와 파일·줄 수 계수, 817 줄의 분해, 열세 타입의 선언 형태와 메서드 수, 전이 규칙이 적힌 자리 추적, 자바독을 걷어낸 뒤의 참조 재계수 -소스 수정 : x - -## 재현 조건 - -1. 이 하위 범위의 리프 넷을 나열하고 각각의 src 아래 디렉터리를 읽는다. -2. 리프마다 main 과 test 파일 수, 그리고 main 원시 줄 수를 센다. -3. 그 줄 수를 공백과 주석과 코드로 나눈다. -4. 테스트 디렉터리가 없는 리프의 타입을 전부 나열하고 선언 형태와 줄 수를 확인한다. -5. 두 열거형의 메서드 수를 세고, 전이 규칙이 실제로 선언된 자리를 찾는다. -6. 같은 리프의 다른 열거형이 값에 규칙을 실었는지 확인한다. -7. 원문이 지목한 구현 리프 이름이 실재하는지 확인하고, 같은 문서의 표가 적은 실제 이름과 계수를 읽는다. -8. 두 열거형의 test 참조를 셀 때 자바독과 문자열 리터럴을 먼저 걷어낸다. -9. 열세 타입 각각에 대해 test 참조와 자기 파일 밖 main 참조를 센다. - -## 본문 - - - -`messaging` 플랫폼의 첫 하위 범위에 리프가 넷 있다. 셋은 자기 `test` 디렉터리를 갖고 하나는 갖지 않는다. - -## 네 리프의 소스 세트와 817 줄의 내역 - -:::evidence key="a19-f003-messaging-reliability-api" alt="저장소 루트에서 돌린 정적 검색 출력 70줄. 네 리프의 main·test 파일 수와 main 줄 수와 소스 세트가 먼저 나오는데 messaging-reliability-api 만 소스 세트가 main 하나다. 이어서 그 817 줄이 공백 79 · 주석 415 · 코드 323 으로 갈리고, 열세 타입이 선언 형태와 줄 수로 나열된다. 그다음 InboxResult 만 isSafeToSettle 이라는 메서드를 갖고 OutboxStatus 와 OutboxTransitionResult 의 메서드 수가 0 이며, 전이 규칙이 OutboxRepository 의 자바독에 적혀 있다는 것이 보인다. 원문 §3.6 이 지목한 두 리프 이름은 src 디렉터리가 없고 같은 문서의 표가 적은 실제 이름 셋의 계수가 이어진다. jpa 쌍은 modules.json 등재 0 건 git 추적 파일 0 개다. 끝으로 주석과 문자열을 제거하고 단어 경계로 다시 센 두 열거형의 test 참조가 파일별 횟수와 소속 리프까지 나오고, 열세 타입 중 test 참조가 0 인 넷이 보인다." caption="네 리프의 소스 세트 · 817 줄의 내역 · 열세 타입과 메서드 수 · 실제 구현 리프 이름 · 재계수한 참조와 참조 0 인 넷 — 70줄 · exit 0" zoom="true" -::: - -`messaging-core-api` 는 main 85 에 test 8, `messaging-transport-spi` 는 13 에 4, `messaging-runtime-core` 는 6 에 4 다. `messaging-reliability-api` 만 main 13 에 test 0 이고, 그 리프의 `src` 아래에는 `main` 디렉터리 하나만 있다. - -main 줄 수 817 은 원시 줄 수다. 갈라 보면 공백 79, 주석 415, 코드 323 이다. 절반이 자바독이라 제목의 `817 LOC` 를 구현 규모로 읽으면 두 배 넘게 과장된다. - -## 열세 타입이 무엇인가 - -레코드가 다섯이다. `OutboxCanonicalMetadata`(158줄), `OutboxRecord`(140줄), `ClaimCheckReference`(50줄), `OutboxLease`(42줄), `InboxRecord`(27줄). - -인터페이스가 다섯이다. `OutboxRepository`(152줄), `InboxRepository`(53줄), `IdempotentMessageHandler`(33줄), `TransactionalMessageAction`(29줄), `ReliableMessagePublisher`(27줄). - -열거형이 셋이다. `InboxResult`(45줄), `OutboxStatus`(37줄), `OutboxTransitionResult`(24줄). - -## 두 열거형은 규칙이 아니라 어휘다 - -`OutboxStatus` 와 `OutboxTransitionResult` 는 메서드가 0 개다. 상수와 자바독뿐이고 전이를 판정하는 코드가 없다. - -규칙이 선언된 자리는 `OutboxRepository` 의 자바독이다. `:70` 이 확정된 발행 기록을 이 임차가 아직 그 행을 소유할 때만 한다고 적고, `:72` 가 다른 릴레이가 가져갔으면 `STALE_LEASE` 를 돌려준다고 적으며, `:87` 이 모호한 결과에 같은 조건을 건다. 강제하는 것은 JDBC 구현의 SQL 이다. - -이 리프에서 실행 가능한 규칙을 가진 열거형은 `InboxResult` 하나다. `:42` 의 `isSafeToSettle()` 이 `CLAIMED_ELSEWHERE` 에서 정산하면 효과가 사라진다는 것을 값으로 들고 있다. - -## 계약 타입은 다른 리프의 시험이 참조한다 - -주석과 문자열을 제거하고 단어 경계로 다시 세면 `OutboxStatus` 를 참조하는 test 는 여섯 파일이고 전부 `messaging-outbox-jdbc-postgresql` 안에 있다. 가장 많이 쓰는 것은 `OutboxRelayTest`(18회)와 `OutboxPostgresIT`(8회)다. - -`OutboxTransitionResult` 는 네 파일이다. `OutboxRelayTest`(19회), `OutboxOperationsTest`(11회), `MessagingOutboxRelayLifecycleTest`(7회), `OutboxPostgresIT`(4회)이고 마지막 하나만 다른 리프다. - -다만 열세 중 넷은 test 참조가 0 이다. `IdempotentMessageHandler`, `InboxRecord`, `ReliableMessagePublisher`, `TransactionalMessageAction` 이고, 그중 `InboxRecord` 와 `ReliableMessagePublisher` 는 자기 파일 밖 main 참조도 0 이다. - -그래서 이 리프의 계약이 전부 시험된다는 뜻은 아니다. 시험되는 타입은 다른 리프에서 시험되고, 시험되지 않는 타입이 넷 있다. - -## 원문과 갈리는 자리 - -원문 §3.6 은 구현 쪽 확인을 다음 범위로 넘기면서 `messaging-outbox-jdbc` 와 `messaging-inbox-jdbc` 를 지목했다. 그 이름의 `src` 디렉터리는 없다. - -다만 이것은 미확인이 아니라 축약 표기다. 같은 문서의 리프 표가 `messaging-outbox-jdbc-postgresql`, `messaging-inbox-jdbc-postgresql`, `messaging-claim-check` 를 실제 이름으로 싣고 있다. 세 리프 모두 자기 시험을 갖는다 — 각각 main 13 에 test 8, main 6 에 test 4, main 6 에 test 3 이다. 원문의 표는 리소스를 포함한 다른 기준으로 세어 수치가 다르다. - -원문이 `OutboxTransitionResult` 와 `OutboxStatus` 를 "상태 전이 규칙을 담을 수 있는 타입" 이라고 조심스럽게 적은 것도 그대로 옳다. 둘 다 메서드가 없어서 담을 수 있을 뿐 담고 있지는 않다. - -## 확인하지 못한 것 - -두 열거형에 전이 로직이 없다는 것은 메서드 계수와 본문 읽기로 판정했다. `OutboxRepository` 의 자바독 계약을 JDBC 구현이 실제로 어떻게 강제하는지는 SQL 을 열어 보지 않았다. - -참조 계수는 파일 수와 등장 횟수다. 어느 시험이 무엇을 단언하는지까지는 들어가지 않았다. - -두 열거형에 전이 로직이 없다는 것은 메서드 계수와 본문 읽기로 판정했다. `OutboxRepository` 의 자바독 계약을 JDBC 구현이 실제로 어떻게 강제하는지는 SQL 을 열어 보지 않았다. - -`messaging-outbox-jpa` 와 `messaging-inbox-jpa` 디렉터리는 `modules.json` 등재가 0 건이고 git 이 추적하는 파일도 0 개다. 모듈이 아니라 빌드 산출물이 남은 자리로 보고 계수에서 뺐다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a19-f006-messaging-cloudevents.md b/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a19-f006-messaging-cloudevents.md deleted file mode 100644 index 19f3245..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a19-f006-messaging-cloudevents.md +++ /dev/null @@ -1,127 +0,0 @@ ---- -kind: CASE -slug: a19-f006-messaging-cloudevents -title: messaging-cloudevents는 출하 leaf이고 starter의 의존이며 소비자가 없다 -topic: messaging-and-outbox -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a19-f006-messaging-cloudevents -evidenceCapturedOn: 2026-09-04 -body: case-a19-f006-messaging-cloudevents.body.md -assets: - - key: a19-f006-messaging-cloudevents - file: ../../../final/evidence/rendered/a19-f006-messaging-cloudevents.svg -evidence: - - ../../../final/evidence/raw/a19-f006-messaging-cloudevents.txt -source: - - 원본 분석 절은 final/document.md#a19#L431 이다. ---- - -# messaging-cloudevents는 출하 leaf이고 starter의 의존이며 소비자가 없다 - -`modules.json` 이 이 리프의 런타임 소속을 `app-bootstrap` 으로 적고 `messaging-spring-boot-starter/build.gradle:26` 이 `implementation` 으로 물어 출하 산출물에 들어간다. 그런데 `new DefaultCloudEventMapper` 는 `CloudEventMappingTest:30` 한 줄뿐이고, `CloudEventMapper` 와 `CloudEventExtensions` 를 참조하는 main 파일은 이 리프의 세 파일 자신이다. main 자바 소스 전체에서 `CloudEvent` 를 언급하는 파일도 그 셋뿐이다. - -## 관계 - -- **outbox 가 둘이고, 출하되는 것은 messaging 플랫폼 쪽이 아니다** - 저쪽은 조건이 참이 될 수 없어 사슬 전체가 조립되지 않고, 여기는 조립하는 코드가 아예 없다. 둘 다 main 파일은 남아 있는데 그것을 실행하는 경로가 없다. -- **브로커 ACL 매니페스트의 자기 점검이 존재하지 않는다** - 두 기록 모두 출하 리프의 타입이 main 코드에서 한 번도 참조되지 않는다. 저쪽은 자바독이 약속한 시작 시 대조가 실행되지 않고, 여기는 CloudEvents 헤더 매핑을 부르는 경로가 없다. -- **runtime_memberships를 먼저 읽고 심각도를 정한다** - 이 리프의 런타임 소속이 `app-bootstrap` 이라 빌드 전용 리프에 주는 미조립 면제가 적용되지 않고, 그래서 세 main 파일의 미조립이 기록할 사건이 된다. - -## 문제 - -이 리프는 CloudEvents 상호운용 규격을 담는다. - -런타임 소속이 app-bootstrap 이라 세 main 파일이 출하 산출물에 들어가므로, 그 세 파일을 부르는 main 코드가 있는지 셌다. - -## 결론 - -없다. - -DefaultCloudEventMapper(171줄)를 생성하는 코드는 CloudEventMappingTest:30 하나다. 계약 타입 둘도 리프 밖 main 에서는 나오지 않고, 파일 이름 규칙 없이 CloudEvent 를 main 전수 검색해도 결과가 같다. 서비스 로더가 읽을 등록 파일도 저장소에 없다. - -출하는 선언만이 아니라 해소된 결과로도 확인된다. 런타임 프로젝트 클로저에 이 리프가 있고 app-bootstrap/gradle.lockfile 이 io.cloudevents 두 아티팩트를 productionRuntimeClasspath 로 등재한다. - -빌드 파일이 스스로 모순을 적어 둔 자리도 있다. messaging-spring-boot-starter/build.gradle:24 의 주석은 이 그룹을 자동설정이 배선하고 채택자가 이름을 대지 않는 것으로 적는데, 이 리프는 배선하는 자동설정이 없고 implementation 이라 채택자가 이름을 댈 수도 없다. - -위험은 안 쓰이는 코드가 있다는 것이 아니다. 설계 스펙은 이 프로파일을 제공한다고 적고 한 절을 그 매핑에 할애했는데, 실행으로 옮기는 코드가 어느 경로에도 없다. - -한 방향에는 부르는 코드가 있다. 봉투 필드 사칭을 판정하는 메서드를 두 브로커 헤더 매퍼가 쓰는데, 그중 조립되는 것은 Kafka 발행 쪽뿐이다. 반대 방향을 맡는 코드는 이 리프 안에 갇혀 있다. - -판정은 P2 이고 원문과 같다. 다만 지원 매트릭스와 설정 참조 문서에는 CloudEvents 언급이 0 건이라, 약속이 적힌 곳은 설계 스펙까지다. - -## 검증 환경 - -확인 방식 : 리프 파일과 줄 수 확인, 런타임 소속과 빌드 선언과 해소된 클로저와 잠금 파일 대조, new DefaultCloudEventMapper 와 두 계약 타입의 main 참조 전수, CloudEvent 문자열의 main 전수 검색과 설정 클래스 계수, META-INF/services 계수, 원문이 적은 28 의 출처 대조, CanonicalEnvelopeHeaders 호출 네 줄의 성격과 그 위 transport 의 생성 지점 확인 -소스 수정 : x - -## 재현 조건 - -1. 이 리프의 소스 파일을 소스 세트별로 나열하고 줄 수를 센다. -2. modules.json 의 런타임 소속과 스타터 빌드 선언, 그리고 그 선언 위의 주석을 읽는다. -3. 해소된 런타임 프로젝트 클로저와 gradle.lockfile 에서 이 리프와 서드파티 아티팩트를 찾는다. -4. new DefaultCloudEventMapper 를 저장소 전체에서 센다. -5. 두 계약 타입을 참조하는 main 파일을 나열한다. -6. CloudEvent 를 main 자바 전수로 검색해 파일 이름 규칙에 기대지 않은 결과를 얻고, 설정 클래스와 서비스 로더 등록 파일도 함께 센다. -7. 원문이 28 로 적은 수가 어느 디렉터리의 파일 수인지 확인하고 그중 *AutoConfiguration 을 센다. -8. CanonicalEnvelopeHeaders 를 부르는 main 네 줄이 값을 싣는 코드인지 판정 코드인지 읽고, 실제로 헤더를 싣는 줄을 따로 찾는다. -9. 그 네 줄 위의 transport 두 개를 만드는 main 코드를 각각 센다. -10. 설계 스펙과 운영자용 문서에서 CloudEvents 언급을 각각 센다. - -## 본문 - - - -`messaging-cloudevents` 는 파일 넷짜리 리프다. main 셋이 `DefaultCloudEventMapper`(171줄), `CloudEventMapper`(33줄), `CloudEventExtensions`(24줄)이고, test 하나가 `CloudEventMappingTest`(162줄)다. - -## 출하 산출물에 들어간다 - -:::evidence key="a19-f006-messaging-cloudevents" alt="저장소 루트에서 돌린 정적 검색 출력 75줄. 리프의 파일 넷이 소스 세트와 줄 수로 먼저 나오고, 출하를 말하는 네 근거가 이어진다 — modules.json 의 런타임 소속, 스타터 build.gradle 의 주석과 implementation 선언, 해소된 런타임 프로젝트 클로저에 이 리프 이름이 있다는 것, 그리고 gradle.lockfile 에 io.cloudevents 두 아티팩트가 productionRuntimeClasspath 로 등재된 줄이다. 그다음 new DefaultCloudEventMapper 가 자기 시험 한 줄뿐이라는 것과 계약 타입을 참조하는 main 파일이 리프의 셋이라는 것, main 자바 소스 전체에서 CloudEvent 를 언급하는 파일도 그 셋뿐이라는 것, 설정 클래스 계수와 META-INF/services 파일 수 0 이 나온다. 원문이 28 로 적은 수가 스타터 autoconfigure 패키지의 파일 수이고 그중 이름이 AutoConfiguration 인 것은 여섯이라는 대조가 이어진다. 끝으로 CanonicalEnvelopeHeaders 를 부르는 네 줄이 값을 싣는 곳이 아니라 거절과 건너뛰기 판정이라는 것과 실제로 헤더를 싣는 ReservedHeaders 줄, 그리고 두 매퍼 위의 transport 중 Kafka 쪽만 main 에서 만들어진다는 것, 설계 스펙이 CloudEvents 프로파일을 약속한 두 줄과 지원 매트릭스·설정 참조에는 언급이 0 건이라는 것이 보인다." caption="리프 파일 넷 · 출하 근거 넷 · 생성 지점은 시험 하나 · CloudEvent 를 아는 main 파일 셋 · 28 의 출처 · 반대편 네 줄의 성격과 배선 · 약속한 문서와 없는 문서 — 75줄 · exit 0" zoom="true" -::: - -`modules.json` 이 이 리프의 `runtime_memberships` 를 `["app-bootstrap"]` 으로 적고, `messaging-spring-boot-starter/build.gradle:26` 이 `implementation` 으로 문다. 선언만이 아니다. 해소된 런타임 프로젝트 클로저 파일에 이 리프 이름이 있고, `app-bootstrap/gradle.lockfile` 이 `io.cloudevents:cloudevents-api:4.0.1` 과 `cloudevents-core:4.0.1` 을 `productionRuntimeClasspath` 로 등재한다. - -그 `implementation` 선언 바로 위 `:24` 의 주석이 이 그룹의 성격을 적는다 — 자동설정이 배선하고 채택자의 소스에서 이름을 대는 일이 없다는 것이다. 이 리프에 대해서는 앞뒤가 다 어긋난다. 배선하는 자동설정이 없고, `implementation` 이라 채택자의 컴파일 클래스패스에 타입이 오르지 않아 이름을 댈 수도 없다. - -## 부르는 코드가 없다 - -`new DefaultCloudEventMapper` 는 저장소 전체에서 `CloudEventMappingTest:30` 한 줄이다. `CloudEventMapper` 와 `CloudEventExtensions` 를 참조하는 main 파일은 이 리프의 셋 자신이다. - -파일 이름 규칙에 기대지 않고 `CloudEvent` 라는 문자열을 main 자바 소스 전체에서 찾아도 나오는 파일은 같은 셋뿐이다. 이름이 `*AutoConfiguration.java` 인 36 개와 `AutoConfiguration.imports` 등재 14 개는 그 전체 집합의 부분집합이므로, 자동설정에 없다는 것은 그 계수와 무관하게 성립한다. `META-INF/services` 파일은 저장소에 하나도 없다. - -## 그래서 CloudEvents 헤더를 기대하는 소비자와 맺어지는 계약이 없다 - -이 리프가 담은 것은 유선 상호운용 규격이다. 설계 스펙이 `:35` 의 역량 표에서 CloudEvents 를 도메인·통합 이벤트에 선택 가능한 1.0.2 호환 프로파일로 제공한다고 적고, `:756` 부터 한 절을 그 프로파일에 쓴다. - -그 약속을 받아 실행하는 코드가 어느 경로에도 없다. 위험은 안 쓰이는 코드가 산출물에 들어간다는 것이 아니라, 외부 소비자가 CloudEvents 헤더로 메시지를 받을 것으로 기대할 때 그 기대와 맺어지는 계약이 어느 실행 경로에서도 성립하지 않는다는 것이다. - -## 매핑의 한쪽 방향에는 호출자가 있다 - -`CanonicalEnvelopeHeaders.restatesEnvelopeField` 를 `KafkaHeaderMapper:90`·`:141` 과 `RabbitHeaderMapper:102`·`:158` 이 부른다. 다만 이 넷은 값을 싣는 코드가 아니다. `:90` 은 사용자 헤더가 봉투 필드를 사칭하면 `RESERVED_HEADER_FORGED` 로 던지고, `:141` 은 되읽을 때 건너뛴다. 실제로 헤더를 싣는 것은 `KafkaHeaderMapper:36` 부터의 `put(headers, ReservedHeaders.MESSAGE_ID, …)` 계열이다. - -그 넷 중 조립되는 경로에 있는 것도 하나다. `KafkaPublishMapper:34` 가 `KafkaHeaderMapper` 를 만들고, 그 위의 `KafkaMessagingTransport` 를 `KafkaMessagingAutoConfiguration:168` 이 만든다. `RabbitMessagingTransport` 를 만드는 main 코드는 0 건이라 Rabbit 쪽 두 줄은 조립되지 않은 클래스 안에 있다. - -정리하면 정규 봉투 헤더 판정은 Kafka 발행 경로에서 실제로 지나가고, 그 헤더를 `CloudEventExtensions` 로 옮기는 코드는 `DefaultCloudEventMapper` 안에만 있으며 그것을 부르는 main 코드가 없다. - -## 원문과 갈리는 자리 - -원문은 자동설정을 28 개 클래스로 적었다. 28 은 스타터의 `autoconfigure` 패키지 파일 수다. 그중 이름이 `*AutoConfiguration` 인 것은 여섯이고 나머지는 설정 값 타입과 검증기와 발행자다. 근거가 없는 수가 아니라 그 28 개를 "자동설정 클래스" 라고 부른 이름이 부정확하다. - -원문은 `CanonicalEnvelopeHeaders` 와 `CloudEventExtensions` 사이에 매핑이 존재한다고 적었다. 두 클래스는 서로를 참조하지 않는다. `DefaultCloudEventMapper` 가 양쪽 개념을 각각 다루는 것이지 두 타입이 이어져 있지는 않다. - -판정과 등급은 원문과 같다. - -## 확인하지 못한 것 - -헤더를 실은 메시지를 실제로 흘려보내 보지는 않았다. 부르는 경로가 없다는 데서 멈췄다. - -리플렉션이나 서비스 로더로 이 매퍼를 가져가는 경로는 `META-INF/services` 파일이 0 개라는 것까지만 확인했고, 클래스 이름 문자열로 불러 쓰는 경로는 따로 세지 않았다. 판정 근거는 이름 기반 정적 검색이다. - -리플렉션이나 서비스 로더로 이 매퍼를 가져가는 경로는 `META-INF/services` 파일이 0 개라는 것까지만 확인했고, 클래스 이름 문자열로 불러 쓰는 경로는 따로 세지 않았다. 판정 근거는 이름 기반 정적 검색이다. - -지원 매트릭스와 설정 참조 문서에 CloudEvents 언급이 0 건이라, 실제 외부 소비자가 이 약속을 보고 붙었는지는 확인할 방법이 없었다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a19-f008-acl.md b/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a19-f008-acl.md deleted file mode 100644 index e57ad62..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a19-f008-acl.md +++ /dev/null @@ -1,133 +0,0 @@ ---- -kind: CASE -slug: a19-f008-acl -title: 브로커 ACL 매니페스트의 자기 점검이 존재하지 않는다 -topic: messaging-and-outbox -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a19-f008-acl -evidenceCapturedOn: 2026-09-04 -body: case-a19-f008-acl.body.md -assets: - - key: a19-f008-acl - file: ../../../final/evidence/rendered/a19-f008-acl.svg -evidence: - - ../../../final/evidence/raw/a19-f008-acl.txt -source: - - 원본 분석 절은 final/document.md#a19#L495 이다. ---- - -# 브로커 ACL 매니페스트의 자기 점검이 존재하지 않는다 - -`messaging-security` 의 `BrokerAclManifest` 자바독은 기동 대조와 파괴적 권한 거부를 적어 두었다. 두 검사 모두 이 record 안에 메서드로 있고 시험도 단언한다. 그 메서드를 부르는 main 코드가 `BrokerAclManifest.java` 밖에 없다. - -## 관계 - -- **ACL 매니페스트 전체가 쓰이지 않는다** - 저쪽은 이 타입의 소비자가 없다는 미해결 질문이고, 여기서는 그중 자바독이 적어 둔 두 검사가 어느 main 경로에서도 불리지 않는 것을 확인했다. -- **클래스 javadoc 이 강제되지 않는 규칙을 강제된다고 말한다** - 저쪽은 규칙이 코드로 없고 여기는 코드로 있는데 부르는 자리가 없다. 자바독만 읽는 사람에게 보이는 결과는 같다. -- **타입이 문서화한 불변식은 타입이 강제한다** - 이 record 는 그 규칙을 지킨 형태다. 거부가 조건부라 생성자가 아니라 가드 메서드로 두었고, 남은 문제는 그 가드를 부르는 자리다. - -## 문제 - -브로커 권한 매니페스트가 출하 리프에 record 로 있다. - -자바독이 적어 둔 두 검사가 어디까지 존재하고 어디서 불리는지 확인했다. - -## 결론 - -두 검사 모두 이 record 안에 있다. 부르는 main 코드가 없다. - -파괴적 권한 거부는 requireApplicationRuntime()(:103)이 한다. 선언된 파괴 권한이 비어 있지 않으면 APPLICATION_HOLDS_DESTRUCTIVE_GRANT 로 던진다. 기동 대조에 필요한 뺄셈은 undeclared(:122) 와 missing(:135) 에 양방향으로 있다. - -CredentialRuntimeRegistryTest 한 파일이 이 타입을 17 줄에서 쓰는데, 그 시험은 파괴적 grant 를 넣은 매니페스트가 requireApplicationRuntime() 에서 예외를 내는 것과 undeclared() 가 선언에 없는 grant 를 돌려주는 것을 단언한다. 두 메서드를 부르는 자리가 그 시험뿐이다. - -StartupProfileValidation 으로 감싸인 대상 셋에도 이 타입은 들어 있지 않다. 감싸인 것은 KafkaBrokerProfile 과 RabbitBrokerProfile 과 BrokerSecurityProfile 셋이다. 마지막 것은 이 매니페스트와 같은 패키지인데 매니페스트만 빠져 있다. 브로커에 권한을 질의하는 이름 여덟도 0 파일인데, Kafka AdminClient 자체가 main 에 없어서 그 0 이 말해 주는 범위는 좁다. - -간결 생성자가 파괴 여부를 보지 않는 것은 결함이 아니다. 자바독이 거부한다고 적은 것은 애플리케이션 런타임이 파괴적 권한을 선언한 경우이고, 운영 평면의 principal 은 그것을 정당하게 선언한다. 조건을 아는 쪽이 부르는 가드가 맞고 그 가드가 이미 있다. - -원본 분석의 등급은 P2 이고 이 기록은 새로 매기지 않는다. 자바독이 단정한 것이 코드로 없다는 P2 의 지렛대는 이 리비전에서 성립하지 않는다. 낮출 근거도 있는데, 검사 대상이 런타임에 만들어지지 않고 파괴 연산 인터페이스에도 main 구현이 없다는 것이다. - -## 검증 환경 - -확인 방식 : 자바독 원문 확인, 대응 메서드 넷의 본문과 그것을 단언하는 시험 확인, 자기 파일 안팎을 나눈 호출자 계수, 이 타입의 생성·주입 지점 계수, Operation 열거값의 파괴 표시 확인, 기동 검증으로 감싸인 프로파일 전수, 브로커 ACL 질의 API 이름 여덟 검색과 AdminClient 의 소스 세트 분포, 파괴 연산 인터페이스의 구현 계수 -소스 수정 : x - -## 재현 조건 - -1. BrokerAclManifest 의 클래스 자바독에서 적어 둔 두 검사를 읽는다. -2. 그 두 검사에 대응하는 메서드가 있는지 파일 끝까지 읽는다. -3. 그 메서드들을 부르는 자리를 자기 파일 안과 밖으로 나눠 세고 소스 세트도 가른다. -4. 그 동작을 단언하는 시험의 이름과 단언 줄을 읽는다. -5. 이 타입의 생성 지점과 주입 지점을 main 에서 찾는다. -6. 간결 생성자가 검사하는 것을 나열하고, 거부가 무조건인지 조건부인지 자바독에서 확인한다. -7. Operation 열거값의 파괴 표시를 읽고 자바독이 적어 둔 이름과 대조한다. -8. StartupProfileValidation 으로 감싸인 대상을 전부 찾고 각각 무엇을 감싸는지 읽는다. -9. 브로커 권한 질의 API 이름을 검색하고, 그 이름들이 속한 클라이언트가 main 에 있는지도 함께 센다. -10. 파괴 연산을 담은 인터페이스의 main 구현을 센다. - -## 본문 - - - -`BrokerAclManifest` 는 `messaging-security` 의 141줄짜리 record 다. 클래스 자바독에는 플랫폼이 기동 시점에 자신을 이 매니페스트와 대조한다고 적혀 있고, 선언한 것보다 많은 권한을 들고 있는 런타임은 finding 이라고 적혀 있다. 파괴적 권한을 선언한 애플리케이션 런타임은 그대로 거부된다고도 적혀 있다. - -## 자바독이 적어 둔 두 검사 - -:::evidence key="a19-f008-acl" alt="저장소 루트에서 돌린 정적 검색 출력 125줄. BrokerAclManifest 의 줄 수와 자바독의 두 단정이 원문 그대로 먼저 나오고, 이어서 그 두 단정에 대응하는 메서드 넷의 본문이 실린다 — destructiveGrants, 파괴적 권한이 있으면 APPLICATION_HOLDS_DESTRUCTIVE_GRANT 로 던지는 requireApplicationRuntime, 그리고 관측된 권한과 선언을 양방향으로 비교하는 undeclared 와 missing 이다. 간결 생성자는 널 검사와 공백 검사와 리스트 복사 셋만 한다. 그 메서드들을 부르는 자리 전부가 나오는데 BrokerAclManifest.java 밖의 main 코드는 0 건이고, 이 타입을 만들거나 받는 main 코드도 0 건이며, 자기 파일 밖에서 이름이 나오는 파일은 시험 하나다. 그 시험이 파괴적 grant 에 예외가 나는 것과 선언에 없는 grant 를 골라내는 것을 각각 단언한다. 이어서 Operation 열거값 일곱과 파괴 표시 셋, 기동 검증으로 감싸인 프로파일 셋, DestructiveMessagingAdmin 을 구현하는 main 클래스 0, 브로커 권한 질의 API 이름 여덟이 모두 0 파일이고 Kafka AdminClient 자체가 test 에만 있다는 것이 보인다." caption="자바독의 두 검사 · 대응 메서드 넷의 본문 · 파일 밖 main 호출자 0 과 시험의 단언 · 기동 검증 대상 셋 · 파괴 연산 구현 0 · ACL 질의 0 과 그 0 의 의미 — 125줄 · exit 0" zoom="true" -::: - -두 번째 단정에 대응하는 것은 `requireApplicationRuntime()`(`:103`)이다. `destructiveGrants()` 로 선언된 파괴적 권한을 모아 비어 있지 않으면 `MessagingConfigurationException` 을 `APPLICATION_HOLDS_DESTRUCTIVE_GRANT` 코드로 던진다. 자바독 `:99` 에는 애플리케이션 런타임에 파괴적 권한을 주는 매니페스트를 거부한다고 적혀 있다. - -첫 번째 단정에 대응하는 것은 `undeclared(Set)`(`:122`)와 `missing(Set)`(`:135`)다. 관측된 권한과 선언을 양방향으로 뺀다. `:116`\~`:117` 에는 초과가 finding 이고 부족이 아니라고 적혀 있다 — 빠진 권한은 첫 사용에서 시끄럽게 실패하지만 선언되지 않은 여분은 남용될 때까지 눈에 띄지 않기 때문이다. - -시험도 그 둘을 단언한다. `CredentialRuntimeRegistryTest:167` 의 `anApplicationRuntimeMayNotHoldADestructiveGrant` 가 `:181` 에서 `requireApplicationRuntime` 에 예외가 나는 것을, `:187` 의 `aGrantTheBrokerHoldsButNobodyDeclaredIsTheFinding` 가 `:200` 에서 `undeclared` 가 선언에 없는 grant 를 돌려주는 것을 확인한다. - -## 부르는 프로덕션 코드가 없다 - -`BrokerAclManifest.java` 밖에서 이 네 메서드를 부르는 main 코드는 0 건이다. 이 타입을 생성하거나 파라미터로 받는 main 코드도 0 건이고, 자기 파일 밖에서 이름이 나오는 파일은 그 시험 하나다. - -기동 시점 검증에도 이 타입이 없다. `StartupProfileValidation` 이 감싸는 것은 `KafkaMessagingAutoConfiguration:59` 의 Kafka 브로커 프로파일, `RabbitMessagingAutoConfiguration:55` 의 Rabbit 브로커 프로파일, `MessagingCoreAutoConfiguration:221` 의 `compiled.security()` 셋이다. 세 번째는 같은 `messaging.security` 계열인데 매니페스트는 그 목록에 없다. - -브로커에 권한을 질의하는 코드도 없다. `describeAcls`·`AclBinding`·`Authorizer` 를 포함한 이름 여덟이 저장소 전체에서 0 파일이다. 다만 그 0 의 의미는 좁다 — Kafka `AdminClient` 자체가 main 에 없고 test 5 파일에만 있으므로, 애초에 맞을 수 있는 코드가 main 에 없었다. - -## 간결 생성자에 넣을 검사는 아니다 - -`:81`\~`:87` 의 간결 생성자는 `grants` 널 검사와 `principal` 공백 검사와 리스트 복사만 한다. 파괴 여부를 보지 않는다. - -여기에 검사를 넣는 것이 수정 방향은 아니다. 자바독이 거부한다고 적은 것은 파괴적 권한 일반이 아니라 **애플리케이션 런타임**이 그것을 선언한 경우다. 운영 평면의 principal 은 `DELETE` 와 `PURGE` 를 정당하게 선언한다. 생성자에서 막으면 그 매니페스트를 이 타입으로 표현할 수 없다. 조건을 아는 쪽이 부르는 명시적 가드가 맞는 형태이고, 그 형태가 이미 `:103` 에 있다. - -## 원문과 갈리는 자리 - -원문은 두 단정이 모두 실행되는 코드가 아니라고 적었고, 파괴적 권한을 선언한 매니페스트를 막는 코드가 record 자신에도 없다고 했다. 이 자리가 원문이 틀린 자리다. `requireApplicationRuntime()` 이 그 거부를 구현하고 시험이 그것을 고정한다. `undeclared` 와 `missing` 도 기동 대조에 필요한 비교를 갖고 있다. 없는 것은 구현이 아니라 그것을 부르는 자리다. - -원문은 간결 생성자가 `principal` 과 `pattern` 공백을 검사한다고 적었다. `pattern` 공백 검사는 중첩된 `Grant` 의 생성자에 있다. - -원문은 기동 검증으로 감싼 것을 브로커 프로파일 둘로 적었다. 셋이고, 세 번째가 이 매니페스트와 같은 패키지의 보안 프로파일이다. - -원문이 인용한 자바독은 파괴적 권한으로 `DELETE_TOPIC` 과 `PURGE` 를 적는다. `Operation` 에 `DELETE_TOPIC` 이라는 값은 없고 `DELETE` 가 있으며, 파괴로 표시된 값은 `ALTER`·`DELETE`·`PURGE` 셋이다. - -원문이 적은 "테스트 1건" 은 파일 수로는 맞다. 그 한 파일 안에서 이 타입이 나오는 줄은 17 이다. - -## 등급에 대해 - -원본 분석의 등급은 P2 다. 이 리비전에서 그 등급을 떠받치던 근거 하나는 성립하지 않는다. 자바독이 단정한 것이 코드로 존재하지 않는다는 것이 P2 의 지렛대였는데, 코드는 있고 부르는 자리만 없다. - -낮출 근거도 함께 있다. 프로덕션에서 이 매니페스트를 만드는 코드가 0 이라 검사할 대상이 런타임에 없고, 파괴 연산을 담은 `DestructiveMessagingAdmin` 은 main 구현이 0 이라 매니페스트가 거짓이어도 애플리케이션이 파괴 연산을 부를 수 없다. - -이 기록은 등급을 새로 매기지 않는다. 상류가 P2 로 둔 근거와 여기서 확인한 반대 근거를 함께 남긴다. - -## 확인하지 못한 것 - -그런 매니페스트를 실제로 만들어 아무 곳에서도 걸리지 않는 것을 실행으로 보이지는 않았다. `requireApplicationRuntime()` 을 부르는 main 코드가 없다는 데까지다. - -브로커에 붙어 권한을 조회하지 않았다. 그런 조회를 하는 코드가 없다는 것까지 확인했다. - -채택자가 `DestructiveMessagingAdmin` 을 구현해 넣는 배포는 보지 않았다. 이 저장소 main 에 구현이 0 이라는 것까지 확인했다. - -채택자가 `DestructiveMessagingAdmin` 을 구현해 넣는 배포는 보지 않았다. 이 저장소 main 에 구현이 0 이라는 것까지 확인했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a19-f014-kafka-msg.md b/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a19-f014-kafka-msg.md deleted file mode 100644 index e06a26d..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a19-f014-kafka-msg.md +++ /dev/null @@ -1,160 +0,0 @@ ---- -kind: CASE -slug: a19-f014-kafka-msg -title: Kafka 스택이 둘이고, 브로커 이름만 주면 기동이 실패한다 -topic: messaging-and-outbox -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a19-f014-kafka-msg -evidenceCapturedOn: 2026-09-04 -body: case-a19-f014-kafka-msg.body.md -assets: - - key: a19-f014-kafka-msg - file: ../../../final/evidence/rendered/a19-f014-kafka-msg.svg - - key: a19-f014-kafka-msg-probe - file: ../../../final/evidence/rendered/a19-f014-kafka-msg-probe.svg -evidence: - - ../../../final/evidence/raw/a19-f014-kafka-msg.txt - - ../../../final/evidence/raw/a19-f014-kafka-msg-probe.txt -source: - - 원본 분석 절은 final/document.md#a19#L735 이다. ---- - -# Kafka 스택이 둘이고, 브로커 이름만 주면 기동이 실패한다 - -한 아티팩트가 Kafka `Producer` 빈을 둘 발행한다. 원문은 마스터 스위치가 꺼진 채 브로커 이름만 주면 `KafkaSenderConfig` 쪽만 올라온다고 적었는데, 컴포지션 루트를 그 조합으로 띄우면 `kafkaSeamProducer` 에서 컨텍스트가 죽는다. - -## 관계 - -- **중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다** - 이름이 겹치는 둘을 만났을 때 조립되는 쪽을 가리는 절차다. 여기서는 조건 애너테이션만으로는 갈리지 않아 컨텍스트를 띄워 갈랐다. -- **마스터 스위치는 루트 하나가 소유하고 자식 설정은 조건을 갖지 않는다** - 그 결정은 능력 하나를 켜고 끄는 권한을 루트 한 곳에 둔다. 두 스택이 그 루트를 각각 다른 경로로 지나므로 조건만 읽으면 한쪽이 스위치 밖에 있는 것처럼 보인다. -- **조건부 빈의 평가 시점 — 파싱 시점과 등록 시점** - 조건 애너테이션만 나란히 읽으면 `KafkaSenderConfig` 는 조건이 하나로 보인다. 그 빈이 파라미터로 받는 설정 타입을 누가 등록하는지까지 봐야 실제로 조립되는 조합이 나온다. - -## 문제 - -한 아티팩트가 Kafka 생산자를 만드는 자리를 둘 갖고 있고, 둘의 조건이 달라 보인다. - -마스터 스위치가 그중 어디까지 막는지 확인했다. - -## 결론 - -생산자 빈은 둘이다. KafkaSenderConfig:63 이 Producer 을, KafkaMessagingAutoConfiguration:130 이 Producer 를 만든다. modules.json 상 두 모듈은 서로를 의존하지 않는다. - -조건은 달라 보인다. 앞쪽은 클래스에 app.messaging.broker 값 조건 하나만 달고, 뒤쪽은 MessagingPlatformRootAutoConfiguration:28 의 마스터 스위치를 지난 뒤 MessagingProviderSelection:48 이 같은 값으로 고른다. - -그런데 앞쪽도 스위치 뒤에 있다. kafkaSeamProducer 가 받는 KafkaAdapterSettings 는 KafkaAdapterConfig:19 만 등록하고, 그 클래스를 수입하는 main 코드는 MessagingBridgeRootAutoConfiguration:23 하나이며 그 루트가 :21 에서 마스터 스위치를 요구한다. 다른 경로도 없다 — CaSkeletonApplication 의 스캔 제외 정규식이 그 패키지를 잘라 내고 @ConfigurationPropertiesScan 목록에도 없다. - -ShippedCompositionHarness 로 컴포지션 루트를 세 조합으로 띄웠다. 스위치를 끈 채 브로커 이름만 주면 kafkaSeamProducer 가 KafkaAdapterSettings 를 못 찾아 기동이 실패한다. 스위치까지 켜면 그 생산자가 만들어진다. :62 의 빈 조건은 정의 등록 시점을 보는 것이라 이 클래스는 물러나지 않는다. - -브로커 이름만 주고도 통과하는 시험 둘이 있는데 둘 다 슬라이스다. MessagingConfigTest:41 과 OptionalAdapterBeanGatingTest:68 이 KafkaAdapterConfig 를 손으로 넣고, 어느 쪽도 CaSkeletonApplication 이나 KafkaSenderConfig 를 참조하지 않는다. - -CapabilityDependencyValidator:70 은 스위치가 켜졌는데 브로커가 빈 경우만 잡는다. 런북은 :28~:30 에서 이 키의 성격을 선택자로 못박는다. 출하 설정은 두 키를 늘 짝으로 주므로 이 조합에 닿지 않는다. - -판정은 P2 이고 원문과 같다. 다만 받치는 근거가 하나 교체된다. 문서보다 좁은 기본 보호 범위 대신, 검증기를 통과하는 조합이 컨텍스트를 죽인다는 사실이 들어온다. - -## 검증 환경 - -확인 방식 : 두 @Bean 선언과 각각의 조건 애너테이션 확인, 설정 타입의 등록 지점과 그것을 수입하는 자리 전수, 컴포넌트 스캔 제외 정규식과 프로퍼티 스캔 목록 확인, ShippedCompositionHarness 로 컴포지션 루트를 세 조합으로 기동, 브로커 값을 쓰는 시험 파일별 스위치 지정 횟수 계수와 그 시험들의 러너 구성 확인, 능력 의존 검증기의 조건 확인, 런북 서술과 출하 설정 파일의 두 키 확인, modules.json 의 의존 방향 확인 -소스 수정 : x - -## 재현 조건 - -1. Kafka Producer 를 만드는 @Bean 을 main 에서 모두 찾아 시그니처와 빈 이름을 적는다. -2. 각 빈에 걸린 조건 애너테이션을 클래스 단위와 메서드 단위로 나눠 읽는다. -3. 앞쪽 빈이 파라미터로 받는 설정 타입을 등록하는 자리를 main 에서 전수로 찾는다. -4. 그 등록 클래스를 @Import 하거나 자동설정으로 올리는 자리를 찾고 각각의 조건을 읽는다. -5. 컴포넌트 스캔 제외 정규식과 @ConfigurationPropertiesScan 목록이 그 패키지를 덮는지 본다. -6. ShippedCompositionHarness 로 컴포지션 루트를 세 조합으로 띄우고 실패한 빈과 없는 타입을 적는다. -7. 브로커 값을 쓰는 시험 파일마다 스위치를 몇 줄 주는지 세고, 0 줄인 파일의 러너 구성을 읽는다. -8. 능력 의존 검증기가 어떤 조합을 위반으로 모으는지 조건을 읽는다. -9. 런북과 출하 설정 파일이 두 키를 어떻게 다루는지 확인한다. - -## 본문 - - - -`app-bootstrap` 하나에 Kafka `Producer` 를 만드는 `@Bean` 이 둘 있다. `MSG-015` 가 그 상태를 미해결로 들고 있다(`src/messaging/CLAUDE.md:70`, `:82`). - -## 두 생산자 빈과 각각의 조건 - -:::evidence key="a19-f014-kafka-msg" alt="저장소 루트에서 돌린 정적 검색 출력 140줄. 두 Producer 빈의 선언 줄과 각각의 조건이 나온다 — KafkaSenderConfig 는 클래스에 app.messaging.broker=kafka 조건 하나를 달고 Producer 을 만들고, KafkaMessagingAutoConfiguration 은 Producer 를 만들며 MessagingPlatformRootAutoConfiguration 의 app.messaging.enabled=true 와 MessagingProviderSelection 의 제공자 선택을 거쳐 닿는다. 이어서 KafkaAdapterSettings 를 등록하는 유일한 자리와 그 자리를 @Import 하는 유일한 루트, 그 루트의 조건이 나오고, CaSkeletonApplication 의 컴포넌트 스캔 제외 정규식과 @ConfigurationPropertiesScan 목록이 adapter.outbound.messaging 을 덮지 않는 것이 보인다. app.messaging.broker=kafka 를 쓰는 시험 파일마다 app.messaging.enabled 를 몇 줄 주는지 세어 보이고, 그중 0 줄인 두 파일이 KafkaAdapterConfig 를 손으로 등록하며 CaSkeletonApplication 도 KafkaSenderConfig 도 참조하지 않는 것이 나온다. 마지막으로 이 조합을 거르지 않는 CapabilityDependencyValidator 의 조건, 이 키를 선택자라고 설명하는 런북, 두 키를 항상 짝으로 주는 출하 설정, 그리고 KafkaSenderConfig 의 빈 조건 애너테이션이 실린다." caption="두 Producer 빈과 조건 · 설정 빈의 유일한 등록·수입 경로 · 스캔 제외 정규식 · broker 만 준 시험이 통과하는 이유 · 이 조합을 거르지 않는 검증기와 그것을 권하는 런북 · 출하 설정의 짝 — 140줄 · exit 0" zoom="true" -::: - -`KafkaSenderConfig:63` 은 `Producer kafkaSeamProducer` 를 만든다. `:61` 이 빈 이름을 `kafkaSeamProducer` 로 고정하고 `:62` 가 같은 이름의 빈이 없을 때만 만든다는 조건을 단다. 클래스에는 `:38` 의 `@ConditionalOnProperty(name = "app.messaging.broker", havingValue = "kafka")` 하나가 붙어 있다. - -`KafkaMessagingAutoConfiguration:130` 은 `Producer messagingKafkaProducer` 를 만든다. 여기까지 오는 길은 `MessagingPlatformRootAutoConfiguration:28` 의 `app.messaging.enabled=true` 를 지나고, `MessagingProviderSelection:48` 이 `kafka` 라는 값에 이 자동설정을 물린다. - -`modules.json` 상 `adapter-outbound-messaging` 의 의존에 `messaging-*` 이 없고, `messaging-spring-boot-starter` 의 의존에 adapter 가 없다. 두 스택은 서로를 참조하지 않는다. - -## KafkaSenderConfig 도 app.messaging.enabled=true 를 지나야 조립된다 - -조건 애너테이션이 하나뿐이라고 해서 그 하나만으로 조립된다는 뜻은 아니다. - -`kafkaSeamProducer` 가 파라미터로 받는 `KafkaAdapterSettings` 를 등록하는 자리는 `KafkaAdapterConfig:19` 의 `@EnableConfigurationProperties` 하나뿐이다. 그 클래스를 `@Import` 하는 main 코드는 `MessagingBridgeRootAutoConfiguration:23` 하나이고, 그 루트는 `:21` 에서 `app.messaging.enabled=true` 를 요구한다. - -다른 경로로 들어올 수도 없다. `CaSkeletonApplication:100`\~`:105` 의 `AUTO_CONFIGURED_PACKAGES` 정규식이 `dev\.caskeleton\.adapter\.outbound\.messaging\..*` 를 컴포넌트 스캔에서 잘라 내고(`:55`\~`:56`), `@ConfigurationPropertiesScan` 목록에도 그 패키지가 없다. - -`KafkaSenderConfig` 자신은 `dev.caskeleton.bootstrap.messaging` 패키지에 있고 그 이름은 제외 정규식에 없다. 스캔으로 들어온다. - -## 세 조합을 실제로 기동한 결과 - -:::evidence key="a19-f014-kafka-msg-probe" alt="출하 컴포지션 루트를 세 조합으로 기동한 프로브 출력 43줄. 세 조합 모두 ShippedCompositionHarness 의 requiredOperatorInputs 와 allOffArguments 를 받고 조합마다 인자를 덮어썼다. C1 은 마스터 스위치가 꺼지고 브로커 이름이 없는 조합인데 jpaSharedEM_entityManagerFactory 에서 실패한다. C2 는 브로커 이름만 kafka 로 덮어쓴 조합인데 실패한 빈이 kafkaSeamProducer 로 바뀌고 없는 것이 KafkaAdapterSettings 라고 나온다. C3 는 마스터 스위치까지 켠 조합인데 KafkaProducer 가 실제로 만들어져 bootstrap.servers 와 두 직렬화기와 security.protocol 이 찍히고, 그 뒤 C1 과 같은 JPA 빈에서 끝난다. 세 조합을 통틀어 만들어진 KafkaProducer 는 1 개다. 마지막 세 줄은 이 프로브의 클래스패스에 시험 출력이 함께 있어 JPA 저장소 스캔이 켜진다는 것과, C2 의 실패는 그보다 먼저 난다는 것을 밝힌다." caption="컴포지션 루트 세 조합 기동 — 브로커 이름만 더하면 실패 지점이 kafkaSeamProducer 로 바뀐다 · 스위치를 켜면 그 생산자가 만들어진다 · 관측된 KafkaProducer 1 개 · 프로브 클래스패스가 남긴 JPA 실패 명시 — 43줄 · exit 0" zoom="true" -::: - -`ShippedCompositionHarness.shippedComposition("local")` 에 `requiredOperatorInputs()` 와 `allOffArguments()` 를 주고 세 조합으로 돌렸다. `allOffArguments()` 는 `--app.messaging.enabled=false` 를 포함한다. - -브로커 이름을 주지 않은 C1 에서는 messaging 쪽 빈이 만들어지지 않는다. 브로커 이름만 `kafka` 로 덮어쓴 C2 에서는 실패한 빈이 `kafkaSeamProducer` 이고, 없다고 보고된 것이 `KafkaAdapterSettings` 다. 마스터 스위치까지 켠 C3 에서는 그 생산자가 실제로 만들어져 `bootstrap.servers = [localhost:9092]`, 두 직렬화기가 `StringSerializer`, `security.protocol = PLAINTEXT` 로 찍힌다. - -`:62` 의 `@ConditionalOnMissingBean(name = "kafkaSeamProducer")` 은 빈 정의를 등록할지 정하는 조건이다. 그 시점에 같은 이름의 빈이 없으므로 정의는 등록되고, 실패는 정의를 실체로 만들 때 파라미터를 못 찾아서 난다. `KafkaSenderConfig` 는 물러나지 않는다. - -이 프로브의 클래스패스에는 `app-bootstrap` 의 시험 출력이 함께 있어 Spring Data JPA 저장소 스캔이 켜진다. 그래서 C1 과 C3 는 끝에서 `entityManagerFactory` 부재로 죽는다. C2 의 실패는 그보다 먼저 난다. - -## 슬라이스 시험이 보여 주는 것과 다른 것 - -`app.messaging.broker=kafka` 를 쓰는 시험 파일 넷 중 둘은 `app.messaging.enabled` 를 한 줄도 주지 않는다. `MessagingConfigTest` 와 `OptionalAdapterBeanGatingTest` 다. - -그 둘은 `ApplicationContextRunner` 에 설정 클래스를 골라 넣는 슬라이스다. `MessagingConfigTest:41` 과 `OptionalAdapterBeanGatingTest:68` 이 `KafkaAdapterConfig` 를 직접 등록한다. 마스터 스위치 뒤에 있는 것을 손으로 넣으므로 스위치를 지날 필요가 없다. - -두 파일 모두 `CaSkeletonApplication` 을 참조하지 않고 `KafkaSenderConfig` 도 참조하지 않는다. 컴포넌트 스캔이 없으니 실패하는 빈 자체가 그 슬라이스에 없다. - -`MessagingConfigTest` 의 broker 전용 시험 둘은 이름이 `selectedKafkaBrokerWithoutProjectSenderFailsStartupCharacterization`(`:39`)과 `selectedBrokerIdMismatchFailsStartupCharacterization`(`:54`)이다. 선택자만 준 조합을 기동 실패로 특성화한다. - -## 이 조합을 거르는 검증기가 없다 - -`CapabilityDependencyValidator:70` 은 `app.messaging.enabled=true` 인데 브로커가 비어 있으면 위반으로 모은다. 반대 조합은 조건에 없다. - -`docs/runbooks/outbox-publish-failed.md:28`\~`:30` 은 `APP_MESSAGING_BROKER` 가 활성화 스위치가 아니라 선택자이고 messaging 을 끄는 것은 `APP_MESSAGING_ENABLED=false` 라고 적는다. 그 설명대로 스위치를 끈 채 선택자만 남기면 C2 가 된다. - -출하 설정은 그 조합에 닿지 않는다. `compose-profile-contracts.json` 의 세 자리(`:170`, `:202`, `:604`)가 두 키를 항상 짝으로 주고, `.env.example:229` 와 `.env.local.example:21` 은 브로커를 공백으로 둔다. - -## 원문과 갈리는 자리 - -원문은 `app.messaging.enabled=false` 인 기본 상태에서 브로커 이름만 주면 `KafkaSenderConfig` 쪽 스택만 올라온다고 적었고, 그래서 마스터 스위치가 두 스택 중 하나만 막는다고 했다. - -C2 가 그 반대를 보인다. 그 조합에서 올라오는 것은 없고 컨텍스트가 `kafkaSeamProducer` 에서 죽는다. 마스터 스위치는 두 스택을 모두 막는다. 원문이 정정 대상으로 지목한 `src/messaging/CLAUDE.md:82` 의 서술 — 지금 안전한 이유가 설계가 아니라 `app.messaging.enabled=false` 라는 기본값이라는 것 — 은 그대로 성립한다. - -원문이 인용한 `KafkaSenderConfig:23`\~`:27` 의 자바독은 정확하다. 다만 그 자바독이 없다고 적은 빈은 `KafkaSender` 이고, C2 에서 없는 것은 `KafkaAdapterSettings` 다. 층이 다르다. - -## 등급에 대해 - -원본 분석의 등급은 P2 이고 이 기록은 그대로 둔다. 다만 등급을 받치던 근거 하나가 바뀐다. - -기본값이 지켜 주는 범위가 문서보다 좁다는 것은 성립하지 않는다. 남는 근거는 한 아티팩트에 서로를 참조하지 않는 Kafka 스택이 둘이라는 것이고, 이것은 정상 운영 조합에서 빈 이름이 달라 기동이 성공하므로 알려 주는 신호가 없다. - -그 자리에 들어오는 근거가 하나 늘었다. 검증기가 통과시키고 런북의 설명이 이끄는 조합이 기동을 죽인다. 출하 설정이 그 조합에 닿지 않아 P2 를 넘기지는 않는다. - -## 확인하지 못한 것 - -두 생산자가 한 컨텍스트에 함께 등록된 것을 보지 못했다. 스위치를 켠 조합에서 `kafkaSeamProducer` 하나가 만들어진 데까지 갔다. - -두 생산자가 같은 클러스터에 붙는지 확인하지 않았다. 각자 어느 설정에서 주소를 읽는지까지 봤다. - -프로브의 클래스패스는 출하 클래스패스가 아니다. `app-bootstrap` 시험 출력이 함께 있어 JPA 저장소 스캔이 켜지고, C1 과 C3 는 그 때문에 끝에서 죽는다. - -프로브의 클래스패스는 출하 클래스패스가 아니다. `app-bootstrap` 시험 출력이 함께 있어 JPA 저장소 스캔이 켜지고, C1 과 C3 는 그 때문에 끝에서 죽는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a19-f015-compatibilitymatrix-extension.md b/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a19-f015-compatibilitymatrix-extension.md deleted file mode 100644 index bae3c84..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a19-f015-compatibilitymatrix-extension.md +++ /dev/null @@ -1,130 +0,0 @@ ---- -kind: CASE -slug: a19-f015-compatibilitymatrix-extension -title: CompatibilityMatrix 의 EXTENSION 등급을 쓰는 항목이 없다 -topic: messaging-and-outbox -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a19-f015-compatibilitymatrix-extension -evidenceCapturedOn: 2026-09-04 -body: case-a19-f015-compatibilitymatrix-extension.body.md -assets: - - key: a19-f015-compatibilitymatrix-extension - file: ../../../final/evidence/rendered/a19-f015-compatibilitymatrix-extension.svg -evidence: - - ../../../final/evidence/raw/a19-f015-compatibilitymatrix-extension.txt -source: - - 원본 분석 절은 final/document.md#a19#L801 이다. ---- - -# CompatibilityMatrix 의 EXTENSION 등급을 쓰는 항목이 없다 - -`messaging-testkit` 의 `CompatibilityMatrix` 가 어댑터 지원 등급을 `Tier` 셋으로 두는데 `ENTRIES` 다섯 중 `EXTENSION` 을 쓰는 항목이 0 이다. 그 등급 설명에 해당하는 `messaging-spring-cloud-stream-bridge` 는 브로커 버전을 인증하지 않아 `Entry` 생성자를 통과할 수 없다. - -## 관계 - -- **build-only 등급이 90개 파일의 미조립을 오늘의 사고에서 면제한다** - 그 기록은 빌드에만 참여하는 모듈이 조립 검사에서 빠지는 것을 다룬다. 이 leaf 도 `runtime_memberships` 가 비어 있는데, 여기서 어긋난 것은 조립이 아니라 등급 표기다. -- **기계가 읽는 능력 매니페스트와 사람이 읽는 등급표가 커서 서명에 대해 다르게 답한다** - 둘 다 코드가 든 표와 운영자가 읽는 문서 표가 같은 대상에 대해 서로 다른 값을 적고 있는 경우다. -- **지원 등급은 추론이 아니라 선언이고 증거 없이는 올라가지 않는다** - 그 결정은 증거가 있을 때만 등급을 올린다고 정한다. 여기서는 그 규칙 이전에, 등급 값 하나가 어떤 항목에도 붙지 못한 채 열거형에만 남아 있다. - -## 문제 - -지원 등급이 messaging-testkit 의 CompatibilityMatrix 안에 표로 들어 있다. - -세 등급 중 하나가 어느 항목에도 쓰이지 않는데, 그 등급 설명에 맞는 모듈이 저장소에 있다. - -## 결론 - -Tier 값 셋 중 EXTENSION 을 쓰는 항목이 ENTRIES 에 없다. 다섯 항목은 messaging-kafka 하나가 STABLE 이고 나머지 넷이 EXPERIMENTAL 이다. - -그 등급 설명이 말하는 어댑터 SPI 전용·지원 집합 밖에 해당하는 모듈은 messaging-spring-cloud-stream-bridge 다. main 6 파일 507 줄인데 표에 이름이 없다. - -행이 되지 못하는 기계적 이유가 있다. Entry 의 간결 생성자는 브로커 버전 목록이 비면 거부한다. 이 모듈은 브로커가 아니라 Spring Cloud Stream 바인더 위의 이음매라 인증할 브로커 버전이 없다. - -운영자용 docs/messaging/support-matrix.md 는 등급을 한 표로 관리하지 않는다. 앞쪽 표에서 Extension 이 붙은 행은 :37 하나뿐이다. 거기 적힌 어댑터 이름을, 코드 쪽 시험은 표에 없는 이름의 예시로 쓴다. 기능 등급 표(:66)의 :78 에는 이 bridge 가 있고 등급 칸이 Optional 인데, 그 낱말은 Tier 에 없다. - -이 타입을 참조하는 main 코드는 자기 파일 하나뿐이다. 미등록 이름을 거부한다는 보장이 걸리는 범위는 시험과 계약 스위트다. - -판정은 P3 이고 원문과 같다. 다만 원문은 이 bridge 가 운영자 문서에도 없다고 적었는데 :78 에 행이 있다. - -## 검증 환경 - -확인 방식 : Tier 열거값과 각 자바독 확인, ENTRIES 항목 수와 등급별 계수, bridge leaf 의 main 파일 수·줄 수와 modules.json 의 runtime_memberships 확인, Entry 간결 생성자의 거부 조건 확인, of 의 거부 줄과 그것을 고정하는 시험 확인, 그 시험이 넘기는 이름 검색, CompatibilityMatrix 를 쓰는 main 코드 계수, 운영자 문서의 등급 표 전수와 각 표의 해당 행 확인, bridge 자바독과 정책 가드의 조건·예외 코드 확인 -소스 수정 : x - -## 재현 조건 - -1. CompatibilityMatrix.Tier 의 값 셋과 각 값의 자바독을 읽는다. -2. ENTRIES 의 항목을 세고 각 항목의 Tier 를 모은다. -3. 등급별 사용 횟수를 집계해 쓰이지 않는 값을 찾는다. -4. 그 값의 설명에 해당하는 모듈을 저장소에서 찾고 파일 수와 줄 수를 잰다. -5. modules.json 에서 그 모듈의 runtime_memberships 를 읽는다. -6. Entry 의 간결 생성자가 무엇을 거부하는지 읽고, 그 모듈이 그 조건을 만족할 수 있는지 본다. -7. of 가 등록되지 않은 이름을 어떻게 처리하는지와 그것을 고정하는 시험, 그 시험이 넘기는 이름을 확인한다. -8. CompatibilityMatrix 를 자기 파일 밖에서 쓰는 코드를 소스 세트별로 센다. -9. 운영자 문서에서 등급 표를 전부 찾고, 각 표에서 그 등급 값과 그 모듈 이름이 나오는 행을 읽는다. - -## 본문 - - - -`messaging-testkit` 의 `CompatibilityMatrix` 는 어댑터별 지원 등급을 코드 안의 표로 들고 있다. `Tier` 열거값은 셋이고, `ENTRIES` 는 다섯 항목이다. - -## Tier 셋과 ENTRIES 다섯 - -:::evidence key="a19-f015-compatibilitymatrix-extension" alt="저장소 루트에서 돌린 정적 검색 출력 73줄. CompatibilityMatrix.java 의 Tier 열거값 셋이 각각의 자바독과 함께 나오고, ENTRIES 다섯 항목의 어댑터 이름과 Tier 가 이어지며 STABLE 1 · EXPERIMENTAL 4 · EXTENSION 0 으로 집계된다. messaging-spring-cloud-stream-bridge 는 main 6 파일 507 줄에 modules.json 의 runtime_memberships 가 빈 리스트이고 ENTRIES 에 이름이 0 건이다. Entry 의 간결 생성자가 어댑터 이름 공백과 브로커 버전 목록 비어 있음을 각각 IllegalArgumentException 으로 거부하는 본문이 실리고, of 가 등록되지 않은 이름을 거부하는 줄과 그것을 고정하는 시험이 나오는데 그 시험이 미등록 이름의 예로 messaging-artemis 를 넘긴다. 같은 이름이 다른 시험의 픽스처와 계약 스위트에도 나오고, CompatibilityMatrix 를 자기 파일 밖에서 쓰는 main 코드는 0 건이다. 운영자 문서에는 등급 표가 브로커 등급과 기능 등급 둘로 있고 앞쪽의 유일한 Extension 행이 Artemis/JMS, 뒤쪽 78번 줄이 Spring Cloud Stream bridge 를 Optional 로 적는다. 마지막으로 bridge 자신의 자바독과 StreamBridgePolicyGuard 의 조건 넷이 각각의 예외 코드와 함께 나온다." caption="Tier 셋과 ENTRIES 의 등급 분포 · 표 밖의 bridge leaf · Entry 가 요구하는 브로커 버전 · 미등록 이름 거부와 그 시험이 쓰는 이름 · 자기 파일 밖 main 사용 0 · 운영자 문서의 등급 표 둘 · 가드의 조건 넷과 예외 코드 — 73줄 · exit 0" zoom="true" -::: - -`:23`\~`:32` 의 `Tier` 는 `STABLE`, `EXPERIMENTAL`, `EXTENSION` 셋이다. `:30` 의 `EXTENSION` 에는 어댑터 SPI 전용이고 지원 집합 밖이라는 설명이 붙어 있다. - -`ENTRIES` 는 다섯이다. `messaging-kafka` 가 `STABLE`(`:85`), 나머지 넷 — `messaging-rabbit`(`:92`), `messaging-kafka-share-experimental`(`:97`), `messaging-pulsar-experimental`(`:104`), `messaging-nats-experimental`(`:109`) — 이 `EXPERIMENTAL` 이다. `Tier.EXTENSION` 을 쓰는 항목은 0 이다. - -## ENTRIES 밖에 있는 messaging-spring-cloud-stream-bridge - -`messaging-spring-cloud-stream-bridge` 는 main 6 파일 507 줄이고 `ENTRIES` 에 이름이 없다. - -`MessagingBindingBridge:8` 의 자바독에는 상호운용 이음매이지 두 번째 메시징 API 가 아니라고 적혀 있다. `StreamBridgePolicyGuard.validate` 는 네 자리에서 거부한다 — `:30` 은 스위치가 꺼져 있으면 `STREAM_BRIDGE_DISABLED`, `:36` 은 순서 범위를 선언한 목적지를 `STREAM_BRIDGE_ORDERING_UNSUPPORTED`, `:41` 은 재시도 모드가 `NONE` 이 아니면 `STREAM_BRIDGE_RETRY_UNSUPPORTED`, `:46` 은 데드레터가 켜져 있으면 `STREAM_BRIDGE_DLQ_UNSUPPORTED` 로 던진다. 뒤 셋의 메시지는 그런 목적지가 네이티브 어댑터로 가야 한다고 적는다. - -`EXTENSION` 자바독은 어댑터 SPI 전용이고 지원 집합 밖이라고 적고, 이 leaf 의 자바독은 두 번째 메시징 API 가 아니라고 적는다. 둘 다 지원 집합 밖에 두는 서술인데 이 leaf 는 `ENTRIES` 에 없다. - -## Entry 생성자가 브로커 버전을 요구한다 - -`Entry` 의 간결 생성자(`:66`\~`:74`)는 어댑터 이름이 공백이면 거부하고(`:67`), 브로커 버전 목록이 비어 있으면 어댑터는 적어도 하나의 브로커 버전을 인증해야 한다는 메시지로 `IllegalArgumentException` 을 던진다(`:70`\~`:71`). - -이 leaf 는 브로커가 아니라 Spring Cloud Stream 바인더 위의 이음매다. 인증할 브로커 버전이 없으므로 지금 형태로는 `ENTRIES` 의 행이 될 수 없다. `Tier.EXTENSION` 이 비어 있는 것은 아무도 채우지 않아서만은 아니고, 그 등급 설명에 맞는 대상이 이 record 의 요구를 통과하지 못하기 때문이기도 하다. - -## 미등록 이름을 of 에 넘겼을 때 - -`of`(`:126`)는 `ENTRIES` 에 없는 이름을 받으면 `:129` 에서 그 이름이 호환성 표에 없다는 메시지로 던진다. `CompatibilityMatrixTest:83` 의 `anUnknownAdapterIsNotSilentlyTreatedAsSupported` 가 그 동작을 고정하는데, 그 시험이 `of` 에 넘기는 미등록 이름이 `"messaging-artemis"` 다(`:84`). - -## 운영자 문서의 두 등급 표 - -`docs/messaging/support-matrix.md` 에는 등급 표가 둘이다. `:29` 의 `## 브로커 등급` 과 `:66` 의 `## 기능 등급` 이다. - -앞쪽 표에서 `Extension` 이 붙은 행은 하나뿐이고 `:37` 의 `Artemis/JMS` 다. 인증 기준은 범위 밖, Stable 기능은 adapter SPI만이라고 적혀 있다 — `Tier.EXTENSION` 자바독과 같은 내용이다. 그런데 코드의 시험은 같은 `messaging-artemis` 를 등록되지 않은 이름의 예로 쓴다. - -뒤쪽 표 `:78` 에 이 leaf 가 있다. 등급 칸의 값은 `Optional` 이고, 그것은 `Tier` 에 없는 낱말이다. - -## 이 표를 읽는 main 코드가 없다 - -`CompatibilityMatrix` 를 자기 파일 밖에서 쓰는 main 코드는 0 건이다. `of` 의 거부도 `hasLiveBrokerCertification`(`:60`)도 시험과 계약 스위트 안에서만 불린다. 등록되지 않은 이름을 조용히 지원으로 두지 않는다는 보장은 시험 경로에 대한 것이지 런타임에 대한 것이 아니다. - -## 원문과 갈리는 자리 - -원문은 이 leaf 가 코드의 표에도 운영자 문서의 표에도 없다고 적었다. 문서 쪽은 그렇지 않다. `:78` 에 행이 있고 등급 칸이 `Optional` 로 채워져 있다. 원문이 본 것은 `:29` 의 브로커 등급 표이고, 이 leaf 는 `:66` 의 기능 등급 표에 있다. - -원문은 `Tier.EXTENSION` 이 쓰이지 않는 것을 미조립의 한 사례로 묶었다. `runtime_memberships` 가 비어 있는 것은 맞지만, 이 record 에 대해서는 `Entry` 의 브로커 버전 요구가 별도의 이유로 작용한다. - -## 확인하지 못한 것 - -JMS 계열 어댑터가 다른 모듈명으로 존재할 가능성은 좁히지 않았다. `src/messaging` 아래에 해당 디렉터리가 없다는 데까지다. - -`EXTENSION` 항목을 실제로 추가해 생성자에서 거부되는 것을 실행으로 보이지 않았다. 거부 조건을 코드로 읽은 데까지다. - -두 등급 표의 어휘가 다른 것이 의도인지 확인하지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a19-f020-messaging-admin-api.md b/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a19-f020-messaging-admin-api.md deleted file mode 100644 index dd53ad3..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a19-f020-messaging-admin-api.md +++ /dev/null @@ -1,143 +0,0 @@ ---- -kind: CASE -slug: a19-f020-messaging-admin-api -title: messaging-admin-api 의 시험 한 파일이 아홉을 담고 만료까지 단언한다 -topic: messaging-and-outbox -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a19-f020-messaging-admin-api -evidenceCapturedOn: 2026-09-04 -body: case-a19-f020-messaging-admin-api.body.md -assets: - - key: a19-f020-messaging-admin-api - file: ../../../final/evidence/rendered/a19-f020-messaging-admin-api.svg -evidence: - - ../../../final/evidence/raw/a19-f020-messaging-admin-api.txt -source: - - 원본 분석 절은 final/document.md#a19#L997 이다. ---- - -# messaging-admin-api 의 시험 한 파일이 아홉을 담고 만료까지 단언한다 - -`messaging-admin-api` 는 main 25 파일 1,613 줄인데 test 는 `DestructiveOperationGuardTest` 한 파일 147 줄이다. 그 한 파일이 담은 `@Test` 는 아홉이고, 만료된 승인에 대한 거부까지 그 안에 있다. - -## 관계 - -- **messaging-reliability-api는 main 13파일 · 817 LOC에 테스트가 0개다** - 두 기록 모두 같은 가족의 leaf 에서 main 대비 test 파일 수를 세는 데서 출발한다. 그 leaf 는 test 소스 세트 자체가 없어 계수가 그대로 결론이 되지만, 여기서는 시험 파일 하나를 열어 아홉이 무엇을 단언하는지 봐야 했다. -- **브로커 ACL 매니페스트의 자기 점검이 존재하지 않는다** - 저 기록은 `BrokerAclManifest` 의 검사 메서드가 코드에 있는데 부르는 main 코드가 없는 경우다. 여기는 검증자를 만드는 코드가 시험 네 파일에만 있는데, 왜 그래야 하는지가 그 타입의 자바독에 적혀 있다. -- **admin 스위치가 가드를 켜고 서비스는 켜지 않는다** - 저 기록은 관리 스위치를 켜도 서비스 빈이 생기지 않는 것을 빈 목록에서 확인했다. 여기서는 같은 사슬의 가드가 `@Bean` 으로 존재하고 검증자만 test 에서 만들어진다는 것을 소스 세트로 확인했다. - -## 문제 - -관리 평면의 계약 타입이 messaging-admin-api 에 스물다섯 개 있고 그 leaf 의 시험 파일은 하나다. - -그 하나가 무엇을 덮고 나머지 검증이 어디 있는지 확인했다. - -## 결론 - -원문이 적은 계수는 맞다. - -DestructiveOperationGuardTest 가 담은 @Test 는 아홉이다. 승인 요구와 만료 거부와 승인 통과가 각각 :63, :73, :102 에 있는데, 셋 다 단언이 걸리는 대상은 DestructiveOperationGuard 가 내는 예외 코드와 메시지다. 아홉 전부가 승인 객체를 한 헬퍼에서 얻는데, 그 헬퍼의 자바독은 아무나 생성할 수 있던 record 를 서명 없이는 얻을 수 없는 타입으로 바꾼 것이 목적이라고 적는다. 재구동의 자기 대상 금지(:113), 배치 상한(:119), TopologyManifest 의 차이 보고(:128, :140)도 같은 파일이다. - -실제 시험은 대부분 messaging-admin-runtime 에 있다. 그 leaf 의 시험 6 파일 1,051 줄이 시험 51 개를 담고, 그중 ApprovalForgeryTest 열 개가 승인 위조 경로를 덮는다. - -HmacApprovalVerifier 를 생성하는 시험 파일은 두 leaf 에 넷이고 생성 지점은 다섯 줄이다. 그 타입을 만드는 main 코드는 0 건이다. - -그것이 공백은 아니다. HmacApprovalVerifier:50~:53 의 자바독이 그 부재를 설계로 못박는다 — 키 보관자가 곧 발급자이므로 실행 런타임에 두어서는 안 된다는 것이다. 같은 사슬의 DestructiveOperationGuard 는 MessagingAdminAutoConfiguration:37 이 @Bean 으로 만들며 false 를 넘기는데, 그 인자가 같은 경계를 값으로 적은 것이다. - -PlanDigest, ApprovalGrant, TopologyManifest 는 그 이름을 건 시험 파일이 모두 0 개다. 세 타입을 참조하는 test 파일은 각각 6, 4, 4 인데 그 시험들은 이름이 가리키는 다른 대상을 단언한다. - -판정은 P3 이고 원문과 같다. 원문이 미확인으로 남긴 예 둘 가운데 ApprovalGrant 의 만료는 DestructiveOperationGuardTest:73 과 ApprovalForgeryTest:158 이 단언하고, PlanDigest 의 정규화만 미확인으로 남는다. - -## 검증 환경 - -확인 방식 : messaging-admin-api 와 messaging-admin-runtime 의 소스 세트 목록과 파일 수·줄 수 계수, 계약 leaf 시험 파일의 @Test 수와 메서드 이름 전수, 소비자 leaf 시험 파일별 @Test 수 계수와 ApprovalForgeryTest 메서드 전수, 계약 leaf 의 main 타입 전수, ApprovalVerifier 의 main 구현과 HmacApprovalVerifier 생성 지점을 leaf·소스 세트별로 분리, 그 타입의 자바독과 MessagingAdminAutoConfiguration 의 가드 빈 확인, 계약 타입 여섯의 main·test 참조 파일 수와 그 이름을 건 시험 파일 검색, PlanDigest 의 test 참조 전수 -소스 수정 : x - -## 재현 조건 - -1. messaging-admin-api 와 messaging-admin-runtime 의 소스 세트를 나열하고 각각의 파일 수와 줄 수를 센다. -2. 계약 leaf 의 시험 파일을 열고 @Test 수와 메서드 이름을 줄 순서대로 나열한다. -3. 각 이름이 무엇을 단언하는지 읽고 계약 타입과 짝짓는다. -4. 소비자 leaf 의 시험 파일마다 @Test 수를 세고, 승인 위조 시험의 메서드를 전부 읽는다. -5. 계약 leaf 의 main 타입을 전부 나열한다. -6. ApprovalVerifier 의 main 구현을 찾고, HmacApprovalVerifier 를 생성하는 자리를 leaf 와 소스 세트로 갈라 센다. -7. 그 타입의 자바독에서 main 에 두지 않는 이유가 적혀 있는지 읽는다. -8. 같은 사슬의 다른 타입이 자동 설정에서 빈으로 만들어지는지 확인하고 인자를 읽는다. -9. 계약 타입 몇 개의 main·test 참조 파일 수를 세고, 그 이름을 건 시험 파일을 *Test·*Tests·*IT 로 넓혀 찾는다. -10. 만료를 단언하는 자리를 검색하고, PlanDigest 가 test 에서 어떻게 쓰이는지 전수로 읽는다. - -## 본문 - - - -`messaging-admin-api` 는 관리 평면의 계약 타입을 담는 leaf 다. main 25 파일 1,613 줄에 test 는 `DestructiveOperationGuardTest` 한 파일 147 줄이다. 그 비율만 보면 승인 사슬이 검증되지 않은 것처럼 읽힌다. - -## DestructiveOperationGuardTest 한 파일이 담은 시험 아홉 - -:::evidence key="a19-f020-messaging-admin-api" alt="저장소 루트에서 돌린 정적 검색 출력 95줄. messaging-admin-api 와 messaging-admin-runtime 이 각각 main test 두 소스 세트만 갖고 25/1613·1/147, 12/1253·6/1051 이라는 계수가 먼저 나온다. 계약 leaf 의 유일한 시험 파일이 @Test 아홉을 담고 그 메서드 이름과 줄 번호가 줄 순서대로 나열되며, 이어서 그 시험들이 승인 객체를 얻는 통로인 헬퍼의 자바독과 verify 호출 줄, 각 단언이 기다리는 문자열, 그리고 가드가 그 문자열을 내는 자리가 나온다. 소비자 leaf 시험 여섯의 @Test 개수가 이어지고 그중 승인 위조 시험 열 개의 메서드 이름이 모두 나온다. 계약 leaf 의 main 타입 스물다섯이 이름으로 실리고, ApprovalVerifier 의 main 구현이 하나이며 HmacApprovalVerifier 생성 지점이 다섯 곳 파일 네 개로 전부 test 소스 세트라는 것이 leaf 이름과 함께 나온다. 이어서 그 검증자를 실행 런타임에 두지 않는 이유를 적은 자바독 네 줄과, 같은 사슬의 가드를 @Bean 으로 만드는 자동 설정 일곱 줄이 원문 그대로 실린다. 마지막으로 계약 타입 여섯의 main·test 참조 파일 수와 그 이름을 건 시험 파일이 모두 0 개라는 것, 만료를 고정하는 두 자리, 그리고 PlanDigest 가 test 에서 쓰이는 열두 줄이 나온다. 긴 문자열 리터럴은 가려져 있다." caption="두 leaf 의 소스 세트와 계수 · 계약 leaf 시험 아홉의 이름과 단언 대상 · 승인 객체를 얻는 헬퍼 · 소비자 시험 여섯과 위조 시험 열 · main 타입 스물다섯 · 검증자 생성 지점 다섯 곳 전부 test · 그것이 경계라고 적은 자바독과 main 빈으로 있는 가드 · 타입별 참조와 전용 시험 0 · 만료를 고정하는 두 자리 — 123줄 · exit 0" zoom="true" -::: - -파일 이름은 `DestructiveOperationGuard` 만 가리키는데 `@Test` 는 아홉이다. - -이름에 승인이 들어간 셋이 승인 판정을 단언한다. `anAdminRuntimeStillNeedsAnApproval`(`:63`)이 승인 없는 관리 런타임을 거부하는 것을, `anExpiredApprovalDoesNotAuthorise`(`:73`)가 만료된 승인이 권한을 주지 않는 것을, `anApprovedAdminOperationIsAuthorised`(`:102`)가 승인된 연산이 통과하는 것을 단언한다. - -셋의 단언이 떨어지는 곳은 모두 `DestructiveOperationGuard` 다. `:69` 가 `"approval"` 을, `:87` 이 `"validity window"` 를 기다리는데 그 문자열은 `DestructiveOperationGuard:66` 의 `APPROVAL_REQUIRED` 와 `:70` 의 `APPROVAL_EXPIRED` 메시지에서 온다. `:109` 는 예외가 없는 것만 본다. - -셋이 검증자를 지나는 정도는 서로 다르다. `:63` 은 `Optional.empty()` 를 넘기므로 승인 객체를 만들지 않는다. `:102` 는 정적 필드 `VALID` 를 쓰는데 그것은 클래스 로딩 때 한 번 만들어진다. `:73` 만 자기 시험 안에서 만료 구간을 지정해 승인 객체를 새로 만든다. - -승인 객체를 만드는 통로는 `verified(...)` 헬퍼 하나이고 `:47` 에서 `ISSUER.verify(grant, ISSUER.sign(grant), digest, from)` 을 부른다. 그 헬퍼의 자바독(`:30`\~`:32`)은 가드가 예전에는 아무나 생성할 수 있는 `AdminApproval` 을 그대로 받았고, 지금은 모든 경우가 실제 서명을 지나야 승인 객체를 얻는다고 적는다. 타입을 바꾼 목적이 그것이라는 것이다. - -나머지 여섯은 줄 순서대로 이렇다. `anApplicationRuntimeCannotRedrive`(`:51`), `aDryRunIsAlwaysPermitted`(`:91`), `aRedriveCannotTargetItsOwnSource`(`:113`), `aRedriveBatchIsBoundedSoOneOperationCannotFloodTheSource`(`:119`), `aTopologyManifestReportsEveryDifference`(`:128`), `aMatchingTopologyReportsNoDifferences`(`:140`) 다. 뒤의 둘은 `TopologyManifest` 가 차이를 전부 보고하는 경우와 일치할 때 하나도 보고하지 않는 경우를 짝으로 단언한다. - -## messaging-admin-runtime 의 시험 6 파일이 담은 51 개 - -소비자 leaf 는 `messaging-admin-runtime` 하나이고 main 12 파일 1,253 줄에 시험 6 파일 1,051 줄이다. `@Test` 수는 `TopologyValidatorTest` 13, `ApprovedPlanExecutionTest` 11, `ApprovalForgeryTest` 10, `AdminOperationJournalTest` 8, `RedriveResumptionTest` 5, `TopologyValidationRuntimeTest` 4 로 합계 51 이다. - -`ApprovalForgeryTest` 열 개가 승인 사슬의 위조 경로를 덮는다. 검증자 밖에서 `VerifiedApproval` 을 만들 수 없다는 것(`:51`), 변조된 grant 가 검증되지 않는 것(`:69`), 다른 발급자의 서명이 검증되지 않는 것(`:84`), 한 계획의 승인이 다른 계획을 실행하지 못하는 것(`:96`), 만료된 grant 가 검증되지 않는 것(`:158`), 승인자와 운영자가 달라야 한다는 것(`:172`)이 각각 단언된다. - -## HmacApprovalVerifier 를 main 에 두지 않는 것은 경계다 - -`ApprovalVerifier` 의 main 구현은 `HmacApprovalVerifier` 하나다. 그것을 생성하는 지점은 다섯 곳이고 파일은 넷이다. 하나는 `messaging-admin-api` 의 `DestructiveOperationGuardTest:21` 이고 나머지 셋은 `messaging-admin-runtime` 의 `ApprovalForgeryTest:42`·`:46`, `ApprovedPlanExecutionTest:37`, `RedriveResumptionTest:45` 다. main 에는 생성하는 코드가 없다. - -그 부재가 검증 공백은 아니다. `HmacApprovalVerifier:50`\~`:53` 은 이 키를 쥔 쪽이 곧 발급자이며 그것이 연산을 실행하는 런타임에 있어서는 안 된다고 적는다. - -같은 사슬의 다른 끝은 main 빈으로 있다. `MessagingAdminAutoConfiguration:35`\~`:41` 이 `@Bean @ConditionalOnMissingBean` 으로 `DestructiveOperationGuard` 를 만들면서 `false` 를 넘기고, 주석은 애플리케이션 런타임이 관리 자격을 갖지 않으므로 가드가 그런 연산을 거부한다고, 운영자 도구가 이 빈을 `true` 로 덮어쓴다고 적는다. 생성자에 넘기는 `false` 가 자바독이 말한 경계를 그대로 인코딩한 값이다. - -## PlanDigest·ApprovalGrant·TopologyManifest 를 이름으로 건 시험 파일이 0 개다 - -계약 타입 여섯의 참조를 셌다. `PlanDigest` main 10 · test 6, `VerifiedApproval` main 7 · test 4, `TopologyManifest` main 5 · test 4, `ApprovalGrant` main 3 · test 4, `ApprovedRedrivePlan` main 2 · test 2, `ApprovedReplayPlan` main 2 · test 1 이다. - -여섯 모두 그 이름을 건 시험 파일이 0 개다. `*Test.java` 뿐 아니라 이 저장소가 쓰는 `*IT.java` 와 `*Tests.java` 까지 넓혀 세도 0 이다. - -만료는 두 자리가 고정한다. `DestructiveOperationGuardTest:73` 이 가드 쪽에서, `ApprovalForgeryTest:158` 의 `anExpiredGrantDoesNotVerify` 가 검증자 쪽에서 단언한다. - -`PlanDigest` 는 다르다. test 에서 나오는 열두 줄이 전부 `PlanDigest.ofCanonical(...)` 로 다이제스트를 만들거나 파라미터로 받는 자리다. 정규화 자체를 단언하는 줄은 없다. - -## 원문과 갈리는 자리 - -원문은 계약 leaf 의 test 를 `1 (DestructiveOperationGuardTest)` 로만 적었다. 파일 이름은 그렇지만 담긴 아홉 중 셋이 승인 판정을, 둘이 `TopologyManifest` 를 단언한다. - -원문은 계약 자체의 경계 조건이 별도로 고정돼 있는지 확인되지 않는다고 적으면서 `ApprovalGrant` 의 만료를 예로 들었다. 그 예는 `DestructiveOperationGuardTest:73` 과 `ApprovalForgeryTest:158` 이 고정한다. `PlanDigest` 의 정규화만 남는다. - -원문 §8.2 는 main 참조가 0 인 다섯 중 넷에는 이유가 적혀 있지 않다고 하면서 `HmacApprovalVerifier` 를 그 넷에 넣었다. 이 타입에는 이유가 자기 자바독에 적혀 있다. - -## 계약 타입 목록에 대해 - -원문은 계약 타입을 열 개 들고 `등` 으로 닫았다. 이 leaf 의 main 타입은 스물다섯이고, 그 열에 없는 `AdminOperationLease` 와 `AdminOperationState` 와 `DestinationTopology` 도 같은 스물다섯에 들어 있다. - -## 확인하지 못한 것 - -시험을 한 번도 돌리지 않았다. 세고 읽는 데 그쳤다. - -아홉을 셋과 여섯으로 나눈 기준은 메서드 이름과 단언 대상이다. 실행 경로를 계측해 가른 것이 아니다. - -다른 이름을 단 시험이 `PlanDigest` 의 정규화를 고정하고 있을 가능성은 test 참조 열두 줄까지 읽고 좁혔다. - -두 leaf 의 시험을 실행하지 않았다. 파일과 `@Test` 를 세고 메서드 이름과 단언 대상을 읽은 데까지다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a19-f022-messagingpublicsurfacecontracttest.md b/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a19-f022-messagingpublicsurfacecontracttest.md deleted file mode 100644 index 45ff5ee..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a19-f022-messagingpublicsurfacecontracttest.md +++ /dev/null @@ -1,122 +0,0 @@ ---- -kind: CASE -slug: a19-f022-messagingpublicsurfacecontracttest -title: 공개 표면 계약 시험이 가족 밖 app-bootstrap 에 있다 -topic: messaging-and-outbox -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a19-f022-messagingpublicsurfacecontracttest -evidenceCapturedOn: 2026-09-04 -body: case-a19-f022-messagingpublicsurfacecontracttest.body.md -assets: - - key: a19-f022-messagingpublicsurfacecontracttest - file: ../../../final/evidence/rendered/a19-f022-messagingpublicsurfacecontracttest.svg -evidence: - - ../../../final/evidence/raw/a19-f022-messagingpublicsurfacecontracttest.txt -source: - - 원본 분석 절은 final/document.md#a19#L1114 이다. ---- - -# 공개 표면 계약 시험이 가족 밖 app-bootstrap 에 있다 - -지침이 이 규칙을 붙든다고 지목한 `MessagingPublicSurfaceContractTest` 가 가족 트리가 아니라 `src/app-bootstrap/src/test` 에 있다. 원문은 이것을 자동 검증이 없는 사례로 읽었는데, 경로 필터가 없는 `ci-quality-gates.yml` 이 모든 `pull_request` 에서 `./gradlew check` 를 돌리고 그 안에 이 시험이 들어 있다. - -## 관계 - -- **아무도 돌리지 않는 레인의 게이트는 마지막으로 돌린 사람이 본 것을 보고한다** - 원문이 이 사례를 그 규칙으로 읽었다. 그런데 이 시험은 경로 필터 없는 워크플로에 실려 PR 마다 새로 도므로, 인용된 문장이 겨냥한 상태가 아니다. -- **문서 계약 테스트의 단언 경계 밖에 발견된 드리프트 세 건이 전부 있었다** - 둘 다 문서가 어떤 규칙을 시험이 지킨다고 적었는데, 그 시험이 실제로 붙드는 범위가 문서를 읽은 사람이 기대하는 범위보다 좁다. 저 기록은 단언 여덟 개 밖의 드리프트이고, 여기는 가족 태스크가 고르지 않는 소스 트리다. - -## 문제 - -가족 지침은 :39 에서 이 규칙을 붙드는 것이 문서가 아니라 MessagingPublicSurfaceContractTest 라고 적는다. - -그 시험이 어느 트리에 있고 어떤 경로로 실행되는지 확인했다. - -## 결론 - -MessagingPublicSurfaceContractTest 가 놓인 트리는 src/app-bootstrap/src/test 이고 패키지는 dev.caskeleton.bootstrap.contract.messaging 이다. src/messaging 아래 24 개 트리에는 그 이름이 없다. - -담긴 시험은 셋이고 @Tag 는 0 개다. 하나는 api 선언 누락을, 하나는 검사가 헛도는 경우를, 하나는 broker SDK 두 개의 컴파일 클래스패스 유출을 각각 잡는다. - -가족 태스크는 이 클래스를 고르지 않는다. :messaging:messaging-kafka:test 에 이름을 넘기면 시험을 찾지 못하고 빌드가 실패한다. - -가족 경로에서 도는 messaging-certification.yml 도 이 시험을 돌리지 않는다. 그 워크플로의 gradle 호출은 Kafka 인증 증거 검증 태스크 하나뿐이다. - -그러나 원문이 읽은 것과 달리 자동 경로가 있다. ci-quality-gates.yml 은 paths 필터 없이 모든 pull_request 에서 돌고 :50 에서 ./gradlew check 를 돌린다. check 가 모든 레인을 덮지는 않는다는 경고가 그 아래 주석에 있어 이 프로젝트만 따로 확인했다. dry-run 그래프에 :app-bootstrap:test 가 있고, 그 태스크로 실행하면 test/ 아래에 tests=3 failures=0 이 남는다. - -판정은 P3 이고 원문과 같다. 다만 근거가 다르다. 원문은 이 계약을 자동으로 검증하는 경로가 없다는 것을 들었는데, 검증은 매 PR 에서 돈다. 남는 것은 가족 태스크에 이 시험이 없어 로컬에서 위반을 보지 못한다는 것이다. - -## 검증 환경 - -Gradle : 9.0.0 -확인 방식 : 지침 문장 원문 확인, 시험 클래스의 소스 트리·패키지·태그 확인과 단언 메서드 전수, 가족 test 트리 계수, 가족 경로 워크플로의 트리거 전부와 실행 명령 확인, 경로 필터 없는 워크플로의 트리거·job 조건·실행 명령과 그 아래 주석 확인, :app-bootstrap:check 의 태스크 그래프 dry-run, 루트 test 규약의 제외 태그 확인, 그 시험을 이름으로 지정한 실제 실행과 결과 XML 계수, 가족 태스크에 같은 이름을 넘긴 실행 -소스 수정 : x - -## 재현 조건 - -1. 가족 지침에서 공개 표면 규칙을 붙든다고 적은 문장과 그 시험이 무엇을 대조하는지 문장 끝까지 읽는다. -2. 그 클래스를 찾아 소스 트리와 패키지를 확인하고, 가족 트리에 동명 파일이 있는지 센다. -3. 그 파일의 @Tag 와 @Disabled 를 세고 단언 메서드를 전부 읽는다. -4. 가족 경로에서 도는 워크플로의 트리거를 전부 읽고 실행 명령을 확인한다. -5. 경로 필터가 없는 워크플로를 찾고 job 조건과 실행 명령을 읽는다. -6. 그 명령 주변 주석에서 check 의 포괄 범위에 대한 경고가 있는지 본다. -7. check 를 dry-run 해 그 프로젝트의 test 가 그래프에 있는지 확인한다. -8. 루트 규약이 test 에 거는 제외 태그를 읽는다. -9. 그 시험을 이름으로 지정해 실제로 돌리고 결과 XML 의 디렉터리와 계수를 읽는다. -10. 같은 이름을 가족 태스크에 넘겨 돌린다. - -## 본문 - - - -`src/messaging/CLAUDE.md:39` 는 의존성 노출 규칙을 문서가 아니라 `MessagingPublicSurfaceContractTest` 가 붙들고 있다고 적는다. 그 클래스는 `src/messaging` 아래에 없다. - -## 지침이 지목한 시험과 그 트리 - -:::evidence key="a19-f022-messagingpublicsurfacecontracttest" alt="저장소 루트에서 돌린 정적 검색과 gradle 실행을 합친 출력 76줄. src/messaging/CLAUDE.md 의 36번부터 44번 줄이 원문 그대로 실려 규칙과 그 시험이 무엇을 대조하는지가 문장 끝까지 보인다. 이어서 그 클래스가 app-bootstrap 의 test 트리에 있고 src/messaging 아래 동명 파일이 0 개이며 가족 test 트리가 24 개라는 것, 그 파일의 @Test 가 3 이고 @Tag 와 @Disabled 가 0 이라는 것과 세 메서드 이름이 나온다. messaging-certification 워크플로의 트리거 세 가지가 경로와 cron 과 수동 실행까지 모두 나오고 그것이 돌리는 gradle 명령이 Kafka 인증 증거 검증이라는 것이 보인다. ci-quality-gates 는 on 블록 1번부터 9번 줄까지 실려 paths 필터가 0 건이라는 계수가 붙고, job 정의와 check 를 돌리는 step, 그리고 그 바로 아래 check 가 graphqlStableTest 에 의존하지 않는다고 적은 주석 다섯 줄이 함께 나온다. 마지막으로 app-bootstrap 의 check 를 dry-run 한 결과에 test 가 들어 있는 것, 루트 규약이 거는 유일한 필터, 그 시험을 이름으로 골라 실제로 돌린 결과가 결과 디렉터리 test 에 시험 3 개 실패 0 으로 나온 것, 같은 이름을 가족 태스크로 고르면 시험을 찾지 못하고 빌드가 실패하는 것이 나온다." caption="지침 원문 · 시험 클래스의 트리와 태그와 세 단언 · 가족 워크플로의 트리거 전부와 실행 명령 · 경로 필터 0 인 워크플로와 check 가 모든 시험을 덮지 않는다는 주석 · check 그래프에 있는 test · 이름으로 고른 실행 결과 3개 · 가족 태스크는 찾지 못함 — 76줄 · exit 0" zoom="true" -::: - -`CLAUDE.md:39`\~`:43` 은 이 시험이 leaf 의 production source 에서 public·protected 시그니처에 등장하는 vendor 라이브러리를 뽑아 그 leaf 의 `build.gradle` 이 `api` 로 선언했는지 대조하고, starter 의 `api` closure 에 broker client 둘이 들어오지 않는 것도 함께 본다고 적는다. - -클래스는 `src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/contract/messaging/` 에 있다. 가족 test 트리는 24 개인데 그중 어디에도 같은 이름의 파일이 없다. - -`@Test` 는 셋이다. `vendorTypesInPublicSignaturesAreDeclaredApi`(`:73`)가 공개 시그니처의 vendor 타입이 `api` 로 선언됐는지 보고, `theScanIsNotVacuous`(`:94`)가 그 검사가 아무것도 못 찾은 채로 통과하는 경우를 막고, `neitherBrokerSdkReachesAnAdoptersCompileClasspath`(`:114`)가 두 broker SDK 가 채택자의 컴파일 클래스패스에 닿지 않는 것을 본다. - -## 가족만 바꾸고 가족 태스크만 돌릴 때 - -`:messaging:*:test` 에 이 클래스 이름을 넘겨 봤다. `No tests found for given includes` 로 빌드가 실패한다. - -가족 경로에서 도는 워크플로는 `messaging-certification.yml` 이다. `:16`\~`:19` 가 `src/messaging/**` 와 자기 워크플로 파일에서 돌게 하고 `:20`\~`:22` 가 주 1회 cron 과 수동 실행을 더한다. `:52` 가 돌리는 것은 `:messaging:messaging-kafka:verifyMessagingCertificationEvidence` 다. Kafka 인증 증거이지 이 계약이 아니다. - -## ci-quality-gates.yml 은 경로 필터 없이 매 PR 에서 check 를 돌린다 - -`:3`\~`:9` 의 `on` 블록은 `pull_request` 와 `main` 푸시와 수동 실행이고 `paths` 가 0 건이다. `:20` 의 `quality-gates` job 에 조건이 없고, `:49`\~`:50` 이 `working-directory: src` 에서 `./gradlew check verifyPublicPathSnapshot verifyDependencyLocks` 를 돌린다. - -`check` 가 모든 시험 태스크를 덮는다고 가정할 수는 없다. 바로 아래 `:51`\~`:55` 주석이 `check` 는 `graphqlStableTest` 에 의존하지 않으며 그래서 그 레인의 가드가 CI 에서 아무것도 지키지 못했다고 적는다. 그래서 이 프로젝트에 대해 직접 확인했다. - -`:app-bootstrap:check` 의 태스크 그래프에 `:app-bootstrap:test` 가 있다. 루트 규약(`src/build.gradle:549`\~`:553`)이 `test` 에 거는 필터는 `excludeTags 'quarantine'` 하나이고 이 클래스에는 `@Tag` 가 0 개다. - -그리고 이름으로 골라 실제로 돌렸다. `BUILD SUCCESSFUL` 이고 결과가 `test/` 디렉터리에 `tests=3 failures=0 skipped=0` 으로 남는다. 기본 `test` 가 이 시험을 고른다. - -## 원문과 갈리는 자리 - -원문은 결과를 둘로 적었다. 가족 태스크만 돌리면 검증되지 않는다는 것과 가족 경로 워크플로가 Kafka 인증 레인이라는 것이다. 둘 다 맞다. - -원문은 그 둘에서 이 사례가 모듈 18 §4.1c 의 문장 — 아무도 지역에서 돌리지 않는 레인의 붉은 게이트는 마지막으로 돌린 사람이 본 것을 보고한다 — 과 같은 형태라고 읽었다. 그 문장은 여기에 붙지 않는다. `ci-quality-gates.yml` 이 경로 필터 없이 매 PR 에서 `check` 를 돌리고, 그 안에 이 시험이 들어 있다. 이 게이트가 보고하는 것은 마지막으로 돌린 사람이 본 것이 아니라 그 PR 에서 새로 돈 결과다. - -`api`·`implementation` 분리가 이 가족의 정책이고 위반이 이 가족의 `build.gradle` 에서 난다는 서술은 맞다. 다른 것은 검증의 유무가 아니라 그 검증이 실행되는 시점이다. 가족 태스크에는 이 시험이 없고 `ci-quality-gates.yml` 의 `check` 에만 있다. - -## 확인하지 못한 것 - -가족 leaf 에 `api` 누락을 심어 `:messaging:*:test` 가 초록으로 끝나는 것을 재현하지 않았다. 그 태스크가 이 클래스 이름을 찾지 못한다는 것까지 실행으로 확인했다. - -GitHub 러너에서 `./gradlew check` 를 돌리지 않았다. 워크플로가 그 명령을 돌린다는 것, `check` 의 그래프에 `:app-bootstrap:test` 가 있다는 것, 그 태스크가 이 시험을 고른다는 것을 각각 확인해 이었다. - -워크플로 스물여덟 개의 트리거를 전수로 조사하지 않았다. 이 계약과 관련된 둘을 읽었다. - -워크플로 스물여덟 개의 트리거를 전수로 조사하지 않았다. 이 계약과 관련된 둘을 읽었다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-analysis-finding-a19-f001.md b/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-analysis-finding-a19-f001.md deleted file mode 100644 index bba6812..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-analysis-finding-a19-f001.md +++ /dev/null @@ -1,121 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a19-f001 -title: capability 12개 중 main 코드가 읽는 것은 3개, 거부하는 것은 1개 -topic: messaging-and-outbox -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a19-f001 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a19-f001.body.md -assets: - - key: analysis-finding-a19-f001 - file: ../../../final/evidence/rendered/analysis-finding-a19-f001.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a19-f001.txt -source: - - 원본 분석 절은 final/document.md#a19#L231 이다. ---- - -# capability 12개 중 main 코드가 읽는 것은 3개, 거부하는 것은 1개 - -능력 레코드의 자바독이 없는 능력을 요구하면 플랫폼이 크게 실패한다고 선언한다. 열두 깃발 중 아홉을 주 코드가 읽지 않고, 읽는 셋 중 둘은 거부가 아니라 분기다. 거부하는 것은 하나뿐이다. - -## 관계 - -- **선언된 Advanced 능력 11개 중 9개는 켜는 방법이 없다** - 같은 형태의 능력 선언과 실제 어긋남이다. -- **조용히 약해진 보증은 사고 전까지 동작하는 것과 구분되지 않는다** - 자바독이 적은 근거다. -- **분기와 거부는 다른 답이다** - 이 사례가 그 규칙의 형태다. - -## 문제 - -능력 레코드의 클래스 자바독이 계약을 선언한다. - -프로파일이 여기 없는 것을 요구하면 플랫폼이 크게 실패한다는 것이다. 가능하면 시작 시점에, 아니면 능력 예외로 실패하지 조용히 저하되지 않는다는 것이다. - -그 근거도 적는다. 조용히 약해진 보증은 사고가 나기 전까지 동작하는 보증과 구분되지 않기 때문이라는 것이다. - -열두 깃발 전체에 대해 주 코드와 테스트 참조를 셌다. - -## 결론 - -읽는 것은 셋이다. - -순서 있는 흐름은 재시도 결정 엔진에서 읽고, 있으면 순서 보존 재시도를 고른다. - -지연 배달도 같은 엔진에서 읽고, 있으면 브로커 지연 방식을 쓴다. - -중복 제거 발행은 발행자에서 읽고, 없으면 예외를 던진다. - -나머지 아홉은 모든 브로커 어댑터가 선언하지만 주 코드 어디서도 읽지 않는다. - -브로커 확인과 복제 또는 지속 증거와 메시지 단위 정산과 배치 정산, 키 기반 순서와 재생과 브로커 트랜잭션과 기본 죽은 편지와 위상 관리다. - -읽는 셋 중 둘은 거부가 아니라 분기다. - -없으면 재시도 엔진이 조용히 다른 방식을 고른다. 자바독이 조용한 저하라고 부른 그 동작이다. - -거부하는 것은 중복 제거 발행 하나뿐이다. - -계수에는 주의할 점이 하나 있다. 순서 있는 흐름의 주 참조 다섯 건 중 넷은 프레임워크의 동명 메서드로 잡힌 오탐이다. 실제 깃발 참조는 한 건이다. - -판정은 P2 다. - -## 검증 환경 - -확인 방식 : 깃발별 참조 계수와 동명 오탐 제거 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/252 계열에 있다. - -1. 능력 레코드의 클래스 자바독을 읽는다. -2. 열두 깃발 이름을 나열한다. -3. 각 이름의 주 참조와 테스트 참조를 센다. -4. 동명 메서드로 잡힌 오탐을 제거한다. -5. 읽는 지점에서 무엇을 하는지 확인한다. - -## 본문 - - - -`MessagingCapabilities`의 클래스 javadoc이 이 record의 계약을 선언한다. - -> "When a profile asks for something absent here **the platform fails loudly** — at startup where possible, otherwise with a capability exception — rather than quietly degrading, because **a silently weakened guarantee is indistinguishable from a working one until the incident.**" - -## MessagingCapabilities 참조 위치 - -:::evidence key="analysis-finding-a19-f001" alt="코드베이스에서 MessagingCapabilities 를 검색한 출력 13줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingCapabilities 코드베이스 검색 — 13줄 · exit 0" zoom="true" -::: - -## 12개 플래그의 참조를 전수로 셌다 - -| 플래그 | main | test | main에서 하는 일 | -|---|---|---|---| -| `brokerAcknowledgement` | 0 | 1 | — | -| `replicationOrPersistenceEvidence` | 0 | 0 | — | -| `perMessageSettlement` | 0 | 1 | — | -| `batchSettlement` | 0 | 0 | — | -| `orderedStream` | **1** | 1 | `DefaultRetryDecisionEngine:49` — 있으면 순서보존 재시도 선택 | -| `keyedOrdering` | 0 | 3 | — | -| `replay` | 0 | 2 | — | -| `delayedDelivery` | **1** | 0 | `DefaultRetryDecisionEngine:64` — 있으면 BROKER_DELAYED 사용 | -| `brokerTransaction` | 0 | 3 | — | -| `deduplicatedPublish` | **1** | 1 | `DefaultMessagePublisher:250` — **없으면 예외** | -| `nativeDeadLetter` | 0 | 1 | — | -| `topologyManagement` | 0 | 0 | — | - -## 읽는 것은 셋, 거부하는 것은 하나 - -javadoc이 선언한 "fails loudly"가 실제로 성립하는 플래그는 `deduplicatedPublish` 하나다. P2. - -## 확인하지 못한 것 - -능력이 없는 브로커로 프로파일을 구성해 각 깃발의 동작 차이를 재현하지 않았다. 참조 계수상 아홉은 차이가 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-analysis-finding-a19-f002.md b/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-analysis-finding-a19-f002.md deleted file mode 100644 index 260db42..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-analysis-finding-a19-f002.md +++ /dev/null @@ -1,122 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a19-f002 -title: 8개 profile validator 중 조립에서 실행되는 것은 3개 -topic: messaging-and-outbox -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a19-f002 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a19-f002.body.md -assets: - - key: analysis-finding-a19-f002 - file: ../../../final/evidence/rendered/analysis-finding-a19-f002.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a19-f002.txt -source: - - 원본 분석 절은 final/document.md#a19#L288 이다. ---- - -# 8개 profile validator 중 조립에서 실행되는 것은 3개 - -시작 검증 도우미의 자바독이 이미 한 번 고쳐진 같은 결함을 서술한다. 검증기들이 전부 빈이었는데 아무 데도 주입되지 않아 아무것도 검증하지 않았다는 것이다. 그 수정이 적용된 것은 둘이고, 주 소스의 검증기 여덟 중 실행되는 것은 셋이다. - -## 관계 - -- **capability 12개 중 main 코드가 읽는 것은 3개이고 거부하는 것은 1개다** - 같은 가족의 같은 형태다. -- **브로커 권한 매니페스트의 자기 점검이 존재하지 않는다** - 같은 계열의 시작 검증 부재다. -- **같은 결함이 이미 한 번 고쳐졌는데 나머지에는 적용되지 않았다** - 이 사례가 그 규칙의 형태다. - -## 문제 - -시작 검증 도우미의 자바독이 이미 고쳐진 결함을 서술한다. - -브로커 두 종과 보안 검증기가 전부 빈이었는데 아무 데도 주입되지 않았다는 것이다. 컨텍스트가 브로커마다 검증기를 발행하고 아무것도 검증하지 않았다는 것이다. - -브로커가 줄 수 없는 보증을 약속하는 프로파일이 깨끗하게 부팅하고 그것에 의존하는 첫 메시지에서 실패한다는 것이다. - -수정 방식도 정확하다. - -초기화 콜백으로 돌려서 컨텍스트가 아직 만들어지는 중에 실패하고 원인이 된 프로파일 빈이 스택에 이름으로 남게 한다. - -그리고 프로파일을 공급자로 받는다. 애플리케이션이 선언한 빈과 설정에서 컴파일된 프로파일 두 출처를 모두 보기 위해서다. - -## 결론 - -그 수정이 적용된 것은 둘이다. - -주 소스에 존재하는 프로파일 검증기 여덟 전체의 도달성을 확인했다. - -목적지 검증기는 핵심 자동 설정이 검증 메서드를 직접 부른다. 실행된다. - -브로커 두 종의 검증기는 각자의 자동 설정이 시작 검증 도우미로 감싼다. 실행된다. - -브로커 트랜잭션 검증기는 출하 리프에 있고 자동 설정이 빈으로 선언만 한다. 주입처가 없다. 실행되지 않는다. - -나머지는 빌드 전용 리프이거나 조립 지점이 없다. - -즉 여덟 중 셋만 실행된다. - -그리고 실행되지 않는 것 중 하나는 출하 리프의 것이다. - -판정은 P2 다. - -## 검증 환경 - -Spring Boot : 4.0.8 -확인 방식 : 검증기별 조립 지점과 주입처 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/252 계열에 있다. - -1. 시작 검증 도우미의 자바독을 읽는다. -2. 주 소스의 프로파일 검증기를 모두 나열한다. -3. 각각의 리프와 출하 여부를 확인한다. -4. 각각의 조립 지점을 찾는다. -5. 빈 선언만 있고 주입처가 없는 것을 가려낸다. - -## 본문 - - - -`StartupProfileValidation`의 javadoc이 **이미 한 번 고쳐진 같은 결함**을 서술한다. - -> "The Kafka, RabbitMQ and security validators were all beans and **none of them was injected anywhere**: the context published a validator per broker and **validated nothing**. A profile that promises a guarantee its broker cannot give — an exactly-once claim on a non-transactional producer, a quorum ack on a single replica, a plaintext credential on a production listener — then boots cleanly and fails on the first message that depends on it." - -## StartupProfileValidation 참조 위치 - -:::evidence key="analysis-finding-a19-f002" alt="코드베이스에서 StartupProfileValidation 를 검색한 출력 4줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="StartupProfileValidation 코드베이스 검색 — 4줄 · exit 0" zoom="true" -::: - -## 수정 방식도 정확하다 - -`InitializingBean.afterPropertiesSet`으로 돌려서 "컨텍스트가 아직 만들어지는 중에 실패하고 원인이 된 프로파일 bean이 스택에 이름으로 남게" 한다. 그리고 프로파일을 `ObjectProvider`가 아니라 `Supplier`로 받는다 — 애플리케이션이 선언한 bean과 `app.messaging`에서 컴파일된 프로파일 두 출처를 모두 보기 위해서다. - -## 여덟 validator의 도달성 - -| validator | leaf | 출하? | 조립 지점 | 실행되는가 | -|---|---|---|---|---| -| `DestinationProfileValidator` | policy | 출하 | `MessagingCoreAutoConfiguration:134` | **✔** | -| `KafkaProfileValidator` | kafka | 출하 | `KafkaMessagingAutoConfiguration:58` → `StartupProfileValidation` | **✔** | -| `RabbitProfileValidator` | rabbit | 출하 | `RabbitMessagingAutoConfiguration:54` → `StartupProfileValidation` | **✔** | -| `KafkaTransactionProfileValidator` | kafka | **출하** | `KafkaMessagingAutoConfiguration:74` — @Bean 선언만, 주입처 없음 | **✘** | -| `KafkaShareProfileValidator` | kafka-share | build-only | registrar가 보유, 테스트에서만 생성 | ✘ (등급 일치) | -| `NatsJetStreamProfileValidator` | nats | build-only | 참조가 javadoc 문장 하나 | ✘ (등급 일치) | -| `PulsarProfileValidator` | pulsar | build-only | **참조 0건** — 테스트조차 없다 | ✘ (등급 일치) | -| `BindingProfileValidator` | scs-bridge | build-only | 테스트에서만 생성 | ✘ (등급 일치) | - -## build-only 넷은 등급과 일치한다 - -어떤 런타임에도 오르지 않으므로 조립 지점이 없는 것이 등급과 일치한다 — 오늘의 사고가 아니라 채택 시점의 부채다. 다만 `PulsarProfileValidator`는 **테스트조차 없어서** 다른 셋과도 다르다. P2. - -## 확인하지 못한 것 - -잘못된 프로파일로 띄워 검증되지 않는 것을 재현하지 않았다. 주입처 부재상 그 결과가 나온다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-analysis-finding-a19-f004.md b/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-analysis-finding-a19-f004.md deleted file mode 100644 index 6eacb34..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-analysis-finding-a19-f004.md +++ /dev/null @@ -1,124 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a19-f004 -title: 스키마 호환성 검증기는 출하 leaf에 있고, main 코드에서 호출되지 않는다 -topic: messaging-and-outbox -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a19-f004 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a19-f004.body.md -assets: - - key: analysis-finding-a19-f004 - file: ../../../final/evidence/rendered/analysis-finding-a19-f004.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a19-f004.txt -source: - - 원본 분석 절은 final/document.md#a19#L390 이다. ---- - -# 스키마 호환성 검증기는 출하 leaf에 있고, main 코드에서 호출되지 않는다 - -검증기의 주 참조가 0 건이다. 그것이 거부하도록 만들어진 검사 없음 모드는 설정으로 켤 수 있고, 그 설정을 막는 다른 규칙도 없다. 게다가 검증기가 읽어야 할 이력의 출처 자체가 프로덕션에 없다. - -## 관계 - -- **호환성 게이트를 가진 두 포맷은 build-only이고 출하되는 유일한 코덱에는 게이트가 없다** - 같은 공백의 다른 면이다. -- **8개 profile validator 중 조립에서 실행되는 것은 3개다** - 같은 가족의 같은 형태다. -- **검사하지 않는 모드는 설계 중에는 유용하고 보존 로그가 생긴 뒤에는 위험하다** - 자바독이 적은 근거다. - -## 문제 - -스키마 호환성 검증기가 출하 리프에 있다. - -주 참조를 셌다. - -## 결론 - -0 건이다. 테스트 하나뿐이다. - -이 클래스가 하는 일은 둘이다. - -하나는 호환성 모드에 따라 비교해야 할 후보 판본 목록을 돌려주는 것이다. 이행 모드면 전체 이력이고 짝 모드면 직전 하나다. - -다른 하나는 검사 없음 실험 모드를 운영 목적지에서 거부하는 것이다. - -두 번째의 근거가 클래스 자바독에 있다. - -아무것도 검사하지 않는 모드는 메시지 타입을 설계하는 동안에는 유용하고 보존 로그가 생기면 능동적으로 위험하다는 것이다. 로그가 그것을 아직 읽을 수 있는 모든 소비자보다 오래 살기 때문이라는 것이다. - -그리고 그 모드는 설정으로 켤 수 있다. - -목적지 설정의 기본값이 안전한 값이지만, 설정으로 검사 없음 실험 모드를 쓰면 그대로 통과한다. - -목적지 프로파일 검증기의 열여섯 규칙에 스키마 항목이 없고, 거부 메서드는 호출되지 않는다. - -부수적으로 스키마 저장소에는 주 구현이 하나도 없다. - -유일한 구현은 검증기 테스트 안의 고정 저장소다. - -즉 검증기가 읽어야 할 스키마 이력의 출처 자체가 프로덕션에 존재하지 않는다. 호출하려 해도 넘길 저장소가 없다. - -같은 가족에서 코덱 저장소가 겪었고 등록 목록 타입으로 해결된 것과 같은 모양이고, 이쪽은 아직 해결되지 않았다. - -실패 시나리오는 이렇다. - -운영자가 한 목적지에 검사 없음 모드를 설정한다. 설계 중이라는 정당한 이유다. - -그 설정이 그대로 프로덕션으로 나간다. - -보존 로그에 이전 판본으로 쓴 메시지가 남고, 이후 판본이 필드를 삭제하면 그 로그를 읽는 소비자가 깨진다. - -판정은 P2 다. - -## 검증 환경 - -확인 방식 : 참조 계수와 설정 경로 확인, 저장소 구현 검색 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/252 계열에 있다. - -1. 검증기의 주 참조를 센다. -2. 두 메서드가 무엇을 하는지 읽는다. -3. 클래스 자바독의 거부 근거를 읽는다. -4. 목적지 설정에서 호환성 모드의 기본값과 설정 가능성을 확인한다. -5. 목적지 프로파일 검증기의 규칙에 스키마 항목이 있는지 확인한다. -6. 스키마 저장소의 주 구현을 검색한다. - -## 본문 - - - -`SchemaCompatibilityValidator`(`messaging-schema-api`, **출하**)의 main 참조는 **0건**이다. 테스트 1개뿐. - -## SchemaCompatibilityValidator 참조 위치 - -:::evidence key="analysis-finding-a19-f004" alt="코드베이스에서 SchemaCompatibilityValidator 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="SchemaCompatibilityValidator 코드베이스 검색 — 2줄 · exit 0" zoom="true" -::: - -## 이 클래스가 하는 일 둘 - -1. `versionsToCheck(subject)` — 호환성 모드에 따라 후보 스키마를 비교해야 할 버전 목록(transitive면 전체 이력, pairwise면 직전 하나)을 돌려준다. -2. `requireProductionMode(subject, destination)` — `NONE_EXPERIMENTAL`을 production destination에서 **거부**한다. - -두 번째의 근거가 클래스 javadoc에 있다 — "`NONE_EXPERIMENTAL` is refused for production destinations. A mode that checks nothing is useful while a message type is being designed and **actively dangerous once a retained log exists, because the log outlives every consumer that could still read it.**" - -## 그리고 그 모드는 설정으로 켤 수 있다 - -`DestinationSettings.Schema`의 `@DefaultValue("BACKWARD_TRANSITIVE")`가 안전한 값이지만, `app.messaging.destinations..schema.compatibility=NONE_EXPERIMENTAL`을 쓰면 그대로 통과한다 — `DestinationProfileValidator`의 16개 규칙에 스키마 항목이 없고(§3.5), `requireProductionMode`는 호출되지 않는다. - -## 넘길 registry 자체가 없다 - -`SchemaRegistry`에는 main 구현이 하나도 없다. 유일한 구현은 `SchemaCompatibilityValidatorTest`의 `FixedRegistry`다. §3.5의 `MessageCodecRegistry`가 겪었고 `RegisteredMessageCodecs`로 해결된 것과 같은 모양이며, 이쪽은 아직 해결되지 않았다. P2. - -## 확인하지 못한 것 - -검사 없음 모드를 설정하고 판본을 진화시켜 소비자가 깨지는 것을 재현하지 않았다. 호출 부재상 그 결과가 나온다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-analysis-finding-a19-f005.md b/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-analysis-finding-a19-f005.md deleted file mode 100644 index ab6d9c7..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-analysis-finding-a19-f005.md +++ /dev/null @@ -1,114 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a19-f005 -title: 호환성 게이트를 가진 두 포맷은 build-only이고, 출하되는 유일한 코덱에는 게이트가 없다 -topic: messaging-and-outbox -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a19-f005 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a19-f005.body.md -assets: - - key: analysis-finding-a19-f005 - file: ../../../final/evidence/rendered/analysis-finding-a19-f005.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a19-f005.txt -source: - - 원본 분석 절은 final/document.md#a19#L415 이다. ---- - -# 호환성 게이트를 가진 두 포맷은 build-only이고, 출하되는 유일한 코덱에는 게이트가 없다 - -스키마 진화 검사가 존재하는 두 포맷은 어떤 런타임에도 오르지 않는다. 실제로 유선에 바이트를 쓰는 유일한 코덱에는 포맷 수준의 게이트가 없다. 위험 방향이 뒤집혀 있다. - -## 관계 - -- **스키마 호환성 검증기는 출하 leaf에 있고 main 코드에서 호출되지 않는다** - 이 공백을 메울 자리인데 그것도 호출되지 않는다. -- **상호운용 규격 leaf는 출하되고 starter의 의존이며 소비자가 없다** - 같은 계수에서 드러난 다른 사례다. -- **결함은 미조립 자체가 아니라 출하되는 쪽에 대응하는 게이트가 없다는 비대칭이다** - 이 사례가 그 규칙의 형태다. - -## 문제 - -이 가족에 코덱이 다섯 있다. - -각 코덱의 출하 여부와 조립 지점과 호환성 게이트를 대조했다. - -## 결론 - -위험 방향이 뒤집혀 있다. - -JSON 코덱은 출하되고 핵심 자동 설정이 코덱 목록에 조립한다. 포맷 수준의 호환성 게이트가 없다. - -두 이진 포맷 코덱은 빌드 전용이고 조립 지점이 없다. 각각 호환성 게이트나 전용 테스트를 갖는다. 다만 테스트에서만 실행된다. - -원시 바이트 코덱은 리프에 출하되지만 조립 지점이 없다. 기본 코덱 금지 대상이다. - -상호운용 사상기는 출하되는데 조립 지점이 없다. - -즉 스키마 진화 검사가 존재하는 두 포맷은 어떤 런타임에도 오르지 않고, 실제로 유선에 바이트를 쓰는 유일한 코덱에는 포맷 수준의 게이트가 없다. - -포맷 독립 검증기가 그 공백을 메울 자리인데 그것도 호출되지 않는다. - -JSON 의 진화 위험이 이진 포맷보다 작은 것은 사실이지만 0 은 아니다. - -필드 삭제와 타입 변경과 열거값 제거는 역직렬화 실패로 나타난다. - -그리고 스키마 정책이 목적지마다 호환성 모드를 선언하게 되어 있다. 선언은 있고 집행이 없는 상태다. - -빌드 전용 두 리프의 미조립 자체는 등급과 일치하므로 결함이 아니다. - -결함은 출하되는 쪽에 대응하는 게이트가 없다는 비대칭이다. - -판정은 P2 다. - -## 검증 환경 - -확인 방식 : 코덱별 출하 여부와 조립 지점, 게이트 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/252 계열에 있다. - -1. 이 가족의 코덱 목록을 만든다. -2. 각 코덱의 리프와 런타임 구성원을 확인한다. -3. 각 코덱의 조립 지점을 찾는다. -4. 각 포맷의 호환성 게이트를 확인한다. -5. 스키마 정책이 무엇을 선언하게 하는지 확인한다. - -## 본문 - - - -코덱별로 출하 여부와 게이트가 갈린다. - -| 코덱 | 출하? | 조립 지점 | 호환성 게이트 | -|---|---|---|---| -| `JacksonMessageCodec` (JSON) | **출하** | `MessagingCoreAutoConfiguration:366` `messagingCodecs` ✔ | **없음** | -| `AvroMessageCodec` | build-only | 없음 | `AvroCompatibilityGate` (테스트에서만 실행) | -| `ProtobufMessageCodec` | build-only | 없음 | `ProtobufCompatibilityTest`만 | -| `RawBytesMessageCodec` | 출하(leaf) | 없음 — 기본 코덱 금지 대상 | n/a | -| `DefaultCloudEventMapper` | **출하** | **없음** | n/a | - -## SchemaCompatibilityValidator 참조 위치 - -:::evidence key="analysis-finding-a19-f005" alt="코드베이스에서 SchemaCompatibilityValidator 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="SchemaCompatibilityValidator 코드베이스 검색 — 2줄 · exit 0" zoom="true" -::: - -## 위험 방향이 뒤집혀 있다 - -스키마 진화 검사가 존재하는 두 포맷(Avro·Protobuf)은 어떤 런타임에도 오르지 않고, 실제로 wire에 바이트를 쓰는 유일한 코덱(JSON)에는 포맷 수준의 호환성 게이트가 없다. §4.3의 포맷 독립 검증기가 그 공백을 메울 자리인데 그것도 호출되지 않는다. - -## JSON의 진화 위험은 작지만 0이 아니다 - -필드 삭제, 타입 변경, enum 값 제거는 Jackson에서 런타임 역직렬화 실패로 나타난다. 그리고 `SchemaPolicy`가 destination마다 `compatibility` 모드를 **선언하게** 되어 있으므로, 선언은 있고 집행이 없는 상태다. build-only 두 leaf의 미조립 자체는 등급과 일치하므로 결함이 아니다 — 결함은 **출하되는 쪽에 대응하는 게이트가 없다는 비대칭**이다. P2. - -## 확인하지 못한 것 - -JSON 코덱으로 필드를 삭제한 판본을 흘려 역직렬화 실패를 재현하지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-analysis-finding-a19-f009.md b/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-analysis-finding-a19-f009.md deleted file mode 100644 index 80ed9b4..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-analysis-finding-a19-f009.md +++ /dev/null @@ -1,128 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a19-f009 -title: 접근 검사가 두 갈래로 존재하고, 조립된 쪽이 진단이 약한 쪽이다 (§8.3) -topic: messaging-and-outbox -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a19-f009 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a19-f009.body.md -assets: - - key: analysis-finding-a19-f009 - file: ../../../final/evidence/rendered/analysis-finding-a19-f009.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a19-f009.txt -source: - - 원본 분석 절은 final/document.md#a19#L511 이다. ---- - -# 접근 검사가 두 갈래로 존재하고, 조립된 쪽이 진단이 약한 쪽이다 (§8.3) - -같은 권한 검사가 두 형태로 있다. 조립된 쪽은 거부를 설정 분류로 돌려주고, 조립되지 않은 쪽은 전용 인가 예외를 던진다. 검사 자체는 조립된 쪽에서 수행되므로 보안 구멍은 아니다. - -## 관계 - -- **중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다** - 이 사례가 그 규칙의 형태다. -- **브로커 권한 매니페스트의 자기 점검이 존재하지 않는다** - 같은 가족의 다른 권한 사례다. -- **권한 거부와 설정 실수가 같은 버킷에 들어간다** - 기록하는 이유다. - -## 문제 - -같은 권한 검사가 두 형태로 있다. - -어느 쪽이 조립되었고 두 쪽의 차이가 무엇인지 확인했다. - -## 결론 - -조립된 쪽은 발행자가 접근 정책을 직접 부르는 형태다. - -발행이 허용되지 않으면 발행 금지 코드와 메시지를 담은 거부 결과를 돌려준다. 실패 분류가 설정이고 재시도 불가다. - -조립되지 않은 쪽은 전용 검증기다. 주 참조도 테스트 참조도 0 이다. - -발행이 허용되지 않으면 전용 인가 예외를 던진다. 코드와 함께 자격증명이 그 목적지에 발행할 수 없다는 메시지를 담는다. - -그 클래스의 자바독이 존재 이유를 적는다. - -브로커의 권한 검사보다 먼저 돌고, 일으키는 실패가 논리 목적지와 역할을 이름으로 부른다는 것이다. - -브로커 권한 거부는 애플리케이션 문맥이 없는 연결 수준 오류로 도착하며, 그래서 어느 모듈이 어디로 발행하려 했는가가 로그 한 줄이 아니라 조사가 된다는 것이다. - -두 경로의 차이는 분류다. - -조립된 쪽은 인가 거부를 설정으로 분류한다. 조립되지 않은 쪽은 전용 인가 예외를 던진다. - -핵심 계약의 예외 스물여섯 종에 그 인가 예외가 명시적으로 있는데, 실제 발행 경로는 그 타입을 쓰지 않는다. - -권한 거부가 설정으로 집계되면 설정 실수와 권한 침해 시도가 같은 갈래에 들어간다. - -실제 검사 자체는 조립된 쪽에서 수행되므로 보안 구멍은 아니다. - -분류와 진단의 문제이고, 중복 장치 중 조립되지 않은 쪽이 더 정확한 분류를 갖고 있다. - -판정은 P3 다. - -## 검증 환경 - -확인 방식 : 두 경로의 구현과 참조 계수 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/252 계열에 있다. - -1. 발행자에서 권한 검사를 부르는 지점을 읽는다. -2. 거부 결과의 분류와 재시도 여부를 확인한다. -3. 전용 검증기의 구현과 자바독을 읽는다. -4. 그 검증기의 참조를 센다. -5. 핵심 계약의 예외 목록에 인가 예외가 있는지 확인한다. - -## 본문 - - - -같은 권한 검사가 두 형태로 있다. - -## 조립된 쪽 - -`DefaultMessagePublisher`가 `DestinationAccessPolicy`를 직접 호출한다. - -```java -if (!access.mayPublish(destination.name())) { - return rejected("PUBLISH_FORBIDDEN", - "this application may not publish to '" + destination.name().value() + '\'', startedAt); -} -``` - -`FailureCategory.CONFIGURATION` · `retryable=false`인 `PublishResult`를 돌려준다. - -## DefaultMessagePublisher 참조 위치 - -:::evidence key="analysis-finding-a19-f009" alt="코드베이스에서 DefaultMessagePublisher 를 검색한 출력 6줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DefaultMessagePublisher 코드베이스 검색 — 6줄 · exit 0" zoom="true" -::: - -## 조립되지 않은 쪽 - -`DestinationAccessValidator`(main 참조 0건, 테스트 0건)가 전용 예외를 던진다. - -```java -public void requirePublish(DestinationName destination) { - if (!policy.mayPublish(destination)) { - throw new MessageAuthorizationException("DESTINATION_PUBLISH_DENIED", - "the producer credential may not publish to " + destination.value()); - } -} -``` - -조립된 쪽이 진단이 약한 쪽이다. P3. - -## 확인하지 못한 것 - -권한 거부를 발생시켜 집계 갈래를 확인하지 않았다. 분류 상수상 그 결과가 나온다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-analysis-finding-a19-f013.md b/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-analysis-finding-a19-f013.md deleted file mode 100644 index 6d7467b..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-analysis-finding-a19-f013.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a19-f013 -title: 가족 권위 문서가 이미 정정한 build-only 문장이 지원 매트릭스에 남아 있다 -topic: messaging-and-outbox -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a19-f013 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a19-f013.body.md -assets: - - key: analysis-finding-a19-f013 - file: ../../../final/evidence/rendered/analysis-finding-a19-f013.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a19-f013.txt -source: - - 원본 분석 절은 final/document.md#a19#L682 이다. ---- - -# 가족 권위 문서가 이미 정정한 build-only 문장이 지원 매트릭스에 남아 있다 - -같은 저장소의 두 문서가 정반대를 말한다. 한쪽은 자기가 틀렸었다는 사실과 그 원인까지 적어 두었고, 다른 쪽은 고쳐지지 않았다. 고쳐지지 않은 쪽이 운영자용 문서다. - -## 관계 - -- **문서 계약 test가 존재하고 그 커버리지 경계가 드리프트 위치를 정확히 예측한다** - 이 표류가 그 커버리지 밖에 있다. -- **README readiness 표가 있는 것을 없다고 적는다** - 같은 방향의 문서 표류다. -- **산문에서 세는 순간 다시 drift한다** - 가족 문서가 이미 적어 둔 근거다. - -## 문제 - -이 가족의 운영자용 지원 매트릭스가 런타임 구성원에 대한 문장을 담는다. - -레지스트리의 이 가족 리프는 모두 런타임 구성원이 비어 있으며, 따라서 빌드 전용이고 어느 조립 루트에도 편입되지 않았다는 것이다. - -같은 저장소의 가족 지침 문서와 대조했다. - -## 결론 - -가족 지침 문서가 정반대를 말한다. - -그리고 자기가 틀렸었다는 사실까지 적는다. - -이 절은 한동안 사실이 아닌 채로 남아 있었다는 것이다. 모든 리프가 빌드 전용이라고 쓰여 있었는데, 다섯 어댑터 보정이 스타터를 부트스트랩 의존성으로 넣으면서 그 폐포 전체가 실행 클래스패스에 올라갔다는 것이다. - -그리고 원인까지 적는다. 정확한 목록은 레지스트리가 소유하므로 여기서 세지 않는다는 것이다. 세는 순간 다시 표류하기 때문이라는 것이다. - -레지스트리 실측은 출하 열여덟에 빌드 전용 일곱이다. - -같은 저장소의 두 문서가 정반대를 말하고, 한쪽은 자기가 틀렸었다는 사실과 그 원인을 적어 두었으면서 다른 쪽은 고쳐지지 않았다. - -그리고 고쳐지지 않은 쪽이 운영자용 문서다. - -이 표류의 실질적 무게는 이 가족 분석 전체의 심각도 판정 축과 같다. - -지원 매트릭스만 읽은 운영자는 이 가족이 아무것도 출하하지 않는다고 결론 내린다. - -실제로는 두 브로커 어댑터와 스타터와 보안과 관측과 상호운용 규격, 발신함과 수신함과 청구 확인, 그리고 관리 평면이 전부 부트스트랩 산출물에 실려 있다. - -판정은 P2 다. - -## 검증 환경 - -확인 방식 : 두 문서 대조와 레지스트리 실측 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/252 계열에 있다. - -1. 지원 매트릭스의 런타임 구성원 문장을 읽는다. -2. 가족 지침 문서의 같은 주제 절을 읽는다. -3. 레지스트리에서 이 가족 리프의 런타임 구성원을 센다. -4. 출하와 빌드 전용을 가른다. -5. 어느 문서가 운영자용인지 확인한다. - -## 본문 - - - -지원 매트릭스가 "모든 messaging leaf는 build-only"라고 적고, 가족 권위 문서는 그 문장이 틀렸다고 이미 기록했다. - -## 두 문서가 같은 문장에 대해 하는 말 - -:::evidence key="analysis-finding-a19-f013" alt="분석 문서 final/document.md#a19 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a19 발췌 — 15줄" zoom="true" -::: - -## 매트릭스만 읽으면 반대 결론에 도달한다 - -지원 매트릭스만 읽은 운영자는 messaging이 아무것도 출하하지 않는다고 결론 내리는데, 실제로는 `messaging-kafka`·`messaging-rabbit`·`messaging-spring-boot-starter`·`messaging-security`·`messaging-observability`·`messaging-cloudevents`·outbox/inbox/claim-check·admin plane이 전부 `app-bootstrap` 아티팩트에 실려 있다. 이 드리프트의 실질적 무게는 이 문서 전체의 심각도 판정 축과 같다(§1.1). - -## 확인하지 못한 것 - -지원 매트릭스가 언제부터 어긋났는지 이력에서 확인하지 않았다. 가족 문서가 원인을 적어 두었다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-analysis-finding-a19-f018.md b/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-analysis-finding-a19-f018.md deleted file mode 100644 index 44cbab9..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-analysis-finding-a19-f018.md +++ /dev/null @@ -1,110 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a19-f018 -title: claim-check는 starter에 배선 코드가 한 줄도 없다 -topic: messaging-and-outbox -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a19-f018 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a19-f018.body.md -assets: - - key: analysis-finding-a19-f018 - file: ../../../final/evidence/rendered/analysis-finding-a19-f018.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a19-f018.txt -source: - - 원본 분석 절은 final/document.md#a19#L926 이다. ---- - -# claim-check는 starter에 배선 코드가 한 줄도 없다 - -출하 리프의 두 진입 타입이 주 참조 0 건이고 신뢰성 자동 설정에 그 이름이 아예 나오지 않는다. 프로파일은 임계값을 선언할 수 있고 검증도 받는데 그것을 수행하는 코드가 조립되지 않는다. - -## 관계 - -- **상호운용 규격 leaf는 출하되고 starter의 의존이며 소비자가 없다** - 같은 가족의 같은 형태다. -- **두 개의 outbox 중 하나만 조립되어 있다** - 같은 계열의 조립 문제다. -- **조용한 잘못된 성공이 아니라 시끄러운 실패다** - 판정을 낮춘 이유다. - -## 문제 - -청구 확인 리프가 출하된다. 주 파일 여섯에 사백여 줄이다. - -바깥에서 들어오는 경로가 있는지 확인했다. - -## 결론 - -없다. - -발행자와 해석기의 주 참조가 0 건이고, 신뢰성 자동 설정에 이 이름의 문자열이 등장하지 않는다. - -무결성 가드와 정책과 저장소는 리프 내부에서 서로를 참조한다. 그러므로 리프는 내부적으로 일관되다. - -다만 바깥에서 들어오는 경로가 없다. - -목적지 프로파일 검증기는 이 기능을 알고 있다. - -임계값이 최대 크기보다 크면 거부한다. - -즉 프로파일은 임계값을 선언할 수 있고 검증도 받는데, 그 임계값을 넘는 적재물에 대해 이 기능을 수행하는 코드가 조립되지 않는다. - -임계값은 설정 가능하고 효과는 없다. - -같은 가족의 발신함 사례보다 낮은 등급으로 두는 이유가 있다. - -이 기능은 부재 시 동작이 명확하다. - -적재물이 그대로 전송되고, 크기 한도에 걸리면 전용 예외로 명시적으로 실패한다. - -조용한 잘못된 성공이 아니라 시끄러운 실패다. - -판정은 P3 다. - -## 검증 환경 - -확인 방식 : 참조 계수와 자동 설정 문자열 검색 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/252 계열에 있다. - -1. 리프의 주 파일과 줄 수를 센다. -2. 두 진입 타입의 주 참조를 센다. -3. 신뢰성 자동 설정에서 이 이름을 검색한다. -4. 리프 내부의 상호 참조를 확인한다. -5. 목적지 프로파일 검증기의 관련 규칙을 확인한다. - -## 본문 - - - -`messaging-claim-check`(6 main, 418 LOC, **출하**)의 `ClaimCheckPublisher`·`ClaimCheckResolver`는 main 참조 0건이고, `MessagingReliabilityAutoConfiguration`에 `ClaimCheck` 문자열이 등장하지 않는다. - -## ClaimCheckPublisher 참조 위치 - -:::evidence key="analysis-finding-a19-f018" alt="코드베이스에서 ClaimCheckPublisher 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ClaimCheckPublisher 코드베이스 검색 — 2줄 · exit 0" zoom="true" -::: - -## leaf는 내부적으로 일관되고 바깥 경로가 없다 - -`ClaimCheckIntegrityGuard`·`ClaimCheckPolicy`·`ClaimCheckStore`는 leaf 내부에서 서로를 참조한다. - -## 임계값은 설정 가능하고 효과는 없다 - -`DestinationProfileValidator`는 claim check를 알고 있다 — `profile.payload().claimCheckThresholdBytes() > profile.payload().maxBytes()`를 거부한다. 즉 프로파일은 claim check 임계값을 선언할 수 있고 검증도 받지만, 그 임계값을 넘는 payload에 대해 claim check를 수행하는 코드가 조립되지 않는다. - -## P3으로 두는 이유 - -claim check는 outbox와 달리 **부재 시 동작이 명확**하다 — payload가 그대로 전송되고, 크기 한도(`BoundedByteSink`, §4.1)에 걸리면 `MessageTooLargeException`으로 명시적으로 실패한다. 조용한 잘못된 성공이 아니라 시끄러운 실패다. - -## 확인하지 못한 것 - -임계값을 넘는 적재물을 보내 그대로 전송되는 것을 재현하지 않았다. 조립 부재상 그 결과가 나온다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-analysis-finding-a19-f019.md b/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-analysis-finding-a19-f019.md deleted file mode 100644 index 9e11c5d..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-analysis-finding-a19-f019.md +++ /dev/null @@ -1,135 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a19-f019 -title: admin 스위치가 가드를 켜고 서비스는 켜지 않는다 -topic: messaging-and-outbox -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a19-f019 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a19-f019.body.md -assets: - - key: analysis-finding-a19-f019 - file: ../../../final/evidence/rendered/analysis-finding-a19-f019.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a19-f019.txt -source: - - 원본 분석 절은 final/document.md#a19#L975 이다. ---- - -# admin 스위치가 가드를 켜고 서비스는 켜지 않는다 - -관리 스위치를 켜면 빈 넷이 만들어진다. 전부 가드와 기록과 검증기다. 그중 관리 서비스 인터페이스의 유일한 구현과 승인 검증자의 유일한 구현은 만들어지지 않는다. 부재가 문서화된 것은 하나뿐이다. - -## 관계 - -- **관리 계약 leaf는 main 25파일 1613줄에 테스트 파일이 1개다** - 같은 하위 범위의 다른 사례다. -- **멱등 실행 계층과 durable-operation 표면이 픽스처에서만 조립된다** - 다른 가족의 같은 형태다. -- **켰다고 생각한 기능이 없다는 것을 사고 한가운데에서 발견한다** - 이 사례가 그 규칙의 형태다. - -## 문제 - -관리 스위치를 켜면 무엇이 만들어지는지 확인했다. - -## 결론 - -빈 넷이다. - -파괴적 연산 가드와 관리 연산 기록, 관리 내구성 검증기, 그리고 위상 조사기가 있을 때 합성 위상 검증기다. - -만들지 않는 것이 다섯이다. - -파괴적 관리자는 주 참조가 둘이고 부재가 자바독에 명시되어 있다. - -기본 관리 서비스는 주 참조가 0 이고 부재 이유가 없다. - -해시 기반 승인 검증자도 주 참조가 0 이고 테스트가 넷이다. 부재 이유가 없다. - -위상 검증 실행체도 0 이고 이유가 없다. - -재구동 서비스와 재생 서비스도 각각 주 참조가 있고 이유가 없다. - -기본 관리 서비스는 관리 서비스 인터페이스의 유일한 구현이다. - -즉 관리 평면을 켜도 관리 서비스가 없다. - -해시 기반 승인 검증자는 승인 검증자의 유일한 구현이고, 테스트 넷이 그것을 검증한다. 위조 테스트도 포함된다. - -승인된 재구동 계획과 재생 계획과 검증된 승인과 계획 요약으로 이루어진 승인 사슬 전체가 검증자 없이는 시작될 수 없다. - -부재의 등급이 넷 다 다르지 않은데 문서화는 하나만 됐다. - -파괴적 관리자의 부재에는 명확한 이유가 있다. 이 실행체는 관리 자격증명을 갖지 않는다는 것이다. - -나머지 넷에는 이유가 적혀 있지 않고, 그중 둘은 파괴적이지 않은 관리 동작에 필요한 것이다. 재구동과 재생의 승인과 실행이다. - -실패 시나리오는 이렇다. - -운영 절차서에 따라 사고 대응 중 재구동을 실행하려 한다. - -관리 스위치를 켠다. 부팅은 성공하고 가드와 기록과 내구성 검증기가 올라온다. - -그런데 관리 서비스 빈이 없으므로 재구동을 호출할 대상이 없다. - -사고 한가운데에서 켰다고 생각한 기능이 없다는 것을 발견한다. - -내구성 검증기의 자바독이 경계한 상황과 정확히 같은 시점이다. 그 간극은 그 연산을 돌리게 만든 사고 도중에만 드러나며 그것이 발견하기에 가장 나쁜 순간이라는 것이다. - -판정은 P2 다. - -## 검증 환경 - -Spring Boot : 4.0.8 -확인 방식 : 자동 설정 빈 목록과 타입별 참조 계수 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/252 계열에 있다. - -1. 관리 자동 설정의 조건과 빈 목록을 읽는다. -2. 관리 평면에 필요한 타입 목록을 만든다. -3. 각 타입의 주 참조를 센다. -4. 각 타입의 부재가 문서화되었는지 확인한다. -5. 관리 서비스 인터페이스의 구현을 센다. - -## 본문 - - - -`app.messaging.admin.enabled=true`가 만드는 bean은 넷이다 — `DestructiveOperationGuard`, `AdminOperationJournal`, `MessagingAdminDurabilityValidator`, (`BrokerTopologyInspector`가 있을 때) `CompositeTopologyValidator`. - -## DestructiveOperationGuard 참조 위치 - -:::evidence key="analysis-finding-a19-f019" alt="코드베이스에서 DestructiveOperationGuard 를 검색한 출력 6줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DestructiveOperationGuard 코드베이스 검색 — 6줄 · exit 0" zoom="true" -::: - -## 만들지 않는 것 - -| 타입 | leaf | main 참조 | 부재가 문서화됐는가 | -|---|---|---|---| -| `DestructiveMessagingAdmin` | admin-runtime | 2 | **예** — javadoc이 명시 | -| `DefaultMessagingAdminService` | admin-runtime | **0** | 아니오 | -| `HmacApprovalVerifier` | admin-api | **0** (test 4) | 아니오 | -| `TopologyValidationRuntime` | admin-runtime | **0** | 아니오 | -| `RedriveService` / `ReplayService` | admin-runtime | 2 / 1 | 아니오 | - -`DefaultMessagingAdminService`는 `MessagingAdminService`(인터페이스, main 참조 2)의 유일한 구현이다. 즉 admin plane을 켜도 admin 서비스가 없다. `HmacApprovalVerifier`는 `ApprovalVerifier`의 유일한 구현이고, `ApprovedRedrivePlan`/`ApprovedReplayPlan`/`VerifiedApproval`/`PlanDigest`(main 참조 10)로 이루어진 승인 사슬 전체가 **검증자 없이는 시작될 수 없다.** - -## 부재의 등급이 다르지 않은데 문서화는 하나만 됐다 - -`DestructiveMessagingAdmin`의 부재에는 명확한 이유가 있다("이 런타임은 admin 자격 증명을 갖지 않는다"). 나머지 넷에는 이유가 적혀 있지 않고, 그중 둘은 파괴적이지 않은 admin 동작(redrive/replay의 승인·실행)에 필요한 것이다. - -## 실패 시나리오 - -운영 절차서(`docs/messaging/retry-dlq-redrive.md`)에 따라 사고 대응 중 redrive를 실행하려 한다. `app.messaging.admin.enabled=true`로 켠다. 부팅은 성공하고 가드·journal·durability 검증기가 올라온다. 그런데 `MessagingAdminService` bean이 없으므로 redrive를 호출할 대상이 없다 — `MessagingAdminDurabilityValidator`의 javadoc이 경계한 상황("the gap only shows up during the incident the operation was run to resolve, which is **the worst possible moment to discover it**")과 정확히 같은 시점이다. P2. - -## 확인하지 못한 것 - -관리 스위치를 켜고 재구동을 호출해 대상이 없는 것을 재현하지 않았다. 빈 목록상 그 결과가 나온다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a03-f001.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a03-f001.md deleted file mode 100644 index 0f10d9e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a03-f001.md +++ /dev/null @@ -1,155 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a03-f001 -title: 네 답을 주는 claim 이 있는데 서비스는 있음·없음 두 갈래로 판단한다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a03-f001 -evidenceCapturedOn: 2026-09-04 -body: case-analysis-finding-a03-f001.body.md -assets: - - key: analysis-finding-a03-f001 - file: ../../../final/evidence/rendered/analysis-finding-a03-f001.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a03-f001.txt -source: - - 원본 분석 절은 final/document.md#a03 §16 P1 이다. ---- - -# 네 답을 주는 claim 이 있는데 서비스는 있음·없음 두 갈래로 판단한다 - -`AdminOperationClaim` 은 `CLAIMED`·`REPLAY`·`IN_PROGRESS`·`CONFLICT` 를 돌려주도록 만들어졌고, 그 넷을 하나로 접으면 안 되는 이유가 클래스 자바독에 적혀 있다. `NotificationAdminApplicationService` 는 `claim` 을 한 줄도 부르지 않고 네 경로 모두 `findByOperationId` 의 `Optional` 이 비었는지로 판단한다. - -## 관계 - -- **조건부 update로 행을 claim하고 읽은 값으로 판단하지 않는다** - `NotificationAdminApplicationService` 의 네 경로가 어기는 규칙이다. 붙들고 판단하는 연산이 포트에 있는데 서비스는 조회 결과의 있음·없음으로 판단한다. -- **CAS 튜플을 where 절에 전부 반복하고 update count를 답으로 쓴다** - 삽입 건수를 답으로 쓰는 부분을 `JpaAdminOperationStore:40` 이 지킨다. 그 값을 받아 분기하는 코드가 서비스에 없다. -- **중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다** - 이 사례가 속한 구조다. 만들어지는 것과 호출되는 것을 나눠 세면 `claim` 은 포트와 JPA 구현이 다 있는데 부르는 코드가 없다. - -## 문제 - -관리자 연산은 멱등 키를 받는다. 그 키가 있는 이유를 포트 자바독 첫 줄이 적어 둔다. - -그 키를 원자적으로 붙드는 연산이 포트와 JPA 구현 양쪽에 있다. 서비스가 그것을 부르는지 확인했다. - -## 결론 - -포트에는 연산이 셋 있다. 서비스가 부르는 조회 메서드에 붙은 자바독이 그것을 claim 이라고 부르는데, 그 메서드는 아무것도 붙들지 않는다. - -그 자바독은 네 답이 각각 무엇을 요구하는지도 적는다. 재생은 앞선 결과를 돌려주고 실행하지 않아야 하고, 다른 호출자가 들고 있는 동안에는 실행하지도 끝난 척하지도 않아야 하며, 같은 식별자에 다른 명령이 온 것은 멱등한 반복이 아니라 보고할 실수다. - -JPA 구현은 그 자바독대로 붙든다. JpaAdminOperationStore:40 이 부르는 claimOperation 은 ON CONFLICT (operation_id) DO NOTHING 을 붙인 native INSERT 이고, 삽입된 행 수가 1 이면 이 호출자가 붙든 것이다. :48·:55·:62·:69 가 네 답을 나눠 돌려준다. - -NotificationAdminApplicationService 는 claim 을 부르지 않는다. operations 포트에 거는 호출 여덟이 전부 findByOperationId 넷과 save 넷이고, claim 은 0 줄이다. 대조로 센 값이 4 를 내므로 이 계수는 살아 있다. - -네 경로가 그 사이에 부르는 협력자는 서로 다르다. redrive:121 이 recipients.transition, reconcile:172 가 명령이 준 시도마다 reconciliation.reconcile, suppress:229·:236 이 suppressions.remove 또는 upsert, setProviderState:298 이 runtimes.setState 를 부른다. - -부를 준비도 되어 있지 않다. claim 의 두 번째 인자를 채울 다이제스트를 만드는 코드가 저장소 어디에도 없고, 그 이름이 나오는 자리는 파라미터 선언과 컬럼뿐이다. - -V8 마이그레이션 헤더가 실제 증상을 적는다. 두 번째 요청은 결국 저장에서 막혔지만 그때는 파괴적 동작이 이미 두 번 돈 뒤였다. - -그 계약을 고정한 시험은 postgresqlIntegrationTest 소스 세트에 있고 기본 check 에 걸리지 않는다. 다만 @Tag("jpa-contract") 가 jpaPlatformContractTest 레인에 묶여 있고, notification-platform.yml:85 가 그 레인을 pull_request 에서 부르며 경로 필터가 이 서비스 파일을 포함한다. 서비스를 고치는 PR 마다 이 시험이 돈다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 포트와 청구 결과 타입 전문 게재, 마이그레이션 헤더 인용, JPA 구현의 네 분기와 리포지터리의 native 문장 인용, 서비스가 포트에 거는 호출 전수와 claim 계수를 없는 이름과 대조 이름으로 함께 계수, commandFingerprint 가 나오는 자리 전수, 협력자 필드 선언에서 정규식을 만들어 네 메서드 창 안의 호출을 뽑음, 시험 태그에서 gradle 레인과 워크플로 경로 필터까지 배선 추적 -소스 수정 : x - -## 재현 조건 - -1. 저장소 포트와 청구 결과 타입을 전문으로 싣는다. -2. 그 연산을 만든 마이그레이션의 헤더 주석을 읽는다. -3. JPA 구현이 무엇을 보내고 네 답을 어디서 나누는지 확인하고, 리포지터리의 실제 문장을 인용한다. -4. 서비스가 포트에 거는 호출을 전부 나열하고, claim 계수를 없는 이름과 대조 이름으로 함께 센다. -5. commandFingerprint 가 나오는 자리를 전부 찾아 값을 만드는 코드가 있는지 본다. -6. 서비스가 선언한 협력자 필드 이름에서 패턴을 유도해 네 메서드 창 안의 호출을 뽑는다. 고정 목록을 손으로 적지 않는다. -7. 계약 시험의 태그를 찾아 gradle 레인과 워크플로와 그 경로 필터까지 따라간다. - -## 본문 - - - -`AdminOperationStorePort` 는 관리자 연산의 멱등 기록을 맡는다. 같은 키로 온 재시도가 파괴적 동작을 두 번 돌리지 못하게 하는 것이 목적이다. - -## 붙드는 것은 \:31 의 claim 인데 \:8 의 자바독은 findByOperationId 를 claim 이라고 적는다 - -:::evidence key="analysis-finding-a03-f001" alt="저장소 루트에서 돌린 정적 검색 출력 227줄. 먼저 AdminOperationStorePort 33줄이 전문으로 실린다. 8번 줄 자바독은 findByOperationId 를 연산 식별자를 claim 하거나 앞선 결과를 돌려주는 것으로 적고, 15~30번 줄 자바독은 관리 경로가 조회 다음 부수 효과 다음 저장이었다는 것과 두 호출자가 모두 없음을 읽고 모두 실행하고 모두 저장했다는 것, 그리고 지문이 청구의 일부라는 것을 적는다. 이어서 AdminOperationClaim 5~35번 줄이 실려 네 답 CLAIMED, REPLAY, IN_PROGRESS, CONFLICT 와 넷을 합치면 중요한 구별이 사라진다는 클래스 자바독이 보인다. 다음으로 V8 마이그레이션 헤더 1~11번 줄이 실리는데, operation_id 의 유니크 제약이 이미 있어서 두 번째 저장은 실패했지만 그것은 두 번째 부수 효과가 이미 일어난 뒤였다고 적고, 이제 청구는 삽입 자체이며 ON CONFLICT DO NOTHING 이 정확히 한 호출자만 행을 만들게 한다고 적는다. 그 아래에 JpaAdminOperationStore 30~71번 줄이 실려 claim 이 삽입 건수로 claimed 를 판정하고 48번에서 claimed, 62번에서 지문이 다르면 conflict, 55번과 65번에서 inProgress, 69번에서 replay 를 돌려주는 것이 보인다. AdminAuditJpaRepository 28~45번 줄은 그 문장이 ON CONFLICT (operation_id) DO NOTHING 을 붙인 native INSERT 임을 보여 준다. 서비스가 operations 포트에 거는 호출은 여덟이고 findByOperationId 넷과 save 넷이다. operations.claim 은 0 줄, 없는 이름으로 건 자기시험도 0 줄, 대조로 센 operations.save 는 4 줄이다. commandFingerprint 가 나오는 자리는 파라미터와 컬럼 이름뿐이고 값을 계산하는 코드가 없다. 그 아래에 네 경로가 조회와 저장 사이에 부르는 협력자가 나오는데, 서비스가 선언한 필드 이름에서 패턴을 유도했다. redrive 는 121번에서 recipients.transition, reconcile 은 172번에서 reconciliation.reconcile, suppress 는 229번에서 suppressions.remove 또는 236번에서 suppressions.upsert, setProviderState 는 298번에서 runtimes.setState 를 부른다. 마지막으로 그 경합을 고정한 시험이 어느 레인에서 도는지 나온다. AdminOperationClaimContractTest 는 postgresqlIntegrationTest 소스 세트에 있고 34번 줄이 jpa-contract 태그를 달며, build.gradle 251~252번이 그 태그를 jpaPlatformContractTest 레인에 묶고, jpa-pr.yml 과 notification-platform.yml 이 pull_request 에서 그 태스크를 부르는데 경로 필터가 src/application-core/src/**/notification/** 을 포함한다. 서비스 이름을 파일명에 가진 시험은 0 개다." caption="포트 33줄 전문과 findByOperationId 자바독 · claim 의 네 답 · V8 헤더가 적은 실제 증상 · JPA 구현 30~71 과 ON CONFLICT DO NOTHING INSERT · 서비스의 호출 여덟과 claim 0 · 지문을 계산하는 코드 부재 · 필드에서 유도한 네 경로의 협력자 호출 · 태그에서 워크플로까지의 레인 배선 — 227줄 · exit 0" zoom="true" -::: - -포트는 `findByOperationId:9` 와 `save:12` 와 `claim:31` 을 선언한다. - -`:8` 의 자바독은 `findByOperationId` 를 "연산 식별자를 claim 하거나 그 식별자의 앞선 결과를 돌려준다" 고 적는다. 실제로 이 메서드는 조회만 하고 아무것도 붙들지 않는다. - -`claim` 의 자바독 `:15`\~`:30` 이 이 연산이 왜 생겼는지 적는다. 관리 경로가 조회 다음 부수 효과 다음 저장이었고, 같은 식별자를 낸 두 호출자가 모두 "없음" 을 읽고 모두 재구동을 실행하고 모두 저장했다는 것이다. 멱등 키가 검사되기만 하고 붙들리지 않아서 반복은 막았지만 경합은 막지 못했다는 것이 그 이유다. - -같은 자바독이 명령 지문도 청구의 조건이라고 적는다. 같은 식별자에 다른 명령이 오면 재생이 아니라 충돌이고, 그것을 앞선 결과로 답하면 두 명령 중 어느 것도 실행되지 않는다. - -## claim 은 네 답을 주고 findByOperationId 는 두 갈래를 준다 - -`AdminOperationClaim` 은 `CLAIMED`·`REPLAY`·`IN_PROGRESS`·`CONFLICT` 를 갖는 record 다. 클래스 자바독은 넷을 합치면 중요한 구별이 사라진다고 적는다. - -`JpaAdminOperationStore` 가 그 넷을 나눈다. `:48` 이 `claimed()`, `:55` 와 `:65` 가 `inProgress()`, `:62` 가 지문이 다를 때 `conflict()`, `:69` 가 `replay()` 다. - -서비스가 쓰는 `findByOperationId` 의 반환은 `Optional` 하나뿐이라 이 네 갈래를 표현할 자리가 없다. - -## JPA 구현은 ON CONFLICT DO NOTHING 의 삽입 건수를 답으로 쓴다 - -`JpaAdminOperationStore:40` 이 `audits.claimOperation` 을 부르고 `:39` 가 그 반환값을 `claimed` 에 담는다. - -`AdminAuditJpaRepository:31`\~`:40` 을 보면 그 문장은 `notification_admin_audit` 에 행을 넣는 native INSERT 이고 끝에 `ON CONFLICT (operation_id) DO NOTHING` 이 붙어 있다. `UPDATE` 도 `WHERE` 절도 없다. 원자성은 조건절이 아니라 `operation_id` 의 유니크 제약에서 나온다. - -V8 마이그레이션 헤더가 그 관계를 적는다. 그 제약은 예전에도 있었고 두 번째 저장을 실패시켰지만, 그것은 두 번째 부수 효과가 이미 일어난 뒤였다. 이제 청구가 삽입 자체이므로 정확히 한 호출자만 행을 만든다. - -## 서비스가 operations 포트에 거는 호출 여덟은 findByOperationId 와 save 뿐이다 - -`findByOperationId` 가 `:94`·`:154`·`:203`·`:282` 넷이고 `save` 가 `:145`·`:194`·`:272`·`:328` 넷이다. - -`operations.claim` 을 부르는 줄은 0 개다. 이 0 이 검색식 오류가 아닌지 보려고 같은 파일에서 `operations.save` 를 세면 4 가 나온다. - -부를 수 없는 이유도 있다. `claim` 은 `commandFingerprint` 를 요구하는데 그 값을 계산하는 코드가 저장소에 없다. 그 이름이 나오는 자리는 포트 선언과 JPA 구현의 파라미터, 리포지터리의 컬럼 이름, 계약 시험의 고정 문자열뿐이다. - -## 네 경로가 조회와 저장 사이에 부르는 협력자 - -부수 효과를 손으로 적은 목록으로 찾으면 이름을 하나 빠뜨렸을 때 조용히 사라진다. 그래서 서비스가 선언한 협력자 필드에서 패턴을 유도했다. - -`redrive:90`\~`:147` 은 `:94` 에서 조회하고 `:117` 의 트랜잭션 안에서 `:121` 의 `recipients.transition` 으로 수신자 배달 상태를 옮기고 `:145` 에서 저장한다. - -`reconcile:150`\~`:195` 는 `:154` 에서 조회하고 `:172` 에서 명령이 준 시도 식별자마다 `reconciliation.reconcile` 을 부른 뒤 `:194` 에서 저장한다. 트랜잭션 안이 아니다. - -`suppress:198`\~`:275` 는 `:203` 에서 조회하고 `:218` 의 트랜잭션 안에서 `:229` 의 `suppressions.remove` 또는 `:236` 의 `suppressions.upsert` 를 부른 뒤 `:272` 에서 저장한다. 둘은 배타적 분기다. - -`setProviderState:278`\~`:329` 는 `:282` 에서 조회하고 `:298` 에서 `runtimes.setState` 로 제공자 런타임 상태를 바꾼 뒤 `:328` 에서 저장한다. 여기도 트랜잭션 밖이다. - -자바독이 과거형으로 적은 조회 → 부수 효과 → 저장 순서가 지금 네 경로에 그대로 있다. - -## 청구 계약 시험은 postgresqlIntegrationTest 소스 세트에 있다 - -`AdminOperationClaimContractTest` 는 `adapter/outbound/persistence-jpa` 의 `postgresqlIntegrationTest` 에 있고 `:34` 가 `@Tag("jpa-contract")` 를 단다. - -`persistence-jpa/build.gradle:251`\~`:252` 가 그 태그를 `jpaPlatformContractTest` 레인에 묶는다. `jpa-pr.yml:86` 과 `notification-platform.yml:85` 가 `pull_request` 에서 그 태스크를 부르고, 뒤엣것의 경로 필터 `:21` 이 `src/application-core/src/**/notification/**` 을 포함한다. - -즉 이 시험은 기본 `check` 에는 없지만 서비스를 고치는 PR 마다 돈다. 저장소 계층의 청구는 그렇게 고정돼 있다. - -`NotificationAdminApplicationService` 를 이름에 가진 시험 파일은 0 개다. - -## 원문과 갈리는 자리 - -원문은 서비스가 네 경로에서 조회 후 부수 효과 후 저장을 쓴다고 적었고 그것은 그대로다. - -원문이 적지 않은 것이 셋이다. `claim` 이 네 답을 주는데 `findByOperationId` 는 두 갈래뿐이라는 것, 두 번째 저장이 유니크 제약으로 실패한다는 것과 그것이 두 번째 부수 효과 뒤라는 것, 그리고 `commandFingerprint` 를 만드는 코드가 없어서 오늘은 `claim` 을 부를 수도 없다는 것이다. - -## 확인하지 못한 것 - -스레드 둘로 같은 식별자를 밀어 넣어 파괴적 동작이 두 번 도는 장면을 만들지 않았다. 조회와 부수 효과와 저장이 세 연산이라는 것까지다. - -네 협력자의 동작이 두 번 실행됐을 때 각각 어떤 상태가 되는지 구현까지 읽지 않았다. 포트 자바독이 파괴적 연산의 이중 실행을 막으려는 것이라고 적은 것을 근거로 삼았다. - -이 네 메서드를 호출하는 인바운드 어댑터가 있는지 세지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a04-f002.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a04-f002.md deleted file mode 100644 index ea59280..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a04-f002.md +++ /dev/null @@ -1,165 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a04-f002 -title: 메시징만 성공 로그를 try 밖으로 옮기고 알림은 같은 try 에 남겨 두었다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a04-f002 -evidenceCapturedOn: 2026-09-04 -body: case-analysis-finding-a04-f002.body.md -assets: - - key: analysis-finding-a04-f002 - file: ../../../final/evidence/rendered/analysis-finding-a04-f002.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a04-f002.txt -source: - - 원본 분석 절은 final/document.md#a04 §5 이다. ---- - -# 메시징만 성공 로그를 try 밖으로 옮기고 알림은 같은 try 에 남겨 두었다 - -`821fe00c` 초기 커밋에서 `OutboundMessagePublisher` 와 `FailOpenNotificationProvider` 는 같은 모양이었다. `2f5d2fc2` 가 메시징 쪽만 `boolean sent` 와 `observeQuietly` 로 갈랐고 알림 쪽은 그대로다. 그래서 알림 쪽에서는 위임 전송이 성공한 뒤 성공 로거가 던지면 그 예외를 전송 실패용 `catch` 가 받는다. - -## 관계 - -- **시그니처가 payload를 받지 않는데 예외 메시지로 PII가 로그에 남았다** - 같은 `FailOpenDependencyLogger` 를 다룬다. 그 사례는 `logFailure` 가 예외 메시지를 그대로 로그에 넣는 것을 프로브로 확인했고, 여기서는 그 `logFailure` 를 두 소비자가 각각 어느 `try` 안에서 부르는지를 확인했다. -- **중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다** - 그 규칙은 구현이 둘일 때 어느 것이 조립되는지를 묻는다. 여기서는 두 소비자가 모두 조립되고, 갈리는 것은 같은 로거를 부르는 자리다. -- **모르는 것은 성공도 실패도 아닌 세 번째 결과여야 한다** - 그 규칙은 확정되지 않은 결과를 성공이나 실패로 접지 말라고 한다. 여기서는 확정된 성공이 실패로 접힌다. - -## 문제 - -메시징 어댑터와 알림 어댑터가 같은 FailOpenDependencyLogger 를 쓴다. 둘 다 실패를 삼키는 fail-open 구조다. - -두 소비자가 그 로거를 어느 자리에서 부르는지, 그리고 언제부터 그렇게 갈렸는지 확인했다. - -## 결론 - -821fe00c 이 출하한 메시징 publish 에는 지금의 알림 쪽과 구별되는 점이 없다. 브로커 호출과 성공 로그가 한 블록에 있고 예외 처리기가 실패 로그를 남긴다. - -2f5d2fc2 가 메시징 쪽만 바꿨다. :32 가 boolean sent 를 두고 :33~:35 의 try 가 broker.send 만 감싼다. 관측은 :39 와 :43 에서 observeQuietly 를 거치고 :54~:60 이 그 안에서 RuntimeException 을 삼킨다. FailOpenNotificationProvider 는 초기 커밋 이후 수정된 적이 없다. - -알림 쪽에는 그 플래그가 없다. :38 의 delegate.send 와 :39 의 logSuccess 가 같은 try 안에 있고 :40 의 catch 가 :42 에서 logFailure 를 부른다. - -저장소 클래스를 그대로 로드한 프로브에 slf4j Logger 프록시를 물렸다. debug 에서만 던지게 하면 알림 쪽은 위임 전송이 성공했는데도 warn 이 한 번 불린다. 메시징 쪽은 같은 조건에서 warn 이 0 회다. - -debug 와 warn 양쪽에서 던지게 하면 알림 쪽만 IllegalStateException 이 send 밖으로 나간다. :8~:10 이 선언한 fail-open 계약이 그 경우에 성립하지 않는다. - -RoutingNotifier:120~:122 의 팬아웃 루프에는 예외 처리기가 없다. 데코레이터가 아무것도 전파하지 않는다는 전제 위에 그렇게 쓰였다고 :117~:119 가 밝힌다. - -이름이 회귀를 가리키는 그 시험은 회귀를 막지 못한다. OutboundMessagePublisherTest:141 과 :142 가 단언하는 것은 예외가 나가지 않는다는 것과 브로커가 메시지를 받았다는 것뿐이다. 프로브에서 초기 커밋 구조를 돌려 보니 둘 다 통과했다. - -알림 쪽에서 이 클래스를 조립하는 시험은 열둘이다. 그 시험들이 던지게 만든 것은 위임 제공자다. 메시징 회귀 시험이 로거를 던지게 할 때 쓴 Proxy.newProxyInstance 는 알림 모듈에 한 번도 나오지 않는다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Gradle : 9.0.0 -확인 방식 : 같은 로거가 나오는 자리 전수, 두 파일의 커밋 이력과 초기 커밋 시점의 메시징 본문 인용, 지금의 두 본문과 팬아웃 루프 인용, NotificationPort 를 참조하는 main 파일 계수, 메시징 회귀 시험 111~145 전문 인용, 알림 쪽에서 이 클래스를 조립하는 시험과 그 시험들이 던지게 만든 대상 나열 및 두 모듈의 Proxy.newProxyInstance 계수, 저장소 build 산출물에서 로드했음을 출력에 찍은 프로브로 세 구조에 같은 로거를 물려 일곱 행 관측 -소스 수정 : x - -## 재현 조건 - -1. 그 로거가 나오는 자리를 저장소에서 모두 찾는다. -2. 두 소비자 파일의 커밋 이력을 나란히 뽑고, 초기 커밋 시점의 메시징 본문을 꺼낸다. -3. 지금의 두 본문과 알림 데코레이터를 부르는 팬아웃 루프를 인용한다. -4. 메시징 회귀 시험을 단언부까지 포함해 전문으로 싣는다. -5. 알림 쪽에서 이 클래스를 조립하는 시험을 모두 찾고, 그 시험들이 무엇을 던지게 만들었는지 나열한다. -6. 두 모듈에서 Proxy.newProxyInstance 를 센다. -7. slf4j Logger 프록시를 만들어 debug 에서만, 그리고 양쪽에서 던지게 한다. 로드한 클래스의 출처를 출력에 찍고, 지금의 두 구조와 초기 커밋 구조에 같은 로거를 물린다. -8. 초기 커밋 구조에서 그 회귀 시험의 두 단언이 성립하는지 프로브 안에서 판정한다. - -## 본문 - - - -`FailOpenDependencyLogger` 는 선택적 어댑터들이 공유하는 로그 계약이다. 실패는 항상 WARN 이고, 클래스 자바독은 본문·수신자·payload 를 인자로 받지 않으므로 PII 가 로그에 닿을 수 없다고 적는다. 예외 메시지를 통해서는 닿는다는 것을 다른 기록이 프로브로 확인했다. - -## 같은 로거를 두 어댑터가 쓴다 - -:::evidence key="analysis-finding-a04-f002" alt="저장소 루트에서 돌린 정적 검색과 프로브 실행의 출력 209줄. 먼저 FailOpenDependencyLogger 가 나오는 자리 여덟이 실리는데 메시징 어댑터 main 과 그 시험, 알림 어댑터 main 과 그 시험 둘, 지원 모듈 시험, 그리고 두 설정 클래스다. 이어서 두 파일의 git 이력이 나온다. FailOpenNotificationProvider 는 821fe00c 초기 커밋 한 줄뿐이고, OutboundMessagePublisher 는 2f5d2fc2 와 821fe00c 두 줄이다. 다음으로 821fe00c 시점의 메시징 publish 26~37번 줄이 실리는데 try 안에 broker.send 와 logSuccess 가 함께 있고 catch 가 logFailure 를 부른다. 그 아래에 지금의 메시징 publish 26~60번 줄이 실려 28~31번 줄 주석과 32번 줄 sent 플래그와 33~35번 줄 try 와 39번·43번 줄의 관측 호출과 54~60번 줄 observeQuietly 가 보인다. 지금의 알림 send 는 7~11번 줄 클래스 자바독과 35~45번 줄 본문이 실리는데 37번 줄 try 안에 38번 delegate.send 와 39번 logSuccess 가 있고 40번 catch 가 42번에서 logFailure 를 부른다. 그 send 를 부르는 RoutingNotifier 115~123번 줄이 나오는데 117~119번 주석이 데코레이터가 전파하지 않으므로 try/catch 가 필요 없다고 적고 120~122번이 try/catch 없는 팬아웃 루프다. NotificationPort 를 참조하는 main 파일은 NotificationConfig 와 RoutingNotifier 둘이다. 메시징 쪽 회귀 시험 111~145번 줄이 전문으로 실리는데 119~135번이 던지는 로거 프록시를 만들고 141번과 142~144번이 두 단언이다. 알림 쪽에서 그 클래스를 조립하는 시험은 NotificationAdapterTest 네 자리와 RoutingNotifierTest 여덟 자리인데, 그 두 파일이 던지게 만든 것은 각각 79번과 130번의 위임 제공자뿐이고 Proxy.newProxyInstance 는 알림 모듈에 0 개 메시징 모듈에 1 개다. 클래스 이름을 파일명에 가진 시험은 0 개다. 마지막으로 프로브가 실린다. 로드한 세 클래스가 모두 저장소 build 산출물에서 왔다는 것이 먼저 나오고, 알림 쪽은 로거가 던지지 않으면 전송 1 debug 1 warn 0, 성공 로거만 던지면 warn 1, 둘 다 던지면 IllegalStateException 이 나간다. 메시징 쪽은 세 경우 모두 warn 0 이고 나간 예외가 없다. 821fe00c 구조에 성공 로거만 던지게 하면 warn 1 이 되는데, 그 회귀 시험의 두 단언은 그 구조에서도 모두 통과한다." caption="같은 로거를 쓰는 여덟 자리 · 두 파일의 이력과 초기 커밋의 메시징 구조 · 지금의 두 구조 · 팬아웃 루프와 NotificationPort 참조 · 회귀 시험 전문과 두 단언 · 알림 쪽 조립 시험 열둘과 던지는 대상 · 저장소 클래스를 로드한 프로브의 일곱 행 — 209줄 · exit 0" zoom="true" -::: - -이 타입을 필드로 가진 main 클래스는 `OutboundMessagePublisher:19` 와 `FailOpenNotificationProvider:17` 둘이다. - -## 두 구조는 같은 커밋에서 같은 모양으로 출발했다 - -`FailOpenNotificationProvider` 의 커밋 이력은 `821fe00c` 한 줄이다. `OutboundMessagePublisher` 는 `821fe00c` 와 `2f5d2fc2` 두 줄이다. - -`821fe00c` 시점의 메시징 `publish` 를 꺼내 보면 `try` 안에 `broker.send` 와 `logSuccess` 가 함께 있고 `catch (Exception ex)` 가 `logFailure` 를 부른다. 지금의 알림 `send` 와 같은 모양이다. - -설계가 갈린 것이 아니라 한쪽만 옮겨 갔다. - -## OutboundMessagePublisher 는 sent 플래그를 세운 뒤 try 밖에서 관측을 부른다 - -`:32` 가 `boolean sent = false` 를 두고, `:33`\~`:35` 의 `try` 가 감싸는 것은 `broker.send(message)` 하나다. 성공하면 `:35` 가 플래그를 세운다. - -실패 관측은 `catch` 안 `:39` 에서, 성공 관측은 `try` 밖 `:42`\~`:45` 에서 일어난다. 둘 다 `observeQuietly` 를 거치고 `:55`\~`:59` 가 그 안에서 던진 `RuntimeException` 을 삼킨다. 그 메서드의 자바독에는 디스크가 찬 appender 가 호출자가 브로커에 대해 믿는 것을 바꾸면 안 되므로 진단은 권위를 갖지 않는다고 적혀 있다. - -`:28`\~`:31` 주석은 이 구조가 무엇을 고친 것인지 적는다. 전송과 성공 로그가 한 `try` 를 공유하던 시절, 브로커가 이미 메시지를 받은 뒤 로거가 던지면 같은 `catch` 가 그것을 발행 실패로 보고했다. - -## 알림 쪽은 전송과 성공 로그가 한 try 안에 있다 - -`:37` 의 `try` 안에 `:38` 의 `delegate.send(notification)` 와 `:39` 의 `logSuccess` 가 함께 있다. `:40` 의 `catch (Exception ex)` 가 `:42` 에서 `logFailure` 를 부른다. - -메시징 쪽의 `sent` 같은 플래그가 없어서 `delegate.send` 의 결과와 `logSuccess` 의 결과를 구분하지 않는다. - -클래스 자바독 `:8`\~`:10` 에는 제공자 실패를 기록하고 삼켜서 알림이 코어 유스케이스를 실패시키지 않게 한다고 적혀 있다. - -## 던지는 로거를 세 구조에 넣었을 때의 호출 수 - -프로브는 저장소의 build 산출물에서 세 클래스를 로드한다. 출력 첫 세 줄이 그 경로를 찍는다. - -slf4j `Logger` 를 프록시로 만들어 `debug` 나 `warn` 이 불릴 때 던지게 했다. 저장소의 메시징 회귀 시험이 쓰는 방식과 같다. 위임 전송과 브로커 전송은 항상 성공하도록 두었다. - -알림 쪽에서 로거가 던지지 않으면 `delegate.send` 1 회 · `debug` 1 회 · `warn` 0 회다. 성공 로거만 던지면 `warn` 이 1 회가 된다. `debug` 호출이 던져서 SUCCESS 줄은 남지 않고, 위임 전송이 성공한 그 한 번에 대해 실패 쪽 기록이 남는다. - -성공 로거와 실패 로거가 모두 던지면 `IllegalStateException` 이 `send` 밖으로 나간다. 위임 전송은 이미 성공한 뒤다. - -메시징 쪽은 같은 세 경우 모두 `warn` 0 회이고 나가는 예외가 없다. - -## 그 예외가 나가면 남은 제공자도 호출되지 않는다 - -`RoutingNotifier:120`\~`:122` 의 팬아웃 루프는 라우트가 지목한 제공자들을 차례로 부른다. `try`/`catch` 가 없다. - -`:117`\~`:119` 주석이 그 이유를 적는다 — `FailOpenNotificationProvider.send` 가 `throws` 를 선언하지 않으므로 `try`/`catch` 가 필요 없다는 것이다. - -`NotificationPort` 를 참조하는 main 파일은 `NotificationConfig` 와 `RoutingNotifier` 둘이다. - -## 메시징 쪽 회귀 시험은 이 회귀를 고정하지 못한다 - -`OutboundMessagePublisherTest:114` 의 `aLoggerFailureAfterAConfirmedSendIsNotAPublishFailure` 는 `:119`\~`:135` 에서 `debug` 에만 던지는 로거 프록시를 만든다. 단언은 둘이다 — `:141` 이 `publish` 가 예외를 던지지 않는 것, `:142`\~`:144` 가 브로커가 그 메시지를 받은 것이다. - -프로브에서 `821fe00c` 구조에 같은 로거를 물려 보면 `warn` 이 1 회 불리지만 두 단언은 모두 통과한다. 그 시험은 WARN 이 남았는지를 보지 않는다. - -## 알림 쪽 시험은 위임 제공자만 던지게 한다 - -`FailOpenNotificationProvider` 를 조립하는 시험은 `NotificationAdapterTest` 네 자리와 `RoutingNotifierTest` 여덟 자리다. - -그 두 파일이 던지게 만든 것은 각각 `:79` 와 `:130` 의 위임 제공자다. 로거를 던지게 만드는 `Proxy.newProxyInstance` 는 알림 모듈에 0 개이고 메시징 모듈에 1 개다. - -그 클래스 이름을 파일명에 가진 시험 파일은 0 개다. - -## 원문과 갈리는 자리 - -원문은 메시징 쪽 회귀 시험이 이 구조를 고정한다고 적었다. 프로브는 그것을 반박한다. 그 시험의 두 단언은 초기 커밋 구조에서도 통과하므로, 되돌려도 초록이다. - -원문은 두 소비자가 같은 로거를 다르게 쓴다고 적었다. 맞지만 이력을 보면 설계가 갈린 것이 아니다. 둘은 같은 커밋에서 같은 모양으로 출발했고 한쪽만 옮겨 갔다. - -알림 쪽에서 성공한 전송에 실패 기록이 남는다는 것과 소스 주석이 과거 버그를 적는다는 것은 원문대로다. - -## 확인하지 못한 것 - -실제 배포에서 어떤 조건이 로거를 던지게 하는지 조사하지 않았다. 프로브는 던지는 상황을 만들어 세 구조를 나란히 놓은 것이다. - -예외가 나갔을 때 같은 라우트의 남은 제공자가 실제로 건너뛰어지는지 실행으로 보지 않았다. 팬아웃 루프에 `try`/`catch` 가 없다는 줄과 그 이유를 적은 주석을 읽었다. - -WARN 줄을 세는 알림 규칙이 저장소 밖 어딘가에 있는지는 찾지 않았다. - -예외가 나갔을 때 같은 라우트의 남은 제공자가 실제로 건너뛰어지는지 실행으로 보지 않았다. 팬아웃 루프에 `try`/`catch` 가 없다는 줄과 그 이유를 적은 주석을 읽었다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a04-f004.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a04-f004.md deleted file mode 100644 index c276ec1..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a04-f004.md +++ /dev/null @@ -1,142 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a04-f004 -title: 실패 로거가 던지면 그 예외가 제공자 예외를 밀어내고 호출자로 나간다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a04-f004 -evidenceCapturedOn: 2026-09-04 -body: case-analysis-finding-a04-f004.body.md -assets: - - key: analysis-finding-a04-f004 - file: ../../../final/evidence/rendered/analysis-finding-a04-f004.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a04-f004.txt -source: - - 원본 분석 절은 final/document.md#a04 §5 이다. ---- - -# 실패 로거가 던지면 그 예외가 제공자 예외를 밀어내고 호출자로 나간다 - -`FailOpenNotificationProvider:40` 의 `catch` 는 `:42` 에서 `logFailure` 를 부르는데 그 호출을 감싸는 `try` 가 없다. 위임 제공자가 `provider is down` 으로 던진 뒤 그 로거까지 던지면, 프로브에서 호출자가 받은 것은 `IllegalStateException : the appender is out of disk` 였고 그 예외의 `cause` 가 없음, `suppressed` 가 0 이었다. - -## 관계 - -- **메시징만 성공 로그를 try 밖으로 옮기고 알림은 같은 try 에 남겨 두었다** - 같은 `FailOpenNotificationProvider` 의 성공 경로를 다룬 기록이다. 그 기록은 `delegate.send` 가 성공한 뒤 `logSuccess` 가 던지는 경우이고, 여기는 `delegate.send` 가 실패한 뒤 `catch` 안의 `logFailure` 가 던지는 경우다. -- **모르는 것은 성공도 실패도 아닌 세 번째 결과여야 한다** - 그 규칙은 확정되지 않은 결과를 성공이나 실패로 접지 말라고 한다. 여기서는 확정된 제공자 실패가 로거 실패에 덮여, 호출자가 받는 예외가 제공자 결과를 가리키지 않는다. - -## 문제 - -이 데코레이터의 클래스 자바독은 알림이 어떤 경우에도 업무 흐름을 깨뜨리지 않게 하는 것이 자기 일이라고 적는다. - -그 계약이 관측 자체가 실패하는 경우에도 성립하는지 확인했다. - -## 결론 - -send 는 :37 의 try 안에서 위임 전송과 성공 로그를 함께 부른다. 실패를 받는 :40 의 처리기가 :42 에서 로그를 남기는데, 그 자리에서 예외가 나면 잡아 줄 것이 남아 있지 않다. - -메시징 쪽은 같은 자리를 다르게 쓴다. OutboundMessagePublisher:39 의 실패 관측도 observeQuietly 를 거치고 :57 이 RuntimeException 을 삼킨다. :28~:31 주석은 그 구조가 무엇을 고친 것인지 적어 둔다. - -프로브에서 위임 제공자가 항상 던지게 하고 로거 프록시를 물렸다. 로거가 멀쩡한 경우는 계약대로다. - -그 로거까지 던지게 하면 예외가 send 밖으로 나간다. 나간 것에는 원인도 억제된 예외도 붙어 있지 않아서, 제공자가 왜 실패했는지가 호출자 쪽에서 복구되지 않는다. - -이 데코레이터를 부르는 RoutingNotifier 의 루프는 개별 실패를 잡지 않고 지나간다. 그렇게 써도 되는 근거를 :117~:119 가 밝혀 두었는데, 프로브가 그 근거를 무너뜨리는 경우를 하나 보였다. - -이 경우를 붙드는 시험도 없다. 던지는 로거를 만드는 기법이 메시징 모듈에는 한 번 쓰였고 알림 모듈에는 한 번도 쓰이지 않았다. 조립 시험 둘이 던지도록 손본 것은 로거가 아니라 그 아래 제공자다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Gradle : 9.0.0 -확인 방식 : 데코레이터의 클래스 자바독과 send 본문 인용, 메시징 쪽 publish 전체와 관측 헬퍼 인용, 팬아웃 루프와 그 서명·주석 인용, NotificationPort 를 참조하는 main 파일 전수, 저장소 클래스를 그대로 써서 위임 제공자가 던지는 경로에 로거 예외를 겹친 두 경우 관측하고 나간 예외의 cause 와 suppressed 까지 출력, 두 모듈의 로거 프록시 계수와 조립 시험이 던지게 만드는 대상 나열 -소스 수정 : x - -## 재현 조건 - -1. 데코레이터의 클래스 자바독과 send 본문을 인용한다. -2. 메시징 쪽 publish 를 주석과 플래그까지 포함해 싣고 관측 헬퍼를 함께 인용한다. -3. 이 데코레이터를 부르는 팬아웃 루프를 메서드 서명부터 인용한다. -4. NotificationPort 를 참조하는 main 파일을 전부 찾는다. -5. 위임 제공자가 항상 던지도록 만들고 slf4j Logger 프록시를 붙인다. -6. warn 이 정상인 경우와 던지는 경우로 나눠 호출 수와 나간 예외를 적고, 그 예외의 cause 와 suppressed 도 함께 출력한다. -7. 두 모듈에서 로거 프록시를 만드는 자리를 세고, 조립 시험이 무엇을 던지게 만드는지 나열한다. - -## 본문 - - - -`FailOpenNotificationProvider` 는 제공자 실패를 삼켜서 알림이 코어 유스케이스를 실패시키지 않게 하는 데코레이터다. 그 계약이 클래스 자바독 `:8`\~`:10` 에 적혀 있다. - -## catch 안의 logFailure 호출에는 try 가 없다 - -:::evidence key="analysis-finding-a04-f004" alt="저장소 루트에서 돌린 정적 검색과 프로브의 출력 89줄. 먼저 FailOpenNotificationProvider 7~11번 줄의 클래스 자바독이 실려 제공자 실패를 기록하고 삼켜서 알림이 코어 유스케이스를 실패시키지 않는다고 적는다. 35~45번 줄은 send 본문인데 37번 try 안에 38번 delegate.send 와 39번 logSuccess 가 있고 40번 catch 가 42번에서 logFailure 를 부른다. 이어서 메시징 쪽 OutboundMessagePublisher 26~46번 줄이 실리는데 28~31번 주석이 전송과 성공 로그가 한 try 를 공유하던 시절의 버그를 적고 32번이 sent 플래그를 두며 33~35번 try 가 브로커 전송만 감싸고 39번과 43번이 관측을 부른다. 48~60번 줄의 observeQuietly 가 RuntimeException 을 삼킨다. 그 아래에 RoutingNotifier 112~123번 줄이 실려 notify 서명과 117~119번 주석과 예외 처리기 없는 팬아웃 루프가 보인다. NotificationPort 를 참조하는 main 파일은 NotificationConfig 와 RoutingNotifier 둘이다. 마지막으로 프로브가 실린다. 위임 제공자가 항상 던지도록 만들고 로거 프록시를 물렸는데, 실패 로거가 정상일 때는 debug 0 warn 1 이고 호출자에게 나간 예외가 없다. 실패 로거도 던질 때는 debug 0 warn 1 이고 IllegalStateException 이 the appender is out of disk 라는 메시지로 나가며, 그 예외의 cause 가 없음이고 suppressed 가 0 이다. 그 경로를 고정하는 시험으로는 알림 모듈의 Proxy.newProxyInstance 가 0 개 메시징 모듈이 1 개이고, 이 데코레이터를 조립하는 두 시험이 던지게 만드는 것은 각각 79번과 130번의 위임 제공자뿐이다." caption="데코레이터가 자기에게 매긴 계약과 send 본문 · 메시징 쪽 publish 전체와 observeQuietly · notify 서명과 예외 처리기 없는 팬아웃 루프 · 위임 실패에 로거 예외를 겹친 두 경우와 그 예외의 원인 사슬 · 그 경로를 고정하는 시험의 부재 — 89줄 · exit 0" zoom="true" -::: - -`:37` 의 `try` 안에 `:38` 의 `delegate.send` 와 `:39` 의 `logSuccess` 가 있다. `:40` 의 `catch (Exception ex)` 가 `:42` 에서 `logFailure` 를 부른다. - -`catch` 블록 안에서 던진 예외는 그 `catch` 가 다시 받지 않는다. 메서드 밖으로 나간다. - -## OutboundMessagePublisher 는 실패 관측도 observeQuietly 로 감싼다 - -`:28`\~`:31` 주석이 그 구조의 내력을 적는다. 전송과 성공 로그가 한 `try` 를 공유하던 시절, 브로커가 이미 메시지를 받은 뒤 로거가 던지면 같은 `catch` 가 그것을 발행 실패로 보고했다. - -지금은 `:32` 가 `boolean sent` 를 두고 `:33`\~`:35` 의 `try` 가 `broker.send` 만 감싼다. 실패 관측은 `catch` 안 `:39` 에서, 성공 관측은 `try` 밖 `:43` 에서 일어나고 둘 다 `observeQuietly` 를 거친다. - -`:54`\~`:60` 의 그 헬퍼가 `:57` 에서 `RuntimeException` 을 삼킨다. 자바독은 진단이 권위를 갖지 않는다고 적는다. - -## 위임 실패에 로거 예외를 겹쳤을 때 - -프로브는 위임 제공자가 항상 `IllegalStateException("provider is down")` 을 던지도록 만든다. 로거는 slf4j `Logger` 프록시다. - -실패 로거가 정상이면 `debug` 0 회 · `warn` 1 회이고 호출자에게 나가는 예외가 없다. 계약대로 삼킨다. - -실패 로거도 던지게 하면 호출 수는 같은데 `IllegalStateException : the appender is out of disk` 가 `send` 밖으로 나간다. - -그 예외의 `cause` 는 없음이고 `suppressed` 는 0 이다. 제공자가 던진 `provider is down` 은 `ex` 변수에 담겨 `logFailure` 의 넷째 인자로만 넘어갔고, 그 호출이 던졌으므로 호출자가 받는 예외에 아무 흔적도 남기지 않았다. - -## RoutingNotifier 의 팬아웃 루프에는 예외 처리기가 없다 - -`:113` 의 `notify` 가 `:114` 에서 라우트를 풀고 `:120`\~`:122` 가 제공자들을 차례로 부른다. - -`:117`\~`:119` 주석이 그 이유를 적는다. 개별 제공자 실패는 데코레이터 안에서 관측되고 전파되지 않으므로 하나가 실패해도 남은 팬아웃이 막히지 않는다는 것이다. - -프로브에서는 그 데코레이터가 예외를 전파했다. 주석이 든 전제가 그 경우에는 성립하지 않는다. - -## 오늘 그 예외를 받을 호출자가 없다 - -`NotificationPort` 를 참조하는 main 파일은 `NotificationConfig` 와 `RoutingNotifier` 둘이다. 앞엣것은 빈을 조립하고 뒤엣것은 그 포트를 구현한다. - -그 포트를 부르는 프로덕션 코드가 없다. 지금 이 경로를 지나는 요청이 없다는 뜻이고, 이 템플릿을 가져다 호출자를 붙이는 순간 살아난다. - -## 로거가 던지는 경우를 만드는 시험이 없다 - -로거를 던지게 만드는 `Proxy.newProxyInstance` 는 알림 모듈에 0 개이고 메시징 모듈에 1 개다. - -`NotificationAdapterTest` 와 `RoutingNotifierTest` 가 던지게 만드는 것은 각각 `:79` 와 `:130` 의 위임 제공자다. 로거는 정상이다. - -## 원문과 갈리는 자리 - -원문은 실패 경로에서 경고 로거가 실패하면 그 예외가 호출자까지 전파된다고 적었고 프로브가 그대로 재현한다. - -원문에는 무엇이 전파되는지가 빠져 있다. 나간 예외는 로거의 것이고, `cause` 와 `suppressed` 가 비어 있어 제공자 예외와 연결되지 않는다. - -원문이 확인하지 않은 것도 있다. 이 경로를 지나는 프로덕션 호출자가 지금은 없다. - -## 확인하지 못한 것 - -실제 배포에서 WARN 호출이 던질 조건을 조사하지 않았다. - -진짜 로그 설비가 던지기 직전에 무엇을 남기는지는 보지 않았다. 프록시는 아무 기록 없이 곧바로 던진다. - -수정 위치를 공유 로거 쪽으로 잡을지 이 데코레이터 쪽으로 잡을지 결론 내지 않았다. - -## 등급에 대해 - -원본은 P1 이다. 다만 이 리비전에서 그 포트를 참조하는 main 파일은 빈을 조립하는 쪽과 구현하는 쪽 둘뿐이고 실제로 알림을 보내는 코드가 없다. 오늘 이 예외를 받을 자리가 없으므로 등급을 그대로 두되 그 조건을 적어 둔다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a04-f005.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a04-f005.md deleted file mode 100644 index e8bd9ce..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a04-f005.md +++ /dev/null @@ -1,146 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a04-f005 -title: README 의 세 문장 중 둘은 쓰일 때부터 틀렸고 하나만 나중에 어긋났다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a04-f005 -evidenceCapturedOn: 2026-09-04 -body: case-analysis-finding-a04-f005.body.md -assets: - - key: analysis-finding-a04-f005 - file: ../../../final/evidence/rendered/analysis-finding-a04-f005.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a04-f005.txt -source: - - 원본 분석 절은 final/document.md#a04 §6 이다. ---- - -# README 의 세 문장 중 둘은 쓰일 때부터 틀렸고 하나만 나중에 어긋났다 - -`support/README.md` 와 `support/CLAUDE.md` 는 `821fe00c` 에서 나란히 추가됐다. 그래서 README 가 적은 "아직 별도 CLAUDE.md 를 두지 않았다" 는 쓰인 날부터 거짓이었고, "의존 정책의 SSOT 는 `src/build.gradle` 의 `allowedProjectDependencies`" 도 그때 이미 파생 코드를 가리켰다. 실제로 시간이 지나 어긋난 것은 셋 중 하나뿐이다. - -## 관계 - -- **문서의 수치는 세지 말고 파생하거나 게이트로 붙든다** - 그 규칙은 문서의 서술을 소스에서 파생하거나 검사로 붙들라고 한다. 이 README 의 문장들은 손으로 적혀 있고 소스와 대조하는 검사가 없다. -- **과대 진술 문서를 과소보다 먼저 고친다** - 그 규칙은 문서가 실제보다 많이 약속하는지 적게 약속하는지부터 가르라고 한다. 이 셋의 방향이 서로 다르다. -- **다섯 문서가 "exactly 19 leaf"라고 적고 레지스트리는 62다** - 그 기록에서도 문서가 레지스트리를 따라가지 못했다. 다만 그쪽은 시간이 지나 벌어진 차이이고 여기는 절반이 작성 시점의 오기다. - -## 문제 - -이 README 는 이 모듈이 왜 갈라져 나왔고 어떤 의존이 허용되는지를 적어 둔다. 다른 어댑터가 이 모듈에 기댈 때 참고하는 자리다. - -세 문장이 지금 소스와 맞지 않는다. 각각이 언제부터 틀렸는지 커밋으로 되짚었다. - -## 결론 - -두 마크다운 파일의 추가 커밋이 같다. 이후 규범 문서만 한 번 더 수정됐고 README 에는 그 뒤 커밋이 없다. 규범 문서가 없다고 적은 문장은 나중에 낡은 것이 아니라 처음부터 사실과 달랐다. - -의존 정책 쪽도 마찬가지다. 그 커밋의 빌드 스크립트는 이미 저장소 바깥 파일에서 맵을 만들어 내고 있었고, 지금의 레지스트리 파일은 나흘 뒤에야 들어왔다. allowedProjectDependencies 가 SSOT 였던 적이 없고, 옮겨 간 것은 레지스트리 파일의 위치다. - -지금도 그 이름은 빌드 스크립트에 세 줄 남아 있지만 셋 다 레지스트리에서 파생하거나 그 결과를 읽는 자리다. 값을 담는 것은 modules.json:44~:48 이고, 같은 디렉터리의 CLAUDE.md:9 가 그것을 Registry SSOT 로 못 박는다. - -셋 중 진짜 드리프트는 하나다. OutboundHttpDependencyLogger 는 821fe00c 에 main·test 두 파일로 추가됐다가 5f10b791 에서 함께 삭제됐다. README 가 쓰였을 때는 맞는 문장이었다. - -그 모듈에는 지금 로거 클래스도, slf4j 를 끌어오는 파일도, ERROR 를 남기는 호출도 없다. 대조 기준 하나가 사라진 것이 아니라 대조할 로깅 자체가 남아 있지 않다. - -같은 자리에 넷째가 있다. 이 문단은 PII 가 로그에 닿을 수 없다고 단언하는데, FailOpenDependencyLogger:47 이 예외 메시지를 형식 문자열에 그대로 채워 넣는다. - -## 검증 환경 - -확인 방식 : README 의 두 대목과 그 파일의 커밋 이력 인용, 같은 디렉터리 두 마크다운 파일의 추가·수정 이력을 나란히 뽑기, README 가 쓰인 커밋의 build.gradle 에서 레지스트리 파일 경로 확인과 modules.json 의 추가 커밋 확인, 지금의 파생 자리 전수와 레지스트리 항목 인용, CLAUDE.md 머리 열 줄 인용, 없는 로거 이름 계수를 자기시험·대조와 함께 확인하고 그 이름의 추가·삭제 이력 추적, httpclient main 의 로거 파일·slf4j import·error 호출 계수, README 의 PII 주장과 로거 본문 대조 -소스 수정 : x - -## 재현 조건 - -1. README 에서 정본 위치와 규범 문서 존재 여부와 대조 로거 이름과 PII 주장을 적은 대목을 인용한다. -2. 그 파일과 같은 디렉터리 규범 문서의 추가·수정 이력을 나란히 뽑는다. -3. README 가 쓰인 커밋의 빌드 스크립트에서 레지스트리 파일 경로를 확인하고, 지금 레지스트리 파일이 언제 들어왔는지 본다. -4. 지금 그 이름이 나오는 자리를 전부 세고 값을 담는 파일과 규범 문서의 SSOT 선언을 인용한다. -5. 대조 로거 이름을 자기시험과 대조 이름과 함께 세고, 그 이름의 추가·삭제 커밋을 뽑는다. -6. 그 모듈의 로거 파일 수와 slf4j import 수와 error 호출 수를 센다. -7. README 의 PII 주장과 로거의 실패 기록 본문을 나란히 놓는다. - -## 본문 - - - -`src/adapter/outbound/support/README.md` 는 이 모듈의 존재 이유와 의존 정책을 설명한다. 새 어댑터를 붙이는 사람이 먼저 여는 문서다. - -## README 가 적은 네 문장 - -:::evidence key="analysis-finding-a04-f005" alt="저장소 루트에서 돌린 정적 검색 출력 96줄. 먼저 support/README.md 7~11번 줄과 20~25번 줄이 실린다. 앞엣것은 허용·금지 의존 정책의 SSOT 가 src/build.gradle 의 allowedProjectDependencies 항목이며 이 모듈은 아직 별도 CLAUDE.md 를 두지 않았다고 적는다. 뒤엣것은 이 로거가 WARN 을 쓰는 이유를 적으면서 httpclient 모듈의 OutboundHttpDependencyLogger 와 구분되고 시그니처가 본문·수신자·페이로드를 받지 않아 PII 가 로그에 닿지 않는다고 적는다. 그 README 의 커밋 이력은 821fe00c 한 줄뿐이다. 이어서 두 파일이 언제 태어났는지가 나온다. CLAUDE.md 와 README.md 가 821fe00c 에서 나란히 A 로 추가되고 CLAUDE.md 만 b3add016 에서 M 으로 수정된다. README 가 쓰인 커밋의 build.gradle 558번 줄은 레지스트리 파일을 저장소 바깥의 .harness/project/modules.yaml 로 가리키고, src/config/architecture/modules.json 은 나흘 뒤 b3add016 에서 처음 들어온다. 그 아래에 지금의 src/build.gradle 에서 allowedProjectDependencies 가 나오는 세 줄이 전부 실리는데 1419번이 registry.modules 에서 만들어 내는 자리이고, modules.json 41~52번 줄이 이 모듈의 allowed_dependencies 셋과 runtime_memberships 를 담는다. 다음으로 이 디렉터리의 마크다운 파일 둘과 CLAUDE.md 1~10번 줄이 실리는데 9번 줄이 Registry SSOT 를 src/config/architecture/modules.json 으로 못 박는다. 그 파일의 이력도 함께 나온다. 세 번째로 OutboundHttpDependencyLogger 를 가진 파일이 0 개이고, httpclient main 에 이름에 Logger 가 든 파일도 0 개, org.slf4j 를 import 하는 파일도 0 개, log.error 호출도 0 줄이다. 없는 이름 자기시험은 0 개, 대조 FailOpenDependencyLogger 는 12 개다. 그 이름의 이력은 821fe00c 에서 main 과 test 두 파일이 A 로 추가되고 5f10b791 에서 둘 다 D 로 삭제된 것이다. 마지막으로 README 24~25번 줄의 PII 주장과 FailOpenDependencyLogger 36~48번 줄이 나란히 실리는데, 그 logFailure 가 47번 줄에서 cause.getMessage() 를 로그 형식 문자열에 넣는다." caption="README 의 두 대목과 그 파일의 단일 커밋 이력 · 두 파일이 같은 커밋에서 태어난 기록과 초기 레지스트리 위치 · 지금의 파생 자리 셋과 레지스트리의 실제 항목 · CLAUDE.md 가 못 박은 SSOT · 없는 로거의 계수와 자기시험과 대조와 그 삭제 이력 · README 의 PII 주장과 로거가 실제로 넣는 값 — 96줄 · exit 0" zoom="true" -::: - -`:7`\~`:9` 한 문장에 두 가지가 들어 있다. 허용·금지 의존 정책의 SSOT 가 `src/build.gradle` 의 `allowedProjectDependencies['adapter:outbound:support']` 항목이고, 이 모듈에는 아직 별도 `CLAUDE.md` 가 없다는 것이다. - -`:23`\~`:25` 에 둘이 더 있다. 이 로거가 `httpclient` 모듈의 `OutboundHttpDependencyLogger` 와 구분된다는 것, 그리고 메서드 시그니처가 본문·수신자·페이로드를 받지 않아 PII 가 로그에 닿지 않는다는 것이다. - -이 파일에는 `821fe00c` 이후 커밋이 없다. - -## 두 파일은 같은 커밋에서 태어났다 - -`CLAUDE.md` 와 `README.md` 가 `821fe00c` 에서 나란히 `A` 로 추가된다. `CLAUDE.md` 만 `b3add016` 에서 `M` 으로 한 번 더 수정됐다. - -그러므로 "이 모듈은 아직 별도 CLAUDE.md 를 두지 않았다" 는 문장은 갱신을 놓친 것이 아니다. 그것을 적은 커밋이 같은 파일을 함께 넣었다. - -## SSOT 는 그 자리에 있었던 적이 없다 - -`821fe00c` 시점의 `build.gradle:558` 은 레지스트리 파일을 `.harness/project/modules.yaml` 로 가리킨다. 저장소 바깥 경로다. `:577` 이 그 레지스트리에서 `allowedProjectDependencies` 를 만들어 낸다. - -`src/config/architecture/modules.json` 은 나흘 뒤 `b3add016` 에 처음 들어온다. - -즉 README 가 쓰인 날에도 `src/build.gradle` 의 그 이름은 파생물이었다. 이후에 바뀐 것은 레지스트리 파일이 저장소 밖에서 안으로 들어온 것이고, 정본의 성격이 옮겨 간 것이 아니다. - -## 지금의 파생 관계 - -`src/build.gradle` 에 `allowedProjectDependencies` 가 나오는 줄은 셋이고 `:1419` 가 `registry.modules` 에서 만들어 내는 자리다. 나머지 둘은 그 맵을 읽는다. - -값을 담는 것은 `modules.json:41`\~`:52` 다. 이 모듈의 `allowed_dependencies` 셋과 `runtime_memberships` 가 거기 있다. - -같은 디렉터리의 `CLAUDE.md:9` 가 `Registry SSOT: src/config/architecture/modules.json` 이라고 적는다. README 가 없다고 한 그 파일이 정본 위치를 못 박고 있다. - -## 하나만 시간이 지나 어긋났다 - -`OutboundHttpDependencyLogger` 는 `821fe00c` 에서 main 과 test 두 파일로 추가됐다. `5f10b791` 에서 둘 다 `D` 로 삭제됐다. - -README 가 쓰인 날에는 실재하는 클래스였고 그 뒤에 사라졌다. 셋 중 이것만이 문서가 소스를 따라가지 못한 경우다. - -지금 그 이름을 가진 파일은 0 개다. 없는 이름으로 같은 검색을 걸어도 0 이 나오므로, 실재하는 `FailOpenDependencyLogger` 로 같은 검색을 걸어 12 를 받았다. - -`httpclient` main 에는 이름에 `Logger` 가 든 파일이 0 개이고, `org.slf4j` 를 import 하는 파일이 0 개이며, `log.error` 호출이 0 줄이다. 대조 대상만 사라진 것이 아니라 그 모듈의 로깅 자체가 남아 있지 않다. - -## 넷째 문장 - -`:24`\~`:25` 는 메서드 시그니처가 본문·수신자·페이로드를 받지 않아 PII 가 로그에 닿지 않는다고 적는다. - -`FailOpenDependencyLogger:37`\~`:48` 의 `logFailure` 는 `Throwable cause` 를 받는다. `:41` 의 형식 문자열이 `error="{}: {}"` 를 담고 `:46`\~`:47` 이 그 자리에 `cause.getClass().getSimpleName()` 과 `cause.getMessage()` 를 넣는다. - -시그니처가 payload 를 받지 않는다는 것은 맞다. 예외 메시지를 통해 닿지 않는다는 것은 그 문장이 보장하지 못한다. - -## 원문과 갈리는 자리 - -원문은 정본 위치와 안내 문서 존재 여부와 HTTP 로거 존재 여부 셋이 어긋난다고 적었다. 셋 다 지금 소스와 다르다. - -갈리는 것은 그 셋을 드리프트로 묶은 부분이다. 커밋 이력을 보면 앞의 둘은 문서가 낡은 것이 아니라 작성 시점부터 사실과 달랐다. - -원문이 적지 않은 것은 넷째다. 같은 README 가 PII 가 로그에 닿지 않는다고 적는데 그 로거는 예외 메시지를 형식 문자열에 넣는다. - -## 확인하지 못한 것 - -이 서술을 근거로 잘못된 의존이 들어간 적이 있는지 커밋을 뒤지지 않았다. - -`5f10b791` 이 그 로거를 지운 이유를 커밋 메시지 밖에서 확인하지 않았다. - -여기서 다룬 넷 밖의 서술은 소스와 대조하지 않았다. - -`5f10b791` 이 그 로거를 지운 이유를 커밋 메시지 밖에서 확인하지 않았다. - -README 의 나머지 문장까지 소스와 맞춰 보지는 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f001.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f001.md deleted file mode 100644 index 3de0d7d..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f001.md +++ /dev/null @@ -1,163 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a05-f001 -title: 추정식의 여유분이 상수라서 인코드가 내준 세 크기를 디코드가 거절한다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a05-f001 -evidenceCapturedOn: 2026-09-04 -body: case-analysis-finding-a05-f001.body.md -assets: - - key: analysis-finding-a05-f001 - file: ../../../final/evidence/rendered/analysis-finding-a05-f001.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a05-f001.txt -source: - - 원본 분석 절은 final/document.md#a05 §6.2 이다. ---- - -# 추정식의 여유분이 상수라서 인코드가 내준 세 크기를 디코드가 거절한다 - -`SignedJsonCursorCodec:127` 의 `encodedLength / 4 * 3 + 3` 은 나눗셈이 버린 그룹을 상수 3 으로 메운다. 실제로 필요한 여유는 인코딩 길이를 4 로 나눈 나머지에 따라 3 이거나 2 이거나 1 이라서, 3 이 필요한 자리에서만 추정값이 상한을 넘고 2046·2047·2048 바이트가 인코드는 통과하고 디코드에서 막힌다. - -## 관계 - -- **서명된 커서의 구조와 검증 순서** - 이 추정이 놓인 검증 순서다. 크기 사전 검사가 서명 확인보다 앞에 있다. -- **이진 승인 코덱은 decode 후 재인코딩이 원본과 같아야 한다** - 이 사례가 어기는 왕복 성질이다. 재인코딩까지 가지도 못하고 디코드가 먼저 막는다. -- **커서 서명 검증이 길이·상수시간·순서를 전부 지켜야 했던 이유** - 같은 코덱의 검증 순서를 다룬 사례다. - -## 문제 - -디코드는 페이로드를 풀기 전에 크기를 추정해 상한을 넘으면 거절한다. 큰 토큰이 먼저 메모리에 풀리는 것을 막기 위해서다. - -인코드도 같은 상한을 쓴다. 두 검사가 같은 집합을 가르는지 확인했다. - -## 결론 - -추정식의 여유분이 상수다. L / 4 가 마지막 그룹을 버리므로 그만큼을 메워야 하는데, 메울 양이 L % 4 에 따라 셋 중 하나로 달라진다. 코드는 그중 가장 큰 값을 언제나 쓴다. - -프로브가 두 인코딩을 나란히 계산한 표가 그 주기를 보여 준다. 2043 부터 2049 까지 무패딩 초과분이 3, 2, 1, 3, 2, 1, 3 으로 돈다. 초과가 3 인 자리에서만 추정값이 2049 가 되어 2048 을 넘는다. - -그래서 경계가 셋이다. 2046 바이트는 무패딩 인코딩이 2728 자이고 나머지가 0 이라 추정식이 2049 를 낸다. 2047 과 2048 도 같은 값을 받는다. 그 아래 크기들은 2046 을 받아 상한 안에 든다. - -패딩은 원인이 아니다. 같은 표의 패딩 열을 보면 2044 와 2045 도 2049 를 받는다. 그 인코더를 택했다면 오늘 통과하는 두 크기까지 함께 막혔을 것이다. 2046 바이트는 3 의 배수여서 두 인코딩의 길이가 아예 같다. - -두 주석이 서로 다르게 적는다. :125 자바독은 이 값이 최대치라고 적어 옳고, :103~:104 인라인 주석은 정확히 한정한다고 적어 틀렸다. 설계 의도는 절대 과소평가하지 않는 보수적 사전 검사였고, 실패한 것은 건전성이 아니라 정밀도다. - -인코드 쪽 주석은 큰 페이로드를 막는 까닭을 이렇게 적는다 — 되돌려 받지 못할 토큰은 다음 페이지 요청을 깨뜨린다. 같은 까닭이 이 세 크기에도 걸리는데 인코드는 통과시킨다. - -이 코덱은 프로덕션에 배선돼 있지 않다. 생성자를 부르는 자리가 시험 소스 세트에만 있고, 페이로드를 JSON 으로 옮길 구현체도 main 에 없다. 웹과 GraphQL 과 Mongo 쪽 페이지네이션은 각자 다른 코덱을 쓰며 그 셋 중 이 추정식을 가진 파일이 없다. - -시험은 추정식에 도달한다. SignedJsonCursorCodecTest:168 이 상한을 훨씬 넘는 위조 구간으로 사전 검사를 발화시킨다. 빠진 것은 경계 바로 옆에서 왕복을 단언하는 시험이고 그 수가 0 이다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Gradle : 9.0.0 -확인 방식 : 인코더·디코더 선언과 두 상한 인용, 인코드의 크기 검사와 그 주석 인용, 디코드의 사전 추정과 추정식과 두 주석 인용, 그 식을 부르는 자리 전수, 이 코덱을 만드는 자리와 페이로드 코덱 구현 계수와 실제 배선된 코덱 셋의 추정식 사용 여부, 경계 값을 쓰는 시험 줄 계수와 추정식에 도달하는 시험 인용, 같은 식을 무패딩과 패딩 인코딩에 각각 적용한 표 계산, 저장소 클래스를 그대로 써서 여섯 크기를 인코드한 뒤 곧바로 디코드 -소스 수정 : x - -## 재현 조건 - -1. 인코더와 디코더 선언, 두 상한 상수, 인코드의 크기 검사와 그 주석을 인용한다. -2. 디코드의 사전 추정 자리와 추정식, 그리고 그것을 설명하는 주석 둘을 함께 인용한다. -3. 이 코덱을 만드는 자리를 소스 세트별로 세고, 페이로드 코덱을 구현한 main 파일과 실제 배선된 코덱을 확인한다. -4. 2043 부터 2049 까지 무패딩 길이와 패딩 길이를 각각 계산하고, 같은 식을 둘에 적용해 실제 최대치와의 차를 표로 만든다. -5. 페이로드 바이트 수를 그대로 통제하는 코덱을 붙여 여섯 크기를 인코드하고 곧바로 디코드한다. -6. 경계 값을 쓰는 시험 줄을 세고, 추정식에 도달하는 시험과 왕복을 단언하는 시험을 나눠 확인한다. - -## 본문 - - - -`SignedJsonCursorCodec` 은 커서를 JSON 으로 만들어 서명하고 Base64URL 로 인코딩한다. 디코드는 서명을 확인하기 전에 페이로드 크기를 먼저 거른다. - -## 여유분이 상수인 추정식 - -:::evidence key="analysis-finding-a05-f001" alt="저장소 루트에서 돌린 정적 검색과 왕복 프로브의 출력 115줄. 먼저 SignedJsonCursorCodec 37~38번 줄의 인코더·디코더 선언과 53~58번 줄의 두 상한, 63~72번 줄의 encode 크기 검사와 그 주석, 103~108번 줄의 decode 사전 추정, 125~128번 줄의 추정식이 차례로 실린다. 그 식을 부르는 자리는 106번 줄 하나다. 이어서 이 코덱이 프로덕션에 배선되는지가 나온다. new SignedJsonCursorCodec 이 나오는 여섯 자리가 모두 시험이고, CursorPayloadCodec 을 구현한다고 선언한 main 파일이 0 개이며, 실제로 배선된 커서 코덱은 HmacGraphQlCursorCodec 과 HmacWebCursorCodec 과 MongoKeysetCursorCodec 셋인데 그 셋은 같은 추정식을 쓰지 않는다. 그 아래에 두 주석이 나란히 실린다. 125번 자바독은 이 값이 최대치라고 적고 103~104번 인라인 주석은 정확히 한정한다고 적는다. 다음으로 경계 값을 쓰는 시험을 세면 2044·2045·2046·2047 이 0 개이고 2048 이 17 개 2049 가 1 개다. 추정식에 실제로 도달하는 시험은 SignedJsonCursorCodecTest 166~176번 줄인데 3000 자짜리 위조 구간을 넣어 payload exceeds 를 확인하는 것이고, 왕복을 상한 근처에서 단언하는 시험은 0 개이며 이 코덱을 만드는 자리는 test 소스 세트뿐이다. 마지막으로 프로브가 실린다. 먼저 같은 식을 두 인코딩에 적용한 표가 나오는데 n 이 2043 부터 2049 까지일 때 무패딩 길이와 그 추정값, 패딩 길이와 그 추정값, 실제 최대치, 무패딩 초과분이 함께 나온다. 초과분은 3, 2, 1 이 반복되고 패딩 쪽 추정값이 무패딩 쪽보다 크거나 같다. 이어서 같은 코덱 인스턴스로 여섯 크기를 왕복시킨 결과가 나온다. 2044 와 2045 바이트는 인코드도 디코드도 성공하고 추정식이 2046 을 낸다. 2046·2047·2048 바이트는 인코드는 성공하는데 디코드가 cursor payload exceeds the maximum size 로 거절하고 추정식이 2049 를 낸다. 2049 바이트는 인코드 자체가 거절한다." caption="인코더·디코더 선언과 두 상한 · encode 검사와 decode 사전 추정과 추정식 · 이 코덱을 만드는 자리가 전부 시험이라는 것과 실제 배선된 세 코덱 · 서로 다르게 적는 두 주석 · 경계 값을 쓰는 시험 계수와 추정식에 도달하는 시험 · 같은 식을 두 인코딩에 적용한 표와 여섯 크기 왕복 결과 — 115줄 · exit 0" zoom="true" -::: - -`:126`\~`:128` 의 `decodedLengthOf` 는 `encodedLength / 4 * 3 + 3` 을 돌려준다. - -`L / 4` 는 정수 나눗셈이라 마지막 그룹을 버린다. 그 그룹이 디코딩하는 바이트 수는 남는 문자 수에 달려 있다 — `L % 4` 가 2 면 1 바이트, 3 이면 2 바이트, 0 이면 그룹이 온전하므로 버린 것이 3 바이트다. - -그래서 필요한 여유는 3 이거나 2 이거나 1 이다. 코드는 언제나 3 을 더한다. 나머지가 2 나 3 인 자리에서는 추정값이 실제보다 1 이나 2 크고, 0 인 자리에서만 3 크다. - -## 초과분이 3, 2, 1 로 돈다 - -프로브가 2043 부터 2049 까지를 표로 계산한다. 무패딩 초과분이 3, 2, 1, 3, 2, 1, 3 으로 반복된다. - -상한이 2048 이므로 추정값이 2049 이상이 되는 자리만 막힌다. 그 자리는 초과가 3 인 곳, 즉 `L % 4 == 0` 인 곳이다. - -n = 2046 이 그렇다. 무패딩 길이가 2728 이고 4 로 나누어떨어져서 추정식이 2049 를 낸다. 2047 과 2048 은 길이가 2730 과 2731 인데 `/4*3` 이 같은 2046 을 내므로 +3 이 붙어 역시 2049 가 된다. - -2045 까지는 2046 이 나와 통과한다. - -## 패딩은 원인이 아니다 - -같은 표의 패딩 열이 그것을 보여 준다. 패딩을 붙이면 2044 와 2045 의 길이가 2728 이 되어 추정식이 2049 를 낸다. 지금은 통과하는 두 크기가 막히게 된다. - -n = 2046 은 3 의 배수라 패딩 인코딩과 무패딩 인코딩의 길이가 2728 로 같다. 이 경계를 만든 것은 인코더의 패딩 여부가 아니다. - -패딩 인코더로 바꾸면 막히는 구간이 {2046, 2047, 2048} 에서 {2044 … 2048} 로 넓어진다. - -## 두 검사가 가르는 집합 - -`encode:66` 은 `json.length` 를 `MAX_PAYLOAD_BYTES` 와 직접 견준다. 바이트 수 그대로다. - -`decode:106` 은 `decodedLengthOf(encodedPayloadLength)` 를 같은 상한과 견준다. 추정값이다. - -같은 코덱 인스턴스로 왕복시키니 2044 와 2045 는 양쪽을 통과하고, 2046·2047·2048 은 인코드만 통과하며, 2049 는 인코드가 거절한다. - -`:67`\~`:68` 주석은 인코드가 큰 페이로드를 막는 근거를 적는다. 코덱이 되돌려 줄 수 없는 크기의 토큰을 만들면 다음 페이지에서 실패하므로, 그것은 호출자의 잘못이 아니라 애플리케이션 자신의 결함이라는 것이다. 그 근거가 세 크기에서 그대로 성립하는데 인코드는 통과시킨다. - -## 자바독은 맞고 인라인 주석은 틀리다 - -`:125` 자바독은 이 값을 "at most" 라고 적는다. 보수적 상한이라는 뜻이고 맞는 서술이다. - -`:103`\~`:104` 인라인 주석은 인코딩된 구간 길이가 디코딩 크기를 "exactly" 한정한다고 적는다. 표가 보여 주는 대로 초과분이 1 에서 3 사이로 움직이므로 그렇지 않다. - -설계가 노린 것은 절대 과소평가하지 않는 사전 검사였다. 그 성질은 지켜졌다. 어긋난 것은 정밀도다. - -## 이 코덱은 아직 아무도 쓰지 않는다 - -`new SignedJsonCursorCodec` 이 나오는 여섯 자리가 전부 `SignedJsonCursorCodecTest` 다. 이 코덱을 만드는 자리가 있는 소스 세트는 test 뿐이다. - -`CursorPayloadCodec` 을 구현한다고 선언한 main 파일도 0 개다. 페이로드를 JSON 으로 만들 구현이 없다. - -실제 페이지네이션은 `HmacWebCursorCodec` 과 `HmacGraphQlCursorCodec` 과 `MongoKeysetCursorCodec` 이 맡는다. 그 셋에서 같은 추정식을 쓰는 파일은 0 개다. - -## 시험은 추정식에 닿지만 왕복을 묻지 않는다 - -`SignedJsonCursorCodecTest:166`\~`:172` 는 3000 자짜리 위조 구간을 넣어 토큰 상한 아래·페이로드 상한 위에서 사전 검사가 먼저 터지는 것을 확인한다. 이 시험은 추정식에 실제로 도달한다. - -없는 것은 상한 근처에서 `decode(encode(x))` 를 단언하는 시험이고, 그런 시험이 0 개다. 2046 과 2047 을 쓰는 시험 줄도 각각 0 개다. - -## 원문과 갈리는 자리 - -원문은 2046 부터 2048 까지가 인코드 성공·디코드 거절이라고 적었고 왕복 프로브가 그대로 재현한다. - -원인 설명은 갈린다. 원문은 코덱이 패딩 없는 인코딩을 쓰기 때문에 추정이 어긋난다고 적었다. 표를 계산해 보면 패딩을 붙여도 같은 식이 어긋나고 오히려 막히는 구간이 넓어진다. 어긋나게 만드는 것은 인코딩 방식이 아니라 여유분이 상수라는 점이다. - -원문이 적지 않은 것은 이 코덱이 프로덕션에 배선되지 않았다는 것과, `:125` 자바독이 옳게 적혀 있다는 것이다. - -## 확인하지 못한 것 - -이 공개 타입을 실제로 가져다 쓴 프로젝트가 있는지는 저장소 밖의 일이라 알 수 없다. - -나머지에 맞춰 여유분을 계산하도록 고쳤을 때 무엇이 깨지는지 빌드로 확인하지 않았다. - -`MAX_ENCODED_LENGTH` 검사가 어떤 입력에서 발화하는지는 따져 보지 않았다. - -토큰 전체 길이를 보는 상한이 실제 도달 가능한 값인지 계산하지 않았다. - -## 등급에 대해 - -원본은 P2 다. 프로덕션 배선이 없고 실제 페이지네이션 코덱 셋에 같은 결함이 없으므로 오늘의 사고는 아니다. 다만 이 코덱은 공개 API 표면에 있으므로 채택자가 붙이는 순간 살아난다. 등급을 새로 매기지 않고 그 조건을 적어 둔다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f010.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f010.md deleted file mode 100644 index 5408b6b..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f010.md +++ /dev/null @@ -1,132 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a05-f010 -title: 정본이라던 코디네이터는 그 포트를 구현하지 않고 다른 클래스가 구현한다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a05-f010 -evidenceCapturedOn: 2026-09-04 -body: case-analysis-finding-a05-f010.body.md -assets: - - key: analysis-finding-a05-f010 - file: ../../../final/evidence/rendered/analysis-finding-a05-f010.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a05-f010.txt -source: - - 원본 분석 절은 final/document.md#a05 §10 이다. ---- - -# 정본이라던 코디네이터는 그 포트를 구현하지 않고 다른 클래스가 구현한다 - -`JpaTransactionAutoConfiguration:37` 주석이 정본 경계로 지목한 메서드는 `PolicyTransactionPort` 의 것인데, 같은 문장이 아래에 만들어지는 재시도 코디네이터로 이어진다. 그 포트를 실제로 구현하는 것은 어댑터 쪽 `SpringTransactionPort` 이고 코디네이터는 자기 실행기와 짝을 이룬다. - -## 관계 - -- **중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다** - 이 사례가 속한 구조다. 두 스택이 다 조립되고 어느 쪽이 정본인지가 갈린다. -- **재시도 코디네이터는 빈이지만 그것을 어디에도 적용하지 않는다** - 같은 코디네이터의 배선 공백을 다룬 기록이다. -- **같은 개념의 두 어휘가 공존하면 하나를 죽은 것으로 표시한다** - 이 사례가 어기는 규칙이다. 둘 다 살아 있고 어느 쪽도 죽은 것으로 표시되지 않았다. - -## 문제 - -정본 트랜잭션 경계가 어디인지는 이 리프의 구조를 읽는 출발점이다. - -자동설정 주석이 그 경계를 지목한다. 그 지목이 실제 구현 관계와 맞는지 확인했다. - -## 결론 - -그 포트를 구현한다고 선언한 파일은 하나뿐이다. 없는 이름으로 같은 검색식을 걸어도 0 이 나오므로, 상위 인터페이스로 같은 검색을 걸어 21 을 받아 검색이 도는 것을 확인했다. - -그 클래스가 자기 자바독에서 설명하는 것은 모드별 템플릿과 격리 수준 고정이다. 재시도라는 낱말이 거기 없다. - -자동설정이 만드는 빈은 여섯이다 — CommitFailureClassifier, SpringJpaTransactionExecutor 둘, FullTransactionRetryCoordinator 둘, RetrySleeper. 그 목록에 포트 구현이 없다. - -즉 주석이 한 문장 안에서 두 스택을 잇고 있다. 앞부분이 지목한 포트는 어댑터 쪽 클래스가 구현하고, 뒷부분이 설명하는 코디네이터는 자동설정 쪽 실행기와 짝을 이룬다. - -네 이름의 main 참조 파일 수가 모두 4 개다. 어느 쪽도 죽어 있지 않다. - -저장소 자신도 이것을 문제로 적어 두었다. docs/reviews/2026-08-14-jpa-module-code-review.md:119 가 트랜잭션 권위와 애플리케이션 계약이 두 벌이라는 것을 P1 로 매기고 PolicyTransactionPort 단일 facade 로 통합하자고 적는다. - -동작이 틀리는 것은 아니다. 두 스택이 각각 돈다. 비용은 다음에 고치는 사람이 어느 쪽을 고쳐야 하는지 주석만으로는 알 수 없다는 데 있다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 자동설정 주석 인용, implements PolicyTransactionPort 검색을 없는 이름 자기시험과 대조 이름과 함께 계수, 구현체의 선언과 자바독 인용, 자동설정이 만드는 빈 목록 전수, 네 이름의 main 참조 파일 수 계수와 그중 하나의 파일 목록, 저장소 문서가 같은 것을 어떻게 적었는지 전수 검색 -소스 수정 : x - -## 재현 조건 - -1. 자동설정에서 정본 경계를 지목하는 주석을 인용한다. -2. 그 포트를 구현한다고 선언한 클래스를 찾고, 없는 이름과 대조 이름으로 같은 검색을 함께 건다. -3. 구현체의 선언과 클래스 자바독을 인용한다. -4. 자동설정이 만드는 빈을 전부 나열한다. -5. 두 스택의 이름 넷을 main 참조 파일 수로 센다. -6. 저장소 문서에서 그 포트 이름이 나오는 자리를 전부 찾는다. - -## 본문 - - - -`JpaTransactionAutoConfiguration` 은 JPA 트랜잭션 스택을 조립한다. 그 파일의 주석이 정본 경계가 어디인지 적어 둔다. - -## 주석이 지목한 경계와 실제 구현 관계 - -:::evidence key="analysis-finding-a05-f010" alt="저장소 루트에서 돌린 정적 검색 출력 84줄. 먼저 JpaTransactionAutoConfiguration 28~55번 줄이 실리는데 37번 줄 주석이 PolicyTransactionPort.inTransaction 을 정본 경계로 지목하고 아래의 재시도 코디네이터를 함께 설명한다. 이어서 PolicyTransactionPort 를 구현한다고 선언한 클래스가 SpringTransactionPort 31번 줄 하나로 나오고, 없는 이름으로 건 자기시험이 0 개, 대조로 센 implements TransactionPort 가 21 개다. 그 구현체의 선언과 자바독이 24~40번 줄로 실리는데 모드별로 미리 만든 트랜잭션 템플릿을 쓰고 모두 READ_COMMITTED 로 고정한다고 적는다. 그 아래에 자동설정이 만드는 빈들이 나오는데 CommitFailureClassifier 와 SpringJpaTransactionExecutor 둘과 FullTransactionRetryCoordinator 둘과 RetrySleeper 이고, 기본 최대 재시도 경과 시간이 30 초로 선언돼 있다. 두 스택을 참조하는 main 파일 수는 PolicyTransactionPort 와 FullTransactionRetryCoordinator 와 SpringJpaTransactionExecutor 와 SpringTransactionPort 가 각각 4 개이고, PolicyTransactionPort 를 참조하는 네 파일이 이름으로 나열된다. 마지막으로 문서 쪽 서술이 나오는데 코드 리뷰 문서가 두 벌의 트랜잭션 계약이 공존한다는 것을 P1 로 적고 PolicyTransactionPort 를 단일 facade 로 통합하자고 적는다." caption="자동설정 주석이 지목한 정본 경계 · 그 포트를 구현한 클래스 하나와 자기시험과 대조 · 구현체의 선언과 자바독 · 자동설정이 만드는 빈 목록 · 두 스택의 main 참조 수 · 코드 리뷰 문서가 같은 것을 P1 로 적은 자리 — 84줄 · exit 0" zoom="true" -::: - -`:37` 주석은 `PolicyTransactionPort.inTransaction(TransactionRequest, Supplier)` 를 지목하고, 같은 문장이 아래의 재시도 코디네이터를 함께 설명한다. - -## 그 포트를 구현한 클래스는 하나다 - -`implements PolicyTransactionPort` 를 가진 파일은 `SpringTransactionPort:31` 하나다. 없는 이름으로 같은 검색식을 걸면 0 이 나오므로, 그 1 이 검색 운은 아닌지 확인하려고 `implements TransactionPort` 를 세면 21 이 나온다. - -`SpringTransactionPort` 는 `adapter/outbound/persistence-jpa` 에 있고 `@Component` 다. 자바독 `:26`\~`:28` 은 모드별로 미리 만든 `TransactionTemplate` 을 쓰고 모두 `READ_COMMITTED` 로 고정한다고 적는다. 미리 만드는 이유는 가변 템플릿 경합 때문이라고 README 를 가리킨다. - -재시도는 그 자바독 어디에도 없다. - -## 자동설정이 만드는 것은 다른 스택이다 - -빈 목록은 여섯이다. `:61` 의 `CommitFailureClassifier`, `:66` 과 `:82` 의 `SpringJpaTransactionExecutor`, `:99` 와 `:107` 의 `FullTransactionRetryCoordinator`, `:114` 의 `RetrySleeper` 다. - -포트 구현을 만드는 빈이 없다. `SpringTransactionPort` 는 `@Component` 로 스캔되어 들어온다. - -그래서 `:37` 주석은 한 문장 안에서 두 계통을 잇는다. 앞이 가리키는 포트는 어댑터 쪽이 구현하고, 뒤가 설명하는 코디네이터는 같은 파일이 만드는 실행기와 짝이다. - -## 두 스택 다 살아 있다 - -`PolicyTransactionPort` 와 `FullTransactionRetryCoordinator` 와 `SpringJpaTransactionExecutor` 와 `SpringTransactionPort` 의 main 참조 파일 수가 모두 4 개다. - -`PolicyTransactionPort` 를 참조하는 넷은 그 인터페이스 자신과 `SpringPolicyTransactionPort` 와 `SpringTransactionPort` 와 `JpaTransactionAutoConfiguration` 이다. - -어느 쪽에도 폐기 표시가 없다. - -## 저장소 자신이 이미 적어 둔 것 - -`docs/reviews/2026-08-14-jpa-module-code-review.md:31` 은 애플리케이션이 이미 쓰는 포트와 새 JPA 실행기·AOP 계약이 두 벌로 존재한다고 적는다. - -`:119` 는 그것을 `JPA-007` 로 번호 매겨 P1 · High 로 두고 `PolicyTransactionPort` 단일 facade 로 통합하자고 적는다. `:427` 은 템플릿의 정본 경계를 그 포트의 `inTransaction` 으로 삼자고 적는다. - -즉 이 상태는 발견되지 않은 것이 아니라 결정이 내려지지 않은 것이다. - -## 원문과 갈리는 자리 - -원문은 문서와 소스 주석이 코디네이터가 포트를 구현한다고 설명하는데 실제 구현체는 다른 클래스라고 적었다. 실제 주석은 그보다 조금 다르다. `:37` 은 코디네이터가 그 포트를 구현한다고 적지 않고, 정본 경계로 포트를 지목한 뒤 아래 코디네이터를 이어서 설명한다. 두 계통이 한 문장에 붙어 있어서 읽는 사람이 그렇게 이해하게 된다. - -원문이 적지 않은 것은 저장소가 이미 이 중복을 `JPA-007` 로 기록하고 통합 방향까지 정해 두었다는 것이다. - -## 확인하지 못한 것 - -실제 요청이 두 계통 중 어느 쪽을 더 자주 지나는지 세지 않았다. - -그 문장이 쓰였을 때는 맞았는지 커밋을 되짚지 않았다. - -`JPA-007` 이 어느 쪽으로 결정됐는지 추적하지 않았다. - -`JPA-007` 이 어느 쪽으로 결정됐는지 추적하지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f023.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f023.md deleted file mode 100644 index 76f9b2f..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f023.md +++ /dev/null @@ -1,130 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a05-f023 -title: 쿼터 원장은 사용량을 적기만 하고 승인은 그것을 읽지 않는다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a05-f023 -evidenceCapturedOn: 2026-09-04 -body: case-analysis-finding-a05-f023.body.md -assets: - - key: analysis-finding-a05-f023 - file: ../../../final/evidence/rendered/analysis-finding-a05-f023.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a05-f023.txt -source: - - 원본 분석 절은 final/document.md#a05 §23 이다. ---- - -# 쿼터 원장은 사용량을 적기만 하고 승인은 그것을 읽지 않는다 - -`DefaultTransferAdmissionController.acquireUpload:45`~`:62` 가 확인하는 것은 파일 크기와 저장소 고수위와 세마포어 둘이다. `JpaFileQuotaService:84` 와 `:89` 가 범위별 예약·확정 바이트를 계산하지만 그 둘을 부르는 main 코드가 정의 파일 밖에 0 줄이다. - -## 관계 - -- **선언은 통과하는데 강제하는 주체가 없음 계열** - 이 사례가 속한 구조다. 값을 계산하는 코드는 있고 그것으로 무언가를 거절하는 코드가 없다. -- **tenant별 풀이 개별적으로 합리적이고 그 합이 서버 상한을 넘긴다** - 같은 계열의 자원 격리 문제다. -- **회수가 페이지 하나를 다 쓰면 남은 바이트를 들고 그대로 끝난다** - 같은 원장의 다른 결함이다. 그쪽은 기록된 값이 실제보다 커지는 것이다. - -## 문제 - -파일서버 설계 문서는 쿼터를 범위별 바이트 강제로 정의한다. 승인 컨트롤러도 클래스 설명에서 같은 어휘를 쓴다. - -업로드 요청이 그 상한을 실제로 지나는지 확인했다. - -## 결론 - -acquireUpload 는 네 가지를 지난다. :46 이 단일 파일 크기 정책, :47 이 저장소 하드 고수위, :49 가 범위별 업로드 세마포어, :54 가 인스턴스 업로드 세마포어다. - -넷 중 어느 것도 바이트 집계가 아니다. 인자로 받은 바이트 수는 :46 한 곳에서만 쓰이고, 그 범위에 이미 쌓인 양은 아무 검사도 조회하지 않는다. - -JpaFileQuotaService.reserve:38~:52 도 마찬가지다. 음수만 거른 뒤 예약 엔티티를 만들어 조건 없이 저장한다. 상한 조회도 집계도 없다. - -집계를 계산하는 코드는 있다. :84 의 reservedBytes 와 :89 의 committedBytes 가 각각 합계 질의를 부른다. 두 메서드가 불리는 자리를 저장소 전역에서 훑으면 정의한 파일 자신 말고는 통합 시험과 가짜 구현뿐이다. 검색이 도는지 보려고 acquireUpload 를 같은 방식으로 세면 main 2 줄이 나온다. - -설정 쪽에도 없다. 승인 컨트롤러가 읽는 프로퍼티 다섯은 허가 수 셋과 고수위 둘이고, 테넌트나 네임스페이스의 용량을 담은 값이 없다. - -QuotaExceededException 이 던져지는 자리는 :50 하나인데, 그 이유는 바이트 초과가 아니라 범위 업로드 동시성 소진이다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 설계 문서의 쿼터 서술과 승인 컨트롤러 클래스 자바독 인용, acquireUpload 본문 전문 인용, 예약 저장 자리 인용, 범위별 바이트 집계를 읽는 자리 전수와 정의 파일 밖 main 호출자 계수를 대조 이름과 함께 확인, 설정의 fileserver 절 인용, 승인 컨트롤러가 읽는 프로퍼티 전수 -소스 수정 : x - -## 재현 조건 - -1. 설계 문서에서 쿼터를 바이트 강제로 정의한 줄과 승인 컨트롤러의 클래스 자바독을 인용한다. -2. acquireUpload 본문을 전문으로 싣고 무엇을 검사하는지 센다. -3. 예약을 저장하는 자리를 인용하고 조건절이 있는지 본다. -4. 범위별 예약·확정 바이트를 읽는 자리를 저장소 전역에서 찾고, 정의 파일을 뺀 main 호출자를 센다. -5. 같은 방식으로 대조 이름을 세어 검색이 도는지 확인한다. -6. 설정의 fileserver 절을 싣고 승인 컨트롤러가 읽는 프로퍼티를 전부 나열한다. - -## 본문 - - - -파일서버 설계 문서는 쿼터를 단순 회계가 아니라 범위별 바이트 강제로 설명한다. `DefaultTransferAdmissionController` 의 클래스 자바독도 범위가 상한을 넘는 것을 쿼터 초과라고 부른다. - -## acquireUpload 가 실제로 지나는 네 검사 - -:::evidence key="analysis-finding-a05-f023" alt="저장소 루트에서 돌린 정적 검색 출력 132줄. 먼저 docs/fileserver/design-deviations.md 에서 쿼터를 다루는 줄들이 실리고, DefaultTransferAdmissionController 1~30번 줄의 클래스 자바독이 나온다. 이어서 44~62번 줄의 acquireUpload 본문이 실리는데, 파일 크기 정책과 고수위를 확인한 뒤 범위 세마포어와 인스턴스 세마포어를 얻고 둘 다 얻으면 허가를 돌려준다. 바이트 집계를 읽는 줄이 없다. 그다음 JpaFileQuotaService 36~53번 줄의 reserve 가 나오는데 음수만 거른 뒤 예약 엔티티를 만들어 조건 없이 저장한다. 범위별 바이트 집계를 읽는 자리를 저장소 전역에서 찾으면 JpaFileQuotaService 84번과 89번 줄의 정의 둘과 postgresqlIntegrationTest 와 시험용 가짜 구현들이 나오고, 정의 파일 밖의 main 호출자는 0 줄이다. 대조로 센 acquireUpload 를 부르는 main 줄은 2 줄이다. 그 아래에 application.yml 810~832번 줄의 fileserver 설정이 실리는데 목적지의 최대 행 수와 최대 인코딩 바이트, 제공자의 루트 디렉터리와 마운트 검사만 있고 범위별 바이트 상한이 없다. 범위별 바이트 상한 이름을 fileserver 안에서 찾은 결과가 나오고, 마지막으로 승인 컨트롤러가 읽는 프로퍼티 다섯이 나오는데 인스턴스 업로드 허가 수, 직접 다운로드 허가 수, 소프트 고수위, 하드 고수위, 범위 업로드 허가 수다." caption="설계 문서의 쿼터 정의와 승인 컨트롤러의 자바독 · acquireUpload 가 실제로 검사하는 넷 · 예약이 조건 없이 저장되는 자리 · 바이트 집계를 읽는 main 호출자 0 과 대조 · 설정의 fileserver 절과 승인이 읽는 프로퍼티 다섯 — 132줄 · exit 0" zoom="true" -::: - -`:46` 이 단일 파일 크기 정책을 확인하고 `:47` 이 저장소 하드 고수위를 확인한다. - -`:48`\~`:53` 이 범위별 업로드 세마포어를 얻는다. 실패하면 `:50` 이 `QuotaExceededException` 을 던지는데 메시지는 `scope upload concurrency is exhausted` 다. 동시성이지 바이트가 아니다. - -`:54`\~`:59` 가 인스턴스 업로드 세마포어를 얻는다. 실패하면 다른 예외다. - -`requestedBytes` 는 `:46` 에만 쓰인다. 그 범위가 이미 얼마를 예약하고 확정했는지는 어느 검사도 묻지 않는다. - -## 예약은 조건 없이 저장된다 - -`JpaFileQuotaService.reserve:38` 은 음수 바이트만 거른다. - -`:43`\~`:51` 이 예약 엔티티를 만들고 `:52` 가 `reservations.save(entity)` 를 부른다. 조회도 조건절도 없다. - -## 집계는 계산되지만 아무도 읽지 않는다 - -`:84` 의 `reservedBytes` 가 만료되지 않은 예약 바이트 합계를 부르고 `:89` 의 `committedBytes` 가 확정 바이트 합계를 부른다. - -그 둘을 부르는 자리를 저장소 전역에서 찾으면 정의 파일 밖의 main 호출자가 0 줄이다. 나머지는 `PostgreSqlFileserverMetadataStoreIntegrationTest` 와 `PostgreSqlFileserverReclamationIntegrationTest` 의 단언, 그리고 `FakeFileQuotaService` 같은 시험용 구현이다. - -같은 방식으로 `acquireUpload` 호출을 세면 main 2 줄이 나온다. 검색이 main 을 보고 있다. - -즉 통합 시험은 그 합계가 맞는지 확인한다. 프로덕션 코드는 그 합계를 근거로 무엇도 거절하지 않는다. - -## 설정에도 그 상한이 없다 - -`application.yml:810`\~`:832` 의 fileserver 절에는 목적지의 최대 행 수와 최대 인코딩 바이트, 제공자의 루트 디렉터리와 마운트 검사가 있다. 범위별 바이트 상한이 없다. - -승인 컨트롤러가 읽는 프로퍼티는 다섯이다 — `:40` 인스턴스 업로드 허가 수, `:41` 직접 다운로드 허가 수, `:80` 소프트 고수위, `:93` 하드 고수위, `:102` 범위 업로드 허가 수. - -앞의 둘과 마지막은 동시성이고 가운데 둘은 디스크 사용률이다. 어느 것도 테넌트나 네임스페이스의 용량이 아니다. - -## 원문과 갈리는 자리 - -원문은 프로덕션 호출 그래프에 그 상한이 없다고 적었고 그대로다. - -원문이 적지 않은 것은 `QuotaExceededException` 이 실제로 던져지는 자리다. 그 예외는 `:50` 에서 한 번 던져지는데 사유가 동시성 소진이다. 로그나 대시보드에서 이 예외를 바이트 초과로 읽으면 실제와 다르다. - -## 확인하지 못한 것 - -한 범위가 실제로 큰 바이트를 쌓을 수 있는지 업로드를 돌려 확인하지 않았다. - -이 상한을 애초에 넣었다가 뺀 이력이 있는지 추적하지 않았다. - -디스크 사용률 상한이 범위별 상한의 대체가 되는지는 판단하지 않았다. - -## 등급에 대해 - -원본은 이 항목을 P1 로 매겼다. 원장이 쓰이지 않는다는 사실은 이 리비전에서도 그대로이므로 등급을 새로 매기지 않는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f024.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f024.md deleted file mode 100644 index af5e89d..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f024.md +++ /dev/null @@ -1,139 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a05-f024 -title: 회수가 페이지 하나를 다 쓰면 남은 바이트를 들고 그대로 끝난다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a05-f024 -evidenceCapturedOn: 2026-09-04 -body: case-analysis-finding-a05-f024.body.md -assets: - - key: analysis-finding-a05-f024 - file: ../../../final/evidence/rendered/analysis-finding-a05-f024.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a05-f024.txt -source: - - 원본 분석 절은 final/document.md#a05 §24 이다. ---- - -# 회수가 페이지 하나를 다 쓰면 남은 바이트를 들고 그대로 끝난다 - -`JpaQuotaReclaimGateway:53`~`:54` 는 확정 행을 `Limit.of(64)` 로 한 번 조회하고 그 목록만 순회한다. 프로브에서 1 바이트 행 200 개에 200 바이트 회수를 요청했더니 차감이 64 회에 그쳤고 나머지 136 은 어디에도 보고되지 않았다. - -## 관계 - -- **쿼터 원장은 사용량을 적기만 하고 승인은 그것을 읽지 않는다** - 같은 원장의 다른 결함이다. 그쪽은 기록된 값을 아무도 읽지 않는 것이고, 여기는 그 값이 실제보다 커지는 것이다. -- **빠뜨림이 통과가 되는 게이트는 게이트가 아니다** - 같은 계열이다. 남은 작업이 있는데 아무 신호 없이 정상 종료한다. -- **모르는 것은 성공도 실패도 아닌 세 번째 결과여야 한다** - 회수 연산이 결과를 돌려주지 않으므로 다 했는지 못 했는지가 어디에도 표현되지 않는다. - -## 문제 - -물리 삭제가 끝나면 정리 서비스가 회수 게이트웨이를 부른다. 그 게이트웨이가 원장에서 확정 사용량을 줄인다. - -요청한 바이트가 여러 행에 걸쳐 있을 때 어떻게 되는지 확인했다. - -## 결론 - -:33 이 페이지 상한을 64 로 둔다. :55~:61 이 그 한 페이지를 순회하며 차감하고, 마지막 원소를 지나면 루프가 그대로 빠져나온다. 재조회도 남은 값 반환도 로그도 없다. - -저장소 클래스를 그대로 쓰고 리포지터리만 프록시로 대신해 돌렸다. 원장 행이 64 개일 때는 요청한 만큼 전부 회수된다. 65 개일 때 1 이 남고, 200 개일 때 136 이 남는다. 남는 양은 페이지를 넘는 행 수만큼 늘어난다. - -클래스 자바독 :25~:27 은 나머지를 버리는 이유를 적어 두었다. 원장에 흡수할 데가 없을 때의 이야기이고, 그런 상황은 총량이 이미 잘못 적혔다는 신호라는 것이다. - -프로브가 만든 나머지는 종류가 다르다. 원장에는 아직 뺄 데가 넉넉한데 한 페이지만 보고 끝냈기 때문이다. 자바독이 허용한 나머지와 상한이 만든 나머지를 코드가 구분하지 않는다. - -이 메서드는 아무것도 돌려주지 않는다. 정리 서비스가 회수 결과를 물을 자리가 애초에 없다. - -이 경계를 겨냥한 시험도 없다. 페이지 상한과 그 바로 위아래 값을 쓰는 시험 줄이 모두 0 개다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Gradle : 9.0.0 -확인 방식 : 게이트웨이 전문 게재와 클래스 자바독 인용, 페이지 질의와 차감 질의의 정의와 그 자바독 인용, 회수 호출자 전수, 저장소 게이트웨이를 그대로 쓰고 리포지터리만 프록시로 대신해 원장 행 수와 요청 바이트를 바꿔 가며 페이지 질의 횟수와 차감 횟수와 남은 바이트를 관측, 경계 값을 쓰는 시험 줄 계수와 이름에 회수가 든 파일 나열 -소스 수정 : x - -## 재현 조건 - -1. 게이트웨이를 전문으로 싣고 클래스 자바독을 함께 읽는다. -2. 페이지 질의와 차감 질의의 정의를 인용한다. -3. 회수를 부르는 자리를 전부 찾는다. -4. 리포지터리 인터페이스를 프록시로 바꿔 끼우고, 질의가 오면 원장에 넣어 둔 행을 상한까지 잘라 돌려주게 한다. -5. 원장 행 수와 요청 바이트를 64·65·200 으로 바꿔 가며 게이트웨이를 부르고 페이지 질의 횟수와 차감 횟수와 남은 바이트를 적는다. -6. 64 와 65 와 페이지 상한 상수를 쓰는 시험 줄을 센다. - -## 본문 - - - -`JpaQuotaReclaimGateway` 는 물리 삭제가 끝난 뒤 원장의 확정 사용량을 줄인다. 정리 서비스가 삭제한 바이트 수를 넘겨 부른다. - -## 게이트웨이는 확정 행을 한 번만 조회한다 - -:::evidence key="analysis-finding-a05-f024" alt="저장소 루트에서 돌린 정적 검색과 프로브의 출력 129줄. 먼저 JpaQuotaReclaimGateway 64줄이 전문으로 실린다. 25~27번 줄 클래스 자바독은 어떤 행도 흡수하지 못하는 나머지는 이월하지 않고 버린다고 적으면서, 원장의 바닥이 0 이며 기록된 총량을 넘어서는 회수는 총량이 이미 과소 기록됐다는 뜻이라고 덧붙인다. 33번 줄이 RECLAIM_PAGE 를 Limit.of(64) 로 두고, 44~63번 줄의 reclaim 이 51번에서 시각을 얻고 52번에서 outstanding 을 요청 바이트로 두며 53~54번에서 확정 행을 한 번 조회해 그 결과를 순회한다. 55~57번은 outstanding 이 0 이면 돌아가고, 58번이 차감량을 정하고 59~61번이 갱신 건수가 1 일 때만 뺀다. 루프가 끝나면 62~63번에서 메서드가 그대로 끝난다. 이어서 그 한 번의 질의가 무엇을 돌려주는지 리포지터리 106~117번 줄로 나오는데, 확정 상태이고 바이트가 남은 행을 갱신 시각 내림차순으로 상한만큼 가져온다. 119~139번 줄의 차감 질의는 자바독에서 그 가드가 동시 회수를 안전하게 만든다고 적는다. 회수를 부르는 자리로 정리 서비스 한 곳과 시험 몇 곳이 나온다. 그다음 프로브가 실린다. 리포지터리만 프록시로 대신하고 게이트웨이는 저장소 클래스를 그대로 쓴다. 1바이트 확정 행 64 개에 64 바이트를 요청하면 페이지 질의 1 회에 차감 64 회로 남는 바이트가 0 이다. 65 개에 65 바이트를 요청하면 페이지 질의는 그대로 1 회이고 차감이 64 회에서 멈춰 1 바이트가 남는다. 200 개에 200 바이트를 요청하면 차감이 64 회이고 136 바이트가 남는다. 마지막으로 그 경계를 짚는 시험을 세면 64 와 65 와 RECLAIM_PAGE 를 쓰는 시험 줄이 각각 0 개이고, 이름에 Reclaim 이 든 파일은 게이트웨이와 포트와 시험용 기록기 셋뿐이다." caption="게이트웨이 64줄 전문과 그 클래스 자바독 · 한 번의 페이지 질의가 돌려주는 것과 차감 질의의 가드 · 회수 호출자 · 저장소 게이트웨이를 그대로 부른 세 경우 · 그 경계를 짚는 시험의 부재 — 129줄 · exit 0" zoom="true" -::: - -`:33` 이 `RECLAIM_PAGE` 를 `Limit.of(64)` 로 둔다. - -`reclaim:44` 은 음수와 0 을 먼저 걸러 낸다. `:52` 가 `outstanding` 을 요청 바이트로 두고, `:53`\~`:54` 가 그 상한으로 확정 행을 조회해 향상된 for 문으로 순회한다. - -루프 안에서 `:55` 가 `outstanding` 이 0 이면 돌아가고, `:58` 이 차감량을 `Math.min` 으로 정하고, `:59` 가 갱신 건수가 1 일 때만 `:60` 에서 뺀다. - -목록이 끝나면 `:62`\~`:63` 에서 메서드가 끝난다. `outstanding` 이 얼마든 상관없다. - -## 그 한 번의 질의가 무엇을 돌려주는가 - -`FileserverQuotaRepository:107`\~`:117` 은 확정 상태이고 확정 바이트가 남은 행을 갱신 시각 내림차순으로 상한만큼 가져온다. - -`:126`\~`:139` 의 차감 질의는 조건부 갱신이다. `:122`\~`:124` 자바독은 그 가드가 동시 회수를 안전하게 만든다고 적는다 — 다른 회수가 이미 낮춰 놓은 행은 0 행을 갱신하고 호출자가 다음 행으로 넘어간다는 것이다. - -그 설계는 한 페이지 안에서만 성립한다. 다음 행이 페이지 밖에 있으면 넘어갈 곳이 없다. - -## 저장소 게이트웨이를 그대로 부른 결과 - -리포지터리 인터페이스만 프록시로 대신했다. 프록시는 요청한 만큼의 1 바이트 확정 행을 만들어 페이지 상한까지 돌려주고 차감 호출을 센다. 게이트웨이는 저장소 클래스 그대로다. - -1 바이트 행 64 개에 64 바이트를 요청하면 페이지 질의 1 회에 차감 64 회이고 남는 바이트가 0 이다. - -65 개에 65 바이트를 요청하면 페이지 질의는 여전히 1 회이고 차감이 64 회에서 멈춘다. 1 바이트가 남는다. - -200 개에 200 바이트를 요청하면 차감이 64 회이고 136 바이트가 남는다. 남는 양은 원장에 쌓인 행 수에 따라 늘어난다. - -## 자바독이 정당화한 나머지는 다른 나머지다 - -`:25`\~`:27` 은 나머지를 이월하지 않는 근거를 적는다. 어떤 행도 흡수하지 못하는 나머지라면 원장의 총량이 이미 과소 기록됐다는 뜻이고, 음수 잔액이 그것을 고쳐 주지는 않는다는 것이다. - -프로브가 만든 나머지는 그 경우가 아니다. 흡수할 행이 원장에 그대로 남아 있는데 페이지 상한이 그것을 보지 못하게 했다. - -코드는 두 나머지를 구분하지 않는다. 둘 다 같은 자리에서 조용히 끝난다. - -## 남았다는 사실을 담을 자리가 없다 - -`reclaim` 의 반환형은 `void` 다. 호출자는 요청한 바이트가 전부 회수됐는지 알 방법이 없다. - -64 와 65 와 `RECLAIM_PAGE` 를 쓰는 시험 줄은 각각 0 개다. 이름에 회수가 든 파일은 게이트웨이와 포트와 시험용 기록기 셋이다. - -## 원문과 갈리는 자리 - -원문은 65 개 행에 65 바이트를 요청하면 1 이 남는다고 적었고 프로브가 그대로 재현한다. 원문은 실제 PostgreSQL 로 확인했고 이 기록은 리포지터리를 프록시로 대신했다. - -원문이 적지 않은 것이 둘이다. 남는 양이 원장 행 수에 비례해 커진다는 것과, 클래스 자바독이 정당화한 나머지가 페이지 상한이 만드는 나머지와 다른 것이라는 점이다. - -## 확인하지 못한 것 - -데이터베이스를 띄워 확인하지는 않았다. 프록시가 돌려준 목록으로 루프의 종료 조건만 봤다. - -운영 중인 원장에서 한 범위가 64 행을 넘기는 일이 있는지 세지 않았다. - -차감 질의가 0 행을 갱신해 건너뛰는 경우와 페이지 상한이 겹치면 남는 바이트가 더 늘어나는지 시험하지 않았다. - -실제 PostgreSQL 로 돌리지 않았다. 리포지터리를 프록시로 대신해 게이트웨이의 루프만 관측했다. - -차감 질의가 0 행을 갱신해 건너뛰는 경우와 페이지 상한이 겹치면 남는 바이트가 더 늘어나는지 시험하지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f028.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f028.md deleted file mode 100644 index e5b839c..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f028.md +++ /dev/null @@ -1,151 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a05-f028 -title: 제공자 제출 뒤의 다섯 쓰기 가운데 결과 기록만 조건이 없다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a05-f028 -evidenceCapturedOn: 2026-09-04 -body: case-analysis-finding-a05-f028.body.md -assets: - - key: analysis-finding-a05-f028 - file: ../../../final/evidence/rendered/analysis-finding-a05-f028.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a05-f028.txt -source: - - 원본 분석 절은 final/document.md#a05 §89 이다. ---- - -# 제공자 제출 뒤의 다섯 쓰기 가운데 결과 기록만 조건이 없다 - -제공자 제출은 `NotificationDispatchService:169` 이고 그 뒤에 남는 쓰기는 다섯이다. `applyNextAction` 의 네 분기는 리스를 `where` 절에 넣는 연산을 쓰고, `DispatchOutcomeRecorder:135` 만 조건 없는 `save` 를 쓴다. 그 클래스는 리스를 받지도 않는다. - -## 관계 - -- **fenced lease — 만료 시각만으로는 부족한 이유** - 그 개념이 정의하는 펜스 값이 여기서 `where` 절의 세 번째 조건이다. `RecipientDeliveryJpaRepository:80` 과 `:105` 가 `id` 와 `lease_owner` 와 `lease_fence` 를 함께 요구한다. -- **native claim이 @Version을 올리지 않아 충돌을 보고하지 않는 낙관적 잠금이 됐다** - 그 기록의 네이티브 청구문과 이 기록의 포트 자바독이 같은 이유를 든다. `version` 이 잡는 것은 동시 편집이지 밀려난 작업자가 아니다. -- **조건부 update로 행을 claim하고 읽은 값으로 판단하지 않는다** - 펜싱하는 쪽이 지키는 규칙이다. `RecipientDeliveryJpaRepository:82` 의 `saveProjectionHeldBy` 가 소유자와 펜스를 `where` 에 넣고 `int` 갱신 건수를 돌려주며, 포트의 `saveHeldBy` 는 그 건수를 `Optional` 로 옮긴다. - -## 문제 - -리스를 붙든 작업자가 제공자에 제출하는 동안 리스가 만료되면, 그 작업은 다른 작업자에게 넘어간다. 밀려난 작업자가 돌아와 쓰는 것을 막는 것이 펜싱이다. - -제출 뒤에 남은 쓰기가 그것을 쓰는지 확인했다. - -## 결론 - -포트는 다섯 쓰기를 선언한다. save 와 transition 과 saveHeldBy 와 transitionHeldBy 와 renewLease 다. 뒤의 셋만 RecipientLease 를 인자로 받고 Optional 을 돌려준다. - -구현은 그 자바독대로다. JpaRecipientDeliveryStore:69 의 saveHeldBy 가 RecipientDeliveryJpaRepository:82 의 saveProjectionHeldBy 를 부르고, 그 갱신문의 where 절 :80 이 id 와 lease_owner 와 lease_fence 를 모두 요구한다. - -stillHeld 검사는 :161 이라 제출 앞이다. 제출 뒤의 쓰기는 두 갈래로 나뉜다. - -한쪽은 :176 이 부르는 applyNextAction 이다. 그 안의 switch 는 분기가 넷인데 :422·:428·:434·:436 이 모두 펜싱 연산이라, 어느 쪽으로 가도 리스가 조건에 들어간다. - -다른 쪽은 :173 이 부르는 DispatchOutcomeRecorder.record 이고 별도 파일에 있다. 그 서명에 RecipientLease 가 없으며 :135 가 recipients.save(updated) 다. - -그 save 가 덮는 것은 소유권이 아니다. JpaRecipientDeliveryStore:39 가 넘기는 값이 전부 투영 필드라, delivery_state 부터 next_dispatch_at 까지 열 개 컬럼이 밀려난 작업자의 값으로 바뀐다. - -펜싱 갱신문 자체도 절반이다. :80 과 :105 는 소유자와 펜스만 보고 lease_until 을 보지 않는다. renewLease:134 와 :154 는 본다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 포트가 선언한 다섯 쓰기와 saveHeldBy 자바독 인용, 펜싱 갱신문과 세 where 절 대조, 조건 없는 save 가 거치는 구현과 applyProjection 인자 전수, 결과 쓰기 자리 인용, 연산별 호출자 전수, stillHeld 검사와 제출과 기록의 순서 인용, applyNextAction 자바독과 switch 네 분기 인용, 펜싱 연산이 나오는 시험 자리 전수 -소스 수정 : x - -## 재현 조건 - -1. 포트가 선언한 쓰기를 전부 뽑고 saveHeldBy 자바독을 인용한다. -2. 펜싱 갱신문을 인용하고 세 갱신문의 where 절을 나란히 놓는다. -3. 조건 없는 save 가 거치는 구현과 그것이 부르는 applyProjection 의 인자를 전부 나열한다. -4. 결과를 쓰는 메서드를 인용한다. -5. 다섯 연산 각각의 호출자를 저장소 전체에서 찾는다. -6. stillHeld 검사와 제출과 기록의 순서를 인용하고, applyNextAction 자바독과 switch 를 함께 싣는다. -7. saveHeldBy 와 transitionHeldBy 가 나오는 시험 파일과 줄을 전부 나열한다. - -## 본문 - - - -`RecipientDeliveryStorePort` 는 배달 행을 쓰는 다섯 연산을 선언한다. 그중 셋만 리스를 인자로 받는다. - -## 포트가 이 사고를 자기 자바독에 적어 두었다 - -:::evidence key="analysis-finding-a05-f028" alt="저장소 루트에서 돌린 정적 검색 출력 250줄. 먼저 RecipientDeliveryStorePort 가 선언한 다섯 쓰기가 나오고 21~36번 줄의 saveHeldBy 자바독이 실린다. 그 자바독은 청구도 갱신도 펜싱되는데 완료만 한동안 그렇지 않았다고 적고, 제공자 호출 중에 리스가 만료된 작업자가 돌아와 조건 없는 save 로 새 소유자가 이미 청구한 행 위에 썼다고 적으며, 낙관적 version 컬럼은 동시 편집을 잡지 밀려난 작업자를 잡지 못한다고 적는다. 이어서 RecipientDeliveryJpaRepository 56~82번 줄이 실리는데 56~65번 자바독이 이 갱신문이 제공자 호출 뒤의 쓰기를 위한 것이며 제출 이전의 작업은 새 소유자가 다시 하면 그만이지만 결과는 그렇지 않다고 적고, 80번 where 절이 id 와 lease_owner 와 lease_fence 를 모두 요구한다. 그다음 세 갱신문의 where 절이 나란히 실린다. saveProjectionHeldBy 의 80번과 transitionHeldBy 의 105번은 세 조건뿐이고, renewLease 의 127~134번과 153~154번에만 lease_until 이 now 보다 크다는 조건이 붙는다. 이어서 조건 없는 save 가 거치는 구현이 나온다. JpaRecipientDeliveryStore 32~52번이 findById 로 엔티티를 읽어 applyProjection 을 부르고 saveAndFlush 하는데, RecipientDeliveryEntity 158~182번의 applyProjection 이 받는 인자 열하나는 deliveryState 와 submissionOutcome 과 deliveryOutcome 과 evidenceLevel 과 두 불린과 routeCursor 와 attemptCount 와 lastFailureCategory 와 nextDispatchAt 과 시각이고 리스 필드가 하나도 없다. 그 아래에 DispatchOutcomeRecorder 14~19번 클래스 자바독과 108~136번 결과 쓰기가 나오는데 109~134번이 새 RecipientDeliveryRecord 를 만들고 135번이 recipients.save 를 부른다. 다음으로 같은 흐름의 쓰기가 연산별로 나열된다. save 는 DispatchOutcomeRecorder 135번 하나이고, transition 은 NotificationAdminApplicationService 121번과 NotificationDispatchService 139번·396번과 NotificationSubmissionService 305번과 ReconciliationService 108번 다섯이며, saveHeldBy 는 NotificationDispatchService 434번, transitionHeldBy 는 같은 파일 422번·428번·436번이다. 그다음 NotificationDispatchService 158~178번 줄이 실려 161번이 leases.stillHeld 를 확인하고 162~164번 주석이 그 검사가 부수 효과 직전이며 그것을 막을 수 있는 마지막 순간이라고 적으며, 168~169번이 트랜잭션 밖에서 제출하고 171~173번이 그 뒤에 레코더를 부르고 176번이 applyNextAction 을 부른다. 이어서 398~412번 줄이 실리는데 401~411번 자바독이 applyNextAction 의 모든 쓰기가 제공자 호출 뒤에 일어나고 그것이 플랫폼이 일부러 트랜잭션 밖에서 시간을 쓰는 유일한 구간이며 그 사이에 리스가 만료돼 다른 작업자가 일을 가져갈 수 있고 펜싱된 변형은 그런 쓰기가 아무것도 맞지 않게 만든다고 적고, 리스를 잃은 것은 보고할 오류가 아니라고도 적는다. 그 아래 417~445번이 그 메서드의 switch 인데 RetryAfter 와 Reconcile 과 Stop 분기가 transitionHeldBy 를, Fallback 분기가 saveHeldBy 를 부르고 마지막에 refreshStatus 가 분기와 무관하게 돈다. 마지막으로 파일명에 Fenc 나 Lease 가 든 시험 파일이 열한 개, DispatchOutcomeRecorder 를 이름에 가진 시험 파일이 0 개라고 나오고, 펜싱 연산 이름이 나오는 시험 자리 여섯이 실린다. LeaseRecoveryServiceTest 306·313번과 PlatformFakes 263·272번은 시험 대역이 그 메서드를 구현하는 선언이고, 실제로 부르는 것은 postgresqlIntegrationTest 소스 세트의 PostgreSqlRecipientLeaseFencingIntegrationTest 93번과 127번이다." caption="포트의 다섯 쓰기와 saveHeldBy 자바독 · 펜싱 갱신문과 세 where 절 대조 · 조건 없는 save 가 덮는 투영 컬럼 · 결과 쓰기 자리 · 연산별 호출자 · stillHeld 검사와 제출과 기록의 순서 · applyNextAction 자바독과 switch 네 분기 · 펜싱 연산이 나오는 시험 자리 — 250줄 · exit 0" zoom="true" -::: - -`:21`\~`:36` 자바독에는 청구와 갱신은 펜싱되는데 완료만 한동안 그렇지 않았다고 적혀 있다. 제공자 호출 중에 리스가 만료된 작업자가 돌아와 조건 없는 `save` 로, 새 소유자가 이미 청구하고 어쩌면 이미 발송한 행 위에 썼다는 것도 같은 자리에 있다. - -낙관적 `version` 컬럼은 동시 편집을 잡지, 밀려난 작업자를 잡지 못한다. 늦은 작업자의 읽기는 이기기에 충분할 만큼 최근이었다. - -`saveHeldBy` 와 `transitionHeldBy` 는 둘 다 `Optional` 을 돌려준다. 리스가 밀려났으면 아무것도 쓰지 않은 채 빈 `Optional` 이 나온다. - -## 펜싱 갱신문이 where 절에 넣는 것 - -`RecipientDeliveryJpaRepository:80` 의 `where` 절이 `id = :id AND lease_owner = :owner AND lease_fence = :fence` 다. 셋이 모두 맞아야 한 행이 갱신된다. - -그 위 `:57`\~`:62` 자바독은 이 갱신문이 어느 자리를 위한 것인지 적는다. 제공자 호출 *뒤에* 일어나는 쓰기이고, 제출 이전의 작업은 새 소유자가 다시 하면 그만이지만 결과는 그렇지 않다는 것이다. 밀려난 리스로 결과를 쓰면 한 작업자의 결과가 다른 작업자의 시도에 보고되고, 둘이 발송 여부에 대해 같은 답을 가질 이유가 없다. - -세 갱신문의 `where` 절을 나란히 놓으면 하나가 더 보인다. `saveProjectionHeldBy:80` 과 `transitionHeldBy:105` 는 소유자와 펜스만 본다. `lease_until > :now` 조건은 `renewLease:134` 와 `:154` 에만 있다. 아직 아무도 가져가지 않은 만료된 리스로는 앞의 둘이 성공한다. - -## applyNextAction 은 네 분기 모두 펜싱을 쓴다 - -`:176` 이 부르는 `applyNextAction:412` 은 `RetryDecision` 을 `switch` 로 가른다. `RetryAfter` 와 `Reconcile` 과 `Stop` 세 분기가 `transitionHeldBy`(`:422`·`:428`·`:436`), `Fallback` 분기가 `saveHeldBy`(`:434`)를 부른다. 한 번 실행에 한 분기만 돌고, 어느 쪽이든 리스가 조건에 들어간다. - -그 메서드의 자바독 `:402`\~`:407` 이 이유를 적는다. 여기의 모든 쓰기가 제공자 호출 뒤에 일어나고, 그 제출이 이 플랫폼이 일부러 트랜잭션 밖에서 시간을 쓰는 유일한 구간이며, 그 사이에 리스가 만료돼 다른 작업자가 일을 가져갈 수 있다는 것이다. 펜싱된 변형은 그런 쓰기가 아무 행에도 맞지 않게 만든다. - -`:409`\~`:410` 은 리스를 잃은 것이 보고할 오류가 아니라고도 적는다. 새 소유자가 자기 결과를 기록할 것이고, 밀려난 작업자에게 남은 의무는 멈추는 것뿐이다. - -`:444` 의 `refreshStatus(work)` 는 그 `switch` 밖이라 갱신 건수와 무관하게 돈다. - -## 결과 기록은 다른 클래스에 있고 리스를 받지 않는다 - -`recipients.save` 를 부르는 main 자리는 `DispatchOutcomeRecorder:135` 하나다. - -그 클래스는 `NotificationDispatchService` 가 아니라 별도 파일이고, `record` 의 인자에 `RecipientLease` 가 없다. `:109`\~`:134` 가 새 `RecipientDeliveryRecord` 를 만들고 `:135` 가 그것을 저장한다. - -그 `save` 가 무엇을 덮는지는 구현에 있다. `JpaRecipientDeliveryStore:33`\~`:52` 가 `findById` 로 행을 읽어 `applyProjection` 을 부르고 `saveAndFlush` 한다. `RecipientDeliveryEntity:159`\~`:170` 의 `applyProjection` 이 받는 인자 열하나에 리스 필드가 없다. 그래서 늦은 쓰기가 소유권을 빼앗지는 않는다. 덮이는 것은 `delivery_state`·`submission_outcome`·`delivery_outcome`·`evidence_level`·`ambiguous_attempt_exists`·`duplicate_risk`·`route_cursor`·`attempt_count`·`last_failure_category`·`next_dispatch_at` 이다. 새 소유자의 행에 밀려난 작업자의 결과가 실린다. - -`findById` 가 같은 쓰기 트랜잭션 안에서 도는 신선한 읽기라, `@Version` 이 견줄 값은 이미 현재값이다. - -펜싱 없는 `transition` 은 다섯 자리 더 있다. `NotificationAdminApplicationService:121`, `NotificationDispatchService:139` 와 `:396`, `NotificationSubmissionService:305`, `ReconciliationService:108` 이다. `:139` 와 `:396` 은 제출 이전 경로이고 나머지 셋은 관리와 제출과 재조정 흐름이라 이 구간 밖이다. - -## stillHeld 검사는 제출 앞에 있고, 결과 쓰기는 제출 뒤에 있다 - -`:161` 이 `leases.stillHeld` 를 확인한다. `:162`\~`:164` 주석이 이 검사가 부수 효과 직전이며 그것을 막을 수 있는 마지막 순간이라고 적는다. - -`:168`\~`:169` 가 트랜잭션 밖에서 제공자에 제출한다. `:171`\~`:173` 이 그 뒤에 레코더를 부른다. - -검사와 제출 사이가 아니라 제출과 기록 사이에 리스가 만료되면, `:161` 은 이미 지난 뒤다. - -## DispatchOutcomeRecorder 를 다루는 시험이 없다 - -파일명에 `Fenc` 나 `Lease` 가 든 시험 파일은 열하나다. `DispatchOutcomeRecorder` 를 이름에 가진 시험 파일은 0 개다. - -펜싱 연산 이름이 나오는 시험 자리는 여섯인데 그중 넷은 시험 대역이 그 메서드를 구현하는 선언이다. `LeaseRecoveryServiceTest:306`·`:313` 과 `PlatformFakes:263`·`:272` 다. - -실제로 그 연산을 부르는 것은 `postgresqlIntegrationTest` 소스 세트의 `PostgreSqlRecipientLeaseFencingIntegrationTest:93` 과 `:127` 두 자리이고, 둘 다 `transitionHeldBy` 다. - -## 원문에 없는 것 - -원문은 제공자 호출 뒤의 투영 쓰기가 펜싱을 우회한다고 적는다. 여기에 더한 것은 그 우회가 어디서 갈리는지와 대가가 정확히 무엇인지다. - -갈리는 지점은 메서드다. `applyNextAction` 의 네 분기는 모두 펜싱을 쓰고, 별도 클래스에 있는 `record` 만 쓰지 않는다. 대가는 소유권이 아니다. `applyProjection` 에 리스 인자가 없어서 덮이는 것은 투영 컬럼 열이다. 그리고 펜싱 갱신문 둘도 `lease_until` 을 보지 않으므로, 펜싱으로 바꾸는 것만으로 만료 구간이 전부 닫히지는 않는다. - -## 확인하지 못한 것 - -이 결함이 실제 행 위에서 일어나는 것을 데이터베이스로 보지 않았다. 필요한 스키마가 다른 마이그레이션 집합의 테이블을 참조해 단독으로 세워지지 않는다. - -낙관적 잠금이 이 쓰기를 걸러 내지 못한다는 판단은 코드 경로를 읽어 내린 것이다. 실행으로 확인하지 않았다. - -만료됐지만 아직 아무도 가져가지 않은 리스로 두 갱신문이 통과하는 시간이 얼마나 되는지 재지 않았다. - -밀려난 작업자의 늦은 `save` 가 새 소유자의 행을 덮는 장면을 데이터베이스로 재현하지 않았다. V1 이 `capability_schema_registry` 를 읽는데 그 테이블은 다른 마이그레이션 계열에 있어서, 알림 플랫폼 마이그레이션만 빈 데이터베이스에 올리면 거기서 멈춘다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f030.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f030.md deleted file mode 100644 index 5f209c2..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f030.md +++ /dev/null @@ -1,146 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a05-f030 -title: claim 이 넣은 CLAIMED 행을 완료로 바꾸는 코드가 없다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a05-f030 -evidenceCapturedOn: 2026-09-04 -body: case-analysis-finding-a05-f030.body.md -assets: - - key: analysis-finding-a05-f030 - file: ../../../final/evidence/rendered/analysis-finding-a05-f030.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a05-f030.txt -source: - - 원본 분석 절은 final/document.md#a05 §91 이다. ---- - -# claim 이 넣은 CLAIMED 행을 완료로 바꾸는 코드가 없다 - -`JpaAdminOperationStore.claim:40` 이 `AdminAuditJpaRepository.claimOperation:41` 을 부르고, 그 native INSERT `:34`~`:39` 가 `phase` 자리에 `'CLAIMED'` 를 넣는다. 그 행을 `COMPLETED` 로 옮기는 프로덕션 코드는 0 줄이고, `save:96` 은 새 식별자로 두 번째 행을 만들려다 유일성 제약에서 멈춘다. - -## 관계 - -- **네 답을 주는 claim 이 있는데 서비스는 있음·없음 두 갈래로 판단한다** - `NotificationAdminApplicationService` 는 `claim` 을 한 번도 부르지 않는다. 이 기록은 불러도 그 뒤가 이어지지 않는다는 것을 저장소 계층에서 확인했다. -- **조건부 update로 행을 claim하고 읽은 값으로 판단하지 않는다** - 이 청구가 구현하는 규칙이다. 삽입 건수를 답으로 쓰는 부분까지는 그 규칙대로다. -- **@Bean이 있다는 것은 조립 증거가 아니다** - main 참조 0 이 강한 신호라는 규칙이다. `AdminOperationClaim` 을 참조하는 프로덕션 파일이 포트와 JPA 구현뿐이라 청구 연산은 후보로만 남아 있다. - -## 문제 - -V8 마이그레이션은 조회 후 실행 후 저장이 낳은 사고를 헤더에 적고, 청구를 삽입 자체로 바꿨다. - -그 설계가 청구부터 완료까지 이어지는지 확인했다. - -## 결론 - -마이그레이션이 세 컬럼을 더한다. command_fingerprint 와 phase 와 claimed_at 이고 phase 의 기본값은 COMPLETED 다. operation_id 의 유일성 제약 uk_notification_admin_operation 은 V3 :61 에 이미 있었다. - -청구는 그 설계대로다. AdminAuditJpaRepository:34~:39 가 ON CONFLICT (operation_id) DO NOTHING 을 붙인 삽입이고 phase 자리에 'CLAIMED' 를 넣는다. 삽입 건수가 1 이면 이 호출자가 붙든 것이다. - -붙든 행을 완료로 옮기는 프로덕션 코드가 0 줄이다. UPDATE notification_admin_audit 과 SET phase 를 저장소 전체에서 세면 한 줄이 나오는데 그것도 시험이고, CHECK 제약이 알 수 없는 값을 거절하는지 보려고 넣은 것이다. - -완료를 기록하는 save 는 앞선 행을 찾지 않는다. :98 이 만든 새 식별자로 엔티티를 하나 더 만들어 :96 이 밀어 넣으므로, 같은 operation_id 가 두 번째로 들어간다. - -그 엔티티가 매핑하는 필드는 아홉인데 commandFingerprint 도 phase 도 claimedAt 도 그중에 없다. V8 이 더한 셋을 JPA 쪽이 아직 모른다. - -PostgreSQL 에 두 삽입을 이어 넣어 확인했다. 두 번째가 uk_notification_admin_operation 위반으로 거절되고, 남는 행은 청구가 넣은 CLAIMED 하나다. - -지금은 그 장면이 배포에서 나지 않는다. NotificationAdminApplicationService 가 operations.claim 을 0 줄 부르고, 대조로 센 operations.save 는 4 줄이다. - -계약 시험의 범위도 청구까지다. @Test 다섯이 배타성과 독립성과 단계 기록과 지문 기록과 단계 제약을 확인하고, COMPLETED 는 그 파일에 한 번도 나오지 않는다. - -## 검증 환경 - -OpenJDK : 21.0.12 -PostgreSQL : postgres:16-alpine -확인 방식 : V8 마이그레이션 헤더와 추가 컬럼 인용, V3 의 유일성 제약 확인, claim 과 native INSERT 와 save 본문 인용, 서비스의 청구 호출 계수를 대조와 함께 확인, 청구 타입을 참조하는 파일 전수, phase 갱신 자리 계수와 전수, 엔티티 필드 전수, 계약 시험의 메서드 이름 전수와 COMPLETED 계수, 실제 데이터베이스에 두 삽입을 이어 실행 -소스 수정 : x - -## 재현 조건 - -1. 마이그레이션 헤더와 그것이 더한 컬럼을 인용하고, 유일성 제약이 언제부터 있었는지 확인한다. -2. claim 과 그것이 부르는 native INSERT 와 save 본문을 나란히 인용한다. -3. 서비스가 청구를 부르는 줄을 세고 대조 이름으로 같은 검색을 건다. -4. 청구 결과 타입을 참조하는 파일을 전부 찾는다. -5. phase 를 갱신하는 자리를 프로덕션과 전체로 나눠 세고 감사 엔티티의 필드를 전부 나열한다. -6. 계약 시험의 메서드 이름을 전부 뽑고 COMPLETED 가 나오는 줄을 센다. -7. postgres:16-alpine 에 V3 와 V8 의 해당 DDL 을 적용하고 두 삽입을 이어 실행한다. - -## 본문 - - - -V8 마이그레이션은 관리 연산의 멱등 청구를 삽입 자체로 바꿨다. 헤더 주석이 그 이전에 무슨 일이 있었는지 적는다. - -## V8 이 더한 세 컬럼과 V3 부터 있던 유일성 제약 - -:::evidence key="analysis-finding-a05-f030" alt="저장소 루트에서 돌린 정적 검색과 데이터베이스 실행 출력 182줄. 먼저 V8 마이그레이션 1~29번 줄이 실린다. 헤더 주석은 관리 경로가 조회 다음 부수 효과 다음 저장이었고 두 호출자가 모두 없음을 읽고 모두 실행했으며, operation_id 의 유일성 제약이 이미 있어 두 번째 저장은 실패했지만 그것은 두 번째 부수 효과 뒤였다고 적는다. 이제 청구가 삽입 자체이며 ON CONFLICT DO NOTHING 이 정확히 한 호출자만 행을 만들게 한다고 적고, 12~15번이 command_fingerprint 와 phase 와 claimed_at 을 더하는데 phase 의 기본값이 COMPLETED 다. 17~25번 COMMENT 가 phase 는 붙든 동안 CLAIMED 이고 끝나면 COMPLETED 이며 CLAIMED 로 남은 행은 두 번째 호출자에게 일이 끝난 것이 아니라 진행 중임을 알린다고 적는다. 27~29번이 phase 를 CLAIMED 와 COMPLETED 와 FAILED 로 제한하는 CHECK 다. 그 아래 V3 마이그레이션 61번의 uk_notification_admin_operation 유일성 제약이 나온다. 이어서 JpaAdminOperationStore 30~72번의 claim 이 실려 40번이 audits.claimOperation 을 부르고 47번이 삽입 건수 1 을 청구됨으로 판정하며, AdminAuditJpaRepository 22~47번이 그 native INSERT 인데 34~39번 문자열이 phase 자리에 'CLAIMED' 를 넣고 ON CONFLICT (operation_id) DO NOTHING 으로 끝난다. 그다음 JpaAdminOperationStore 91~108번의 save 가 실리는데 98번 ids.nextId 로 새 식별자를 만들어 AdminAuditEntity 를 새로 생성하고 96번이 saveAndFlush 하며 phase 를 지정하지 않는다. 다음으로 operations.claim 을 부르는 main 줄이 0 이고 대조로 센 operations.save 가 4 줄이며 AdminOperationClaim 을 참조하는 파일이 포트와 그 타입과 JPA 구현과 계약 시험 넷이라는 것이 나온다. 이어서 프로덕션에서 phase 컬럼을 갱신하는 자리가 0 개이고, 시험을 포함해도 그 문장이 나오는 자리는 AdminOperationClaimContractTest 110번 하나인데 'ALMOST' 를 넣어 제약을 시험하는 줄이다. AdminAuditEntity 가 가진 필드 아홉이 나열되는데 id 와 operationId 와 action 과 actorRef 와 reasonCode 와 tenantId 와 attributes 와 dryRun 과 occurredAt 이고 commandFingerprint 도 phase 도 claimedAt 도 없다. 그다음 계약 시험의 메서드가 애너테이션과 함께 나오는데 54~55번이 BeforeEach 의 migrate 이고 68번과 77번과 84번과 94번과 104번이 각각 동시 청구 중 하나만 이긴다는 것, 다른 식별자는 독립적으로 청구된다는 것, 청구가 phase 를 기록한다는 것, 청구가 지문을 기록한다는 것, CHECK 제약이 알 수 없는 단계를 거절한다는 것이다. 그 파일에서 COMPLETED 가 나오는 줄은 0 개이고 phase 를 단언하는 줄은 1 개다. 마지막으로 postgres:16-alpine 에 V3 51~62 와 V8 12~15·27~29 를 적용하고 두 삽입을 이어 실행한 결과가 나온다. 첫 번째 claimOperation 의 삽입은 조용히 통과하고, 두 번째 save 의 삽입은 uk_notification_admin_operation 유일성 제약 위반으로 거절되며 DETAIL 이 operation_id op-1 이 이미 있다고 적는다. 남은 행을 읽으면 op-1 이 phase CLAIMED 로 한 줄뿐이고 id 는 청구가 넣은 값이다." caption="V8 헤더가 적은 설계와 phase COMMENT 와 CHECK 제약 · V3 의 유일성 제약 · claim 이 부르는 native INSERT · save 가 새로 짓는 엔티티 · 청구 호출 0 과 대조 4 · 프로덕션의 phase 갱신 0 과 엔티티 필드 아홉 · 계약 시험 다섯과 BeforeEach · 두 삽입을 이어 실행한 결과 — 182줄 · exit 0" zoom="true" -::: - -헤더는 관리 경로가 조회 다음 부수 효과 다음 저장이었다고 적는다. 두 호출자가 모두 "없음" 을 읽고 모두 실행했으며, `operation_id` 의 유일성 제약이 이미 있어 두 번째 저장은 실패했지만 그때는 두 번째 부수 효과가 이미 일어난 뒤였다. - -그래서 청구를 삽입으로 바꿨다. `ON CONFLICT DO NOTHING` 이 정확히 한 호출자만 행을 만들게 하고 나머지는 그 호출자가 무엇을 하는지 읽는다. - -`:12`\~`:15` 가 컬럼 셋을 더한다. `command_fingerprint` 와 `phase` 와 `claimed_at` 이다. `phase` 의 기본값은 `COMPLETED` 인데, 기존 행이 전부 완료된 것이기 때문이다. - -`:17`\~`:25` 의 `COMMENT` 가 그 컬럼의 계약을 적는다. 붙든 동안 `CLAIMED` 이고 끝나면 `COMPLETED` 이며, `CLAIMED` 로 남은 행은 두 번째 호출자에게 일이 끝난 것이 아니라 진행 중이라고 알린다는 것이다. - -## claim 은 CLAIMED 행을 넣는다 - -`JpaAdminOperationStore:40` 이 `audits.claimOperation` 을 부른다. - -그 메서드는 `AdminAuditJpaRepository:34`\~`:39` 의 native INSERT 다. 컬럼 목록에 `command_fingerprint` 와 `phase` 와 `claimed_at` 이 들어가고, `VALUES` 의 `phase` 자리에 리터럴 `'CLAIMED'` 가 놓이며, 마지막이 `ON CONFLICT (operation_id) DO NOTHING` 이다. - -`JpaAdminOperationStore:47` 이 삽입 건수가 1 일 때 청구됨을 돌려준다. 아니면 기존 행을 읽어 재생이나 진행 중이나 충돌로 나눈다. - -## 그 행을 COMPLETED 로 옮기는 코드가 없다 - -`UPDATE notification_admin_audit` 이나 `SET phase` 가 나오는 프로덕션 줄이 0 이다. - -시험까지 넣어도 그 문장이 나오는 자리는 `AdminOperationClaimContractTest:110` 하나인데, `'ALMOST'` 를 넣어 CHECK 제약이 거절하는지 보는 줄이다. - -`JpaAdminOperationStore:98` 의 `save` 는 `ids.nextId()` 로 새 식별자를 만들고 `AdminAuditEntity` 를 새로 생성한다. `:96` 이 그것을 `saveAndFlush` 한다. 앞선 행을 찾지도 갱신하지도 않는다. - -그 엔티티에는 마이그레이션이 더한 세 컬럼에 대응하는 필드가 없다. `id`·`operationId`·`action`·`actorRef`·`reasonCode`·`tenantId`·`attributes`·`dryRun`·`occurredAt` 아홉이다. - -## 두 삽입을 이어 넣으면 두 번째가 거절된다 - -`postgres:16-alpine` 에 V3 `:51`\~`:62` 의 테이블과 V8 `:12`\~`:15`·`:27`\~`:29` 를 그대로 적용하고, 두 SQL 문을 코드에 적힌 모양대로 이어 실행했다. - -첫 번째는 조용히 통과한다. 두 번째는 `uk_notification_admin_operation` 위반이고 `DETAIL` 이 `Key (operation_id)=(op-1) already exists` 다. - -남은 행은 하나다. `operation_id` 가 `op-1`, `phase` 가 `CLAIMED`, `id` 는 청구가 넣은 값이다. 청구한 행은 그대로 남고 완료 기록은 만들어지지 않는다. - -## 서비스가 claim 을 한 줄도 부르지 않는다 - -`NotificationAdminApplicationService` 에서 `operations.claim` 이 나오는 줄이 0 이다. 같은 파일에서 `operations.save` 를 세면 4 줄이 나오므로, 0 은 검색이 안 걸린 것이 아니라 실제로 호출이 없는 것이다. - -`AdminOperationClaim` 을 참조하는 파일은 `AdminOperationStorePort`, `AdminOperationClaim`, `JpaAdminOperationStore`, `AdminOperationClaimContractTest` 넷이다. 그중 프로덕션 코드는 포트와 구현뿐이고, 그 값을 받아 분기하는 코드는 없다. - -그래서 서비스가 여전히 조회 후 실행 후 저장을 쓰는 동안에는 `save` 가 만나는 행이 청구가 넣은 것이 아니다. 같은 `operation_id` 가 동시에 두 번 저장되는 경우에는 V8 헤더가 적은 대로 두 번째 저장이 제약에서 실패한다. - -## 계약 시험 다섯은 청구까지만 단언한다 - -`@Test` 는 다섯이다. 동시 청구 중 하나만 이기는 것(`:68`), 다른 식별자가 독립적으로 청구되는 것(`:77`), 청구가 `phase` 를 기록하는 것(`:84`), 청구가 지문을 기록하는 것(`:94`), CHECK 제약이 알 수 없는 단계를 거절하는 것(`:104`)이다. `:55` 의 `migrate` 는 `@BeforeEach` 로 매 시험 전에 스키마를 다시 만든다. - -그 파일에 `COMPLETED` 는 한 번도 나오지 않는다. 청구가 넣은 행이 나중에 완료가 되는지 보는 시험이 없다. - -## 원문에 없는 것 - -원문은 프로덕션 호출 그래프에 청구 호출이 0 이고 완료로 잇는 상태 전이도 이어지지 않는다고 적는다. 여기에 더한 것은 그 단절이 어디까지 굳어 있는지다. `phase` 를 갱신하는 프로덕션 코드가 0 줄이고, `AdminAuditEntity` 에 그 컬럼을 담을 필드가 없으며, 계약 시험에 `COMPLETED` 가 한 번도 나오지 않는다. 세 자리 모두 청구 뒤를 다루지 않는다. - -## 확인하지 못한 것 - -두 SQL 문을 psql 로 직접 넣었다. 하이버네이트가 `saveAndFlush` 를 어떤 문장으로 바꾸는지는 보지 않았다. - -경로마다 트랜잭션이 그 위반을 어디까지 덮는지 나눠 세지 않았다. - -단계를 완료로 옮기는 책임을 어느 계층에 두어야 하는지 결론 내지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f031.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f031.md deleted file mode 100644 index d794d6a..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f031.md +++ /dev/null @@ -1,155 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a05-f031 -title: 오타 난 벤더는 기동을 멈추지만 그 프로퍼티를 지목하지 못한다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a05-f031 -evidenceCapturedOn: 2026-09-04 -body: case-analysis-finding-a05-f031.body.md -assets: - - key: analysis-finding-a05-f031 - file: ../../../final/evidence/rendered/analysis-finding-a05-f031.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a05-f031.txt -source: - - 원본 분석 절은 final/document.md#a05 §116 이다. ---- - -# 오타 난 벤더는 기동을 멈추지만 그 프로퍼티를 지목하지 못한다 - -`PersistenceVendorSettings:13`~`:16` 은 열거형에 바인딩하는 것이 알 수 없는 벤더를 기동 실패로 만드는 근거라고 적고, 그 바인딩이 없을 때 무엇이 대신 나타나는지도 같은 자리에 적어 둔다. `app-bootstrap` 에서 그 타입은 빈이 아니고, 프로브가 자바독이 예고한 증상을 그대로 냈다. - -## 관계 - -- **@Bean이 있다는 것은 조립 증거가 아니다** - 애너테이션이 후보만 만든다는 규칙이다. `@ConfigurationProperties` 가 붙어 있어도 어느 스캔 범위에 드는지에 따라 바인딩 여부가 배포마다 갈린다. -- **"꺼짐"은 조건의 반복이 아니라 구조여야 한다** - 켜지 않은 능력이 자기 설정을 바인딩하지도 거절하지도 못하게 한다는 점에서 같은 방향이다. 다만 여기서 뺀 것은 컴포넌트 스캔이 아니라 `@ConfigurationPropertiesScan` 이다. -- **스캔에서 뺀 다섯 패키지의 컴포넌트 여섯을 두 자동설정 어느 쪽도 소유하지 않았다** - 그 기록은 `@ComponentScan` 의 정규식 제외라 컴포넌트가 어디에도 없게 됐고, 이 기록은 `@ConfigurationPropertiesScan` 의 basePackages 목록이라 값 검증이 사라졌다. - -## 문제 - -벤더 선택기는 프로퍼티 하나로 RDBMS 조합을 고른다. 그 타입의 자바독은 열거형 바인딩이 오타를 기동 실패로 만드는 근거라고 적는다. - -그 바인딩이 어디서 일어나고 어디서 일어나지 않는지, 일어나지 않는 쪽에서 오타가 실제로 무엇을 내는지 확인했다. - -## 결론 - -PersistenceVendorSettings:18 에 @ConfigurationProperties 가 붙어 있고, 중첩된 Vendor 열거형 :25~:28 의 값은 POSTGRESQL 과 H2 둘이다. 압축 생성자 :30~:34 가 값이 없으면 POSTGRESQL 로 채운다. - -이 타입을 손으로 켜는 main 줄이 하나도 없고, CaSkeletonApplication:58 이 열거한 스무 개 basePackages 에 persistence 패키지가 빠져 있다. SamplePortfolioApplication:46 은 다르다. basePackages = "dev.caskeleton" 을 제외 없이 걸고, sample-portfolio/build.gradle:37 이 그 리프를 의존에 넣는다. - -제외는 실수가 아니다. PersistenceJpaRootAutoConfiguration:85~:91 과 JpaAdapterComponentsConfig:55~:56 이 각각 그 사실과 목적을 적어 둔다. - -app-bootstrap 이 벤더를 정하는 자리는 PersistenceJpaRootAutoConfiguration:111~:112 의 environment.getProperty 와 :113 의 "postgresql".equalsIgnoreCase(vendor) 다. 허용값 목록이 없어서 mysql 도 postgresq1 도 H2 도 여기서 false 가 된다. - -그 false 는 두 곳으로 간다. JpaDataSourceProfileValidator.validateResolved:52~:55 는 false 를 받으면 곧바로 돌아가므로 PostgreSQL 버전 검사가 함께 꺼진다. 그리고 @ConditionalOnProperty 두 개가 모두 어긋나 벤더 SPI 빈이 하나도 만들어지지 않는다. - -프로브가 그 뒤를 확인했다. 벤더 설정 둘만 올린 조립은 기동에 성공하고 OutboxClaimRepository 빈이 0 이다. 거기에 그 저장소를 요구하는 @Repository 를 얹으면 두 오타 모두 기동이 멎는데, 예외가 없는 빈의 이름만 말하고 프로퍼티 이름은 말하지 않는다. 반대로 그 타입을 바인딩한 조립에서는 같은 값이 ca-skeleton.persistence.vendor 를 지목하며 멎는다. - -PersistenceVendorSelectionTest:52 가 단언하는 것이 뒤쪽이다. :59 가 실패 스택에 프로퍼티 이름이 담기는지 보는데, :92 가 @EnableConfigurationProperties 로 그 타입을 직접 켠 뒤다. app-bootstrap 은 켜지 않으므로 같은 값이 같은 메시지를 내지 않는다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 벤더 설정 타입 전문 게재와 그 자바독 인용, 그 타입을 등록하는 main 자리 계수와 이름이 나오는 자리 전수, 두 애플리케이션의 스캔 범위 대조, 합성 루트의 제외 설명과 벤더 판정 자리와 그 불린을 받는 검증기 인용, 조립 경로 인용, ApplicationContextRunner 세 조립 실행, 시험이 그 타입을 켜는 자리 인용 -소스 수정 : x - -## 재현 조건 - -1. 벤더 설정 타입을 전문으로 싣고 열거형 바인딩을 근거로 든 자바독을 읽는다. -2. 그 타입을 켜는 main 줄을 세고 두 애플리케이션의 @ConfigurationPropertiesScan 범위를 나란히 싣는다. -3. 제외를 적어 둔 두 자바독과 벤더 판정 자리와 그 불린을 받는 검증기를 인용한다. -4. 무조건 @Import 되는 컴포넌트 스캔과 그 안의 @Repository 생성자를 인용한다. -5. /tmp/probe-vendor/VendorProbe.java 를 컴파일해 세 조립에 =mysql 과 =postgresq1 을 넣고 돌린다. - -## 본문 - - - -`PersistenceVendorSettings` 는 배포가 어떤 RDBMS 조합을 돌릴지 프로퍼티 하나로 고르게 한다. - -## 그 타입이 약속하는 기동 실패 - -:::evidence key="analysis-finding-a05-f031" alt="저장소 루트에서 돌린 정적 검색과 프로브 실행 출력 279줄. 먼저 PersistenceVendorSettings 1~35번 줄이 실린다. 13~16번 자바독은 열거형에 바인딩하는 것이 알 수 없는 벤더를 기동 실패로 만드는 근거라고 적고, 문자열로 두면 두 조건부 벤더 설정이 모두 꺼진 채 남아 첫 번째로 없는 SPI 빈이 OutboxClaimRepository 를 지목하는 NoSuchBeanDefinitionException 으로 나타난다고 적는다. 18번이 ConfigurationProperties 애너테이션, 19번이 record 선언, 25~28번이 POSTGRESQL 과 H2 두 값, 30~34번 압축 생성자가 값이 없으면 POSTGRESQL 로 채운다. 다음으로 EnableConfigurationProperties 가 이 타입을 담은 main 줄이 0 개라고 나오고, ConfigurationPropertiesScan 이 붙은 main 자리 둘이 나온다 — app-bootstrap 의 CaSkeletonApplication 58번과 sample-portfolio 의 SamplePortfolioApplication 46번이며 뒤쪽은 basePackages 가 dev.caskeleton 하나다. 이어서 이 타입 이름이 나오는 자리가 전부 나열되는데 main 은 자기 자신과 H2PersistenceConfig 와 PostgreSqlPersistenceConfig 와 PersistenceJpaRootAutoConfiguration 넷이고 나머지는 PersistenceVendorSelectionTest 이며, 그 시험 92번에만 EnableConfigurationProperties 가 있다. 다음으로 두 애플리케이션의 스캔 범위가 나란히 실린다. CaSkeletonApplication 58~80번은 basePackages 를 스무 개 열거하는데 bootstrap 하위 열하나와 adapter.inbound 셋과 adapter.outbound 의 cache.redis·fileserver·objectstorage 와 application·domain·shared 이고 adapter.outbound.persistence 로 시작하는 항목이 없다. SamplePortfolioApplication 44~48번은 46번이 basePackages 를 dev.caskeleton 으로만 두고, 그 아래 sample-portfolio/build.gradle 37번이 persistence-jpa 를 implementation 의존에 넣는다. 이어서 PersistenceJpaRootAutoConfiguration 56~70번이 실려 57~60번 ConditionalOnProperty 와 62~69번 Import 목록이 나오는데 JpaAdapterComponentsConfig 가 그 목록 맨 앞이고 조건이 붙어 있지 않다. 85~115번에서는 85~91번 자바독이 벤더를 Environment 에서 읽는 이유를 적는다 — 그 타입이 여기 등록된 빈이 아니고 합성 루트의 프로퍼티 스캔이 persistence 패키지를 일부러 제외했으며, 켠 적 없는 선택적 기능이 자기 세부 설정을 바인딩하거나 거절하지 못하게 하려는 것이라고 적는다. 111~112번이 프로퍼티를 기본값 postgresql 로 읽고 113번이 postgresql 과 대소문자 무시로 견준 불린을 validateResolved 에 넘긴다. 다음으로 JpaDataSourceProfileValidator 44~60번이 실리는데 50번 서명 뒤 52~55번이 그 불린이 false 면 주석 두 줄을 달고 곧바로 돌아간다. 이어서 조립 경로가 실린다. JpaAdapterComponentsConfig 55~56번 자바독이 ConfigurationPropertiesScan 이 이 나무를 ComponentScan 만큼 일부러 제외한다고 적고, 59~67번 ComponentScan 의 basePackages 여섯 중 하나가 outbox 패키지이며, OutboxStoreAdapter 23번이 Repository 이고 29~30번 생성자가 OutboxEventJpaRepository 와 OutboxClaimRepository 를 요구한다. 그다음 프로브가 나온다. VendorProbe.java 36~47번이 두 조립 클래스를 정의하는데 OutboxConsumer 가 OutboxStoreAdapter 를 빈으로 만들고 BoundSettings 가 EnableConfigurationProperties 로 그 타입을 켠다. 74~81번 main 이 세 조립을 부른다. 그 아래가 실행 결과다. 두 벤더 설정만 올리고 mysql 을 넣으면 기동 실패가 false 이고 OutboxClaimRepository 빈이 0 이다. SPI 소비자를 더하면 mysql 과 postgresq1 모두 기동 실패가 true 인데 ca-skeleton.persistence.vendor 를 지목하는지가 false 이고 OutboxClaimRepository 를 지목하는지가 true 이며 맨 앞 예외가 UnsatisfiedDependencyException 이다. 그 타입을 켜고 mysql 을 넣으면 기동 실패가 true 이고 이번에는 프로퍼티를 지목하는지가 true 이며 맨 앞 예외가 ConfigurationPropertiesBindException 이다. 마지막으로 PersistenceVendorSelectionTest 28~62번과 88~94번이 실리는데, 52번 시험이 mysql 을 넣고 57번에서 실패를, 59번에서 그 스택에 프로퍼티 이름이 담기는지를 단언하며, 92번이 EnableConfigurationProperties 로 그 타입을 켠다." caption="벤더 설정 타입과 그 자바독이 예고한 증상 · 그 타입을 켜는 main 줄 0 과 두 애플리케이션의 스캔 범위 · 제외를 적어 둔 자바독과 문자열 비교와 그 불린을 받는 검증기 · 무조건 Import 되는 스캔과 그 안의 Repository 생성자 · 세 조립에 오타를 넣고 돌린 결과 · 시험이 그 타입을 켜고 단언하는 자리 — 279줄 · exit 0" zoom="true" -::: - -`:13`\~`:16` 자바독이 열거형 바인딩의 목적을 적는다. 문자열로 두면 두 조건부 벤더 설정이 모두 꺼진 채로 남고, 첫 번째로 없는 SPI 빈이 `OutboxClaimRepository` 를 지목하는 `NoSuchBeanDefinitionException` 으로 나타난다는 것이다. 오타 난 값과 그 예외 사이에는 벤더 설정 둘이 함께 꺼지는 단계와 SPI 빈이 없어 주입이 실패하는 단계가 있어서, 예외를 읽어도 어느 프로퍼티가 잘못됐는지 알 수 없다. - -`:18` 이 `@ConfigurationProperties` 이고 `:25`\~`:28` 이 `POSTGRESQL` 과 `H2` 둘이다. `:30`\~`:34` 압축 생성자는 값이 없으면 `POSTGRESQL` 로 채운다. 이 선택기가 생기기 전의 모든 배포가 PostgreSQL 을 썼기 때문에, 키를 설정하지 않고 올린 배포가 쓰던 데이터스토어를 그대로 유지하게 하려는 것이다. - -## app-bootstrap 의 스캔 목록에 이 패키지가 없다 - -`@EnableConfigurationProperties(PersistenceVendorSettings.class)` 를 쓰는 main 줄은 0 이다. 그렇게 쓰는 자리는 `PersistenceVendorSelectionTest:92` 하나이고 시험이다. - -이름이 나오는 main 파일은 넷이다. 자기 자신, `H2PersistenceConfig`, `PostgreSqlPersistenceConfig`, `PersistenceJpaRootAutoConfiguration` 이다. 앞의 둘은 `@ConditionalOnProperty` 의 `prefix` 자리에 `PREFIX` 상수만 쓰고, 마지막 하나는 `import` 와 자바독과 `VENDOR_PROPERTY` 상수만 쓴다. - -`CaSkeletonApplication:58`\~`:80` 의 `@ConfigurationPropertiesScan` 은 basePackages 를 스무 개 열거한다. `dev.caskeleton.adapter.outbound` 로 시작하는 항목은 `cache.redis` 와 `fileserver` 와 `objectstorage` 셋이고 `persistence` 가 없다. - -이름으로 세는 방식은 패키지째 스캔하는 쪽을 잡지 못하므로 두 합성 루트를 따로 봐야 한다. `SamplePortfolioApplication:46` 은 `basePackages = "dev.caskeleton"` 을 제외 없이 걸고, `sample-portfolio/build.gradle:37` 이 `implementation project(':adapter:outbound:persistence-jpa')` 로 그 리프를 클래스패스에 올린다. 그 배포에서는 이 타입이 스캔 범위 안이다. - -## PersistenceJpaRootAutoConfiguration 자바독이 그 제외를 적어 두었다 - -`:85`\~`:91` 은 벤더를 `Environment` 에서 읽는 이유를 적는다. 그 타입이 여기서 등록된 빈이 아니고, 합성 루트의 `@ConfigurationPropertiesScan` 이 persistence 패키지를 일부러 제외했기 때문이다. - -목적도 함께 있다. 켠 적 없는 선택적 기능이 자기 세부 설정을 바인딩하거나 거절하지 못하게 하려는 것이다. 선택기를 설정하지 않은 배포가 PostgreSQL 로 가는 것이 `PostgreSqlPersistenceConfig` 의 `matchIfMissing = true` 와 같은 결과라는 것도 같은 자바독에 있다. - -`JpaAdapterComponentsConfig:55`\~`:56` 이 같은 사실을 한 번 더 적는다. 그 패키지들의 `@ConfigurationProperties` 타입을 각자 자기 패키지 안에서 켜야 하는 이유가 이 제외라는 것이다. - -제외는 의도된 것이다. 다만 그 제외 때문에 열거형 바인딩이 하던 값 검증도 `app-bootstrap` 에서는 일어나지 않는다. - -## \:113 이 "postgresql".equalsIgnoreCase 로 벤더를 정한다 - -`:111`\~`:112` 가 `environment.getProperty(PersistenceVendorSettings.VENDOR_PROPERTY, "postgresql")` 로 문자열을 읽고 `trim` 한다. - -`:113` 이 `"postgresql".equalsIgnoreCase(vendor)` 를 `validator.validateResolved(dataSource, ...)` 에 넘긴다. - -허용값 목록이 없다. `mysql` 도 `postgresq1` 도 `H2` 도 여기서 `false` 가 되어 같은 값으로 들어간다. 열거형에 바인딩했다면 앞의 둘은 값 변환에서 실패했을 것이고 `H2` 는 통과했을 것이다. - -## 그 false 를 받는 검증기는 곧바로 돌아간다 - -`JpaDataSourceProfileValidator.validateResolved:50` 이 그 불린을 `requirePostgreSql` 로 받는다. - -`:52`\~`:55` 가 `false` 면 즉시 `return` 한다. 주석은 로컬 개발이 H2 를 돌리는 것이 설계이고 여기서까지 PostgreSQL 을 요구하면 모든 노트북을 거절하게 된다고 적는다. - -그래서 오타 난 벤더는 `:57`\~`:58` 의 PostgreSQL 버전 검사도 함께 지나친다. 자바독 `:47` 이 이 인자를 "벤더 선택기가 PostgreSQL 을 골랐는지"라고 적는데, 오타는 고르지 않은 것과 구별되지 않는다. - -## 세 조립에 오타를 넣고 돌린 결과 - -`PersistenceJpaRootAutoConfiguration:62`\~`:69` 의 `@Import` 는 조건이 없다. 그 목록 맨 앞이 `JpaAdapterComponentsConfig` 이고, 그것이 `@ComponentScan` 하는 여섯 패키지에 `dev.caskeleton.adapter.outbound.persistence.outbox` 가 있다. 그 패키지의 `OutboxStoreAdapter:23` 이 `@Repository` 이며 `:29`\~`:30` 생성자가 `OutboxClaimRepository` 를 요구한다. - -`ApplicationContextRunner` 로 세 조립을 만들어 값을 넣었다. - -두 벤더 설정만 올리고 `=mysql` 을 넣으면 컨텍스트는 성공하고 `OutboxClaimRepository` 빈이 0 이다. `@ConditionalOnProperty` 둘이 모두 어긋난 결과다. - -거기에 `OutboxStoreAdapter` 를 소비자로 더하면 `=mysql` 과 `=postgresq1` 모두 기동이 실패한다. 예외 사슬이 `OutboxClaimRepository` 를 지목하고 `ca-skeleton.persistence.vendor` 는 지목하지 않는다. 맨 앞은 `UnsatisfiedDependencyException` 이다. - -`@EnableConfigurationProperties` 로 그 타입을 켜면 같은 `=mysql` 이 `ConfigurationPropertiesBindException` 을 내고 이번에는 `ca-skeleton.persistence.vendor` 를 지목한다. - -자바독 `:13`\~`:16` 이 적은 증상이 첫째와 둘째이고, 그것이 막겠다던 증상이 셋째다. - -## 시험이 켜는 것을 app-bootstrap 은 켜지 않는다 - -`PersistenceVendorSelectionTest:52` 의 `rejectsAnUnknownVendorAtStartupRatherThanComposingNothing` 이 `=mysql` 을 넣고 `:57` 에서 컨텍스트 실패를, `:59` 에서 그 스택에 `VENDOR_PROPERTY` 가 담기는지를 단언한다. - -그 시험이 쓰는 러너는 `:91`\~`:93` 의 `@EnableConfigurationProperties(PersistenceVendorSettings.class)` 를 함께 올린다. 프로브의 셋째 조립과 같은 모양이다. - -그래서 이 시험이 초록이어도 `app-bootstrap` 에서 같은 값이 같은 메시지를 내는지는 말해 주지 않는다. 프로브의 둘째 조립이 그 답이고, 거기서는 프로퍼티 이름이 나오지 않는다. - -## 원문에 없는 것 - -원문은 이 타입이 프로덕션에서 설정 프로퍼티 빈으로 등록되지 않는다고 적는다. 여기에 더한 것은 그것이 실수가 아니라는 점과 그 대가가 무엇인지다. 제외는 두 자바독에 적혀 있고 목적도 함께 적혀 있다. 대가는 둘인데, 오타가 기동을 멈추기는 하되 잘못된 프로퍼티를 지목하지 못한다는 것과, `:113` 의 `false` 가 PostgreSQL 버전 검사까지 함께 끈다는 것이다. - -## 확인하지 못한 것 - -프로브가 세운 것은 `ApplicationContextRunner` 위의 부분 조립이고 `app-bootstrap` 전체를 부팅하지 않았다. `OutboxEventJpaRepository` 는 `Proxy` 로 대신했고, 실제 애플리케이션을 오타 난 값으로 띄우지는 않았다. - -`sample-portfolio` 쪽은 스캔 범위와 의존 그래프로만 판단했고 부팅해 보지 않았다. - -제외를 되돌렸을 때 다른 선택적 기능이 무엇을 바인딩하게 되는지 따지지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f032.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f032.md deleted file mode 100644 index 0e596b7..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f032.md +++ /dev/null @@ -1,156 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a05-f032 -title: 포화된 풀의 대기 수를 단언하는 시험이 풀 계약 레인에 없다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a05-f032 -evidenceCapturedOn: 2026-09-04 -body: case-analysis-finding-a05-f032.body.md -assets: - - key: analysis-finding-a05-f032 - file: ../../../final/evidence/rendered/analysis-finding-a05-f032.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a05-f032.txt -source: - - 원본 분석 절은 final/document.md#a05 §125 이다. ---- - -# 포화된 풀의 대기 수를 단언하는 시험이 풀 계약 레인에 없다 - -`jpa-nightly.yml:128` 은 이 레인이 포화된 풀의 대기 수 보고를 검사한다고 적는다. 그 주장을 확인하는 줄은 레인 전체에 `PoolPressureContractTest:31` 하나뿐이고, 거기 들어간 `3` 은 바로 앞줄이 생성자에 써 넣은 값이다. 실제 풀을 띄우는 `HikariPoolSaturationContractTest` 는 `:100` 에서 `getThreadsAwaitingConnection()` 을 읽어 담고도 그 값만 빼고 단언한다. - -## 관계 - -- **빠뜨림이 통과가 되는 게이트는 게이트가 아니다** - 워크플로가 적은 셋 가운데 둘째만 실제 풀에 닿지 않는데, 레인이 초록이면 셋 다 확인된 것으로 읽힌다. -- **풀 계약 레인이 실행되지 않아 포화 동작이 확인되지 않았다** - 그 질문은 이 레인이 세 계약을 검증한다는 것을 사실로 두고 실행만 남았다고 적는다. 이 기록은 그중 대기 수 보고에 단언이 없다는 것을 확인했으므로, 레인을 돌려도 그 미지수는 닫히지 않는다. -- **REQUIRES_NEW의 커넥션 비용과 풀 사이징 제약** - 그 제약이 이 레인에서 실제 풀로 확인되는 유일한 주장이다. `RequiresNewPoolPressureContractTest:45` 와 `:61` 이 크기 1 과 2 로 나눠 확인한다. - -## 문제 - -야간 워크플로가 이 레인이 검사하는 것을 셋으로 적는다. 같은 문장이 build.gradle 의 태스크 주석에도 있다. - -셋 각각에 대응하는 단언이 있는지 확인했다. - -## 결론 - -이 레인은 릴리스에 걸려 있다. build.gradle:309 가 jpaPlatformPoolContractTest 를 jpaPlatformReleaseGate 의 의존으로 넣고, 그 태스크가 도는 소스 세트에는 파일 셋에 시험 여덟이 있다. - -셋 중 첫째에는 실제 HikariDataSource 가 있다. RequiresNewPoolPressureContractTest:45 가 크기 1 짜리 풀에서 안쪽 트랜잭션이 커넥션을 못 얻는 것을, :61 이 크기 2 에서는 얻는 것을 잡는다. - -셋째는 절반만 그렇다. HikariPoolSaturationContractTest:64~:66 이 재는 것은 대기 시간의 상한뿐이라 즉시 실패해도 통과한다. 옆의 :77 은 아예 대기를 만들지 않는데, :81 이 하나를 놓은 다음에 :83 이 집기 때문이다. - -두 번째는 다르다. 레인 안에서 pending() 을 단언하는 줄이 PoolPressureContractTest:31 하나인데, 그 값은 :29 의 new PoolMeasurement(4, 2, 3, Duration.ofMillis(80)) 에 넣은 것이다. HikariDataSource 가 없다. - -같은 시험의 나머지 셋도 계산이거나 되읽기다. PoolMeasurement:26 의 total() 은 active + idle, :31 의 saturated() 는 pending > 0 이므로, :33 과 :34 는 손으로 넣은 값에서 유도되는 항등식이다. - -실제 풀에서 그 값을 읽는 자리는 HikariPoolSaturationContractTest:100 이다. :95~:101 이 HikariPoolMXBean 에서 세 값을 읽어 PoolMeasurement 를 만드는데, :103~:105 의 단언은 active 와 total 과 커넥션 유효성이다. - -그 시험이 쥔 커넥션은 :94 의 하나뿐이고 POOL_SIZE 는 :32 에서 2 다. 기다리는 스레드가 생길 수 없는 조합이다. - -PoolMeasurement 자체가 testkit 소스 세트에 있고, HikariPoolMXBean 이나 getThreadsAwaitingConnection 을 읽는 프로덕션 자리는 main 파일 4714 개에 0 이다. 잘못된 것은 런타임이 아니라 레인이 자기에 대해 적은 문장이다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 야간 워크플로의 단계 주석과 그 내력 인용, 레인이 도는 소스 세트와 릴리스 게이트 의존 인용, 세 파일의 시험과 단언 전수, 레인 범위에서 pending() 단언 계수, 값 객체 시험과 포화 세 시험의 본문 인용, PoolMeasurement 의 import 자리와 계산 메서드 인용, 프로덕션에서 대기 수를 읽는 자리 계수와 훑은 파일 수 대조 -소스 수정 : x - -## 재현 조건 - -1. 워크플로의 단계 주석과 build.gradle 의 같은 문장, 그리고 릴리스 게이트 의존을 인용한다. -2. 레인이 도는 소스 세트의 파일을 나열하고 세 파일의 시험과 단언을 전부 뽑는다. -3. 레인 범위에서 pending() 을 단언하는 줄을 세고 그 시험 본문을 싣는다. -4. 실제 풀을 띄우는 세 시험을 전문으로 싣는다. -5. PoolMeasurement 를 어디서 import 하는지와 그 타입의 계산 메서드를 인용한다. -6. 프로덕션에서 대기 수를 읽는 자리를 세고 훑은 파일 수를 함께 센다. - -## 본문 - - - -야간 워크플로가 JPA 풀 계약 레인을 부른다. 그 단계의 주석이 레인이 무엇을 검사하는지 셋으로 적는다. - -## 야간 워크플로가 적은 셋과 레인의 여덟 시험 - -:::evidence key="analysis-finding-a05-f032" alt="저장소 루트에서 돌린 정적 검색 출력 271줄. 먼저 jpa-nightly.yml 122~134번 줄이 실린다. 단계 이름은 풀 포화와 REQUIRES_NEW 커넥션 동작을 검증한다는 것이고, 124~126번 주석이 이 단계가 예전에는 명시적 프로퍼티로 단언을 끄고 그 결과를 인증이라 불렀으며 그래서 유일하게 단언된 임계값이 임계값을 단언하지 않는다는 것이었다고 적는다. 127~129번이 지금 검사하는 셋을 적는데 REQUIRES_NEW 가 동시 스레드당 커넥션 둘을 요구한다는 것, 포화된 풀이 자기 대기 수를 보고한다는 것, 호출자가 커넥션 없이 진행하는 대신 기다린다는 것이며, 어느 러너에서나 참이라 끌 것이 없다고 적는다. 이어서 persistence-jpa/build.gradle 281~310번이 실린다. 281~286번 주석이 같은 셋을 다시 적고, 287~296번의 jpaPlatformPoolContractTest 태스크가 jpaPlatformPerformanceTest 소스 세트를 돌리며, 300~310번의 jpaPlatformReleaseGate 가 309번에서 그 태스크를 의존에 넣는다. 그 아래 소스 세트의 시험 파일 셋이 나열되는데 HikariPoolSaturationContractTest 와 PoolPressureContractTest 와 RequiresNewPoolPressureContractTest 다. 다음으로 세 파일의 시험 이름과 단언이 전부 나열되고 시험이 여덟이다. 이어서 레인 범위에서 measurement.pending 을 단언하는 줄이 1 개라고 나오고, 그 줄이 있는 시험이 실린다. PoolPressureContractTest 26~47번인데 29번이 new PoolMeasurement(4, 2, 3, Duration.ofMillis(80)) 을 만들고 31번이 pending 이 3 인지, 32번이 지연이 80밀리초인지, 33번이 total 이 6 인지, 34번이 saturated 가 참인지 확인한다. 37~47번의 두 번째 시험은 동시 스레드 8 과 깊이 1 로 required 를 8 곱하기 2 더하기 1 로 계산해 47번에서 17 과 같은지 확인한다. 그 아래 HikariPoolSaturationContractTest 32번의 POOL_SIZE 가 2 라는 것과 50~118번의 세 시험이 실린다. 52번 시험은 POOL_SIZE 만큼 쥔 뒤 61번에서 한 번 더 요청해 SQLException 이 나는 것과 64~66번에서 기다린 시간이 ACQUIRE_TIMEOUT 더하기 2초보다 작은 것을 확인한다. 77번 시험은 79번과 80번에서 둘을 얻고 81번에서 하나를 놓은 뒤 83번에서 세 번째를 얻어 유효한지 확인한다. 92번 시험은 94번에서 커넥션 하나만 쥐고 95~101번이 HikariPoolMXBean 에서 활성 수와 유휴 수와 getThreadsAwaitingConnection 을 읽어 PoolMeasurement 를 만드는데, 103~105번의 단언은 active 가 1 인지와 total 이 1 이상인지와 쥔 커넥션이 유효한지 셋이다. 110~118번이 그 풀을 만드는 pool 메서드다. 다음으로 RequiresNewPoolPressureContractTest 43~89번이 실린다. 45번 시험이 크기 1 짜리 풀에서 바깥 커넥션을 쥔 채 안쪽을 얻으려 하면 예외가 나는 것을, 61번 시험이 크기 2 에서는 얻어지고 두 커넥션이 다른 객체인 것을 확인하며, 80번 시험은 동시 스레드 1 과 깊이 1 로 required 를 계산해 88번에서 3 과 같은지 확인한다. 마지막으로 PoolMeasurement 를 import 하는 자리가 두 시험 파일이라는 것과 그 타입 6~34번이 실리는데, 9~11번 자바독이 대기 수와 획득 지연을 함께 기록하는 이유를 적으며 표본을 뜨는 순간에 대기 수가 0 으로 보이면서도 호출자들이 늘 기다리는 풀이 있을 수 있다고 적고, 26~28번의 total 이 active 더하기 idle 을, 31~33번의 saturated 가 pending 이 0 보다 큰지를 계산한다. 그 아래 프로덕션에서 대기 수를 읽는 자리가 0 개이고 그 검색이 훑은 main 파일이 4714 개라고 나온다." caption="야간 워크플로가 적은 셋과 그 내력 · 레인이 도는 태스크와 릴리스 게이트 의존 · 세 파일의 시험 여덟과 단언 전수 · 레인 범위의 pending 단언 하나와 그 입력 · 포화를 다루는 세 시험 전문 · REQUIRES_NEW 세 시험 전문 · PoolMeasurement 자바독과 계산 메서드 · 프로덕션의 대기 수 읽기 0 — 271줄 · exit 0" zoom="true" -::: - -`jpa-nightly.yml:127`\~`:129` 가 셋을 적는다. `REQUIRES_NEW` 가 동시 스레드당 커넥션 둘을 요구한다는 것, 포화된 풀이 자기 대기 수를 보고한다는 것, 호출자가 커넥션 없이 진행하는 대신 기다린다는 것이다. - -`:124`\~`:126` 은 이 단계의 내력도 적는다. 예전에는 명시적 프로퍼티로 단언을 끄고 그 결과를 인증이라 불렀고, 그래서 유일하게 단언된 임계값이 임계값을 단언하지 않는다는 것이었다. - -`build.gradle:281`\~`:286` 이 같은 셋을 다시 적고 `:287` 의 태스크가 `jpaPlatformPerformanceTest` 소스 세트를 돌린다. `:300` 의 `jpaPlatformReleaseGate` 가 `:309` 에서 그 태스크를 의존에 넣으므로, 릴리스가 이 셋에 기댄다. - -그 소스 세트에 있는 것은 `HikariPoolSaturationContractTest` 와 `PoolPressureContractTest` 와 `RequiresNewPoolPressureContractTest` 셋이고, 그 안의 `@Test` 를 전부 세면 여덟이다. - -## 첫 번째는 실제 풀로 확인된다 - -`RequiresNewPoolPressureContractTest:45` 의 `poolOfOneCannotServeAnInnerTransaction` 은 크기 1 짜리 풀에서 바깥 커넥션을 쥔 채 안쪽을 얻으려 하면 `SQLException` 이 나는 것을 확인한다. - -`:61` 의 `poolOfTwoServesTheSameNesting` 은 크기 2 에서는 얻어지고 두 커넥션이 다른 객체인 것을 확인한다. - -둘 다 실제 `HikariDataSource` 를 띄운다. - -같은 파일 `:80` 의 `sizingRuleMatchesTheObservedRequirement` 는 다르다. `:81`\~`:84` 가 `concurrentThreads = 1` 과 `maxRequiresNewDepth = 1` 로 `required = 1 * (1 + 1) + 1` 을 계산하고 `:88` 이 그것을 `3` 과 견준다. `PoolPressureContractTest:39` 도 같은 모양이고 숫자만 8 과 17 이다. - -## 세 번째는 절반이다 - -`HikariPoolSaturationContractTest:52` 의 `saturatedPoolFailsWithinItsTimeout` 이 `POOL_SIZE` 만큼 쥔 뒤 한 번 더 요청한다. `:61` 이 `SQLException` 을, `:64`\~`:66` 이 기다린 시간이 `ACQUIRE_TIMEOUT` 에 2 초를 더한 값보다 작은 것을 확인한다. - -상한만 있다. 얼마나 기다렸는지에 대한 하한 단언이 없어서, 즉시 실패해도 이 시험은 통과한다. - -`:77` 의 `releasingAConnectionLetsTheNextCallerThrough` 는 기다림과 무관하다. `:79` 와 `:80` 이 둘을 얻고 `:81` 이 하나를 놓은 뒤 `:83` 이 세 번째를 얻으므로, 세 번째 호출은 이미 빈 자리를 집는다. - -## 두 번째를 단언하는 유일한 줄은 손으로 만든 값이다 - -레인 안에서 `pending()` 을 단언하는 줄은 `PoolPressureContractTest:31` 하나다. - -그 시험 `:29` 가 `new PoolMeasurement(4, 2, 3, Duration.ofMillis(80))` 을 만든다. `:31` 이 `pending()` 이 3 인지, `:32` 가 지연이 80 밀리초인지, `:33` 이 `total()` 이 6 인지, `:34` 가 `saturated()` 가 참인지 확인한다. - -앞의 둘은 생성자에 넣은 값을 그대로 되읽는다. 뒤의 둘도 새로운 것을 보지 않는다. `PoolMeasurement:26`\~`:28` 의 `total()` 이 `active + idle` 이고 `:31`\~`:33` 의 `saturated()` 가 `pending > 0` 이므로, `4 + 2 = 6` 과 `3 > 0` 을 확인하는 것이다. - -`HikariDataSource` 도 데이터베이스도 이 시험에는 없다. - -## 실제 풀에서 읽은 대기 수는 단언되지 않는다 - -`HikariPoolSaturationContractTest:92` 의 `measurementReportsPoolState` 는 실제 풀을 띄우고 `:94` 에서 커넥션 하나를 쥔다. - -`:95`\~`:101` 이 `HikariPoolMXBean` 에서 활성 수와 유휴 수와 `getThreadsAwaitingConnection()` 을 읽어 `PoolMeasurement` 를 만든다. - -`:103`\~`:105` 의 단언은 셋이다. `active()` 가 1 인지, `total()` 이 1 이상인지, 쥔 커넥션이 유효한지다. `pending()` 은 없다. - -그 풀은 포화 상태도 아니다. `:32` 의 `POOL_SIZE` 가 2 인데 커넥션 하나만 쥐었으므로 기다리는 스레드가 생길 수 없다. - -`PoolMeasurement:9`\~`:11` 자바독이 이 함정을 직접 적는다. 표본을 뜨는 순간에 대기 수가 0 으로 보이면서도 호출자들이 늘 기다리는 풀이 있을 수 있다는 것이다. - -## 이 결함이 있는 곳 - -`PoolMeasurement` 는 `testkit` 소스 세트의 타입이고, 두 시험 파일만 그것을 `import` 한다. - -프로덕션에서 `getThreadsAwaitingConnection` 이나 `HikariPoolMXBean` 을 읽는 자리는 main 파일 4714 개에 0 이다. 런타임이 대기 수를 잘못 다루는 것이 아니라, 레인이 검사한다고 적은 것을 검사하지 않는다. - -## 원문에 없는 것 - -원문은 야간 워크플로가 광고하는 셋 중 하나에 대한 단언이 레인 전체에 없다고 적는다. 여기에 더한 것은 그 주장에 가장 가까운 두 자리가 각각 어떻게 비껴가는지와, 나머지 두 주장에도 같은 모양이 섞여 있다는 점이다. - -`PoolPressureContractTest:31` 은 값을 손으로 넣고 되읽고, `HikariPoolSaturationContractTest:100` 은 실제 값을 읽어 담고서 그것만 빼고 단언한다. 그리고 첫 번째 주장 쪽 `RequiresNewPoolPressureContractTest:80` 과 `PoolPressureContractTest:39` 는 리터럴 산술의 결과를 리터럴과 견주는 시험이다. - -## 확인하지 못한 것 - -`jpaPlatformPoolContractTest` 를 돌리지 않았다. 여덟 시험의 통과 여부는 코드만 읽고 판단하지 않았다. - -커넥션 둘을 모두 쥔 상태에서 그 MXBean 이 무엇을 돌려주는지 직접 재지 않았다. - -계수 범위를 레인으로 한정했다. 저장소 전체에는 `src/test` 의 `PoolMeasurementTest` 가 같은 단언을 한 번 더 갖고 있고, 그쪽까지 포함해 전수로 따지지는 않았다. - -레인을 실제로 돌려 여덟 시험이 통과하는지 보지 않았다. - -`getThreadsAwaitingConnection()` 이 포화 상태에서 어떤 값을 내는지 띄워서 재지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f034.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f034.md deleted file mode 100644 index 47657fe..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f034.md +++ /dev/null @@ -1,136 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a05-f034 -title: 세 카드의 태그를 채우는 시험이 프로덕션 타입을 한 줄도 부르지 않는다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a05-f034 -evidenceCapturedOn: 2026-09-04 -body: case-analysis-finding-a05-f034.body.md -assets: - - key: analysis-finding-a05-f034 - file: ../../../final/evidence/rendered/analysis-finding-a05-f034.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a05-f034.txt -source: - - 원본 분석 절은 final/document.md#a05 §133 이다. ---- - -# 세 카드의 태그를 채우는 시험이 프로덕션 타입을 한 줄도 부르지 않는다 - -`readiness-cards.yaml` 의 `selected` 카드 일곱 중 셋이 `postgresqlIntegrationTest` 의 시험을 시나리오로 지목하는데, 그 세 파일은 `dev.caskeleton` 을 `import` 하는 줄이 0 이다. 대조로 센 `jpa-transaction-runtime` 의 시험은 15 줄을 `import` 한다. - -## 관계 - -- **증거 등급과 provenance — R1과 R2를 가르는 것** - 그 등급의 조건은 결과가 어디서 왔는지를 보지 무엇을 지났는지를 보지 않는다. 시나리오가 지목한 시험이 프로덕션 코드를 부르는지는 R1 에서도 R2 에서도 검사되지 않는다. -- **빠뜨림이 통과가 되는 게이트는 게이트가 아니다** - 여기서는 빠뜨림이 아니라 지목이 문제다. 태그마다 시나리오가 붙어 있어 레지스트리 검사는 통과하는데, 그 시나리오가 도는 것이 시험이 자기 `@BeforeAll` 로 만든 표다. -- **후보 증거는 통과해도 R1에 머무르고 R2는 별도 게이트가 판정한다** - 그 결정이 후보 검증에 맡긴 것은 건너뛴 시험과 스키마와 내용 해시와 선행 조건이다. 시나리오가 프로덕션 타입을 한 줄도 부르지 않는 것은 그 넷 중 어디에도 걸리지 않는다. - -## 문제 - -준비도 카드는 태그마다 그것을 덮는 시나리오나 task-claim 을 적고, 게이트가 그 selector 를 실제로 돌린다. - -그 selector 가 무엇을 지나는지 확인했다. - -## 결론 - -selected 일곱 모두에서 no-skip 이 비어 있는데, 이것은 결함이 아니라 설계다. build.gradle:1672~:1674 가 그 태그를 필수로 요구하면서 :1678~:1680 이 클레임 가능한 태그에서 빼고, jpa-evidence.gradle:680~:684 가 JUnit XML 의 실행·건너뜀·실패 건수로 그 태그를 채운다. - -jpa-primary-foundation 의 base-card-manifests 도 생성기가 채운다. 그 카드는 readiness-task 자체가 나머지 여섯을 모으는 롤업이라 시나리오가 0 인 것이 정상이고, support-task 는 다섯 걸려 있다. - -남는 것은 셋이다. jpa-aggregate-store 와 jpa-query-model 은 시나리오가 하나씩이고, jpa-observability-lifecycle 은 둘이다. - -그 셋이 지목하는 세 파일은 dev.caskeleton 을 import 하는 줄이 0 이다. import 없이 쓰는 같은 패키지 타입까지 잡으려고 대문자 이름을 전부 뽑아 다시 셌는데 그것도 0 이다. 대조로 센 PostgreSqlTransactionIntegrationTest 는 두 방식 모두 15 다. - -그 시험들이 읽고 쓰는 표는 자기 @BeforeAll 이 만든 것이다. readiness_aggregate 도 readiness_query 도 프로덕션 소스와 마이그레이션 4714 개 파일 어디에도 없다. - -세 번째 카드의 observability 는 표가 아니라 문자열이다. 견주는 쪽과 견주어지는 쪽이 모두 PostgreSqlLifecycleIntegrationTest 안에 있고, 그 리터럴 postgresql-primary 는 프로덕션에 0 건이다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 카드 전체 계수와 selected 카드의 태그·시나리오·task-claim·support-task 계수, 레지스트리가 덮을 수 없는 두 태그의 근거를 빌드 스크립트와 생성기에서 인용, 세 카드의 시나리오 selector 나열, 그 세 시험의 본문 인용, import 계수와 소스 세트 파일 수 대조, 표 이름과 문자열의 프로덕션 등장 계수와 훑은 파일 수 대조, 시나리오 일곱 카드의 import 전수 -소스 수정 : x - -## 재현 조건 - -1. 카드 전체를 상태별로 세고, selected 카드마다 필수 태그와 시나리오와 task-claim 과 support-task 를 센다. -2. no-skip 과 base-card-manifests 가 레지스트리 밖에서 채워지는 자리를 빌드 스크립트와 생성기에서 인용한다. -3. 남은 세 카드의 시나리오 selector 를 나열하고 그 시험 본문을 싣는다. -4. 세 파일의 dev.caskeleton import 줄을 세고, 파일이 쓰는 대문자 이름 가운데 프로덕션에 정의된 타입이 몇인지도 센다. -5. 두 표 이름과 postgresql-primary 가 프로덕션에 나오는지 세고 훑은 파일 수를 함께 센다. -6. 시나리오 일곱을 가진 카드의 시험이 무엇을 import 하는지 나열한다. - -## 본문 - - - -`readiness-cards.yaml` 은 카드마다 필수 증거 태그를 적고, 그 태그를 덮는 시나리오나 task-claim 을 함께 적는다. - -## 태그 커버리지에서 두 태그는 빼고 읽어야 한다 - -:::evidence key="analysis-finding-a05-f034" alt="저장소 루트에서 돌린 준비도 레지스트리 분석과 정적 검색 출력 237줄. 먼저 카드가 열일곱이고 implemented-candidate 여섯, not-implemented 넷, selected 일곱이라고 나온다. 그 일곱마다 필수 태그 수와 시나리오 수와 task-claim 수와 support-task 수와 레지스트리가 덮지 않는 태그가 나열되는데, jpa-observability-lifecycle 은 태그 넷에 시나리오 둘, jpa-security-baseline 은 태그 여섯에 시나리오 셋과 task-claim 하나, jpa-flyway-migration 은 태그 넷에 시나리오 넷, jpa-transaction-runtime 은 태그 넷에 시나리오 일곱, jpa-aggregate-store 와 jpa-query-model 은 각각 태그 넷에 시나리오 하나, jpa-primary-foundation 은 태그 넷에 시나리오 0 과 task-claim 둘과 support-task 다섯이다. 덮지 않는 태그는 여섯 카드가 no-skip 하나씩이고 jpa-primary-foundation 만 base-card-manifests 가 더 있다. 이어서 그 두 태그를 레지스트리가 덮을 수 없는 근거가 실린다. build.gradle 1672~1674번이 no-skip 을 필수 태그에 넣으라고 강제하고 1678~1680번이 클레임 가능한 태그 집합에서 no-skip 을 빼며, jpa-evidence.gradle 676~692번이 JUnit XML 의 실행 건수와 건너뜀과 실패와 오류 건수로 noSkipResult 를 계산해 no-skip 을 채우고, cardId 가 jpa-primary-foundation 이고 선행 조건이 모두 매니페스트를 가졌으면 base-card-manifests 를 채운다. 다음으로 남은 세 카드의 시나리오가 나열된다. jpa-aggregate-store 는 필수 태그 real-postgresql 과 mapping 과 optimistic-conflict 와 no-skip 에 시나리오 하나가 앞의 셋을 함께 덮고, jpa-query-model 도 keyset 과 query-plan 을 시나리오 하나가 함께 덮으며, jpa-observability-lifecycle 은 시나리오 둘이 각각 real-postgresql 과 lifecycle 을, observability 를 덮는다. 그 아래에 세 시험의 본문이 실린다. PostgreSqlAggregateIntegrationTest 는 19~30번 BeforeAll 이 Docker 가용성을 확인하고 create table readiness_aggregate 를 실행하며, 36~84번이 JDBC 로 UUID 와 Instant 를 넣고 다시 읽어 값을 견주고, 66~84번이 커넥션 둘을 열어 version 을 조건에 넣은 갱신을 각각 실행해 먼저 커밋한 쪽이 1 이고 두 번째가 0 임을 확인한다. PostgreSqlQueryIntegrationTest 는 22~35번이 readiness_query 표와 인덱스를 만들고 generate_series 로 천 행을 넣으며, 45~80번이 keyset 페이지를 읽고 70번에서 set enable_seqscan=off 를 실행한 뒤 72번 explain format json 의 계획에 인덱스 이름이 들어 있는지 78번에서 확인한다. PostgreSqlLifecycleIntegrationTest 는 51~66번이 크기 2 인 풀을 만들어 커넥션 둘을 쥐고 상태가 SATURATED 인지와 active 와 maximum 이 2 인지를 확인한 뒤 63~65번이 boundedTags 를 component 가 postgresql-primary 이고 state 가 saturated 인 맵과 견주는데, 107~119번의 private record 안 115~117번이 바로 그 맵을 만드는 자리다. 다음으로 세 시험이 dev.caskeleton 을 import 하는 줄이 각각 0 개이고 같은 소스 세트에 파일이 스물일곱이며, 이름 단위로 훑어도 세 파일이 쓰는 대문자 이름 중 프로덕션에 정의된 타입이 각각 0 개인데 대조로 건 PostgreSqlTransactionIntegrationTest 는 15 개라고 나온다. 두 시험이 쓰는 표 이름 readiness_aggregate 와 readiness_query 가 프로덕션 소스와 마이그레이션에 0 건이고 그 검색이 훑은 main 파일이 4714 개이며, postgresql-primary 도 프로덕션에 0 건이다. 마지막으로 대조 카드 jpa-transaction-runtime 의 시나리오 일곱이 각각 어떤 태그를 덮는지 나열되고, 그 시험이 import 하는 dev.caskeleton 타입 열다섯 중 여덟이 실린다." caption="카드 열일곱과 selected 일곱의 태그 커버리지 · no-skip 과 base-card-manifests 를 레지스트리 밖에서 채우는 자리 · 남은 세 카드의 시나리오 · 그 세 시험의 본문 · import 와 이름 단위 계수 대조 · 표 이름과 문자열의 프로덕션 등장 0 · 시나리오 일곱 카드의 대조 — 237줄 · exit 0" zoom="true" -::: - -카드는 열일곱이고 `selected` 가 일곱이다. 일곱 모두 `no-skip` 이 시나리오로도 task-claim 으로도 덮이지 않는다. - -그것이 설계다. `build.gradle:1672`\~`:1674` 가 모든 카드에 `no-skip` 을 필수 태그로 넣으라고 강제하고, `:1678`\~`:1680` 이 클레임 가능한 태그 집합을 만들 때 그 하나를 뺀다. 카드가 `no-skip` 을 덮겠다고 적으면 검증기가 거절한다. - -대신 `jpa-evidence.gradle:680`\~`:684` 가 매니페스트를 만들 때 JUnit XML 의 실행 건수와 건너뜀·실패·오류 건수를 읽어 `noSkipResult` 를 계산하고 그 태그를 채운다. - -`base-card-manifests` 는 `jpa-primary-foundation` 에만 있는 태그이고, `jpa-evidence.gradle:685`\~`:691` 이 그 카드의 선행 조건 여섯이 모두 매니페스트를 가졌을 때 채운다. `readiness-task` 가 `verifyJpaPrimaryFoundationEvidence` 이고 선행 조건이 나머지 여섯 base 카드 전부라, 이 카드는 자기 시나리오를 갖지 않는 쪽이 맞다. - -## 남는 세 카드 - -`jpa-aggregate-store` 는 시나리오 하나가 `real-postgresql` 과 `mapping` 과 `optimistic-conflict` 를 함께 덮는다. - -`jpa-query-model` 도 시나리오 하나가 `real-postgresql` 과 `keyset` 과 `query-plan` 을 함께 덮는다. - -`jpa-observability-lifecycle` 은 둘인데, 하나가 `real-postgresql` 과 `lifecycle` 을, 다른 하나가 `observability` 를 덮는다. - -## 그 시나리오들이 도는 것 - -`PostgreSqlAggregateIntegrationTest` 의 `@BeforeAll:19`\~`:30` 이 `assertDockerAvailable()` 뒤에 `create table readiness_aggregate` 를 실행한다. `:36`\~`:64` 가 `UUID` 와 `Instant` 를 JDBC 로 넣고 다시 읽어 값을 견준다. `:66`\~`:84` 는 커넥션 둘을 열어 `where id = ? and version = ?` 갱신을 각각 실행하고, 먼저 커밋한 쪽의 갱신 건수가 1 이고 두 번째가 0 인 것을 확인한 뒤 롤백한다. - -`optimistic-conflict` 를 덮는 것이 그 부분이다. `@Version` 이 아니라 손으로 쓴 조건부 갱신이고, 대상은 이 시험이 만든 표다. - -`PostgreSqlQueryIntegrationTest:22`\~`:35` 가 `readiness_query` 와 인덱스를 만들고 `generate_series` 로 천 행을 넣는다. `:45`\~`:68` 이 keyset 페이지를 읽고, `:70` 이 `set enable_seqscan=off` 를 실행한 뒤 `:72` 의 `explain (format json)` 계획에 인덱스 이름이 들어 있는지를 `:78` 에서 확인한다. - -`query-plan` 태그는 실제 EXPLAIN 계획을 본다. 다만 대안을 끈 뒤에 본 것이고, 대상은 역시 이 시험이 만든 표다. - -`PostgreSqlLifecycleIntegrationTest:51`\~`:66` 은 크기 2 인 풀에 커넥션 둘을 쥐고 상태와 `active` 와 `maximum` 을 확인한다. `observability` 를 덮는 것은 `:63`\~`:65` 의 `boundedTags()` 비교인데, 그 맵을 만드는 `:115`\~`:117` 이 같은 파일 private record 안에 있고 리터럴도 같은 파일에 있다. - -## 세 파일 모두 프로덕션 타입에 닿지 않는다 - -`dev.caskeleton` 을 `import` 하는 줄이 세 파일 다 0 이다. 같은 패키지 타입은 `import` 없이 쓸 수 있으므로 파일이 쓰는 대문자 이름을 전부 뽑아 프로덕션에 같은 이름의 `.java` 가 있는지도 셌는데, 그것도 셋 다 0 이다. 같은 소스 세트에 파일이 스물일곱 있다. - -표 이름 둘과 `boundedTags()` 가 견주는 `postgresql-primary` 를 프로덕션 소스와 마이그레이션에서 찾으면 셋 다 0 건이다. 그 검색이 훑은 main 파일은 4714 개다. - -## 시나리오 일곱을 가진 카드는 다르다 - -`jpa-transaction-runtime` 은 태그 넷에 시나리오 일곱이고, 하나가 여러 태그를 겸하는 대신 태그마다 여러 시나리오가 붙는다. - -그 시나리오가 가리키는 `PostgreSqlTransactionIntegrationTest` 는 `dev.caskeleton` 을 15 줄 `import` 한다. `PersistenceExceptionTranslator`, `StandardSqlStateErrorMapping`, `PostgreSqlLocalTimeoutConfigurer`, `PostgreSqlSqlStateErrorMapping`, `JpaTransactionSettings`, `SpringTransactionPort` 같은 것들이다. 이름 단위로 세도 15 다. - -## 원문에 없는 것 - -원문은 이 세 카드의 시나리오가 프로덕션 경로를 지나지 않는다고 적는다. 여기에 더한 것은 그 판정을 남기기 위해 무엇을 빼야 하는지다. - -`no-skip` 은 일곱 카드 모두에서 비어 있지만 레지스트리가 덮을 수 없는 태그이고, `jpa-primary-foundation` 의 시나리오 0 은 롤업 카드의 정상 상태다. 둘을 빼고 나면 남는 것이 셋이고, 그 셋에 대해서는 `import` 계수와 이름 단위 계수가 같은 답을 낸다. - -## 확인하지 못한 것 - -매니페스트를 생성해 등급이 무엇으로 찍히는지 보지 않았다. - -이름 단위 검사의 판정 기준은 프로덕션 트리에 같은 이름의 `.java` 가 있는지다. 시험 전용 타입과 이름이 겹치면 과대 계수될 수 있다. - -`verifyJpaCandidateEvidence` 가 이 세 카드에 어떤 블로커를 붙이는지 빌드를 돌려 보지 않았다. - -`verifyJpaCandidateEvidence` 가 이 세 카드에 어떤 블로커를 붙이는지 빌드를 돌려 보지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f002.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f002.md deleted file mode 100644 index 6479def..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f002.md +++ /dev/null @@ -1,148 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a06-f002 -title: 가드가 막겠다는 문장이 실제로 있는 두 파일이 탐색 범위 밖이다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a06-f002 -evidenceCapturedOn: 2026-09-04 -body: case-analysis-finding-a06-f002.body.md -assets: - - key: analysis-finding-a06-f002 - file: ../../../final/evidence/rendered/analysis-finding-a06-f002.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a06-f002.txt -source: - - 원본 분석 절은 final/document.md#a06 §5 이다. ---- - -# 가드가 막겠다는 문장이 실제로 있는 두 파일이 탐색 범위 밖이다 - -`MongoNamespaceContractTest:25`~`:26` 은 결함을 "문서가 운영자에게 폐기된 키를 쓰라고 말하는 것" 으로 정의한다. 그 가드의 탐색 범위는 두 리프 아래 `/src/main/` 경로의 `.java`·`.yml`·`.properties` 뿐이고, 그 정의에 정확히 들어맞는 `README.md:37` 과 `CLAUDE.md:25` 는 확장자로도 경로로도 걸리지 않는다. - -## 관계 - -- **빠뜨림이 통과가 되는 게이트는 게이트가 아니다** - 그 규칙이 요구하는 것을 이 가드는 자바 소스 쪽에서만 지킨다. `:37`~`:39` 가 목록이 비었으면 실패하는데, `.yml` 을 보는 쪽에는 같은 단언이 없다. -- **문서와 상수가 서로 일치하는 것으로는 아무것도 증명되지 않는다** - 그 규칙이 요구하는 검사 경계 명시가 이 가드에 없다. 무엇이 훑이고 무엇이 빠지는지 어디에도 적혀 있지 않아, 초록불이 저장소의 어떤 문서도 폐기된 키를 안내하지 않는다는 뜻으로 읽힌다. -- **과대 진술 문서를 과소보다 먼저 고친다** - 같은 안내가 `README.md:37` 과 `:53` 과 `CLAUDE.md:25` 셋에 있다. 하나만 고치면 나머지 둘이 남는다. - -## 문제 - -이 모듈에는 폐기된 프로퍼티 이름의 재유입을 막는 계약 시험이 있다. 그 클래스 자바독에 무엇을 결함으로 볼지가 한 문장으로 적혀 있다. - -그 정의에 해당하는 자리를 탐색 범위가 덮는지 확인했다. - -## 결론 - -범위는 :72~:90 이 만든다. repositoryRoot():100~:110 이 src/config/architecture/modules.json 을 찾아 위로 올라가며 정한 루트 아래 adapter/outbound/persistence-mongo 와 app-bootstrap 두 리프를 훑고, 확장자가 맞고 경로에 /src/main/ 이 있고 /build/ 가 없는 파일만 남긴다. - -시험은 둘이다. :34 가 주석을 뗀 자바 소스에 그 키가 없는지 보고, :56 이 출하되는 .yml 과 .properties 에 없는지 본다. :37~:39 는 목록이 비었으면 검색이 헛돈 것이라며 그것부터 단언한다. :56 의 자원 시험에는 같은 단언이 없다. - -그 키가 있는 자리는 시험 소스를 빼면 여섯이다. 설계 계획 문서 셋과 persistence-mongo 의 CLAUDE.md 와 README.md 와 MongoPersistenceSettings.java 다. - -여섯 중 자바 파일 하나만 조건을 통과한다. 그 파일에서 키가 나오는 곳은 :15 이고 자바독 안이며, 이 자바독이 예전에 그 키를 가리켰다는 기록이다. 주석을 떼고 보는 규칙이 정확히 이런 문장을 위해 있다. - -훑는 대상이 없는 것은 아니다. 두 리프의 /src/main/ 아래 .java 와 app-bootstrap 의 .yml 넷을 실제로 읽고, 그 안에서 주석을 떼고 나면 걸 것이 남지 않는다. - -나머지 다섯은 범위 밖이다. 그중 둘이 이 모듈의 운영자용 문서다. - -README.md:37 은 properties 코드 블록 안에 그 키의 완전한 설정 한 줄을 적어 둔다. :53 과 CLAUDE.md:25 는 URI 와 데이터베이스가 그 네임스페이스에서 온다고 산문으로 적는다. - -즉 자바독이 정의한 결함 — 문서가 운영자에게 폐기된 키를 쓰라고 말하는 것 — 이 지금 그 모듈의 두 문서에 그대로 있고, 그것을 막으려고 만든 가드의 범위가 그 둘에 닿지 않는다. - -시험 소스에는 그 키를 산문이 아니라 실제로 바인딩하는 자리도 둘 있다. MongoPersistenceConfigTest:20 과 :64 의 withPropertyValues 다. /src/main/ 조건이 이쪽도 함께 걸러 낸다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 가드의 클래스 자바독과 두 시험과 탐색 범위 코드 인용, 폐기된 키가 있는 자리를 시험 소스를 빼고 전수 검색, 각 자리가 그 범위에 드는지 확장자와 경로로 판정, 범위 안 자바 파일에서 그 키가 놓인 줄 인용, 운영자용 두 문서의 해당 줄과 앞뒤 인용 -소스 수정 : x - -## 재현 조건 - -1. 가드의 클래스 자바독에서 막으려는 결함의 정의를 인용한다. -2. 두 시험이 각각 무엇을 보는지, 그리고 자기 검증을 어떻게 하는지 인용한다. -3. 탐색 범위를 만드는 코드를 인용한다. -4. 폐기된 키가 있는 자리를 시험 소스를 빼고 전부 찾는다. -5. 각 자리가 그 범위의 확장자와 경로 조건에 드는지 판정한다. -6. 범위 안에 든 파일에서 그 키가 놓인 줄과, 범위 밖 운영자용 문서의 해당 줄을 인용한다. - -## 본문 - - - -`MongoNamespaceContractTest` 는 폐기된 Mongo 프로퍼티 이름이 되돌아오는 것을 막는다. 클래스 자바독이 그 결함을 한 문장으로 정의한다. - -## 가드가 정의한 결함 - -:::evidence key="analysis-finding-a06-f002" alt="저장소 루트에서 돌린 정적 검색 출력 162줄. 먼저 MongoNamespaceContractTest 14~31번 줄이 실린다. 17~18번 자바독은 spring.data.mongodb 로 시작하는 키가 스프링 부트 4 메타데이터에서 오류 수준으로 폐기됐고 정본이 spring.mongodb 라고 적고, 18~22번은 런타임이 한 번도 틀린 쪽에 있지 않았으며 모든 Compose 레인이 SPRING_MONGODB_URI 를 줬는데 MongoPersistenceSettings 의 자바독이 운영자를 폐기된 키로 안내했고 자바독이 그것을 그 표류가 앉기에 가장 나쁜 자리라고 적는다고 옮긴다. 24~26번이 결함을 정의하는데 주석은 제거하고 보며 옛 네임스페이스가 폐기됐다고 기록한 문장은 결함의 반대이고 결함은 문서가 운영자에게 그것을 쓰라고 말하는 것이며, 자원 파일은 통째로 본다고 적는다. 30번이 RETIRED_NAMESPACE 상수다. 이어서 32~65번의 두 시험이 실린다. 34번 시험은 주석을 뗀 자바 소스를 훑고 37~39번이 그 목록이 비지 않았는지 먼저 단언하며, 56번 시험은 yml 과 properties 를 통째로 보는데 같은 단언이 없다. 다음으로 67~110번의 범위 코드가 실린다. 68~70번 withoutJavaComments 가 블록 주석과 줄 주석을 지우고, 72~90번 productionSources 가 73번에서 repositoryRoot 아래 src 를 루트로 잡아 74번의 두 리프를 훑으며 81번이 확장자로, 82번이 경로에 /src/main/ 이 있는지로, 83번이 /build/ 를 빼는 것으로 거른다. 100~110번 repositoryRoot 는 현재 디렉터리에서 src/config/architecture/modules.json 이 나올 때까지 부모로 올라가고 못 찾으면 107번이 예외를 던진다. 이어서 시험 소스를 뺀 그 키의 등장 자리가 나오는데 docs/superpowers 아래 계획과 증거와 설계 문서 셋, persistence-mongo 의 CLAUDE.md 와 README.md, 그리고 MongoPersistenceSettings.java 여섯이다. 그중 범위 안은 마지막 하나뿐이고 나머지 다섯은 범위 밖이다. 다음으로 MongoPersistenceSettings 5~22번이 실리는데 9~12번이 연결 URI 를 여기서 모델링하지 않고 스프링 자신의 spring.mongodb.uri 에서 읽으며 이 클래스는 모듈의 opt-in 스위치만 소유한다고 적고, 14~19번이 정본 네임스페이스와 폐기된 쪽을 대비하면서 이 자바독이 폐기된 쪽을 가리켰던 것이 왜 나쁜 자리였는지와 MongoNamespaceContractTest 가 되돌아가는 것을 막는다고 적는다. 그 키가 나오는 줄은 15번 하나이고 자바독 안이다. 이어서 운영자가 읽는 두 문서가 실린다. README.md 35~38번은 properties 코드 블록인데 36번이 모듈 스위치를 켜고 37번이 spring.data.mongodb.uri 를 mongodb://localhost:27017/portfolio 로 적으며, 52~54번은 URI 와 데이터베이스와 자격증명이 그 표준 설정을 쓴다고 적고, CLAUDE.md 24~26번도 연결 URI 와 데이터베이스가 그 설정에서 온다고 적는다. 마지막으로 시험 소스에 그 키가 나오는 자리가 실리는데, MongoNamespaceContractTest 17번과 30번은 자바독과 상수이고 MongoPersistenceConfigTest 20번과 64번은 withPropertyValues 로 spring.data.mongodb.database 를 실제로 바인딩한다." caption="가드가 결함을 정의하는 자바독 · 두 시험과 자바 소스 쪽에만 있는 자기 검증 · 범위를 만드는 코드와 저장소 루트 탐색 · 그 키가 남은 여섯 파일과 범위 판정 · 범위 안 하나가 자바독인 것 · 범위 밖 두 문서의 원문 · 시험 소스의 실제 바인딩 둘 — 162줄 · exit 0" zoom="true" -::: - -`:17`\~`:18` 은 `spring.data.mongodb.*` 가 스프링 부트 4 메타데이터에서 오류 수준으로 폐기됐고 정본이 `spring.mongodb.*` 라고 적는다. - -`:18`\~`:22` 는 런타임이 한 번도 틀린 쪽에 있지 않았다고 적는다. 모든 Compose 레인이 `SPRING_MONGODB_URI` 를 준다. 뒤처진 것은 문서 쪽이다. 스위치를 소유한 클래스를 읽은 운영자가 거기 적힌 프로퍼티를 설정하면, 고르지 않은 폐기를 자기가 고르지 않은 채 물려받는다고 자바독은 적는다. - -`:24`\~`:26` 이 결함을 정의한다. 주석은 제거하고 보는데, 옛 네임스페이스가 폐기됐다고 기록한 문장은 결함의 반대이기 때문이다. 결함은 문서가 운영자에게 그것을 쓰라고 말하는 것이다. 자원 파일은 통째로 보는데, YAML 안의 키는 주석일 수 없기 때문이다. - -## 두 시험과 자바 소스 쪽에만 있는 자기 검증 - -`:34` 의 `noProductionSourceNamesTheDeprecatedNamespace` 는 주석을 뗀 자바 소스를 훑는다. `:38`\~`:39` 가 목록이 비지 않았는지 먼저 단언하는데, 아무 소스에도 닿지 않은 검색은 모든 참조를 없다고 보고하기 때문이다. - -`:56` 의 `noShippedResourceBindsTheDeprecatedNamespace` 는 `.yml` 과 `.properties` 를 통째로 본다. 이쪽에는 목록이 비지 않았는지 보는 단언이 없다. 자원 스캔이 0 개를 훑어도 조용히 통과한다. - -## 탐색 범위 - -`:72`\~`:90` 의 `productionSources` 가 범위를 만든다. `:73` 이 `repositoryRoot()` 아래 `src` 를 루트로 잡고, `:74` 가 `adapter/outbound/persistence-mongo` 와 `app-bootstrap` 둘을 훑으며, `:81` 이 확장자로, `:82` 가 경로에 `/src/main/` 이 있는지로, `:83` 이 `/build/` 를 빼는 것으로 거른다. - -`repositoryRoot():100`\~`:110` 은 현재 작업 디렉터리에서 `src/config/architecture/modules.json` 이 나올 때까지 부모로 올라간다. 못 찾으면 `:107` 이 예외를 던진다. - -확장자는 두 시험이 넘기는 `.java` 와 `.yml` 과 `.properties` 셋이다. - -## 그 키가 남아 있는 여섯 파일 - -시험 소스를 빼면 여섯이다. `docs/superpowers` 아래 계획·증거·설계 문서 셋, `persistence-mongo/CLAUDE.md`, `persistence-mongo/README.md`, 그리고 `MongoPersistenceSettings.java` 다. - -범위 안에 드는 것은 마지막 하나다. 나머지 다섯은 확장자가 `.md` 이거나 경로에 `/src/main/` 이 없다. 두 문서는 모듈 루트에 있으므로 둘 다에 해당한다. - -## 범위 안에 든 하나는 MongoPersistenceSettings 의 자바독이다 - -`MongoPersistenceSettings:5`\~`:22` 에서 그 키가 나오는 줄은 `:15` 하나다. `:9`\~`:12` 가 연결 URI 를 여기서 모델링하지 않고 스프링 자신의 `spring.mongodb.uri` 에서 읽는다고 적고, 이 클래스는 모듈의 opt-in 스위치만 소유한다고 적는다. 자바독 안이고, 이 자바독이 폐기된 쪽을 가리켰던 과거와 그것이 왜 나쁜 자리였는지를 기록한다. `:19` 는 그 계약 시험이 되돌아가는 것을 막는다고도 적는다. - -가드가 주석을 떼고 보는 이유가 이 문장이다. - -## 범위 밖 두 문서는 결함의 정의에 그대로 들어맞는다 - -`README.md:35`\~`:38` 은 `properties` 코드 블록이다. `:36` 이 모듈 스위치를 켜고 `:37` 이 `spring.data.mongodb.uri=mongodb://localhost:27017/portfolio` 를 적는다. 그대로 옮겨 붙일 수 있는 완전한 한 줄이다. - -`:52`\~`:54` 는 URI 와 데이터베이스와 자격증명이 표준 `spring.data.mongodb.*` 설정을 쓴다고 적는다. - -`CLAUDE.md:24`\~`:26` 도 연결 URI 와 데이터베이스가 그 설정에서 온다고 적는다. - -셋 다 폐기 사실을 기록하는 문장이 아니라 그 키를 쓰라는 안내다. - -## 시험 소스에는 실제 바인딩이 둘 있다 - -`MongoPersistenceConfigTest:20` 이 `withPropertyValues("spring.data.mongodb.database=portfolio")` 를 부르고, `:64` 가 같은 키를 모듈 스위치와 함께 넘긴다. - -산문이 아니라 실제 프로퍼티 바인딩이다. 범위가 `/src/main/` 을 요구하므로 이쪽도 두 단언에 걸리지 않는다. - -## 원문에 없는 것 - -원문은 가드의 탐색 범위가 운영자가 읽는 두 문서를 덮지 않는다고 적는다. 여기에 더한 것은 범위 안에 남은 것이 무엇이고 범위 밖에 또 무엇이 있는지다. - -범위 안의 유일한 등장 `MongoPersistenceSettings:15` 는 이미 고쳐져 기록만 남은 문장이고 `withoutJavaComments` 가 지운다. 그래서 두 단언이 지금 걸 수 있는 문장은 범위 안에 하나도 없다. 범위 밖에는 문서 둘 말고도 시험 소스의 실제 바인딩 둘이 더 있다. - -## 확인하지 못한 것 - -그 안내를 따라 폐기된 키를 넣은 배포가 실제로 있었는지는 저장소 밖의 일이다. - -확장자 조건에 마크다운을 더하면 계획 문서 셋도 걸릴 텐데, 그것을 가르는 방법은 생각해 두지 않았다. - -그 계약 시험을 돌리지 않았다. 단언과 범위 코드를 읽는 데까지다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f004.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f004.md deleted file mode 100644 index c70f5ac..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f004.md +++ /dev/null @@ -1,176 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a06-f004 -title: 전용 category 를 붙이는 유일한 분기가 도달할 수 없다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a06-f004 -evidenceCapturedOn: 2026-09-04 -body: case-analysis-finding-a06-f004.body.md -assets: - - key: analysis-finding-a06-f004 - file: ../../../final/evidence/rendered/analysis-finding-a06-f004.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a06-f004.txt -source: - - 원본 분석 절은 final/document.md#a06 §16 이다. ---- - -# 전용 category 를 붙이는 유일한 분기가 도달할 수 없다 - -`MongoFailureCategory:68` 에 `SCHEMA_VERSION_UNSUPPORTED` 가 있고 `MongoDataSchemaUnsupportedException:42`~`:53` 이 세 버전을 공개 접근자로 노출한다. 그 값을 쓰는 자리는 저장소 전체에 열거형 선언과 `DefaultMongoFailureTranslator:110` 의 `case` 둘뿐인데, 분류기가 그 값을 만들지 않으므로 그 `case` 는 돌지 않는다. - -## 관계 - -- **모르는 것은 성공도 실패도 아닌 세 번째 결과여야 한다** - 그 규칙은 한번 접힌 정보가 복구되지 않는다고 적는다. 여기서는 스키마 버전 거절이 `OPERATION_REJECTED` 로 접혀 다른 로컬 거절과 구별되지 않는다. -- **위험한 조합은 정책이 아니라 생성자가 거부하게 만든다** - 그 규칙대로면 버전을 모르는 예외는 만들어질 수 없어야 한다. `MongoDataSchemaUnsupportedException` 의 생성자는 `-1` 셋을 그대로 받고, `DefaultMongoFailureTranslator:111` 이 그 값으로 예외를 만든다. -- **중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다** - 같은 예외를 만드는 자리가 둘인데 조립 상태가 반대다. `DefaultMongoFailureTranslator` 는 `MongoPlatformAutoConfiguration:93` 이 빈으로 만들지만 그 `case` 가 실행되지 않고, `MongoSchemaVersionPolicy` 는 값을 아는 쪽인데 `new` 를 부르는 main 줄이 0 이다. - -## 문제 - -이 실패에는 열거형 값 하나와 예외 타입 하나가 따로 마련돼 있다. 예외는 세 정수를 접근자로 내주므로 분기할 범주와 진단할 값을 함께 줄 수 있다. - -그 어휘가 실제로 쓰이는지 확인했다. - -## 결론 - -MongoFailureCategory:3~:9 는 이 값이 메트릭과 대시보드에 나타난다고 적는다. 그래서 알 수 없는 서버 코드는 새 범주를 만드는 대신 UNCLASSIFIED 로 간다. - -new MongoDataSchemaUnsupportedException 을 부르는 main 자리는 둘이다. - -MongoSchemaVersionPolicy:85 가 실제 버전 값 셋을 넘긴다. version.value() 와 range.minimumSupported().value() 와 range.current().value() 다. - -그런데 그 예외가 들고 가는 context 는 :84 의 MongoFailureContext.rejected(...) 가 만든 것이다. MongoFailureContext:135~:148 이 그 안에 OPERATION_REJECTED 와 NOT_SENT 를 박는다. 전용 범주가 아니라 로컬 거절 범주다. - -DefaultMongoFailureTranslator:111 이 전용 범주를 붙이는 유일한 자리인데 세 값 모두 -1 이다. - -그런데 그 분기에 들어올 값이 없다. SCHEMA_VERSION_UNSUPPORTED 라는 이름이 저장소 전체에 두 줄만 있고 그 둘이 방금 인용한 선언과 case 이므로, 그 값을 컨텍스트에 넣는 코드가 존재하지 않는다. 같은 검색을 OPERATION_REJECTED 로 걸면 넷이 나온다. - -분류기가 범주를 정하는 자리는 DefaultMongoFailureClassifier 안에 여럿이다. :88~:96 이 드라이버 라벨을 먼저 보고, :99 와 :106~:109 와 :112 가 단계와 결과에 따라 고르며, 마지막이 :156 의 CATEGORY_BY_CODE 조회다. 어느 자리에서도 이 범주가 나오지 않는다. - -그래서 SCHEMA_VERSION_UNSUPPORTED 로 집계되는 실패가 생기지 않는다. 다른 범주로 새는 것도 아직 없다. new MongoSchemaVersionPolicy 를 부르는 main 줄이 0 이고 그 이름이 main 에 나오는 자리가 자기 선언 셋뿐이라, 이 리비전에서는 그 예외를 던지는 코드가 돌지 않는다. 시험만 여섯 번 만든다. - -MongoSchemaVersionPolicyTest 가 그 어긋남을 잡지 못한다. 시험 여섯이 전부 던져진 타입이나 반환값만 보고, 범주와 세 접근자를 확인하는 줄이 하나도 없다. - -그래서 이 결함은 채택자가 정책을 배선하는 순간에 드러난다. 전용 칸이 비어 있는 채로 거절만 다른 칸에 쌓이기 시작한다. - -이 모듈에 살아 있는 다른 스키마 실패 경로는 세 번째 범주를 쓴다. MongoFailureContext.schemaMismatch:157 이 SCHEMA_VALIDATION 과 NO_WRITE_PERFORMED 를 박고, PolicyAwareMongoTypeMapper:112·:185·:192 가 그것을 부른다. 저장된 문서가 이 릴리스가 기대하는 스키마와 맞지 않을 때 실제로 도는 것이 그 경로다. - --1 을 채워 넣는 자리가 하나 더 있다. DefaultMongoFailureTranslator:106 의 MongoDocumentTooLargeException 인데, 이쪽은 서버 코드 10334 와 17420 이 그 범주로 매핑돼 있어 실제로 만들어진다. 접근자 계약에 값이 없을 수 있다는 말이 없는 것만 남는다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 범주 열거형의 클래스 자바독과 두 상수 인용, 전용 예외 전문 인용, 프로덕션 생성 지점 전수, 정책이 만드는 context 와 그 팩터리가 박는 값 인용, translator 의 switch 전문 인용, 전용 범주가 나오는 줄의 저장소 전체 계수와 대조 계수, 분류기가 범주를 정하는 자리 전부 인용, 같은 모듈의 다른 스키마 실패 팩터리와 그 호출자 인용, 두 생성 지점의 조립 계수와 대조, 정책 시험의 단언 전수와 접근자 단언 계수와 대조 -소스 수정 : x - -## 재현 조건 - -1. 범주 열거형의 클래스 자바독과 이 값의 주석을 인용한다. -2. 전용 예외의 자바독과 세 접근자를 인용한다. -3. new MongoDataSchemaUnsupportedException 을 부르는 main 자리를 전부 찾는다. -4. 정책이 만드는 context 와 그 팩터리 메서드 본문을 인용한다. -5. translator 의 switch 를 전문으로 싣는다. -6. 전용 범주가 저장소 전체에 몇 줄 나오는지 세고, 다른 범주 이름으로 같은 검색을 건다. -7. 분류기가 범주를 정하는 자리를 전부 인용하고, 같은 모듈의 다른 스키마 실패 팩터리와 그 호출자를 함께 싣는다. -8. 정책 시험의 단언을 전부 뽑고 접근자를 단언하는 줄을 센다. - -## 본문 - - - -`MongoFailureCategory` 는 실패의 안정된 부모 범주다. 스키마 버전 실패를 위한 값이 그 안에 있다. - -## 이 실패에 마련된 값과 타입 - -:::evidence key="analysis-finding-a06-f004" alt="저장소 루트에서 돌린 정적 검색 출력 288줄. 먼저 MongoFailureCategory 3~10번 클래스 자바독이 실리는데 서버 코드와 드라이버 오류 라벨은 릴리스마다 바뀌지만 이 범주는 안 바뀌며 메트릭과 대시보드에 나타나는 값이 이것이고, 그래서 알 수 없는 서버 코드가 런타임에 새 범주를 만드는 대신 UNCLASSIFIED 로 간다고 적는다. 63~72번에 OPERATION_REJECTED 와 SCHEMA_VERSION_UNSUPPORTED 와 UNCLASSIFIED 세 상수가 각자 주석과 함께 나오는데, 68번의 주석은 저장된 문서의 스키마 버전이 지원 범위 밖이라는 것이다. 이어서 MongoDataSchemaUnsupportedException 5~11번 자바독이 실리는데 도메인 역직렬화 전에 올라오는 것이 이 예외의 가치이고 새 릴리스가 쓴 문서나 은퇴한 버전이 남긴 문서를 현재 모양으로 조용히 밀어 넣으면 안 되며 버전은 정수라 메시지에 이름을 적어도 안전하다고 적는다. 22~39번 생성자가 세 정수를 요구하고 stored schema version 뒤에 문서 버전과 지원 범위를 이어 붙인 메시지를 만들며, 41~54번이 documentVersion 과 minimumSupported 와 currentVersion 세 공개 접근자다. 다음으로 new MongoDataSchemaUnsupportedException 을 부르는 main 자리가 둘 나오는데 MongoSchemaVersionPolicy 85번과 DefaultMongoFailureTranslator 111번이다. 그 아래 MongoSchemaVersionPolicy 82~87번이 실리는데 83~84번이 MongoFailureContext.rejected 로 context 를 만들고 85~86번이 version.value 와 range.minimumSupported().value 와 range.current().value 를 넘긴다. 이어서 MongoFailureContext 135~149번의 rejected 팩터리가 실리는데 140번이 MongoFailureCategory.OPERATION_REJECTED 를, 141번이 MongoExecutionOutcome.NOT_SENT 를 박는다. 다음으로 DefaultMongoFailureTranslator 88~118번이 실린다. 91번이 classification.category() 로 switch 를 열고 106번이 MongoDocumentTooLargeException 을 -1L 과 -1L 로, 110~111번이 MongoDataSchemaUnsupportedException 을 -1 과 -1 과 -1 로 만들며, 112~114번 주석이 벌크 부분 실패와 로컬 거절은 그 세부를 소유한 계층이 올리는 것이라 그 범주를 달고 번역까지 온 실패는 이 릴리스가 알아보지 못하는 것이라고 적고 115~116번이 그 셋을 MongoUnclassifiedFailureException 으로 보낸다. 이어서 SCHEMA_VERSION_UNSUPPORTED 가 저장소 전체에 나오는 줄이 2 개라고 나오고 그 둘이 MongoFailureCategory 68번의 선언과 DefaultMongoFailureTranslator 110번의 case 로 나열되며, 대조로 센 OPERATION_REJECTED 는 4 개다. 그 아래 DefaultMongoFailureClassifier 24~52번의 CATEGORY_BY_CODE 지도가 실리는데 서버 코드 11000 부터 31 까지 스물여섯 항목이 DUPLICATE_KEY 와 SCHEMA_VALIDATION 과 WRITE_CONFLICT 와 WRITE_CONCERN 과 READ_CONCERN 과 SERVER_SELECTION 과 CONNECTION 과 TIMEOUT 과 CURSOR 와 DOCUMENT_TOO_LARGE 와 SHARD_ROUTING 과 RESUME 과 ENCRYPTION 으로 가고 스키마 버전 항목이 없다. 이어서 같은 파일 86~120번이 실리는데 86~87번 주석이 라벨을 먼저 보는 이유가 서버 자신의 복구 가능성 진술이 어떤 코드 표보다 우선하기 때문이라고 적고, 88~89번과 91~92번이 커밋 미상과 일시적 트랜잭션 라벨을 각각 처리하며, 94~96번이 쓰기 없음 라벨을, 99번이 categoryOf 를, 101~109번이 아무것도 보내지 않은 단계에서 UNCLASSIFIED 를 SERVER_SELECTION 으로 바꾸는 것을, 111~112번이 커밋 단계의 미상 결과를 처리한다. 150~157번의 categoryOf 는 서버 코드가 없으면 재시도 가능 쓰기 라벨 여부로 CONNECTION 이나 UNCLASSIFIED 를 돌려주고 있으면 그 지도를 조회하며 없으면 UNCLASSIFIED 다. 그 아래 같은 정규식을 MongoFailureContextTest 에 걸면 1 개가 나온다는 대조가 있다. 다음으로 MongoFailureContext 151~172번의 schemaMismatch 팩터리가 실린다. 152~155번 자바독은 저장된 문서가 이 릴리스가 기대하는 스키마와 맞지 않는 경우이고 쓴 것이 없지만 읽기가 서버에 닿았으며 돌아온 것이 쓸 수 없는 값이라 결과가 NOT_SENT 가 아니라 NO_WRITE_PERFORMED 라고 적으며, 162번이 SCHEMA_VALIDATION 을 163번이 NO_WRITE_PERFORMED 를 박는다. 그 팩터리를 부르는 자리는 PolicyAwareMongoTypeMapper 112번과 185번과 192번이다. 이어서 new MongoSchemaVersionPolicy 를 부르는 main 줄이 0 개라고 나오고 그 타입이 main 에 나오는 자리가 MongoSchemaVersionPolicy 17번의 클래스 선언과 22번과 29번의 두 생성자 셋뿐이며, 대조로 센 시험 쪽 생성은 6 개다. DefaultMongoFailureTranslator 를 빈으로 만드는 main 줄은 2 개인데 MongoPlatformAutoConfiguration 93번과 DefaultMongoFailureTranslator 47번의 정적 팩터리다. 다음으로 MongoDocumentTooLargeException 26~36번이 실리는데 28번 자바독이 추정 직렬화 크기이며 내용이 아니라 크기라 로그에 남겨도 안전하다고만 적고, 33번은 초과된 상한이라고만 적는다. 마지막으로 MongoSchemaVersionPolicyTest 의 시험 여섯과 그 단언이 나오는데 21번과 31번과 45번이 isInstanceOf(MongoDataSchemaUnsupportedException.class) 이고 나머지는 resolveMissingVersion 과 writeVersion 과 requiresReadTimeConversion 과 음수 버전 거절을 본다. 그 시험이 category 나 세 버전 접근자를 단언하는 줄은 0 개다." caption="범주 열거형의 대시보드 자바독과 두 상수 · 전용 예외의 자바독과 세 접근자 · 프로덕션 생성 지점 둘 · 정책이 만드는 context 와 rejected 팩터리가 박는 값 · translator 의 switch 전문 · 그 범주가 저장소 전체에 두 줄뿐인 것과 분류기의 범주 지도 · 두 생성 지점의 조립 상태 · 같은 모양의 DocumentTooLarge · 정책 시험의 단언과 접근자 단언 0 — 288줄 · exit 0" zoom="true" -::: - -`:3`\~`:9` 클래스 자바독이 이 열거형의 용도를 적는다. 릴리스마다 바뀌는 서버 코드와 드라이버 라벨 위에 얹는 고정된 값이고, 알 수 없는 서버 코드가 런타임에 새 범주를 만들어 메트릭 카디널리티를 터뜨리는 대신 `UNCLASSIFIED` 로 가는 이유가 그것이다. - -`:68` 의 `SCHEMA_VERSION_UNSUPPORTED` 는 저장된 문서의 스키마 버전이 지원 범위 밖이라는 뜻이다. 바로 위 `:65` 의 `OPERATION_REJECTED` 는 플랫폼이 서버에 닿기 전에 로컬에서 거절했다는 뜻이다. - -`MongoDataSchemaUnsupportedException:5`\~`:11` 은 이 예외가 도메인 역직렬화 전에 올라오는 것이 핵심이라고 적는다. 버전은 정수이고 데이터를 담지 않아서 메시지에 이름을 적어도 안전하다는 것도 함께 있다. - -`:22`\~`:39` 의 생성자가 그 셋을 요구하고 메시지를 조립한다. `:42`·`:47`·`:52` 가 `documentVersion()` 과 `minimumSupported()` 와 `currentVersion()` 이다. 분기할 범주와 진단할 값이 둘 다 준비돼 있다. - -## 생성 지점 둘 - -`new MongoDataSchemaUnsupportedException` 을 부르는 main 자리는 `MongoSchemaVersionPolicy:85` 와 `DefaultMongoFailureTranslator:111` 둘이다. - -## MongoSchemaVersionPolicy 가 넘기는 context 의 범주 - -`MongoSchemaVersionPolicy:85`\~`:86` 이 `version.value()` 와 `range.minimumSupported().value()` 와 `range.current().value()` 를 넘긴다. 진짜 값이다. - -그런데 `:83`\~`:84` 가 그 예외에 넣을 context 를 `MongoFailureContext.rejected(new MongoOperationName("schema.version-check"))` 로 만든다. - -`MongoFailureContext:135`\~`:148` 이 그 팩터리다. `:140` 이 범주를 `OPERATION_REJECTED` 로, `:141` 이 결과를 `NOT_SENT` 로 박는다. 인자로 받는 것은 연산 이름 하나뿐이라 다른 범주를 넣을 자리가 없다. - -## DefaultMongoFailureTranslator\:111 이 넣는 -1, -1, -1 - -`DefaultMongoFailureTranslator:91` 이 `classification.category()` 로 `switch` 를 연다. `:110`\~`:111` 이 그 범주를 받으면 `MongoDataSchemaUnsupportedException` 을 `-1, -1, -1` 로 만든다. - -여기서는 버전을 알 수 없다. 드라이버가 보고한 실패에서 문서의 스키마 버전을 읽을 방법이 없기 때문이다. - -## 그 case 에 들어오는 값이 없다 - -`SCHEMA_VERSION_UNSUPPORTED` 가 저장소 전체에 나오는 줄은 둘이다. `MongoFailureCategory:68` 의 선언과 `DefaultMongoFailureTranslator:110` 의 `case` 다. `OPERATION_REJECTED` 로 같은 검색을 걸면 넷이 나온다. 2 는 pathspec 이 아무것도 훑지 못해 나온 값이 아니다. - -분류기가 범주를 정하는 자리는 하나가 아니다. `:88`\~`:89` 와 `:91`\~`:92` 가 드라이버 라벨을 먼저 보고 커밋 미상이나 일시적 트랜잭션으로 보내고, `:94`\~`:96` 이 쓰기 없음 라벨을 처리하며, `:99` 가 `categoryOf` 를 부르고, `:106`\~`:109` 가 아무것도 보내지 않은 단계에서 `UNCLASSIFIED` 를 `SERVER_SELECTION` 으로 바꾸고, `:112` 가 커밋 단계의 미상 결과를 처리한다. - -마지막이 `:150`\~`:157` 의 `categoryOf` 다. 서버 코드가 없으면 재시도 가능 쓰기 라벨 여부로 `CONNECTION` 이나 `UNCLASSIFIED` 를 주고, 있으면 `CATEGORY_BY_CODE` 를 조회한다. 그 지도 `:27`\~`:52` 의 스물여섯 항목이 열세 범주로 나뉘는데 스키마 버전이 없다. - -어느 자리에서도 `SCHEMA_VERSION_UNSUPPORTED` 가 나오지 않는다. 그 이름이 저장소 전체에 두 줄뿐이라는 계수가 그것을 한 번에 말한다. - -그래서 `classification.category()` 가 `SCHEMA_VERSION_UNSUPPORTED` 인 경우가 생기지 않고, `-1, -1, -1` 예외는 만들어지지 않는다. - -## 두 생성 지점의 조립 상태가 반대다 - -`DefaultMongoFailureTranslator` 는 빈이다. `MongoPlatformAutoConfiguration:93` 이 분류기를 넣어 만들고, `DefaultMongoFailureTranslator:47` 에 기본 분류기를 쓰는 정적 팩터리도 있다. 조립돼 있는데 문제의 분기만 죽었다. - -`MongoSchemaVersionPolicy` 는 반대다. `new MongoSchemaVersionPolicy` 를 부르는 main 줄이 0 이고, 그 이름이 main 에 나오는 자리는 `:17` 의 클래스 선언과 `:22`·`:29` 의 두 생성자뿐이다. 같은 검색을 시험에 걸면 여섯이 나온다. - -그래서 이 리비전에서는 세 버전을 아는 예외가 아예 던져지지 않는다. 대시보드의 `SCHEMA_VERSION_UNSUPPORTED` 칸이 비는 것은 맞지만, `OPERATION_REJECTED` 칸으로 새는 것도 아직 없다. - -이 정책은 채택자가 쓰라고 공개된 타입이다. 배선하는 순간 스키마 버전 거절이 로컬 가드레일 거절과 같은 칸에 들어가고, 전용 칸은 계속 비어 있다. - -`MongoSchemaVersionPolicyTest` 의 `:21`·`:31`·`:45` 는 `isInstanceOf` 로 예외 타입만 확인한다. 나머지 시험은 `resolveMissingVersion` 과 `writeVersion` 과 `requiresReadTimeConversion` 과 음수 버전 거절을 본다. `category()` 나 세 접근자가 나오는 줄이 하나도 없다. - -같은 정규식을 `MongoFailureContextTest` 에 걸면 1 이 나온다. 0 은 그 정규식이 아무것도 못 잡아서 나온 값이 아니다. - -## 살아 있는 스키마 실패는 세 번째 범주로 간다 - -`MongoFailureContext:151`\~`:171` 에 `schemaMismatch` 팩터리가 있다. `:162` 가 `SCHEMA_VALIDATION` 을, `:163` 이 `NO_WRITE_PERFORMED` 를 박는다. `:154`\~`:155` 자바독이 그 결과를 고른 이유를 적는다 — 쓴 것이 없지만 읽기는 서버에 닿았고 돌아온 것이 쓸 수 없는 값이라는 것이다. - -부르는 자리는 `PolicyAwareMongoTypeMapper:112`·`:185`·`:192` 셋이다. 타입 매퍼가 문서를 도메인 타입으로 옮기다 실패할 때 도는 경로다. - -그래서 이 모듈의 스키마 관련 실패는 세 범주로 갈린다. 타입 매퍼가 쓰는 `SCHEMA_VALIDATION`, 조립되지 않은 정책이 쓰는 `OPERATION_REJECTED`, 그리고 아무도 만들지 않는 `SCHEMA_VERSION_UNSUPPORTED` 다. - -## -1 을 넣는 다른 예외 하나 - -`DefaultMongoFailureTranslator:106` 이 `MongoDocumentTooLargeException` 을 `-1L, -1L` 로 만든다. - -`MongoDocumentTooLargeException:28` 은 그 값을 추정 직렬화 크기라고, `:33` 은 초과된 상한이라고 적는다. 값이 없을 수 있다는 말은 없다. - -이쪽은 도달 가능하다. `CATEGORY_BY_CODE:46`\~`:47` 이 서버 코드 10334 와 17420 을 `DOCUMENT_TOO_LARGE` 로 보낸다. 드라이버가 보고한 실패에서 두 수를 알 수 없는 것은 어쩔 수 없지만, 접근자 계약에 그 사실이 없다. - -## 원문과 갈리는 자리 - -원문은 생성 지점이 둘이고 각각 반쪽만 맞다고 적으면서, 전용 범주 칸이 세 버전이 `-1` 인 실패만 받는다고 적는다. - -확인 결과는 다르다. 분류기가 그 범주를 만들지 않으므로 `-1` 예외조차 생기지 않고, `DefaultMongoFailureTranslator:110`\~`:111` 에는 값이 들어오지 않는다. 원문이 제안한 수정 — 정책이 전용 범주를 가진 context 를 만들면 translator 쪽 `-1` 경로를 정리할 수 있다 — 은 그대로 맞고, 정리 대상이 이미 실행되지 않는 `case` 라는 점만 다르다. - -## 확인하지 못한 것 - -다른 분류기 구현이 조립될 수 있는지는 조립 경로로 따지지 않았다. 도달 불가라는 판정은 이름 계수 하나에 기대고 있다. - -이 범주 이름을 소비하는 대시보드나 메트릭 설정은 저장소 밖에 있어 보지 않았다. - -`MongoSchemaVersionPolicy` 가 던진 예외를 잡는 호출자가 세 접근자를 읽어 진단에 쓰는지 추적하지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f005.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f005.md deleted file mode 100644 index 200ffce..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f005.md +++ /dev/null @@ -1,170 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a06-f005 -title: cause 를 받지 않는다는 루트 규칙이 스물둘 중 하나에 대해 거짓이다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a06-f005 -evidenceCapturedOn: 2026-09-04 -body: case-analysis-finding-a06-f005.body.md -assets: - - key: analysis-finding-a06-f005 - file: ../../../final/evidence/rendered/analysis-finding-a06-f005.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a06-f005.txt -source: - - 원본 분석 절은 final/document.md#a06 §17 이다. ---- - -# cause 를 받지 않는다는 루트 규칙이 스물둘 중 하나에 대해 거짓이다 - -루트 예외의 자바독은 생성자가 원인을 받지 않으므로 드라이버 예외가 다시 새지 않는다고 적는다. 상속 타입 스물둘 중 `MongoTimeoutException:25` 가 그 생성자를 갖고 있고, 생성자를 아예 지나지 않는 자리도 하나 있다 — `SpringMongoTransactionSessionFactory:194` 다. - -## 관계 - -- **관측을 위해 수집한 데이터가 관측 대상보다 위험할 수 있다** - `MongoPersistenceException` 의 두 번째 규칙이 그 규칙을 구현한다. 실패 컨텍스트가 일부러 버린 값이 원인 사슬로 되돌아오는 경로를 막으려는 것이다. -- **시그니처가 payload를 받지 않는데 예외 메시지로 PII가 로그에 남았다** - 그 기록에서는 실제로 값이 새어 로그에 남았다. `MongoTimeoutException:25` 가 받는 원인은 Reactor 자신의 타임아웃이라 문서도 질의도 자격증명도 담지 않는다. -- **문서와 상수가 서로 일치하는 것으로는 아무것도 증명되지 않는다** - 규칙과 검사가 같은 것을 말하는지의 문제다. `MongoFailureContextTest:58` 이 원인을 받는 생성자가 없는 타입을 골라 단언한다. - -## 문제 - -루트 예외의 자바독이 두 규칙을 세운다. 하나는 메시지가 담을 수 있는 것을 제한하고, 다른 하나는 원인 사슬 자체를 금지한다. - -그 두 번째 규칙이 하위 타입 전부에 대해 참인지 확인했다. - -## 결론 - -이 규칙이 걸리는 범위는 스물둘이고, 전부 final 이라 그 아래가 더 없다. - -그중 원인을 받는 생성자를 가진 것은 MongoTimeoutException 하나이고 :27 이 initCause 를 부른다. 검색이 351 개 파일을 훑었고 persistence-jpa 에 같은 것을 걸면 19 줄이 나온다. - -MongoTimeoutException:20~:24 의 자바독이 원인을 남기는 이유를 적는다. - -그 생성자를 부르는 자리는 DefaultReactiveMongoExecutor:152 하나다. 넘기는 값은 :146 이 잡은 Reactor 의 java.util.concurrent.TimeoutException 이고, 드라이버 예외는 :161 이후의 다른 블록에서 처리된다. - -:147~:150 주석이 감싸게 된 내력을 적는다. 예전 동작은 호출자에게 이름조차 알린 적 없는 타입이 아무 맥락 없이 도달하는 것이었고 실패가 기록되지도 않았다. - -어긋난 것은 루트 자바독의 문장이다. 하위 타입 자바독에는 원인을 남기는 이유가 적혀 있는데 루트에는 그 예외가 없어서, 규칙만 읽은 채택자가 잘못된 전제로 로깅 정책을 세울 수 있다. - -생성자만 훑어서는 잡히지 않는 자리가 하나 더 있다. :194 가 inFlight 에 정리 실패를 붙이는데, 그 필드는 :214·:220 에서 이 계층의 번역된 예외가 되고 붙는 값은 :169·:175 가 세션 중단과 닫기에서 잡은 드라이버 예외다. - -규칙의 문자는 지켜진다. 지켜지지 않는 것은 그 규칙이 든 두 통로 중 뒤쪽이다. - -검사도 이 두 자리를 비껴간다. 이 계층에서 getCause() 를 단언하는 시험 줄은 MongoFailureContextTest:65 하나인데, :60 이 고른 대상이 MongoTransactionCommitUnknownException 이다. 그 타입에는 원인을 받는 생성자가 없어서 :65 의 isNull() 이 무엇을 넣든 통과한다. 공교롭게도 그 타입이 SpringMongoTransactionSessionFactory:249 가 만드는 둘 중 하나여서, 억눌린 예외를 보는 단언이었다면 결과가 달랐을 수 있다. - -MongoTimeoutException 은 시험 소스에 이름조차 없다. 같은 검색이 다른 타입에 대해서는 4 줄을 찾는다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 루트 예외의 자바독과 생성자 인용, 하위 타입 계수와 전수 나열, 이 계층에서 원인을 받거나 설정하는 줄 계수와 전수와 옆 계층 대조, 그 하위 타입 전문 인용, 2 인자 생성자 호출처 전수와 그 자리 인용, addSuppressed 를 부르는 줄 전수와 그 세 자리 인용, getCause() 단언 줄 계수와 저장소 전체 대조, 규칙 검사 시험 인용, 두 타입의 시험 등장 계수 -소스 수정 : x - -## 재현 조건 - -1. 루트 예외의 자바독 두 규칙과 유일한 생성자를 인용한다. -2. MongoPersistenceException 을 상속하는 main 타입을 세고 전부 나열한다. -3. 이 계층에서 Throwable cause) 와 initCause 가 나오는 줄을 세고, 훑은 파일 수와 옆 계층 계수를 함께 낸다. -4. 규칙을 벗어난 타입을 전문으로 싣는다. -5. 그 타입의 생성자를 부르는 자리를 전부 찾고 DefaultReactiveMongoExecutor:152 의 본문을 인용한다. -6. 이 계층에서 addSuppressed 를 부르는 줄을 전부 찾고, 무엇이 무엇에 붙는지 정리 경로와 번역 경로를 함께 인용한다. -7. 이 계층에서 getCause() 를 단언하는 줄을 세고 저장소 전체와 대조한다. -8. 그 규칙을 검사하는 시험을 인용하고, 두 타입이 시험에 몇 줄 나오는지 센다. - -## 본문 - - - -`MongoPersistenceException` 은 이 계층의 오류 계층 루트다. 클래스 자바독이 규칙 둘을 적는다. - -## 루트가 세우는 두 규칙 - -:::evidence key="analysis-finding-a06-f005" alt="저장소 루트에서 돌린 정적 검색 출력 238줄. 먼저 MongoPersistenceException 6~31번 줄이 실린다. 6~15번 자바독이 이 클래스를 빚는 규칙 둘을 적는데, 첫째는 메시지가 MongoFailureContext 에서 파생되고 그 컨텍스트가 경계 있는 데이터 없는 값만 담을 수 있어서 어떤 호출자도 문서나 질의 파라미터를 예외 메시지에 실수로 넣을 수 없다는 것이고, 둘째는 어떤 생성자도 Throwable 원인을 받지 않는다는 것이며 드라이버 예외를 붙이면 실패 컨텍스트가 일부러 버린 모든 것이 getCause 와 모든 스택 트레이스 출력기를 통해 다시 노출된다고 적고, 드라이버의 정보는 컨텍스트가 이미 들고 있는 오류 라벨과 서버 코드로 남는다고 적는다. 22~26번의 유일한 생성자는 요약 문자열과 실패 컨텍스트만 받는다. 이어서 그 클래스를 상속하는 main 타입이 22 개라고 나오고 이름이 전부 나열되는데 MongoBulkPartialFailureException 부터 MongoWriteConflictException 까지다. 다음으로 이 계층에서 Throwable 을 인자로 받는 생성자 줄이 1 개, initCause 를 부르는 줄이 1 개라고 나오고 그 둘이 MongoTimeoutException 25번과 27번으로 나열되며, 그 검색이 훑은 파일이 351 개이고 옆 계층 persistence-jpa 에 같은 검색을 걸면 19 줄이 나온다고 적힌다. 이어서 MongoTimeoutException 5~29번 전문이 실린다. 5~11번 자바독은 타임아웃이 클라이언트가 기다리기를 멈춘 시점을 말할 뿐 서버가 무엇을 했는지는 말하지 않으며, 보내진 뒤 시간이 다한 쓰기는 WRITE_RESULT_UNKNOWN 이라 재조정해야 하고 명령이 떠나기 전에 터진 타임아웃만 재실행이 안전하다고 적는다. 16~18번이 1 인자 생성자이고, 20~24번 자바독이 클라이언트 쪽 타임아웃에서 이 예외를 만든다며 원인을 리액티브 스택 트레이스가 출처를 계속 이름 짓도록 남긴다고 적고, 25~28번이 2 인자 생성자로 27번에서 initCause 를 부른다. 다음으로 그 생성자를 부르는 자리가 나열되는데 선언 둘 말고 DefaultMongoFailureTranslator 104번의 1 인자 호출과 DefaultReactiveMongoExecutor 152번의 완전한 이름으로 쓴 2 인자 호출 둘이다. 그 아래 DefaultReactiveMongoExecutor 138~162번이 실린다. 140~145번이 이미 번역된 예외면 그대로 돌려주고, 146번이 java.util.concurrent.TimeoutException 을 잡으며, 147~150번 주석이 이것은 드라이버가 아니라 Reactor 자신의 타임아웃이고 예전에는 날것으로 새어 나가 이 플랫폼이 호출자에게 알려 준 적 없는 타입이 연산도 결과도 관측도 없이 도달했으며 관측기에 닿기 전에 반환해 실패가 기록되지도 않았다고 적는다. 151~155번이 MongoFailureContext.timedOut 으로 컨텍스트를 만들어 reactorTimeout 을 원인으로 넘기고, 156~159번이 관측기에 실패를 알린 뒤 돌려주고, 161번부터가 드라이버 예외를 다루는 블록이라 162~172번이 그것을 translator 로 보낸다. 이어서 이 계층에서 addSuppressed 를 부르는 줄이 3 개라고 나오고 그 셋이 SpringMongoTransactionSessionFactory 88번과 194번과 200번으로 나열된다. 그 아래 같은 파일 159~176번이 실려 161번의 session.abortTransaction 과 166~176번의 close 가 abort 와 session.close 에서 나온 RuntimeException 을 각각 잡아 recordCleanupFailure 로 넘기는 것이 보인다. 185~199번에서는 186~190번 자바독이 정리 실패를 결과로 만들지 않으면서 호출자가 보는 스택에 실제로 잘못된 것이 남고 실패한 abort 가 그것을 대신하는 대신 옆에 붙게 하려는 것이라고 적고, 192~195번이 inFlight 가 있으면 거기에 붙이고 돌아간다. 212~256번에서는 212~216번의 translate 와 218~222번의 translateCommit 이 classify 결과를 inFlight 에 넣고, 224~256번의 classify 가 248~251번에서 MongoTransactionCommitUnknownException 을, 253~255번에서 MongoTransactionTransientException 을 돌려준다. 마지막으로 이 계층의 시험에서 getCause 를 단언하는 줄이 1 개이고 그것이 MongoFailureContextTest 65번이며 test 소스 세트에서 9 줄, 시험 소스 세트를 모두 훑어도 9 줄이고, 이 계층의 하위 타입 가운데 final 로 닫힌 것이 22 개다. 그 시험 56~67번이 실리는데 58번 이름이 exceptionsDoNotExposeADriverCause 이고 60번이 대상으로 고른 것이 MongoTransactionCommitUnknownException 이며 65번이 원인이 null 인지, 66번이 범주가 TRANSACTION_COMMIT_UNKNOWN 인지 단언한다. 그 아래 MongoTimeoutException 이 시험 소스에 나오는 줄이 0 개이고 대조로 센 MongoTransactionCommitUnknownException 은 4 줄이다." caption="루트 예외의 두 규칙과 유일한 생성자 · 상속 타입 스물둘 · 이 계층에서 원인을 받는 줄 하나와 옆 계층 대조 · 규칙을 벗어난 타입 전문 · 그 생성자를 부르는 자리와 Reactor 타임아웃을 감싸는 이유 · addSuppressed 세 자리와 번역 예외에 드라이버 예외가 붙는 경로 · getCause 단언 하나가 고른 대상과 두 타입의 시험 등장 계수 — 238줄 · exit 0" zoom="true" -::: - -첫째는 메시지 쪽이다. 메시지가 `MongoFailureContext` 에서 파생되고 그 컨텍스트는 경계 있는 데이터 없는 값만 담을 수 있으므로, 어떤 호출자도 문서나 질의 파라미터를 예외 메시지에 실수로 넣을 수 없다. - -둘째가 `:11`\~`:13` 이다. 생성자가 `Throwable` 을 받는 일이 없다고 못 박는다. 드라이버 예외를 붙이면 실패 컨텍스트가 일부러 버린 모든 것이 `getCause()` 와 모든 스택 트레이스 출력기를 통해 다시 노출되기 때문이다. - -`:13`\~`:14` 는 그 대신 무엇이 남는지도 적는다. 드라이버의 정보는 컨텍스트가 이미 들고 있는 오류 라벨과 서버 코드로 남는다. - -`:22`\~`:26` 의 유일한 생성자가 요약 문자열과 실패 컨텍스트만 받는다. - -## 스물둘 중 하나 - -그 클래스를 상속하는 main 타입은 스물둘이고 전부 `final` 이다. 사이에 낀 추상 계층이 없으므로 닫힌 집합이다. - -이 계층에서 `Throwable cause)` 가 나오는 생성자 줄이 하나, `initCause` 를 부르는 줄이 하나다. 둘 다 `MongoTimeoutException` 의 `:25` 와 `:27` 이다. - -1 은 pathspec 이 아무것도 훑지 못해 나온 값이 아니다. 같은 pathspec 이 훑은 파일이 351 개이고, `persistence-jpa` 에 같은 검색을 걸면 19 줄이 나온다. - -## MongoTimeoutException\:20\~\:24 자바독이 적은 이유 - -`MongoTimeoutException:20`\~`:24` 가 2 인자 생성자의 자바독이다. 클라이언트 쪽 타임아웃에서 이 예외를 만들고, 원인을 리액티브 스택 트레이스가 출처를 계속 이름 짓도록 남긴다고 적는다. - -`:16`\~`:18` 의 1 인자 생성자는 원인을 받지 않는다. `DefaultMongoFailureTranslator:104` 는 이 쪽을 부른다. - -## 붙는 값은 드라이버 예외가 아니다 - -그 2 인자 생성자를 부르는 main 자리는 하나뿐이다. `DefaultReactiveMongoExecutor:152` 다. - -`:146` 이 `java.util.concurrent.TimeoutException` 을 잡고 `:155` 가 그것을 원인으로 넘긴다. `:161` 부터가 드라이버 예외를 다루는 블록이라, 이 `catch` 에는 드라이버 예외가 들어오지 않는다. - -`:147`\~`:150` 주석이 그렇게 감싸는 이유를 적는다. 이것은 드라이버가 아니라 Reactor 자신의 타임아웃이고, 예전에는 날것으로 새어 나가 이 플랫폼이 호출자에게 알려 준 적 없는 타입이 연산도 결과도 관측도 없이 도달했다. 관측기에 닿기 전에 반환했으므로 실패가 기록되지도 않았다. - -`:156`\~`:158` 이 지금은 관측기에 알린다. - -## 루트 자바독에는 이 예외가 적혀 있지 않다 - -루트 자바독만 읽으면 이 계층의 예외는 원인 사슬까지 로깅해도 안전하다고 읽을 수 있다. - -그 판단의 근거가 스물둘 중 하나에 대해 거짓이다. 하위 타입 자바독에는 이유가 적혀 있지만 루트 규칙에는 그 예외가 없다. - -## 생성자를 지나지 않는 두 번째 경로 - -이 계층에서 `addSuppressed` 를 부르는 줄은 셋인데 전부 `SpringMongoTransactionSessionFactory` 에 있다. - -`:194` 의 `inFlight.addSuppressed(failure)` 가 그중 하나다. `inFlight` 는 `:214` 와 `:220` 에서 `classify(...)` 가 돌려준 값이 되는데, `:249` 의 `MongoTransactionCommitUnknownException` 이거나 `:254` 의 `MongoTransactionTransientException` 이다. 둘 다 `MongoPersistenceException` 하위 타입이다. - -붙는 쪽은 드라이버 예외다. `:168` 이 `abort()` 를 부르고 그 안 `:161` 이 `session.abortTransaction()` 을 부르는데, 거기서 나온 `RuntimeException` 을 `:169` 가 잡아 `recordCleanupFailure` 로 넘긴다. `:173` 의 `session.close()` 도 같은 경로다. - -`:186`\~`:190` 자바독이 의도를 적는다. 정리 실패를 결과로 만들지 않으면서, 호출자가 보는 스택에 실제로 잘못된 것이 남고 실패한 abort 가 그것을 대신하는 대신 옆에 붙게 하려는 것이다. - -생성자 규칙은 여기서 깨지지 않는다. 어떤 생성자도 `Throwable` 을 받지 않았다. 다만 루트 자바독이 든 두 통로 가운데 `getCause()` 가 아니라 모든 스택 트레이스 출력기 쪽이 이 경로에서 열린다. - -`:88` 의 `startFailed.addSuppressed(closeFailed)` 는 다르다. 번역 이전의 두 예외끼리라 이 계층의 타입이 관여하지 않는다. - -## 그 규칙을 검사하는 시험이 고른 대상 - -이 계층의 시험에서 `getCause()` 를 단언하는 줄은 `MongoFailureContextTest:65` 하나다. 시험 소스 세트를 모두 훑어도 저장소 전체에 9 줄이다. - -그 시험 `:58` 의 이름이 `exceptionsDoNotExposeADriverCause` 인데, `:60` 이 고른 대상은 `MongoTransactionCommitUnknownException` 이다. 원인을 받는 생성자가 없는 타입이라 `:65` 의 `isNull()` 은 그 타입에 대해 항상 참이다. - -그 타입이 `:249` 에서 `inFlight` 가 되는 둘 중 하나다. `getCause()` 로는 드라이버 예외를 드러내지 않는데 `getSuppressed()` 로는 드러낼 수 있고, 시험은 앞의 것만 본다. - -규칙을 유일하게 벗어나는 타입은 시험 소스에 한 줄도 나오지 않는다. 같은 검색으로 `MongoTransactionCommitUnknownException` 은 4 줄이 나온다. - -## 원문에 없는 것 - -원문은 하위 타입 스무 개 중 하나가 규칙을 벗어난다고 적는다. 이 리비전에서 `MongoPersistenceException` 을 상속하는 main 타입은 스물둘이고, 생성자로 원인을 받는 것이 하나라는 판정은 그대로다. - -여기에 더한 것은 둘이다. 하나는 그 1 이 검색 실패가 아니라는 확인이고, 다른 하나는 생성자를 세는 것으로는 잡히지 않는 경로다. `SpringMongoTransactionSessionFactory:194` 가 드라이버 예외를 번역된 예외에 `addSuppressed` 로 붙이므로, 루트 규칙을 읽고 스택 트레이스를 통째로 남겨도 안전하다고 판단하면 그 판단이 두 자리에서 틀린다. - -그리고 `MongoTimeoutException` 이 시험 소스에 한 줄도 나오지 않고, `getSuppressed()` 를 보는 시험도 이 계층에 없다. - -## 확인하지 못한 것 - -붙은 원인이 스택 트레이스에 무엇을 남기는지 찍어 보지 않았다. 코드에서 확인한 것은 `:25` 가 원인을 받고 `:27` 이 `initCause` 를 부른다는 것까지다. - -이 계층의 예외를 원인 사슬까지 로깅하는 호출자가 실제로 있는지 저장소 밖을 보지 않았다. - -`Throwable cause)` 검색은 그 문자열 모양에 기댄다. 억눌린 예외가 붙는 장면을 세션을 띄워 관측하지 않았다. 정리 실패와 번역이 함께 일어나는 조합을 만들지 않았다. - -`addSuppressed` 경로가 실제로 도는 것을 실행으로 재현하지 않았다. `abortTransaction()` 이 던지고 그보다 먼저 번역이 일어난 조합을 만들어 보지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f007.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f007.md deleted file mode 100644 index 4e39b58..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f007.md +++ /dev/null @@ -1,157 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a06-f007 -title: 게이트웨이가 적은 순서의 timeout 단계가 정책에 없다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a06-f007 -evidenceCapturedOn: 2026-09-04 -body: case-analysis-finding-a06-f007.body.md -assets: - - key: analysis-finding-a06-f007 - file: ../../../final/evidence/rendered/analysis-finding-a06-f007.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a06-f007.txt -source: - - 원본 분석 절은 final/document.md#a06 §25 이다. ---- - -# 게이트웨이가 적은 순서의 timeout 단계가 정책에 없다 - -`PolicyAwareMongoNativeGateway:11`~`:12` 의 일곱 단계와 `README.md:65`~`:67` 의 열한 단계에 타임아웃이 들어 있다. `MongoNativeOperationPolicy.require` 가 던지는 거부는 여섯이고 타임아웃을 보는 것이 없으며, `ApprovedMongoNativeOperation` 이 선언한 두 예산 값은 어디서도 읽히지 않는다. - -## 관계 - -- **단일 admission point는 우회 경로를 세어야 성립한다** - 이 게이트웨이는 우회 경로가 아니라 자기가 열거한 단계가 문제다. 정책이 던지는 거부 여섯 가운데 등록 여부와 지원 등급은 두 문서의 목록에 아예 없고, 목록에 있는 타임아웃에는 거부가 없다. -- **문서와 상수가 서로 일치하는 것으로는 아무것도 증명되지 않는다** - 자바독과 README 가 서로 다른 목록을 적는데 둘 다 코드보다 길다. -- **명령 카탈로그와 admission 아홉 단계** - Redis 쪽도 자바독이 승인 지점의 검사 순서를 이름으로 열거한 기록이다. 그 개념은 순서의 기준이 비용이라고 적는다. - -## 문제 - -D3 게이트웨이는 자기 자바독과 모듈 README 양쪽에서 검사 순서를 이름으로 열거한다. 두 목록의 길이가 서로 다르고 둘 다 타임아웃을 담는다. - -그 단계들이 코드에 있는지 확인했다. - -## 결론 - -게이트웨이 자신은 세 가지만 한다. :36 이 policy.require(operation) 을 부르고, :39 가 본문을 실행하고, :43 이 감사 기록을 남긴다. - -검사는 전부 정책에 있고 그 안의 거부는 여섯이다. 등록 여부와 능력 일치와 지원 등급과 두 허용 목록과 관리 범주 차단이며, 각각 :56·:64·:75·:80·:85·:92 에서 던진다. - -자바독이 적은 일곱 단계 가운데 대응하는 거부가 없는 것은 타임아웃 하나다. ApprovedMongoNativeOperation:26 이 Duration timeout 을 선언하지만, 그 필드가 쓰이는 곳은 :36 의 널 검사와 :40 의 음수 거절과 :58 의 Duration.ZERO 와 :65 의 hasBody() 넷뿐이고 서버로 나가는 자리가 없다. - -hasBody() 는 정의만 있고 부르는 코드가 없다. - -:27 의 int maxResults 는 :59 가 0 을 넣는 것 말고 등장하지 않는다. 두 접근자를 부르는 줄이 그 타입을 참조하는 main 파일 넷 전체에서 0 개다. - -.timeout() 과 .maxResults() 를 실제로 부르는 줄은 같은 모듈에 열여덟 있는데 수신 타입이 전부 다르다. PolicyAwareMongoAggregationExecutor:78 의 effective, DefaultMongoImperativeExecutor:93 의 context, MongoReactiveCursorPublisher:59 의 budget, SpringMongoTransactionSessionFactory:74 의 profile 같은 것들이다. - -README 쪽 목록에만 있는 다섯 — 연산 이름, 일관성, 결과 제한, 추적, 편집 — 도 마찬가지다. - -지금은 이 게이트웨이를 지나는 호출이 없다. new PolicyAwareMongoNativeGateway 를 부르는 main 줄이 0 이고 시험은 2 이며, MongoNativeCapabilityGateway 와 PolicyAwareMongoNativeGateway 가 main 에 나오는 셋은 전부 선언이다. - -그런데도 README 는 이 클래스를 D3 안전성의 근거로 든다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 게이트웨이 자바독과 README 의 단계 목록 인용, 게이트웨이 본문 인용, 정책의 require 전문 인용과 거부 자리 계수, 값 타입의 선언과 압축 생성자와 hasBody 인용, 그 타입을 참조하는 main 파일 전수와 그 안의 접근자 호출 계수, 같은 이름 접근자의 다른 타입 호출 전수, 두 게이트웨이 이름의 등장 자리 전수와 생성 계수 -소스 수정 : x - -## 재현 조건 - -1. 게이트웨이 자바독과 README 의 두 단계 목록을 나란히 싣는다. -2. 게이트웨이의 execute 본문을 인용한다. -3. 정책의 require 를 전문으로 싣고 그 안에서 예외를 던지는 자리를 센다. -4. 값 타입의 필드 선언과 압축 생성자와 hasBody 를 인용한다. -5. 그 타입을 참조하는 main 파일을 전부 찾고 그 안에서 .timeout() 과 .maxResults() 를 부르는 줄을 센다. -6. 같은 이름의 접근자를 부르는 모듈 전체 줄을 나열해 수신 타입을 대조한다. -7. 두 게이트웨이 이름이 나오는 자리를 소스 세트와 함께 전부 나열하고 생성 줄을 센다. - -## 본문 - - - -`PolicyAwareMongoNativeGateway` 는 D3 능력 평면의 게이트웨이다. 클래스 자바독이 검사 순서를 적는다. - -## 두 문서가 적은 순서 - -:::evidence key="analysis-finding-a06-f007" alt="저장소 루트에서 돌린 정적 검색 출력 180줄. 먼저 PolicyAwareMongoNativeGateway 8~15번 자바독이 실린다. 이것이 D3 능력 게이트웨이이며 설계가 서술한 순서를 실행하고 첫 거부에서 멈춘다고 적는데 그 순서가 등록과 능력과 데이터베이스 프로파일과 컬렉션 프로파일과 타임아웃과 범주 그리고 실행이고, 감사 기록은 연산 아이디와 결과만 담고 BSON 인자는 절대 담지 않으며 데이터를 흘리는 감사 기록은 통제가 아니라 부담이라고 적는다. 그 아래 README 65~67번이 실리는데 D3 는 원시 클라이언트 탈출구가 아니며 이 게이트웨이가 능력에서 데이터베이스 프로파일과 컬렉션 허용 목록과 연산 이름과 타임아웃과 일관성과 결과 제한과 추적과 편집과 명령 범주를 거쳐 D4 차단까지 순서를 고정한다고 적는다. 이어서 게이트웨이 33~46번의 execute 가 실리는데 35번이 널 검사, 36번이 policy.require, 39번이 본문을 데이터베이스 리졸버에 적용해 실행, 43번이 finally 에서 감사 기록을 남기는 것이 전부다. 다음으로 MongoNativeOperationPolicy 52~100번의 require 가 전문으로 실린다. 54~62번이 등록되지 않은 연산을 거절하며 플랫폼은 미리 등록된 구현만 실행하고 호출자가 준 명령은 절대 실행하지 않는다고 적고, 63~72번이 등록된 능력과 제출된 능력이 다르면 거절하며, 73~78번이 지원 등급이 UNSUPPORTED 면 제약과 함께 거절하고, 79~83번이 데이터베이스 프로파일 허용 목록을, 84~90번이 컬렉션 프로파일 허용 목록을 검사하며, 91~97번이 능력 평면에서 허용되지 않는 범주를 거절하면서 drop 과 collMod 와 샤드와 사용자 관리는 자기 자격증명을 가진 D4 관리 평면에서 돈다고 적는다. 그 아래 require 안에서 예외를 던지는 자리가 6 개라고 나온다. 이어서 ApprovedMongoNativeOperation 20~43번이 실리는데 20~28번 record 헤더가 operationId 와 capability 와 category 와 databaseProfile 과 collectionProfile 과 Duration timeout 과 int maxResults 와 body 여덟을 선언하고, 30~43번 압축 생성자가 널 검사 다섯과 빈 아이디 거절과 40~42번의 음수 타임아웃 거절을 한다. 44~61번에는 unregistered 팩터리가 있는데 45~50번 자바독이 이름만 있고 본문이 없는 연산이며 아무도 등록하지 않은 아이디는 실행될 수 없고 그것을 요청하는 모양이 이것이라 게이트웨이의 거부 경로가 도달 가능하고 시험 가능하게 하려고 존재한다고 적고, 58번이 Duration.ZERO 를 59번이 0 을 넣는다. 62~67번의 hasBody 는 timeout 이 양수인지를 돌려준다. 다음으로 ApprovedMongoNativeOperation 을 참조하는 main 파일이 자기 자신과 MongoNativeCapabilityGateway 와 MongoNativeOperationPolicy 와 PolicyAwareMongoNativeGateway 넷이라고 나오고, 그 파일들 안에서 점 timeout 괄호나 점 maxResults 괄호를 부르는 줄이 0 개이며, timeout 이라는 이름이 나오는 줄은 36번의 널 검사와 41번의 음수 거절과 58번의 Duration.ZERO 와 65번의 양수 검사이고, hasBody 를 부르는 줄이 0 개인데 선언까지 포함하면 1 개다. 이어서 대조로 같은 모듈에서 그 두 접근자를 실제로 부르는 열여덟 줄이 나열되는데 PolicyAwareMongoAggregationExecutor 78번과 84번과 92번과 94번, DefaultMongoImperativeExecutor 83번과 93번과 103번, PolicyAwareMongoQueryBuilder 199번, MongoBudgetEnforcer 32번과 40번, DefaultReactiveMongoExecutor 76번과 108번과 154번, MongoReactiveCursorPublisher 59번, MongoResultBudgetTracker 51번과 54번, SpringMongoTransactionSessionFactory 74번, SpringReactiveMongoTransactionSessionFactory 76번이며 수신자가 effective 와 context 와 registered 와 budget 과 requested 와 profile 로 전부 다른 타입이다. 마지막으로 두 게이트웨이 이름이 나오는 자리 전부가 소스 세트와 함께 나오는데 main 은 MongoNativeCapabilityGateway 10번의 인터페이스 선언과 PolicyAwareMongoNativeGateway 16번의 클래스 선언과 24번의 생성자 셋뿐이고 나머지 여섯은 PolicyAwareMongoNativeGatewayTest 의 것이며, new PolicyAwareMongoNativeGateway 를 부르는 main 줄이 0 개이고 시험에서는 2 개다." caption="게이트웨이 자바독의 일곱 단계와 README 의 열한 단계 · execute 가 하는 세 가지 · require 전문과 거부 여섯 · 값 타입이 선언한 timeout 과 maxResults 와 hasBody · 그 타입을 참조하는 넷 안에서 접근자 호출 0 · 같은 접근자를 실제로 적용하는 다른 타입 열여덟 줄 · main 참조 셋과 생성 0 — 180줄 · exit 0" zoom="true" -::: - -`:11`\~`:12` 가 일곱을 적는다. 등록, 능력, 데이터베이스 프로파일, 컬렉션 프로파일, 타임아웃, 범주, 그리고 실행이다. 설계가 서술한 순서를 실행하고 첫 거부에서 멈춘다고 적는다. - -`README.md:65`\~`:67` 은 더 길다. 능력에서 시작해 데이터베이스 프로파일과 컬렉션 허용 목록과 연산 이름과 타임아웃과 일관성과 결과 제한과 추적과 편집과 명령 범주를 거쳐 D4 차단까지 열한 단계다. - -## execute 안의 세 줄 - -`:34` 의 `execute` 안에서 `:36` 이 `policy.require(operation)` 을 부른다. - -`:39` 가 `operation.body()` 를 데이터베이스 리졸버가 준 `MongoDatabase` 에 적용한다. - -`:43` 이 `finally` 에서 연산 아이디와 성공 여부를 감사 기록기에 넘긴다. - -검사는 전부 정책에 있다. - -## 정책이 던지는 거부는 여섯이다 - -`MongoNativeOperationPolicy.require:52`\~`:100` 안에서 `MongoOperationRejectedException` 을 던지는 자리가 여섯이다. - -`:56` 이 등록되지 않은 연산을 거절한다. `:58`\~`:61` 의 메시지가 플랫폼은 미리 등록된 구현만 실행하고 호출자가 준 명령은 절대 실행하지 않는다고 적는다. - -`:64` 가 등록된 능력과 제출된 능력의 불일치를, `:75` 가 `UNSUPPORTED` 등급을, `:80` 이 데이터베이스 프로파일 허용 목록을, `:85` 가 컬렉션 프로파일 허용 목록을 본다. - -`:92` 가 능력 평면에서 허용되지 않는 범주를 거절하면서, `drop` 과 `collMod` 와 샤드와 사용자 관리는 자기 자격증명을 가진 D4 관리 평면에서 돈다고 적는다. - -타임아웃을 보는 자리가 없다. - -## 선언은 돼 있고 읽히지 않는다 - -`ApprovedMongoNativeOperation:26` 이 `Duration timeout` 을, `:27` 이 `int maxResults` 를 선언한다. - -`timeout` 이 쓰이는 자리는 넷이다. `:36` 이 널 검사를 하고, `:40`\~`:42` 가 음수를 거절하고, `:58` 이 `unregistered` 팩터리에서 `Duration.ZERO` 를 넣고, `:65` 의 `hasBody()` 가 양수인지를 돌려준다. 값을 검사하거나 채울 뿐 서버에 보내는 자리가 없다. - -`:45`\~`:50` 자바독이 그 팩터리의 목적을 적는다. 아무도 등록하지 않은 아이디는 실행될 수 없고 그것을 요청하는 모양이 이것이라, 게이트웨이의 거부 경로가 도달 가능하고 시험 가능하게 하려는 것이다. `:59` 가 `maxResults` 자리에 `0` 을 넣는다. - -`hasBody()` 를 부르는 줄은 0 개다. 선언까지 포함해도 1 이므로, 그 이름이 나오는 자리는 정의 한 줄뿐이다. - -`maxResults` 는 `:27` 의 선언 이후 한 번도 등장하지 않는다. `ApprovedMongoNativeOperation` 을 참조하는 main 파일 넷 — 자기 자신과 `MongoNativeCapabilityGateway` 와 `MongoNativeOperationPolicy` 와 `PolicyAwareMongoNativeGateway` — 안에서 `.timeout()` 이나 `.maxResults()` 를 부르는 줄이 0 개다. - -## .timeout() 과 .maxResults() 를 부르는 열여덟 줄 - -같은 모듈에서 그 두 접근자를 부르는 줄은 열여덟이다. 수신자가 전부 다른 타입이다. - -`PolicyAwareMongoAggregationExecutor:78` 은 `effective.maxResults()` 로 결과 수를 견주고 `:92` 는 `context.timeout()` 을 밀리초로 바꾼다. `DefaultMongoImperativeExecutor:93` 은 경과 시간을 `context.timeout()` 과 견준다. `MongoReactiveCursorPublisher:59` 는 `budget.maxResults()` 로 커서를 제한하고, `MongoResultBudgetTracker:51` 은 그 수를 넘으면 실패시킨다. `SpringMongoTransactionSessionFactory:74` 는 `profile.timeout()` 을 트랜잭션 옵션에 넣는다. - -같은 모듈의 다른 타입들은 두 접근자를 열여덟 줄에서 실제로 적용한다. `ApprovedMongoNativeOperation` 이 선언한 두 값만 어디서도 읽히지 않는다. - -README 가 적은 일관성과 추적과 편집과 연산 이름 단계는 정책에 대응하는 거부가 아예 없다. - -## 지금 조립되지 않는다 - -두 이름이 main 에 나오는 줄은 셋이다. `MongoNativeCapabilityGateway:10` 의 인터페이스 선언과 `PolicyAwareMongoNativeGateway:16` 의 클래스 선언과 `:24` 의 생성자다. - -`new PolicyAwareMongoNativeGateway` 를 부르는 main 줄이 0 이다. 나머지 여섯 줄은 `PolicyAwareMongoNativeGatewayTest` 의 것이다. - -그래도 `README.md:65` 는 D3 가 원시 클라이언트 탈출구가 아닌 근거로 이 클래스를 든다. 그 클래스를 만드는 main 줄은 0 이고, 자바독이 적은 일곱 단계 중 타임아웃에 대응하는 거부가 없으며, README 에만 있는 다섯 단계에도 없다. - -## 원문에 없는 것 - -원문은 문서가 열거한 단계 중 여럿이 코드에 없고 그중 둘은 값 타입이 필드로 선언까지 해 둔 것이라고 적는다. - -여기에 더한 것은 그 판정을 검색 실패와 가르는 대조다. `.timeout()` 과 `.maxResults()` 는 같은 모듈에서 열여덟 번 불리는데 전부 다른 타입이고, D3 값 타입을 참조하는 파일 넷 안에서만 0 이다. `hasBody()` 도 저장소 전체에서 호출자가 없다. - -## 확인하지 못한 것 - -배선된 뒤에 어느 단계가 꼭 있어야 하는지는 설계 문서를 따로 읽어 판단하지 않았다. - -선언된 타임아웃을 실제로 걸려면 드라이버의 어느 옵션에 실어야 하는지 조사하지 않았다. - -추적과 편집이 다른 계층에서 이미 이뤄지는지는 이 모듈 밖이라 보지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f008.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f008.md deleted file mode 100644 index 59336bc..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f008.md +++ /dev/null @@ -1,154 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a06-f008 -title: 데드라인을 서버로 보내는 접근자를 부르는 프로덕션 코드가 없다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a06-f008 -evidenceCapturedOn: 2026-09-04 -body: case-analysis-finding-a06-f008.body.md -assets: - - key: analysis-finding-a06-f008 - file: ../../../final/evidence/rendered/analysis-finding-a06-f008.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a06-f008.txt -source: - - 원본 분석 절은 final/document.md#a06 §32 이다. ---- - -# 데드라인을 서버로 보내는 접근자를 부르는 프로덕션 코드가 없다 - -`BoundScopedOperations` 가 질의에 데드라인을 붙이는 자리는 `:53` 과 `:98` 둘이다. 그 타입을 돌려주는 `scoped()` 를 부르는 줄이 저장소 전체에 0 이고, `SpringMongoGeospatialOperations` 와 `MongoAtomicOperationsTemplate` 와 `MongoBulkExecutor` 는 `MongoRawAccessBoundaryTest:20` 이 데드라인이 없다고 적은 `rawOperations()` 를 다섯 번 부른다. - -## 관계 - -- **데드라인은 호출 예산에서 시작해 세 단계로 좁힌다** - 그 규칙의 세 번째 단계인 데이터베이스 로컬 타임아웃이 MongoDB 에서는 질의에 `maxTimeMS` 를 붙이는 것이다. 그 값을 붙이는 타입이 이 리프에 있는데 호출자가 없다. -- **쓰기 트랜잭션에는 유한 타임아웃이 필수다** - `MongoOperationContext` 가 양수 타임아웃을 요구하므로 선언은 언제나 유한하다. 그 값이 서버까지 가는지는 별개이고, 여기서는 세 경로에서 가지 않는다. -- **@Bean이 있다는 것은 조립 증거가 아니다** - 그 규칙은 프로덕션 참조가 0 이면 조립되지 않은 것으로 본다. `scoped()` 는 참조가 0 은 아니어서 인터페이스 둘과 구현 둘에 선언이 있는데, 그것을 부르는 프로덕션 코드만 없다. - -## 문제 - -모든 연산은 타임아웃을 선언해야 한다. 그 값을 maxTimeMS 로 붙여 서버까지 보내는 타입이 BoundScopedOperations 다. - -그 타입에 실제로 도달하는 경로가 있는지 확인했다. - -## 결론 - -그 자바독 :19~:23 은 질의에 maxTimeMS 를 함께 보내는지가 데드라인을 실제로 거는 것과 초과를 보고만 하는 것을 가르는 차이라고 적는다. 사후 측정은 초과를 알려 줄 뿐이고 서버가 받은 숫자는 질의를 끊는다는 것이다. :51~:54 의 bounded 가 질의 형태 메서드에 maxTimeMsec 을 붙이고 :95~:99 가 집계에 maxTime 을 붙인다. - -ScopedMongoOperations 를 돌려주는 메서드는 MongoCollectionAccess:32 의 scoped() 하나인데, 자바 파일 6444 개를 훑어도 그것을 부르는 줄이 없다. - -대신 SpringMongoGeospatialOperations:58·:82 와 MongoAtomicOperationsTemplate:84·:92 와 MongoBulkExecutor:69 가 MongoPlatformCollectionAccess:19 의 rawOperations() 를 쓴다. - -그것이 실수가 아니라는 것은 MongoPlatformCollectionAccess:6~:14 와 MongoRawAccessBoundaryTest:24~:26 두 곳에 적혀 있다. 네 실행기가 드라이버 수준 명령을 만들려면 제한 없는 템플릿이 필요하다는 것이고, 그래서 규칙은 그 손잡이를 지우는 것이 아니라 플랫폼 밖에서 부르지 못하게 하는 것이다. - -그 다섯 줄이 받는 MongoOperations 에는 데드라인이 붙지 않는다. MongoRawAccessBoundaryTest:18~:20 이 그 손잡이가 컬렉션 결속도 일관성 템플릿도 데드라인도 결과 예산도 없는 무제한 템플릿을 내준다고 적는다. 세 파일 안에서 시간 제한을 거는 줄도 0 인데, 같은 검색이 BoundScopedOperations 에서는 8 줄을 찾는다. - -서버에 데드라인이 붙는 경로는 따로 셋 있다. PolicyAwareMongoAggregationExecutor:96 이 등록된 예산과 컨텍스트 밀리초의 최솟값을 쓰고, PolicyAwareMongoQueryBuilder:200 과 MongoReactiveCursorPublisher:58 은 등록된 예산 값만 쓴다. - -context.timeout() 을 읽는 자리를 전부 세면 예산을 좁히는 것은 PolicyAwareMongoAggregationExecutor:92 하나다. 나머지는 DefaultMongoImperativeExecutor 의 사후 검사와 DefaultReactiveMongoExecutor 의 클라이언트 쪽 timeout 연산자다. 연산이 선언한 타임아웃이 서버까지 닿는 길은 집계 하나이고, 그것도 등록된 예산과의 최솟값으로만 간다. 나머지 셋에는 사후 검사만 남는데, DefaultMongoImperativeExecutor:94~:96 의 주석이 그 검사가 작업을 끊을 수 없다고 스스로 적는다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 그 타입의 자바독과 데드라인을 붙이는 두 자리 인용, 두 접근자의 선언과 raw 접근이 의도된 이유를 적은 자바독 인용, 두 이름을 부르는 줄을 저장소 전체에서 계수하고 등장 자리 전수와 훑은 파일 수 대조, rawOperations() 구현 인용, 집계 실행기의 데드라인 계산과 maxTimeMillis() 와 context.timeout() 을 부르는 자리 전수, 세 실행기의 조립 상태와 그 안의 데드라인 계수와 대조, 경계 가드 시험의 자바독과 금지 목록 인용, 사후 검사 분기와 그 주석 인용 -소스 수정 : x - -## 재현 조건 - -1. BoundScopedOperations 의 클래스 자바독과 maxTime 을 거는 줄을 인용한다. -2. scoped() 와 rawOperations() 의 선언을 두 인터페이스에서 인용한다. -3. 두 이름을 부르는 줄을 각각 세고, 선언까지 포함해 등장 자리를 전부 나열하고, 훑은 파일 수를 함께 낸다. -4. rawOperations() 구현이 무엇을 돌려주는지 인용한다. -5. 집계 실행기가 데드라인을 만드는 자리와 maxTimeMillis() 를 부르는 자리를 전부 인용한다. -6. 사후 경과 검사 분기와 그 주석을 인용한다. - -## 본문 - - - -`MongoOperationContext` 는 모든 연산에 타임아웃을 선언하게 한다. 그 값이 드라이버 명령에 실려 나가려면 질의에 `maxTimeMS` 로 붙어야 한다. - -## BoundScopedOperations 가 존재하는 이유 - -:::evidence key="analysis-finding-a06-f008" alt="저장소 루트에서 돌린 정적 검색 출력 214줄. 먼저 BoundScopedOperations 12~27번 클래스 자바독이 실린다. 13~17번은 이것이 물리 컬렉션 하나 위의 ScopedMongoOperations 이고 모든 메서드가 이 범위가 만들어질 때 받은 컬렉션을 넘기며, 호출자가 다른 것을 줄 수 없으므로 등록된 컬렉션 프로파일과 거기서 파생된 테넌트 경계가 호출자의 협조 없이 성립한다고 적는다. 19~23번은 모든 질의 형태 메서드가 연산의 데드라인을 maxTimeMS 로 함께 보내며 그것이 데드라인과 그것에 대한 보고를 가르는 차이라고 적고, 블로킹 실행기는 콜백이 반환된 뒤에야 경과 시간을 잴 수 있으며 자바 콜백은 드라이버 호출 중간에 끊을 수 없어서 예산을 넘긴 연산은 탐지될 뿐 중단되지 않았지만 같은 숫자를 서버로 보내면 작업이 끝난다고 적는다. 25~26번은 그것이 Query 나 Aggregation 을 받는 메서드에 적용되며 insert 는 붙일 질의가 없다고 적는다. 그 아래 maxTimeMS 가 실제로 붙는 자리로 자바독 19번과 98번의 점 maxTime 괄호 timeout 이 나온다. 이어서 MongoCollectionAccess 24~36번과 MongoPlatformCollectionAccess 10~24번이 실려 각각 scoped 와 rawOperations 선언을 보인다. 다음으로 scoped 를 부르는 줄이 0 개이고 rawOperations 를 부르는 줄이 5 개라고 나오며, 두 이름이 나오는 자리가 소스 세트와 함께 전부 나열된다 — SpringMongoGeospatialOperations 58번과 82번, DefaultMongoImperativeExecutor 184번과 192번의 선언, MongoCollectionAccess 32번과 MongoPlatformCollectionAccess 19번의 선언, MongoAtomicOperationsTemplate 84번과 92번, MongoBulkExecutor 69번, DefaultReactiveMongoExecutor 218번과 ReactiveMongoCollectionAccess 31번의 리액티브 쪽 선언이며 전부 main 이다. 그 검색이 훑은 파일은 491 개다. 이어서 DefaultMongoImperativeExecutor 180~196번이 실려 184번의 scoped 가 BoundScopedOperations 를 만들고 192~194번의 rawOperations 가 경계 없는 MongoOperations 를 그대로 돌려주는 것이 보인다. 다음으로 PolicyAwareMongoAggregationExecutor 88~108번이 실리는데 91번 주석이 컨텍스트의 타임아웃은 호출자의 데드라인이며 maxTimeMS 를 좁힐 수는 있어도 늘릴 수는 없다고 적고, 96번이 registered.maxTimeMillis 와 contextMillis 의 최솟값을 쓰며, 106번이 그 값을 AggregationOptions 의 maxTime 에 넣는다. 그 아래 maxTimeMillis 를 부르는 자리가 전부 나열되는데 PolicyAwareMongoAggregationExecutor 96번과 106번, PolicyAwareMongoQueryBuilder 200번, MongoReactiveCursorPublisher 58번이다. 마지막으로 DefaultMongoImperativeExecutor 85~106번이 실려 88번이 콜백을 부르고 89번이 경과 시간을 재며 93번이 그것을 컨텍스트 타임아웃과 견주고, 94~96번 주석이 컨텍스트가 모든 연산에 데드라인을 선언하는데 아무것도 그것과 견주지 않았으며 콜백은 드라이버 호출 중간에 끊을 수 없어 이것이 작업을 끊지는 못하지만 예산을 넘긴 작업에 성공을 보고하는 것보다는 낫다고 적는다." caption="데드라인을 서버로 보내는 타입의 자바독과 maxTime 을 거는 줄 · 두 접근자의 선언 · scoped 호출 0 과 rawOperations 호출 다섯의 전수 · rawOperations 가 돌려주는 것 · 서버에 데드라인을 붙이는 다른 세 경로 · 사후 경과 검사와 그 주석 — 214줄 · exit 0" zoom="true" -::: - -`BoundScopedOperations:15`\~`:17` 이 첫 목적을 적는다. 모든 메서드가 이 범위를 만들 때 받은 컬렉션을 넘기므로 호출자가 다른 것을 줄 수 없고, 등록된 컬렉션 프로파일과 거기서 파생된 테넌트 경계가 호출자의 협조 없이 성립한다. - -`:19`\~`:23` 이 두 번째다. 질의 형태 메서드가 연산의 데드라인을 `maxTimeMS` 로 함께 보내는 것이 데드라인과 그것에 대한 보고를 가르는 차이라는 것이다. - -이유도 함께 있다. 블로킹 실행기는 콜백이 반환된 뒤에야 경과 시간을 잴 수 있고 자바 콜백은 드라이버 호출 중간에 끊을 수 없으므로, 예산을 넘긴 연산은 탐지될 뿐 중단되지 않았다. 같은 숫자를 서버로 보내면 작업이 끝난다. - -붙이는 자리는 둘이다. `:51`\~`:54` 의 private `bounded` 가 `query.maxTimeMsec(timeout.toMillis())` 로 질의 형태 메서드에 붙이고, `:95`\~`:99` 가 `AggregationOptions` 의 `maxTime` 으로 집계에 붙인다. `:25`\~`:26` 은 그것이 `Query` 나 `Aggregation` 을 받는 메서드에만 붙는다고도 적는다. `insert` 에는 붙일 질의가 없다. - -## scoped() 와 rawOperations() 가 돌려주는 것 - -`MongoCollectionAccess:32` 의 `scoped()` 가 `ScopedMongoOperations` 를 돌려준다. 콜백이 그것을 받으면 데드라인이 붙은 연산을 쓴다. - -`MongoPlatformCollectionAccess:19` 의 `rawOperations()` 는 `MongoOperations` 를 돌려준다. - -그 인터페이스의 자바독 `:6`\~`:11` 이 존재 이유를 적는다. 벌크와 원자적 연산과 집계와 지리 공간 실행기는 드라이버 수준 명령을 만들기 때문에 제한 없는 템플릿이 필요하고, 그것들은 플랫폼 안에서 가드레일을 설치하는 코드라 호출자의 콜백과 위치가 다르므로, raw 손잡이는 모든 콜백이 받는 것이 아니라 이름을 대고 요청하는 타입이라는 것이다. - -`:13`\~`:14` 가 그것을 `MongoCollectionAccess` 밖에 둔 이유를 적는다. 범위 API 와 탈출구를 함께 제공하는 인터페이스는 탈출구를 제공하는 것이다. - -## scoped() 호출 0 과 rawOperations() 호출 다섯 - -`.scoped()` 를 부르는 줄이 0 이다. 나오는 자리는 `MongoCollectionAccess:32` 와 `ReactiveMongoCollectionAccess:31` 의 선언, `DefaultMongoImperativeExecutor:184` 와 `DefaultReactiveMongoExecutor:218` 의 구현 넷뿐이다. - -`.rawOperations()` 를 부르는 줄은 다섯이다. `SpringMongoGeospatialOperations:58` 과 `:82`, `MongoAtomicOperationsTemplate:84` 와 `:92`, `MongoBulkExecutor:69` 다. - -같은 검색이 파일 6444 개를 훑었으므로 0 은 검색 대상이 잡히지 않아 나온 값이 아니다. 같은 정규식이 다른 이름에 대해 다섯을 찾았다. - -`DefaultMongoImperativeExecutor:192`\~`:194` 의 `rawOperations()` 는 `:82` 가 일관성 프로파일로 고른 템플릿을 그대로 돌려준다. 컬렉션도 데드라인도 붙지 않는다. - -세 실행기 안에서 `maxTime` 이나 `timeout` 이 나오는 줄도 0 이다. 같은 검색을 `BoundScopedOperations` 에 걸면 8 줄이 나온다. - -## 저장소가 그 손잡이에 대해 아는 것 - -`MongoRawAccessBoundaryTest` 가 그 두 탈출구를 막는 아키텍처 가드다. `:37` 이 금지 문자열로 `.rawOperations()` 와 `.executeInternal(` 을 세우고, `:40`\~`:41` 이 허용 경로를 mongo 어댑터의 프로덕션 소스로 한정한다. - -그 자바독 `:18`\~`:20` 이 두 탈출구가 무엇을 내주는지 적는다. 컬렉션 결속도, 일관성 템플릿도, **데드라인도**, 결과 예산도 없는 스프링 데이터의 무제한 `MongoOperations` 다. - -`:24`\~`:26` 은 둘 다 플랫폼 안에서는 정말로 필요하다고 적는다. 지리 공간과 원자적 연산과 벌크가 그것 위에 세워져 있으므로 규칙은 지우라는 것이 아니라 밖에서 부르지 말라는 것이고, 그것은 가시성 문제가 아니라 경계 문제라는 것이다. - -그래서 저장소는 데드라인이 없다는 것을 알고 있고, 그 사실을 경계로만 다룬다. 밖에서 못 부르게 막을 뿐 안에서 부른 다섯 자리가 데드라인 없이 도는 것은 다루지 않는다. - -## 서버에 데드라인을 붙이는 다른 세 경로 - -`PolicyAwareMongoAggregationExecutor:96` 이 `Math.min(registered.maxTimeMillis(), contextMillis)` 를 만들고 `:106` 이 그것을 `AggregationOptions` 의 `maxTime` 에 넣는다. `:91` 주석이 컨텍스트의 타임아웃은 `maxTimeMS` 를 좁힐 수는 있어도 늘릴 수는 없다고 적는다. - -`PolicyAwareMongoQueryBuilder:200` 과 `MongoReactiveCursorPublisher:58` 은 `budget.maxTimeMillis()` 만 쓴다. - -`context.timeout()` 과 `narrowedTo` 가 나오는 자리를 전부 세면 예산을 컨텍스트로 좁히는 것은 `PolicyAwareMongoAggregationExecutor:72` 와 `:92` 뿐이다. `DefaultMongoImperativeExecutor:83`·`:93`·`:103` 은 사후 검사이고, `DefaultReactiveMongoExecutor:76`·`:108` 은 Reactor 의 클라이언트 쪽 `timeout` 연산자다. - -그래서 `context.timeout()` 이 서버까지 가는 경로는 집계 하나이고, 그것도 예산과의 최솟값으로만 간다. - -## 지리 공간과 원자적 연산과 벌크에 남는 사후 검사 - -`DefaultMongoImperativeExecutor:88` 이 콜백을 부르고 `:89` 가 경과 시간을 재고 `:93` 이 그것을 `context.timeout()` 과 견준다. - -`:94`\~`:96` 주석이 그 검사의 한계를 적는다. 컨텍스트가 모든 연산에 데드라인을 선언하는데 아무것도 그것과 견주지 않았고, 콜백은 드라이버 호출 중간에 끊을 수 없어 이 검사가 작업을 끊지 못하며, 다만 예산을 넘긴 작업에 성공을 보고하는 것보다는 위반을 보고하는 편이 낫다는 것이다. - -`SpringMongoGeospatialOperations` 와 `MongoAtomicOperationsTemplate` 와 `MongoBulkExecutor` 는 그 사후 검사만 받는다. 서버에는 데드라인이 전달되지 않는다. - -## 원문에 없는 것 - -원문은 `scoped()` 를 부르는 프로덕션 코드가 0 이고 세 실행기가 `rawOperations()` 를 쓴다고 적으면서, 서버에 데드라인을 붙이는 경로별 어휘를 표로 갈라 놓는다. 그대로다. - -여기에 더한 것은 셋이다. 그 0 이 검색 실패가 아니라는 확인 — 같은 pathspec 이 6444 개 파일을 훑었고 같은 검색이 `rawOperations()` 에 대해 다섯을 찾았다. 데드라인이 붙는 자리가 `:98` 하나가 아니라 `:53` 과 둘이라는 것. 그리고 raw 손잡이가 실수가 아니라 설계라는 저장소 자신의 진술 둘 — `MongoPlatformCollectionAccess:8`\~`:14` 와 `MongoRawAccessBoundaryTest:18`\~`:26` 이다. 뒤쪽은 그 손잡이에 데드라인이 없다는 것까지 적으면서 경계만 지킨다. - -## 확인하지 못한 것 - -`maxTimeMsec` 이 붙은 질의를 서버에 보내 실제로 잘리는 것을 관측하지 않았다. - -세 컴포넌트를 `scoped()` 로 옮기려면 어떤 연산이 더 있어야 하는지 조사하지 않았다. - -`scoped()` 의 의도된 호출자가 누구인지, 포크의 콜백이 그 메서드를 부르도록 설계된 것인지 설계 문서로 확인하지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f009.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f009.md deleted file mode 100644 index a325f95..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f009.md +++ /dev/null @@ -1,130 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a06-f009 -title: 예산 초과 분기가 한 관측에 success 와 failure 를 차례로 부른다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a06-f009 -evidenceCapturedOn: 2026-09-04 -body: case-analysis-finding-a06-f009.body.md -assets: - - key: analysis-finding-a06-f009 - file: ../../../final/evidence/rendered/analysis-finding-a06-f009.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a06-f009.txt -source: - - 원본 분석 절은 final/document.md#a06 §33 이다. ---- - -# 예산 초과 분기가 한 관측에 success 와 failure 를 차례로 부른다 - -`DefaultMongoImperativeExecutor:97` 이 `observation.success(outcome)` 를 부른 뒤 `:98` 이 `MongoOperationRejectedException` 을 던진다. 그 예외가 `MongoPersistenceException` 하위 타입이고 같은 `try` 안이라 `:108` 의 `catch` 가 잡아 `:109` 에서 `observation.failure(...)` 를 부른다. - -## 관계 - -- **메시징만 성공 로그를 try 밖으로 옮기고 알림은 같은 try 에 남겨 두었다** - 그 사례도 성공 기록 호출이 `try` 안에 남아 있어서 뒤따르는 예외를 같은 블록의 `catch` 가 받는다. 여기서는 로그가 아니라 관측이고, `catch` 가 성공을 지우는 대신 `failure` 를 덧쓴다. -- **데드라인을 서버로 보내는 접근자를 부르는 프로덕션 코드가 없다** - 같은 메서드의 같은 분기가 두 기록의 출발점이다. 그쪽은 그 검사가 왜 사후일 수밖에 없는지를, 이쪽은 그 검사가 관측에 무엇을 남기는지를 본다. -- **port 계약은 동시성 요구를 적는다** - 그 규칙의 두 번째 항목이 현재 구현이 안전한 것과 계약이 안전을 요구하는 것을 가르라고 적는다. 여기서도 두 구현이 나중 호출로 태그를 덮어써서 결과가 하나로 남을 뿐, `MongoOperationObservation` 자바독에는 두 메서드를 각각 몇 번 부를 수 있는지가 없다. - -## 문제 - -예산을 넘긴 연산에 대해 실행기는 거부 예외를 던진다. 그 던짐이 이미 열려 있는 관측에 무엇을 남기는지 확인했다. - -## 결론 - -MongoOperationObservation:16 의 success 와 :19 의 failure 가 결과를 기록한다. 어느 쪽을 몇 번 불러야 하는지, 여러 번 불렀을 때 무엇이 남는지는 자바독에 없다. - -DefaultMongoImperativeExecutor:93 이 경과 시간을 context.timeout() 과 견준다. 넘었으면 :97 이 observation.success(outcome) 를 부르고 :98 이 MongoOperationRejectedException 을 던진다. - -그 예외는 MongoPersistenceException 을 상속한다. 던짐이 :87 에서 열린 try 안에 있으므로 :108 의 catch (MongoPersistenceException alreadyTranslated) 가 잡는다. - -그 catch 가 실패를 기록한 뒤 예외를 다시 던진다. 그래서 예산 초과 경로에서만 한 관측 객체가 두 결과를 다 받는다. - -이 저장소에 있는 구현은 둘뿐이고, 둘 다 그 두 번째 호출을 문제로 만들지 않는다. MicrometerMongoOperationObserver:76~:88 의 두 메서드가 outcomeTags 를 통째로 덮어쓰고 :96~:102 의 close() 가 stopped 플래그를 보고 타이머를 한 번만 멈추므로, 나중에 부른 failure 의 태그가 남는다. NoOpMongoOperationObserver:29~:50 은 아무것도 기록하지 않는다. - -그 무해함은 두 구현이 필드를 덮어쓰기 때문이지 인터페이스가 그렇게 하라고 적어 둔 것이 아니다. 호출마다 계수기를 올리는 구현이 들어오면 이 경로의 연산 하나가 두 건이 된다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 관측 인터페이스 전문 인용, 예산 초과 분기와 그것을 감싸는 try 와 catch 인용, 거부 예외의 상속 관계 인용, 그 파일에서 두 메서드를 부르는 자리 전수와 범위별 계수, 두 구현의 메서드와 close() 인용, 인터페이스 구현체 계수와 전수 -소스 수정 : x - -## 재현 조건 - -1. MongoOperationObservation 인터페이스를 전문으로 싣는다. -2. 실행기의 try 블록을 예산 초과 분기와 catch 절까지 함께 인용한다. -3. 거부 예외가 무엇을 상속하는지 인용한다. -4. try 범위와 catch 범위에서 success 와 failure 를 부르는 줄을 각각 세고, 그 파일의 두 메서드 호출을 전부 나열한다. -5. 두 구현의 메서드와 close() 를 인용한다. -6. 그 인터페이스를 구현하는 자리를 세고 전부 나열한다. - -## 본문 - - - -블로킹 실행기는 콜백이 반환된 뒤 경과 시간을 잰다. 예산을 넘겼으면 거부 예외를 던진다. - -## success 와 failure 의 자바독이 정하지 않은 것 - -:::evidence key="analysis-finding-a06-f009" alt="저장소 루트에서 돌린 정적 검색 출력 171줄. 먼저 MongoOperationObservation 6~26번 전문이 실린다. 자바독은 이것이 진행 중인 연산 하나의 관측 범위이고 AutoCloseable 이라 콜백이 플랫폼이 분류한 적 없는 것을 던지는 경로까지 모든 경로에서 닫히며, 성공에서만 닫히는 관측은 시스템이 건강하지 않을 때 정확히 건강해 보이는 지표를 만든다고 적는다. 16번이 success 로 성공적 완료와 쓰기 결과를 기록하고, 19번이 failure 로 실패 컨텍스트가 담을 수 있는 경계 있는 메타데이터만 써서 실패를 기록하며, 22번이 traceId, 25번이 close 다. 둘 중 하나만 불러야 한다거나 마지막 호출이 이긴다는 말은 없다. 이어서 DefaultMongoImperativeExecutor 85~118번이 실린다. 86번이 관측을 열고 87번이 try 를 열며 88번이 콜백을 부르고 89번이 경과 시간을 재고 92번이 결과를 정한다. 93번이 경과 시간을 컨텍스트 타임아웃과 견주고, 94~96번 주석이 컨텍스트가 모든 연산에 데드라인을 선언하는데 아무것도 그것과 견주지 않았으며 콜백은 드라이버 호출 중간에 끊을 수 없어 이것이 작업을 끊지 못하지만 예산을 넘긴 작업에 성공을 보고하는 것보다 위반을 보고하는 편이 낫다고 적는다. 97번이 observation.success 를 부르고 98~104번이 MongoOperationRejectedException 을 던지는데 메시지에 실제 경과 밀리초와 선언된 타임아웃 밀리초가 들어간다. 106번이 정상 경로의 success 이고 107번이 결과를 돌려준다. 108번의 catch 가 MongoPersistenceException 을 잡아 109번에서 observation.failure 를 부르고 110번에서 다시 던지며, 111번과 113번의 catch 가 드라이버 예외와 스프링 예외를 번역으로 보낸다. 다음으로 MongoOperationRejectedException 1~24번이 실리는데 7~12번 자바독이 이것은 서버에 닿기 전에 플랫폼이 로컬에서 거절한 것이고 등록되지 않은 필드와 연산자, 올려 잡은 예산, 읽기 API 안의 쓰기 단계, 깊은 skip, 없는 테넌트 컨텍스트, 런타임 클라이언트의 관리 명령 같은 가드레일 예외이며 언제나 NOT_SENT 이고 재시도 불가라고 적는다. 14번이 그것이 MongoPersistenceException 을 상속한다고 보인다. 이어서 try 블록 안에서 observation.success 를 부르는 줄이 2 개, 같은 try 의 catch 가 observation.failure 를 부르는 줄이 1 개라고 나오고, 그 파일에서 두 메서드를 부르는 자리가 97번과 106번의 success, 109번과 129번의 failure 넷으로 나열된다. 다음으로 MicrometerMongoOperationObserver 70~103번이 실리는데 72번이 outcomeTags 를 result unknown 으로 초기화하고, 76~78번의 success 와 81~88번의 failure 가 둘 다 그 필드를 통째로 덮어쓸 뿐이며, 96~102번의 close 가 stopped 플래그를 보고 한 번만 sample.stop 을 부른다. 마지막으로 MongoOperationObservation 을 구현하는 자리가 2 개라고 나오고 그 둘이 NoOpMongoOperationObserver 29번의 NoOpObservation 과 MicrometerMongoOperationObserver 56번의 MicrometerObservation 이며, 대조로 센 MongoOperationObserver 구현도 2 개다." caption="관측 인터페이스 전문과 두 메서드의 계약 · 예산 초과 분기와 그것을 잡는 catch · 거부 예외가 상속하는 것 · 범위별 호출 계수와 그 파일의 네 자리 · 출하 구현의 두 메서드와 한 번만 도는 close · 구현체 둘 — 171줄 · exit 0" zoom="true" -::: - -`MongoOperationObservation:9`\~`:11` 은 이 타입이 `AutoCloseable` 인 이유를 적는다. 콜백이 플랫폼이 분류한 적 없는 것을 던지는 경로까지 포함해 모든 경로에서 범위가 닫혀야 하고, 성공에서만 닫히는 관측은 시스템이 건강하지 않을 때 정확히 건강해 보이는 지표를 만들기 때문이다. - -`:16` 의 `success` 는 성공적 완료와 쓰기 결과를 기록한다. `:19` 의 `failure` 는 실패 컨텍스트가 담을 수 있는 경계 있는 메타데이터만 써서 실패를 기록한다. - -둘 중 하나만 불러야 한다는 말도, 여러 번 부르면 나중 호출이 앞선 호출을 덮는다는 말도 없다. - -## 예산 초과 분기 - -`DefaultMongoImperativeExecutor:86` 이 관측을 열고 `:87` 이 `try` 를 연다. `:88` 이 콜백을 부르고 `:89` 가 경과 시간을 잰다. - -`:93` 이 그 경과 시간을 `context.timeout()` 과 견준다. 넘었으면 `:97` 이 `observation.success(outcome)` 를 부르고, `:98`\~`:104` 가 실제 경과 밀리초와 선언된 타임아웃 밀리초를 담은 `MongoOperationRejectedException` 을 던진다. - -`:94`\~`:96` 주석이 이 순서의 의도를 적는다. 이 검사가 작업을 끊을 수는 없지만, 예산을 넘긴 작업에 성공을 보고하는 것보다 위반을 보고하는 편이 낫다는 것이다. - -## 거부 예외를 같은 try 의 catch 가 잡는다 - -`MongoOperationRejectedException:14` 가 `MongoPersistenceException` 을 상속한다. `:7`\~`:12` 자바독은 이것이 서버에 닿기 전 로컬 거절이며 언제나 `NOT_SENT` 이고 재시도 불가라고 적는다. - -`:108` 의 `catch (MongoPersistenceException alreadyTranslated)` 가 그 예외를 잡는다. `:109` 가 `observation.failure(...)` 를 부르고 `:110` 이 다시 던진다. - -`try` 범위 안에서 `success` 를 부르는 줄이 둘이고 그 `catch` 가 `failure` 를 부르는 줄이 하나다. 예산 초과 경로에서는 같은 관측 객체에 대해 `:97` 의 `success` 가 먼저 불리고 `:109` 의 `failure` 가 이어서 불린다. - -## 두 구현 모두 나중 호출의 태그만 남긴다 - -`MicrometerMongoOperationObserver:72` 가 `outcomeTags` 를 `result` `unknown` 으로 초기화한다. - -`:76`\~`:78` 의 `success` 와 `:81`\~`:88` 의 `failure` 가 둘 다 그 필드를 통째로 덮어쓴다. 누적하지 않는다. - -`:96`\~`:102` 의 `close()` 가 `stopped` 플래그를 보고 한 번만 `sample.stop(...)` 을 부른다. 그때 남아 있는 태그는 나중에 부른 `failure` 쪽이다. - -`NoOpMongoOperationObserver:29`\~`:50` 의 구현은 세 메서드 몸통이 전부 비어 있다. 이 저장소에서 `MongoOperationObservation` 을 구현하는 것은 그 둘뿐이다. - -## 인터페이스에 호출 횟수 규칙이 없다 - -두 구현 모두 나중 호출이 앞선 것을 덮는 모양이라 지금은 기록되는 결과가 달라지지 않는다. - -그 성질이 인터페이스에 쓰여 있지 않다. `success` 와 `failure` 를 각각 계수하는 구현을 포크가 만들면, 예산을 넘긴 연산 하나가 성공 한 번과 실패 한 번으로 세어진다. - -## 원문이 "출하 구현" 이라 부른 것 - -원문이 무해하다고 적은 그 구현은 둘이다. `MicrometerMongoOperationObserver:56` 의 `MicrometerObservation` 과 `NoOpMongoOperationObserver:29` 의 `NoOpObservation` 이고, 저장소에 다른 구현은 없다. - -## 확인하지 못한 것 - -타이머에 실제로 무엇이 찍히는지 연산을 돌려 재현하지 않았다. - -두 호출을 각각 계수하는 구현을 포크가 만들 가능성이 얼마나 되는지는 판단하지 않았다. - -위반을 성공으로 먼저 남기는 이 순서를 바꾸면 무엇이 달라지는지 따져 보지 않았다. - -두 호출을 각각 계수하는 구현을 포크가 만들 가능성이 얼마나 되는지는 판단하지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f010.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f010.md deleted file mode 100644 index 40280aa..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f010.md +++ /dev/null @@ -1,156 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a06-f010 -title: 집계 실행기가 컬렉션 이름을 문자열로 따로 받는다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a06-f010 -evidenceCapturedOn: 2026-09-04 -body: case-analysis-finding-a06-f010.body.md -assets: - - key: analysis-finding-a06-f010 - file: ../../../final/evidence/rendered/analysis-finding-a06-f010.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a06-f010.txt -source: - - 원본 분석 절은 final/document.md#a06 §42 이다. ---- - -# 집계 실행기가 컬렉션 이름을 문자열로 따로 받는다 - -`MongoCollectionProfileRegistry` 의 클래스 자바독은 이 레지스트리를 지나지 않고 컬렉션 이름이 드라이버까지 가는 경로는 없다고 적는다. 그런 경로가 둘 있다. `PolicyAwareMongoAggregationExecutor:63` 과 `MongoReactiveCursorPublisher:37` 인데, 둘 다 널 검사 말고는 어떤 검사도 거치지 않고 그 문자열을 드라이버 연산에 넘긴다. - -## 관계 - -- **단일 admission point는 우회 경로를 세어야 성립한다** - 이 사례가 그 규칙의 형태다. 레지스트리가 유일한 승인 지점이라는 문장은 그것을 지나지 않는 서명이 하나도 없어야 성립하는데, 여기서는 둘이 있다. -- **path·identifier는 등록하고 value는 바인딩한다** - 컬렉션 이름은 등록 대상이고 값이 아니다. 두 서명은 컬렉션 이름을 `String` 값으로 받는다. -- **데드라인을 서버로 보내는 접근자를 부르는 프로덕션 코드가 없다** - 그 기록이 연산 타임아웃이 서버까지 닿는 유일한 경로로 든 것이 이 집계 실행기다. 같은 `MongoOperationContext` 에서 `timeout()` 은 읽어 `maxTime` 에 반영하면서 `collectionProfile()` 은 한 번도 읽지 않는다. - -## 문제 - -컬렉션 프로파일 레지스트리의 자바독이 이 리프의 강한 주장 하나를 편다. 동적 컬렉션 이름 금지를 이 간접이 강제 가능하게 만든다는 주장이다. - -그 주장을 깨는 서명이 있는지 확인했다. - -## 결론 - -MongoCollectionProfileRegistry:12~:15 가 근거를 적는다. 물리 이름은 기동 시점에 고정되고, 그 대응을 아는 코드가 이 클래스뿐이라는 것이다. - -MongoOperationContext 도 같은 것을 요구한다. 호출자가 넘겨야 하는 다섯 값 가운데 하나가 :18 의 CollectionProfileName collectionProfile 이다. - -collections.require(...) 를 부르는 main 줄은 셋이다. DefaultMongoImperativeExecutor:81 과 DefaultReactiveMongoExecutor:130 과 SpringReactiveChangeStreamSource:47 이다. - -PolicyAwareMongoAggregationExecutor 의 execute 가 다르다. :63 이 컨텍스트와 별개로 컬렉션 이름을 String 인자로 받고, :66 의 널 검사 말고는 어떤 검사도 거치지 않고 :76 의 operations.aggregate(...) 로 그대로 간다. - -같은 모양이 하나 더 있다. MongoReactiveCursorPublisher:37 도 String collection 을 받고 :44 의 널 검사만 거쳐 :64 의 operations.find(...) 에 넘긴다. 그쪽은 컨텍스트를 아예 읽지 않는다 — 받아서 널인지만 본다. - -그 파일은 MongoCollectionProfileRegistry 를 한 줄도 언급하지 않고 context.collectionProfile() 도 부르지 않는다. import 열둘 가운데 레지스트리가 없다. - -집계 실행기는 같은 컨텍스트에서 timeout() 은 읽는다. :72 가 budgetFor(context, budget) 을 부르고 그 메서드 :92 가 context.timeout() 을 밀리초로 바꿔 서버 데드라인을 좁힌다. collectionProfile() 만 읽지 않는다. - -둘 다 지금 조립되지 않는다. new PolicyAwareMongoAggregationExecutor 와 new MongoReactiveCursorPublisher 를 부르는 main 줄이 각각 0 이다. 대조로 센 new MongoBudgetEnforcer 는 main 에 한 줄 있다. 이름이 걸린 시험조차 집계 실행기를 인스턴스로 만들지 않는다. - -포크가 둘 중 하나를 배선하면 그 강제가 사라진다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 레지스트리 자바독과 require 인용, 컨텍스트 타입의 자바독과 필드 인용, require 를 부르는 main 자리 전수, 집계 실행기의 서명과 본문 인용, 그 파일의 레지스트리 언급 계수와 collectionProfile() 계수와 import 전수, 그 실행기의 등장 자리 전수와 생성 계수와 대조 계수 -소스 수정 : x - -## 재현 조건 - -1. 레지스트리의 클래스 자바독과 require 를 인용한다. -2. MongoOperationContext 의 자바독과 필드를 인용한다. -3. collections.require(...) 를 부르는 main 자리를 전부 찾는다. -4. 집계 실행기의 execute 서명과 드라이버 호출까지의 본문을 인용한다. -5. 그 파일이 레지스트리를 언급하는 줄과 collectionProfile() 을 부르는 줄을 세고 import 를 전부 나열한다. -6. 그 실행기의 이름이 나오는 자리를 소스 세트와 함께 전부 나열하고, 생성 줄을 세고 대조 이름으로 같은 검색을 건다. - -## 본문 - - - -`MongoCollectionProfileRegistry` 는 등록된 컬렉션 프로파일을 물리 이름으로 바꾼다. 클래스 자바독이 그 간접이 무엇을 보장하는지 적는다. - -## 레지스트리 자바독이 적은 보장 - -:::evidence key="analysis-finding-a06-f010" alt="저장소 루트에서 돌린 정적 검색 출력 242줄. 먼저 MongoCollectionProfileRegistry 9~45번이 실린다. 9~16번 자바독은 이것이 등록된 컬렉션 프로파일을 물리 컬렉션 이름으로 대응시키며, 이 간접이 동적 컬렉션 이름 금지를 강제 가능하게 만드는 것이고, 애플리케이션 코드는 프로파일을 이름 짓고 물리 이름을 아는 것은 이 레지스트리뿐이며 기동 시점에 고정되고, 따라서 요청 값에서 조립된 컬렉션 이름은 드라이버에 도달할 수 없는데 문자열에서 컬렉션으로 가는 경로 중 여기를 지나지 않는 것이 없기 때문이라고 적는다. 35~40번의 require 가 등록되지 않은 프로파일이면 MongoOperationRejectedException 을 던진다. 이어서 DefaultMongoImperativeExecutor 80~84번이 실려 81번이 collections.require 로 컨텍스트의 컬렉션 프로파일을 물리 이름으로 바꾸는 것이 보인다. 그 아래 require 를 부르는 main 줄이 셋 나오는데 SpringReactiveChangeStreamSource 47번과 DefaultMongoImperativeExecutor 81번과 DefaultReactiveMongoExecutor 130번이다. 다음으로 MongoOperationContext 1~40번이 실린다. 8~13번 자바독은 이것이 모든 플랫폼 연산이 요구하는 불변 실행 컨텍스트이고 실행 경로에 닿는 유일한 길로 일부러 만들어졌으며 호출자에게 연산 이름과 데이터베이스 프로파일과 컬렉션 프로파일과 요청하는 일관성 보장과 양수 타임아웃을 이름 짓게 만들고, 여기에는 문서도 테넌트도 사용자도 식별하는 것이 없어서 컨텍스트 전체를 편집 단계 없이 텔레메트리에 붙일 수 있다고 적는다. 15~20번이 다섯 필드를 선언하는데 그중 하나가 CollectionProfileName collectionProfile 이다. 이어서 PolicyAwareMongoAggregationExecutor 36~76번이 실린다. 59~64번의 execute 서명이 MongoOperationContext 와 MongoAggregationProfile 과 MongoAggregationPlan 과 String collection 과 Class 출력 타입 다섯을 받고, 65~67번이 널 검사를 하며 68번이 계획을 검증하고, 70~72번이 예산을 받아 좁히며, 74~75번이 Aggregation 을 만들고 76번이 operations.aggregate 에 aggregation 과 collection 과 outputType 을 넘긴다. 그 String 인자는 어떤 검사도 거치지 않고 드라이버로 간다. 다음으로 이 파일이 MongoCollectionProfileRegistry 를 언급하는 줄이 0 개이고 context.collectionProfile 을 부르는 줄도 0 개라고 나오며, import 열둘이 전부 나열되는데 MongoOperationContext 와 MongoOperationRejectedException 과 예산 관련 셋과 자바 표준 셋과 스프링 데이터 넷이고 레지스트리가 없다. 마지막으로 PolicyAwareMongoAggregationExecutor 이름이 나오는 자리가 소스 세트와 함께 나오는데 main 은 24번의 클래스 선언과 32번의 생성자 둘뿐이고 나머지 하나는 PolicyAwareMongoAggregationExecutorTest 14번이며, main 에서 그것을 만드는 줄이 0 개이고 대조로 센 new MongoBudgetEnforcer 는 1 개다." caption="레지스트리가 펴는 주장과 require · 그 주장을 지키는 세 자리 · 컨텍스트가 이미 들고 있는 컬렉션 프로파일 · 집계 실행기의 서명과 드라이버 호출 · 그 파일의 레지스트리 언급 0 과 import 전수 · main 참조 둘과 생성 0 — 242줄 · exit 0" zoom="true" -::: - -`:12`\~`:13` 이 근거를 적는다. 애플리케이션 코드는 프로파일을 이름 짓고, 물리 이름을 아는 것은 이 레지스트리뿐이며, 그 대응은 기동 시점에 고정된다. - -`:14`\~`:15` 가 결론이다. 요청에서 온 값으로 컬렉션 이름을 만들어도 드라이버까지 갈 수 없는데, 문자열에서 컬렉션으로 가는 경로 중 이 레지스트리를 지나지 않는 것이 없기 때문이다. - -`:35`\~`:40` 의 `require` 가 등록되지 않은 프로파일을 `MongoOperationRejectedException` 으로 거절한다. - -## 컨텍스트가 이미 들고 있는 CollectionProfileName - -`MongoOperationContext:10`\~`:13` 은 이 타입이 실행 경로에 닿는 유일한 길로 일부러 만들어졌다고 적는다. 호출자가 연산 이름과 두 프로파일과 일관성 보장과 양수 타임아웃을 넘겨야 한다. - -`:18` 이 `CollectionProfileName collectionProfile` 이다. 문자열이 아니라 프로파일 이름 타입이다. - -## collections.require(...) 를 부르는 세 자리 - -그 관문을 지나는 main 줄은 셋이다. `DefaultMongoImperativeExecutor:81` 과 `DefaultReactiveMongoExecutor:130` 이 컨텍스트의 프로파일을 물리 이름으로 바꾸고, `SpringReactiveChangeStreamSource:47` 이 구독의 프로파일로 같은 일을 한다. - -셋 다 컨텍스트나 구독에서 프로파일을 꺼내 레지스트리에 넣는다. 문자열을 받지 않는다. - -## 집계 실행기의 서명 - -`PolicyAwareMongoAggregationExecutor:59`\~`:64` 의 인자 목록에는 `MongoOperationContext` 와 `MongoAggregationProfile` 과 `MongoAggregationPlan` 과 출력 타입 말고 `String collection` 이 하나 더 있다. - -`:66` 이 그 문자열에 널 검사만 한다. `:68` 의 `validate(plan, profile)` 은 단계 수와 허용 목록과 `lookup` 대상 컬렉션을 보고 이 인자는 보지 않는다. `:76` 의 `operations.aggregate(aggregation, collection, outputType)` 이 그것을 드라이버에 그대로 넘긴다. - -생성자 `:32`\~`:33` 이 받는 것도 `MongoOperations` 와 예산 레지스트리와 예산 강제기 셋이다. 레지스트리가 없다. - -## 같은 모양이 리액티브 쪽에도 있다 - -`MongoReactiveCursorPublisher:32`\~`:39` 의 `stream` 도 `MongoOperationContext` 를 받으면서 `:37` 에서 `String collection` 을 따로 받는다. - -`:44` 가 널 검사를 하고, `:55`\~`:59` 가 질의를 복사해 배치 크기와 데드라인과 결과 상한을 걸고, `:64` 의 `operations.find(bounded, documentType, collection)` 이 그 문자열을 넘긴다. - -그 파일에서 `context` 가 나오는 줄은 `:33` 의 인자와 `:40` 의 널 검사 둘뿐이다. 컬렉션 프로파일을 한 번도 읽지 않는다. 레지스트리를 언급하는 줄도 0 이다. - -컨텍스트와 `String collection` 을 함께 받는 main 파일은 셋인데, `MongoAtomicOperationsTemplate` 은 공개 메서드가 컨텍스트만 받고 `String collection` 이 `:104`·`:123` 의 private 헬퍼 인자라 이 모양이 아니다. - -## 두 파일에는 레지스트리 import 도 collectionProfile() 호출도 없다 - -집계 실행기에서 `MongoCollectionProfileRegistry` 를 언급하는 줄이 0 이고 `context.collectionProfile()` 을 부르는 줄도 0 이다. 같은 두 검색을 `DefaultMongoImperativeExecutor` 에 걸면 2 와 1 이 나오므로, 두 0 은 검색이 도는 상태에서 나온 값이다. - -`import` 열둘이 전부 실려 있다. `MongoOperationContext` 와 `MongoOperationRejectedException` 과 예산 관련 셋과 자바 표준 셋과 스프링 데이터 넷이다. 레지스트리가 없다. - -컨텍스트를 아예 안 쓰는 것은 아니다. `:72` 가 `budgetFor(context, budget)` 을 부르고 그 private 메서드 `:89`\~`:98` 의 `:92` 가 `context.timeout()` 을 밀리초로 바꿔 등록된 예산과의 최솟값을 만든다. `:91` 주석이 컨텍스트의 타임아웃은 `maxTimeMS` 를 좁힐 수는 있어도 늘릴 수는 없다고 적는다. - -같은 컨텍스트에서 타임아웃은 꺼내고 컬렉션 프로파일은 꺼내지 않는다. - -## 지금은 조립되지 않는다 - -이 클래스 이름이 main 에 나오는 줄은 `:24` 의 클래스 선언과 `:32` 의 생성자 둘뿐이다. 나머지 하나는 `PolicyAwareMongoAggregationExecutorTest:14` 다. - -`new PolicyAwareMongoAggregationExecutor` 를 부르는 main 줄이 0 이고 `new MongoReactiveCursorPublisher` 도 0 이다. 같은 검색을 `new MongoBudgetEnforcer` 로 걸면 1 이 나오므로, 이 리프에도 자동 설정이 만드는 빈은 있다. - -이름이 걸린 `PolicyAwareMongoAggregationExecutorTest` 조차 그 실행기를 만드는 줄이 0 이다. - -그래서 레지스트리 자바독의 문장은 지금 성립한다. 포크가 둘 중 하나를 조립하고 요청 값으로 컬렉션 이름을 만들면 성립하지 않게 된다. - -## 원문이 든 서명 말고 하나가 더 있다 - -원문은 집계 실행기의 서명을 그 경로로 든다. `MongoReactiveCursorPublisher:37` 도 같은 모양인데, 그쪽은 컨텍스트를 받아 널 검사만 하고 프로파일을 한 번도 읽지 않는다. - -고치는 비용도 원문에 없다. 컨텍스트가 이미 `CollectionProfileName` 을 들고 있으므로, `String` 인자를 지우고 `context.collectionProfile()` 을 쓰는 것은 새 인자를 만드는 일이 아니다. - -## 확인하지 못한 것 - -이 실행기를 조립한 예시가 저장소에 없어 실제 사용 모양을 보지 못했다. - -그 인자를 프로파일 타입으로 바꿀 때 집계 계획 쪽이 무엇을 더 요구하게 되는지 따져 보지 않았다. - -리액티브 쪽 집계 경로에 같은 서명이 있는지 따로 훑지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f014.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f014.md deleted file mode 100644 index 632bd32..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f014.md +++ /dev/null @@ -1,148 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a06-f014 -title: 인덱스 diff 는 열네 요소 중 keySignature 와 unique 만 비교한다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a06-f014 -evidenceCapturedOn: 2026-09-04 -body: case-analysis-finding-a06-f014.body.md -assets: - - key: analysis-finding-a06-f014 - file: ../../../final/evidence/rendered/analysis-finding-a06-f014.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a06-f014.txt -source: - - 원본 분석 절은 final/document.md#a06 §57 이다. ---- - -# 인덱스 diff 는 열네 요소 중 keySignature 와 unique 만 비교한다 - -`MongoIndexManifest:22`~`:35` 가 열네 요소를 선언하는데 `MongoIndexDescriptorView:15`~`:20` 은 여섯 컴포넌트만 갖는다. `MongoIndexDiffEngine.compare` 가 두 값을 함께 읽는 곳은 셋인데, `:49`~`:50` 은 양방향이고 `:55` 의 `hidden` 은 한 방향만 본다. - -## 관계 - -- **빠뜨림이 통과가 되는 게이트는 게이트가 아니다** - 이 사례가 그 규칙의 형태다. 견주지 않은 요소가 달라져도 `MongoIndexDiff` 의 네 목록이 전부 빈다. `MongoIndexDiff:9`~`:11` 은 그 결과를 CI 산출물로 설계했다고 적는데, 지금 그것을 만드는 프로덕션 코드는 없다. -- **문서와 상수가 서로 일치하는 것으로는 아무것도 증명되지 않는다** - 그 규칙이 요구하는 검사 경계 명시가 여기에 없다. 그 규칙 3 이 요구하는 검사 경계 명시가 여기에 없다. `MongoIndexDescriptorView:9` 가 축소 사실은 밝히지만, 그 결과 비교되지 않는 아홉 요소를 어디에도 적지 않는다. -- **TTL 규칙 셋을 가진 타입들을 부르는 프로덕션 코드가 없다** - 그 기록의 `expireAfter` 가 이 diff 에서 견주어지지 않는 열 요소 중 하나다. TTL 보존 기간을 바꿔도 빈 보고서가 나온다. - -## 문제 - -인덱스 드리프트 보고서는 선언한 인덱스와 서버에 있는 인덱스를 견주어 만든다. 매니페스트는 인덱스 하나에 대해 열네 가지를 선언한다. - -그 열넷 중 무엇이 실제로 견주어지는지 확인했다. - -## 결론 - -MongoIndexManifest:22~:35 의 요소는 열넷이고 MongoIndexDescriptorView:15~:20 은 여섯이다. 매니페스트 요소 가운데 뷰에 같은 이름이 있는 것은 name 과 unique 와 hidden 과 metadataOwnership 넷뿐이다. - -MongoIndexDiffEngine.compare:41~:58 이 매니페스트와 뷰를 대조한다. 두 값을 한 줄에서 함께 읽는 곳은 셋인데, :49~:50 이 keySignature 와 unique 로 change 를 정하고 :55 가 declared.hidden() && !actual.hidden() 로 hide 를 정한다. - -그 반대는 검사하지 않는다. 서버가 숨긴 인덱스를 매니페스트가 보인다고 선언한 경우인데, MongoIndexDiff:13~:14 가 적은 폐기·숨김·관측·승인 순서에서 숨김까지 간 인덱스를 다시 쓰기로 바꾸면 그 조합이 남는다. 그때 계획기는 그 인덱스를 쓰지 않는다. - -뷰에 대응 필드가 없는 열 요소는 어느 방향으로도 비교되지 않는다. 그 열에서 keys 하나만 keySignature 로 요약돼 들어가고, 남는 아홉에 expireAfter 가 있다. - -MongoIndexDiff:9~:11 은 이 결과가 CI 산출물이며 실행마다 순서가 바뀌면 이전 실행과 대조할 수 없어서 모든 목록을 정렬한다고 적는다. 네 목록이 모두 비면 render() 가 빈 문자열을 낸다. - -다만 그 산출물을 지금 만드는 프로덕션 코드는 없다. 엔진 이름이 main 에 나오는 줄이 자기 클래스 선언 하나뿐이고, 부르는 것은 시험 넷이다. - -MongoIndexDescriptorView:9~:12 는 그 축소 자체는 밝힌다. 무엇이 감지 불가가 되는지만 적지 않는다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 매니페스트와 뷰의 요소 전수 인용, 두 목록의 요소 계수와 자기검증, 요소마다 뷰에 있는지 이름으로 대조, 비교 엔진 본문 인용과 두 값을 함께 읽는 줄 계수와 전수, diff 레코드 전문 인용, 엔진의 main 등장 계수와 시험 호출 대조 -소스 수정 : x - -## 재현 조건 - -1. MongoIndexManifest 와 MongoIndexDescriptorView 의 레코드 헤더를 전문으로 싣는다. -2. 두 목록의 요소를 세고, 열넷이 아니면 스크립트가 실패하게 한다. -3. 매니페스트 요소마다 같은 이름이 뷰에 있는지 대조해 전부 나열한다. -4. MongoIndexDiffEngine.compare 를 인용하고 그 안에서 declared 와 actual 을 한 줄에서 함께 읽는 줄을 세고 전부 뽑는다. -5. MongoIndexDiff 의 클래스 자바독을 인용한다. -6. MongoIndexDiffEngine 이 main 에 나오는 줄을 세고, 그 엔진의 compare 를 부르는 시험 줄과 대조한다. - -## 본문 - - - -`MongoIndexDiffEngine` 이 매니페스트와 서버에서 읽은 뷰를 대조해 드리프트 보고서를 만든다. - -## 매니페스트가 선언하는 열넷 - -:::evidence key="analysis-finding-a06-f014" alt="저장소 루트에서 돌린 정적 검색 출력 192줄. 먼저 MongoIndexManifest 11~45번이 실린다. 12~19번 자바독은 이것이 diff 나 검토에 필요한 모든 것을 담은 선언된 인덱스 하나이고, expectedUsage 가 이 인덱스가 존재하는 연산을 이름 지어 이 인덱스가 아직 필요한지를 프로덕션 통계로 추측하지 않고 답할 수 있게 하며, metadataOwnership 이 누가 만들었는지를 적어 드리프트 정리가 선언하지 않은 암호화나 검색 인덱스를 지우자고 제안하지 못하게 한다고 적고, 키 순서가 표현상의 세부가 아니라 인덱스 정체성의 일부라 보존된다고 적는다. 21~35번의 record 헤더가 name 과 keys 와 unique 와 sparse 와 hidden 과 deprecated 와 partialFilterExpression 과 collationProfile 과 expireAfter 와 wildcardProjection 과 shardKeySupport 와 expectedUsage 와 owner 와 metadataOwnership 열넷을 선언한다. 이어서 MongoIndexDescriptorView 6~20번이 실린다. 7~12번 자바독은 이것이 서버에 실제로 있는 인덱스이며 D4 관리 클라이언트로 읽어 diff 가 견줄 수 있는 필드로 줄인 것이고, 소유권은 매니페스트를 믿는 대신 여기서 추론하는데 diff 의 목적 자체가 매니페스트가 모르는 인덱스를 찾는 것이며 그중 일부는 암호화나 검색에 속해 절대 삭제 후보가 되면 안 되기 때문이라고 적는다. 14~20번이 collection 과 name 과 keySignature 와 unique 와 hidden 과 metadataOwnership 여섯을 선언한다. 다음으로 MongoIndexDiffEngine 25~70번이 실린다. 41~58번 반복문이 선언된 인덱스마다 같은 이름의 뷰를 찾는데, 43~48번이 뷰가 없고 폐기 표시도 없으면 create 에 넣고, 49~50번이 keySignature 가 다르거나 unique 가 다르면 change 에 넣으며 51~52번 주석이 같은 이름에 다른 정의는 MongoDB 가 조용히 다시 만들지 않으므로 질의가 스캔을 시작할 때 발견되는 대신 재빌드가 계획돼야 한다고 적고, 55~57번이 선언은 숨김인데 서버는 아닐 때만 hide 에 넣는다. 60~67번은 매니페스트에 없는 서버 인덱스를 dropCandidates 에 넣는데 애플리케이션 드리프트로 삭제 가능한 소유권만 본다. 그 아래 한 줄에서 declared 값과 actual 값을 견주는 줄이 3 개라고 나오고 그 셋이 49번과 50번과 55번으로 실리며, 참고로 그 반복문에서 declared 나 actual 이 나오는 줄 전부도 함께 나온다. 이어서 매니페스트 요소가 14 개이고 뷰 요소가 6 개라고 나온 뒤, 매니페스트 요소마다 뷰에 있는지가 전부 나열되는데 name 과 unique 와 hidden 과 metadataOwnership 넷만 뷰에 있고 나머지 열은 뷰에 없다. 요소 수가 14 가 아니면 실패하는 자기검증도 함께 찍힌다. 마지막으로 MongoIndexDiff 1~49번이 실리는데 6~15번 자바독이 모든 목록이 정렬돼 있어 같은 입력에 대해 diff 가 바이트 단위로 같으며 실행마다 순서가 바뀌는 CI 산출물은 이전 실행과 견줄 수 없고 그것이 인덱스 드리프트 보고서의 목적 대부분이라고 적고, dropCandidates 는 이름 그대로 폐기와 숨김과 관측과 승인을 통과해야 무엇이든 삭제되는 제안일 뿐이라고 적으며, 31번의 empty 와 36~38번의 isClean 과 41~49번의 render 가 이어진다. 그 아래 MongoIndexDiffEngine 이 main 에 나오는 줄이 1 개인데 그것이 23번의 클래스 선언이고, 대조로 센 시험 쪽 engine.compare 호출은 MongoIndexDiffEngineTest 30·42·49·61번 네 줄이다." caption="매니페스트가 선언하는 열넷과 그 자바독 · 비교용 뷰가 나르는 여섯과 축소를 밝힌 자바독 · 비교 엔진 본문과 실제로 견주는 두 줄 · 요소마다 뷰에 있는지 대조한 전수와 자기검증 · CI 산출물이라 적은 diff 자바독과 호출 계수 — 192줄 · exit 0" zoom="true" -::: - -`MongoIndexManifest:22`\~`:35` 가 인덱스 하나에 대해 열네 요소를 선언한다. `name`, `keys`, `unique`, `sparse`, `hidden`, `deprecated`, `partialFilterExpression`, `collationProfile`, `expireAfter`, `wildcardProjection`, `shardKeySupport`, `expectedUsage`, `owner`, `metadataOwnership` 이다. - -`:14`\~`:17` 자바독은 그 열넷 가운데 둘을 따로 설명한다. `expectedUsage` 는 이 인덱스가 존재하는 연산을 이름 지어 "아직 필요한가" 를 프로덕션 통계로 추측하지 않고 답할 수 있게 하고, `metadataOwnership` 은 누가 만들었는지를 적어 드리프트 정리가 선언하지 않은 암호화나 검색 인덱스를 지우자고 제안하지 못하게 한다. - -## 비교용 뷰가 선언하는 여섯 필드 - -`MongoIndexDescriptorView:15`\~`:20` 이 여섯 컴포넌트를 선언한다. `collection`, `name`, `keySignature`, `unique`, `hidden`, `metadataOwnership` 이다. - -`:9`\~`:12` 자바독이 그 축소를 밝힌다. D4 관리 클라이언트로 읽어 diff 가 비교할 수 있는 필드로 줄였다는 것이다. 소유권을 매니페스트에서 받지 않고 여기서 추론하는 이유도 적는다 — diff 의 목적이 매니페스트가 모르는 인덱스를 찾는 것이고, 그중 일부는 암호화나 검색에 속해 삭제 후보가 되면 안 되기 때문이다. - -그 축소로 어떤 요소가 비교 대상에서 빠지는지는 어디에도 적혀 있지 않다. - -## compare 가 declared 와 actual 을 함께 읽는 세 줄 - -`MongoIndexDiffEngine.compare:41` 이 선언된 인덱스마다 같은 이름의 뷰를 찾는다. - -`:43`\~`:48` 이 뷰가 없고 폐기 표시도 없으면 `create` 에 넣는다. - -`:49`\~`:50` 이 `keySignature` 가 다르거나 `unique` 가 다르면 `change` 에 넣는다. `:51`\~`:52` 주석은 같은 이름에 다른 정의를 MongoDB 가 조용히 다시 만들지 않으므로, 질의가 스캔을 시작할 때 발견되는 대신 재빌드가 계획돼야 한다고 적는다. - -`:55`\~`:57` 이 `declared.hidden() && !actual.hidden()` 일 때 `hide` 에 넣는다. - -한 줄에서 선언 값과 서버 값을 함께 읽는 줄은 셋이다. `:49` 와 `:50` 과 `:55` 다. 앞의 둘이 `change` 를 결정하고 `:55` 가 `hide` 를 결정한다. - -## 한 방향만 보는 검사 - -`:55` 의 조건은 선언이 숨김이고 서버가 아닌 경우다. - -반대에 대한 분기가 없다. 서버에서는 숨겨져 있는데 매니페스트가 보인다고 선언한 상태다. - -이 상태에서는 매니페스트가 `hidden` 을 거짓으로 선언한 인덱스를 서버가 숨겨 두고 있고, 질의 계획기는 숨겨진 인덱스를 쓰지 않는다. 보고서는 그것을 어느 목록에도 넣지 않는다. - -## 뷰에 없는 열 요소 - -매니페스트 요소마다 뷰에 같은 이름이 있는지 대조했다. `name` 과 `unique` 와 `hidden` 과 `metadataOwnership` 넷만 있다. 뷰 요소가 여섯이 아니면 스크립트가 실패한다. - -나머지 열은 없다. `keys`, `sparse`, `deprecated`, `partialFilterExpression`, `collationProfile`, `expireAfter`, `wildcardProjection`, `shardKeySupport`, `expectedUsage`, `owner` 다. - -`keys` 만 `keySignature` 라는 다른 이름으로 요약돼 들어간다. 나머지 아홉에는 뷰에 대응하는 컴포넌트가 없다. - -그 아홉에 `expireAfter` 가 있다. 30일을 1일로 바꾸는 것은 대량 삭제인데, 이 diff 는 그것을 차이로 보고하지 않는다. - -## 네 목록이 모두 비었을 때의 산출물 - -`MongoIndexDiff:9`\~`:11` 은 모든 목록을 정렬해 같은 입력에 대해 바이트 단위로 같은 결과를 낸다고 적는다. 실행마다 순서가 바뀌는 CI 산출물은 이전 실행과 견줄 수 없고 그것이 드리프트 보고서의 목적 대부분이기 때문이다. - -`:13`\~`:14` 는 `dropCandidates` 가 이름 그대로 제안일 뿐이며 폐기와 숨김과 관측과 승인을 통과해야 무엇이든 삭제된다고 적는다. - -`:41`\~`:49` 의 `render()` 가 네 목록을 이어 붙인다. 모두 비면 결과가 빈 문자열이다. 비교하지 않는 아홉 요소가 달라져도 같은 결과다. - -다만 지금 그 보고서를 만드는 프로덕션 코드는 없다. `MongoIndexDiffEngine` 이 main 에 나오는 줄이 `:23` 의 클래스 선언 하나뿐이고, 그 엔진의 `compare` 를 부르는 것은 `MongoIndexDiffEngineTest:30`·`:42`·`:49`·`:61` 네 줄이다. - -## 원문과 갈리는 자리 - -원문은 비교되는 것이 `keySignature` 와 `unique` 둘이고 `hidden` 이 한 방향이라고 적는다. 그 판정은 그대로다. 다만 한 줄에서 두 값을 함께 읽는 줄로 세면 `:55` 를 포함해 셋이다. - -여기에 더한 것은 둘이다. 하나는 뷰에 무엇이 없는지를 이름으로 전부 맞춰 본 결과다 — 매니페스트 열넷 가운데 뷰에 이름이 있는 것은 넷이고 `keys` 는 `keySignature` 로 형태를 바꿔 들어가므로 감지 불가가 되는 것은 아홉이다. 다른 하나는 그 엔진이 아직 프로덕션에서 불리지 않는다는 것이다. - -## 확인하지 못한 것 - -선언과 서버 상태를 실제로 넣어 빈 보고서가 나오는 것을 프로브로 보이지 않았다. - -이 보고서가 어느 CI 단계의 산출물이 되는지 워크플로 파일을 훑지 않았다. - -`keySignature` 의 생성 규칙을 열어 보지 않았다. 키 순서가 그 서명에 담기는지 확인하지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f015.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f015.md deleted file mode 100644 index 710b173..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f015.md +++ /dev/null @@ -1,148 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a06-f015 -title: MongoTtlPolicyValidator 를 부르는 프로덕션 코드가 없다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a06-f015 -evidenceCapturedOn: 2026-09-04 -body: case-analysis-finding-a06-f015.body.md -assets: - - key: analysis-finding-a06-f015 - file: ../../../final/evidence/rendered/analysis-finding-a06-f015.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a06-f015.txt -source: - - 원본 분석 절은 final/document.md#a06 §58 이다. ---- - -# MongoTtlPolicyValidator 를 부르는 프로덕션 코드가 없다 - -`MongoIndexManifest:30` 의 `expireAfter` 에 걸리는 검사는 `:54` 의 음수 거절 하나다. `MongoTtlPolicyValidator` 는 오버로드 셋에 걸쳐 다섯 경우를 거절하는데, `schema/ttl` 밖 main 에서 `MongoTtlPolicy` 와 `MongoTtlIndexDescriptor` 와 `MongoExpirationAccessPolicy` 와 `MongoTtlPolicyValidator` 를 언급하는 줄이 0 이다. - -## 관계 - -- **같은 개념의 두 어휘가 공존하면 하나를 죽은 것으로 표시한다** - 이 사례가 그 규칙의 형태다. 두 표현 사이에 참조가 없어서 어느 쪽이 정본인지 코드가 말하지 않는다. -- **diff 가 견주는 것은 열넷 중 keySignature 와 unique 둘이다** - 그 기록의 감지 불가 목록에 `expireAfter` 가 들어 있다. 매니페스트로 선언한 TTL 은 검증도 받지 않고 드리프트로도 잡히지 않는다. -- **중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다** - 그 규칙 2 대로 만들어지는 것과 불리는 것을 나눠 세면 둘 다 0 이다. `MongoIndexManifest` 를 밖에서 만드는 main 줄도, `MongoTtlPolicyValidator` 를 만드는 main 줄도 없다. - -## 문제 - -매니페스트의 한 요소와 schema/ttl 패키지가 각각 TTL 을 표현한다. - -둘이 서로를 아는지, 그리고 어느 쪽이 실제로 쓰이는지 확인했다. - -## 결론 - -MongoIndexManifest:30 이 Duration expireAfter 를 선언한다. 그 필드에 걸리는 검사는 :54~:56 의 음수 거절 하나다. :72~:73 의 ttl() 이 Optional 을 돌려주고 :77~:78 의 isTtlIndex() 가 널 여부를 돌려준다. 값의 크기를 보는 코드는 없다. - -MongoTtlPolicyValidator 가 거절하는 경우는 다섯이고 세 오버로드에 나뉘어 있다. validate(MongoTtlPolicy) 가 :30·:39·:49 에서 정확한 만료 시각 주장과 질의 시점 검사 누락과 1 분 미만 보존 기간을, validate(MongoTtlIndexDescriptor):68 이 날짜가 아닌 만료 필드 타입을, validate(MongoExpirationAccessPolicy):86 이 읽기 필터 미적용을 거절한다. - -:10~:13 자바독에는 이 클래스가 막는 두 오용이 적혀 있다. 스윕 간격이 명세되지 않은 장치를 스케줄러로 쓰는 것과, 아직 지워지지 않은 문서가 읽히는데도 접근 제어로 기대는 것이다. - -두 표현 사이에 참조가 없다. schema/ttl 밖 main 에서 이 네 타입을 언급하는 줄이 0 이고, 같은 검색이 schema/ttl 안에서는 22 줄을 찾는다. MongoIndexManifest 도 그 네 이름을 한 번도 쓰지 않는다. - -isTtlIndex() 와 ttl() 을 부르는 줄도 0 이다. 두 이름이 나오는 줄은 선언 둘뿐이다. - -그래서 매니페스트 경로로 선언된 TTL 인덱스에는 그 다섯 중 어느 것도 걸리지 않는다. 안전 하한이 1 분으로 선언돼 있는데 매니페스트 쪽에는 그 값을 읽는 코드가 없어서 1 초짜리 보존 기간도 통과한다. - -그리고 어느 쪽도 아직 배선되지 않았다. MongoIndexManifest.named(...) 나 new MongoIndexManifest 를 부르는 main 줄이 :226 의 자기 빌더뿐인데 시험에는 7 이 있고, 검증기를 만드는 main 줄은 0 이며 시험에 1 이 있다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 매니페스트의 TTL 요소와 그 검사와 접근자와 빌더 인용, 검증기 파일 전문 인용과 세 오버로드의 다섯 거절 확인, 정책 타입 인용, schema/ttl 밖 main 참조 계수와 안쪽 대조, 매니페스트가 그 네 타입을 언급하는 줄 계수, 두 접근자 호출 계수와 선언 포함 대조, 두 표현의 생성 계수와 시험 대조 -소스 수정 : x - -## 재현 조건 - -1. 매니페스트의 expireAfter 선언과 그 필드에 걸리는 검사와 접근자와 빌더 메서드를 인용한다. -2. MongoTtlPolicyValidator 를 파일 끝까지 싣고 세 오버로드가 각각 무엇을 거절하는지 본다. -3. MongoTtlPolicy 를 인용해 정책이 무엇을 요구하는지 보인다. -4. schema/ttl 밖 main 에서 네 타입을 언급하는 줄을 세고, 같은 검색을 schema/ttl 안에 걸어 대조한다. -5. 매니페스트가 그 네 타입을 언급하는 줄과 두 접근자를 부르는 줄을 세고, 선언까지 포함한 계수와 대조한다. -6. 두 표현을 만드는 main 줄을 각각 세고 시험 쪽 계수와 대조한다. - -## 본문 - - - -이 범위 안에 TTL 을 표현하는 방법이 둘 있다. - -## MongoIndexManifest 의 expireAfter - -:::evidence key="analysis-finding-a06-f015" alt="저장소 루트에서 돌린 정적 검색 출력 187줄. 먼저 MongoIndexManifest 28~32번이 실려 30번의 Duration expireAfter 선언과 그 앞뒤 필드가 보이고, 50~58번의 압축 생성자에서 54~56번이 expireAfter 가 널이 아니고 음수면 거절한다. 68~80번에는 72~73번의 ttl 접근자가 Optional.ofNullable 을 돌려주고 77~78번의 isTtlIndex 가 널이 아닌지를 돌려준다. 66~80번에 named 팩터리가 있고 186~194번의 빌더 메서드 expireAfter 는 널 검사만 한다. 그 아래 expireAfter 라는 이름이 그 파일에 나오는 줄이 8 개이고 그중 값을 거절하는 줄이 1 개라고 나온다. 이어서 MongoTtlPolicyValidator 7~94번 전문이 실린다. 7~14번 자바독은 TTL 인덱스가 오용되는 두 방식을 거절한다고 적는데, 첫째는 그것을 스케줄러로 다루는 것이며 TTL 모니터의 스윕 간격이 명세되지 않고 부하에 따라 달라져서 아홉 시에 지우라는 것은 약속하지 않는다는 것이고, 둘째는 접근 제어로 기대는 것이며 아직 존재하는 문서는 여전히 읽히므로 만료가 데이터를 가려야 한다면 읽기 쪽이 그것을 말해야 한다는 것이다. 17~21번이 MINIMUM_SAFE_RETENTION 을 1 분으로 두는데 그 아래로 줄이면 한 번의 스윕으로 큰 모집단이 만료돼 설정 변경이 계획되지 않은 대량 삭제가 된다고 적는다. 28~58번의 첫 validate 가 셋을 던지는데 30번이 정확한 업무 전이를 주장하는 정책을, 39번이 질의 시점 만료 검사를 선언하지 않은 정책을, 49번이 최소 보존 기간보다 짧은 것을 거절한다. 60~77번의 두 번째 오버로드는 TTL 인덱스 서술자를 받아 67번에서 정책 검증을 위임하고 68~76번에서 만료 필드가 BSON 날짜로 저장되지 않으면 MongoDB 가 날짜 필드만 만료시키고 나머지는 조용히 무시한다는 메시지로 거절하며, 79~93번의 세 번째 오버로드는 읽기 쪽 술어를 받아 86~92번에서 그것이 적용되지 않으면 만료된 문서가 TTL 모니터가 지울 때까지 계속 보인다는 메시지로 거절한다. 다음으로 MongoTtlPolicy 1~40번이 실린다. 9~12번 자바독은 TTL 모니터가 대략 1 분에 한 번 돌며 배치로 지우므로 만료된 문서가 명세되지 않은 간격 동안 계속 읽히고 부하가 높거나 바쁜 세컨더리에서는 더 길어지는데, 그것이 공간 회수에는 괜찮고 특정 시점에 보이지 않아야 하는 것에는 틀렸다고 적는다. 14~16번은 그래서 두 불린이 선언의 일부이며 physicalCleanupOnly 는 이 TTL 이 공간만 회수한다는 뜻이고 queryChecksLogicalExpiry 는 애플리케이션이 expiresAt 이 현재보다 큰지로 걸러 가시성이 모니터의 타이밍에 달리지 않게 한다는 뜻이라고 적는다. 18~22번이 field 와 retention 과 두 불린 넷을 선언한다. 이어서 schema/ttl 밖 main 에서 그 네 타입을 언급하는 줄이 0 개이고 schema/ttl 안에서는 22 개이며, MongoIndexManifest 가 그 네 타입을 언급하는 줄이 0 개이고, isTtlIndex 나 ttl 을 부르는 줄이 0 개인데 선언까지 포함하면 2 개다. 마지막으로 MongoIndexManifest.named 나 new MongoIndexManifest 를 부르는 main 줄이 1 개인데 그것이 226번의 자기 빌더이고 시험에서는 7 개이며, new MongoTtlPolicyValidator 를 부르는 main 줄이 0 개이고 시험에서는 1 개다." caption="매니페스트의 expireAfter 와 거기 걸리는 음수 검사와 두 접근자와 빌더 · 검증기 전문과 세 규칙과 최소 보존 기간 · 정책 타입이 두 불린으로 요구하는 것 · schema/ttl 밖 참조 0 과 안쪽 22 · 두 접근자 호출 0 과 선언 2 · 두 표현의 생성 계수 — 187줄 · exit 0" zoom="true" -::: - -`MongoIndexManifest` 는 TTL 을 요소 하나로 담는다. `:30` 의 `Duration expireAfter` 다. - -그 필드에 걸리는 검사는 하나다. `:54`\~`:56` 이 널이 아니면서 음수인 경우를 `IllegalArgumentException` 으로 거절한다. - -`:72`\~`:73` 의 `ttl()` 이 `Optional.ofNullable(expireAfter)` 를 돌려주고, `:77`\~`:78` 의 `isTtlIndex()` 가 널이 아닌지를 돌려준다. - -`:189`\~`:191` 의 빌더 메서드는 널 검사만 한다. 값의 크기를 보지 않는다. - -## MongoTtlPolicyValidator 가 거절하는 다섯 경우 - -`MongoTtlPolicyValidator` 는 `validate` 오버로드를 셋 갖고, 그 셋에 걸쳐 다섯 경우를 거절한다. - -`validate(MongoTtlPolicy):28`\~`:58` 이 그중 셋이다. - -`:30` 이 정확한 업무 전이를 주장하는 정책에 `MongoOperationRejectedException` 을 던진다. 메시지가 MongoDB 의 TTL 모니터는 명세되지 않은 간격으로 스윕하므로 정확한 전이에는 자기 스케줄러가 필요하고 TTL 인덱스는 물리적 정리로 남는다고 적는다. - -`:39` 가 질의 시점 만료 검사를 선언하지 않은 정책을 거절한다. 만료된 문서는 모니터가 지울 때까지 계속 읽히므로 읽기가 만료 필드를 현재와 견주어야 한다는 것이다. - -`:49` 가 `MINIMUM_SAFE_RETENTION` 보다 짧은 보존 기간을 거절한다. `:21` 이 그 값을 1 분으로 두고, `:18`\~`:19` 가 그 아래로 줄이면 한 번의 스윕으로 큰 모집단이 만료돼 설정 변경이 계획되지 않은 대량 삭제가 된다고 적는다. - -`:10`\~`:13` 자바독이 두 오용을 이름 짓는다. TTL 을 스케줄러로 다루는 것과 접근 제어로 기대는 것이다. - -나머지 둘은 다른 오버로드에 있다. `validate(MongoTtlIndexDescriptor):65`\~`:77` 이 `:67` 에서 정책 검증을 위임한 뒤 `:68` 에서 만료 필드가 BSON 날짜가 아니면 거절하는데, MongoDB 가 날짜 필드만 만료시키고 나머지는 조용히 무시하기 때문이다. `validate(MongoExpirationAccessPolicy):84`\~`:93` 이 `:86` 에서 읽기가 논리적 만료를 걸지 않으면 거절한다. - -`MongoTtlPolicy:18`\~`:22` 가 그 검증을 받는 값이다. `field` 와 `retention` 말고 `physicalCleanupOnly` 와 `queryChecksLogicalExpiry` 두 불린이 선언의 일부다. `:9`\~`:12` 자바독에는 모니터가 대략 1 분에 한 번 배치로 지우므로 만료된 문서가 명세되지 않은 간격 동안 계속 읽히고 부하가 높거나 바쁜 세컨더리에서는 더 길어진다고 적혀 있다. - -## 둘 사이에 참조가 없다 - -`schema/ttl` 밖의 main 에서 `MongoTtlPolicy` 나 `MongoTtlIndexDescriptor` 나 `MongoExpirationAccessPolicy` 나 `MongoTtlPolicyValidator` 를 언급하는 줄이 0 이다. - -같은 검색을 `schema/ttl` 안에 걸면 22 줄이 나오므로, 이 0 은 검색이 대상을 못 찾아서 나온 값이 아니다. - -매니페스트 파일 자체에도 그 네 이름이 한 번도 나오지 않는다. - -반대 방향도 없다. `isTtlIndex()` 나 `ttl()` 을 부르는 줄이 0 이고, 두 이름이 나오는 줄은 `:77` 과 `:72` 의 선언뿐이다. - -## 매니페스트 경로가 지나지 않는 다섯 검사 - -매니페스트로 선언한 TTL 인덱스는 그 다섯 중 어느 것도 지나지 않는다. - -`MINIMUM_SAFE_RETENTION` 이 1 분인데 `:189`\~`:191` 의 빌더는 널 검사만 하므로 1 초짜리 값도 그대로 들어간다. 실제로 넣어 보지는 않았다. `expireAfter` 는 앞의 기록에서 확인한 대로 인덱스 diff 가 비교하지 않는 아홉 요소 중 하나이므로, 그 값을 바꿔도 드리프트 보고서가 빈다. - -## 둘 다 밖에서 만들어지지 않는다 - -`MongoIndexManifest.named(...)` 나 `new MongoIndexManifest` 를 부르는 main 줄은 1 인데 그것이 `:226` 의 자기 빌더다. 밖에서 만드는 코드는 없고, 같은 검색이 시험에서는 7 을 찾는다. - -`new MongoTtlPolicyValidator` 를 부르는 main 줄은 0 이고 시험에 1 이 있다. - -지금은 두 표현 다 밖에서 만들어지지 않아 이 차이가 드러나지 않는다. 포크가 `MongoIndexManifest` 를 조립하면 `expireAfter` 는 `:54`\~`:56` 의 음수 거절만 지나고 검증기의 다섯 조건은 걸리지 않는다. - -## 원문과 갈리는 자리 - -두 표현 사이에 참조가 없고 규칙을 가진 쪽이 쓰이지 않는다는 판정은 그대로다. - -규칙의 목록이 갈린다. 원문이 든 셋은 최소 보존 기간과 만료 필드의 BSON 타입과 읽기의 만료 검사다. 실제 파일에는 그 셋 말고 `:30` 의 정확한 만료 시각 주장 거절이 더 있고, 원문이 든 셋 가운데 BSON 타입은 첫 오버로드가 아니라 `:68` 에 있다. 세 오버로드를 다 세면 다섯이다. - -그리고 그 0 들이 검색 실패가 아니라는 대조를 더했다. 같은 검색이 `schema/ttl` 안에서 22 줄을 찾고, 두 접근자는 선언까지 포함하면 2 줄이 나오며, 매니페스트 생성 검색은 시험에서 7 을 찾는다. - -## 확인하지 못한 것 - -빌더나 매니페스트로 1 초짜리 TTL 을 선언해 실제로 통과하고 만들어지는 것을 프로브로 재현하지 않았다. - -`MongoExpirationAccessPolicy` 자체는 열지 않았다. 확인한 것은 검증기가 그 타입에 대한 오버로드를 갖는다는 데까지다. - -설계가 둘 중 어느 쪽을 정본으로 두었는지 설계 문서를 읽어 판단하지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f019.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f019.md deleted file mode 100644 index 0f98fc3..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f019.md +++ /dev/null @@ -1,145 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a06-f019 -title: MongoChangeHistoryLostException 을 만드는 코드가 없다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a06-f019 -evidenceCapturedOn: 2026-09-04 -body: case-analysis-finding-a06-f019.body.md -assets: - - key: analysis-finding-a06-f019 - file: ../../../final/evidence/rendered/analysis-finding-a06-f019.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a06-f019.txt -source: - - 원본 분석 절은 final/document.md#a06 §69 이다. ---- - -# MongoChangeHistoryLostException 을 만드는 코드가 없다 - -`MongoChangeHistoryLostException` 의 클래스 자바독은 이 예외가 왜 전용 타입인지를 적고, `docs/mongodb/change-stream-guide.md:75` 는 그것이 던져진다고 적는다. 그런데 자바 소스에서 그 이름이 나오는 두 줄은 둘 다 자기 선언이고, 히스토리 유실을 재현한 시험은 호출자가 받는 예외를 `MongoQueryException` 으로 단언한다. - -## 관계 - -- **타입이 문서화한 불변식은 타입이 강제한다** - `MongoChangeHistoryLostException:11`~`:14` 가 복구를 업무 결정이라 부르며 전용 타입을 둔 이유를 적는데, 그 예외를 `new` 하는 줄이 main 에도 시험에도 없다. -- **중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다** - `onFailure:53`~`:55` 가 히스토리 유실을 자기 안에서 처리하므로 `onHistoryLost:29` 는 같은 결정을 만드는 두 번째 구현이다. -- **TTL 규칙 셋을 가진 타입들을 부르는 프로덕션 코드가 없다** - 두 기록 모두 자바독에 규칙을 적어 둔 타입이 선언만 있고, 그것을 만들거나 부르는 프로덕션 코드가 0 이다. - -## 문제 - -이 패키지는 스트림 실패 뒤의 처리를 담고, 네 파일이 정책과 결정과 무효화 복구와 전용 예외를 나눠 맡는다. - -그중 무엇이 프로덕션 호출자를 갖는지 확인했다. - -## 결론 - -MongoChangeStreamRecoveryPolicy 는 실패 종류마다 진입점을 두어 넷이 있는데, 프로덕션에서 불리는 것은 onFailure:51 뿐이다. 나머지 셋 가운데 둘은 시험만 부르고 onInvalidate 는 아무도 부르지 않는다. - -그 이유가 onFailure 안에 있다. :53~:55 가 히스토리 유실 서버 코드를 자기 안에서 걸러 onHistoryLost:31~:32 와 똑같은 결정을 만든다. - -무효화 복구 쪽도 둘 중 하나만 불린다. checkpointFor 는 프로덕션 호출이 없고, 복구 패키지 밖 타입인 MongoChangeStreamState:32 의 autoResumable() 도 마찬가지다. - -그 하나뿐인 호출이 자기 자신과 견준다. ReactiveMongoChangeStreamConsumer:119 가 체크포인트와 그 체크포인트의 위치를 두 인자로 넘기는데, MongoInvalidateRecovery:35 의 검사는 그 둘이 다른지를 본다. - -MongoChangeHistoryLostException 은 어디에서도 만들어지지 않는다. 그 이름이 자바 소스에 나오는 줄이 둘뿐이고 둘 다 그 파일 자신의 선언이며, new 를 부르는 줄은 main 에도 시험에도 없다. - -:11~:14 자바독이 그 타입이 왜 전용이어야 하는지 적는다. 남은 선택지가 투영 재생성과 원본 재조정 둘뿐이고 어느 쪽도 코드가 혼자 고를 수 없다는 것이다. - -실제로는 상태만 HISTORY_LOST 가 되고 드라이버 예외가 그대로 나간다. ChangeStreamConsumerLifecycleTest:115 가 verifyError(MongoQueryException.class) 로 그것을 고정한다. 그래서 호출자는 예외 타입으로 이 상황을 구분할 수 없고 MongoChangeStreamState 를 확인해야 한다. - -문서 셋이 그 반대를 적는다. docs/mongodb/change-stream-guide.md:71 이 히스토리 유실을 이 예외로 잇고 :75 가 그것이 던져진다고 적으며, docs/mongodb/runbooks/history-lost.md:23 이 증상 목록에 그 이름을 올린다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 복구 패키지의 파일 전수, 정책 전문 인용, 진입점마다 main 과 시험 호출 계수와 대조, 전용 예외의 자바독과 그 이름이 자바 소스와 문서에 나오는 줄 전수, onFailure 의 히스토리 유실 분기 인용, 소비자가 복구를 부르는 자리 전수, requireCorrectResumeOption 정의와 호출처 전수, 히스토리 유실 시 호출자가 받는 것을 고정한 시험 인용, 상태 열거형 자바독과 autoResumable 인용 -소스 수정 : x - -## 재현 조건 - -1. 복구 패키지의 파일을 전부 나열하고 정책을 전문으로 싣는다. -2. 진입점마다 main 과 시험에서 부르는 줄을 각각 센다. -3. 전용 예외의 이름이 자바 소스에 몇 줄 나오는지 세고 전부 나열한 뒤, 같은 이름을 문서가 쓰는 자리도 함께 싣는다. -4. 그 예외의 클래스 자바독을 인용한다. -5. onFailure 의 히스토리 유실 분기와 소비자가 복구를 부르는 자리를 인용한다. -6. requireCorrectResumeOption 의 검사와 그것을 부르는 자리를 전부 나열한다. -7. 히스토리 유실 시 호출자가 받는 예외 타입을 고정한 시험을 인용한다. - -## 본문 - - - -변경 스트림이 실패한 뒤의 처리를 이 패키지가 맡는다. - -## 자바독이 나눈 세 실패와 진입점 넷 - -:::evidence key="analysis-finding-a06-f019" alt="저장소 루트에서 돌린 정적 검색 출력 208줄. 먼저 복구 패키지의 파일 넷이 나열되는데 MongoChangeHistoryLostException 과 MongoChangeStreamRecoveryDecision 과 MongoChangeStreamRecoveryPolicy 와 MongoInvalidateRecovery 다. 이어서 MongoChangeStreamRecoveryPolicy 7~66번 전문이 실린다. 8~13번 자바독은 실패를 셋으로 나누는데 재개 가능한 네트워크나 선출 실패는 저장된 토큰이 아직 유효하므로 자동으로 재개하고, 컬렉션이 삭제되거나 이름이 바뀐 무효화는 resumeAfter 가 아니라 startAfter 가 필요하며 드라이버가 그것을 암묵적으로 해 주지 않고, 유실된 히스토리는 사람이 필요하다고 적는다. 17~21번이 두 런북 경로 상수이고, 23~33번의 onHistoryLost 는 절대 자동 재개하지 않으며 현재 시각부터 다시 시작하면 알 수 없는 범위의 변경이 빠진 투영이 스스로를 건강하다고 보고하게 된다고 적는다. 36~38번이 onResumableFailure, 40~48번이 onInvalidate 인데 43~44번 자바독이 저장하는 체크포인트가 startAfter 위치여야 한다고 적는다. 50~61번의 onFailure 가 53~55번에서 서버 코드가 히스토리 유실이면 HISTORY_LOST 로 멈추고, 57~58번에서 재개 가능 라벨이면 재개하며, 60번에서 나머지를 FAILED 로 멈춘다. 63~66번이 그 서버 코드를 286 과 280 으로 적는다. 다음으로 진입점마다 main 과 시험 호출 계수가 나오는데 onFailure 가 main 1 에 시험 3, onHistoryLost 가 main 0 에 시험 1, onResumableFailure 가 main 0 에 시험 1, onInvalidate 가 둘 다 0, requireCorrectResumeOption 이 main 1 에 시험 1, checkpointFor 가 main 0 에 시험 1, autoResumable 이 main 0 에 시험 3 이고, new MongoChangeHistoryLostException 은 main 과 시험 모두 0 이다. onInvalidate 라는 이름이 나오는 줄 전부가 2 개라는 대조가 먼저 나오고, 그 예외 이름이 자바 소스에 나오는 줄이 2 개인데 둘 다 MongoChangeHistoryLostException 자신의 16번 클래스 선언과 22번 생성자다. 이어서 같은 이름을 문서가 쓰는 자리 넷이 나오는데 docs/architecture/mongo-api-surface.txt 168번이 공개 표면 목록에 올리고, docs/mongodb/change-stream-guide.md 71번 표가 oplog 에 토큰이 없는 경우를 이 예외로 잇고 75번이 재개 토큰이 가장 오래된 oplog 항목보다 앞설 때 이 예외가 던져진다고 적으며, docs/mongodb/runbooks/history-lost.md 23번이 증상 목록에 그 이름을 올린다. 이어서 그 예외 8~29번이 실린다. 9번 자바독은 oplog 에 저장된 재개 위치가 더 이상 없다는 뜻이고, 11~14번은 복구가 기술적 결정이 아니라 업무 결정이라 전용 타입이며, 플랫폼이 지금부터 재개할 수도 있고 드라이버가 그것을 쉽게 해 주지만 그러면 투영이 oplog 에서 떨어진 모든 변경을 조용히 놓치므로 정직한 선택지는 투영을 다시 만들거나 원본과 재조정하는 것뿐이고 둘 다 누군가가 골라야 한다고 적는다. 22~28번 생성자의 메시지는 변경 스트림의 재개 위치가 더 이상 oplog 에 없어서 지금부터 다시 시작해 틈을 조용히 건너뛰는 대신 소비를 멈췄다는 것이다. 그 아래 onFailure 50~61번이 다시 실리고, 소비자가 복구를 부르는 자리로 ReactiveMongoChangeStreamConsumer 119번과 214번이 나온다. 이어서 히스토리 유실 시 호출자가 받는 것을 고정한 시험 ChangeStreamConsumerLifecycleTest 108~124번이 실리는데 112번이 서버 코드 286 오류를 흘리고 115번이 MongoQueryException 으로 끝나는 것을 단언하며 117~119번이 상태가 HISTORY_LOST 이고 런북이 히스토리 유실 런북인지를, 120~122번이 스트림을 한 번만 열었는지를 확인한다. 다음으로 MongoInvalidateRecovery 24~45번이 실려 31~32번 서명과 35번의 checkpoint.position 이 intended 와 다른지 보는 검사가 나오고, 그 메서드를 부르는 자리가 ReactiveMongoChangeStreamConsumer 119번과 정의와 시험 하나로 나열되는데 소비자 쪽은 checkpoint 와 checkpoint.position 을 두 인자로 넘긴다. 마지막으로 MongoChangeStreamState 1~35번이 실리는데 6~9번 자바독이 HISTORY_LOST 가 FAILED 와 분리된 이유를 적는다 — 플랫폼이 스스로 복구하기를 거부하는 유일한 상태이고, oplog 가 저장된 토큰을 지나쳐 굴러간 뒤 지금부터 재개하면 그 사이의 모든 변경을 조용히 버려서 투영이 건강해 보이면서 조용히 틀리게 되는데 그것이 누군가 봐야 하는 멈춘 소비자보다 나쁘다는 것이다." caption="복구 패키지 파일 넷과 정책 전문 · 진입점별 main·시험 호출 계수 · 예외 이름이 자바 소스에 두 줄이고 문서에 넷 · 그 예외가 적은 존재 이유 · onFailure 의 히스토리 유실 분기와 소비자가 부르는 두 자리 · 호출자가 받는 예외 타입을 고정한 시험 · 자기 자신과 견주는 검사 · 상태 열거형이 적은 구분 — 208줄 · exit 0" zoom="true" -::: - -`MongoChangeStreamRecoveryPolicy:8`\~`:13` 자바독이 실패를 셋으로 나눈다. 재개 가능한 네트워크나 선출 실패는 저장된 토큰이 아직 유효하므로 자동 재개하고, 무효화는 `startAfter` 가 필요하며 드라이버가 암묵적으로 해 주지 않고, 유실된 히스토리는 사람이 필요하다. - -진입점은 넷이다. `onHistoryLost:29`, `onResumableFailure:36`, `onInvalidate:46`, `onFailure:51` 이다. - -## 호출자를 가진 것과 갖지 못한 것 - -main 에서 불리는 것은 `onFailure` 하나다. `onHistoryLost` 와 `onResumableFailure` 는 main 0 에 시험 1 씩이고 `onInvalidate` 는 둘 다 0 이다. - -`MongoInvalidateRecovery` 의 두 메서드도 호출자 유무가 다르다. `requireCorrectResumeOption` 은 main 1 이고 `checkpointFor` 는 0 이다. `MongoChangeStreamState:32` 의 `autoResumable()` 도 main 0 에 시험 3 인데, 이쪽은 복구 패키지 밖 타입이다. - -`onHistoryLost` 가 호출자를 갖지 못한 이유는 `onFailure` 안에 있다. `:53`\~`:55` 가 서버 코드 286 이나 280 이면 `HISTORY_LOST` 로 멈추는 결정을 만든다. `onHistoryLost:31`\~`:32` 가 만드는 것과 같은 결정이다. - -## 전용 예외가 만들어지지 않는다 - -`MongoChangeHistoryLostException` 이라는 이름이 저장소 전체에 두 줄 나온다. `:16` 의 클래스 선언과 `:22` 의 생성자다. - -`new` 를 부르는 줄이 main 도 시험도 0 이다. - -`:11`\~`:14` 자바독은 복구가 기술적 결정이 아니라 업무 결정이라 전용 타입을 두었다고 적는다. 플랫폼이 지금부터 재개할 수도 있고 드라이버가 그것을 쉽게 해 주지만, 그러면 투영이 oplog 에서 떨어진 모든 변경을 조용히 놓친다. 남은 선택지는 투영을 다시 만들거나 원본과 재조정하는 것뿐이고 둘 다 누군가가 골라야 한다. - -`:22`\~`:28` 의 생성자 메시지에도 재개 위치가 더 이상 oplog 에 없어서, 지금부터 다시 시작해 틈을 건너뛰는 대신 소비를 멈췄다고 적혀 있다. - -## 실제로 나가는 것은 드라이버 예외다 - -`ReactiveMongoChangeStreamConsumer:214` 가 `recovery.onFailure(MongoDriverFailureView.from(driverFailure))` 로 결정을 받는다. 상태와 런북은 그 결정에서 온다. - -예외는 바뀌지 않는다. `ChangeStreamConsumerLifecycleTest:112` 가 서버 코드 286 오류를 흘리고 `:115` 가 `verifyError(MongoQueryException.class)` 로 끝나는 것을 단언한다. `:117`\~`:119` 가 상태는 `HISTORY_LOST` 이고 런북은 히스토리 유실 런북인 것을 확인한다. - -그래서 호출자가 `catch (MongoChangeHistoryLostException)` 으로 이 상황을 가르려 하면 잡히지 않는다. 상태를 물어보는 경로로만 알 수 있다. - -`MongoChangeStreamState:6`\~`:9` 는 `HISTORY_LOST` 가 `FAILED` 와 분리된 이유를 적는다. 플랫폼이 스스로 복구하기를 거부하는 유일한 상태이고, 지금부터 재개하면 투영이 건강해 보이면서 조용히 틀리게 되는데 그것이 누군가 봐야 하는 멈춘 소비자보다 나쁘기 때문이다. - -이 구분은 `MongoChangeStreamState` 에만 있고, 호출자가 받는 예외 타입에는 없다. - -## requireCorrectResumeOption 에 같은 값이 두 번 들어간다 - -`MongoInvalidateRecovery:31`\~`:32` 의 `requireCorrectResumeOption` 이 체크포인트와 의도한 위치를 받는다. `:35` 가 `checkpoint.position() != intended` 면 던진다. - -`requireCorrectResumeOption` 을 부르는 main 코드는 `ReactiveMongoChangeStreamConsumer:119` 하나인데, 인자가 `checkpoint` 와 `checkpoint.position()` 이다. 같은 값을 두 번 넣으므로 그 조건이 참이 될 수 없다. - -시험 쪽 `MongoChangeStreamRecoveryPolicyTest:59` 만 다른 값을 넘겨 던지는 것을 확인한다. - -## 원문에 없는 것 - -원문은 호출 계수를 정리하면서 전용 예외가 어디에서도 만들어지지 않는 것을 가장 무겁게 봤다. 그 판정은 그대로다. - -원문이 적지 않은 것은 문서 쪽이다. `docs/mongodb/change-stream-guide.md:75` 가 이 예외는 재개 토큰이 가장 오래된 oplog 항목보다 앞설 때 던져진다고 적고, `:71` 의 표가 히스토리 유실을 그 예외로 잇는다. `docs/mongodb/runbooks/history-lost.md:23` 은 그것을 증상 목록에 올린다. 세 자리가 코드에 없는 예외를 운영자에게 안내한다. - -## 확인하지 못한 것 - -히스토리 유실을 실제로 일으켜 보지 않았다. 예외 타입 판단은 시험 단언에 기댄다. - -`MongoChangeStreamRecoveryDecision` 이 `halt` 와 `resume` 말고 다른 상태를 만들 수 있는지 그 타입을 따로 열지 않았다. - -`docs/architecture/mongo-api-surface.txt:168` 이 이 예외를 공개 표면에 올려 둔 것이 의도인지 판단하지 않았다. - -서버 코드 286 을 직접 흘려 호출자가 받는 타입을 관측하지 않았다. 판단 근거는 시험이 단언하는 값이다. - -호출자가 없는 다섯 진입점을 포크가 쓰도록 남겨 둔 것인지 설계 문서로 판단하지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f021.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f021.md deleted file mode 100644 index 31afff1..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f021.md +++ /dev/null @@ -1,138 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a06-f021 -title: 드라이런만 감사 싱크를 직접 불러 번역을 거치지 않는다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a06-f021 -evidenceCapturedOn: 2026-09-04 -body: case-analysis-finding-a06-f021.body.md -assets: - - key: analysis-finding-a06-f021 - file: ../../../final/evidence/rendered/analysis-finding-a06-f021.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a06-f021.txt -source: - - 원본 분석 절은 final/document.md#a06 §76 이다. ---- - -# 드라이런만 감사 싱크를 직접 불러 번역을 거치지 않는다 - -감사 레코드를 만드는 자리는 넷이다. `:114`·`:117`·`:122` 가 `MongoAdminGateway:127` 의 `audit` 헬퍼를 지나 싱크 실패를 `MongoOperationRejectedException` 으로 바꾸고, `dryRun:147` 만 `auditSink.accept` 를 직접 부른다. - -## 관계 - -- **단일 admission point는 우회 경로를 세어야 성립한다** - 그 규칙 2 가 하위 계층 타입을 직접 참조하는 곳을 세라고 적는다. 감사 레코드를 만드는 네 자리 가운데 `dryRun:147` 하나가 `auditSink` 를 직접 부른다. -- **도달성 판정은 단어가 아니라 import로 확인한다** - `dryRun` 호출 0 이라는 판정을 같은 pathspec 과 같은 정규식 모양이 `execute` 에 대해 18 줄을 찾은 것과 대조해 확인했다. -- **실패 번역 사슬의 순서는 계약이다** - `audit` 헬퍼가 싱크의 `RuntimeException` 을 `MongoOperationRejectedException` 으로 바꾸는 단일 번역 지점인데, `dryRun:147` 이 그것을 지나지 않아 싱크가 던진 예외가 그대로 호출자에게 간다. - -## 문제 - -관리 게이트웨이는 모든 감사 쓰기를 헬퍼로 보내고, 그 헬퍼가 싱크 실패를 플랫폼 거부 예외로 바꾼다. - -그 헬퍼를 지나지 않는 감사 쓰기가 있는지 확인했다. - -## 결론 - -audit:127~:137 이 싱크 호출을 try 로 감싸고 RuntimeException 을 플랫폼 거부 예외로 바꾼다. 거절된 레코드의 단계 이름이 메시지에 들어간다. - -실행 경로의 세 기록이 그 헬퍼를 지난다. :114 의 의도와 :117 의 성공과 :122 의 실패다. :113 이 그 순서를 fail closed 라고 부른다. - -싱크를 직접 부르는 줄은 헬퍼 안의 :129 와 드라이런 안의 :147 둘이고, 뒤쪽만 번역을 거치지 않는다. - -dryRun:145~:149 는 권한 검사 뒤에 싱크를 바로 부른다. 그래서 싱크가 던진 예외가 그대로 호출자에게 도달한다. - -:142~:143 자바독은 드라이런을 고위험 작업의 전제 조건으로 놓는다. 자바독대로면 고위험 명령은 감사가 강제되지 않는 호출을 먼저 지난다. - -시험은 execute 쪽만 고정한다. MongoAdminAuditStateMachineTest:86 이 던지는 싱크로 거부 예외와 본문 미실행을 함께 단언하는데, 같은 파일에 dryRun 을 거는 줄이 0 이다. - -dryRun 을 부르는 줄이 저장소 전체에 0 이다. 같은 게이트웨이의 execute 는 18 줄이 부르고 그중 일곱이 main 이다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 감사 헬퍼와 그 번역 인용, 실행 경로가 헬퍼를 부르는 세 자리 인용, auditSink 이름이 나오는 자리 전수와 헬퍼 호출 계수, dryRun 본문과 그 자바독 인용, 감사 실패를 고정하는 시험 인용과 그 파일의 dryRun 계수, 두 진입점의 호출 계수와 다른 게이트웨이 오염분 대조 -소스 수정 : x - -## 재현 조건 - -1. audit 헬퍼와 그것이 던지는 예외를 인용한다. -2. 실행 경로가 그 헬퍼를 부르는 세 자리를 인용한다. -3. auditSink 라는 이름이 나오는 자리와 audit 헬퍼를 부르는 자리를 각각 전부 찾는다. -4. dryRun 본문과 그 자바독을 인용한다. -5. 감사 실패를 고정하는 시험을 인용하고, 같은 파일에서 dryRun 이 나오는 줄을 센다. -6. dryRun 과 execute 를 부르는 줄을 각각 세어 대조한다. - -## 본문 - - - -관리 게이트웨이는 감사 쓰기를 헬퍼 하나로 모은다. - -## 감사 실패를 거부로 바꾸는 헬퍼 - -:::evidence key="analysis-finding-a06-f021" alt="저장소 루트에서 돌린 정적 검색 출력 145줄. 먼저 MongoAdminGateway 120~137번이 실린다. 120~121번 주석이 종결 레코드가 실패 전파 전에 쓰이고 그 세부가 예외 메시지가 아니라 예외 타입이라고 적고, 122~123번이 실패 레코드를 남긴 뒤 다시 던진다. 127~137번의 private audit 헬퍼가 128~129번에서 auditSink.accept 를 try 로 감싸고 130~136번에서 RuntimeException 을 잡아 MongoOperationRejectedException 으로 바꾸는데, 메시지가 관리 감사 싱크가 해당 단계 레코드를 거절했으며 감사할 수 없는 관리 작업은 실행되지 않는다는 것이다. 이어서 실행 경로 전체인 83~126번이 실린다. 83번 서명이 명령과 승인과 본문 셋을 받고, 87번이 authorization.require 로 권한을 요구하며, 88~93번이 만료된 명령을 거절하는데 오래된 명령은 더 이상 그것이 쓰인 때의 모습이 아닌 클러스터를 서술할 수 있다는 메시지다. 94번이 연산의 highRisk 를 보고 95~102번이 승인이 null 이면 데이터를 파괴하거나 컬렉션을 다시 쓰는 연산은 이 명령에 묶인 승인 아래에서 돌거나 아예 돌지 않는다며 거절하고, 103번이 approval.require 를, 106~110번이 단일 사용을 강제하며 104~105번 주석이 그것이 없으면 한 reshard 에 대한 승인 하나가 같은 모양의 이후 모든 reshard 를 허가한다고 적는다. 113번 주석이 fail closed 라며 의도를 기록할 수 없으면 명령이 돌지 않는다고 적고, 114번이 의도 레코드를, 117번이 성공 레코드를, 122번이 실패 레코드를 헬퍼로 보낸다. 그 아래 auditSink.accept 를 부르는 줄이 2 개이고 audit 헬퍼를 부르는 줄이 3 개라고 나오며, auditSink 라는 이름이 나오는 자리가 26번 필드와 38번 생성자 인자와 42번 대입과 129번과 147번 다섯으로, 헬퍼를 부르는 자리가 114·117·122번 셋으로 나열된다. 다음으로 MongoAdminGateway 139~149번의 dryRun 이 실린다. 140~144번 자바독은 이것이 실행 없이 연산을 검증하고 의도를 기록하는 것이며, 드라이런이 모든 고위험 작업의 전제 조건이라 누군가 기억해서 넘기는 플래그가 아니라 일급 호출이어야 한다고 적는다. 145번 서명 뒤 146번이 권한을 요구하고 147~148번이 auditSink.accept 를 직접 부른다. 헬퍼를 거치지 않는다. 마지막으로 MongoAdminAuditStateMachineTest 84~113번이 실린다. 85번 DisplayName 이 실패하는 감사 싱크가 명령이 도는 것을 막는다고 적고, 86번의 anUnauditableCommandDoesNotRun 이 88~94번에서 감사 컬렉션에 닿을 수 없다며 던지는 싱크로 게이트웨이를 만들고 95번에서 ran 플래그를 두며, 97~107번이 COLL_MOD 일상 명령을 승인 null 로 execute 에 넘기고, 108~109번이 MongoOperationRejectedException 과 cannot be audited 메시지를, 110~112번이 나중에 아무도 재구성할 수 없는 관리 작업은 일어나면 안 된다는 설명과 함께 본문이 돌지 않았다는 것을 단언한다. 그 아래 드라이런의 감사 실패를 보는 시험 줄이 0 개이고, 이 게이트웨이의 dryRun 을 부르는 줄이 저장소 전체에 0 개이며, 대조로 센 같은 게이트웨이의 execute 는 18 개이고 그 호출 자리가 main 일곱과 test 열하나로 나뉘어 전부 나열된 뒤, 같은 정규식이 다른 게이트웨이에서 잡는 줄이 8 개라고 참고로 붙는다." caption="감사 실패를 플랫폼 예외로 바꾸는 헬퍼 · execute 가 감사 앞에서 지나는 네 검사와 세 기록이 헬퍼를 지나는 자리 · auditSink 를 부르는 자리 전수 · 헬퍼를 거치지 않는 dryRun 과 그 자바독 · 감사 실패를 고정하는 시험의 단언 셋과 dryRun 계수 0 · 두 진입점의 호출 대조 — 145줄 · exit 0" zoom="true" -::: - -`MongoAdminGateway:127`\~`:137` 의 `audit` 이 `auditSink.accept(record)` 를 `try` 로 감싼다. - -`:130` 이 `RuntimeException` 을 잡아 `:131`\~`:135` 에서 `MongoOperationRejectedException` 으로 바꾼다. 메시지는 감사 싱크가 그 단계 레코드를 거절했고, 감사할 수 없는 관리 작업은 실행되지 않는다는 것이다. - -## 실행 경로의 세 기록 - -`:114` 가 의도 레코드를 헬퍼로 보낸다. `:113` 주석에는 fail closed 라고, 의도를 기록할 수 없으면 명령이 돌지 않는다고 적혀 있다. - -`:117` 이 성공 레코드를, `:122` 가 실패 레코드를 보낸다. `:120`\~`:121` 주석은 종결 레코드가 실패 전파 전에 쓰이고 그 세부가 예외 메시지가 아니라 예외 타입이라고 적는다. - -셋 다 헬퍼를 지난다. 싱크가 던지면 셋 다 플랫폼 예외가 된다. - -## audit 헬퍼를 지나지 않는 dryRun\:147 - -`auditSink.accept` 를 부르는 줄은 둘이다. `:129` 가 헬퍼 안이고 `:147` 이 드라이런 안이다. `audit` 헬퍼를 부르는 줄은 셋이고 전부 실행 경로에 있다. - -`dryRun:145`\~`:149` 는 `:146` 에서 권한을 요구한 뒤 `:147`\~`:148` 에서 `auditSink.accept(...)` 를 바로 부른다. - -싱크가 던지면 그 예외가 번역 없이 호출자에게 간다. `MongoOperationRejectedException` 이 아니라 싱크가 던진 것 그대로다. - -## 자바독이 적는 드라이런의 지위 - -`:142`\~`:143` 자바독에는 드라이런이 모든 고위험 작업의 전제 조건이라서, 누군가 기억해서 넘기는 플래그가 아니라 일급 호출이어야 한다고 적혀 있다. - -자바독대로라면 고위험 작업은 드라이런이 먼저 돌아야 하는데, 그 드라이런이 남기는 레코드는 싱크가 거절해도 명령을 멈추지 않는다. `execute` 가 남기는 세 레코드와 다른 점이 이것이다. - -## 시험이 거는 것은 execute 뿐이다 - -`MongoAdminAuditStateMachineTest:86` 의 `anUnauditableCommandDoesNotRun` 이 `:88`\~`:94` 에서 던지는 싱크로 게이트웨이를 만들고, `:108`\~`:109` 에서 `MongoOperationRejectedException` 과 `cannot be audited` 메시지를, `:110`\~`:112` 에서 본문이 돌지 않았다는 것을 단언한다. - -같은 파일에서 `dryRun` 을 다루는 줄은 0 이다. - -## dryRun 을 부르는 코드가 없다 - -그 메서드를 부르는 줄이 저장소 전체에 하나도 없다. 형제인 `execute` 는 18 줄이 부른다. - -같은 pathspec 과 같은 정규식 모양이 한쪽에서 18 을 찾았으므로, 0 은 검색이 아무것도 훑지 못해 나온 값이 아니다. 그 정규식은 다른 게이트웨이에서도 8 줄을 잡는데 `RedisRawGatewayContractTest` 와 `PolicyAwareMongoNativeGatewayTest` 의 것이라 이 계수에서 뺐다. - -## 원문에 없는 것 - -원문은 두 감사 경로 중 하나만 fail-closed 이고 그 차이가 문서화돼 있지 않다고 적는다. 그 판정은 그대로다. - -원문이 적지 않은 것은 `execute` 와 `dryRun` 이 감사 앞에서 지나는 검사의 수다. `execute` 는 `:87` 의 권한과 `:88` 의 만료와 `:94`\~`:102` 의 승인과 `:106` 의 단일 사용 넷을 지난 뒤에 `:114` 로 간다. `dryRun` 은 `:146` 의 권한 하나만 지난다. - -## 확인하지 못한 것 - -던지는 싱크를 넣고 `dryRun` 을 불러 어떤 예외가 나오는지 프로브로 재현하지 않았다. - -포크가 드라이런을 어디서 부르도록 설계된 것인지 설계 문서로 판단하지 않았다. - -`dryRun` 이 남기는 레코드를 승인 사슬이 어떻게 참조하는지 그 소비자를 찾지 않았다. - -던지는 싱크를 넣고 `dryRun` 을 불러 어떤 예외가 나오는지 프로브로 재현하지 않았다. - -`dryRun` 이 남기는 레코드를 승인 사슬이 어떻게 참조하는지 그 소비자를 찾지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f022.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f022.md deleted file mode 100644 index da18a44..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f022.md +++ /dev/null @@ -1,146 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a06-f022 -title: 허용 목록 검사를 부르는 프로덕션 코드가 없다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a06-f022 -evidenceCapturedOn: 2026-09-04 -body: case-analysis-finding-a06-f022.body.md -assets: - - key: analysis-finding-a06-f022 - file: ../../../final/evidence/rendered/analysis-finding-a06-f022.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a06-f022.txt -source: - - 원본 분석 절은 final/document.md#a06 §77 이다. ---- - -# 허용 목록 검사를 부르는 프로덕션 코드가 없다 - -`MongoObservationConvention:14`~`:15` 가 이 허용 목록을 강제라고 적는다. 그런데 `:66` 의 `requireAllowed` 를 부르는 이 리프의 main 줄이 0 이고, 관측 클래스 넷은 `Tags.of(...)` 로 문자열을 직접 넣는다. - -## 관계 - -- **카디널리티 경계를 타입으로 표현하기** - 그 규칙이 요구하는 것을 이 허용 목록이 선언은 한다. 강제는 호출 지점이 그것을 지날 때만 생기는데 지나는 코드가 없다. -- **이름은 값이 아니라 registry key다** - 태그 이름이 등록된 것이어야 한다는 규칙이다. 여기서는 `Tags.of` 의 첫 인자가 그냥 문자열이다. -- **단일 admission point는 우회 경로를 세어야 성립한다** - `requireAllowed` 를 지나는 main 경로가 0 이고 `Tags.of` 로 문자열을 직접 넣는 경로가 이 패키지에 아홉이다. - -## 문제 - -허용 목록과 금지 목록이 한 파일에 있고, 자바독이 그것을 강제라고 부른다. - -그 강제가 실제로 어디서 일어나는지 확인했다. - -## 결론 - -MongoObservationConvention:9~:12 가 막으려는 두 가지를 적는다. 메트릭 카디널리티 폭증과, 메트릭 저장소가 프로덕션 데이터의 관리되지 않는 사본이 되는 것이다. - -:14~:15 가 그 둘을 하나의 허용 목록으로 막는다고 적는다. - -그 판단이 통하려면 :66 의 requireAllowed 를 호출 지점이 지나야 한다. 허용 목록은 여덟이고 금지 목록은 열하나다. - -이 리프의 main 에서 그것을 부르는 줄이 0 이다. MongoObservationConvention 을 언급하는 main 파일은 자기 자신과 MongoDriverObservabilityConfiguration 둘뿐이다. - -관측 클래스들은 Tags.of(...) 로 문자열을 직접 넣는다. 이 패키지에서 그 호출이 아홉 줄이고, MicrometerMongoOperationObserver 와 MongoCommandObservationListener 와 MongoPoolObservationListener 와 MongoSdamObservationListener 넷에 흩어져 있다. - -지금 값들은 전부 허용 목록 안이다. 그것을 지키는 것은 코드가 아니라 검토와 MongoObservationConventionTest 다. - -MongoDriverObservabilityConfiguration:26~:37 의 apply 가 리스너 셋을 만들어 붙이는 동안 규약을 지나지 않는다. 규약은 :40 의 convention() 안에만 있고 그것을 부르는 main 줄이 0 이다. - -MongoObservationRedactor:36 의 describe 도 main 호출자가 없고, :45 의 isAlwaysRedacted 만 MongoCommandObservationListener:67 이 부른다. 프로덕션이 실제로 읽는 집합은 ALWAYS_REDACTED 하나이고, DESCRIBABLE_COMMANDS 는 어느 main 코드도 지나지 않는다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 규약의 클래스 자바독과 두 목록 인용, requireAllowed 본문 인용, 그 규약을 언급하는 main 파일 전수와 이 리프의 호출 계수와 같은 이름의 다른 메서드 대조, 그 둘째 파일의 조립 메서드 전문 인용과 standard()·convention() main 호출 계수, 이 패키지의 Tags.of 호출 전수와 저장소 전체 대조, 편집기의 세 갈래 인용과 두 메서드의 main 호출 계수와 시험 대조, 리스너가 쓰는 갈래 인용 -소스 수정 : x - -## 재현 조건 - -1. 규약의 클래스 자바독과 허용·금지 목록을 인용한다. -2. requireAllowed 본문을 인용한다. -3. 그 규약을 언급하는 main 파일을 전부 찾고, 이 리프에서 그 메서드를 부르는 줄을 센다. -4. 같은 이름의 메서드를 저장소 전체 main 에서 부르는 줄을 대조로 센다. -5. 그 둘째 파일의 apply 와 convention 을 전문으로 싣고 두 메서드의 main 호출을 센다. -6. 이 패키지에서 Tags.of 를 부르는 줄을 전부 나열하고 저장소 전체 계수와 대조한다. -7. 편집기의 세 갈래를 인용하고 두 메서드의 main 호출을 각각 센다. -8. 리스너가 그중 무엇을 쓰는지 인용한다. - -## 본문 - - - -`MongoObservationConvention` 은 MongoDB 텔레메트리가 실을 수 있는 태그를 목록으로 못박는다. - -## 그 목록이 막으려는 두 가지 - -:::evidence key="analysis-finding-a06-f022" alt="저장소 루트에서 돌린 정적 검색 출력 205줄. 먼저 MongoObservationConvention 1~57번이 실린다. 6~16번 자바독은 이것이 MongoDB 텔레메트리가 실을 수 있는 유일한 태그들이라며 두 문제를 하나의 허용 목록으로 막는다고 적는다. 문서 아이디나 테넌트 아이디나 질의 파라미터 같은 높은 카디널리티 태그가 시계열을 늘려 메트릭 백엔드가 데이터를 버리거나 비용을 청구하게 만드는 것이 하나이고, 같은 값들이 메트릭 저장소를 프로덕션 데이터의 관리되지 않는 사본으로 바꿔 데이터베이스가 받는 보존과 접근 규칙 밖에 두는 것이 다른 하나다. 14~15번이 둘 다 같은 방식으로 막힌다며 이 목록에 없는 태그는 붙일 수 없으므로 실수가 호출 지점이 아니라 이 파일에서 일어나야 한다고 적는다. 19~28번의 허용 목록이 mongoProfile 과 databaseProfile 과 collectionProfile 과 operationName 과 operationType 과 result 와 failureCategory 와 consistencyProfile 여덟이고, 30~42번의 금지 목록 열하나가 documentId 와 rawTenantId 와 tenantId 와 dynamicCollectionName 과 queryParameter 와 query 와 fullBson 과 resumeToken 과 shardKeyValue 와 plaintextPII 와 credential 이다. 46~49번의 standard 가 인스턴스를 만들고 51~54번의 allowedTagNames 와 56~57번의 forbiddenTagNames 가 두 집합을 그대로 돌려준다. 이어서 58~76번이 실려 61~65번 자바독이 허용 목록에 없는 태그를 거절한다고 적고 66~75번의 requireAllowed 가 목록에 없으면 거절된 태그 이름과 허용 목록을 담은 IllegalArgumentException 을 던진다. 다음으로 MongoObservationConvention 을 언급하는 main 파일이 2 개라고 나오고 그 둘이 MongoDriverObservabilityConfiguration 과 MongoObservationConvention 자신이며, 이 리프 main 에서 requireAllowed 를 부르는 줄이 0 개다. requireAllowed 가 이 리프에 나오는 자리가 전부 나열되는데 main 은 MongoSearchQuery 48번과 ShardAwareQueryValidator 59번과 MongoAggregationProfile 86번과 PolicyAwareMongoAggregationExecutor 49번과 MongoObservationConvention 66번이고 앞의 넷은 이름이 비슷한 다른 메서드이며, test 쪽에 MongoSearchReadinessGateTest 52·54번과 ShardAwareQueryValidatorTest 57·60번과 PolicyAwareMongoAggregationExecutorTest 20·29·37·39·48·55·64·67번과 MongoObservationConventionTest 38·39번과 MongoSecurityIntegrationLaneTest 163·165·167번이 있다. 대조로 센 저장소 전체 main 의 같은 이름 호출은 3 개로 GraphQlRSocketAdmission 47번과 BlockingRedirectCoordinator 93번과 PublishPosterImageUseCase 106번이고, 같은 pathspec 으로 Tags.of 를 세면 9 개다. 이어서 MongoDriverObservabilityConfiguration 20~43번이 실린다. 25번 주석이 명령과 SDAM 과 풀 리스너를 클라이언트 설정 빌더에 더한다고 적고, 26~37번의 apply 가 28번에서 MongoCommandObservationListener 를, 29~31번에서 MongoSdamObservationListener 를, 32~35번에서 MongoPoolObservationListener 를 새로 만들어 붙인 뒤 빌더를 돌려준다. 39번 주석이 이 리스너들이 따르는 태그 규약이라고 적고 40~42번의 convention 이 MongoObservationConvention.standard 를 돌려준다. 그 아래 standard 를 부르는 main 줄이 1 개, convention 을 부르는 main 줄이 0 개다. 다음으로 이 패키지에서 Tags.of 를 부르는 줄이 9 개라고 나오고 그 아홉이 MicrometerMongoOperationObserver 46·72·77·83번과 MongoCommandObservationListener 63번과 MongoPoolObservationListener 40·60번과 MongoSdamObservationListener 46·61번이며, 저장소 전체에서는 30 개다. 마지막으로 MongoObservationRedactor 18~48번이 실린다. 18~20번의 DESCRIBABLE_COMMANDS 가 ping 과 hello 와 buildInfo 와 listCollections 와 listIndexes 와 collStats 여섯이고, 22~33번의 ALWAYS_REDACTED 가 authenticate 와 saslStart 와 saslContinue 와 getnonce 와 createUser 와 updateUser 와 copydbgetnonce 와 copydbsaslstart 와 copydb 아홉이다. 35~42번의 describe 가 항상 가림 목록에 있으면 redacted 를 돌려주고 아니면 서술 가능 목록에 있는지에 따라 이름 그대로거나 이름 뒤에 괄호 셋을 붙이며, 44~47번의 isAlwaysRedacted 는 항상 가림 목록 포함 여부만 돌려준다. 그 아래 redactor.describe 를 부르는 main 줄이 0 개, redactor.isAlwaysRedacted 가 1 개, 시험에서 describe 는 6 개라고 나오고, MongoCommandObservationListener 59~72번이 실려 63~69번이 Tags.of 로 mongoProfile 과 operationType 과 result 를 넣는데 67번이 isAlwaysRedacted 로 명령 이름을 가리거나 그대로 쓰는 것이 보인다." caption="규약이 적는 두 문제와 강제 문장 · 허용 여덟과 금지 열하나 · requireAllowed 본문 · 그 규약을 언급하는 main 파일 둘과 이 리프의 호출 0 · 리스너 셋을 조립하면서 규약을 지나지 않는 apply 와 convention 호출 0 · 관측 클래스 넷이 Tags.of 로 직접 넣는 아홉 줄 · 편집기의 세 갈래와 리스너가 쓰는 하나 — 205줄 · exit 0" zoom="true" -::: - -`:9`\~`:12` 가 둘을 적는다. 문서 아이디나 테넌트 아이디나 질의 파라미터 같은 높은 카디널리티 태그는 시계열을 늘려 메트릭 백엔드가 데이터를 버리거나 비용을 청구하게 만든다. 그리고 같은 값들이 데이터베이스의 보존·접근 규칙 밖에 프로덕션 데이터 사본을 하나 더 만든다. - -`:14`\~`:15` 가 둘 다 같은 방법으로 막힌다고 적는다. 목록에 없는 태그는 붙일 수 없으므로 실수가 호출 지점이 아니라 이 파일에서 일어나야 한다는 것이다. - -`:19`\~`:28` 이 허용 여덟이고 `:30`\~`:42` 가 금지 열하나다. `shardKeyValue` 와 `plaintextPII` 와 `credential` 이 그 끝이다. - -## 그 강제를 수행하는 메서드 - -`:66`\~`:75` 의 `requireAllowed` 가 허용 목록에 없으면 `IllegalArgumentException` 을 던진다. 메시지에 거절된 태그 이름과 허용 목록이 함께 들어간다. - -`:62` 자바독이 그 역할을 한 줄로 적는다. - -## requireAllowed 를 부르는 프로덕션 코드가 없다 - -`MongoObservationConvention` 을 언급하는 main 파일은 둘이다. 자기 자신과 `MongoDriverObservabilityConfiguration` 이다. - -이 리프의 관측 패키지 main 에서 `requireAllowed` 를 부르는 줄이 0 이다. - -그 둘째 파일이 규약으로 무엇을 하는지는 `MongoDriverObservabilityConfiguration:26`\~`:37` 에 있다. `apply` 가 `:28` 과 `:31` 과 `:35` 에서 명령·SDAM·풀 리스너 셋을 새로 만들어 클라이언트 설정 빌더에 붙이는데, 그 사이에 규약이 없다. 규약은 `:40`\~`:42` 의 `convention()` 이 `standard()` 를 돌려주는 별도 메서드로만 있고, 그것을 부르는 main 줄이 0 이다. `:39` 자바독은 그 메서드를 「이 리스너들이 따르는 태그 규약」이라고 부른다. - -이 리프에 그 접두사가 나오는 main 자리 다섯 중 넷은 다른 타입의 것이다. `MongoSearchQuery:48` 의 `requireAllowedPaths` 와 `ShardAwareQueryValidator:59` 의 `requireAllowedRead` 는 이름부터 다르고, `MongoAggregationProfile:86` 과 그것을 부르는 `PolicyAwareMongoAggregationExecutor:49` 는 집계 단계를 받는 동명 메서드다. - -부르는 것은 시험뿐이다. `MongoObservationConventionTest:38`·`:39` 와 `MongoSecurityIntegrationLaneTest:163`·`:165`·`:167` 이다. - -## 관측 클래스들은 문자열을 직접 넣는다 - -이 패키지에서 `Tags.of(...)` 를 부르는 줄이 아홉이다. `MicrometerMongoOperationObserver:46`·`:72`·`:77`·`:83`, `MongoCommandObservationListener:63`, `MongoPoolObservationListener:40`·`:60`, `MongoSdamObservationListener:46`·`:61` 이다. - -첫 인자가 그냥 문자열이다. `mongoProfile` 이나 `result` 나 `failureCategory` 처럼 허용 목록 안의 값이 들어 있지만, 그 사실을 코드가 확인하지 않는다. - -지금 어긋남이 없는 것은 검토와 시험이 지킨 결과다. - -## describe 를 지나지 않는 MongoCommandObservationListener - -`MongoObservationRedactor:35`\~`:42` 의 `describe` 가 세 갈래를 나눈다. 항상 가리는 명령이면 ``, 서술 가능 목록에 있으면 이름 그대로, 나머지는 이름 뒤에 괄호를 붙인다. - -`:44`\~`:47` 의 `isAlwaysRedacted` 는 `ALWAYS_REDACTED` 포함 여부만 본다. `DESCRIBABLE_COMMANDS` 를 읽는 프로덕션 코드는 없다. - -`redactor.describe` 를 부르는 main 줄이 0 이고 `redactor.isAlwaysRedacted` 는 1 이다. 그 하나가 `MongoCommandObservationListener:67` 인데, 삼항으로 `` 아니면 명령 이름 그대로를 쓴다. - -프로덕션이 읽는 집합은 `ALWAYS_REDACTED` 하나이고, 리스너가 만드는 렌더링은 `` 와 원문 이름 둘이다. 시험에서는 `describe` 를 6 줄이 부른다. - -## 원문과 갈리는 자리 - -원문은 이 허용 목록이 규약이지 강제가 아니며 `describe` 도 프로덕션 호출자가 0 이라고 적는다. 그대로 확인된다. - -원문은 세 갈래 중 실제로 쓰이는 것이 하나라고 적는다. 세는 대상이 다르다 — 프로덕션이 읽는 집합으로 세면 `ALWAYS_REDACTED` 하나이고, 리스너가 실제로 만드는 태그 값으로 세면 `` 와 원문 이름 둘이다. - -여기에 더한 것은 그 0 이 검색 실패가 아니라는 대조다. 같은 이름의 메서드가 이 리프의 다른 타입에서 넷, 저장소 전체 main 에서 셋 불린다. `Tags.of` 도 이 패키지에서 아홉, 저장소 전체에서 서른이 나온다. - -그리고 조립부가 그 관계를 어떻게 적는지다. `MongoDriverObservabilityConfiguration:39` 는 리스너들이 규약을 따른다고 적지만, 같은 파일의 `apply` 는 리스너 셋을 만들면서 규약을 지나지 않고 `convention()` 을 부르는 main 코드도 없다. - -## 확인하지 못한 것 - -실제로 금지된 태그가 통과하는지 리스너를 만들어 돌려 보지 않았다. - -`convention()` 이 나중에 붙일 강제 지점으로 남겨진 것인지, 이미 잊힌 것인지 커밋 이력으로 따지지 않았다. - -`describe` 의 세 갈래 가운데 둘이 쓰이지 않는 것이 설계인지 누락인지 판단하지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f023.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f023.md deleted file mode 100644 index b1efdf1..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f023.md +++ /dev/null @@ -1,148 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a06-f023 -title: 고위험 관리 작업 넷이 승인을 널로 넘기는 오버로드를 부른다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a06-f023 -evidenceCapturedOn: 2026-09-04 -body: case-analysis-finding-a06-f023.body.md -assets: - - key: analysis-finding-a06-f023 - file: ../../../final/evidence/rendered/analysis-finding-a06-f023.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a06-f023.txt -source: - - 원본 분석 절은 final/document.md#a06 §85 이다. ---- - -# 고위험 관리 작업 넷이 승인을 널로 넘기는 오버로드를 부른다 - -`MongoAdminGateway:66` 이 승인 자리에 `null` 을 넣은 채 세 인자 오버로드를 부른다. `:94`~`:102` 는 고위험 연산에 승인이 없으면 거절한다. 그 편의 오버로드를 부르는 main 일곱 줄 가운데 넷이 고위험 연산을 넘긴다. - -## 관계 - -- **권한이 센 절반이 설정 한 줄로 켜지면 안 된다** - 그 규칙이 다루는 것이 승인 어휘가 둘일 때의 문제다. 여기서는 편의 오버로드가 승인 없는 쪽을 기본으로 만든다. -- **같은 개념의 두 어휘가 공존하면 하나를 죽은 것으로 표시한다** - `execute` 가 둘인데 승인을 받는 쪽을 부르는 main 호출이 0 이다. -- **두 형태를 나란히 내놓는 포트는 이미 안전하지 않은 쪽을 고른 것이다** - 그 규칙 2 가 폐기 표시도 이름 차이도 가시성 차이도 없으면 호출자가 짧은 쪽을 고른다고 적는다. `execute` 둘이 이름도 가시성도 같고, main 일곱 호출이 전부 짧은 쪽이다. - -## 문제 - -이 평면의 실행 경로는 고위험 연산에 명령별 승인을 강제한다. - -승인을 실제로 넘기는 호출이 있는지 확인했다. - -## 결론 - -편의 오버로드가 :63~:67 에서 본 메서드를 부르면서 :66 의 승인 자리에 null 을 넣는다. - -:94~:102 가 그 null 을 거절한다. 고위험 연산에 승인이 없으면 MongoOperationRejectedException 이 나가고, 메시지가 그런 연산은 명령에 묶인 승인 아래에서만 돈다고 적는다. - -그 검사에 걸릴 수 있는 연산은 여덟이다. 그중 넷이 다섯 인자 오버로드로 넘어간다. MongoShardingAdminGateway:64 의 SHARD_COLLECTION, :75 의 REFINE_SHARD_KEY, :86~:87 의 RESHARD_COLLECTION, 그리고 MongoQueryableEncryptionCollectionManager:63~:64 의 MANAGE_ENCRYPTION_KEY 다. - -넷 다 인자를 무엇으로 채워도 완료되지 않는다. 승인 자리가 null 로 고정돼 있어 :95 에서 걸린다. - -MongoShardingAdminGateway:92 의 BALANCER_CONTROL 은 highRisk(false) 라 이 검사에 걸리지 않는다. MongoQueryableEncryptionCollectionManager:42 의 CREATE_COLLECTION 과 :70 의 COLL_MOD 도 그렇다. - -승인 객체를 만드는 프로덕션 코드가 없다. MongoAdminApproval.of(...) 를 부르는 main 줄이 0 이고, 그 타입이 main 에 등장하는 다섯 자리는 전부 자기 선언이거나 게이트웨이 서명이다. 시험에는 그것을 만드는 줄이 넷 있다. - -샤딩 게이트웨이가 받는 ReshardApproval 은 다른 타입이다. main 에 나오는 넷이 전부 자기 선언이거나 파라미터이고, 그것을 MongoAdminApproval 로 옮기는 자리가 없다. - -MongoShardingAdminGateway:49~:63 이 그 앞에서 승인 준비 상태와 지원 인덱스를 검사하지만, 그것을 통과해도 :64 에서 막힌다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 샤딩 게이트웨이의 네 메서드 전문 인용과 그 호출 계수, 다섯 인자 오버로드 인용, 세 인자 오버로드의 승인 검사 인용, 연산 열거형의 위험 등급 인용과 계수, main 에 나오는 연산마다 위험 등급과 등장 줄 대조, 고위험을 넘기는 자리 전수 인용, 승인 객체를 만드는 main 계수와 그 이름의 main 등장 전수와 시험 대조, ReshardApproval 의 main 등장 전수 -소스 수정 : x - -## 재현 조건 - -1. 샤딩 게이트웨이의 네 메서드를 전문으로 싣고 adminGateway.execute 호출을 센다. -2. 다섯 인자 오버로드를 인용해 승인 자리에 무엇이 들어가는지 보인다. -3. 실행 경로의 고위험 승인 검사를 인용한다. -4. 그 오버로드를 지나는 연산 상수의 위험 등급을 열거형에서 인용하고 전체 상수와 고위험 개수를 함께 센다. -5. main 에 나오는 연산 상수마다 위험 등급과 등장 줄을 대조한다. -6. 고위험을 다섯 인자 오버로드로 넘기는 자리를 전부 인용한다. -7. 승인 객체를 만드는 main 줄을 세고 그 타입이 main 에 등장하는 자리를 전부 나열한 뒤, 시험 쪽 계수를 대조로 낸다. -8. ReshardApproval 이 main 에 나오는 자리를 전부 나열해 두 승인 타입을 잇는 코드가 있는지 본다. - -## 본문 - - - -`MongoAdminOperation:4` 자바독이 이 열거형을 D4 평면이 수행할 수 있는 관리 연산이라고 부른다. 그 평면은 고위험 연산에 명령별 승인을 요구한다. 샤딩 게이트웨이가 네 작업을 그 위에 올린다. - -## 샤딩 게이트웨이의 네 메서드 - -:::evidence key="analysis-finding-a06-f023" alt="저장소 루트에서 돌린 정적 검색 출력 215줄. 먼저 MongoShardingAdminGateway 33~93번이 실린다. 38~65번의 shardCollection 이 컬렉션과 샤드 키와 준비도 보고서와 지원 인덱스 필드를 받아 49~53번에서 승인되지 않은 샤드 키를 거절하고 54~63번에서 지원 인덱스가 샤드 키로 시작하지 않으면 거절한 뒤 64번에서 adminGateway.execute 에 SHARD_COLLECTION 을 넘긴다. 68~76번의 refineShardKey 와 79~88번의 reshardCollection 은 ReshardApproval 을 require 한 뒤 각각 75번과 86~87번에서 REFINE_SHARD_KEY 와 RESHARD_COLLECTION 을 넘기고, 91~93번의 controlBalancer 는 92번에서 BALANCER_CONTROL 을 넘긴다. 네 메서드가 부르는 execute 가 4 개다. 이어서 MongoAdminGateway 40~68번이 실리는데 47~56번 자바독이 다섯 인자의 뜻을 적고, 57~62번 서명이 연산과 대상과 운영자와 사유와 본문을 받으며, 63~67번이 MongoAdminCommand.routine 으로 명령을 만들어 66번에서 승인 자리에 null 을, 67번에서 본문을 넘긴다. 다음으로 69~112번의 세 인자 오버로드가 실린다. 70~81번 자바독은 단일 레코드 버전에서 셋이 바뀌었다며 의도가 명령 실행 전에 기록되고 아무것도 주장하지 않으며 무엇이 실제로 일어났는지 적는 종결 레코드가 뒤따르고 실패하는 감사 싱크가 명령을 멈추는데 감사할 수 없는 관리 작업은 나중에 누구도 재구성할 수 없기 때문이고 drop 이나 reshard 에는 그것이 이 평면을 두는 이유 전부라고 적으며, 79번이 승인은 고위험 연산에 필요하고 이 명령에 묶인다고 적는다. 83번 서명이 명령과 승인과 본문 셋을 받고, 87번이 권한을 요구하고 88~93번이 만료된 명령을 거절하며, 94~102번이 고위험 연산에 승인이 null 이면 데이터를 파괴하거나 컬렉션을 다시 쓰는 연산은 이 명령에 묶인 승인 아래에서 돌거나 아예 돌지 않는다는 메시지로 거절하고, 103번이 승인을 검사하며 104~110번이 단일 사용을 강제하면서 그렇지 않으면 한 reshard 에 대한 승인 하나가 같은 모양의 이후 모든 reshard 를 허가하게 된다고 주석에 적는다. 이어서 MongoAdminOperation 3~10번 자바독이 이것이 D4 평면이 수행할 수 있는 관리 연산의 닫힌 열거형이고 모든 상수가 애플리케이션 런타임이 닿으면 안 되는 것이며 여기 모아 두는 것이 경계를 어떤 메서드가 있느냐에서 나오는 성질이 아니라 누군가 검토할 수 있는 목록으로 만든다고 적는다. 13번 CREATE_COLLECTION 과 16번 COLL_MOD 와 46번 BALANCER_CONTROL 이 false 이고 37번 SHARD_COLLECTION 과 40번 REFINE_SHARD_KEY 와 43번 RESHARD_COLLECTION 과 49번 MANAGE_ENCRYPTION_KEY 가 true 이며, 그 일곱 가운데 고위험이 4 개, 열거형 전체에서 고위험이 8 개, 상수 전체가 15 개다. 다음으로 승인을 실제로 넘기는 main 호출이 0 개이고, execute 를 부르는 main 자리 일곱이 나열되는데 MongoQueryableEncryptionCollectionManager 41번과 63번과 70번, MongoShardingAdminGateway 64번과 75번과 86번과 92번이며, 시험에서 세 인자 execute 를 부르는 줄은 10 개이고, MongoAdminApproval 을 만드는 main 줄은 0 개이며 그 이름이 main 에 나오는 자리는 MongoAdminApproval 자신의 19·21·31·33번과 MongoAdminGateway 83번의 서명 다섯이고, 시험에서 그것을 만드는 줄은 4 개다. 이어서 main 에 나오는 연산 상수마다 위험 등급과 등장 줄이 대조되는데 BALANCER_CONTROL 과 COLL_MOD 와 CREATE_COLLECTION 이 false 이고 MANAGE_ENCRYPTION_KEY 와 REFINE_SHARD_KEY 와 RESHARD_COLLECTION 과 SHARD_COLLECTION 이 true 이며 각각 한 줄씩이다. 고위험인데 다섯 인자 오버로드로 넘어가는 자리 넷이 실리는데 MongoShardingAdminGateway 64번과 75번과 86~87번, 그리고 MongoQueryableEncryptionCollectionManager 61~65번의 rotateDataKey 가 64번에서 MANAGE_ENCRYPTION_KEY 를 넘기는 자리이며, 대조로 같은 클래스 34~43번의 createEncryptedCollection 이 41~42번에서 넘기는 CREATE_COLLECTION 은 false 다. 마지막으로 ReshardApproval 이 main 에 나오는 줄이 4 개라고 나오고 그 넷이 MongoShardingAdminGateway 70번과 81번의 파라미터와 ReshardApproval 자신의 15번과 22번이다." caption="샤딩 게이트웨이의 네 메서드와 그 네 호출 · 승인 자리에 null 을 넣는 다섯 인자 오버로드 · 고위험에 승인을 요구하는 세 인자 오버로드 · 연산 열거형의 위험 등급과 계수 · 승인을 넘기는 main 호출 0 과 execute 일곱 자리 · 고위험을 넘기는 넷 · 두 승인 타입을 잇는 코드 부재 — 215줄 · exit 0" zoom="true" -::: - -`MongoShardingAdminGateway:38` 의 `shardCollection` 이 컬렉션과 샤드 키와 준비도 보고서와 지원 인덱스 필드를 받는다. `:49`\~`:53` 이 승인되지 않은 샤드 키를 거절하고, `:54`\~`:63` 이 지원 인덱스가 샤드 키로 시작하지 않으면 거절한다. - -`:68` 의 `refineShardKey` 와 `:79` 의 `reshardCollection` 은 `ReshardApproval` 을 `require()` 한다. `:91` 의 `controlBalancer` 는 앞선 검사가 없다. - -넷 다 마지막 줄에서 `adminGateway.execute(...)` 를 부른다. - -## 그 오버로드가 승인 자리에 넣는 것 - -`MongoAdminGateway` 에 `execute` 가 둘이다. `:57`\~`:62` 의 다섯 인자 오버로드가 연산과 대상과 운영자와 사유와 본문을 받고, `:83` 의 세 인자 오버로드가 `MongoAdminCommand` 와 `MongoAdminApproval` 과 본문을 받는다. - -다섯 인자 쪽이 `:63`\~`:67` 에서 `MongoAdminCommand.routine(...)` 으로 일상 명령을 만들어 세 인자 쪽에 넘긴다. `:66` 이 승인 자리다. `null` 이다. - -## 세 인자 오버로드가 그 null 을 거절한다 - -`:87` 이 권한을 요구하고 `:88`\~`:93` 이 만료된 명령을 거절한다. - -`:94` 가 연산이 고위험인지 본다. 고위험이면 `:95` 가 승인이 `null` 인지 보고, 그렇다면 `:96`\~`:102` 가 거절한다. 메시지는 데이터를 파괴하거나 컬렉션을 다시 쓰는 연산은 이 명령에 묶인 승인 아래에서 돌거나 아예 돌지 않는다는 것이다. - -`:103` 이 승인 자체를 검사하고 `:106` 이 단일 사용을 강제한다. `:104`\~`:105` 주석은 그것이 없으면 한 reshard 에 대한 승인 하나가 같은 모양의 이후 모든 reshard 를 허가하게 된다고 적는다. - -승인이 `null` 인 고위험 명령은 `:95` 에서 끝난다. - -## 넷이 그 조합에 걸린다 - -`MongoAdminOperation` 상수는 열다섯이고 그중 여덟이 `highRisk(true)` 다. - -main 에서 다섯 인자 오버로드로 넘어가는 연산은 일곱 줄에 나뉘어 있다. `BALANCER_CONTROL` 과 `COLL_MOD` 와 `CREATE_COLLECTION` 은 `false` 이고, `SHARD_COLLECTION` 과 `REFINE_SHARD_KEY` 와 `RESHARD_COLLECTION` 과 `MANAGE_ENCRYPTION_KEY` 는 `true` 다. - -고위험 넷의 위치는 `MongoShardingAdminGateway:64`·`:75`·`:86`\~`:87` 과 `MongoQueryableEncryptionCollectionManager:63`\~`:64` 다. 마지막 것은 `:62` 의 `rotateDataKey` 로, 자바독이 자기 런북과 증거를 요구한다고 적는다. - -넷 다 호출자가 무엇을 넘겨도 `:95` 를 지나지 못한다. 승인 자리가 그 오버로드 안에서 이미 정해져 있기 때문이다. - -## 승인 객체를 만드는 main 코드가 없다 - -`MongoAdminApproval.of(...)` 나 `new MongoAdminApproval` 을 부르는 main 줄이 0 이다. - -그 이름이 main 에 나오는 자리는 다섯인데, 넷이 `MongoAdminApproval` 자신의 선언과 팩터리이고 하나가 `MongoAdminGateway:83` 의 서명이다. - -세 인자 `execute` 를 부르는 시험 줄은 `MongoAdminAuditStateMachineTest` 에 10 이고, 승인 객체를 만드는 시험 줄은 4 다. 프로덕션에는 그 짝이 없다. - -`MongoShardingAdminGateway` 의 앞선 검사들은 그 뒤를 바꾸지 못한다. `:49` 의 준비도 검사와 `:54` 의 인덱스 검사와 `:74`·`:85` 의 `ReshardApproval.require()` 를 전부 통과해도 `:64`·`:75`·`:86` 에서 같은 자리에 막힌다. - -`ReshardApproval` 과 `MongoAdminApproval` 은 다른 타입이다. `ReshardApproval` 이 main 에 나오는 줄은 넷인데 `MongoShardingAdminGateway:70`·`:81` 의 파라미터 둘과 `ReshardApproval:15`·`:22` 의 자기 선언 둘이다. `MongoAdminApproval` 을 만드는 자리는 그 안에 없다. - -## 원문에 없는 것 - -원문은 샤딩 게이트웨이의 네 작업 중 셋이 어떤 입력으로도 완료될 수 없다고 적는다. 그 셋은 그대로 확인된다. - -네 번째가 있다. `MongoQueryableEncryptionCollectionManager:63`\~`:64` 의 `rotateDataKey` 도 같은 오버로드에 `MANAGE_ENCRYPTION_KEY` 를 넘기고, 그 상수도 `highRisk(true)` 다. 같은 클래스 `:41`\~`:42` 의 `CREATE_COLLECTION` 은 `false` 라 걸리지 않는다. - -다섯 인자 오버로드에 고위험 연산을 넘기는 main 자리를 저장소 전체에서 세면 넷이고, 그 넷이 두 클래스에 나뉘어 있다. 샤딩 리프만의 문제가 아니다. - -## 확인하지 못한 것 - -그 넷을 불러 예외가 나는 것을 프로브로 보이지 않았다. - -`MongoShardingAdminGateway` 와 `MongoQueryableEncryptionCollectionManager` 를 만드는 프로덕션 코드가 있는지 조립 경로를 세지 않았다. - -포크가 승인을 어디서 만들어 넘기도록 설계된 것인지 설계 문서로 판단하지 않았다. - -`ReshardApproval` 을 `MongoAdminApproval` 로 옮기는 코드가 왜 없는지, 두 타입의 관계를 설계 문서로 확인하지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f024.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f024.md deleted file mode 100644 index ea938d3..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f024.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a06-f024 -title: promotion 증거 어휘가 둘이고, gate는 하나만 검사한다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a06-f024 -evidenceCapturedOn: 2026-09-01 -assets: - - key: analysis-finding-a06-f024 - file: ../../../final/evidence/rendered/analysis-finding-a06-f024.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a06-f024.txt -source: - - 원본 분석 절은 final/document.md#a06#L1305 이다. ---- - -# promotion 증거 어휘가 둘이고, gate는 하나만 검사한다 - -승격 증거의 필수 범주가 여섯이고 게이트가 그 여섯을 검사한다. 벡터 검색 벤치마크 게이트는 완전히 다른 다섯 범주를 요구하는데 그 집합을 읽는 프로덕션 코드가 없다. - -## 관계 - -- **빠뜨림이 통과가 되는 게이트는 게이트가 아니다** - 이 사례가 그 규칙의 형태다. -- **같은 개념의 두 어휘가 공존하면 하나를 죽은 것으로 표시한다** - 증거 어휘가 둘인 구조다. -- **능력 등급은 코드가 아니라 실행된 증거에서 파생한다** - 승격이 요구하는 증거의 성질이다. - -## 문제 - -승격 증거의 필수 범주는 여섯이다. 안정 플랫폼과 실제 토폴로지와 보안과 마이그레이션과 실패와 런북이다. - -승격 게이트가 그 여섯을 전부 검사한다. - -그리고 그 파일에는 고쳐진 결함이 주석으로 남아 있다. 한 범주가 필수 목록에는 있고 게이트에는 없어서, 게이트가 선언한 여섯 중 다섯만 요구했다는 것이다. 주석은 그것을 실행한 것보다 많이 인증하는 게이트의 모양이라고 부른다. - -## 결론 - -같은 모양이 모듈 경계를 건너 다시 나타난다. - -벡터 검색 벤치마크 게이트는 완전히 다른 다섯 범주를 요구한다. 인덱스 준비도와 재현율과 지연과 메모리와 실제 토폴로지다. - -겹치는 것은 실제 토폴로지 하나뿐이고, 이 집합을 읽는 프로덕션 코드가 없다. 승격 게이트는 이 집합을 모른다. - -그래서 벡터 검색을 승격하는 경로는 승격 게이트를 통과할 수 있고, 그 통과는 재현율과 지연과 인덱스 메모리에 대해 아무것도 말하지 않는다. - -벤치마크 게이트의 javadoc 이 정확히 그 위험을 적는데도 그렇다. 기능적 성공은 벡터 검색의 증거가 아니며, 근사 인덱스는 어떤 질의에 대해서도 결과를 돌려주고 그것이 옳은 결과인지는 재현율에 달렸다는 것이다. - -방금 한 범주 누락으로 고쳤던 것과 같은 모양이 반복된다. 선언한 것보다 적게 검사하는 게이트다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 두 게이트의 요구 범주 대조와 참조 계수 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/137 계열에 있다. - -1. 승격 증거의 필수 범주 목록을 확인한다. -2. 승격 게이트가 검사하는 범주를 확인한다. -3. 그 파일의 고쳐진 결함 주석을 읽는다. -4. 벤치마크 게이트가 요구하는 범주를 확인한다. -5. 그 집합을 읽는 프로덕션 코드를 센다. - -## 본문 - - - -`MongoAdvancedPromotionEvidence.REQUIRED`는 여섯 범주다 — `stable-platform`, `actual-topology`, `security`, `migration`, `failure`, `runbook`. `MongoAdvancedPromotionGate.verify(...)`가 그 여섯을 전부 검사한다. 그 파일에는 고쳐진 결함이 주석으로 남아 있다 — "`migration` was in `MongoAdvancedPromotionEvidence.REQUIRED` and not here, so the gate demanded five of the six categories it declares… which is the shape MNG-008 names: a gate that certifies more than it ran." - -## MongoAdvancedPromotionEvidence 참조 위치 - -:::evidence key="analysis-finding-a06-f024" alt="코드베이스에서 MongoAdvancedPromotionEvidence 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MongoAdvancedPromotionEvidence 코드베이스 검색 — 3줄 · exit 0" zoom="true" -::: - -## 다른 gate가 완전히 다른 다섯 범주를 반환한다 - -`MongoVectorSearchBenchmarkGate.requiredEvidence()`는 `index-readiness`, `recall`, `latency`, `memory`, `actual-topology`를 반환한다. 겹치는 것은 `actual-topology` 하나뿐이고, 이 집합을 읽는 production 코드는 없다(`137-...` §8.3). `MongoAdvancedPromotionGate`는 이 집합을 모른다. - -## 통과가 아무것도 말하지 않는 구간 - -vector search를 promotion하는 경로는 `MongoAdvancedPromotionGate.verify`를 통과할 수 있고, 그 통과는 recall·latency·index memory에 대해 아무것도 말하지 않는다 — `MongoVectorSearchBenchmarkGate`의 javadoc이 정확히 그 위험을 적는데도: "Functional success is not evidence for vector search. An approximate index returns results for any query; whether they are the right results depends on recall." 방금 `migration` 누락으로 고쳤던 것과 같은 모양이 모듈 경계를 건너 다시 나타난다. P3. - -## 확인하지 못한 것 - -벡터 검색 승격을 실제로 시도해 통과하는 것을 재현하지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f025.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f025.md deleted file mode 100644 index f578ae1..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f025.md +++ /dev/null @@ -1,95 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a06-f025 -title: 구현 없는 4개의 계약 중 셋은 그 사실을 적고, 하나는 적지 않는다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a06-f025 -evidenceCapturedOn: 2026-09-01 -assets: - - key: analysis-finding-a06-f025 - file: ../../../final/evidence/rendered/analysis-finding-a06-f025.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a06-f025.txt -source: - - 원본 분석 절은 final/document.md#a06#L1324 이다. ---- - -# 구현 없는 4개의 계약 중 셋은 그 사실을 적고, 하나는 적지 않는다 - -세 인터페이스가 구현이 없다는 사실을 같은 문단으로 명시한다. 네 번째도 구현이 0 인데 그 문단이 없고, 넷 중 오해가 가장 비싼 것이 그것이다. - -## 관계 - -- **등급은 네 단계로 나누고 관측보다 높게 적지 않는다** - 같은 계열의 자기 한정 규칙이다. -- **Bean 애너테이션이 있다는 것은 조립 증거가 아니다** - 인터페이스의 존재가 능력의 증거가 아니라는 규칙이다. -- **Hibernate filter는 보안 경계가 아니다** - 격리 보장의 주체를 확인하는 규칙이다. - -## 문제 - -세 인터페이스가 같은 문단을 담는다. - -이 저장소는 구현을 출하하지 않는다. 메서드 시그니처를 사용 가능한 능력이 아니라 명세로 읽어야 한다. 구현이 없는 인터페이스는 주입될 수 없고, 그것을 출하된 동작으로 다루는 것이 플랫폼이 검색을 지원한다는 말이 문서에서는 참이고 배포에서는 거짓이 되는 방식이다. - -훌륭한 자기 한정이고 이 리프에서 반복적으로 필요했던 종류의 정직함이다. - -## 결론 - -네 번째 인터페이스도 구현이 0 인데 그 문단이 없다. - -네 인터페이스 모두 구현 검색이 일치를 내지 않는다. - -그리고 넷 중 오해가 가장 비싼 것이 바로 그것이다. javadoc 이 테넌트 술어 없이는 실행될 수 없는 연산이라고 시작하므로 능동적인 안전장치로 읽힌다. - -실제로 그 보장을 제공하는 것은 별도의 술어 주입기이고 그것은 구현이 있다. 이 인터페이스는 어떤 배포가 구현했을 때 그 주입기를 부르게 되는 형태일 뿐이다. - -즉 이름과 첫 문장이 보장을 약속하는데, 그 보장을 만드는 것은 다른 타입이고 이 타입은 비어 있다. - -수정은 같은 자기 한정 문단을 이 인터페이스에도 추가하고, 실제 보장이 어디서 오는지 함께 적는 것이다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 네 인터페이스의 구현 검색과 javadoc 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/137 계열에 있다. - -1. 세 인터페이스의 자기 한정 문단을 읽는다. -2. 네 인터페이스 모두에 대해 구현 검색을 수행한다. -3. 네 번째 인터페이스의 javadoc 첫 문장을 읽는다. -4. 실제로 테넌트 술어를 강제하는 타입을 찾는다. - -## 본문 - - - -`MongoSearchOperations`·`MongoTimeSeriesOperations`·`MongoVectorSearchOperations`는 모두 동일한 문단을 담는다. - -> **Scaffold.** This repository ships no implementation… Read a method signature as a specification, not as an available capability — an interface with no implementation cannot be injected, and treating it as shipped behaviour is how "the platform supports search" becomes true in a document and false in a deployment. - -## MongoSearchOperations 참조 위치 - -:::evidence key="analysis-finding-a06-f025" alt="코드베이스에서 MongoSearchOperations 를 검색한 출력 1줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MongoSearchOperations 코드베이스 검색 — 1줄 · exit 0" zoom="true" -::: - -## 넷째에는 그 문단이 없다 - -`TenantScopedMongoOperations`도 구현이 0인데(`137-...` §8.3d: 네 interface 모두 `implements` 검색 exit=1) 그 문단이 없다. - -## 하필 오해가 가장 비싼 것이다 - -javadoc이 "Operations that cannot run without a tenant predicate"라고 시작하므로 능동적인 안전장치로 읽힌다. 실제로 그 보장을 제공하는 것은 `MongoTenantPredicateInjector`(policy, 구현 있음)이고, 이 interface는 fork가 구현했을 때만 그 injector를 부르게 되는 **형태**일 뿐이다. P3. - -## 확인하지 못한 것 - -이 인터페이스를 구현했을 때 실제로 주입기가 호출되는 경로가 있는지 확인하지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f026.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f026.md deleted file mode 100644 index 707a3e6..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f026.md +++ /dev/null @@ -1,112 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a06-f026 -title: 커버리지 gate 둘이 나란히 있고, 하나는 발화할 수 없다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a06-f026 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a06-f026.body.md -assets: - - key: analysis-finding-a06-f026 - file: ../../../final/evidence/rendered/analysis-finding-a06-f026.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a06-f026.txt -source: - - 원본 분석 절은 final/document.md#a06#L1383 이다. ---- - -# 커버리지 gate 둘이 나란히 있고, 하나는 발화할 수 없다 - -계약 묶음의 javadoc 은 실패한 계약과 한 번도 돌지 않은 계약을 구분한다고 적는다. 구현에서 실행 집합이 전체와 항상 같으므로 누락 항목이 어떤 입력으로도 생성되지 않는다. 같은 테스트킷에 옳게 구현된 형제가 있다. - -## 관계 - -- **빠뜨림이 통과가 되는 게이트는 게이트가 아니다** - 이 사례가 그 규칙의 형태다. -- **아무것도 발견하지 못한 레인은 성공이 아니라 실패여야 한다** - 같은 원칙의 레인 판이다. -- **중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다** - 형제 구현과 비교하는 방법이다. - -## 문제 - -계약 묶음의 javadoc 이 존재 이유를 적는다. - -보고서는 실패한 계약과 한 번도 돌지 않은 계약을 구분한다. 절반이 건너뛰어졌기 때문에 실패 없음을 보고하는 묶음은 아무것도 인증하지 않는 초록 빌드의 전형이므로, 누락된 계약은 여기서 실패다. - -## 결론 - -구현이 그 구분을 만들 수 없다. - -루프가 실행 집합을 무조건 채운다. 각 계약을 실행 집합에 넣고 그다음 검사를 수행한다. - -그러므로 실행 집합은 전체 집합과 언제나 같고, 누락 집합은 언제나 비며, 실행되지 않음 항목은 어떤 입력으로도 생성되지 않는다. - -인증 여부를 판정하는 조건도 마찬가지로 항상 참이다. - -조건부 형제가 같은 테스트킷 안에 있다. 카오스 게이트의 보고 메서드는 같은 일을 옳게 한다. 실행 집합이 명시적 기록 호출로만 채워지는 맵이고, 누락은 전체에서 기록되지 않은 것을 뺀 것이다. - -그리고 그 형제의 테스트가 그것을 증명한다. 열세 시나리오 중 하나만 기록하고 나머지가 실행되지 않음으로 나타나는지 단언한다. - -계약 묶음의 대응 테스트는 그렇게 하지 않는다. 모든 계약에 통과를 주고 나서 실행 집합이 전부를 담는지 단언한다. 구조상 항상 참인 것을 단언하는 것이다. - -판정은 두 인증 레인의 커버리지 주장이 무효라는 것이다. - -수정은 형제를 따르면 된다. 실행 메서드가 실행할 계약 집합을 인자로 받거나, 실제로 호출된 것만 실행 집합에 넣는 것이다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 구현 코드 확인과 형제 구현 비교 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/138 계열에 있다. - -1. 계약 묶음의 javadoc 을 읽는다. -2. 보고 메서드의 루프에서 실행 집합이 어떻게 채워지는지 확인한다. -3. 누락 집합이 어떻게 계산되는지 확인한다. -4. 카오스 게이트의 같은 메서드와 비교한다. -5. 두 테스트가 각각 무엇을 단언하는지 비교한다. - -## 본문 - - - -`MongoStableContractSuite`의 javadoc이 존재 이유를 적는다. - -> The report distinguishes a failed contract from a contract that never ran. A suite that reports "no failures" because half of it was skipped is exactly the shape of green build that certifies nothing, **so a missing contract is a failure here.** - -## suite javadoc 이 적은 존재 이유 - -:::evidence key="analysis-finding-a06-f026" alt="분석 문서 final/document.md#a06 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a06 발췌 — 15줄" zoom="true" -::: - -## 구현은 그 구분을 만들 수 없다 - -```java -Set executed = new LinkedHashSet<>(); -for (MongoReplicaSetContract contract : MongoReplicaSetContract.all()) { - executed.add(contract); // ← 루프가 무조건 채운다 - if (!contractRunner.test(contract)) { failures.add(...); } -} -Set missing = new LinkedHashSet<>(MongoReplicaSetContract.all()); -missing.removeAll(executed); // ← 항상 비어 있다 -missing.forEach(contract -> failures.add(... + " (not executed)")); -``` - -`executed`는 `all()`과 언제나 같으므로 `missing`은 언제나 비고, `(not executed)` 항목은 **어떤 입력으로도 생성되지 않는다**. `certified()`의 `executed.containsAll(all())`(78행)도 항상 참이다. 두 인증 lane(7.0/8.0)의 커버리지 주장이 무효다. P2. - -## 조건부 형제가 같은 testkit 안에 있다 - -`MongoChaosGate.report()`는 같은 일을 옳게 한다 — `executed`는 명시적 `record(scenario, passed)` 호출로만 채워지는 map이고, `missing`은 `all()`에서 기록되지 않은 것을 뺀 것이다. 그 test가 그것을 증명한다: `aScenarioThatNeverRanIsAFailureRatherThanASilence`는 13개 시나리오 중 **하나만** 기록하고 나머지가 `(not executed)`로 나타나는지 단언한다. 수정은 형제를 따르면 된다 — `run(...)`이 실행할 contract 집합을 인자로 받거나, runner가 실제로 호출된 것만 `executed`에 넣는 것. - -## 확인하지 못한 것 - -계약 하나를 실제로 건너뛰게 만들어 보고서가 여전히 깨끗한지 재현하지 않았다. 코드 구조상 그 결과가 나온다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f027.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f027.md deleted file mode 100644 index 7dfbe65..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f027.md +++ /dev/null @@ -1,101 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a06-f027 -title: release gate가 실제로 차단하는 것은 hermetic test 3개이고, mongo용 CI workflow는 없다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a06-f027 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a06-f027.body.md -assets: - - key: analysis-finding-a06-f027 - file: ../../../final/evidence/rendered/analysis-finding-a06-f027.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a06-f027.txt -source: - - 원본 분석 절은 final/document.md#a06#L1410 이다. ---- - -# release gate가 실제로 차단하는 것은 hermetic test 3개이고, mongo용 CI workflow는 없다 - -증거 검증기는 정성껏 만들어져 있다. 그 검증기가 실제로 지키는 차단 목록 셋은 전부 컨테이너가 필요 없는 클래스이고, 실험 셋은 어느 빌드 파일에도 등록되지 않은 태스크를 가리키며, 컨테이너가 필요한 여섯 레인은 차단 목록에 하나도 없다. - -## 관계 - -- **아무도 돌리지 않는 레인의 게이트는 마지막으로 돌린 사람이 본 것을 보고한다** - 이 사례가 그 규칙의 형태다. -- **빠뜨림이 통과가 되는 게이트는 게이트가 아니다** - 같은 계열의 상위 규칙이다. -- **인증 레인만 Docker 가드를 달지 않는다** - 컨테이너 레인을 게이트에 묶는 다른 가족의 결정이다. - -## 문제 - -이 리프는 릴리스 증거 장치를 정성껏 만들었다. - -증거 검증기는 종료 코드 대신 테스트 결과 XML 을 읽는다. 묶음 이름이 계약의 클래스와 일치하는지 확인하고, 파일이 실행 시작 시각보다 오래됐으면 거부하며, 전부 건너뛴 레인을 거부한다. - -그 근거도 정확하다. Gradle 테스트 태스크는 테스트를 돌렸을 때도 선택자가 다른 테스트에 맞았을 때도 0 으로 끝나므로, 샤딩 토폴로지가 인증됐다는 주장이 이름에 특정 문자열이 들어간 밀폐 단위 테스트로 충족될 수 있었다는 것이다. - -## 결론 - -그 장치가 실제로 지키는 목록을 열어 보면 다르다. - -차단 계약 셋이 전부 토폴로지 없음이다. 즉 컨테이너가 필요 없는 밀폐 클래스다. - -실험 셋은 어느 빌드 파일에도 등록되지 않은 태스크를 가리킨다. 스크립트가 그 사실을 스스로 적는다. - -그리고 컨테이너가 필요한 여섯 레인은 차단 목록에 하나도 없다. 복제 세트와 장애 조치와 마이그레이션과 호환성과 보안 통합과 성능이다. - -이것은 개별 코드 결함이 아니라 이 리프의 검증 지형이다. - -그리고 앞선 발견들이 왜 살아남았는지를 설명한다. 변경 소실과 TLS 미적용과 샤딩 미완료와 펜스 계약 위반을 잡을 레인은 릴리스를 막지 않고 CI 에서 돌지 않는다. - -수정은 두 갈래다. 컨테이너 레인 중 최소한 복제 세트와 마이그레이션과 보안 통합을 차단 계약으로 승격하고, 다른 가족과 같은 형태의 워크플로를 추가하는 것이다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Gradle : 9.0.0 -확인 방식 : 릴리스 계약 정의 파일과 빌드 파일 대조, CI 워크플로 계수 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/138 계열에 있다. - -1. 증거 검증기가 무엇을 검사하는지 읽는다. -2. 릴리스 계약 정의 파일에서 차단 목록과 실험 목록을 확인한다. -3. 각 항목의 토폴로지 값을 확인한다. -4. 실험 목록의 태스크가 빌드 파일에 등록되어 있는지 확인한다. -5. 컨테이너가 필요한 레인 목록과 차단 목록을 대조한다. -6. CI 워크플로에서 이 리프를 이름에 담은 것을 센다. - -## 본문 - - - -release gate가 실제로 차단하는 것은 hermetic test 3개이고, mongo용 CI workflow는 없다. - -## release gate 가 실제로 거는 것 - -:::evidence key="analysis-finding-a06-f027" alt="분석 문서 final/document.md#a06 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a06 발췌 — 15줄" zoom="true" -::: - -## 개별 코드 결함이 아니라 검증 지형이다 - -앞선 sub-scope들에서 찾은 것들 — §67의 change stream 소실, §75의 TLS 미적용, §85의 sharding 미완료, §56의 fence 계약 — 이 왜 살아남았는지를 설명한다: 그것들을 잡을 lane은 릴리스를 막지 않고 CI에서 돌지 않는다. P2. - -## 수정은 두 갈래다 - -(a) 컨테이너 lane 중 최소한 `mongoReplicaSetTest`·`mongoMigrationTest`·`mongoSecurityIntegrationTest`를 blocking contract로 승격하고, (b) JPA와 같은 형태의 workflow를 추가하는 것. - -## 확인하지 못한 것 - -릴리스 게이트를 실제로 실행하지 않았다. 확인한 것은 그것이 지키는 목록의 구성이다. - -차단 계약 셋 전부가 topology=none, 즉 컨테이너가 필요 없는 hermetic 클래스다. experimental 셋은 어느 build 파일에도 등록되지 않은 task를 가리킨다(grep mongoShardedTest build.gradle → 매치 0; 스크립트가 그 사실을 스스로 적는다: registered by no build file). 그리고 컨테이너가 필요한 여섯 lane — mongoReplicaSetTest·mongoFailoverTest·mongoMigrationTest·mongoCompatibilityTest·mongoSecurityIntegrationTest·mongoPerformanceTest — 은 차단 목록에 하나도 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f028.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f028.md deleted file mode 100644 index 84f7011..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a06-f028.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a06-f028 -title: 소비자가 없는 fixture 셋 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a06-f028 -evidenceCapturedOn: 2026-09-01 -assets: - - key: analysis-finding-a06-f028 - file: ../../../final/evidence/rendered/analysis-finding-a06-f028.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a06-f028.txt -source: - - 원본 분석 절은 final/document.md#a06#L1433 이다. ---- - -# 소비자가 없는 fixture 셋 - -테스트킷의 세 타입이 테스트와 테스트킷 양쪽 모두에서 소비자가 0 이다. 그중 하나는 서버를 통과하는 왕복 계약을 실행하도록 만들어진 것이고, 그 계약은 열거된 계약 목록에 들어 있다. - -## 관계 - -- **계약 테스트는 어댑터가 실제로 돌리는 statement를 실행해야 한다** - 왕복 계약이 존재하는 이유다. -- **release gate가 실제로 차단하는 것은 hermetic test 3개이고 이 리프용 CI workflow는 없다** - 같은 리프의 검증 지형을 다룬 사례다. -- **Bean 애너테이션이 있다는 것은 조립 증거가 아니다** - 타입의 존재와 사용을 나눠 세는 규칙이다. - -## 문제 - -테스트킷은 계약을 실행할 도구를 담는다. 그 도구들이 실제로 쓰이는지 확인이 필요하다. - -## 결론 - -소비자 계수에서 양쪽 모두 0 인 타입이 셋이다. - -로컬 컨테이너 타입은 검색과 벡터 계약의 빠른 피드백용이다. 관련 보고 타입은 테스트 한 곳에서 쓰이지만 실제 컨테이너를 띄우는 곳은 없다. - -청크 이동 제어기는 트래픽 중 청크 이동을 재현하는 유일한 장치다. 재조정 중 프로덕션 요청이 들어오는 상황을 만든다. - -왕복 계약은 자바에서 BSON 으로 서버를 거쳐 원시 BSON 으로 다시 자바로 돌아오는 왕복을 검증한다. javadoc 이 그 이유를 적는다. 왕복의 절반은 아무것도 증명하지 않으며 가운데의 원시 BSON 만이 그것을 보여 준다. - -셋째가 가장 무겁다. - -계약 열거형이 골든 BSON 을 열거된 계약으로 두는데, 그 계약을 실행하도록 만들어진 타입에 호출자가 없다. - -BSON 스냅숏과 그 단언 도우미는 쓰인다. 그래서 정규형 단언은 존재하지만 서버를 통과하는 왕복은 돌지 않는다. - -그리고 그 차이가 정확히 이 클래스가 존재하는 이유다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 테스트킷 타입별 소비자 계수 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/138 계열에 있다. - -1. 테스트킷의 타입 목록을 만든다. -2. 각 타입의 테스트와 테스트킷 소비자를 센다. -3. 소비자가 0 인 타입의 javadoc 을 읽는다. -4. 왕복 계약이 계약 열거형에 있는지 확인한다. -5. 정규형 단언 도우미가 쓰이는지 확인한다. - -## 본문 - - - -`138-...` §8.1의 소비자 계수에서 test·testkit 양쪽 모두 0인 타입이 셋이다. - -| 타입 | 무엇을 위한 것인가 | -|---|---| -| `MongoAtlasLocalContainer` | Atlas Local 컨테이너 — search·vector 계약의 빠른 피드백용. `MongoAtlasCapabilityContractSuite`(report 타입)는 test 1곳에서 쓰이지만, **실제 컨테이너를 띄우는 곳은 없다** | -| `MongoChunkMigrationController` | 트래픽 중 청크 이동 — "production hits during a rebalance"를 재현하는 유일한 장치 | -| `MongoRoundTripContract` | Java → BSON → **서버** → raw BSON → Java 왕복. javadoc: "Half a round trip proves nothing… only the raw BSON in the middle shows it" | - -## test 와 testkit 양쪽에서 0 인 타입 - -:::evidence key="analysis-finding-a06-f028" alt="분석 문서 final/document.md#a06 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a06 발췌 — 15줄" zoom="true" -::: - -## 셋째가 가장 무겁다 - -`MongoReleaseContract`의 형제인 `MongoReplicaSetContract`는 `GOLDEN_BSON`을 열거된 계약으로 두는데, 그 계약을 실행하도록 만들어진 타입에 호출자가 없다. `MongoBsonSnapshot`·`MongoBsonSnapshotAssert`는 쓰이므로 **정규형 단언은 존재하지만 서버를 통과하는 왕복은 돌지 않는다** — 그리고 그 차이가 정확히 이 클래스가 존재하는 이유다. P3. - -## 확인하지 못한 것 - -왕복 계약을 실제로 실행해 원시 BSON 이 기대와 다른지 확인하지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-search-path-survived-the-return-to-the-pool.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-search-path-survived-the-return-to-the-pool.md deleted file mode 100644 index 138f1f4..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-search-path-survived-the-return-to-the-pool.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -kind: CASE -slug: search-path-survived-the-return-to-the-pool -title: search_path가 풀로 돌아간 커넥션에 남아 다음 tenant가 상속한다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:search-path-survived-the-return-to-the-pool -evidenceCapturedOn: 2026-09-01 -assets: - - key: search-path-survived-the-return-to-the-pool - file: ../../../final/evidence/rendered/search-path-survived-the-return-to-the-pool.svg -evidence: - - ../../../final/evidence/raw/search-path-survived-the-return-to-the-pool.txt -source: - - 원본 분석 절은 final/document.md#a05 §13.2 이다. ---- - -# search_path가 풀로 돌아간 커넥션에 남아 다음 tenant가 상속한다 - -검색 경로는 세션 설정이므로 풀로 돌아간 커넥션이 마지막 테넌트의 스키마를 여전히 들고 있다. 다음 차용자는 어떤 문장도 틀리지 않은 채 그 스키마에서 읽고 쓴다. - -## 관계 - -- **격리 설정은 트랜잭션 로컬이어야 한다** - 이 사례에서 끌어낸 규칙이다. -- **세션 스코프 설정은 풀로 돌아간 커넥션에 남는다** - 같은 성질을 일반화한 규칙이다. -- **네 가지 멀티테넌시 전략과 각각의 격리 경계** - 이 전략의 실패 모드다. - -## 문제 - -테넌트별 스키마 전략은 검색 경로로 격리한다. 커넥션마다 그 테넌트의 스키마를 검색 경로에 넣는다. - -검색 경로는 세션 설정이다. 트랜잭션이 끝나도 사라지지 않는다. - -## 결론 - -풀로 돌아간 커넥션이 마지막 테넌트의 스키마를 들고 있다. - -다음 차용자는 아마 다른 테넌트이거나 테넌트가 없는 백그라운드 작업이다. 그 차용자가 실행하는 문장은 전부 문법적으로 옳고 의미적으로도 옳다. 다만 다른 테넌트의 스키마에서 읽고 쓴다. - -이 실패에는 오류가 없다. 테이블 이름이 같으므로 조회가 성공하고 결과가 돌아온다. - -해법은 반환 시 검색 경로를 중립 스키마로 되돌리는 것이다. 이 구현은 시스템 카탈로그 스키마를 중립 값으로 쓴다. - -같은 성질이 이 저장소의 다른 곳에도 있다. H2 의 로컬 타임아웃 설정도 세션 스코프라 트랜잭션이 끝나도 남는다. 그쪽은 매 트랜잭션 전에 다시 적용하므로 실무상 가려진다. - -두 대응이 다르다. 하나는 반환 시 되돌리고 다른 하나는 사용 전 덮어쓴다. 후자는 값이 항상 설정되는 경우에만 안전하다. - -## 검증 환경 - -데이터베이스 : PostgreSQL -확인 방식 : 커넥션 제공자의 재설정 구현과 javadoc 확인 -소스 수정 : x - -## 재현 조건 - -1. 스키마 멀티테넌트 커넥션 제공자의 재설정 구현을 읽는다. -2. 중립 스키마 상수를 확인한다. -3. 그 재설정이 커넥션 반환 시점에 호출되는지 확인한다. -4. H2 로컬 타임아웃 설정기의 대응 방식과 비교한다. - -## 본문 - - - -`search_path`는 세션 설정이라 풀로 돌아간 커넥션이 여전히 마지막 tenant의 스키마를 들고 있다. - -## search_path 가 세션 설정이라는 것 - -:::evidence key="search-path-survived-the-return-to-the-pool" alt="분석 문서 final/document.md#a05 에서 이 기록의 근거 절을 그대로 잘라낸 16줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a05 발췌 — 16줄" zoom="true" -::: - -## 다음 borrower 가 아무 오류 없이 거기서 읽고 쓴다 - -아마 다른 tenant, 또는 tenant 없는 백그라운드 job이다 — **어떤 statement도 틀리지 않은 채로** 일어난다. - -## 해법과 같은 성질의 다른 자리 - -반환 시 `NEUTRAL_SCHEMA = "pg_catalog"`로 되돌린다. 같은 성질이 H2의 로컬 타임아웃 설정에도 있고(세션 스코프라 트랜잭션이 끝나도 남는다) 그쪽은 매 트랜잭션 전 재적용으로 실무상 가려진다. - -## 확인하지 못한 것 - -풀 재사용에서 잔존을 실제로 관측하지 않았다. 이 전략은 실험 플래그 뒤에 있고 이 사이클에서 켜지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-tenant-pools-summed-past-the-server-ceiling.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-tenant-pools-summed-past-the-server-ceiling.md deleted file mode 100644 index e2ad903..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-tenant-pools-summed-past-the-server-ceiling.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -kind: CASE -slug: tenant-pools-summed-past-the-server-ceiling -title: tenant별 풀이 개별적으로 합리적이고 그 합이 서버 상한을 넘긴다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:tenant-pools-summed-past-the-server-ceiling -evidenceCapturedOn: 2026-09-01 -assets: - - key: tenant-pools-summed-past-the-server-ceiling - file: ../../../final/evidence/rendered/tenant-pools-summed-past-the-server-ceiling.svg -evidence: - - ../../../final/evidence/raw/tenant-pools-summed-past-the-server-ceiling.txt -source: - - 원본 분석 절은 final/document.md#a05 §13.2 이다. ---- - -# tenant별 풀이 개별적으로 합리적이고 그 합이 서버 상한을 넘긴다 - -테넌트별 데이터베이스 전략은 특정한 방식으로 실패한다. 각 테넌트의 풀은 개별적으로 합리적이고 그 합이 아니다. 실패는 유휴 상태이던 테넌트를 포함해 모든 테넌트에 동시에 도착한다. - -## 관계 - -- **REQUIRES_NEW의 커넥션 비용과 풀 사이징 제약** - 같은 자원 계산의 다른 축이다. -- **네 가지 멀티테넌시 전략과 각각의 격리 경계** - 이 전략의 실패 모드다. -- **데드라인은 호출 예산에서 시작해 세 단계로 좁힌다** - 풀 획득이 예산의 일부라는 점에서 연결된다. - -## 문제 - -테넌트별 데이터베이스는 커넥션 자체가 격리 경계다. 각 테넌트가 자기 풀을 갖는다. - -풀 크기는 테넌트마다 정한다. 그 값은 개별적으로는 합리적이다. - -## 결론 - -합이 서버 상한을 넘는다. - -테넌트 50 개에 각각 커넥션 10 개면 500 이다. 서버의 최대 커넥션이 100 이면 400 이 거절된다. - -그리고 그 거절은 한 테넌트에만 오지 않는다. 유휴 상태이던 테넌트를 포함해 모든 테넌트에 동시에 커넥션 거절로 도착한다. 서버의 상한은 전역이기 때문이다. - -그래서 예산 타입이 두 상한을 갖는다. 풀 개수와 커넥션 총합이다. - -둘 다 필요한 이유가 javadoc 에 적혀 있다. 풀 개수만으로는 풀 크기 차이를 무시하고, 커넥션 총합만으로는 각자 스레드와 모니터링을 가진 무한한 수의 작은 풀을 허용한다. - -다만 예산 자체에 결함이 있다. 새 풀의 크기를 계산에 넣지 않아 상한을 넘길 수 있다. - -## 검증 환경 - -확인 방식 : 예산 타입의 두 상한과 javadoc 확인 -소스 수정 : x - -## 재현 조건 - -1. 테넌트 풀 예산 타입의 두 상한을 확인한다. -2. 각 상한이 필요한 이유를 javadoc 에서 읽는다. -3. 새 풀을 승인할 때 그 풀의 크기가 계산에 들어가는지 확인한다. - -## 본문 - - - -database-per-tenant는 특정한 방식으로 실패한다 — 각 tenant의 풀은 개별적으로 합리적이고 **그 합이 아니다.** 50 tenant × 10 커넥션 = `max_connections`가 100인 서버에 500 커넥션이고, 실패는 **idle이던 것 포함 모든 tenant에 동시에** connection refusal로 도착한다. - -## TenantPoolBudget 참조 위치 - -:::evidence key="tenant-pools-summed-past-the-server-ceiling" alt="코드베이스에서 TenantPoolBudget 를 검색한 출력 4줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="TenantPoolBudget 코드베이스 검색 — 4줄 · exit 0" zoom="true" -::: - -## ceiling 이 둘인 이유 - -pool count만으로는 풀 크기 차이를 무시하고, connection total만으로는 각자 스레드와 모니터링을 가진 무한한 수의 작은 풀을 허용한다. - -## 예산이 새 pool 크기를 계산하지 않는다 - -`final/document.md#a05` §98이 기록하듯 그래서 ceiling을 넘길 수 있다. - -## 확인하지 못한 것 - -다중 테넌트 풀을 실제로 세워 상한 초과를 재현하지 않았다. 이 전략은 실험 플래그 뒤에 있다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/concept/concept-four-multitenancy-strategies.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/concept/concept-four-multitenancy-strategies.md deleted file mode 100644 index 95714c0..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/concept/concept-four-multitenancy-strategies.md +++ /dev/null @@ -1,102 +0,0 @@ ---- -kind: CONCEPT -slug: four-multitenancy-strategies -title: 네 가지 멀티테넌시 전략과 각각의 격리 경계 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:four-multitenancy-strategies -evidenceCapturedOn: 2026-09-01 -assets: - - key: four-multitenancy-strategies - file: ../../../final/evidence/rendered/four-multitenancy-strategies.svg - - key: four-multitenancy-strategies-diagram - file: ../../../final/assets/diagrams/four-multitenancy-strategies.svg -evidence: - - ../../../final/evidence/raw/four-multitenancy-strategies.txt -source: - - 원본 분석 절은 final/document.md#a05 §13.2 이다. ---- - -# 네 가지 멀티테넌시 전략과 각각의 격리 경계 - -이 저장소는 네 전략을 각각 실험 플래그 뒤에 구현했다. 격리 경계가 다르고 실패 모드도 다르며, 넷 모두 테넌트 식별자의 형식 제약을 공통 기반으로 쓴다. - -## 관계 - -- **RLS가 성립하기 위한 세 전제** - 두 번째 전략의 전제 조건이다. -- **tenant별 풀이 개별적으로 합리적이고 그 합이 서버 상한을 넘긴다** - 네 번째 전략의 실패 모드다. -- **클래스패스에 있는 것은 실행 동의가 아니다** - 네 전략이 전부 플래그 뒤에 있는 이유다. - -## 본문 - - - -이 저장소가 네 전략(discriminator column · RLS · schema-per-tenant · database-per-tenant)을 각각 플래그 뒤에 두고 구현한 구조의 설명이다. - -## 격리 경계가 놓이는 층 - -:::evidence key="four-multitenancy-strategies-diagram" alt="database-per-tenant 와 schema-per-tenant 와 RLS 행 수준과 discriminator column 이 위에서 아래로 쌓여 있고 오른쪽에 격리가 얕아지는 방향 화살표가 있다" caption="격리 경계가 놓이는 층" zoom="false" -::: - -## 실패 모드가 전략마다 다르다 - -column은 predicate 누락이 곧 유출, RLS는 세 전제(§concept\:rls-three-preconditions), schema는 `search_path` 잔존, database는 커넥션 예산의 곱셈이다. - -## 네 전략이 각각 놓인 플래그 - -:::evidence key="four-multitenancy-strategies" alt="분석 문서 final/document.md#a05 에서 이 기록의 근거 절을 그대로 잘라낸 16줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a05 발췌 — 16줄" zoom="true" -::: - -## TenantId 정규식이 네 전략 전부의 기반인 이유 - -스키마 이름·`set_config` 값·라우팅 키에 들어가므로 `../public` 같은 값이 path-traversal이 된다. - -:::note - -네 전략 중 어느 것도 실제로 켜서 관측하지 않았다 — 전부 experimental 플래그 뒤에 있다 - -::: - -## 네 전략과 각각의 실패 모드 - -| 전략 | 격리 경계 | 실패 모드 | -|---|---|---| -| 판별 컬럼 | 애플리케이션 술어 | 술어 누락이 곧 유출 | -| 행 수준 보안 | 데이터베이스 정책 | 세 전제 중 하나만 어긋나도 무력화 | -| 테넌트별 스키마 | 검색 경로 | 세션 설정이 풀로 돌아간 커넥션에 잔존 | -| 테넌트별 데이터베이스 | 커넥션 자체 | 풀 예산의 곱셈 | - -경계가 위로 갈수록 애플리케이션에 가깝고 아래로 갈수록 인프라에 가깝다. 그리고 실패의 성질도 달라진다. 위쪽은 코드 한 줄을 빠뜨리면 유출이고, 아래쪽은 자원 계산을 틀리면 전체 장애다. - -## 공통 기반은 식별자의 형식이다 - -테넌트 식별자는 네 전략 전부에서 쓰인다. 그리고 쓰이는 자리가 위험하다. - -스키마 이름이 된다 -세션 설정 값이 된다 -라우팅 키가 된다 - -그래서 식별자 타입이 형식을 정규식으로 제약한다. 그 제약이 없으면 상위 경로를 가리키는 값 같은 것이 스키마 이름 자리에 들어가 경로 순회가 된다. - -:::warning - -식별자 형식 제약은 네 전략 중 어느 것을 쓰든 필요하다. 전략을 바꿔도 그 값이 들어가는 자리는 여전히 위험하다. - -::: - -## 실험 플래그 뒤에 있는 이유 - -네 전략 전부가 게이트 뒤에 있다. 클래스패스에 있다는 것이 실행 동의가 아니라는 원칙이다. - -테넌트 격리 기능이 jar 가 있다는 이유로 스스로 켜지면, 켜졌는지 모르는 상태에서 격리를 신뢰하게 된다. 게이트는 결정의 부재를 활성화가 아니라 오류로 만든다. - -## 이 저장소에서의 상태 - -네 전략 중 어느 것도 실제로 켜서 관측되지 않았다. 전부 실험 플래그 뒤에 있고, 이 사이클의 확인은 코드와 마이그레이션과 그 주석에 근거한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-a02-f001-lro.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-a02-f001-lro.md deleted file mode 100644 index e5fda8b..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-a02-f001-lro.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -kind: QUESTION -slug: a02-f001-lro -title: response/LRO invariant enforcement boundary -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: open-question:a02-f001-lro -questionStatus: OPEN -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# response/LRO invariant enforcement boundary - -응답 봉투 계열 타입의 유효한 형태가 팩토리 테스트에는 고정되어 있고 공개 정규 생성자에서는 강제되지 않는다. 생성자를 확장 표면으로 둘 것인지 결정이 필요하다. - -## 사실 - -봉투와 벌크 봉투와 연산과 페이지 메타의 유효한 형태가 팩토리 테스트에 고정되어 있다. - -공개 정규 생성자는 그 불변식을 강제하지 않는다. - -## 가정 - -정규 생성자가 외부에서 호출되지 않을 것이라고 암묵적으로 전제하고 있다. 그 전제가 확인되지 않았다. - -## 미지수 - -원시 생성자가 의도된 확장 표면인가. - -유효하지 않은 상태를 생성자에서 차단해야 하는가. - -## 제약 - -이 타입들은 공유 계약 계층에 있으므로 변경이 여러 어댑터에 영향을 준다. - -## 선택지 - -생성자에 불변식을 넣는다 -팩토리와 같은 규칙이 적용되고 유효하지 않은 값이 존재할 수 없다. 확장 표면이 좁아진다. - -확장 표면으로 유지하고 문서화한다 -허용 범위와 실패 의미를 계약에 적는다. 유효하지 않은 값이 만들어질 수 있다는 것을 받아들인다. - -## 다음 검증 - -프로덕션에서 원시 생성자를 호출하는 곳을 전수 확인한다. 그리고 유효하지 않은 형태의 생성자 테스트를 추가해 현재 허용 표면을 고정한다. - -원시 생성자가 외부 확장 계약이면 허용 범위와 실패 의미를 문서화한다. - -그렇지 않으면 생성자 수준 불변식을 추가하고 팩토리와 같은 규칙을 검증한다. - -## 관계 - -- **위험한 조합은 정책이 아니라 생성자가 거부하게 만든다** - 이 질문이 선택할 수 있는 한쪽 방향의 규칙이다. -- **모르는 것은 성공도 실패도 아닌 세 번째 결과여야 한다** - 같은 계약 계층의 상태 어휘 원칙이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-a02-f002-domaincontextkey.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-a02-f002-domaincontextkey.md deleted file mode 100644 index 6febcba..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-a02-f002-domaincontextkey.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -kind: QUESTION -slug: a02-f002-domaincontextkey -title: DomainContextKey same-name different-type collision -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: open-question:a02-f002-domaincontextkey -questionStatus: OPEN -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# DomainContextKey same-name different-type collision - -컨텍스트 키의 신원이 이름뿐인데 조회는 요청된 타입으로 캐스팅한다. 같은 이름의 다른 타입 키를 선언하는 것이 금지인지 허용인지 정해져 있지 않다. - -## 사실 - -키의 신원은 이름만으로 정해진다. - -조회는 요청된 타입으로 캐스팅한다. - -## 가정 - -같은 이름의 키가 하나뿐일 것이라고 전제하고 있다. 그 전제를 강제하는 것이 없다. - -## 미지수 - -같은 이름의 다른 타입 키 선언이 금지된 계약인가. - -허용이라면 캐스팅 실패의 의미가 무엇인가. - -## 제약 - -키 선언이 여러 모듈에 흩어져 있으면 생성 시점 충돌 탐지가 전역 레지스트리를 요구한다. - -## 선택지 - -생성이나 등록 단계에서 충돌을 거부한다 -같은 이름이 두 번 선언되면 실패한다. 전역 레지스트리가 필요하다. - -허용하고 실패 의미를 문서화한다 -캐스팅 실패가 언제 어떤 형태로 나타나는지 계약에 적는다. - -## 다음 검증 - -같은 이름에 다른 타입을 갖는 키 픽스처를 만들고 생성과 조회의 실패를 고정한다. 그다음 레지스트리의 실제 키 선언을 전수 대조한다. - -같은 이름 다른 타입이 금지라면 생성이나 등록 단계에서 충돌을 거부한다. - -허용이라면 캐스팅 실패 의미와 사용 조건을 계약에 명시한다. - -## 관계 - -- **이름은 값이 아니라 registry key다** - 이 질문이 다루는 이름의 성질이다. -- **위험한 조합은 정책이 아니라 생성자가 거부하게 만든다** - 한쪽 방향의 구현 규칙이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-a05-f003-capabilitysupport-constraints.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-a05-f003-capabilitysupport-constraints.md deleted file mode 100644 index 3b94e41..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-a05-f003-capabilitysupport-constraints.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -kind: QUESTION -slug: a05-f003-capabilitysupport-constraints -title: CapabilitySupport.constraints 의 경계가 타입에 없다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: open-question:a05-f003-capabilitysupport-constraints -questionStatus: OPEN -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# CapabilitySupport.constraints 의 경계가 타입에 없다 - -능력 선언의 제약 목록이 경계가 있다고 서술되지만 십만 자 문자열도 받아들인다. 그 목록은 액추에이터 리포트 모델에 포함된다. - -## 사실 - -십만 자 제약 문자열이 받아들여진다. - -능력 목록은 플랫폼 리포트를 통해 액추에이터 엔드포인트 모델에 포함된다. - -현재 출하 조립은 짧은 정적 리터럴만 만든다. - -## 가정 - -외부나 동적 생산자가 임의 제약을 넣지 않을 것이라고 전제하고 있다. 그 전제를 확인하지 않았다. - -## 미지수 - -외부나 포크나 동적 생산자가 공개 API 에 임의 제약을 넣는 실제 경로가 있는가. - -경계를 타입이 소유해야 하는가 리포트 투영이 소유해야 하는가. - -## 제약 - -현재 조립에서는 사고가 아니다. 공개 API 불변식의 공백이며, 포크나 동적 조립에서는 경계가 호출자 규율에 의존한다. - -## 선택지 - -타입 수준 최대 길이나 어휘를 강제한다 -공개 API 를 쓰는 모든 생산자에 적용된다. - -리포트 투영에서 경계를 건다 -공개되는 지점에서만 자른다. 값 자체는 자유롭다. - -## 다음 검증 - -공개 API 의 외부 생산자와 포크 확장점을 추적하고, 과대 제약에 대한 생성자와 리포트 경계 테스트를 추가한다. - -동적이나 외부 생산자가 확인되면 경계를 타입이나 리포트 투영에서 강제하고 이 항목을 Case 로 승격한다. - -없다면 공개 불변식과 문서를 현재 조립의 보장 수준으로 좁힌다. - -## 관계 - -- **진단 리포트가 살아 있는 리소스를 담지 않도록 값 타입을 좁혔다** - 같은 값 타입의 다른 제약이다. -- **관측을 위해 수집한 데이터가 관측 대상보다 위험할 수 있다** - 리포트로 공개되는 값의 성질이다. -- **카디널리티 경계를 타입으로 표현하기** - 경계를 타입에 두는 같은 계열의 개념이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-analysis-finding-a02-f003.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-analysis-finding-a02-f003.md deleted file mode 100644 index c4df19f..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-analysis-finding-a02-f003.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -kind: QUESTION -slug: analysis-finding-a02-f003 -title: bounded operational record identifiers -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: open-question:analysis-finding-a02-f003 -questionStatus: OPEN -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# bounded operational record identifiers - -운영 레코드의 네임스페이스와 키가 경계가 있다고 문서에 적혀 있는데, 생성자는 공백 여부만 확인한다. 그 경계를 공유 계약이 소유할지 어댑터가 소유할지 정해지지 않았다. - -## 사실 - -운영 레코드 문서가 네임스페이스와 키를 경계가 있는 값으로 설명한다. - -생성자는 공백 여부만 확인한다. - -## 가정 - -각 어댑터가 자기 제공자의 제한을 알아서 지킬 것이라고 전제하고 있다. 그 전제를 확인하지 않았다. - -## 미지수 - -제공자별 키 크기와 문자 집합 제한을 공유 계약이 소유해야 하는가 어댑터가 소유해야 하는가. - -여러 제공자가 공통으로 요구하는 최소 경계가 있는가. - -## 제약 - -공유 계약이 특정 제공자의 제한을 담으면 그 계약이 제공자에 묶인다. - -어댑터가 각자 검증하면 같은 검사가 여러 곳에 생긴다. - -## 선택지 - -공유 값 객체에서 공통 최소 경계를 강제한다 -여러 제공자가 공통 최소 경계를 요구하면 이쪽이 맞다. - -공유 문서를 좁히고 어댑터 경계에서 검증한다 -제한이 제공자별이면 공유 계약은 경계를 주장하지 않는 편이 정확하다. - -## 다음 검증 - -실제 제공자와 어댑터의 식별자 제한을 조사하고 프로덕션 생성 지점과 대조한다. 경계값 픽스처를 추가한다. - -여러 제공자가 공통 최소 경계를 요구하면 공유 값 객체에서 강제한다. - -제공자별이면 공유 문서의 표현을 좁히고 어댑터 경계에서 검증한다. - -## 관계 - -- **카디널리티 경계를 타입으로 표현하기** - 경계를 타입으로 표현하는 같은 계열의 개념이다. -- **위험한 조합은 정책이 아니라 생성자가 거부하게 만든다** - 값 타입에서 강제하는 방향의 규칙이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-analysis-finding-a02-f004.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-analysis-finding-a02-f004.md deleted file mode 100644 index 0502626..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-analysis-finding-a02-f004.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -kind: QUESTION -slug: analysis-finding-a02-f004 -title: permission component grammar -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: open-question:analysis-finding-a02-f004 -questionStatus: OPEN -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# permission component grammar - -권한 값은 콜론 구분 세그먼트 수와 공백과 정규화를 강제하지만 세그먼트의 문자 문법은 제한하지 않는다. 레지스트리가 더 좁은 문법을 실제 정본으로 쓰는지 확인되지 않았다. - -## 사실 - -권한 값 객체가 강제하는 것은 세 가지다. 콜론 세그먼트 수와 공백 여부와 정규화다. - -세그먼트의 문자 문법은 제한하지 않는다. - -## 가정 - -레지스트리의 권한 리터럴이 공유 값 객체가 허용하는 범위 안에 있을 것이라고 전제하고 있다. 반대 방향은 확인하지 않았다. - -## 미지수 - -레지스트리 정본이 공유 값 객체보다 좁은 문자 문법을 계약으로 요구하는가. - -## 제약 - -공유 값 객체를 좁히면 기존 권한 리터럴 중 일부가 무효가 될 수 있다. - -## 선택지 - -공유 값 객체가 레지스트리와 같은 문법을 강제한다 -레지스트리가 더 좁은 문법을 실제 정본으로 쓰면 이쪽이 맞다. 패리티 테스트로 두 쪽을 고정한다. - -현재의 넓은 문법이 의도임을 문서화한다 -레지스트리가 관행으로만 좁게 쓰는 것이면 이쪽이다. - -## 다음 검증 - -레지스트리의 모든 권한 리터럴을 수집해 허용 문자 집합을 만들고, 공유 파서와 패리티 테스트로 대조한다. - -레지스트리가 더 좁은 문법을 실제 정본으로 쓰면 공유 값 객체가 같은 문법을 강제한다. - -아니면 현재의 넓은 문법이 의도임을 문서화한다. - -## 관계 - -- **이름은 값이 아니라 registry key다** - 권한 값이 등록된 것인지 자유 문자열인지의 문제다. -- **문서와 상수가 서로 일치하는 것으로는 아무것도 증명되지 않는다** - 패리티 테스트가 필요한 이유다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-analysis-finding-a02-f005.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-analysis-finding-a02-f005.md deleted file mode 100644 index b96fbea..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-analysis-finding-a02-f005.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -kind: QUESTION -slug: analysis-finding-a02-f005 -title: messaging schema qualification boundary -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: open-question:analysis-finding-a02-f005 -questionStatus: OPEN -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# messaging schema qualification boundary - -현재 검사는 JDK 만으로 자원과 다이제스트와 선택된 의미 벡터를 검증한다. 실제 JSON 스키마 검증기와의 호환성은 그 검사만으로 주장할 수 없다. - -## 사실 - -현재 테스트는 JDK 만 쓴다. 정확한 자원과 다이제스트와 선택된 의미 벡터를 검증한다. - -그 테스트는 통과한다. - -## 가정 - -JDK 만으로 만든 의미 벡터가 실제 검증기의 해석과 같을 것이라고 전제하고 있다. 그 전제가 확인되지 않았다. - -## 미지수 - -채택할 검증기가 커밋된 스키마와 긍정 부정 벡터를 동일하게 해석하는가. - -## 제약 - -검증기를 테스트 클래스패스에 추가하면 이 리프의 의존이 늘어난다. 프레임워크 없는 계약 리프를 유지하려는 방침과 충돌할 수 있다. - -## 선택지 - -별도 소스셋이나 레인에서 실제 검증기로 실행한다 -리프의 기본 의존을 늘리지 않으면서 증거를 만든다. - -현재 상태를 유지하고 표현을 좁힌다 -JDK 만으로 확인한 것이 무엇인지 문서에 적고 호환성 주장을 하지 않는다. - -## 다음 검증 - -채택할 검증기로 커밋된 스키마와 긍정 부정 벡터를 실행하고 결과를 증거로 남긴다. - -실제 검증기가 동일한 의미 벡터를 통과해야 호환성을 주장한다. - -실패하면 스키마나 지원 범위를 수정하고 JDK 전용 게이트의 표현을 좁힌다. - -## 관계 - -- **문서와 상수가 서로 일치하는 것으로는 아무것도 증명되지 않는다** - 이 질문이 다루는 증거의 성질이다. -- **능력 등급은 코드가 아니라 실행된 증거에서 파생한다** - 호환성 주장에 증거가 필요한 이유다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-analysis-finding-a03-f002.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-analysis-finding-a03-f002.md deleted file mode 100644 index 9f21f1d..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-analysis-finding-a03-f002.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -kind: QUESTION -slug: analysis-finding-a03-f002 -title: notification derived idempotency key가 32-bit hash -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: open-question:analysis-finding-a03-f002 -questionStatus: OPEN -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# notification derived idempotency key가 32-bit hash - -멱등성 키가 주어지지 않았을 때 쓰는 대체 키가 32비트 해시다. 서로 다른 요청의 충돌을 배제할 수 없어 문서의 강한 유일성 표현과 실제 보장이 맞지 않는다. - -## 사실 - -대체 키는 표준 라이브러리 해시를 16진수 문자열로 만든 값이다. 32비트다. - -정규 지문 비교가 별도로 존재한다. - -## 가정 - -32비트 공간에서 실제 요청들이 충돌하지 않을 것이라고 전제하고 있다. 그 전제를 확인하지 않았다. - -## 미지수 - -서로 다른 정규 요청이 같은 대체 키를 만들 수 있는가. - -만들 수 있다면 하류가 잘못된 충돌로 끝나는가. - -이 위험을 허용할 것인가. - -## 제약 - -정규 지문 비교가 있으므로 조용한 수렴보다는 잘못된 충돌 쪽이 중심 위험이다. 즉 서로 다른 두 요청이 같은 것으로 합쳐지는 것이 아니라, 한쪽이 중복으로 거절될 수 있다. - -## 선택지 - -정규 계획에 대한 암호학적 다이제스트로 교체한다 -충돌 확률이 실질적으로 사라진다. 키 길이가 늘어난다. - -현재 값을 유지하고 문서를 좁힌다 -유일성 표현을 실제 보장 수준으로 낮춘다. - -## 다음 검증 - -알려진 해시 충돌 픽스처나 속성 탐색으로 서로 다른 정규 요청이 같은 대체 키를 만드는 사례를 찾고, 그때 하류의 충돌 동작을 고정한다. - -서로 다른 정규 요청의 충돌이 재현되면 암호학적 다이제스트로 교체한다. - -재현하지 못해도 유일성 문서 표현은 실제 보장 수준으로 좁힌다. - -## 관계 - -- **digest는 길이 프레이밍하고 버전을 붙인다** - 다이제스트로 교체할 때 따라야 할 규칙이다. -- **모르는 것은 성공도 실패도 아닌 세 번째 결과여야 한다** - 잘못된 충돌이 어떤 결과로 보고되는지의 문제다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-analysis-finding-a03-f004.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-analysis-finding-a03-f004.md deleted file mode 100644 index 8ecee75..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-analysis-finding-a03-f004.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -kind: QUESTION -slug: analysis-finding-a03-f004 -title: isolation vocabulary와 legacy routing capability의 시차 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: open-question:analysis-finding-a03-f004 -questionStatus: OPEN -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# isolation vocabulary와 legacy routing capability의 시차 - -공개 격리 수준 어휘에는 더 엄격한 값들이 있는데, 옛 트랜잭션 포트의 템플릿은 하나로 고정되어 있다. 어휘만 보면 이미 지원되는 능력으로 오해할 수 있다. - -## 사실 - -공개 격리 수준 타입은 더 엄격한 값들을 표현한다. - -옛 트랜잭션 포트의 템플릿은 읽기 커밋으로 고정되어 있다. - -테스트도 더 엄격한 라우팅을 계획됨으로 적는다. - -## 가정 - -어휘에 있는 값이 언젠가 라우팅될 것이라고 전제하고 있다. 그 시점과 소유자가 정해지지 않았다. - -## 미지수 - -더 엄격한 격리를 옛 포트까지 확장할 것인가. - -아니면 정규 정책 트랜잭션 경로에서만 제공할 것인가. - -## 제약 - -라우팅 소유자가 둘이면 같은 유스케이스 정책이 어느 경로로 들어오느냐에 따라 다른 격리를 받는다. - -## 선택지 - -옛 포트까지 확장한다 -기존 호출자가 그대로 더 엄격한 격리를 쓸 수 있다. 옛 포트의 표면이 넓어진다. - -정규 경로에서만 제공한다 -소유자가 하나가 된다. 옛 포트 호출자는 이전해야 한다. - -## 다음 검증 - -현재 프로덕션 유스케이스의 격리 요구와 옛 트랜잭션 포트 호출자를 대조한다. 그리고 라우팅 소유자를 하나로 정하는 설계와 계약 테스트를 만든다. - -선택한 소유자에서 유스케이스 정책과 어댑터 트랜잭션 정의가 일대일로 검증되어야 한다. - -지원하지 않는 경로는 능력으로 노출하지 않는다. - -## 관계 - -- **트랜잭션 템플릿은 모드별로 미리 만들어 둔다** - 템플릿이 고정되어 있는 구조다. -- **Bean 애너테이션이 있다는 것은 조립 증거가 아니다** - 어휘의 존재가 능력의 증거가 아니라는 같은 계열이다. -- **등급은 네 단계로 나누고 관측보다 높게 적지 않는다** - 능력을 실제보다 높게 노출하지 않는 규칙이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-analysis-finding-a04-f006.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-analysis-finding-a04-f006.md deleted file mode 100644 index b9c3667..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-analysis-finding-a04-f006.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -kind: QUESTION -slug: analysis-finding-a04-f006 -title: cache-redis 와 httpclient 의 support 간선이 죽었는지 확정되지 않았다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: open-question:analysis-finding-a04-f006 -questionStatus: OPEN -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# cache-redis 와 httpclient 의 support 간선이 죽었는지 확정되지 않았다 - -두 리프가 지원 모듈에 대한 Gradle 의존을 갖는데 현재 프로덕션 자바 참조가 0 이고 지원 자원도 쓰지 않는다. 죽은 간선인지 아직 확정할 수 없다. - -## 사실 - -두 리프 모두 지원 모듈에 대한 Gradle 의존이 선언되어 있다. - -현재 프로덕션 자바 참조는 0 이다. - -지원 모듈의 자원도 쓰지 않는다. - -## 가정 - -빌드나 테스트나 리플렉션 경로에서 그 의존이 필요하지 않을 것이라고 전제하고 있다. 확인하지 않았다. - -## 미지수 - -빌드나 테스트나 리플렉션이나 후속 범위에서 이 간선이 필요한가. - -## 제약 - -각 리프의 전수 분석이 끝나기 전에는 죽은 간선으로 단정할 수 없다. - -## 선택지 - -간선을 제거하고 레인을 돌린다 -불필요한 의존이 사라지고 클래스패스와 아키텍처 서술이 좁아진다. 소비자가 있으면 실패로 드러난다. - -유지하고 소유자를 문서화한다 -소비자가 발견되면 그 이유를 적는다. - -## 다음 검증 - -각 하류 리프의 전수 분석 결과를 확인한 뒤, 지원 의존을 제거한 상태로 집중 테스트와 애플리케이션 조립 테스트를 실행한다. - -프로덕션과 빌드와 테스트와 런타임 소비자가 0 이고 의존 제거 후 관련 레인이 통과하면 간선을 제거한다. - -소비자가 발견되면 그 소유자와 이유를 문서화한다. - -## 관계 - -- **Bean 애너테이션이 있다는 것은 조립 증거가 아니다** - 선언과 사용을 구별하는 같은 계열이다. -- **legacy compatibility surface의 제거 조건을 세 가지로 고정한다** - 제거 후보를 판정하는 같은 계열의 규칙이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-performance-and-capacity-unmeasured.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-performance-and-capacity-unmeasured.md deleted file mode 100644 index 4632d6f..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/question/openquestion-performance-and-capacity-unmeasured.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -kind: QUESTION -slug: performance-and-capacity-unmeasured -title: 실제 성능·용량 특성을 어떤 모듈에서도 측정하지 않았다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: open-question:performance-and-capacity-unmeasured -questionStatus: OPEN -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# 실제 성능·용량 특성을 어떤 모듈에서도 측정하지 않았다 - -이 분석은 성능을 측정하지 않았다. 문서에 남은 성능과 용량 관련 서술은 전부 코드가 선언한 상한과 그 강제 여부에 대한 것이다. - -## 사실 - -성능 레인은 릴리스 게이트에서 의도적으로 빠져 있다. 기계 의존적인 측정을 게이트 판정의 입력으로 삼지 않는다는 결정이다. - -풀 사이징 수식은 설정 파일 주석에 있고 부하 테스트로 조정하라고 적혀 있다. - -테넌트 풀 예산은 두 상한을 갖지만 그 값이 어떤 부하에서 적절한지에 대한 근거가 없다. - -httpclient 의 성능 레인과 벤치마크는 기계 의존적이라는 이유로 기본 검사에서 빠져 있다. - -GraphQL 리프의 등급표는 실부하와 장애를 미달성으로 적고, 증거가 없으면 릴리스 게이트가 거부한다고 적는다. - -## 가정 - -선언된 상한들이 합리적인 범위에 있다고 전제하고 있다. 그 전제의 근거는 문서에 적힌 시작점 수식이며 측정이 아니다. - -## 미지수 - -각 상한이 실제 부하에서 적절한가. - -테넌트 풀 예산의 두 상한이 어떤 테넌트 수와 부하에서 유효한가. - -성능 회귀가 발생하면 무엇이 그것을 알아채는가. - -## 제약 - -실부하 증거는 실제 부하 인프라를 요구한다. 리프 안에서 생성할 수 없다. - -기계 의존적 측정을 게이트에 넣지 않는다는 결정이 이미 있다. - -## 선택지 - -전용 성능 인프라를 만든다 -러너와 워밍업과 표본 수와 기록된 기준선이 필요하다. 그것이 생기면 별도 레인으로 만든다. - -측정하지 않은 상태를 명시적으로 유지한다 -등급표가 이미 그렇게 하고 있다. 릴리스 게이트가 증거 없이는 거부한다. - -## 다음 검증 - -성능 레인을 실행할 인프라를 정하고, 어떤 지표를 어떤 조건에서 잴지 먼저 문서로 고정한다. 그다음 기준선을 기록한다. - -기준선이 기록되면 이 질문을 닫고, 이후의 측정은 그 기준선과의 비교가 된다. - -인프라를 만들지 않기로 하면 그 결정과 그때 포기하는 것을 기록한다. - -## 관계 - -- **성능 측정은 릴리스 게이트에 넣지 않는다** - 이 질문이 전제하는 결정이다. -- **tenant별 풀이 개별적으로 합리적이고 그 합이 서버 상한을 넘긴다** - 측정되지 않은 용량 판단의 사례다. -- **능력 등급은 코드가 아니라 실행된 증거에서 파생한다** - 실부하 증거가 최상위 등급의 조건인 이유다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/reference/reference-analysis-finding-a03-f003.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/reference/reference-analysis-finding-a03-f003.md deleted file mode 100644 index fb90868..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/reference/reference-analysis-finding-a03-f003.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -kind: REFERENCE -slug: analysis-finding-a03-f003 -title: legacy compatibility surface의 제거 조건을 세 가지로 고정한다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:analysis-finding-a03-f003 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# legacy compatibility surface의 제거 조건을 세 가지로 고정한다 - -## 목적 - -이름이 deprecated 이거나 legacy 라는 이유로 아직 배선되어 있는 호환 표면을 지워, 마이그레이션이 끝나기 전에 경로가 끊기는 것을 막는다. - -## 규칙 - -1. 이름은 삭제 가능성의 증거가 아니다 - deprecated 표시는 방향을 말하지 그 시점을 말하지 않는다. - -2. 세 조건이 함께 확인되어야 제거 후보가 된다 - 외부 프로덕션 참조 0, 대체 경로의 특성화, 설정 경로 제거다. - -3. 외부 참조 0 만으로는 부족하다 - 대체 경로가 같은 동작을 증명하지 못하면 지우는 순간 동작이 바뀐다. - -4. 설정 경로가 남아 있으면 운영자가 여전히 그것을 켤 수 있다 - 코드에서 지워도 설정이 남아 있으면 기동 오류가 된다. - -5. 실제 제거는 별도 결정 증거를 요구한다 - 이 규칙은 후보를 고르는 기준이지 제거 자체를 승인하지 않는다. - -## 적용 조건 - -deprecated 나 legacy 계약이 아직 프로덕션 어댑터나 런타임 배선에 연결된 마이그레이션 구간 - -## 예외 - -외부 호환 계약을 의도적으로 유지하는 경우와 대체 경로가 아직 동일 동작을 증명하지 못한 경우에는 제거하지 않는다. - -## 예시 - -deprecated 계약이 프로덕션 배선에 여전히 사용된다. 이름만 보고 죽은 코드로 판단하면 어댑터와 런타임 마이그레이션이 끝나기 전에 경로가 끊긴다. - -## 관계 - -- **notification admin atomic claim contract가 service에서 사용되지 않음** - 같은 리프에서 옛 경로가 남아 있는 사례다. -- **같은 개념의 두 어휘가 공존하면 하나를 죽은 것으로 표시한다** - 같은 계열의 짝 규칙이다. -- **중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다** - 제거 전에 확인해야 할 것을 다룬 규칙이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/reference/reference-hibernate-filter-is-not-a-security-boundary.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/reference/reference-hibernate-filter-is-not-a-security-boundary.md deleted file mode 100644 index 7abec6e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/reference/reference-hibernate-filter-is-not-a-security-boundary.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -kind: REFERENCE -slug: hibernate-filter-is-not-a-security-boundary -title: Hibernate filter는 보안 경계가 아니다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:hibernate-filter-is-not-a-security-boundary -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# Hibernate filter는 보안 경계가 아니다 - -## 목적 - -ORM 의 필터 기능을 테넌트 격리 경계로 삼아, 그 기능이 적용되지 않는 경로로 다른 테넌트의 행에 도달하는 것을 막는다. - -## 규칙 - -1. 필터는 엔티티 쿼리에만 적용된다 - 네이티브 SQL 과 벌크 DML 과 참조 획득과 2차 캐시를 통한 도달에는 적용되지 않는다. - -2. 기준은 우회 경로의 개수다 - 이 격리를 우회하는 경로가 몇 개인가를 묻는다. ORM 기능의 답은 항상 0 이 아니다. - -3. 실제 경계는 아래층이어야 한다 - 데이터베이스의 행 수준 보안이나 별도 가드가 경계이고 필터는 편의로만 쓴다. - -4. 편의로 쓴다는 것을 적는다 - 필터가 있으면 그것이 경계라고 읽힌다. 아니라는 것이 코드나 문서에 있어야 한다. - -## 적용 조건 - -테넌트와 소유자와 가시성 격리 전반 - -ORM 을 통해 데이터에 접근하는 모든 경로 - -## 예외 - -읽기 경로만 있고 네이티브 쿼리와 벌크 연산이 구조적으로 금지된 좁은 컨텍스트면 필터로 충분할 수 있다. 다만 그 금지를 아키텍처 테스트 같은 것이 강제해야 하고, 강제가 없으면 전제가 유지되지 않는다. - -## 예시 - -행 수준 보안이 실제 경계로 쓰이고, 그 전제 셋을 시작 검증기가 확인한다. - -테넌트 인식 저장소 가드가 별도 경계로 존재한다. - -## 관계 - -- **RLS가 성립하기 위한 세 전제** - 실제 경계로 쓰이는 메커니즘이다. -- **RLS가 아무것도 하지 않는 세 가지 방법** - 그 경계가 무력화되는 경로다. -- **tenant 컬럼이 있는 테이블의 모든 unique 제약에 그 컬럼이 들어가야 한다** - 컬럼 기반 격리를 쓸 때의 짝 규칙이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/reference/reference-isolation-settings-must-be-transaction-local.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/reference/reference-isolation-settings-must-be-transaction-local.md deleted file mode 100644 index 4d6e005..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/reference/reference-isolation-settings-must-be-transaction-local.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -kind: REFERENCE -slug: isolation-settings-must-be-transaction-local -title: 격리 설정은 트랜잭션 로컬이어야 한다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:isolation-settings-must-be-transaction-local -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# 격리 설정은 트랜잭션 로컬이어야 한다 - -## 목적 - -테넌트 바인딩이나 검색 경로처럼 격리를 결정하는 세션 설정이 풀로 돌아간 커넥션에 남아, 다음 차용자가 그것을 상속하는 것을 막는다. - -## 규칙 - -1. 격리를 결정하는 설정은 트랜잭션과 함께 되돌아간다 - 세션 스코프면 커넥션이 살아 있는 동안 유지된다. - -2. 데이터베이스가 제공하는 트랜잭션 로컬 옵션을 쓴다 - PostgreSQL 에서는 설정 함수의 세 번째 인자가 그것을 보장한다. - -3. 대응물이 없으면 반환 시 명시적으로 되돌린다 - 중립 값으로 재설정하는 것이 대안이다. - -4. 사용 전 덮어쓰기는 값이 항상 설정될 때만 안전하다 - 한 경로라도 설정 없이 커넥션을 쓰면 앞 사용자의 값이 적용된다. - -5. 커넥션이 테넌트에 고정 할당되면 이 문제가 사라진다 - 다만 그때는 풀 예산이 새 문제가 된다. - -## 적용 조건 - -행 수준 보안의 테넌트 바인딩 - -테넌트별 스키마 라우팅 - -세션 상태로 표현되는 모든 격리 - -## 예외 - -테넌트별 데이터베이스처럼 커넥션 자체가 격리 경계인 구성. 그 경우 세션 스코프가 문제가 되지 않는다. - -## 예시 - -검색 경로가 세션 설정이라 풀로 돌아간 커넥션이 마지막 테넌트의 스키마를 들고 있다. 다음 차용자는 어떤 문장도 틀리지 않은 채 거기서 읽고 쓴다. - -로컬 타임아웃 설정은 매 트랜잭션 전에 다시 적용하는 방식으로 실무상 가려진다. 그 방식은 값이 항상 설정되는 경우에만 안전하다. - -## 관계 - -- **search_path가 풀로 돌아간 커넥션에 남아 다음 tenant가 상속한다** - 이 규칙을 만든 사례다. -- **세션 스코프 설정은 풀로 돌아간 커넥션에 남는다** - 같은 성질의 일반형이다. -- **RLS가 성립하기 위한 세 전제** - 테넌트 바인딩이 이 규칙을 따라야 하는 이유다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/reference/reference-tenant-column-belongs-in-every-unique-constraint.md b/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/reference/reference-tenant-column-belongs-in-every-unique-constraint.md deleted file mode 100644 index bf25c7c..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/reference/reference-tenant-column-belongs-in-every-unique-constraint.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -kind: REFERENCE -slug: tenant-column-belongs-in-every-unique-constraint -title: tenant 컬럼이 있는 테이블의 모든 unique 제약에 그 컬럼이 들어가야 한다 -topic: multitenancy-isolation -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:tenant-column-belongs-in-every-unique-constraint -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# tenant 컬럼이 있는 테이블의 모든 unique 제약에 그 컬럼이 들어가야 한다 - -## 목적 - -테넌트 컬럼이 빠진 유일 제약이 한 테넌트의 삽입을 다른 테넌트의 데이터 때문에 실패시켜, 버그와 정보 유출을 동시에 만드는 것을 막는다. - -## 규칙 - -1. 유일성 요구에 테넌트 컬럼을 포함한다 - 격리가 컬럼에 의존하면 그 테이블의 모든 유일성 요구가 그 컬럼을 포함해야 한다. - -2. 빠뜨리면 두 가지가 동시에 일어난다 - 정상적인 삽입이 실패하고, 그 실패가 다른 테넌트에 그 값이 존재한다는 사실을 알린다. - -3. 부분 유일 인덱스와 배제 제약에도 같은 논리가 적용된다 - 조건부 유일성도 유일성이다. - -4. 전역 유일이 의도라면 그것을 적는다 - 외부 시스템의 식별자처럼 전역적으로 유일해야 하는 값은 테넌트를 포함하지 않는 것이 맞다. 그때는 그 값이 테넌트 간에 노출되는 것이 의도임을 적어야 한다. - -## 적용 조건 - -판별 컬럼 전략을 쓰는 모든 테이블 - -테넌트 컬럼이 있는 모든 인덱스와 제약 - -## 예외 - -전역 유일이 요구사항인 값. 그 사실과 노출 범위를 함께 적는다. - -## 예시 - -값만으로 걸린 유일 인덱스는 다른 테넌트가 그 값을 이미 썼다는 이유로 한 테넌트의 삽입을 실패시킨다. 존재하지 않아야 할 행의 존재를 알려 주는 것이다. - -## 관계 - -- **RLS가 아무것도 하지 않는 세 가지 방법** - 같은 마이그레이션이 함께 다룬 축이다. -- **Hibernate filter는 보안 경계가 아니다** - 컬럼 기반 격리의 다른 면이다. -- **위험한 조합은 정책이 아니라 생성자가 거부하게 만든다** - 제약으로 강제하는 같은 계열의 원칙이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/non-atomic-check-then-act/case/case-a-rotation-that-overwrites-and-a-generation-that-cannot-be-reclaimed.md b/docs/clean-architecture-backend-template/tech-log-studio/non-atomic-check-then-act/case/case-a-rotation-that-overwrites-and-a-generation-that-cannot-be-reclaimed.md deleted file mode 100644 index b82f0f4..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/non-atomic-check-then-act/case/case-a-rotation-that-overwrites-and-a-generation-that-cannot-be-reclaimed.md +++ /dev/null @@ -1,143 +0,0 @@ ---- -kind: CASE -slug: a-rotation-that-overwrites-and-a-generation-that-cannot-be-reclaimed -title: 회전이 비교 후 교체가 아니라 덮어쓰기이고, 세대 계수기는 음수가 되면 회수되지 않는다 -topic: non-atomic-check-then-act -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a-rotation-that-overwrites-and-a-generation-that-cannot-be-reclaimed -evidenceCapturedOn: 2026-09-02 -assets: - - key: a-rotation-that-overwrites-and-a-generation-that-cannot-be-reclaimed - file: ../../../final/evidence/rendered/a-rotation-that-overwrites-and-a-generation-that-cannot-be-reclaimed.svg -evidence: - - ../../../final/evidence/raw/a-rotation-that-overwrites-and-a-generation-that-cannot-be-reclaimed.txt -source: - - 분석 문서는 grpc-client 편 §17.1 이 회전의 덮어쓰기를, §17.2 가 비원자적 감소를, §17.3 이 배수 목록 순회를 다룬다. 권고는 각각 비교 후 교체, 하한을 포함한 원자 갱신, 복사 후 쓰기 배열이다. 세대가 실제 채널을 들고 있지 않다는 것은 같은 문서 §16 이 확인하지 못한 것으로 적어 둔 자리다. - - 가족을 건넌 재현은 플랫폼 편 §7.4 와 교차 범위 편 §6 이 다룬다. ---- - -# 회전이 비교 후 교체가 아니라 덮어쓰기이고, 세대 계수기는 음수가 되면 회수되지 않는다 - -채널 런타임 레지스트리의 회전이 읽고 판단한 뒤 조건 없이 쓴다. 같은 모듈의 세대 계수기는 확인하고 별도로 감소시켜, 음수가 되면 그 세대를 영영 회수할 수 없다. - -## 관계 - -- **원자 타입 위의 검사 후 실행과 비교 후 교체 루프** - 이 사례가 속한 구조다. -- **같은 자격 증명 회전 결함이 한 가족에서 닫히고 다른 가족에서 재현됐다** - 같은 자료구조 오용이 옆 모듈에도 있는데, 그 모듈의 사례는 다른 가족이 이미 닫은 형태의 재현이다. -- **배수 완료가 자기가 읽은 값으로 상태를 다시 써서 진행 중인 자격증명 회전을 되돌린다** - 옆 모듈의 그 결과를 다루는 기록이다. - -## 문제 - -채널 런타임은 세대를 갖는다. 설정이 바뀌면 새 세대를 만들고, 옛 세대는 진행 중인 호출이 끝날 때까지 배수 목록에 남아 있다가 회수된다. - -준비하고 교체하고 배수하는 순서를 지키는 연산이 셋이다. 교체와 계수와 회수다. - -## 결론 - -앞의 둘이 같은 형태의 결함을 갖는다. 읽기와 쓰기가 원자 타입 위에서 두 연산으로 갈라진다. 셋째는 다른 곳에서 어긋난다. 동기화 목록을 잠그지 않고 순회한다. - -교체는 현재 런타임을 읽어 판단한 뒤 조건 없이 쓴다. 두 회전이 겹치면 나중 것이 먼저 것을 덮고, 덮인 쪽은 배수 목록에 오르지 못한 채 사라진다. 그 세대 위에서 시작된 호출은 아무도 세지 않는다. 같은 클래스의 첫 설치는 비교 후 교체를 쓴다. - -계수기 감소는 확인 후 실행이다. 값이 0보다 큰지 확인하고 별도 연산으로 감소시킨다. 증가와 감소가 짝을 유지하는 한 이 값은 0 아래로 내려가지 않는다. 짝 없는 감소가 섞이면 두 스레드가 같은 값을 읽고 둘 다 감소시킬 수 있다. - -두 번째 오용은 스스로 회복되지 않는다. 조용함을 판정하는 술어가 계수기를 0과 비교하고, 배수가 시작된 런타임은 증가를 거절하므로, 0 아래로 내려간 값은 그 자리에 머문다. - -같은 자료구조 오용이 옆 모듈의 자격증명 회전 관리자에도 있다. 그 관리자의 사례는 다른 가족이 계약 테스트로 이미 닫은 형태의 재현이다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 읽기와 쓰기가 별개 연산이라는 코드 형태 확인과 호출자 계수 -소스 수정 : x - -## 재현 조건 - -1. 레지스트리의 회전 메서드에서 읽기와 쓰기를 찾고, 그 사이에 비교 후 교체가 있는지 확인한다. -2. 같은 클래스의 설치 메서드가 무엇을 쓰는지 대조하고, 배수 목록에 들어가는 값이 무엇인지 확인한다. -3. 세대가 들고 있는 필드를 나열하고, 이 모듈이 grpc 타입을 참조하는지 센다. -4. 계수기의 확인과 감소, 증가 경로의 배수 거절, 조용함 판정의 비교 대상을 확인한다. -5. 그 메서드들의 프로덕션 호출자를 센다. -6. 회수와 배수 목록 반환이 어떤 목록을 어떻게 순회하는지 확인한다. -7. 옆 모듈의 배수 완료 메서드 전문을 읽고 그 파일의 원자 갱신을 센다. - -## 본문 - - - -설정이 바뀌면 레지스트리가 새 세대를 만들어 포인터를 옮기고 옛 세대를 배수 목록에 넣는다. 준비하고 교체하고 배수하는 순서다. - -그 순서를 지키는 세 연산이 각각 다른 곳에서 어긋난다. 앞의 둘은 원자 타입 위에서 읽고 판단한 뒤 따로 쓴다. 셋째는 다른 형태다 — 동기화 목록을 잠그지 않고 순회한다. - -## 같은 클래스가 같은 참조를 두 가지로 다룬다 - -:::evidence key="a-rotation-that-overwrites-and-a-generation-that-cannot-be-reclaimed" alt="코드베이스에서 회전 메서드가 참조를 읽고 판단한 뒤 조건 없이 쓰는 구간과 같은 클래스의 첫 설치가 쓰는 비교 후 교체, 그 파일의 비교 후 교체 매치 수와 배수 목록에 들어가는 값, 세대가 들고 있는 네 필드와 이 모듈의 grpc 참조 수, 계수기를 확인 후 감소시키는 두 메서드와 그 파일의 원자 갱신 매치 수와 배수 중에 증가를 거절하는 분기와 조용함 판정, 그 메서드의 프로덕션 호출자 수, 동기화 목록을 잠그지 않고 순회하는 두 메서드와 회수의 프로덕션 호출자 수, 옆 모듈의 배수 완료 전문과 그 파일의 원자 갱신 매치 수, 그리고 두 모듈의 런타임 소속을 뽑은 출력 77줄. 세대가 채널을 들고 있지 않고 두 모듈 어디에도 원자 갱신이 없다는 것이 그 출력에 보인다." caption="회전은 덮어쓰기 · 설치는 비교 후 교체 · 세대의 네 필드와 grpc 참조 0 · 계수기의 확인 후 감소 · 잠금 밖 순회 둘 · 옆 모듈의 판단 없는 쓰기 — 77줄" zoom="true" -::: - -회전은 프로파일의 참조를 꺼내고, 현재 런타임을 지역 변수에 담고, 새 세대가 그것을 이어받는지 확인한 뒤, 조건 없이 쓴다. - -같은 클래스의 첫 설치는 다르다. 널에서 새 런타임으로 비교 후 교체하고, 실패하면 이미 설치되어 있다는 뜻으로 다룬다. 이 파일의 비교 후 교체는 그 한 번뿐이다. - -읽기와 쓰기 사이에 다른 회전이 끼면 나중 쓰기가 먼저 쓰기를 덮는다. 배수 목록에 들어가는 것은 이전 값 하나이므로, 덮인 대체본은 어디에도 등록되지 않는다. 그 세대는 배수도 회수도 되지 않고, 그 위에서 시작된 호출은 아무도 세지 않는다. - -## 그 세대가 들고 있는 것은 채널이 아니다 - -세대의 필드는 넷이다. 세대 기술자와 계수기 둘과 배수 플래그다. - -이 모듈의 프로덕션 코드에는 grpc 타입 참조가 0 이다. 닫아야 할 채널 객체가 여기에는 없다. 잃는 것은 자원이 아니라 추적이다. - -## 계수기는 원자 타입 위에서 원자적이지 않다 - -두 종료 메서드가 같은 순서로 같은 일을 한다. 값이 0보다 큰지 읽어 보고, 그다음 별도 연산으로 감소시킨다. 이 파일에는 그 둘을 한 연산으로 묶는 호출이 없다. - -그 확인은 하한이지만 원자적인 하한이 아니다. 감소가 증가와 짝을 이루는 동안에는 값이 음수가 되지 않는다. 두 스레드가 동시에 종료 안에 있으면 계수기는 최소 2 이기 때문이다. - -짝 없는 감소가 하나라도 섞이면 달라진다. 같은 호출을 두 번 끝내거나, 시작이 거절된 뒤에도 종료를 부르면, 두 스레드가 같은 값을 읽고 둘 다 감소시킬 수 있다. - -이 저장소에는 그 호출자가 아직 없다. 감소가 그 보호를 호출자에게 맡기고 있다는 것이 형태의 결함이다. - -## 내려간 값은 그 자리에 머문다 - -조용함을 판정하는 술어가 두 계수기를 각각 0 과 비교한다. - -계수기를 올리는 두 경로는 배수가 시작된 런타임의 작업을 거절한다. 회전이 이전 세대의 배수를 시작하고 목록에 넣으므로, 배수 목록에 오른 런타임은 새 작업을 받지 않고 따라서 값이 다시 올라가지 않는다. - -0 아래로 내려간 값은 그래서 돌아오지 않는다. 그 세대는 회수 대상이 되지 못한 채 목록에 남는다. - -## 회수는 다른 곳에서 어긋난다 - -배수 목록은 동기화 래퍼로 감싼 리스트다. 그 래퍼는 개별 연산만 동기화하고, 순회는 호출자가 목록을 잠그고 해야 한다는 것이 그 API 의 계약이다. - -회수 메서드는 잠그지 않고 스트림으로 거르고, 그 결과를 목록에서 뺀다. 배수 목록을 밖으로 내주는 메서드도 잠그지 않고 복사한다. 복사도 순회다. - -어느 쪽이든 회전이 동시에 항목을 더하면 순회 중 변경이 된다. - -읽고 지우는 두 단계가 원자적이지 않은 것도 같지만, 이쪽은 무해하다. 그 사이에 조용해진 세대는 다음 호출에서 회수된다. - -## 옆 모듈은 판단조차 없다 - -자격증명 회전 관리자가 상태를 하나의 참조로 들고 있다. 회전은 그것을 읽어 담고 승계를 판정한 뒤 조건 없이 쓴다. - -배수 완료는 판정이 없다. 읽은 값의 현재 세대로 새 상태를 만들어 그대로 쓴다. 그래서 읽기와 쓰기 사이에 회전이 끼면 그 회전이 활성화한 세대가 지워지고 이전 세대가 다시 현재가 된다. 방금 교체하기 전의 자격 재료가 다시 현재 값이 된다. - -그 파일에는 비교 후 교체도, 원자 갱신도, 동기화 블록도 0 이다. - -이 형태는 처음 나온 것이 아니다. 다른 가족이 같은 회전 결함을 동시성 계약 테스트로 닫고 그 이력을 남겼는데, 이쪽 가족의 회전 관리자가 그것을 반복한다. - -## 배선되지 않은 두 모듈 - -두 모듈 모두 모듈 레지스트리의 런타임 소속이 비어 있다. 계수기를 움직이는 메서드도, 회수를 부르는 메서드도 프로덕션 호출자가 0 이다. - -남는 것은 형태다. 이 클래스는 같은 참조를 한 곳에서는 비교 후 교체로 다루고 다른 곳에서는 덮어쓴다. - -## 확인하지 못한 것 - -경합 자체를 돌려서 만들어 보지는 않았다. 두 모듈 모두 런타임 소속이 비어 있고, 이 연산들을 부르는 프로덕션 코드도 없다. - -경합을 실행으로 재현하지 않았다. 판정은 읽기와 쓰기가 별개 연산이라는 코드 형태와 조용함 술어의 비교 대상에 근거한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/non-atomic-check-then-act/case/case-a05-f026-enqueue.md b/docs/clean-architecture-backend-template/tech-log-studio/non-atomic-check-then-act/case/case-a05-f026-enqueue.md deleted file mode 100644 index 916e4ee..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/non-atomic-check-then-act/case/case-a05-f026-enqueue.md +++ /dev/null @@ -1,201 +0,0 @@ ---- -kind: CASE -slug: a05-f026-enqueue -title: upsert 라고 적힌 연산이 갱신하고 안 되면 삽입한다 -topic: non-atomic-check-then-act -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a05-f026-enqueue -evidenceCapturedOn: 2026-09-02 -assets: - - key: a05-f026-enqueue - file: ../../../final/evidence/rendered/a05-f026-enqueue.svg - - key: a05-f026-enqueue-race - file: ../../../final/evidence/rendered/a05-f026-enqueue-race.svg -evidence: - - ../../../final/evidence/raw/a05-f026-enqueue.txt - - ../../../final/evidence/raw/a05-f026-enqueue-race.txt -source: - - 원본 분석 절은 `final/document.md#a05` §82 다. 등급은 P2 이고, 부분 유일 인덱스가 진 트랜잭션을 정상 upsert 로 흡수하지 않는다는 판정과 그 실행 탐침이 그 절에 있다. - - 그 절의 권고는 둘이다. 부분 유일 술어와 같은 의미를 갖도록 native upsert 를 구성하거나, 유일 충돌을 잡아 제한된 갱신 재시도로 수렴시키는 것이다. 회귀는 장벽을 둔 두 트랜잭션의 최초 넣기에서 둘 다 성공하고 열린 행이 하나임을 고정해야 한다. - - 저장이 병합으로 가는 이유와 예외가 번역되는 지점, 두 호출자의 도달 조건은 이 기록에서 확인했다. ---- - -# upsert 라고 적힌 연산이 갱신하고 안 되면 삽입한다 - -복구 큐의 넣기를 javadoc 이 upsert 로 정의한다. 구현은 갱신을 먼저 시도하고 0 이면 저장한다. 열린 항목이 아직 없는 상태에서 두 호출자가 같은 순간에 들어오면 뒤에 삽입한 쪽이 부분 유일 인덱스에 걸린다. - -## 관계 - -- **CAS 튜플을 where 절에 전부 반복하고 update count를 답으로 쓴다** - 갱신된 행 수를 답으로 쓰라는 규칙이고, 이 사례는 그 답을 읽지 않는다. -- **조건부 update로 행을 claim하고 읽은 값으로 판단하지 않는다** - 읽고 나서 쓰는 형태를 피하는 규칙이다. -- **Atomic* 타입의 존재는 원자성의 증거가 아니다** - 타입 이름이 아니라 어디까지가 한 번에 일어나는지를 확인해야 한다. - -## 문제 - -복구 큐는 조정이 해결하지 못한 파일을 담는다. 큐의 javadoc 은 넣기를 upsert 로 정의하고, 덧붙이기만 하는 큐는 미해결 파일 하나당 순회마다 한 행씩 늘어 서로 다른 문제를 같은 문제의 반복 아래 묻는다고 적는다. - -갱신 쪽 의미도 저장소 javadoc 에 적혀 있다. 이기는 쪽은 최신 이유다. 가장 최근 증거를 서술하는 것이 최신 이유이기 때문이다. - -## 결론 - -구현은 갱신을 먼저 시도하고 0 이면 저장한다. 파일당 열린 항목이 하나여야 한다는 것은 데이터베이스가 강제한다. 상태가 대기 중인 행에 대한 부분 유일 인덱스다. - -열린 항목이 없는 상태에서 둘이 같은 순간에 들어오면 둘 다 갱신에서 0 을 받고 둘 다 삽입으로 간다. 실제 PostgreSQL 16 에서 여섯 번 돌렸다. 여섯 번 다 한쪽이 삽입에 성공하고 다른 한쪽이 유일 제약 위반을 받으며, 남는 행은 하나다. 어느 쪽이 지는지는 회차마다 바뀐다. - -호출자가 실제로 받는 것은 드라이버 예외가 아니다. 넣기를 감싼 경계가 SQLSTATE 23505 를 데이터베이스 유일 위반으로 번역해 던진다. - -던져지는 시점도 넣기 안이 아니다. 버전 필드가 없고 식별자가 바깥에서 채워지므로 저장이 병합으로 넘어가고, 그러면 삽입이 플러시 시점까지 밀린다. 그래서 삽입이 실제로 나가는 것은 그것을 감싼 쓰기 경계가 커밋할 때다. - -데이터베이스 불변식은 지켜진다. 인덱스는 중복 행을 막는다. 경합에서 진 요청은 수렴 없이 끝난다. 진 쪽이 들고 온 이유 코드도 롤백과 함께 사라지므로, 저장소 javadoc 이 말한 최신 이유가 이긴다는 것도 이 창에서는 성립하지 않는다. - -마무리 경로에는 문제가 하나 더 있다. 커밋에서 실패하면 그 아래 예외 줄까지 닿지 못한다. 상위 경로가 그 예외 여부로 취소를 판단하기 때문에, 게시가 끝난 업로드까지 취소 쪽으로 흘러간다. - -겹치는 조건은 좁다. 조정이 집어 올리는 것은 갱신 시각이 5분 이상 지난 레코드다. 조정 진입점에는 아직 호출자가 붙어 있지 않다. 지금 실제로 도달 가능한 넣기 지점은 마무리 경로 하나이고, 조정이 켜지면 둘이 된다. - -수정 방향은 둘이다. native upsert 를 부분 유일 술어와 같은 의미로 구성하는 쪽, 아니면 유일 충돌을 잡아 제한된 갱신 재시도로 수렴시키는 쪽이다. 회귀 시험은 장벽을 둔 두 트랜잭션이 처음 넣을 때 둘 다 성공하고 열린 행은 하나로 남는지를 고정해야 한다. - -## 검증 환경 - -OpenJDK : 21.0.12 -데이터베이스 : PostgreSQL 16.15, 실제 실행 -확인 방식 : 넣기 경로와 예외 번역 추적, 실제 PostgreSQL 에서 두 연결을 장벽으로 맞춰 동시에 실행 -소스 수정 : x - -## 재현 조건 - -1. 큐와 저장소의 javadoc 을 나란히 읽는다. -2. 넣기 구현과 그것이 부르는 갱신 질의를 읽는다. -3. 엔티티에 버전 필드와 Persistable 구현이 있는지 센다. -4. 그 테이블의 부분 유일 인덱스를 확인한다. -5. 넣기를 감싼 경계가 예외를 어떻게 번역하는지 읽는다. -6. PostgreSQL 을 띄우고 postgresql 과 jpa/core 와 jpa/fileserver 마이그레이션을 순서대로 적용한다. -7. 열린 항목이 없는 상태에서 두 연결을 장벽으로 맞춰 풀고, 결과와 남은 행 수를 여러 회차 본다. -8. 넣기를 부르는 프로덕션 코드와 각각의 도달 조건을 확인한다. - -## 본문 - - - -복구 큐의 javadoc 은 넣기를 upsert 로 정의한다. - -```text -An enqueue is an upsert: the same file reported twice updates the open item rather than adding -a second one. Reconciliation runs on a schedule and re-raises whatever it still cannot settle, so -an append-only queue would grow one row per sweep per unresolved file and bury the distinct -problems under repetitions of the same one. -``` - -저장소의 javadoc 은 갱신 쪽 의미도 적는다. 최신 이유가 이기고, 그것이 가장 최근 증거를 서술하기 때문이다. - -## javadoc 이 약속한 것과 구현이 하는 것 - -:::evidence key="a05-f026-enqueue" alt="복구 큐와 저장소 인터페이스의 javadoc, 넣기 메서드의 구현과 엔티티의 버전 필드 및 Persistable 구현 수, 그 테이블의 부분 유일 인덱스, 넣기를 감싼 경계가 예외를 번역하는 코드와 SQLSTATE 매핑, 넣기를 부르는 프로덕션 코드 둘과 마무리 경로의 감싼 구간, 조정 진입점의 프로덕션 호출자 수와 조정이 집어 올리는 대상의 조건, 그리고 같은 저장소가 승패를 데이터베이스에 물어보는 자리를 출력한 터미널 기록." caption="두 javadoc 의 약속 · 갱신 뒤 0 이면 저장 · 엔티티에 @Version 0, Persistable 0 · 부분 유일 인덱스 · 23505 를 DB_UNIQUE_VIOLATION 으로 번역 · 조정 진입점의 프로덕션 호출자 0 · 조정은 5분 지난 것만 집음 — 69줄 · exit 0" zoom="true" -::: - -```java -public void enqueue(FileId fileId, String reasonCode) { - Instant now = clock.instant(); - if (items.refreshPending(fileId.value(), reasonCode, now) > 0) { - return; - } - items.save( - new RecoveryItemEntity(UUID.randomUUID(), fileId.value(), reasonCode, STATUS_PENDING, now)); -} -``` - -데이터베이스는 파일당 열린 항목을 하나로 강제한다. - -```sql -CREATE UNIQUE INDEX uq_fs_recovery_open - ON fs_recovery_item (file_id) - WHERE status = 'PENDING'; -``` - -## 열린 항목이 없을 때 둘이 같은 순간에 들어오면 - -:::evidence key="a05-f026-enqueue-race" alt="실제 PostgreSQL 컨테이너를 세우고 postgresql 과 jpa/core 와 jpa/fileserver 마이그레이션을 순서대로 적용한 뒤, 열린 항목이 없는 상태에서 두 연결을 장벽으로 맞춰 동시에 풀어 갱신과 삽입 순서를 실행하는 것을 여섯 번 반복하고 회차마다 이긴 쪽과 진 쪽의 결과와 남은 행 수를 출력한 터미널 기록." caption="PostgreSQL 16.15 에 세 마이그레이션 적용 · 장벽으로 맞춘 두 연결을 6회 반복 · 매번 한쪽만 삽입 성공, 다른 한쪽은 uq_fs_recovery_open 위반, 남은 행은 하나 · 이기는 쪽은 회차마다 바뀜 — 10줄 · exit 0" zoom="true" -::: - -```text -1 회차: 이긴 쪽 B · 진 쪽 duplicate key value violates unique constraint "uq_fs_recovery_open" · 남은 행 1 -3 회차: 이긴 쪽 A · 진 쪽 duplicate key value violates unique constraint "uq_fs_recovery_open" · 남은 행 1 -``` - -여섯 번 다 결과의 모양은 같다. 둘 다 갱신에서 0 을 받은 뒤 삽입으로 가고, 뒤에 온 쪽이 인덱스에 걸리고, 남는 행은 하나다. 어느 쪽이 뒤가 되는지는 고정이 아니다. 여기의 A 와 B 는 같은 순서를 도는 두 연결이지 아래에 나오는 두 프로덕션 호출자가 아니다. - -## 호출자가 실제로 받는 것 - -탐침은 삽입을 직접 쓴다. 실제 경로는 `items.save(...)` 다. 엔티티에 버전 필드가 없고 식별자가 밖에서 채워져 들어오므로 Spring Data 의 저장은 병합으로 가고, 병합은 할당된 식별자의 삽입을 플러시까지 미룬다. 그래서 삽입이 나가는 시점은 넣기 안이 아니라 그것을 감싼 쓰기 경계가 커밋할 때다. - -예외도 그대로 올라오지 않는다. - -```java -private T executeLegacy(TransactionTemplate template, Supplier action) { - try { - return template.execute(status -> action.get()); - } catch (RuntimeException failure) { - throw exceptionTranslator - .translate(failure) - .map(RuntimeException.class::cast) - .orElse(failure); - } -} -``` - -SQLSTATE 23505 는 데이터베이스 유일 위반으로 번역된다. 위 기록은 순서 자체가 재현되는지를 본 것이고, 예외의 타입은 실제 경로 쪽을 따라야 한다. - -## 깨지는 쪽은 어댑터 계약이다 - -인덱스는 중복 행을 막을 뿐이다. 진 쪽 요청은 upsert 로 수렴하지 않고 그대로 실패한다. - -진 쪽이 들고 온 이유 코드도 롤백과 함께 사라진다. 큐에 남는 이유는 이긴 쪽 것 하나이고, 저장소 javadoc 이 말한 최신 이유가 이긴다는 것도 이 창에서는 성립하지 않는다. - -마무리 경로에서는 이것과 다른 문제가 하나 더 나온다. - -```java -} catch (RuntimeException exception) { - transactions.inWrite( - () -> { - recoveryQueue.enqueue(verifying.fileId(), "READY_COMMIT_UNCONFIRMED"); - }); - throw new AmbiguousCompletionException( -``` - -넣기를 감싼 경계가 커밋에서 실패하면 그다음 줄의 예외는 던져지지 않는다. 그 예외인지 아닌지로 취소 여부를 가르는 상위 경로가 있으므로, 바이트가 이미 게시된 업로드가 취소 분기로 넘어간다. - -## 프로덕션 호출자는 둘, 겹치는 조건은 좁다 - -```text -DefaultFinalizeUploadService.java:323 recoveryQueue.enqueue(verifying.fileId(), "READY_COMMIT_UNCONFIRMED"); -DefaultFileReconciliationService.java:220 recoveryQueue.enqueue(record.fileId(), reasonCode); -``` - -조정은 갱신 시각이 5분 이상 지난 레코드만 집어 올린다. 같은 파일에서 두 호출이 만나려면 검증 중에 들어간 지 5분이 지난 마무리가 그때 커밋에 실패해야 한다. - -그리고 이 리비전에서 조정 진입점을 부르는 프로덕션 코드는 없다. 빈 정의와 시험뿐이고, javadoc 이 말하는 일정 순회는 배선되어 있지 않다. 지금 도달 가능한 넣기 지점은 마무리 경로 하나이고, 조정이 켜지는 순간 이 자리는 두 개가 된다. - -## 같은 저장소가 승패를 데이터베이스에 물어보는 자리 - -인박스 예약과 관리 작업 기록이 충돌 시 아무것도 하지 않는 삽입을 쓰고, 바뀐 행 수를 답으로 쓴다. - -```text -JdbcInboxRepository.java:36 ON CONFLICT (message_id, consumer_id) DO NOTHING -JdbcAdminOperationJournal.java:51 ON CONFLICT (approval_ticket, plan_digest) DO NOTHING -``` - -다만 그 형태를 이 큐에 그대로 옮길 수는 없다. 넣기의 계약은 두 번째 보고를 버리는 것이 아니라 열린 항목의 이유를 최신으로 바꾸고 시도를 올리는 것이므로, 필요한 것은 아무것도 하지 않는 삽입이 아니라 갱신하는 삽입이다. 유일 인덱스가 부분 인덱스라 충돌 대상에도 그 조건을 같이 적어야 한다. - -여기 두 자리가 보여 주는 것은 답 자체가 아니라, 이 저장소가 승패를 데이터베이스에게 물어보는 형태를 이미 쓸 줄 안다는 사실이다. 알림 쪽 결과 타입의 javadoc 에는 그 형태를 고른 이유도 함께 적혀 있다. 이긴 호출자에게는 행이 돌아오고 진 호출자에게는 돌아오지 않는다. - -## 확인하지 못한 것 - -조정 쪽은 아직 프로덕션 호출자가 없어 두 지점이 겹치는 일은 현재 배선에서 일어나지 않는다. 확인한 것은 겹칠 때의 결과다. - -병합이 미루는 삽입이 실제 플러시 시점에 같은 경쟁을 만드는지는 탐침으로 재현하지 않았다. 탐침은 INSERT 를 손수 날린다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/non-atomic-check-then-act/case/case-an-admission-boundary-that-leaks-and-a-counter-that-cannot-return.md b/docs/clean-architecture-backend-template/tech-log-studio/non-atomic-check-then-act/case/case-an-admission-boundary-that-leaks-and-a-counter-that-cannot-return.md deleted file mode 100644 index 6d194de..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/non-atomic-check-then-act/case/case-an-admission-boundary-that-leaks-and-a-counter-that-cannot-return.md +++ /dev/null @@ -1,150 +0,0 @@ ---- -kind: CASE -slug: an-admission-boundary-that-leaks-and-a-counter-that-cannot-return -title: queued 를 줄이는 유일한 연산이 상한을 읽지 않는 승격 안에 있다 -topic: non-atomic-check-then-act -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:an-admission-boundary-that-leaks-and-a-counter-that-cannot-return -evidenceCapturedOn: 2026-09-04 -body: case-an-admission-boundary-that-leaks-and-a-counter-that-cannot-return.body.md -assets: - - key: an-admission-boundary-that-leaks-and-a-counter-that-cannot-return - file: ../../../final/evidence/rendered/an-admission-boundary-that-leaks-and-a-counter-that-cannot-return.svg -evidence: - - ../../../final/evidence/raw/an-admission-boundary-that-leaks-and-a-counter-that-cannot-return.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-server §17.4 · final/document.md#a20-grpc-policy §17.1 이다. ---- - -# queued 를 줄이는 유일한 연산이 상한을 읽지 않는 승격 안에 있다 - -`GrpcAdmissionController` 안에서 `queued` 를 낮추는 연산은 `promoteFromQueue:77` 뿐이고, 같은 메서드가 `:78` 에서 `inFlight` 를 올리면서 `maxConcurrentCalls` 를 읽지 않는다. 상한 1 짜리 제어기에 이 메서드를 다섯 번 부르면 `queued` 는 0 이 되고 `inFlight` 는 6 이 된다. - -## 관계 - -- **원자 타입 위의 검사 후 실행과 비교 후 교체 루프** - 이 사례가 속한 구조다. `tryAdmit` 과 `promoteFromQueue` 와 `release` 가 모두 검사 후 실행이고 이 파일에 `compareAndSet` 이 없다. -- **Atomic* 타입의 존재는 원자성의 증거가 아니다** - `inFlight` 와 `queued` 가 `AtomicInteger` 인데도 세 메서드의 검사와 갱신 사이가 끊긴다. 같은 파일에서 `rejected.incrementAndGet()` (`:62`) 만이 연산 하나로 끝난다. -- **승격이 상한 필드를 읽지 않고, 세 승인 메서드의 main 호출자가 0 이다** - 같은 클래스를 다룬 앞선 기록이다. 그쪽은 승격이 상한을 안 읽는다는 것과 호출자 부재를 확인했고, 여기서는 그 두 사실이 합쳐지면 `queued` 에 되돌림 수단이 남지 않는다는 것을 확인했다. - -## 문제 - -승인 제어기는 동시 처리 수를 제한하고, 상한에 닿으면 큐에 넣고, 자리가 나면 큐에 있던 호출을 실행으로 올린다. - -세 계수기가 그 구조를 떠받친다. 둘은 상한과 견주는 값이고 하나는 거부 횟수다. 두 상한 계수기의 증가와 감소가 짝을 이루는지 확인했다. - -## 결론 - -tryAdmit 은 :52 와 :57 에서 값을 읽고 :54 와 :59 에서 따로 올린다. release 는 :84 에서 읽고 :85 에서 내린다. 검사와 갱신이 각각 두 연산이다. - -inFlight 를 바꾸는 자리는 저장소에 세 클래스로 흩어져 있고 그중 내리는 자리가 넷이다. queued 라는 이름의 값을 바꾸는 자리는 두 클래스뿐이다. - -GrpcAdmissionController 안에서 queued 를 내리는 연산은 :77 하나다. 그것은 promoteFromQueue 의 몸통이고, 같은 메서드가 :78 에서 inFlight 를 올린다. :75~:79 어디에도 maxConcurrentCalls 를 읽는 줄이 없다. - -그래서 이 연산은 대기 값을 되돌리는 데 쓸 수 없다. 프로브에서 상한 1 짜리 제어기에 다섯 번 부르니 진행 중이 6 이 되었다. - -SemaphoreAdmissionController:90 은 같은 저장소에서 큐 계수기를 finally 로 돌려놓고, GrpcStreamAdmission:56~:64 는 해제 한 번에 두 계수기를 모두 내리며, GracefulShutdownCoordinator:70 은 하한을 updateAndGet 한 연산에 담는다. 세 자리 모두 이 클래스가 하지 않은 것을 한다. - -tryAdmit 이 돌려주는 Decision 의 두 숫자는 서로 다른 시점의 값이다. :55 는 자기 증가 뒤에 다시 읽은 값이고 :60 은 :52 에서 읽어 둔 running 이다. 거부 분기의 :67~:71 도 그 두 값을 문자열로 이어 붙여 돌려준다. - -세 메서드를 부르는 줄은 src/grpc 아래 아홉인데 main 소스 세트에는 0 이다. GrpcPlatformAutoConfiguration:56 이 빈을 만들지만 :30 의 조건 애너테이션이 요구하는 프로퍼티를 아무 yml 도 켜지 않는다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Gradle : 9.0.0 -확인 방식 : 메서드 서명에서 창을 잡아 세 메서드 본문 전문 인용, 같은 창 안에서 계수기 접근을 읽기와 쓰기로 분류, AtomicInteger 변경 연산 열두 가지로 저장소 전역 검색하되 이 클래스의 알려진 자리를 못 찾으면 중단, 같은 저장소의 다른 세 반납 방식 인용, 호출자 계수와 대조 타입, 빈 생성 조건과 그것을 켜는 설정 파일 수, 단일 스레드 프로브 세 건 -소스 수정 : x - -## 재현 조건 - -1. 세 메서드의 시작 줄을 서명에서 찾고 닫는 괄호까지를 창으로 잡는다. -2. 그 창 안에서 계수기를 읽는 호출과 바꾸는 호출을 나눠 적는다. -3. 원자 정수형이 값을 바꾸는 메서드 이름을 빠짐없이 넣어 전역 검색을 건다. 이 클래스의 :77 과 :85 가 결과에 없으면 검색식이 깨진 것이므로 거기서 멈춘다. -4. promoteFromQueue 본문에 상한 필드가 나오는지 본다. -5. 같은 저장소에서 큐 계수기를 반납하는 다른 방식을 찾아 나란히 놓는다. -6. 상한 1 짜리 제어기를 만들어 큐에 다섯을 넣고 승격을 다섯 번 불러 두 값을 읽는다. -7. 세 메서드의 호출 줄을 소스 세트별로 세고, 빈을 만드는 조건과 그것을 켜는 설정 파일을 센다. - -## 본문 - - - -`GrpcAdmissionController` 는 `AtomicInteger` 필드 셋을 가진다. `inFlight` 와 `queued` 는 상한과 견주는 값이고 `rejected` 는 거부 횟수다(`:17`\~`:19`). 앞의 둘만 승인 판단에 들어간다. - -## 세 메서드가 값을 읽는 자리와 바꾸는 자리 - -:::evidence key="an-admission-boundary-that-leaks-and-a-counter-that-cannot-return" alt="저장소 루트에서 돌린 정적 검색과 단일 스레드 프로브의 출력 167줄. 먼저 전체 자바 파일 6444 개와 src/grpc 아래 249 개를 세고, GrpcAdmissionController 의 AtomicInteger 세 선언과 세 메서드의 서명 줄을 찍는다. 이어서 tryAdmit 51~72 번 줄, promoteFromQueue 75~80 번 줄, release 83~87 번 줄을 서명에서 잡은 창으로 전문 인용하고, 각 메서드 안에서 계수기에 닿는 호출을 줄 번호와 함께 나열한다. tryAdmit 은 일곱 자리이고 promoteFromQueue 는 세 자리이며 release 는 두 자리다. 이 파일의 compareAndSet 은 0 개다. 다음으로 AtomicInteger 변경 연산 열두 가지로 저장소 전역을 훑은 결과가 나온다. inFlight 는 adapter/inbound/web 과 grpc/grpc-server 와 messaging/messaging-transport-spi 세 모듈에서 여덟 자리가 나오고 그중 내리는 자리가 넷이다. queued 는 adapter/inbound/web 의 SemaphoreAdmissionController 와 grpc/grpc-server 의 GrpcAdmissionController 두 자리씩 네 줄이다. 그 아래에 SemaphoreAdmissionController 84~92 번 줄의 finally 반납과 GracefulShutdownCoordinator 62~71 번 줄의 자바독과 updateAndGet 하한, GrpcStreamAdmission 55~64 번 줄의 두 계수기 동시 감소가 차례로 나온다. 세 메서드를 부르는 줄은 아홉인데 전부 GrpcServerProfileTest 이고 main 소스 세트는 0 이다. 타입 이름을 참조하는 main 파일은 GrpcDrainCoordinator 와 GrpcPlatformAutoConfiguration 이고 대조 타입 GrpcExecutorProfile 은 3 이다. 빈을 만드는 GrpcPlatformAutoConfiguration 30 번 줄이 ConditionalOnProperty 이고 그 프로퍼티를 켜는 yml 은 0 개다. 마지막으로 프로브 세 건이 나온다. 상한 1 에 큐 5 를 채우고 승격을 다섯 번 부르면 queued 는 0 이 되고 inFlight 는 6 이 된다. 상한 2 에서 큐로 들어온 호출자가 release 를 부르면 inFlight 가 1 로 내려가 네 번째 호출이 승인되고 실제로 도는 호출이 셋이 된다. 상한 2 큐 3 에서 실호출 둘이 끝나도 queued 는 3 으로 남고, 진행 중이 상한에 닿는 순간 tryAdmit 이 at capacity 로 거부한다." caption="계수기 선언과 세 메서드 전문 · 메서드별 읽기와 쓰기 자리 · Atomic 변경 연산 전부로 훑은 저장소 전역 · 같은 저장소의 세 가지 반납 방식 · 호출자 0/9 와 빈 생성 조건 · 단일 스레드 프로브 세 건 — 167줄 · exit 0" zoom="true" -::: - -`tryAdmit:52` 가 `inFlight` 를 지역 변수 `running` 에 담고, `:53` 이 그것을 `maxConcurrentCalls` 와 견주고, `:54` 가 `incrementAndGet` 을 부른다. 큐 분기도 `:57`\~`:59` 에서 같은 세 걸음을 밟는다. `release:84` 가 `inFlight.get() > 0` 을 확인하고 `:85` 가 내린다. 검사와 갱신이 각각 별개의 호출이다. - -한 연산으로 끝나는 것은 `:62` 의 `rejected.incrementAndGet()` 하나다. 그 값은 상한과 견주지 않는다. - -## queued 를 내리는 연산은 승격의 몸통이다 - -`AtomicInteger` 의 변경 연산 이름 열두 가지로 저장소를 훑었다. 이 클래스의 알려진 자리가 결과에 없으면 스크립트가 멈추도록 걸어 두었다. - -`inFlight` 라는 이름의 값은 세 모듈에서 여덟 자리가 바뀌고, 그중 내리는 것은 넷이다 — `GrpcAdmissionController:85`, `BlockingBridgeBudget:65`, `GracefulShutdownCoordinator:56` 과 `:70`. 뒤 셋은 다른 클래스가 자기 필드로 선언한 값이다. - -`queued` 라는 이름의 값은 두 모듈에서 네 자리가 바뀐다. 이 클래스 안에서 내리는 자리는 `:77` 하나다. - -그 `:77` 은 `promoteFromQueue` 의 몸통이고 바로 다음 줄 `:78` 이 `inFlight.incrementAndGet()` 이다. `:75`\~`:79` 를 다 읽어도 `maxConcurrentCalls` 가 나오지 않는다. 대기 값을 내리는 동작과 동시 처리 값을 올리는 동작이 한 메서드에 묶여 있고, 그 메서드는 올려도 되는지 묻지 않는다. - -## 되돌림으로 쓰면 상한이 무너진다 - -상한 1, 큐 5 로 제어기를 만들고 호출 하나를 승인시킨 뒤 다섯을 큐에 넣었다. `queued` 를 0 으로 돌리려고 `promoteFromQueue` 를 다섯 번 불렀더니 `inFlight` 가 6 이 되었다. 상한은 1 이다. - -반대로 큐에 들어간 호출자가 가진 유일한 반납 메서드는 `release` 인데, 그것은 `inFlight` 를 내린다. 상한 2 에서 실호출 둘과 큐 하나를 만든 뒤 큐 쪽이 `release` 를 부르니 `inFlight` 가 1 이 되었고, 다음 `tryAdmit` 이 승인을 내주어 실제로 도는 호출이 셋이 되었다. - -승격도 해제도 부르지 않으면 값이 그대로 남는다. 상한 2, 큐 3 에서 실호출 둘이 끝난 뒤 `queued` 는 3 이었다. 진행 중이 다시 상한에 닿자 `tryAdmit` 이 `at capacity: 2 in flight and 3 queued` 를 돌려주었다. 대기자가 없는데 큐가 가득 찬 것으로 읽힌다. - -## 같은 저장소에 세 가지 다른 반납이 있다 - -`SemaphoreAdmissionController:74` 가 큐 깊이를 올리고 `:90` 이 `finally` 안에서 내린다. 거부든 인터럽트든 정상 획득이든 같은 자리를 지난다. - -`GrpcStreamAdmission:56`\~`:64` 는 해제 한 번에 호출자별 계수와 전체 계수를 모두 내린다. 승인 쪽 `:44`\~`:51` 은 이 클래스와 똑같이 검사 후 실행인데 해제만 대칭이다. - -`GracefulShutdownCoordinator:70` 은 `updateAndGet(current -> current > 0 ? current - 1 : current)` 로 하한을 한 연산에 담는다. `:65`\~`:67` 의 자바독은 그렇게 바꾼 이유를 적어 두었다 — 이중 해제가 값을 음수로 만들었고, 음수가 된 진행 중 계수는 아직 일이 돌고 있는데도 배수가 끝났다고 보고했다. - -## 세 메서드를 부르는 main 코드가 없다 - -`src/grpc` 아래에서 세 메서드를 부르는 줄은 아홉이고 전부 시험 파일이다. main 소스 세트에서는 0 이다. - -빈 자체는 `GrpcPlatformAutoConfiguration:56` 이 만든다. 다만 `:30` 의 `@ConditionalOnProperty` 가 걸려 있고 그 프로퍼티를 켜는 yml 이 저장소에 0 개라, 오늘은 빈도 만들어지지 않는다. - -타입 이름을 읽는 main 파일은 `GrpcDrainCoordinator` 와 `GrpcPlatformAutoConfiguration` 둘이다. 같은 방식으로 센 `GrpcExecutorProfile` 이 3 을 내므로 이 검색은 main 을 보고 있다. - -## Decision 이 싣는 두 숫자는 시점이 다르다 - -`:55` 는 자기 `incrementAndGet` 뒤에 `inFlight.get()` 을 다시 읽어 담는다. `:60` 은 `:52` 에서 읽어 둔 `running` 을 그대로 담고 `queued` 만 새로 읽는다. 한 record 의 두 필드가 서로 다른 시점의 값이다. - -`:67`\~`:71` 은 그 두 값을 클라이언트에게 돌려줄 문자열에 굽는다. - -## 원문과 갈리는 자리 - -원문은 큐 계수기를 되돌리는 경로가 없다고 적었다. 이 기록은 그 판정을 유지한다. 값을 내리는 줄은 있지만 그 줄이 상한을 읽지 않는 승격 안에 있어서, 되돌림으로 쓰면 동시 처리 상한이 깨진다. - -원문은 세 메서드가 읽은 뒤 따로 쓴다고 적었고 프로덕션 호출자가 0 이라고 적었다. 둘 다 이 리비전에서 그대로다. 여기에 빈을 만드는 조건까지 꺼져 있다는 것이 더해진다. - -## 등급에 대해 - -원본은 P2 다. 근거는 오늘 호출자가 없다는 것이다. 그 근거가 이 리비전에서 더 강해졌으므로 등급을 새로 매기지 않는다. - -## 확인하지 못한 것 - -경합으로 상한이 무너지는 장면은 관측하지 않았다. 프로브 셋은 호출 순서만 바꾼 단일 스레드다. - -`release` 가 음수를 만드는 것도 보지 않았다. `:84` 의 가드가 원자적이지 않다는 것까지다. - -승격을 부를 책임이 어느 계층에 있어야 하는지는 판단하지 않았다. 클래스 자바독도 그 답을 적어 두지 않았다. - -## 등급에 대해 - -원본은 오늘 호출자가 없다는 것을 근거로 P2 를 매겼다. 이 리비전에서도 호출자는 0 이고 빈을 만드는 조건까지 꺼져 있으므로 P2 를 그대로 둔다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/non-atomic-check-then-act/case/case-draining-began-and-a-service-came-back-serving.md b/docs/clean-architecture-backend-template/tech-log-studio/non-atomic-check-then-act/case/case-draining-began-and-a-service-came-back-serving.md deleted file mode 100644 index 88cdbb1..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/non-atomic-check-then-act/case/case-draining-began-and-a-service-came-back-serving.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -kind: CASE -slug: draining-began-and-a-service-came-back-serving -title: 배수를 시작한 뒤에도 한 서비스가 다시 SERVING 이 될 수 있다 -topic: non-atomic-check-then-act -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:draining-began-and-a-service-came-back-serving -evidenceCapturedOn: 2026-09-01 -assets: - - key: draining-began-and-a-service-came-back-serving - file: ../../../final/evidence/rendered/draining-began-and-a-service-came-back-serving.svg -evidence: - - ../../../final/evidence/raw/draining-began-and-a-service-came-back-serving.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-admin §17.4 이다. ---- - -# 배수를 시작한 뒤에도 한 서비스가 다시 SERVING 이 될 수 있다 - -배수 시작이 플래그를 쓰고 상태 맵을 바꾸는 두 단계다. 그 사이에 끼어든 상태 갱신이 한 서비스를 서빙으로 남기고, 전역 상태 재계산이 그것을 따라 전체를 서빙으로 되돌린다. - -## 관계 - -- **원자 타입 위의 검사 후 실행과 비교 후 교체 루프** - 이 사례가 속한 구조다. -- **전이를 시작하는 쓰기와 그 전이가 덮는 집합은 한 연산이어야 한다** - 이 사례가 만든 규칙이다. -- **새 승인을 거절한다는 메서드가 단계만 기록하고 아무것도 거절하지 않는다** - 같은 리프에서 배수를 무력하게 만드는 다른 절반이다. - -## 문제 - -헬스 레지스트리는 서비스별 상태와 전역 상태를 갖는다. 로드밸런서가 읽는 것은 전역 상태다. - -배수를 시작하면 모든 서비스가 배수 중으로 바뀌고 전역 상태도 그렇게 된다. 그래야 로드밸런서가 새 연결을 보내지 않는다. - -## 결론 - -배수 시작이 두 단계다. - -먼저 배수 플래그를 참으로 쓴다. 그다음 상태 맵의 모든 값을 배수 중으로 바꾼다. - -서비스 상태를 서빙으로 바꾸는 메서드는 첫 줄에서 그 플래그를 확인하고 참이면 즉시 돌아간다. 배수 중에는 서빙으로 되돌릴 수 없게 하려는 가드다. - -그 가드를 통과한 스레드가 두 단계 사이에 쓰면 결과가 뒤집힌다. 그 서비스만 서빙으로 남고, 전역 상태 재계산이 서빙인 서비스가 하나라도 있으면 전역을 서빙으로 만든다. - -배수 중인 인스턴스가 로드밸런서에 준비됐다고 답하는 상태이고, 배수의 목적이 정확히 그것을 막는 것이다. - -같은 리프의 새 승인 거절이 단계만 기록하고 아무것도 거절하지 않는 것과 겹치면, 배수라는 절차 전체가 상태 기록으로만 존재하고 트래픽에 대해서는 효과가 없다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Gradle : 9.0.0 -확인 방식 : 배수 시작 메서드의 두 단계 순서와 상태 갱신 메서드의 가드 위치 대조 -소스 수정 : x - -## 재현 조건 - -1. 배수 시작 메서드에서 플래그 쓰기와 상태 맵 갱신의 순서를 확인한다. -2. 서빙 상태로 바꾸는 메서드의 첫 줄 가드를 확인한다. -3. 전역 상태 재계산이 서비스 상태를 어떻게 집계하는지 확인한다. - -## 본문 - - - -`beginDraining()` 이 두 단계다 — 먼저 `draining = true` 를 쓰고, 그다음 `states.replaceAll(...)` 로 모든 서비스를 DRAINING 으로 바꾼다. `markServing` 은 첫 줄에서 `if (draining) return;` 으로 자기를 막는다. - -## beginDraining() 의 두 단계 - -:::evidence key="draining-began-and-a-service-came-back-serving" alt="분석 문서 final/document.md#a20-grpc-admin 에서 이 기록의 근거 절을 그대로 잘라낸 18줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-admin 발췌 — 18줄" zoom="true" -::: - -## 두 단계 사이가 열려 있다 - -그 가드를 통과한 스레드가 두 단계 사이에 쓰면, 그 서비스만 SERVING 으로 남고 `recomputeGlobal()` 이 전체 상태를 SERVING 으로 되돌린다. 배수 중인 인스턴스가 로드밸런서에 준비됐다고 답하는 상태이고, 배수의 목적이 정확히 그것을 막는 것이다. - -## 같은 리프의 짝 - -`rejectNewAdmission()` 이 단계만 기록하고 아무것도 거절하지 않는다. - -## 확인하지 못한 것 - -경합을 실행으로 재현하지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/non-atomic-check-then-act/concept/concept-check-then-act-on-atomic-types.md b/docs/clean-architecture-backend-template/tech-log-studio/non-atomic-check-then-act/concept/concept-check-then-act-on-atomic-types.md deleted file mode 100644 index b77598b..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/non-atomic-check-then-act/concept/concept-check-then-act-on-atomic-types.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -kind: CONCEPT -slug: check-then-act-on-atomic-types -title: 원자 타입 위의 검사 후 실행과 비교 후 교체 루프 -topic: non-atomic-check-then-act -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:check-then-act-on-atomic-types -evidenceCapturedOn: 2026-09-01 -assets: - - key: check-then-act-on-atomic-types - file: ../../../final/evidence/rendered/check-then-act-on-atomic-types.svg - - key: check-then-act-on-atomic-types-diagram - file: ../../../final/assets/diagrams/check-then-act-on-atomic-types.svg -evidence: - - ../../../final/evidence/raw/check-then-act-on-atomic-types.txt -source: - - 원본 분석 절은 final/document.md#a99 §3.5 · final/document.md#a20-grpc-policy §17.1, §17.2 이다. ---- - -# 원자 타입 위의 검사 후 실행과 비교 후 교체 루프 - -이 저장소가 같은 동시성 문제를 세 가지 형태로 푼다. 비교 후 교체 루프, 락, 그리고 검사 후 실행. 셋째는 원자 타입을 쓰면서 원자성을 얻지 못하는 형태다. - -## 관계 - -- **`Atomic*` 타입의 존재는 원자성의 증거가 아니다** - 이 개념에서 나온 규칙이다. -- **전이를 시작하는 쓰기와 그 전이가 덮는 집합은 한 연산이어야 한다** - 이 개념의 상태 전이 판이다. -- **같은 자격 증명 회전 결함이 한 가족에서 닫히고 다른 가족에서 재현됐다** - 이 형태가 가족을 건너 재현된 사례다. - -## 본문 - - - -이 저장소가 같은 문제를 세 가지로 푼다. - -## 같은 문제의 세 형태 - -:::evidence key="check-then-act-on-atomic-types-diagram" alt="같은 갱신 문제에서 비교 후 교체 루프와 synchronized 와 검사 후 실행 세 갈래가 나오고 마지막만 빗금이다" caption="같은 문제의 세 형태" zoom="false" -::: - -(1) 비교 후 교체 루프 — `GrpcRetryBudget.tryConsume` 이 `get()` 으로 현재 값을 읽고 `compareAndSet` 이 실패하면 다시 읽는다. (2) `synchronized` — `GrpcDemandController` 가 같은 형태를 락으로 닫는다. (3) 검사 후 실행 — `get()` 으로 조건을 확인하고 별도 연산으로 `set`·`incrementAndGet`·`put` 한다. - -## GrpcRetryBudget 참조 위치 - -:::evidence key="check-then-act-on-atomic-types" alt="코드베이스에서 GrpcRetryBudget 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcRetryBudget 코드베이스 검색 — 3줄 · exit 0" zoom="true" -::: - -## 셋째가 원자성을 얻지 못한다 - -두 스레드가 같은 조건을 통과한 뒤 각자 쓴다. `AtomicReference` 에서는 나중 쓰기가 먼저 쓰기를 덮고, `AtomicInteger` 에서는 경계가 초과되며, `ConcurrentMap` 에서는 `get` 뒤의 `put` 이 다른 스레드의 갱신을 지운다. - -## 정본이 같은 저장소에 있다 - -저자들이 올바른 형태를 알고 있었고, 열 곳 남짓에서 쓰지 않았다. - -:::note - -어느 사례도 경합을 실행으로 재현하지 않았다. 전부 읽기와 쓰기가 별개 연산이라는 코드 형태로 판정했다 - -::: - -## 세 가지 형태 - -첫째는 비교 후 교체 루프다. 재시도 예산이 그 형태로 쓰였다. 현재 값을 읽고, 새 값을 계산하고, 읽은 값이 그대로일 때만 교체한다. 교체가 실패하면 다시 읽는다. - -둘째는 락이다. 수요 제어기가 같은 구조를 동기화 블록으로 닫는다. 읽기와 쓰기 사이에 다른 스레드가 끼지 못한다. - -셋째가 문제의 형태다. 원자 타입의 읽기 메서드로 조건을 확인하고, 조건이 맞으면 별도 연산으로 쓴다. 두 연산 각각은 원자적이고 그 사이는 아니다. - -## 왜 타입이 안전을 주지 않는가 - -원자 타입이 보장하는 것은 개별 연산의 원자성이다. 읽기 하나와 쓰기 하나가 각각 찢어지지 않는다는 뜻이지, 그 둘을 묶은 판단이 유효하다는 뜻이 아니다. - -두 스레드가 같은 조건을 통과한 뒤 각자 쓰면 결과는 자료구조마다 다르게 나타난다. - -참조 타입에서는 나중 쓰기가 먼저 쓰기를 덮는다. 방금 교체된 값이 사라진다. - -정수 타입에서는 경계가 초과된다. 상한을 확인하고 증가시키는 형태에서 두 스레드가 같은 값을 읽으면 둘 다 통과한다. - -동시성 맵에서는 읽은 뒤의 쓰기가 다른 스레드의 갱신을 지운다. 조건부 갱신 메서드가 있는데 쓰지 않은 경우다. - -## 정본이 같은 저장소에 있다 - -이 개념의 핵심은 저자들이 올바른 형태를 알고 있었다는 점이다. 재시도 예산과 헤징 예산이 비교 후 교체 루프이고, 수요 제어기가 락이다. - -열 곳 남짓에서 그 형태를 쓰지 않았다. 그러므로 이것은 지식의 부재가 아니라 적용의 누락이고, 고치는 방법도 이미 저장소 안에 있다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/non-atomic-check-then-act/reference/reference-beginning-a-transition-and-the-set-it-covers-are-one-operation.md b/docs/clean-architecture-backend-template/tech-log-studio/non-atomic-check-then-act/reference/reference-beginning-a-transition-and-the-set-it-covers-are-one-operation.md deleted file mode 100644 index 39a1515..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/non-atomic-check-then-act/reference/reference-beginning-a-transition-and-the-set-it-covers-are-one-operation.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -kind: REFERENCE -slug: beginning-a-transition-and-the-set-it-covers-are-one-operation -title: 전이를 시작하는 쓰기와 그 전이가 덮는 집합은 한 연산이어야 한다 -topic: non-atomic-check-then-act -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:beginning-a-transition-and-the-set-it-covers-are-one-operation -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# 전이를 시작하는 쓰기와 그 전이가 덮는 집합은 한 연산이어야 한다 - -## 목적 - -이제부터 다르게 동작한다고 선언하는 전이에서, 선언과 그 선언이 지배하는 대상의 갱신 사이에 끼어드는 쓰기를 막는다. - -## 규칙 - -1. 전이를 두 단계로 나눴는지 본다 - 플래그를 세우는 쓰기와 그 플래그가 덮는 대상을 갱신하는 쓰기가 따로 있으면 해당한다. - -2. 그 플래그를 읽는 가드를 전부 찾는다 - 가드를 통과한 뒤 쓰는 코드가 문제의 후보다. - -3. 가드 통과와 쓰기 사이에 다른 스레드가 낄 수 있는지 본다 - 낄 수 있으면 두 단계를 하나로 합치거나 그 구간을 락으로 닫는다. - -4. 전이 이후의 쓰기가 무해한지 확인한다 - 무해하면 두 단계로 나눠도 된다. 다만 무해함은 전이가 덮는 대상 전체에 대해 성립해야 한다. - -## 적용 조건 - -배수, 종료, 회전, 차단처럼 상태 전이로 동작 규칙이 바뀌는 모든 자리. - -## 예외 - -전이 이후에 도착한 쓰기가 결과를 바꾸지 않는 경우. 집합에 다시 넣어도 결과가 같은 종류의 갱신이 그렇다. 대상이 늘어나면 그 무해함을 다시 확인해야 한다. - -## 예시 - -헬스 레지스트리의 배수 시작이 플래그를 쓰고 상태 맵을 바꾸는 두 단계다. 그 사이에 서빙 갱신이 끼면 전역 상태가 서빙으로 되돌아간다. - -자격증명 회전에서 배수 완료가 자기가 읽은 값으로 상태를 다시 써서 진행 중인 회전을 되돌린다. - -채널 세대 교체가 읽고 판단한 뒤 조건 없이 쓴다. - -## 관계 - -- **원자 타입 위의 검사 후 실행과 비교 후 교체 루프** - 이 규칙이 나온 구조다. -- **배수를 시작한 뒤에도 한 서비스가 다시 SERVING 이 될 수 있다** - 이 규칙을 만든 사례다. -- **배수 완료가 자기가 읽은 값으로 상태를 다시 써서 진행 중인 자격증명 회전을 되돌린다** - 같은 규칙을 어긴 다른 리프의 사례다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/notification-and-delivery/case/case-a13-f002-authentication-failed-resumehealthy.md b/docs/clean-architecture-backend-template/tech-log-studio/notification-and-delivery/case/case-a13-f002-authentication-failed-resumehealthy.md deleted file mode 100644 index 8dacd98..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/notification-and-delivery/case/case-a13-f002-authentication-failed-resumehealthy.md +++ /dev/null @@ -1,272 +0,0 @@ ---- -kind: CASE -slug: a13-f002-authentication-failed-resumehealthy -title: 두 호출이 상태와 함께 경보를 끈다 -topic: notification-and-delivery -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a13-f002-authentication-failed-resumehealthy -evidenceCapturedOn: 2026-09-02 -body: case-a13-f002-authentication-failed-resumehealthy.body.md -assets: - - key: a13-f002-authentication-failed-resumehealthy - file: ../../../final/evidence/rendered/a13-f002-authentication-failed-resumehealthy.svg - - key: a13-f002-authentication-failed-resumehealthy-sequence - file: ../../../final/evidence/rendered/a13-f002-authentication-failed-resumehealthy-sequence.svg -evidence: - - ../../../final/evidence/raw/a13-f002-authentication-failed-resumehealthy.txt - - ../../../final/evidence/raw/a13-f002-authentication-failed-resumehealthy-sequence.txt -source: - - 원본 분석 절은 final/document.md#a13#L320 이다. 등급은 P2 이다. 정상 복귀 전이의 자바독 인용, 배수 표시가 현재 상태를 읽지 않는다는 지적, 두 전이가 관리자 포트에 함께 노출되어 있다는 대조, 그래서 인증 실패를 배수로 지운 뒤 정상 복귀를 부르는 두 단계 시퀀스가 보장을 우회한다는 판정, 테스트가 세 전이를 각각 새 런타임에서 확인한다는 관찰, 그리고 전이별로 현재 값을 읽는지 정리한 표가 그 절에 있다. 같은 문서의 21.2 절이 이 우회가 헬스 신호도 함께 끈다는 파급을 기록으로 남긴다. - - 이 기록이 더한 것은 셋이다. 네 단계를 실제로 불러 각 단계의 반환값과 상태와 사유를 받았고, 같은 실행에서 헬스 판정을 나란히 찍어 21.2 절이 서술한 파급이 어느 단계에서 일어나는지 보였다. 비활성 경로는 상태 기계가 같지만 헬스 지시등이 내려간 채로 있다는 차이를 확인했다. 그리고 사라지는 사유 코드를 읽는 운영 표면이 없다는 것 — 남는 것은 설명이지 기록이 아니다. - - 성능 저하로 3 단계가 막힌다는 것은 `degradingAFailedRuntimeIsRefused` 가 이미 단언한다. 이 기록이 더한 것은 그 뒤의 정상 요청까지 거짓이라는 4" 단계다. ---- - -# 두 호출이 상태와 함께 경보를 끈다 - -정상 복귀 전이가 인증 실패 상태를 거부한다고 자바독이 적는다. 같은 원자 참조를 쓰는 일곱 전이 중 여섯이 관리자 포트에 있고, 그중 둘은 현재 값을 매개변수로 받고도 읽지 않는다. 그 둘 중 하나를 거친 뒤 정상을 요청하면 상태가 통과하고, 헬스 지시등도 그 자리에서 함께 올라온다. - -## 관계 - -- **nonce replay 경계가 결과를 읽고 버린다** - 두 사례 모두 경계가 판정은 하는데 그 결과가 다음 단계로 가지 않는다. -- **그 속성을 보일 수 없는 대역 위에서 통과한 테스트는 증인이 아니다** - 이 조합만 런타임 셋을 따로 만들어 확인한다. -- **아직 쓰이지 않은 연산에도 같은 불변식이 성립하게 만든 순서** - 형제 전이가 같은 불변식을 지키는지 묻는 규칙이다. - -## 문제 - -제공자 런타임의 정상 복귀 전이가 자신이 지키는 성질을 자바독에 명시한다. - -인증 실패는 여전히 거부한다. 제공자를 정상이라 선언한다고 해서 제공자가 받아들일 자격증명이 생기지는 않으므로, 첫 시도에서 실패할 런타임을 건네는 대신 호출자에게 그 사실을 알린다. - -## 결론 - -여섯 중 셋은 현재 상태를 읽는다. 정상 복귀와 성능 저하와 스로틀이 조건을 건다. - -나머지 셋은 읽지 않고 덮어쓴다. 인증 실패 표시는 그것이 하는 일이라 set 으로 쓰고, 배수 표시와 비활성 표시 둘만 원자 갱신의 람다로 현재 값을 받아 놓고 쓰지 않는다. - -실행해서 확인했다. 인증 실패 상태에서 정상을 요청하면 거짓이 돌아오고 상태와 사유가 유지된다. 배수를 요청하면 참이 돌아오고 상태가 배수가 되며, 사유가 배수로 바뀌면서 원래 사유가 사라진다. 다시 정상을 요청하면 참이 돌아오고 상태가 정상이 된다. - -같은 자리에서 헬스 지시등이 올라온다. 배수가 보고기의 비정상 목록에 들어 있지 않아서다. 3 단계에서 이미 정상으로 돌아오고, 자격증명은 그대로 거부된 그것이다. 보고기 자신의 자바독이 강조하는 조건이 자격증명 거부이고, 그것이 프로세스 수준 헬스에서는 보이지 않으므로 운영자를 호출해야 하는 조건이라고 적는다. 그 호출이 꺼진다. - -비활성으로 가도 상태 기계는 같다. 다만 보고기는 비활성을 비정상으로 보므로 그 경로에서는 지시등이 내려간 채로 있다. 성능 저하로 가면 상태부터 다르다. 성능 저하 경로는 정상일 때만 상태를 바꾸므로 인증 실패 상태에서 거짓이 돌아오고, 이어지는 정상 요청도 거짓이다. - -포트가 여섯을 그대로 넘기는 것은 문제가 아니다. 받는 쪽 둘에 조건이 없는 것이 문제다. - -이 순서를 밟으려면 제공자 제어 권한이 필요하고, 성공한 두 걸음은 각각 감사 행을 남긴다. 거부된 2 단계는 응용 서비스에서 예외로 바뀌어 운영자에게 도착한다. 막히지는 않지만 흔적 없이 되는 일도 아니다. - -상태 자체는 자가 복구된다. 첫 시도가 실패하면 다시 인증 실패로 돌아간다. - -그 사이에 시도 하나가 제공자를 향해 나간다. 그리고 클래스 자바독이 막으려 한 창이 매 재설정마다 다시 열린다. 다만 실제로 새는 시도는 첫 실패가 상태를 되돌리기 전까지 동시에 떠 있던 발송 수만큼이고, 그 뒤의 발송은 시도 획득이 먼저 막는다. - -런타임이 상태와 짝지어 들고 있던 원인 코드도 배수 단계에서 사라진다. 다만 그 값을 읽는 운영 표면이 없다. 헬스 엔드포인트는 자격증명을 이름 짓는다는 이유로 사유를 싣지 않는다. 없어지는 것은 설명이지 기록이 아니다. - -테스트가 이것을 잡지 못하는 자리는 좁다. 같은 파일이 인증 실패 위에 성능 저하를, 정상 복귀를, 성공 복귀를 한 런타임에서 겹쳐 본다. 겹쳐 보지 않는 조합이 하나 있다. 가드가 없는 두 운영자 전이 다음의 정상 복귀다. 그것을 확인하는 테스트만 런타임 셋을 따로 만든다. - -판정은 P2 다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 일곱 전이의 구현과 관리자 포트 분배 확인, 헬스 보고기 판정 확인, 실행 탐침 -소스 수정 : x - -## 재현 조건 - -1. 같은 원자 참조를 쓰는 전이를 전수로 세고, 각각이 현재 값을 읽는지 확인한다. -2. 정상 복귀 전이의 자바독과 구현을 읽는다. -3. 관리자 포트가 여섯 전이를 어떻게 분배하는지 확인한다. -4. 헬스 보고기가 비정상으로 보는 상태 목록과 그 클래스 자바독을 읽는다. -5. 런타임의 사유 필드를 읽는 곳을 저장소 전체에서 세고, 헬스 스냅숏이 사유를 싣는지 본다. -6. 상태 테스트가 어떤 조합을 한 런타임에서 겹쳐 보는지 확인한다. -7. 런타임 하나를 인증 실패로 두고 정상·배수·정상을 순서대로 부르며, 각 단계의 반환값과 상태와 사유와 헬스 판정을 함께 기록한다. -8. 비활성 경로와 성능 저하 경로로 같은 순서를 반복한다. - -## 본문 - - - -자바독이 지키겠다고 적은 성질이 하나 있다. - -## 적어 둔 보장 - -:::evidence key="a13-f002-authentication-failed-resumehealthy" alt="같은 원자 참조를 쓰는 전이 일곱의 선언, 정상 복귀 전이의 자바독과 구현, 현재 값을 받아 놓고 쓰지 않는 둘과 set 으로 쓰는 하나의 구현, 관리자 포트가 여섯을 분배하는 스위치 전체, 헬스 보고기의 클래스 자바독과 비정상으로 보는 상태 목록, 런타임의 사유 필드를 읽는 곳과 헬스 스냅숏의 필드 목록, 그리고 이 보장을 확인하는 test 둘의 본문을 출력한 터미널 기록." caption="같은 참조를 쓰는 전이는 일곱이고 여섯이 관리자 포트에 있다 · markDraining 과 markDisabled 만 current 를 받아 놓고 쓰지 않는다 · 헬스 보고기가 비정상으로 보는 상태는 인증 실패와 비활성 둘 · 사유 필드를 읽는 프로덕션 코드는 없고 스냅숏도 사유를 싣지 않는다 — 109줄 · exit 0" zoom="true" -::: - -```java -198: *

This is the admin counterpart of {@link #markHealthy()}: it resumes a runtime that an -199: * operator drained or disabled. It still refuses {@code AUTHENTICATION_FAILED}, because declaring -200: * a provider healthy does not give it a credential the provider will accept — the caller is told -201: * so rather than being handed a runtime that will fail on its first attempt. -202: * -203: * @return whether the runtime is now healthy -204: */ -205: public boolean resumeHealthy() { -206: return health -207: .updateAndGet( -208: current -> -209: current.state() == ProviderRuntimeState.AUTHENTICATION_FAILED -210: ? current -211: : new RuntimeHealth(ProviderRuntimeState.HEALTHY, Optional.empty())) -``` - -같은 참조를 쓰는 전이는 일곱이고, 그중 여섯이 관리자 포트에 있다. 남는 하나는 성공한 시도가 부르는 `markHealthy` 다. - -## 현재 값을 받아 놓고 쓰지 않는 둘 - -```java -221: public boolean markDraining() { -222: return health -223: .updateAndGet( -224: current -> -225: new RuntimeHealth(ProviderRuntimeState.DRAINING, Optional.of("DRAINING"))) -226: .state() -227: == ProviderRuntimeState.DRAINING; -228: } -``` - -비활성 표시도 같은 조건에서 같은 값을 돌려준다. 인증 실패 표시는 아예 받지 않는다. - -```java -127: public void markAuthenticationFailed(String reasonCode) { -128: Objects.requireNonNull(reasonCode, "reasonCode"); -129: // One write, so the state and the reason it carries are never observed apart. -130: health.set( -131: new RuntimeHealth(ProviderRuntimeState.AUTHENTICATION_FAILED, Optional.of(reasonCode))); -132: } -``` - -여섯 중 읽는 것이 셋, 읽지 않는 것이 셋이다. - -## 여섯이 한 포트에 있다 - -```java -30: return switch (desiredState) { -31: case DISABLED -> runtime.markDisabled(); -32: case DRAINING -> runtime.markDraining(); -33: case HEALTHY -> runtime.resumeHealthy(); -34: case DEGRADED -> runtime.markDegraded(reason); -35: case THROTTLED -> runtime.markThrottled(); -36: case AUTHENTICATION_FAILED -> { -37: runtime.markAuthenticationFailed(reason); -38: yield true; -39: } -``` - -운영자가 요청한 상태가 그대로 메서드 하나로 간다. 인증 실패만 반환값을 만들지 않고 `true` 를 낸다. - -## 순서대로 불러 보면 - -:::evidence key="a13-f002-authentication-failed-resumehealthy-sequence" alt="관리자 포트가 부르는 여섯 전이가 현재 상태를 읽는지 하나씩 적은 표, 런타임 하나를 인증 실패로 두고 정상·배수·정상을 순서대로 불러 각 단계의 반환값과 상태와 사유와 헬스 판정을 함께 기록한 결과, 비활성 경로로 같은 순서를 반복한 결과, 그리고 가드가 있는 성능 저하 경로로 같은 순서를 반복한 결과를 출력한 터미널 기록." caption="배수를 거치면 사유가 지워지고 다음 정상 요청이 참으로 돌아온다 · 그 배수 단계에서 헬스 판정이 이미 정상으로 올라온다 · 비활성 경로는 헬스가 내려간 채로 있고, 가드가 있는 성능 저하로는 상태부터 막힌다 — 24줄 · exit 0" zoom="true" -::: - -```text -[네 단계 시퀀스] - 0. 시작 반환 - 상태 HEALTHY 사유 (없음) 헬스 true - 1. 제공자가 자격증명 거부 반환 - 상태 AUTHENTICATION_FAILED 사유 INVALID_CREDENTIAL 헬스 false - 2. 운영자가 HEALTHY 요청 반환 false 상태 AUTHENTICATION_FAILED 사유 INVALID_CREDENTIAL 헬스 false - 3. 운영자가 DRAINING 요청 반환 true 상태 DRAINING 사유 DRAINING 헬스 true - 4. 운영자가 다시 HEALTHY 요청 반환 true 상태 HEALTHY 사유 (없음) 헬스 true -``` - -4 단계에는 읽을 인증 실패가 없다. 3 단계가 그것을 지웠기 때문이다. - -## 경보가 먼저 꺼진다 - -헬스 열이 3 단계에서 이미 참이다. 보고기가 비정상으로 보는 상태 목록에 배수가 없다. - -```java -63: ProviderRuntimeState state = runtime.get().state(); -64: if (state == ProviderRuntimeState.AUTHENTICATION_FAILED -65: || state == ProviderRuntimeState.DISABLED) { -66: healthy = false; -67: } -``` - -같은 클래스의 자바독이 무엇을 지키려 했는지 적는다. - -```java -18: *

A provider whose credentials were rejected reports unhealthy even though the process is fine: -19: * that is exactly the condition an operator needs paged on, and it is invisible from process-level -20: * health. -``` - -3 단계에서 그 호출이 꺼진다. 자격증명은 그대로 거부된 그것이다. - -비활성 경로는 다르다. - -```text -[DISABLED 로도 같은지] - 3'. 운영자가 DISABLED 요청 반환 true 상태 DISABLED 사유 DISABLED 헬스 false - 4'. 운영자가 HEALTHY 요청 반환 true 상태 HEALTHY 사유 (없음) 헬스 true -``` - -상태 기계는 같지만 3' 단계에서는 지시등이 내려간 채로 있다. 두 경로가 같은 것은 상태까지다. - -```text -[비교] 가드가 있는 전이로는 통과하지 못한다 - 3". 운영자가 DEGRADED 요청 반환 false 상태 AUTHENTICATION_FAILED 사유 INVALID_CREDENTIAL 헬스 false - 4". 운영자가 HEALTHY 요청 반환 false 상태 AUTHENTICATION_FAILED 사유 INVALID_CREDENTIAL 헬스 false -``` - -우회를 만드는 것은 관리자 포트가 아니라 가드가 없는 두 전이다. - -## 원인 코드는 누가 읽는가 - -3 단계에서 `INVALID_CREDENTIAL` 이 사라진다. 그런데 그 값을 읽는 프로덕션 코드가 없다. - -```text - ProviderRuntime.java:84: *

Prefer this to calling {@link #state()} and {@link #unhealthyReason()} in turn: two reads - ProviderRuntime.java:97: public Optional unhealthyReason() { -``` - -선언과 자기 자바독뿐이다. 헬스 스냅숏도 사유를 싣지 않는다. - -```java - public record ProviderHealth( - String profileId, String state, long credentialGeneration, int activeAttempts) { -``` - -사라지는 것은 상태와 짝을 이루던 런타임 자신의 설명이고, 원인 코드 자체는 시도 기록에 남는다. - -## test 가 못 보는 조합 - -```java -124: @DisplayName("an operator can resume a drained or disabled runtime but not a failed credential") -125: void resumeHealthyClearsOperatorStatesOnly() { -126: ProviderRuntime drained = runtime(1, 1); -127: drained.markDraining(); -128: assertThat(drained.resumeHealthy()).isTrue(); -129: -130: ProviderRuntime disabled = runtime(1, 1); -131: disabled.markDisabled(); -132: assertThat(disabled.resumeHealthy()).isTrue(); -133: -134: ProviderRuntime failed = runtime(1, 1); -135: failed.markAuthenticationFailed("INVALID_CREDENTIAL"); -136: assertThat(failed.resumeHealthy()) -``` - -런타임 셋을 각각 새로 만든다. 세 객체가 만나지 않으므로 인증 실패한 런타임을 배수로 보내는 순서가 생기지 않는다. - -합성을 아예 안 보는 것은 아니다. - -```java -143: @DisplayName("degrading an already failed runtime is refused rather than silently ignored") -144: void degradingAFailedRuntimeIsRefused() { -145: ProviderRuntime runtime = runtime(1, 1); -146: runtime.markAuthenticationFailed("INVALID_CREDENTIAL"); -``` - -가드가 있는 전이 위에서는 한 런타임에 겹쳐 본다. 겹쳐 보지 않는 것은 가드가 없는 두 전이 쪽이다. - -## 확인하지 못한 것 - -관리자 응용 서비스를 통과시키지 않았다. 탐침은 런타임을 직접 부르므로 권한 확인과 멱등 재생과 감사 기록, 그리고 거부를 예외로 바꾸는 단계가 빠져 있다. - -정상이 된 런타임에 실제 알림을 흘려 시도가 제공자를 향해 나가는지 재현하지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/notification-and-delivery/case/case-a13-f003-accesscontext.md b/docs/clean-architecture-backend-template/tech-log-studio/notification-and-delivery/case/case-a13-f003-accesscontext.md deleted file mode 100644 index ffd3421..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/notification-and-delivery/case/case-a13-f003-accesscontext.md +++ /dev/null @@ -1,205 +0,0 @@ ---- -kind: CASE -slug: a13-f003-accesscontext -title: 정책을 담은 필드가 참이 된 적도 읽힌 적도 없다 -topic: notification-and-delivery -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a13-f003-accesscontext -evidenceCapturedOn: 2026-09-02 -body: case-a13-f003-accesscontext.body.md -assets: - - key: a13-f003-accesscontext - file: ../../../final/evidence/rendered/a13-f003-accesscontext.svg - - key: a13-f003-accesscontext-reveal - file: ../../../final/evidence/rendered/a13-f003-accesscontext-reveal.svg -evidence: - - ../../../final/evidence/raw/a13-f003-accesscontext.txt - - ../../../final/evidence/raw/a13-f003-accesscontext-reveal.txt -source: - - 원본 분석 절은 final/document.md#a13#L430 이다. 등급은 P2 이다. 레코드와 포트가 성질을 두 번 선언한다는 인용, 유일한 구현이 문맥을 널 검사만 하고 버린다는 관찰, 세 필드 접근자 계수와 동명 접근자가 다른 타입의 것이라는 확인, 감사 설비는 있는데 이 경로에만 연결되지 않았다는 판정, 프로덕션 reveal 호출자 여섯 곳이 전부 그 팩토리를 넘기고 그것이 감사 필요를 거짓으로 고정한다는 관찰, 참을 넘겨도 결과가 같다는 단정, 그리고 수정 방향이 그 절에 있다. - - 이 기록이 더한 것은 둘이다. 원본 절은 프로덕션 호출자만 셌지만, 이 레코드를 만드는 표현은 저장소 전체를 통틀어 팩토리 안의 것 하나뿐이고 테스트를 포함한 나머지 아홉 건이 전부 그 팩토리 호출이다 — 참을 넣는 표현이 어디에도 없다. 그리고 원본이 코드 읽기로 단정한 참을 넘겨도 결과가 같다는 것을, 문맥 셋으로 실제 복호화해 실행으로 확인했다. ---- - -# 정책을 담은 필드가 참이 된 적도 읽힌 적도 없다 - -접근 문맥 레코드와 그것을 받는 포트가 모든 평문 노출이 감사 가능하다고 두 번 선언한다. 저장소에서 그 레코드를 만드는 길은 감사 필요 여부를 거짓으로 고정한 팩토리 하나뿐이고, 세 필드 중 어느 것도 읽는 코드가 없다. - -## 관계 - -- **소비자가 없는 fixture 셋** - 이쪽도 선언된 타입의 프로덕션 호출자를 세어 0 을 확인했다. -- **타입이 문서화한 불변식은 타입이 강제한다** - 이 타입도 javadoc 으로 불변식을 적어 두고 그것을 강제하지 않는다. -- **실패 사유가 저널에도 감사 이벤트에도 남지 않는다** - 기록할 설비는 있는데 이 경로에서만 아무것도 남지 않는다. - -## 문제 - -복호화 포트가 값과 함께 접근 문맥을 받는다. - -문맥 레코드의 자바독은 평문 연락처가 왜 노출되는지를 담으며 모든 노출이 감사 가능하다고 적는다. 포트의 자바독은 감사되는 목적을 위해 값을 복호화한다고 적는다. - -## 결론 - -유일한 구현은 문맥을 널 검사만 하고 버린다. - -복호화 메서드에서 문맥을 언급하는 줄은 서명과 널 검사 두 개다. 나머지는 키를 찾고 복호화하고 파싱한다. - -문맥을 바꿔 가며 같은 값을 복호화해 봤다. 감사 필요를 참으로 두고 목적 코드를 바꿔도 돌아오는 것이 같다. 복호화 메서드 본문에 감사를 부르는 호출이 하나도 없고, 복호화기가 들고 있는 것도 키 제공자와 난수뿐이다. - -접근자 계수는 다른 각도에서 같은 자리를 짚는다. 목적 코드와 감사 필요 여부는 접근자 호출이 각각 0 이다. 행위자 참조 접근자는 일곱 건 잡히지만 전부 다른 타입의 동명 접근자다. 관리자 행위자 쪽과 감사 사건 쪽이 각각 자기 필드를 읽은 것이다. - -읽히지 않는 것만이 아니다. 이 레코드를 만드는 길이 하나뿐이고, 그 팩토리가 감사 필요 여부를 거짓으로 고정한다. 프로덕션의 여섯 제공자 어댑터가 전부 그 팩토리를 부른다. 참을 넣는 코드가 저장소에 없다. - -사고 조사가 누가 어떤 목적으로 어떤 접촉점의 평문을 열었는지 물으면, 이메일 주소와 전화번호와 디바이스 토큰의 복호화는 아무 흔적도 내놓지 못한다. 타입 서명만 흔적이 남는 것처럼 읽힌다. - -감사 설비가 없어서가 아니다. 포트도 기록 구현도 저장소에 있고, 같은 리프의 회전기가 그 포트를 주입받아 회전 사건을 남긴다. 관리자 평면도 쓴다. - -판정은 P2 다. - -수정은 복호화 구현에 감사 포트를 주입하고 노출 시점에 기록하는 것이다. 사건에는 목적 코드와 행위자 참조와 키 식별자와 유형만 넣고 평문과 지문은 넣지 않는다. 감사 포트 자바독이 원시 주소를 절대 싣지 않는다고 적어 둔 그대로다. 감사 필요가 거짓인 배달 경로까지 표본으로 남길지는 그다음 결정이다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 구현 본문 확인, 생성 지점과 접근자 전수 계수, 실행 탐침 -소스 수정 : x - -## 재현 조건 - -1. 접근 문맥 레코드와 포트의 자바독을 읽는다. -2. 유일한 구현의 복호화 메서드를 읽고, 문맥을 언급하는 줄을 센다. -3. 저장소 전체에서 이 레코드를 만드는 곳을 센다. -4. 그 팩토리가 세 필드에 무엇을 넣는지 확인한다. -5. 세 필드 접근자를 저장소 전체에서 검색하고, 동명 접근자가 다른 타입의 것인지 확인한다. -6. 복호화기를 만들어 문맥을 바꿔 가며 같은 값을 복호화한다. -7. 복호화기가 어떤 협력자를 들고 있는지 반사로 확인한다. -8. 감사 포트와 그 구현이 저장소에 있는지 확인한다. - -## 본문 - - - -평문 연락처를 복호화하는 포트가 접근 문맥을 함께 받는다. - -## 두 번 선언되는 성질 - -:::evidence key="a13-f003-accesscontext" alt="접근 문맥 레코드 전체와 그것을 받는 포트의 자바독, 유일한 구현의 복호화 메서드 전체, 저장소 전체에서 이 레코드를 만드는 곳, 세 필드 접근자의 호출 수와 그중 잡히는 곳의 목록, 그리고 감사 포트와 그 구현이 저장소에 있다는 것을 출력한 터미널 기록." caption="레코드 자바독이 모든 노출은 감사 가능하다고 적고 포트 자바독도 같은 말을 한다 · 복호화 메서드에서 문맥은 널 검사에만 나온다 · 레코드를 만드는 길은 감사 필요를 거짓으로 고정한 팩토리 하나뿐 · 목적 코드와 감사 필요 접근자 호출은 0 — 63줄 · exit 0" zoom="true" -::: - -```java -5:/** Why a plaintext contact point is being revealed. Every reveal is auditable. */ -6:public record AccessContext(String purposeCode, String actorRef, boolean auditRequired) { -``` - -```java -11: /** Decrypt a value for an audited purpose. */ -12: ContactPointValue reveal(ProtectedContactPoint protectedValue, AccessContext context); -``` - -## 유일한 구현이 그것을 버린다 - -```java -86: public ContactPointValue reveal(ProtectedContactPoint protectedValue, AccessContext context) { -87: Objects.requireNonNull(protectedValue, "protectedValue"); -88: Objects.requireNonNull(context, "context"); -89: SecretKeyMaterial key = keys.keyById(protectedValue.keyId()); -90: byte[] plaintext = -91: decrypt( -92: key, -93: protectedValue.nonce(), -94: associatedData(protectedValue.type()), -95: protectedValue.ciphertext()); -96: return parse(protectedValue.type(), new String(plaintext, StandardCharsets.UTF_8)); -97: } -``` - -문맥이 마지막으로 나오는 줄은 88 이다. - -## 문맥을 바꿔 넣어 보면 - -:::evidence key="a13-f003-accesscontext-reveal" alt="복호화기를 만들어 접근 문맥을 셋으로 바꿔 가며 같은 값을 복호화한 결과, 복호화기의 생성자 인자와 필드를 반사로 뽑은 목록, 그리고 dispatch 팩토리가 세 필드에 넣는 값을 출력한 터미널 기록." caption="감사 필요를 참으로 두고 목적 코드를 바꿔도 돌아오는 값이 같다 · 복호화기가 들고 있는 것은 키 제공자와 난수뿐이다 · dispatch 팩토리는 감사 필요를 거짓으로 고정한다 — 17줄 · exit 0" zoom="true" -::: - -```text -[문맥을 바꿔 가며 같은 값을 복호화한다] - purposeCode=DISPATCH auditRequired=false -> EmailAddress - purposeCode=BREAK_GLASS auditRequired=true -> EmailAddress - purposeCode=SUPPORT_LOOKUP auditRequired=true -> EmailAddress -``` - -감사 필요를 참으로 두어도 달라지는 것이 없다. 보낼 곳이 없기 때문이다. - -```text -[복호화기가 들고 있는 협력자] - 생성자 인자 : [SecretMaterialProvider] - 생성자 인자 : [SecretMaterialProvider, SecureRandom] - 필드 CIPHER : String - 필드 HMAC : String - 필드 KEY_ALGORITHM : String - 필드 keys : SecretMaterialProvider - 필드 random : SecureRandom -``` - -## 참을 넣는 코드가 없다 - -```text - application/notification/platform/security/AccessContext.java:17: public static AccessContext dispatch(String providerProfileId) { - application/notification/platform/security/AccessContext.java:18: return new AccessContext("DISPATCH", providerProfileId, false); - adapter/outbound/notification/platform/security/AesGcmContactPointProtectorTest.java:27: assertThat(protector.reveal(first, AccessContext.dispatch("ses-primary"))).isEqualTo(value); - adapter/outbound/notification/platform/security/AesGcmContactPointProtectorTest.java:65: assertThatThrownBy(() -> protector.reveal(moved, AccessContext.dispatch("ses-primary"))) - adapter/outbound/notification/platform/security/AesGcmContactPointProtectorTest.java:86: assertThatThrownBy(() -> protector.reveal(rotated, AccessContext.dispatch("ses-primary"))) - adapter/outbound/notification/platform/provider/ses/SesNotificationProviderAdapter.java:115: AccessContext.dispatch(submission.profile().profileId().value())); - adapter/outbound/notification/platform/provider/webpush/WebPushNotificationProviderAdapter.java:90: AccessContext.dispatch(submission.profile().profileId().value())); - adapter/outbound/notification/platform/provider/smtp/SmtpNotificationProviderAdapter.java:90: AccessContext.dispatch(submission.profile().profileId().value())); - adapter/outbound/notification/platform/provider/twilio/TwilioSmsProviderAdapter.java:86: AccessContext.dispatch(submission.profile().profileId().value())); - adapter/outbound/notification/platform/provider/fcm/FcmBatchCoordinator.java:70: AccessContext.dispatch(submission.profile().profileId().value())); - adapter/outbound/notification/platform/provider/apns/ApnsNotificationProviderAdapter.java:84: AccessContext.dispatch(submission.profile().profileId().value())); -``` - -이 레코드를 만드는 표현은 팩토리 안의 `new AccessContext(...)` 하나뿐이다. 나머지 아홉 건은 전부 그 팩토리 호출이고, 그중 셋은 테스트다. - -```text -[dispatch 팩토리가 정하는 값] - purposeCode=DISPATCH actorRef=ses-primary auditRequired=false -``` - -## 읽는 코드도 없다 - -```text - purposeCode : 0 - actorRef : 7 - auditRequired : 0 -``` - -행위자 참조가 잡히는 일곱 건은 전부 다른 타입이다. - -```text - application/notification/platform/admin/NotificationAdminApplicationService.java:138: actor.actorRef(), - application/notification/platform/admin/NotificationAdminApplicationService.java:189: actor.actorRef(), - application/notification/platform/admin/NotificationAdminApplicationService.java:267: actor.actorRef(), - application/notification/platform/admin/NotificationAdminApplicationService.java:321: actor.actorRef(), - adapter/outbound/notification/platform/observation/LoggingNotificationAudit.java:34: event.actorRef(), - adapter/outbound/persistence/notification/platform/JpaAdminOperationStore.java:44: actor.actorRef(), - adapter/outbound/persistence/notification/platform/JpaAdminOperationStore.java:101: actor.actorRef(), -``` - -관리자 행위자와 알림 감사 사건이다. 접근 문맥의 것이 아니다. - -## 감사 포트와 기록 구현은 이미 있다 - -```text - application/notification/platform/observation/NotificationAuditPort.java:4:public interface NotificationAuditPort { - adapter/outbound/notification/platform/dispatch/ProviderRuntimeRegistryTest.java:183: private static final class RecordingAudit implements NotificationAuditPort { - adapter/outbound/notification/platform/observation/LoggingNotificationAudit.java:23: implements NotificationAuditPort, NotificationSecurityAuditPort { -``` - -포트와 기록 구현이 있고, 같은 리프의 `ProviderRuntimeRotator` 가 그것을 주입받아 회전 사건을 남긴다. 빠진 것은 노출 경로의 연결 하나다. - -## 확인하지 못한 것 - -과거에 그 문맥을 읽는 코드가 있었는지 이력에서 확인하지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/notification-and-delivery/case/case-a13-f004-thymeleaf.md b/docs/clean-architecture-backend-template/tech-log-studio/notification-and-delivery/case/case-a13-f004-thymeleaf.md deleted file mode 100644 index e53970c..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/notification-and-delivery/case/case-a13-f004-thymeleaf.md +++ /dev/null @@ -1,208 +0,0 @@ ---- -kind: CASE -slug: a13-f004-thymeleaf -title: Thymeleaf 메시지 삭제 가드가 프로덕션 경로에 없고, 있어도 값은 로그에 남는다 -topic: notification-and-delivery -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a13-f004-thymeleaf -evidenceCapturedOn: 2026-09-02 -body: case-a13-f004-thymeleaf.body.md -assets: - - key: a13-f004-thymeleaf - file: ../../../final/evidence/rendered/a13-f004-thymeleaf.svg - - key: a13-f004-thymeleaf-messages - file: ../../../final/evidence/rendered/a13-f004-thymeleaf-messages.svg -evidence: - - ../../../final/evidence/raw/a13-f004-thymeleaf.txt - - ../../../final/evidence/raw/a13-f004-thymeleaf-messages.txt -source: - - 원본 분석 절은 final/document.md#a13#L476 이다. ---- - -# Thymeleaf 메시지 삭제 가드가 프로덕션 경로에 없고, 있어도 값은 로그에 남는다 - -렌더 예외를 잡아 메시지를 버리는 catch 는 모드 없는 오버로드 안에만 있다. 프로덕션은 모드 있는 오버로드만 부른다. 그리고 그 catch 가 막으려던 값은 그보다 앞서 Thymeleaf 자신의 ERROR 로그에 적힌다. - -## 관계 - -- **정리 작업이 무제한 DELETE 를 쏘고, 그것을 막는 오버로드는 호출되지 않는다** - 두 사례 모두 가드가 붙은 오버로드를 프로덕션에서 부르지 않는다. -- **정책을 담은 필드가 참이 된 적도 읽힌 적도 없다** - 같은 리프의 다른 개인정보 사례다. -- **두 오버로드가 있었고 호출되는 쪽이 틀린 쪽이었다** - 이 사례가 그 규칙의 형태다. - -## 문제 - -한 클래스에 렌더 메서드가 둘 있고 예외 처리가 서로 다르다. 그 차이가 실제로 무엇을 바꾸는지 재봤다. - -## 결론 - -모드 없는 쪽은 처리 호출을 감싸고 실행 예외를 잡는다. 잡은 뒤 메시지를 버리고 자체 예외를 던진다. 주석이 이유를 적어 뒀다. 템플릿 식은 실패한 변수를 담고 있고, 이 플랫폼에서 그 변수는 일회용 코드이거나 수신자 이름이라는 것이다. - -모드 있는 쪽에는 그 catch 가 없다. 저장소 전체를 뒤져도 배포되는 코드가 엔진을 부르는 자리는 둘이고, 둘 다 모드를 넘긴다. 같은 검색이 테스트에서 잡은 것은 열다섯 자리다. - -빠진 것은 catch 하나다. 변수 누락은 두 오버로드가 똑같이 거른다. 문맥을 만드는 자리가 그 검사를 부르기 때문이다. - -catch 가 없으면 Thymeleaf 예외가 그대로 호출자에게 간다. 그 메시지는 템플릿 원문을 인용하고, 원인 사슬은 실패한 값을 담을 수 있다. - -값이 절에 적힌 자리는 넷이었다. 메서드의 수신 객체, 함수의 인자, 속성의 선택자, 산술의 피연산자다. - -무엇이 적히는지는 자리가 아니라 예외 종류가 정한다. 메서드 실패 예외는 원인을 가리지 않고 수신 객체를 적는다. 없는 메서드를 부르든 있는 메서드가 범위를 벗어나 던지든 같다. 변환기는 변환하지 못한 인자를 적고, 속성 예외는 선택자를 적으며, 수로 바꾸지 못한 피연산자는 형식 예외가 적는다. 변환이 끝난 뒤의 실패가 조용한 것은 피연산자 자리뿐이고, 0 으로 나눈 예외가 피연산자를 적지 않기 때문이다. - -변수 자체가 아니어도 된다. 거기서 한 단계 타고 들어간 필드나 원소면 충분하다. 수신자 레코드의 전자우편을 자르려다 실패한 식이 그 주소를 남겼다. - -적히는 단위가 값이 아니라 객체다. 레코드를 통째로 넘긴 식에서는 그 안의 전자우편까지 절에 들어갔다. - -조용한 쪽은 넷이었다. 셋은 그 자리에 호출자 값이 아예 없었거나 0 으로 나눈 예외가 피연산자를 적지 않은 경우였고, 나머지 하나는 변수를 주지 않아 OGNL 에 닿기 전에 걸렸다. - -두 자리가 서로 이어져 있지 않아 예외의 행선지도 갈린다. 제공자 시도 어댑터에서는 실패가 그 자리 밖으로 나가지 않는다. 배차 서비스는 감싸지 않는다. 거기서 나간 예외는 두 프레임 위 일정 작업자의 포괄 잡기에 닿고, 작업자는 타입만 적는다. 작업자 주석이 이유를 적어 뒀다. 라이브러리 예외 문구가 수신자 주소를 담을 수 있다는 것이다. - -즉 예외를 타고 나가는 쪽은 두 자리에서 모두 막힌다. 막히지 않은 것은 반대쪽이다. - -값이 로그에 닿는 경로는 이 어댑터가 아니라 라이브러리다. 던지기에 앞서 ERROR 사건이 하나 먼저 나가고, 그 사건은 두 오버로드 모두에서 남았다. 원본 분석은 예외가 상위 로거에 기록된다고 적었는데, 예외는 타입만 기록되고 문구를 남기는 것은 라이브러리 자신의 로그다. - -마스킹 카탈로그도 이 문구를 알아보지 못했다. - -판정은 P2 다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 열한 가지 식을 두 오버로드에 넣는 프로브와 호출 경로 전수 확인 -소스 수정 : x - -## 재현 조건 - -이 사례의 프로브는 새로 만든 것이고 원문에는 없다. 하위 범위 04 의 근거 파일은 final/evidence/raw/184-notification-template-security-probes.txt 다. - -1. 고정 리비전 산출물과 잠금 파일로 클래스패스를 만든다. -2. 루트 로거에서 콘솔 어펜더를 떼고 수집용 어펜더를 붙인다. -3. 열한 가지 식을 두 오버로드에 넣고 호출자가 받는 예외 사슬을 잇는다. -4. 수집한 로그 사건을 메시지와 예외 사슬로 펼친다. -5. 두 문자열에 변수 값이 있는지, 마스킹 카탈로그를 지난 뒤에도 있는지 센다. -6. 실패가 OGNL 까지 간 열 식에서 첫 절을 따로 뽑는다. - -## 본문 - - - -`ThymeleafStringTemplateEngine`은 같은 클래스에 `render` 두 개를 갖는다. 처리 호출을 감싸는 것은 그중 하나뿐이다. - -```java -// ThymeleafStringTemplateEngine.java:84-100 (mode-less overload) -public String render(String source, Map variables) { - ... - try { - return engine.process(source, contextFor(source, variables)); - } catch (RuntimeException failure) { - // The message is dropped on purpose. Thymeleaf reports the offending expression, and a - // template expression contains the variable it failed on — which for this platform is a - // one-time code or a recipient name. - throw new TemplateRenderingException(NotificationFailureDescriptor.preDispatch(...)); - } -} -``` - -`:67-82`의 mode-aware overload에는 이 블록이 없다. `engineFor(mode).process(...)`를 그대로 부르고 결과를 `TemplateSlotPolicy.verifyRendered`에 넘긴다. - -## 배포되는 코드가 부르는 것은 감싸지 않은 쪽이다 - -:::evidence key="a13-f004-thymeleaf" alt="코드베이스 정적 검색 출력 29줄. 저장소 전체 엔진 호출과 테스트 건수, 두 오버로드의 검사 위치, 렌더를 부르는 두 자리와 그것이 닿는 작업자, 로거 설정 줄 수가 차례로 보인다." caption="오버로드와 렌더 예외가 지나는 자리 — 29줄 · exit 0" zoom="true" -::: - -`CanonicalNotificationRenderer:125`와 `:133`이 `src/main` 전체에서 유일한 호출이고, 첫 인자가 `modeOf(slot)`이다. `NotificationTemplateEngine:29-34`가 이 오버로드를 `default` 가 아니라 추상으로 둔 경위를 적어 두었고, 다른 구현인 `PlaceholderTemplateEngine`은 `:28-30`에서 모드 없는 쪽이 모드 있는 쪽으로 넘어간다. 어느 쪽으로도 `:92`에 닿지 않는다. - -## 빠진 것은 catch 하나다 - -두 오버로드가 갈리는 지점을 catch 로 좁혀야 한다. 누락 변수 검사는 갈리지 않는다. - -```java -// ThymeleafStringTemplateEngine.java:154-161 -private Context contextFor(String source, Map variables) { - ... - requireEveryReferencedVariable(source, variables); - ... -} -``` - -`:88`이 같은 검사를 한 번 더 부르지만, `contextFor`가 이미 부르므로 모드 있는 쪽도 이 검사를 지난다. 아래 자료의 `[[${code}]]` 줄이 그 결과다. - -## 열한 가지 식을 두 오버로드에 넣었다 - -:::evidence key="a13-f004-thymeleaf-messages" alt="JVM 프로브 출력 42줄. 열한 가지 식을 두 오버로드에 넣고 호출자와 로그와 마스킹 뒤에서 변수 값이 보이는지를 있음 없음으로 적은 뒤, 각 식에서 OGNL 이나 JDK 가 만든 절을 열 줄로 나열한다. 로그와 마스킹 칸은 두 오버로드가 같을 때만 한 값으로 적힌다. 유틸리티 객체의 주소는 실행마다 달라지므로 고정 표시로 바꿔 적었다." caption="열한 가지 식을 두 오버로드에 넣은 프로브 — 42줄 · exit 0" zoom="true" -::: - -값이 나온 식이 일곱, 나오지 않은 식이 넷이다. 가르는 것은 식의 생김새가 아니라 실패한 자리에 무엇이 놓여 있었느냐다. 자료 마지막 열 줄이 각 식에서 OGNL 이나 JDK 가 만든 첫 절을 보여 준다. - -``` -[[${recipient.email.substring(99)}]] MethodFailedException: Method "substring" failed for object alice@example.com -[[${#numbers.formatDecimal(code, 1, 2)}]] IllegalArgumentException: Unable to convert type java.lang.String of 483920 to type of java.lang.Number -[[${code[recipient]}]] NoSuchPropertyException: java.lang.String.Recipient[email=alice@example.com, code=483920] -[[${recipient * 2}]] NumberFormatException: For input string: "Recipient[email=alice@example.com, code=483920]" -``` - -수신 객체, 변환하지 못한 인자, 속성 이름으로 쓰인 선택자, 수로 바꾸지 못한 피연산자다. 네 자리에서 모두 같은 일이 났다. 다만 절에 무엇이 실리느냐는 예외 클래스마다 다르다. 첫 줄이 보여 주듯 그 자리에 놓인 것이 변수 자신일 필요도 없다. `recipient`는 수신 객체가 아니지만 거기서 도달한 필드가 수신 객체가 되면서 그 값이 절에 적혔다. - -나오지 않은 넷 가운데 셋은 이렇게 끝났다. - -``` -[[${#strings.substring(code, 99, 200)}]] MethodFailedException: Method "substring" failed for object org.thymeleaf.expression.Strings@<주소> -[[${code.noSuchProperty}]] NoSuchPropertyException: java.lang.String.noSuchProperty -[[${code.length() / 0}]] ArithmeticException: / by zero -``` - -첫 줄은 수신 객체가 유틸리티이고, 변수는 인자 자리에 있는데 그 변환이 성공했다. 둘째는 선택자가 리터럴 이름이다. 셋째는 두 피연산자가 모두 수로 바뀐 뒤 나눗셈 자체가 실패한 경우다. 변환이 끝난 다음이면 `ArithmeticException` 은 피연산자를 적지 않는다. `[[${code[recipient]}]]`와 `[[${code.noSuchProperty}]]`가 같은 예외 타입에서 갈리는 것이 이 규칙을 가장 짧게 보여 준다. - -`:93-95`의 주석은 노출 범위를 "일회용 코드이거나 수신자 이름"으로 적었다. 실제로 나가는 것은 그 자리에 놓인 객체의 `toString()` 전체이므로, 값 객체를 넘기면 그것이 담은 필드까지 따라 나온다. - -넷째인 `[[${code}]]` 는 OGNL 까지 가지도 않는다. 누락 변수 검사가 먼저 거르므로 절 목록에도 없다. 열한 식이 남긴 절이 열 줄인 이유가 그것이다. - -원본 분석 `:532`가 든 `${amount.formatted('%.2f')}`는 첫 번째 자리에 걸린다. `amount`가 `BigDecimal`이나 `Double`이면 `formatted`가 없는 메서드가 되고, 문자열이면 아예 실패하지 않는다. - -## 렌더를 부르는 두 자리는 서로 독립이다 - -`NotificationProviderAttemptAdapter:163`은 자기 호출을 감싸고 `:164`에서 실행 예외를 잡아 `TEMPLATE_RENDERING_FAILED` 코드만 남긴다. 예외 객체는 버려진다. - -`NotificationDispatchService:146`에는 그런 감싸기가 없다. 거기서 던져진 것은 `dispatch(...)` 밖으로 나가 두 프레임 위에서 잡힌다. - -```java -// NotificationSchedulerWorker.java:95-106 (주석 여섯 줄 중 앞 세 줄 생략) -} catch (RuntimeException failure) { - // The cause chain by type, never by message. ... a library's exception text - // can carry a recipient address, so the types are named and the messages are not. - log.warn("notification dispatch failed worker={} reason={} causes={}", - workerId, failure.getClass().getSimpleName(), causeChain(failure)); -} -``` - -`causeChain`은 `:121-132`에서 원인마다 `getSimpleName()`만 이어 붙인다. 그래서 여기 남는 것은 `reason=TemplateInputException causes=TextParseException<...` 같은 타입 나열이고 문구는 없다. - -## 값은 그 앞에서 로그에 적힌다 - -Thymeleaf 는 예외를 올려보내기 전에 `org.thymeleaf.TemplateEngine` 로거로 같은 사슬을 ERROR 에 남긴다. `app-bootstrap`의 yml·xml·properties 어디에도 이 로거를 따로 설정한 줄이 없다. `application.yml:347`의 루트 준위 기본값은 `INFO`이고 환경 변수로만 바뀌며, `logback-spring.xml`은 네 조합 모두에서 ``를 직접 적는다. 샘플링도 이 사건을 지우지 못한다. - -```java -// SamplingTurboFilter.java:46-48 -if (level.toInt() >= Level.WARN_INT) { - return FilterReply.NEUTRAL; -} -``` - -프로브가 수집한 로그 사건에서 값이 보인 칸은 두 오버로드 모두 `있음`이다. 마스킹도 이것을 잡지 못한다. `SecretMaskingMessageConverter`는 `%maskedMsg` 자리에서 `super.convert(event)`만 훑고, 그 값은 본문이 아니라 예외 쪽에 있다. JSON 어펜더 쪽은 스택까지 훑지만 같은 카탈로그를 지난다. - -```java -// LogMaskingPatterns.java:20-21 -private static final String VALUE = "[^\\s\"',&}]+"; -private static final String SEP = "[\"']?\\s*[:=]\\s*[\"']?"; -``` - -규칙마다 키 이름과 `:` 또는 `=`를 요구한다. 위의 어느 절에도 그 둘이 없다. 프로브가 이 `mask`를 그대로 불러 확인한 칸이 `마스킹 뒤 있음`이다. - -## 확인하지 못한 것 - -운영 템플릿 가운데 이런 식을 쓰는 것이 몇 개인지 세지 않았다. 절을 만드는 예외 종류는 이 열한 식이 만든 것만 봤고, 라이브러리 자신이 던지는 예외가 인자를 문구에 담는 경우는 세지 않았다. 마스킹은 카탈로그 함수로만 확인했고, JSON 어펜더를 붙여 실제 출력 문자열을 만들지는 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/notification-and-delivery/case/case-a13-f005-retry-after-illegalargumentexception.md b/docs/clean-architecture-backend-template/tech-log-studio/notification-and-delivery/case/case-a13-f005-retry-after-illegalargumentexception.md deleted file mode 100644 index cdde449..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/notification-and-delivery/case/case-a13-f005-retry-after-illegalargumentexception.md +++ /dev/null @@ -1,304 +0,0 @@ ---- -kind: CASE -slug: a13-f005-retry-after-illegalargumentexception -title: 음수 Retry-After 를 거르지 않는 파서 셋이 조립되지 않아 결함이 잠재로 남는다 -topic: notification-and-delivery -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a13-f005-retry-after-illegalargumentexception -evidenceCapturedOn: 2026-09-02 -body: case-a13-f005-retry-after-illegalargumentexception.body.md -assets: - - key: a13-f005-retry-after-illegalargumentexception - file: ../../../final/evidence/rendered/a13-f005-retry-after-illegalargumentexception.svg - - key: a13-f005-retry-after-illegalargumentexception-negative - file: ../../../final/evidence/rendered/a13-f005-retry-after-illegalargumentexception-negative.svg - - key: a13-f005-retry-after-illegalargumentexception-unassembled - file: ../../../final/evidence/rendered/a13-f005-retry-after-illegalargumentexception-unassembled.svg - - key: a13-f005-retry-after-illegalargumentexception-ambiguous - file: ../../../final/evidence/rendered/a13-f005-retry-after-illegalargumentexception-ambiguous.svg -evidence: - - ../../../final/evidence/raw/a13-f005-retry-after-illegalargumentexception.txt - - ../../../final/evidence/raw/a13-f005-retry-after-illegalargumentexception-negative.txt - - ../../../final/evidence/raw/a13-f005-retry-after-illegalargumentexception-unassembled.txt - - ../../../final/evidence/raw/a13-f005-retry-after-illegalargumentexception-ambiguous.txt -source: - - 원본 분석 절은 final/document.md#a13#L636 이다. ---- - -# 음수 Retry-After 를 거르지 않는 파서 셋이 조립되지 않아 결함이 잠재로 남는다 - -헤더 파서는 숫자 형식만 방어하고 부호는 방어하지 않는다. 실패 값 타입의 생성자는 음수를 거부한다. 그 조합이 인자 예외를 만드는데, 그 예외를 만들 수 있는 세 분류기는 이 빌드에서 조립되지 않는다. - -## 관계 - -- **검증기가 불리지 않는 지금 실제로 도는 게이트는 생성자다** - 검사가 생산자가 아니라 소비자 생성자에 있다는 점이 같다. -- **"상한을 두고 읽는다"는 본문 핸들러가 전부 읽은 뒤에 자른다** - 같은 리프의 다른 원격 입력 사례다. -- **바이트가 프로세스를 떠났는가가 REJECTED와 AMBIGUOUS를 가른다** - 응답을 다 받은 실패가 AMBIGUOUS 로 적히므로 그 기준으로 설명되지 않는다. - -## 문제 - -원격이 조절 응답과 함께 재시도 대기 헤더를 보낸다. 파서가 그 값을 기간으로 바꾸고 실패 값 타입이 그것을 담는다. 두 쪽의 입력 계약이 같은지, 그리고 그 차이가 실제로 도달 가능한지 확인했다. - -## 결론 - -계약은 다르다. 파서는 정수 파싱이 받아들이는 값을 그대로 초 단위 기간으로 만들고 부호는 보지 않는다. 받는 쪽 생성자는 음수를 인자 예외로 거절한다. - -같은 파서가 저장소에 넷 있다. 부호를 보는 줄은 웹훅 사본 하나뿐이고, 그 술어는 표준이 허용하는 0 까지 걸러 낸다. - -고쳐야 할 술어도 리프마다 다르다. 알림 리프의 소비자는 힌트에 상한을 걸므로 음수만 거르면 되지만, httpclient 사본의 소비자는 힌트에 기간을 더하므로 아주 큰 양수에서 넘친다. - -넷 가운데 하나는 죽어 있다. FCM 분류기가 자기 사본을 공개 메서드로 들고 있는데, 그 분류기를 쓰는 세 자리가 부르는 것은 다른 메서드다. - -문제가 실제로 나는 조합은 공용 파서 쪽이다. 세 분류기가 다섯 자리에서 그것을 부르고, 상태 변환기는 429 뿐 아니라 500 이상에도 같은 값을 넘긴다. 음수 헤더를 단 429 응답을 넣으면 세 분류기가 모두 인자 예외로 끝난다. APNs 만 정상 분류를 돌려주는데 애초에 이 헤더를 읽지 않기 때문이다. - -그런데 그 셋은 이 빌드에서 돌 수 없다. - -세 어댑터를 생성하는 코드가 시험 소스에만 있다. main 에 있는 조립기는 SMTP 하나뿐이다. 그 밖의 계열을 켜면 해당 어댑터를 만드는 조립 코드가 없어 애플리케이션이 기동하지 않는다. 거부 문구가 이유를 적어 뒀다. 해당 전송은 인터페이스만 제공하고 실제 구현은 포함하지 않는다는 것이다. - -조립되면 P2 다. 원격이 정하는 값 하나가 확정 거절을 영구 실패로 바꾸고 재시도와 대체를 막는다. 이 빌드에서는 그 셋이 조립되지 않으므로 한 단계 내려 P3 으로 둔다. 배선되지 않은 발견을 도달 가능한 등급에서 한 단계 내리는 것은 이 저장소가 websocket 리프에서 쓴 방식이다. 같은 저장소가 P3 을 매긴 다른 자리는 닿지 않는다는 것에 더해 결과가 무해하다는 조건을 하나 더 달았는데, 여기 결과는 무해하지 않다. 등급이 심각도를 바꾸되 발견을 없애지는 않는다. - -원문도 P3 이지만 근거가 다르다. 원문은 표준을 지키는 제공자가 음수를 보내지 않는다는 것을 들었다. 그 근거는 계속 유효하지만, 조립되지 않았다는 근거는 조립기가 생기면 사라진다. - -조립기가 생기면 이 결함이 도달 가능해지므로, 그때 무슨 일이 나는지까지 정적으로 따라갔다. - -세 어댑터의 제출 메서드는 미래를 만들기 전에 본체를 먼저 실행한다. 예외는 게이트웨이가 미래를 기다리기 전에 던져지므로 완료 예외가 되지 않고, 게이트웨이의 완료 예외 잡기를 지나친다. 미래를 나중에 채우는 어댑터는 하나뿐이고, 그 계열 분류기는 이 파서와 무관하다. - -배차 서비스의 잡기 셋 가운데 앞의 둘은 타입이 맞지 않는다. 마지막 포괄 잡기가 받아 응답 소실과 모호한 제출로 적는다. 그 가지의 주석은 바이트가 제공자에 닿았을 수 있다는 전제를 적어 뒀는데, 예외 시점에 응답은 이미 읽혀 있었다. - -그다음이 원문과 갈리는 자리다. 원문은 예외가 일정 작업자의 포괄 잡기에 닿아 리스가 만료된다고 적었으나, 배차 서비스가 먼저 잡으므로 그 잡기에는 예외가 도달하지 않는다. - -한 회차의 순서도 짚어야 한다. 라우팅은 이 실패를 볼 수 없다. 그 결정이 제출보다 먼저 나기 때문이다. 그 회차를 끝내는 것은 재시도 정책이다. 정책이 읽는 두 능력 값의 출처는 어댑터가 아니다. 어댑터의 능력 선언 메서드는 프로덕션 호출자가 없다. - -그 두 값의 조합이 셋인데 결과는 둘로 모인다. 상태 조회를 지원하면 바로 조정으로 간다. 둘 다 지원하지 않으면 중지한다. 멱등성만 지원하면 재전송을 예약하지만 그 회차는 제출에 닿지 못한다. 수신자의 ambiguousAttemptExists 가 이미 참이라, 라우팅이 제출 대신 조정으로 보내기 때문이다. - -한 회차에 수신자 상태를 쓰는 자리도 둘이다. 기록기가 먼저 모호한 결과를 조정 필요로 적고 ambiguousAttemptExists 를 참으로 만든다. 그다음 후속 조치가 조건마다 그 상태를 덮는다. 중지에서는 실패로, 재전송 예약에서는 재시도 대기로 덮는다. 둘 중 실패만 다음 회차의 청구 대상이 아니다. 그 시도까지 모호한 시도가 없었다면, 조절은 그 값을 참으로 만들지 않으므로 다시 집혔을 것이다. - -아무도 걸리지 않은 이유도 확인했다. 알림 리프에서 이 헤더를 넣는 시험이 넷인데 값이 10, 7, 3, 7 이다. 다른 리프의 픽스처가 넣는 값도 1 이다. 적대적 값은 한 자리도 없다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 파서 넷과 생성자 대조, 분류기 넷에 음수 헤더를 넣는 프로브, 조립 가능성과 예외 경로 정적 추적 -소스 수정 : x - -## 재현 조건 - -이 사례의 프로브는 새로 만든 것이고 원문에는 없다. 원문 근거는 분석 문서의 §21.1 절이다. - -1. 파싱 구문으로 저장소를 훑어 초 단위 변환 자리를 찾는다. -2. 각 파서가 부호를 검사하는지 읽는다. -3. 받는 값 타입의 정규 생성자 검증을 읽는다. -4. 고정 리비전 산출물로 클래스패스를 만들고 파서 셋에 같은 헤더 넷을 넣는다. -5. 음수 헤더를 단 429 응답을 분류기 넷에 넣는다. -6. 그 분류기를 쓰는 어댑터가 main 에서 생성되는지, 그 계열의 조립기가 있는지 센다. -7. 제출 메서드가 동기인지 확인하고 게이트웨이와 배차 서비스의 잡기를 읽는다. -8. 한 회차 안의 호출 순서와 중지가 남기는 상태, 그리고 다음 회차의 청구 조건을 읽는다. -9. 이 헤더를 넣는 시험이 넣는 값을 전부 나열한다. - -## 본문 - - - -파서는 `Long.parseLong`이 받아들이는 값을 그대로 `Duration`으로 만든다. - -```java -// ProviderResults.java:90-99 -public static Optional retryAfter(Optional headerValue) { - return headerValue.flatMap(value -> { - try { return Optional.of(Duration.ofSeconds(Long.parseLong(value.trim()))); } - catch (NumberFormatException notSeconds) { return Optional.empty(); } - }); -} -``` - -받는 쪽은 그 값을 거부한다. - -```java -// ProviderFailure.java:29-34 (canonical constructor) -retryAfter.ifPresent(delay -> { - if (delay.isNegative()) { throw new IllegalArgumentException("retryAfter"); } -}); -``` - -## 같은 파서가 넷이고 술어는 하나도 맞지 않는다 - -:::evidence key="a13-f005-retry-after-illegalargumentexception" alt="코드베이스 정적 검색 출력 49줄. 파싱 구문 검색을 retry·seconds·header 로 좁힌 결과와, 네 파서 안에서 부호를 보는 줄, 받는 생성자의 검증, 공용 파서를 쓰는 다섯 자리, FCM 분류기가 나오는 자리 전부, 이 헤더를 넣는 시험 값이 차례로 보인다." caption="Retry-After 파서 넷과 그 소비자 — 49줄 · exit 0" zoom="true" -::: - -첫 묶음은 `parseLong`·`parseInt`·`Duration.parse` 로 저장소를 훑은 뒤 `retry`·`seconds`·`header` 가 든 줄만 남긴 것이다. Retry-After 를 초로 바꾸는 것은 그중 넷이다. `ProviderResults:94`, `FcmFailureClassifier:70`, `WebhookNotificationProviderAdapter:243`, `BlockingAttemptExecutor:238`. 목록의 `WebThrottleHttpContract:49`도 이 헤더를 정수로 바꾸지만 파서가 아니라 시험 단언이다. - -그 네 파일에서 부호를 보는 줄은 하나다. - -```java -// WebhookNotificationProviderAdapter.java:243-244 -long seconds = Long.parseLong(value.trim()); -return seconds > 0 ? Optional.of(Duration.ofSeconds(seconds)) : Optional.empty(); -``` - -`seconds > 0`은 음수와 함께 0 도 버린다. RFC 9110 §10.2.3 의 `delta-seconds` 는 `1*DIGIT` 이라 0 을 허용한다. 다만 소비자 쪽에서는 차이가 없다. `RetryBackoff.delay:44`가 힌트를 계산된 후퇴보다 클 때만 쓰므로 `PT0S`와 빈 값이 구분되지 않는다. - -넷에 같은 술어를 처방할 수도 없다. 알림 리프는 `RetryBackoff.delay:45`가 상한을 걸어 주므로 음수만 거르면 되지만, `BlockingAttemptExecutor:238`이 채우는 값은 `DefaultRetryEligibilityEngine:127`에서 `Duration.plus`로 들어간다. 아주 큰 양수는 거기서 넘친다. - -`FcmFailureClassifier`가 나오는 자리는 여섯이다. `FcmBatchCoordinator`는 `classify`만, `FcmContactPointUpdater`는 `invalidatesContactPoint`만 부른다. `:66-75`의 사본에는 호출자가 없다. - -값이 흘러 들어가는 상태도 429 하나가 아니다. `ProviderResults.fromStatus`는 `:54`의 429 가지와 `:78`의 500 이상 가지에 같은 인자를 넘긴다. - -## 음수 헤더를 실제로 넣었다 - -:::evidence key="a13-f005-retry-after-illegalargumentexception-negative" alt="JVM 프로브 출력 29줄. 헤더 네 값을 파서 셋에 넣은 결과와, 음수 헤더를 단 429 응답을 분류기 넷에 넣은 결과가 나온다." caption="음수 Retry-After 를 파서 셋과 분류기 넷에 넣은 프로브 — 29줄 · exit 0" zoom="true" -::: - -`-30`을 넣으면 공용 파서와 FCM 사본이 `PT-30S`를 만들고 웹훅 사본은 빈 값을 준다. 그 `PT-30S`가 생성자에 닿으면 SES·Twilio·WebPush 셋이 모두 `IllegalArgumentException: retryAfter`로 끝난다. APNs 만 `retryAfter=Optional.empty`인 정상 실패를 돌려준다. `ApnsFailureClassifier:55`가 헤더 대신 빈 값을 넘기기 때문이다. - -`0`에서 두 계열이 갈린다. 공용 파서는 `PT0S`를 만들고 그 값은 음수가 아니므로 생성자를 통과한다. - -HTTP-date 형식은 프로브에 넣은 셋 모두 빈 값이 되어 계산된 후퇴로 떨어진다. 프로브에 없는 네 번째도 `BlockingAttemptExecutor:239-243`에서 같은 `NumberFormatException` 가지로 빈 값을 준다. - -## 그 셋은 이 빌드에서 조립되지 않는다 - -:::evidence key="a13-f005-retry-after-illegalargumentexception-unassembled" alt="코드베이스 정적 검색 출력 49줄. 재시도 정책이 보는 두 값의 출처, 알림 경로에서 capabilities 를 선언하고 부르는 자리, 프로파일 스냅샷을 만드는 자리, 조립기 구현 하나, 세 어댑터를 생성하는 자리, 조립기 없는 계열의 기동 거부 문구가 차례로 보인다." caption="세 어댑터의 조립 가능성과 능력 선언의 출처 — 49줄 · exit 0" zoom="true" -::: - -자료의 각 줄 앞에 붙은 `main`·`test` 가 소스셋이다. 세 어댑터를 생성하는 자리는 여섯 줄 모두 `test` 다. `implements ProviderRuntimeAssembler` 도 main 에는 `SmtpProviderRuntimeAssembler:48` 하나뿐이고, `new ProviderProfileSnapshot(` 을 main 에서 부르는 자리도 그 클래스의 `:127` 하나다. - -조립기가 없는 계열의 프로파일을 켜면 기동이 거부된다. - -```java -// NotificationProviderAssembly.java:96-104 - if (assembler == null) { - throw new IllegalStateException( - "notification provider profile '" - + profileId - + "' is of type " - + type - + ", which has no assembler in this build. The transport is a seam, not an" - + " implementation; remove the profile or supply a ProviderRuntimeAssembler for" - + " that family."); -``` - -그래서 이 결함은 잠재다. 원문이 P3 을 매긴 근거는 표준을 지키는 제공자가 음수를 보내지 않는다는 것인데, 그보다 앞서 이 코드에 닿을 방법이 없다. - -## 조립기가 생기면 무슨 일이 나는가 - -:::evidence key="a13-f005-retry-after-illegalargumentexception-ambiguous" alt="코드베이스 정적 검색 출력 46줄. 일곱 어댑터의 제출 반환 줄, 게이트웨이가 던지고 잡는 자리, 배차 서비스의 세 잡기와 마지막 가지가 만드는 값, 한 회차의 호출 순서, 중지가 남기는 상태, 다음 회차의 청구 조건이 차례로 보인다." caption="인자 예외가 지나는 길과 한 회차의 끝 — 46줄 · exit 0" zoom="true" -::: - -먼저 어디를 지나지 않는지가 중요하다. 일곱 어댑터의 반환 줄이 자료에 있다. 여섯은 본체를 먼저 실행하고 그 결과로 완료된 미래를 만들며, `FcmNotificationProviderAdapter:65`만 조정자에게 넘긴다. 그 조정자도 `FcmBatchCoordinator:96`에서 완료된 미래를 돌려주므로 던지는 시점은 같다. - -```java -// SesNotificationProviderAdapter.java:105-108 - public CompletionStage submit(ProviderSubmission submission) { - Objects.requireNonNull(submission, "submission"); - return CompletableFuture.completedFuture(send(submission)); - } -``` - -`send(...)`가 먼저 평가되므로 인자 예외는 `submit` 호출 자체에서 동기로 던져진다. - -```java -// RegistryProviderDispatchGateway.java:45-50 - try (AttemptPermit permit = runtime.acquireAttempt()) { - ProviderSubmissionResult result = - runtime.adapter().submit(submission).toCompletableFuture().join(); - applyHealth(runtime, result); - return result; - } catch (CompletionException failure) { -``` - -`:47`에서 던져지므로 `join()`에 닿지 않고 `CompletionException`도 되지 않는다. `:50`의 잡기는 이 경로에 없다. 자료에서 `supplyAsync` 를 쓰는 줄은 `SmtpNotificationProviderAdapter:82` 하나이고, 그 계열 분류기에는 `ProviderResults` 참조가 없다. - -예외는 try-with-resources 를 그대로 통과해 배차 서비스로 간다. 잡기가 셋인데 `:221`은 `ProviderCallNotStartedException`, `:227`은 `NotificationException` 이라 타입이 맞지 않는다. 남는 것이 마지막이다. - -```java -// NotificationDispatchService.java:243-253 -} catch (RuntimeException transportFailure) { - // Anything the adapter did not classify. The bytes may have reached the provider, so the only - // honest record is an ambiguous one — this branch is the residue, not the default. - return ProviderSubmissionResult.ambiguous( - ProviderFailure.of( - NotificationFailureCode.PROVIDER_RESPONSE_LOST, - FailureCategory.AMBIGUOUS_SUBMISSION, - false), - ProviderExecutionEvidence.responseLost(), - Duration.ZERO); -} -``` - -주석의 전제는 바이트가 제공자에 닿았을 수 있다는 것이다. 이 실패에는 그 전제가 없다. 예외가 난 시점에 429 응답은 이미 읽혀 있었고, 429 는 제공자가 받지 않았다는 확정이다. - -## 그 회차의 끝 - -한 회차 안의 순서가 정해져 있다. `:121`의 라우팅 결정은 제출보다 앞이고 이전 시도 상태로 돈다. 이 실패를 같은 회차에 보는 것은 `:175`의 재시도 정책이다. - -정책이 보는 두 값은 어댑터가 아니라 프로파일 스냅샷에서 온다. - -```java -// NotificationDispatchService.java:372-373 - profile.capabilities().providerIdempotency(), - profile.capabilities().statusQuery(), -``` - -알림 경로의 프로덕션 코드 가운데 `NotificationProviderAdapter.capabilities()` 를 부르는 것은 없다. 스냅샷을 만드는 자리가 조립기이므로, 이 두 값은 그날 쓰인 조립기가 정한다. - -```java -// DefaultNotificationRetryPolicy.java:54-59 - if (context.confirmation() == AttemptConfirmation.AMBIGUOUS) { - if (context.statusQuerySupported()) { - return new RetryDecision.Reconcile(context.now().plus(reconcileDelay)); - } - if (!context.providerIdempotency()) { - return new RetryDecision.Stop("AMBIGUOUS_UNSAFE_TO_RETRY"); -``` - -갈래는 셋이다. 상태 조회가 참이면 `:56`의 조정, 둘 다 거짓이면 `:59`의 중지, 상태 조회만 거짓이고 멱등성이 참이면 블록을 빠져나간다. 셋째 갈래도 `:69`·`:74`·`:77`의 중지 셋을 지나야 `:80`의 재전송에 닿는다. - -그 재전송은 제공자에 닿지 않는다. 이 회차에서 상태를 먼저 쓰는 것은 `:173`의 기록기이고, 그 기록기가 `:108`에서 결과의 확인 상태를 보고 `ambiguous` 를 정한다. - -```java -// DispatchOutcomeRecorder.java:119-126 - ambiguous - ? RecipientDeliveryState.RECONCILIATION_REQUIRED - : RecipientDeliveryState.DISPATCHING, - ... - recipient.ambiguousAttemptExists() || ambiguous, - recipient.duplicateRisk() || ambiguous, -``` - -`ambiguousAttemptExists` 가 세워진 채 커밋되고, `transitionHeldBy` 는 상태와 다음 시각만 쓰므로 그 표시가 남는다. 그사이 요청이 만료되면 애초에 다시 집히지도 않는다. 집힌다면 `RoutingDecisionEngine:24`가 실패 스위치에 가기 전에 조정을 돌려주고, `NotificationDispatchService:122-126`이 제출 앞에서 되돌아온다. `applyRoutingStop:389-393`이 수신자를 조정 필요로 옮긴다. - -중지 갈래에서는 `:435-440`이 확정 수락인지 보고 아니면 실패로 덮는다. - -```java -// NotificationDispatchService.java:435-440 - case RetryDecision.Stop ignored -> - recipients.transitionHeldBy( - work.recipient().id(), - attempt.submissionOutcome() == SubmissionOutcome.CONFIRMED_ACCEPTED - ? RecipientDeliveryState.COMPLETED - : RecipientDeliveryState.FAILED, -``` - -실패는 `RecipientClaimSql:30`의 청구 대상 셋에 없다. `RoutingDecisionEngine:45`의 `AMBIGUOUS_SUBMISSION` 가지는 어차피 닿지 않는다. 모호한 시도 뒤에는 `:24`가 스위치 앞에서 먼저 돌려주기 때문이다. 조절이었다면 그 표시가 서지 않아 `:24`를 지나고 `:38-41`의 같은 채널 재시도에 닿았을 것이다. - -건강 반영도 사라진다. `applyHealth`는 게이트웨이 `:48`에서 불리고 그 줄은 `:47`이 던지면 닿지 않는다. - -## 원문과 어긋나는 지점 - -원문 `:659`는 이 예외가 `NotificationSchedulerWorker`의 포괄 잡기에 닿아 리스가 만료되고 회수가 재대사로 처리한다고 적었다. 배차 서비스의 `:243`이 먼저 잡으므로 `dispatch()`는 정상으로 돌아오고, 작업자의 잡기는 발화하지 않으며, 리스는 만료가 아니라 `:177`에서 반납된다. - -## 아무도 걸리지 않은 이유 - -자료 마지막 두 묶음이 이 헤더를 넣는 자리다. 알림 리프의 시험 넷이 `10`, `7`, `3`, `7`을 쓰고, httpclient 리프의 픽스처 `StatefulUpstream:62`가 `1`을 쓴다. 적대적 값은 없다. - -`NotificationChaosSecurityTest:120`의 `3`은 그 시험이 헤더를 공격해서 나온 값이 아니다. 그 클래스의 javadoc 이 주제를 둘로 적는다. 커밋 후 응답 손실과 텔레메트리 비밀 누출이다. `3`은 비밀 누출 검사가 쓰는 오류 응답 목록에 들어 있을 뿐이다. - -## 확인하지 못한 것 - -조립기를 써서 세 어댑터를 실제로 띄워 보지는 않았다. 6번 이후는 분기 조건을 코드로 확인한 정적 추적이다. httpclient 사본의 넘침도 소비자 코드를 읽어 확인했을 뿐 실행으로 재현하지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/notification-and-delivery/case/case-a13-f008-host.md b/docs/clean-architecture-backend-template/tech-log-studio/notification-and-delivery/case/case-a13-f008-host.md deleted file mode 100644 index a51604a..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/notification-and-delivery/case/case-a13-f008-host.md +++ /dev/null @@ -1,184 +0,0 @@ ---- -kind: CASE -slug: a13-f008-host -title: 포트를 뺀 호스트를 서명하는 자리가 둘이고, 같은 저장소의 네 자리는 그 포트를 지킨다 -topic: notification-and-delivery -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a13-f008-host -evidenceCapturedOn: 2026-09-02 -body: case-a13-f008-host.body.md -assets: - - key: a13-f008-host - file: ../../../final/evidence/rendered/a13-f008-host.svg - - key: a13-f008-host-wire - file: ../../../final/evidence/rendered/a13-f008-host-wire.svg -evidence: - - ../../../final/evidence/raw/a13-f008-host.txt - - ../../../final/evidence/raw/a13-f008-host-wire.txt -source: - - 원본 분석 절은 final/document.md#a13#L831 이다. ---- - -# 포트를 뺀 호스트를 서명하는 자리가 둘이고, 같은 저장소의 네 자리는 그 포트를 지킨다 - -SES 요청 매퍼는 서명할 때 엔드포인트의 포트를 버린다. 전선에 나가는 값은 표준 라이브러리가 URI 에서 채우므로 기본이 아닌 포트가 거기 붙는다. 웹푸시 서명기도 같은 식을 쓰고, 그 포트를 지키는 코드는 같은 저장소에 네 벌 있다. - -## 관계 - -- **지운다고 약속한 사본은 아무도 받지 않고, 서명 자리가 받는 사본은 지울 수 없다** - 같은 SigV4 경로의 다른 자리다. -- **로컬이 다른 DB면 로컬 테스트는 다른 시스템에 대한 진술이다** - 어느 결함이 어느 환경에서만 보이는지 적어 둔다는 규칙이 같다. -- **음수 Retry-After 를 거르지 않는 파서 셋이 조립되지 않아 결함이 잠재로 남는다** - 같은 리프에서 조립 여부가 등급을 정한 사례다. - -## 문제 - -서명 방식은 정규 요청에 실제로 전송되는 호스트 헤더 값을 담도록 요구하고, 기본이 아닌 포트는 그 값에 든다. 매퍼가 무엇을 서명하는지, 그리고 전선에 무엇이 나가는지 확인했다. - -## 결론 - -매퍼는 엔드포인트에서 호스트만 꺼내 서명 대상 헤더 맵에 넣는다. - -요청 자체가 싣는 헤더는 넷이고 그 안에 호스트가 없다. 매퍼가 넣지 않기 때문이다. 게이트웨이의 제한 목록이 막는 것은 다른 경우다. 공급된 헤더를 막는다. - -왕복을 한 번 쟀다. 로컬 서버를 띄우고 같은 게이트웨이로 세 가지 헤더 모양을 보냈다. 매퍼가 실제로 보내는 넷만 넣었을 때도, 거기에 호스트를 더했을 때도, 대문자 키로 더했을 때도 서버가 받은 값은 같았고 서명 대상과 달랐다. - -어긋나는 경우를 가르는 것은 권한부가 아니다. 권한부는 기본 포트를 적기만 해도, 사용자 정보가 붙기만 해도 호스트와 달라진다. 엔드포인트 여섯에 규칙을 적용해 보면 권한부와 갈리는 것이 다섯인데 전선의 값과 갈리는 것은 둘뿐이다. 적힌 포트가 스킴의 기본이 아닐 때에만 어긋난다. 이 표는 실행이 아니라 계산이다. - -두 번째 자리가 있다. 웹푸시 서명기가 대상 클레임을 스킴과 호스트로만 조립한다. 그 클레임은 규격상 출처이고 출처에는 기본이 아닌 포트가 들어간다. 원문에는 이 자리가 없다. - -지켜야 할 판정은 이미 저장소에 네 벌 쓰여 있다. 인바운드 웹의 콜백 URL 재구성과 출처 조립 둘, 그리고 아웃바운드 httpclient 의 정규 대상 하나다. 방향으로 갈리는 문제가 아니다. 그중 콜백 쪽 javadoc 은 이것을 틀리면 정상 웹훅이 전부 서명 실패로 뒤집힌다고 적는다. - -엔드포인트 가드에는 포트를 읽는 줄이 하나도 없다. 비표준 포트를 단 엔드포인트가 어디서도 걸리지 않는다는 뜻이다. SES 속성이 루프백 허용을 상수 참으로 넘기므로 프로파일로 좁혀지지도 않는다. - -이미 벌어지고 있는 일이기도 하다. 제공자 시험 대역이 임시 포트로 주소를 만들고, SES 와 웹푸시 계약 시험이 그 주소를 실제 매퍼에 그대로 먹인다. 검증하는 쪽이 없어 시험은 통과한다. 서명한 호스트 값을 단언하는 시험도 없다. - -수정은 두 자리 모두에 필요하다. - -판정은 P3 다. 근거가 둘이다. 하나는 SES 어댑터가 이 빌드에서 조립되지 않는다는 것이다. main 의 조립기는 SMTP 하나뿐이다. 다른 하나는 조립되더라도 실제 SES 가 443 이라 두 값이 같다는 것이다. - -조립되고, 엔드포인트가 비기본 포트이며, 그 엔드포인트가 서명을 검증하면 그 SES 프로파일로 나가는 발송이 전부 거절된다. 403 은 인가 실패로 분류되어 재시도도 대체 경로도 붙지 않는다. 그 조건에서는 P2 다. 첫째가 없으므로 나머지 둘은 이 빌드에서 물을 수조차 없다. 그래서 한 단계 내린다. 배선되지 않은 발견을 도달 가능한 등급에서 한 단계 내리는 것은 이 저장소가 websocket 리프에서 쓴 방식이다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 서명 대상 식과 전송 값 대조, 로컬 서버로 헤더 세 모양 왕복, 기본 포트만 생략하는 조립 자리 전수 검색 -소스 수정 : x - -## 재현 조건 - -이 사례의 프로브는 새로 만든 것이고 원문에는 없다. 원문 근거는 분석 문서의 #L831 절이다. - -1. 매퍼가 호스트를 꺼내는 식과 그 값이 들어가는 헤더 맵을 읽는다. -2. 요청이 실제로 싣는 헤더 목록을 읽는다. -3. 게이트웨이의 제한 목록에 호스트가 있는지 확인한다. -4. 로컬 서버를 띄우고 헤더 세 모양으로 요청을 보내 서버가 받은 값을 읽는다. -5. 엔드포인트마다 전선의 값과 권한부가 각각 서명 대상과 갈리는지 계산한다. -6. 엔드포인트 가드가 포트를 읽는 줄이 있는지 센다. -7. 임시 포트 주소를 실제 매퍼에 먹이는 시험 자리를 찾는다. -8. 출처나 호스트를 문자열로 조립하면서 기본 포트만 생략하는 자리를 전수로 찾는다. - -## 본문 - - - -SigV4 의 정규 요청은 실제로 전송되는 `Host` 헤더 값을 서명해야 한다. 매퍼가 서명하는 것은 다른 값이다. - -```java -// SesRequestMapper.java:103-110 - String host = properties.endpoint().getHost(); - - var signed = - signer.sign( - "POST", - PATH, - "", - Map.of("host", host, "content-type", "application/json"), -``` - -## 요청은 그 헤더를 싣지 않는다 - -:::evidence key="a13-f008-host" alt="코드베이스 정적 검색 출력 46줄. 매퍼가 서명 대상에 넣는 값과 요청에 싣는 헤더 넷, 게이트웨이의 제한 목록, 엔드포인트 가드에서 포트를 읽는 줄을 찾은 결과와 SES 속성이 그 가드를 부르는 줄, 임시 포트 주소를 만드는 대역과 그것을 먹이는 시험 자리 일곱, 같은 식의 다른 서명기, 기본 포트만 생략하는 조립 자리를 두 문자열로 검색한 결과가 차례로 보인다." caption="서명 대상을 정하는 자리와 그 값이 나가지 못하는 자리 — 46줄 · exit 0" zoom="true" -::: - -`SesRequestMapper:122-126`이 요청에 넣는 헤더는 `content-type`, `x-amz-date`, `x-amz-content-sha256`, `authorization` 넷이다. 서명한 `host` 는 거기 없다. 매퍼가 넣지 않아서이지 무엇이 걸러서가 아니다. - -이것은 매퍼가 넣지 않는다는 사실과 별개다. 누가 그 헤더를 공급하더라도 전선에 닿지 못한다. - -```java -// JdkNotificationHttpGateway.java:26-27, :54 - Set.of("connection", "content-length", "expect", "host", "upgrade"); - if (!RESTRICTED.contains(name)) { -``` - -## 세 가지 헤더 모양으로 보내 봤다 - -:::evidence key="a13-f008-host-wire" alt="JVM 프로브 출력 18줄. 첫 줄은 자바 판이고, 로컬 서버에 헤더 세 모양으로 요청을 보내 서버가 받은 Host 를 적은 결과와, 엔드포인트 여섯에 대해 전선의 값과 권한부가 각각 서명 대상과 갈리는지 표시한 표가 나온다. 포트는 실행마다 달라 자리표시로 바꿨다." caption="서버가 받은 Host 와 갈리는 조건 — 18줄 · exit 0" zoom="true" -::: - -앞 묶음이 왕복이다. 로컬 서버를 띄우고 같은 `JdkNotificationHttpGateway` 로 `POST` 를 세 번 보냈다. 첫 번째는 매퍼가 실제로 넣는 헤더 넷만, 두 번째는 거기에 `host` 를 더해서, 세 번째는 대문자 `Host` 로 더해서 보냈다. 셋 다 서버가 받은 값이 `127.0.0.1:<포트>` 였고 서명 대상 `127.0.0.1` 과 달랐다. - -둘째와 셋째는 매퍼가 하지 않는 일이다. 제한 목록이 실제로 거르는지, 그리고 대문자 키에도 걸리는지를 함께 재려고 일부러 더 넣었다. `NotificationHttpRequest` 가 정규 생성자에서 키를 소문자로 바꾸므로 대문자도 같은 자리에 걸린다. - -## 갈리는 조건은 권한부가 아니다 - -뒤 표가 그것이다. 전선의 값과 갈리는 엔드포인트는 `:8443` 과 `localhost:4566` 둘뿐인데, 권한부와 갈리는 것은 다섯이다. 이 표는 왕복이 아니라 `ExternalRequestUrlResolver` 의 판정을 엔드포인트마다 적용한 계산이다. - -`https://email.eu-central-1.amazonaws.com:443` 은 기본 포트를 적었을 뿐이라 전선의 값은 호스트 그대로다. `http://127.0.0.1:80` 도 같다. `https://user@email.example.com` 은 포트가 아예 없는데 사용자 정보 때문에 권한부가 달라진다. 세 경우 모두 서명은 어긋나지 않는다. - -어긋나는 것은 포트가 적혀 있고 그 포트가 스킴의 기본이 아닌 엔드포인트뿐이다. - -## 옳은 식은 이미 저장소에 있다 - -```java -// ExternalRequestUrlResolver.java:57-63 - boolean defaultPort = - port < 0 - || ("https".equalsIgnoreCase(scheme) && port == 443) - || ("http".equalsIgnoreCase(scheme) && port == 80); - if (!defaultPort) { -``` - -그 클래스의 javadoc 이 이유를 적어 뒀다. - -> Several providers sign the request URL, so getting this wrong turns every valid webhook into a signature failure. - -같은 판정이 `ExternalOrigin:43`, `ExternalRequestContext:57`, 그리고 아웃바운드 httpclient 의 `CanonicalTarget:39` 에도 있다. 자료의 마지막 묶음은 `port == 443` 과 `port == 80` 두 문자열로 훑은 것이다. 네 자리가 모두 포트 비교를 `port == 443` 과 `port == 80` 으로 적어서 이 검색에 다 잡힌다. 스킴 비교는 셋이 서로 다른 형태다. 삼항으로 기본값을 고르는 형태는 걸리지 않으므로, 이 넷은 기본 포트를 판정하는 자리 전부가 아니라 출처를 문자열로 조립하면서 기본 포트만 생략하는 자리 전부다. 인바운드와 아웃바운드로 갈리지 않는다. 규칙을 안 쓰는 것이 이 알림 리프의 두 서명 자리다. - -## 같은 어긋남이 한 자리 더 있다 - -```java -// VapidJwtSigner.java:50 - String audience = endpoint.getScheme() + "://" + endpoint.getHost(); -``` - -VAPID 의 `aud` 는 푸시 서비스의 출처이고, 출처는 기본이 아닌 포트를 포함한다. 원문은 SES 한 자리를 셌다. - -## 가드는 포트를 보지 않는다 - -자료의 네 번째 묶음이 그것이다. `NotificationEndpoints` 에는 `getPort` 를 읽는 줄이 없다. 그래서 라우팅 가능한 https 엔드포인트에 `:8443` 을 달아도 어느 프로파일에서든 통과한다. - -```java -// SesProviderProperties.java:27 - NotificationEndpoints.requireExternallyRoutable(endpoint, "SES endpoint", true); -``` - -세 번째 인자는 상수 참이다. `NotificationEndpoints:61`의 javadoc 이 그 인자를 로컬과 계약 프로파일용이라고 적지만, 그 범위를 강제하는 코드는 없다. - -## 이 어긋남은 오늘 이미 일어난다 - -```java -// ProviderFaultHarness.java:52 - return URI.create("http://127.0.0.1:" + server.getAddress().getPort()); -``` - -`SesNotificationProviderAdapterTest:254` 와 `ContractAdapters:102` 가 그 주소를 실제 `SesRequestMapper` 에 넘긴다. 웹푸시 쪽은 `WebPushProviderAdapterTest:173` 과 `ContractAdapters:264` 가 같은 주소로 `aud` 를 만든다. 자료의 나머지 줄은 호스트를 서명하지 않는 어댑터들이다. 대역이 서명을 검증하지 않으므로 초록으로 지나갈 뿐이다. - -## 확인하지 못한 것 - -서명을 검증하는 대역을 띄워 거절 응답까지 받아 보지는 않았다. 잰 것은 왕복 하나이고, 엔드포인트별 갈림은 계산이다. 웹푸시 쪽은 식과 규격만 읽었고 실제 토큰을 만들어 대조하지는 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/notification-and-delivery/case/case-a13-f009-sigv4-string.md b/docs/clean-architecture-backend-template/tech-log-studio/notification-and-delivery/case/case-a13-f009-sigv4-string.md deleted file mode 100644 index a2db259..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/notification-and-delivery/case/case-a13-f009-sigv4-string.md +++ /dev/null @@ -1,206 +0,0 @@ ---- -kind: CASE -slug: a13-f009-sigv4-string -title: 지운다고 약속한 사본은 아무도 받지 않고, 서명 자리가 받는 사본은 지울 수 없다 -topic: notification-and-delivery -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a13-f009-sigv4-string -evidenceCapturedOn: 2026-09-02 -body: case-a13-f009-sigv4-string.body.md -assets: - - key: a13-f009-sigv4-string - file: ../../../final/evidence/rendered/a13-f009-sigv4-string.svg - - key: a13-f009-sigv4-string-heap - file: ../../../final/evidence/rendered/a13-f009-sigv4-string-heap.svg -evidence: - - ../../../final/evidence/raw/a13-f009-sigv4-string.txt - - ../../../final/evidence/raw/a13-f009-sigv4-string-heap.txt -source: - - 원본 분석 절은 final/document.md#a13#L844 이다. ---- - -# 지운다고 약속한 사본은 아무도 받지 않고, 서명 자리가 받는 사본은 지울 수 없다 - -이 리프에는 비밀의 모양이 둘이다. 닫으면 자신을 0 으로 덮는 핸들과, 닫기가 없는 키 자재 복제다. 핸들을 받는 인터페이스의 구현은 시험 소스에만 있고, 서명 자리가 실제로 받는 것은 복제 쪽이다. - -## 관계 - -- **포트를 뺀 호스트를 서명하는 자리가 둘이고, 같은 저장소의 네 자리는 그 포트를 지킨다** - 같은 SigV4 경로의 다른 자리다. -- **비밀을 힙에서 지우는 마지막 단계가 종료 경로에 연결되지 않았다** - 지우는 장치가 있는데 그 손이 닿지 않는다는 점이 같다. -- **정책을 담은 필드가 참이 된 적도 읽힌 적도 없다** - 선언한 보호 장치가 실제 경로에 없다는 점이 같다. - -## 문제 - -원문은 서명 키 파생 한 줄이 지울 수 있는 비밀 사본을 불변 문자열로 올린다고 적었다. 그 사본이 정말 그 자리까지 오는지, 그리고 힙에 무엇이 남는지 확인했다. - -## 결론 - -오지 않는다. 두 모양이 서로 만나지 않는다. - -지운다고 약속한 쪽은 핸들이다. 자바독이 닫을 때 자신을 지우는 연산 범위 사본이라고 적고, 닫기가 배열과 문자 배열을 각각 0 으로 채운다. 그 핸들을 받는 것은 제공자 시도 클라이언트 인터페이스다. 그 인터페이스를 구현하는 일곱 자리가 모두 시험 쪽에 있다. 핸들을 여는 두 어댑터도 main 에 있지만 main 에서 조립되지 않는다. - -복제를 받는 길을 가르는 것은 목적이다. 제공자 계정 자격증명만 관리자를 거친다. 세대 범위가 붙은 유일한 비밀이라서다. 나머지 목적은 전부 제공자를 바로 조회하고, 웹훅 요청 서명과 VAPID 서명도 그 길을 탄다. 어느 길이든 돌아오는 것은 키 자재 레코드의 접근자가 만든 복제 배열이다. 그 레코드는 하나를 보관하고, 접근자가 불릴 때마다 한 벌씩 더 만든다. 지우는 메서드는 없다. 닫기가 사본에 닿지 못하는 것이 아니라 닫을 주체가 없다. - -그 복제가 불변 문자열이 되는 자리는 셋이다. SES 서명 키 파생, Twilio 요청 매퍼, Twilio 조정 능력이다. 원문은 첫 자리만 셌다. - -셋 모두 바이트를 문자열로 만들어 이어 붙인다. SES 는 그것을 다시 바이트로 되돌리고, Twilio 둘은 base64 문자열로 만든다. - -힙에 무엇이 남는지 네 모드로 재봤다. - -기준선은 둘과 하나다. 그 하나는 아무도 지울 수 없는 쪽이다. 다만 프로브는 받은 배열을 지운다. 실제 세 호출자는 그 배열을 인자 자리에 바로 넘기므로 지울 지역 변수조차 없고, 접근자는 부를 때마다 새 복제를 만든다. - -서명을 지나면 거기서 넷이 늘었다. - -바이트로만 이으면 그 넷 가운데 둘이 사라진다. 이은 배열까지 지우면 셋이다. 두 모드에 공통으로 살아남는 것이 키 명세가 만드는 복제다. - -이웃 하나는 같은 자리에서 다른 선택을 했다. Twilio 서명 검증기는 인증 토큰을 바이트 그대로 키 명세에 넣는다. 다만 그 토큰은 다른 비밀이다. 콜백 서명 목적으로 조회되고 목적 검사까지 받는다. - -SES 서명기 자신도 키 명세를 만든다. 거기 들어가는 배열이 바로 그 문자열에서 나온 것이다. - -기능 쪽 위험도 하나 있다. 실제 AWS 키는 아스키라 왕복이 값을 바꾸지 않지만, 코드는 그것을 요구하지 않는다. 비아스키 바이트가 섞이면 파생 키가 조용히 달라진다. - -판정은 P3 다. 근거가 둘이다. 힙에 닿을 수 있어야 회수된다는 것, 그리고 세 자리를 담은 클래스 가운데 둘은 시험 소스에서만 만들어지고 나머지 하나는 어디에서도 만들어지지 않는다는 것이다. - -따르는 선례는 websocket 리프다. 거기서 도달 가능한 등급을 한 단계 내렸다. 배선되지 않았다는 이유로 P3 을 바로 매긴 grpc 리프는 따르지 않는다. grpc 리프는 런타임 소속이 비어 있고 이 리프는 app-bootstrap 에 올라 있다. 소속이 등급을 가른다는 규칙이 이 저장소에 따로 적혀 있다. - -그래서 완화가 선례보다 약해 보인다. websocket 쪽은 기계가 강제하는 빌드 전용 등급을 근거로 한 단계를 낮췄는데, 여기서는 조립기가 없다는 것뿐이고 그 부재는 포크가 채우라고 기동 메시지가 직접 권한다. - -그래도 한 단계를 낮추는 근거는 선례와 같다. websocket 선례가 실제로 물은 것은 소속이 아니라 출하되는 조합에서 그 시나리오가 일어날 수 있느냐였다. 조립기 없는 제공자 프로필을 설정하면 기동이 실패하므로, 여기서도 출하되는 어떤 조합에서도 이 실패는 일어나지 않는다. 조립되면 P2 다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 비밀 두 모양의 호출 그래프 추적, 네 모드를 새 JVM 에서 각각 실행하고 힙 덤프 두 종 -소스 수정 : x - -## 재현 조건 - -이 사례의 프로브는 새로 만든 것이고 원문에는 없다. 원문 근거는 분석 문서의 #L844 절이다. - -1. 핸들의 자바독과 닫기 구현을 읽는다. -2. 그 핸들을 받는 인터페이스의 구현을 이름 있는 것과 익명인 것 모두 센다. -3. 서명 자리에 비밀을 넘기는 호출을 거꾸로 따라간다. -4. 그 끝에 있는 키 자재 레코드가 사본을 어떻게 만드는지 읽는다. -5. 그 사본을 문자열로 올리는 자리를 리프에서 찾는다. -6. 레코드를 실제 경로와 같게 만들고 힙을 두 종류로 뜬다. -7. 받기만 하는 모드, 바이트로만 잇는 모드, 이은 배열까지 지우는 모드, 서명하는 모드를 하나씩 돌린다. -8. 서명 검증기가 쓰는 비밀의 목적이 무엇인지 확인한다. - -## 본문 - - - -핸들의 존재 이유가 한 줄에 적혀 있다. - -```java -// NotificationSecretMaterialHandle.java:7, :60-70 -/** Versioned operation-scoped mutable secret copy that wipes itself on close. */ - public synchronized void close() { - if (!closed) { - if (bytes != null) { - Arrays.fill(bytes, (byte) 0); - } - if (characters != null) { - Arrays.fill(characters, '\0'); - } - closed = true; - } - } -``` - -## 그 사본은 서명 자리에 오지 않는다 - -:::evidence key="a13-f009-sigv4-string" alt="코드베이스 정적 검색 출력 62줄. 핸들의 약속과 지우는 줄, 그 핸들을 받는 인터페이스의 구현 일곱, 자격증명 관리자와 키 자재 레코드가 사본을 만드는 줄, 그 사본을 서명 자리로 넘기는 세 자리, 문자열로 올리는 세 자리, 그 문자열이 키 명세로 들어가는 줄, 제공자를 바로 조회하는 열세 줄과 그중 관리자 자신의 둘, 제공자 자격증명 목적을 푸는 두 줄, 서명 검증기가 쓰는 비밀의 목적이 차례로 보인다." caption="두 비밀 모양과 그 끝 — 62줄 · exit 0" zoom="true" -::: - -핸들을 받는 것은 `NotificationProviderAttemptClient` 다. 그 인터페이스를 구현하는 자리는 저장소에 일곱이고 전부 시험 소스다. 이름 있는 것은 `NotificationReconciliationAdapterTest:164` 하나이고 나머지 여섯은 익명 클래스다. 메서드가 둘이라 `implements` 만 찾는 검색으로는 여섯이 보이지 않는다. - -서명 자리가 받는 것은 다른 길에서 온다. - -```java -// ProviderCredentialManager.java:145-153 - public byte[] materialFor(ProviderProfileId profileId, long generation) { - ... - if (active.generation() == generation) { - return material(active).material(); -``` - -```java -// SecretKeyMaterial.java:20, :23-26 - material = material.clone(); - @Override - public byte[] material() { - return material.clone(); - } -``` - -레코드가 생성자에서 한 벌을 보관하고 접근자가 부를 때마다 한 벌을 더 만든다. 어느 쪽에도 닫기가 없다. `SesNotificationProviderAdapter:136`, `TwilioSmsProviderAdapter:98`, `TwilioReconciliationCapability:125` 가 그 배열을 인자 자리에 그대로 넘긴다. 지울 지역 변수를 두는 자리는 하나도 없다. - -이 길을 타는 것은 제공자 계정 자격증명뿐이다. 목적을 `PROVIDER_CREDENTIAL` 로 확인하는 자리가 `ProviderCredentialManager:199` 와 `:224` 둘뿐이고 둘 다 관리자 안이다. 검사 없이 그 목적을 바로 조회하는 자리는 자료가 세어 본 대로 0 이다. - -나머지 목적은 비밀 자재 제공자를 바로 조회한다. 자료의 그 묶음이 열세 줄인데 둘은 관리자 자신의 것이므로 밖에서 부르는 것은 열하나다. 웹훅 요청 서명(`WebhookNotificationProviderAdapter:220`)과 VAPID 서명(`VapidKeyRegistry:67`)도 제공자를 바로 조회하므로, 서명이냐 검증이냐로 갈리지 않는다. 아래에서 볼 검증기의 토큰도 여기서 온다. - -## 그 배열을 문자열로 올리는 자리가 셋이다 - -```java -// AwsSignatureV4Signer.java:117-119 - byte[] key = - ("AWS4" + new String(secretAccessKey, StandardCharsets.UTF_8)) - .getBytes(StandardCharsets.UTF_8); -``` - -```java -// TwilioRequestMapper.java:41-45 - String credentials = - Base64.getEncoder() - .encodeToString( - (properties.accountSid() + ":" + new String(authToken, StandardCharsets.UTF_8)) - .getBytes(StandardCharsets.UTF_8)); -``` - -`TwilioReconciliationCapability:119-128` 이 같은 식을 한 번 더 쓴다. 다른 점은 바이트의 출처가 `credentials.materialFor(...)` 라고 그 자리에 직접 적혀 있다는 것뿐이다. - -Twilio 두 자리는 SES 보다 넓다. 문자열이 되는 것이 토큰 하나가 아니라 `accountSid:token` 전체이고, 그것을 base64 로 부호화한 문자열도 함께 남는다. - -## 힙에 몇 벌이 남는가 - -:::evidence key="a13-f009-sigv4-string-heap" alt="JVM 프로브 출력 7줄. 키 자재 레코드에서 받은 사본을 지운 뒤 힙 덤프 두 종에서 비밀 표식이 몇 번 나오는지를 네 모드에 대해 적었다. 표식은 아스키 코드로 조립한 합성 값이다." caption="네 모드에서 힙에 남은 자격증명 사본 — 7줄 · exit 0" zoom="true" -::: - -실제 경로와 같은 모양으로 레코드를 만들고, 접근자가 준 배열을 쓴 뒤 지웠다. 네 모드를 각각 새 JVM 에서 돌렸다. - -받아서 지우기만 한 모드가 기준선이다. 모든 객체 덤프에서 둘, 살아 있는 객체 덤프에서 하나다. 그 하나는 레코드가 계속 들고 있는 사본이다. - -서명 키 파생을 지난 모드는 여섯이다. 기준선보다 넷이 많다. - -문자열을 거치지 않고 바이트로만 이은 모드는 넷이다. 늘어난 넷 중 둘이 없어진 것이다. 원문이 제안하는 수정이 딱 여기까지다. - -그 배열까지 지운 모드는 셋이다. 그 지우기는 제안에 없는 별도의 한 걸음이고, 지금 서명기도 파생 배열을 지우지 않는다. 어느 쪽이든 남는 하나는 `SecretKeySpec` 이 만드는 복제다. 두 모드 모두 HMAC 네 바퀴를 서명기와 똑같이 돈다. - -프로브는 비밀을 문자열 리터럴로 적지 않는다. 아스키 코드 배열로 조립한다. 찾을 바이트열도 덤프를 뜬 뒤에 만든다. 둘 다 프로브 자신이 세어지는 것을 막기 위한 것이다. - -## 이웃이 쓰는 것은 다른 비밀이다 - -```java -// TwilioSignatureValidator.java:36 - mac.init(new SecretKeySpec(authToken, "HmacSHA1")); -``` - -바이트를 그대로 넣는다. 다만 이 `authToken` 은 매퍼가 쓰는 것과 다른 비밀이다. `TwilioCallbackAdapter:78-79` 가 콜백 서명 키 참조로 조회하고 목적이 `CALLBACK_SIGNING` 인지 검사한다. 매퍼 쪽은 `PROVIDER_CREDENTIAL` 이다. - -키 명세를 만드는 자리가 전부 바이트를 쓴다고도 말할 수 없다. `AwsSignatureV4Signer:129` 도 키 명세를 만드는데, 거기 들어가는 `key` 가 `:117-119` 의 문자열에서 나온 배열이다. - -## 값이 상할 여지 - -AWS 가 발급하는 비밀 키는 아스키라 실제로는 UTF-8 왕복이 값을 바꾸지 않는다. 코드가 그것을 강제하지는 않는다. 자격증명은 base64 블롭에서 바인딩되고 길이만 검사받으므로, 0x80 이상 바이트가 들어오면 왕복이 그것을 대체 문자로 바꾸고 서명 키가 조용히 달라진다. - -## 확인하지 못한 것 - -덤프에 남은 것들의 객체 정체는 코드를 읽어 짚었고 덤프 안에서 확인하지는 않았다. 프로브가 센 것은 바이트열의 출현 횟수다. Twilio 두 자리는 정적으로만 읽었고 힙으로 재현하지는 않았다. 0x80 이상 바이트가 실제로 서명을 깨는지도 재현하지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/notification-and-delivery/case/case-analysis-finding-a13-f007.md b/docs/clean-architecture-backend-template/tech-log-studio/notification-and-delivery/case/case-analysis-finding-a13-f007.md deleted file mode 100644 index 36025a3..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/notification-and-delivery/case/case-analysis-finding-a13-f007.md +++ /dev/null @@ -1,122 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a13-f007 -title: '"상한을 두고 읽는다"는 본문 핸들러가 전부 읽은 뒤에 자른다' -topic: notification-and-delivery -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a13-f007 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a13-f007.body.md -assets: - - key: analysis-finding-a13-f007 - file: ../../../final/evidence/rendered/analysis-finding-a13-f007.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a13-f007.txt -source: - - 원본 분석 절은 final/document.md#a13#L795 이다. ---- - -# "상한을 두고 읽는다"는 본문 핸들러가 전부 읽은 뒤에 자른다 - -자바독이 상한에서 읽기를 멈춘다고 적는다. 구현은 무제한 상류 구독자의 결과에 자르기 함수를 얹는다. 자르기는 전체가 힙에 모인 다음에 일어난다. 호출부 주석이 쓰지 말라고 명시한 바로 그 상류를 두 줄 아래 함수가 쓴다. - -## 관계 - -- **음수 Retry-After 가 확정 거절을 영구 중지되는 모호한 제출로 바꾼다** - 같은 리프의 다른 원격 입력 사례다. -- **이름이 상한을 말한다고 상한이 걸리는 것은 아니다** - 이 사례가 그 규칙의 형태다. -- **두 연산자가 이름만 있고 아무것도 하지 않는다** - 같은 계열의 서술과 코드 어긋남이다. - -## 문제 - -응답 본문을 읽는 처리기가 있다. 자바독이 상한에서 읽기를 멈추는 본문 처리기라고 적는다. - -호출부 주석도 이유를 길게 적는다. - -제공자 응답은 진단이라는 것이다. 상태와 헤더 몇 개와 오류 문서라는 것이다. 상한 없이 읽으면 보내는 쪽의 힙이 저쪽이 보내기로 한 양의 함수가 된다는 것이다. 끝나지 않는 조각 응답은 요청 하나짜리 장애라는 것이다. - -## 결론 - -구현이 그것을 하지 않는다. - -바이트 배열 구독자를 상류로 두고 그 결과에 자르기 함수를 얹는다. - -사상 구독자의 마무리 함수는 상류가 완료된 뒤 그 결과에 적용된다. - -상류는 무제한으로 요청하여 본문 전체를 힙에 모은다. 자르기는 그다음이다. - -호출부 주석이 막겠다고 선언한 것과 정확히 반대다. - -주석은 그 상류를 쓰지 말라고 쓰여 있고, 두 줄 아래 함수가 그것을 상류로 쓴다. - -실패 시나리오는 이렇다. - -제공자나 그 자리에 들어온 무엇이든 응답으로 큰 본문을 빠르게 보낸다. - -요청 시간 제한이 시간은 제한하므로 끝나지 않는 조각 응답은 시간 제한에서 끊긴다. - -그러나 주석이 두 번째로 든 위험은 그대로다. 보내는 쪽의 힙이 저쪽이 보내기로 한 양의 함수가 되는 상황이다. - -시간 제한 안에 수 기가바이트를 받을 수 있는 연결에서는 그만큼이 전부 배열로 쌓인 뒤 상한 크기로 잘린다. - -동시 발송이 많을수록 배수로 늘어난다. 이 리프는 발송 작업자가 가상 스레드로 퍼져 나간다. - -판정은 P2 다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 구현과 자바독, 호출부 주석 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/173 계열에 있다. - -1. 본문 처리기의 자바독을 읽는다. -2. 구현이 어떤 상류 구독자를 쓰는지 확인한다. -3. 사상 구독자의 마무리 함수가 언제 적용되는지 확인한다. -4. 호출부 주석을 읽는다. -5. 주석이 배제한 상류와 구현의 상류를 대조한다. - -## 본문 - - - -본문 핸들러는 다음이다. - -```java -// JdkNotificationHttpGateway.java:116-128 -/** - * A body handler that stops reading at the cap. - */ -private static HttpResponse.BodyHandler boundedBody(int maxBytes) { - return responseInfo -> - HttpResponse.BodySubscribers.mapping( - HttpResponse.BodySubscribers.ofByteArray(), - body -> body.length <= maxBytes ? body : java.util.Arrays.copyOf(body, maxBytes)); -} -``` - -## 본문 핸들러가 도는 순서 - -:::evidence key="analysis-finding-a13-f007" alt="분석 문서 final/document.md#a13 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a13 발췌 — 15줄" zoom="true" -::: - -## finisher는 upstream이 끝난 뒤에 돈다 - -`BodySubscribers.mapping(upstream, finisher)`의 finisher는 upstream이 완료된 뒤 그 결과에 적용된다. upstream은 `ofByteArray()`이고, 그것은 무제한으로 요청하여 본문 **전체를 힙에 모은다**. 잘라내기는 그 다음이다. - -## 호출부 주석이 선언한 것과 정확히 반대다 - -`:60-62` — "Bounded, not ofByteArray(). A provider response is diagnostic — a status, some headers, an…". P2. - -## 확인하지 못한 것 - -큰 응답을 보내는 가짜 제공자로 힙 증가를 측정하지 않았다. 구독자 규약상 그 결과가 나온다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/notification-and-delivery/case/case-analysis-finding-a13-f010.md b/docs/clean-architecture-backend-template/tech-log-studio/notification-and-delivery/case/case-analysis-finding-a13-f010.md deleted file mode 100644 index 6143c32..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/notification-and-delivery/case/case-analysis-finding-a13-f010.md +++ /dev/null @@ -1,126 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a13-f010 -title: FCM만 "커밋 후 응답 손실 = ambiguous" 규칙 밖에 있고, 그 FCM이 두 계약 집합 어디에도 없다 -topic: notification-and-delivery -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a13-f010 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a13-f010.body.md -assets: - - key: analysis-finding-a13-f010 - file: ../../../final/evidence/rendered/analysis-finding-a13-f010.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a13-f010.txt -source: - - 원본 분석 절은 final/document.md#a13#L957 이다. ---- - -# FCM만 "커밋 후 응답 손실 = ambiguous" 규칙 밖에 있고, 그 FCM이 두 계약 집합 어디에도 없다 - -공통 결과 변환기의 자바독이 모든 HTTP 제공자 어댑터가 전송 실패를 여기로 보낸다고 적는다. 한 제공자 경로는 그것을 지나지 않는다. 그리고 그 경로는 배치라 한 번의 손실이 최대 배치 크기만큼의 배달에 동시에 영향을 준다. - -## 관계 - -- **재시도 안전은 증거로 결정된다** - 이 규칙이 존재하는 이유다. -- **상한을 두고 읽는다는 본문 handler가 전부 읽은 뒤에 자른다** - 같은 리프의 다른 P2 다. -- **모든 어댑터가 지난다고 적힌 통로에 예외가 있으면 규칙은 한 번 쓰인 것이 아니다** - 이 사례가 그 규칙의 형태다. - -## 문제 - -공통 결과 변환기의 존재 이유가 클래스 자바독에 쓰여 있다. - -모든 HTTP 제공자 어댑터가 전송 실패를 여기로 보내므로, 본문이 커밋된 뒤 응답이 없으면 모호하다는 규칙이 제공자마다 다시 유도되지 않고 한 번만 쓰인다는 것이다. - -모든 어댑터가 실제로 지나는지 확인했다. - -## 결론 - -한 경로가 지나지 않는다. - -세 계층 전부에 번역도 방어도 없다. - -게이트웨이 심은 실패 계약을 선언하지 않은 함수형 인터페이스다. 배치를 보내고 배치 결과를 돌려준다. - -조정자의 제출 메서드에는 잡기도 없고 전송 변환 호출도 없다. 게이트웨이를 부르고 크기를 비교한 뒤 어긋나면 상태 예외를 던진다. - -어댑터는 조정자 결과를 그대로 통과시킨다. - -전송 예외 타입도 본문 커밋 비트도 이 경로에는 존재하지 않는다. - -다른 여섯 어댑터가 모두 전송 예외를 잡아 공통 변환기로 보내고 모호 결과와 응답 손실 증거를 만드는 자리에서, 이 경로는 구현이 던지는 임의의 실행 예외를 그대로 올려보낸다. - -실패 시나리오는 이렇다. - -다중 수신 요청의 본문이 기록된 뒤 연결이 끊긴다. - -배치가 최대 배치 크기만큼의 수신자를 담고 있으므로 한 번의 손실이 그만큼의 배달에 동시에 영향을 준다. - -결과는 모호가 아니라 분류되지 않은 예외다. - -일정 작업자의 포괄 잡기가 예외 타입을 사유로 로그를 남기고 임차를 만료시킨다. - -임차 회수 서비스가 미완 시도를 보고, 증명 가능하게 시작되지 않았다가 거짓이므로 재대사로 넘긴다. - -판정은 P2 다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 어댑터별 전송 실패 경로 전수 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/173 계열에 있다. - -1. 공통 결과 변환기의 클래스 자바독을 읽는다. -2. 어댑터 목록을 만들고 각각의 전송 실패 처리를 확인한다. -3. 예외 경로에 잡기가 있는지 본다. -4. 전송 예외 타입과 커밋 비트가 그 경로에 존재하는지 검색한다. -5. 그 경로가 배치인지 단건인지 확인한다. - -## 본문 - - - -`ProviderResults`의 존재 이유가 클래스 javadoc에 쓰여 있다. - -> "**Every HTTP provider adapter routes its transport failures through here**, so the rule that 'committed body plus no response equals ambiguous' is written once rather than re-derived per provider." - -## ProviderResults 참조 위치 - -:::evidence key="analysis-finding-a13-f010" alt="코드베이스에서 ProviderResults 를 검색한 출력 10줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ProviderResults 코드베이스 검색 — 10줄 · exit 0" zoom="true" -::: - -## FCM 경로는 그것을 지나지 않는다 - -세 계층 전부에 번역도 방어도 없다. - -```java -// FcmGateway.java — 실패 계약이 선언되지 않은 심 -@FunctionalInterface -public interface FcmGateway { - FcmBatchResult sendBatch(List> messages); -} - -// FcmBatchCoordinator.submit — try/catch 없음, fromTransport 없음 -FcmBatchResult batch = gateway.sendBatch(messages); -if (batch.items().size() != submissions.size()) { throw new IllegalStateException(...); } - -// FcmNotificationProviderAdapter — 그대로 통과 -return coordinator.submit(submissions); -``` - -그리고 그 FCM이 두 계약 집합 어디에도 없다. P2. - -## 확인하지 못한 것 - -배치 전송 중 연결을 끊어 실제 흐름을 재현하지 않았다. 경로 구조상 그 결과가 나온다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/notification-and-delivery/case/case-analysis-finding-a13-f011.md b/docs/clean-architecture-backend-template/tech-log-studio/notification-and-delivery/case/case-analysis-finding-a13-f011.md deleted file mode 100644 index 8b57a5e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/notification-and-delivery/case/case-analysis-finding-a13-f011.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a13-f011 -title: 공유 provider 계약이 8종 중 3종에서만 상속되고, 강제 장치가 없다 -topic: notification-and-delivery -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a13-f011 -evidenceCapturedOn: 2026-09-01 -assets: - - key: analysis-finding-a13-f011 - file: ../../../final/evidence/rendered/analysis-finding-a13-f011.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a13-f011.txt -source: - - 원본 분석 절은 final/document.md#a13#L990 이다. ---- - -# 공유 provider 계약이 8종 중 3종에서만 상속되고, 강제 장치가 없다 - -계약 인터페이스의 자바독이 새 제공자는 같은 세 질문에 답하지 않고는 추가될 수 없다고 약속한다. 여덟 어댑터 중 셋만 그것을 상속하고, 그 약속을 지키는 장치가 없다. 같은 실패 양식에 대한 구조적 강제를 이 저장소는 이미 두 번 만들었다. - -## 관계 - -- **한 제공자만 커밋 후 응답 손실은 모호하다 규칙 밖에 있고 그 제공자가 두 계약 집합 어디에도 없다** - 이 계수에서 유일하게 확인되지 않는 제공자다. -- **문서가 선언한 경계는 코드가 닫아야 경계다** - 이 사례가 그 규칙의 형태다. -- **위반 코드 34종 중 22종이 어떤 test에서도 이름으로 확인되지 않는다** - 같은 계열의 강제 장치 부재다. - -## 문제 - -제공자 어댑터 계약 인터페이스가 성질을 자바독으로 약속한다. - -새 제공자는 같은 세 질문에 답하지 않고는 추가될 수 없다는 것이다. - -여덟 어댑터가 그것을 상속하는지 셌다. - -## 결론 - -셋만 상속한다. - -그리고 그 약속을 지키는 장치가 없다. 상속하지 않아도 빌드가 통과한다. - -이 저장소는 같은 실패 양식에 대해 구조적 강제를 이미 두 번 만들었다. 엔드포인트 가드 호출 지점 테스트와 알림 API 표면 검증 태스크다. - -그러므로 형태는 이미 있다. 적용되지 않았을 뿐이다. - -다만 실질 커버리지는 상속보다 넓다. - -셋째 질문인 응답이 오지 않을 때에 대해서는 두 제공자가 자체 테스트에서 모호 결과를 확인하고, 하나는 교차 제공자 묶음이 확인한다. - -확인되지 않는 유일한 제공자가 배치 경로의 그것이고, 그것이 별도 사례로 기록되어 있다. - -판정은 P3 다. - -## 검증 환경 - -확인 방식 : 계약 상속 계수와 강제 장치 검색 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/173 계열에 있다. - -1. 계약 인터페이스의 자바독 약속을 읽는다. -2. 어댑터 목록을 만들고 상속 여부를 센다. -3. 상속을 강제하는 테스트나 빌드 태스크가 있는지 검색한다. -4. 이 저장소의 다른 구조적 강제 장치를 확인한다. -5. 셋째 질문의 실질 커버리지를 제공자별로 확인한다. - -## 본문 - - - -`ProviderAdapterContract`의 javadoc이 약속하는 성질("a new provider cannot be added without answering the same three questions")을 지키는 장치가 없다(§28.1·§28.3). 8종 중 3종에서만 상속된다. - -## ProviderAdapterContract 참조 위치 - -:::evidence key="analysis-finding-a13-f011" alt="코드베이스에서 ProviderAdapterContract 를 검색한 출력 4줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ProviderAdapterContract 코드베이스 검색 — 4줄 · exit 0" zoom="true" -::: - -## 형태는 이미 있다 - -이 저장소는 같은 실패 양식에 대해 `EndpointGuardCallSiteTest`와 `verifyNotificationApiSurface`라는 구조적 강제를 이미 두 번 만들었다 — 적용되지 않았을 뿐이다. - -## 확인하지 못한 것 - -상속하지 않는 제공자를 새로 추가해 빌드가 통과하는지 실행하지 않았다. 강제 장치 부재상 그 결과가 나온다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/objectstorage-staged-lifecycle/case/case-endpoint-check-only-on-upload.md b/docs/clean-architecture-backend-template/tech-log-studio/objectstorage-staged-lifecycle/case/case-endpoint-check-only-on-upload.md deleted file mode 100644 index 4f9ccc0..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/objectstorage-staged-lifecycle/case/case-endpoint-check-only-on-upload.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -kind: CASE -slug: endpoint-check-only-on-upload -title: 서명된 grant의 endpoint 검증이 upload 경로에만 있다 -topic: objectstorage-staged-lifecycle -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:endpoint-check-only-on-upload -evidenceCapturedOn: 2026-09-01 -assets: - - key: endpoint-check-only-on-upload - file: ../../../final/evidence/rendered/endpoint-check-only-on-upload.svg -evidence: - - ../../../final/evidence/raw/endpoint-check-only-on-upload.txt -source: - - 원본 분석 절은 final/document.md#a09 §39 이다. ---- - -# 서명된 grant의 endpoint 검증이 upload 경로에만 있다 - -허가가 지목한 엔드포인트가 실제 요청 대상과 같은지 확인하는 검사가 업로드 경로에만 있다. 같은 허가를 쓰는 다른 경로에는 그 검사가 없다. - -## 관계 - -- **staged lifecycle과 서명된 grant** - 이 허가 체계의 개념이다. -- **단일 admission point는 우회 경로를 세어야 성립한다** - 검증 지점을 세는 같은 계열의 규칙이다. -- **직접 multipart의 마지막 part는 grant를 받을 수 없다** - 같은 허가 체계의 다른 공백이다. - -## 문제 - -서명된 허가는 어느 엔드포인트에 대한 것인지를 담는다. 그 값이 있는 이유는 한 엔드포인트용 허가가 다른 엔드포인트에 쓰이지 않게 하기 위해서다. - -그 검사가 어디에 있는지를 세면 구조가 드러난다. - -## 결론 - -업로드 경로에만 있다. - -허가 검증 지점을 전수로 확인하면, 엔드포인트를 요청 대상과 대조하는 검사가 업로드에만 있고 나머지 경로에는 없다. - -허가의 다른 필드들은 여러 경로에서 확인된다. 유효 기간과 연산 대상 같은 것들이다. 엔드포인트만 비대칭이다. - -이 비대칭의 비용은 허가의 범위가 경로마다 다르다는 것이다. 업로드용으로 발급된 허가가 다른 경로에서는 엔드포인트 제한 없이 통한다. - -검증 지점이 여럿인 구조에서 흔한 형태다. 새 경로를 추가할 때 앞 경로의 검사 목록을 전부 옮겨오지 않으면 이런 차이가 생기고, 각 경로만 보면 각자 합리적으로 보인다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 허가 검증 지점 전수 확인 -소스 수정 : x - -## 재현 조건 - -1. 허가를 검증하는 지점을 리프 전체에서 찾는다. -2. 각 지점이 확인하는 필드 목록을 만든다. -3. 엔드포인트 대조가 있는 지점과 없는 지점을 구분한다. - -## 본문 - - - -grant가 endpoint를 바인딩하는데 그 검증이 upload 경로에만 있고 다른 경로에는 없다. - -## grant 가 바인딩하는 endpoint - -:::evidence key="endpoint-check-only-on-upload" alt="분석 문서 final/document.md#a09 에서 이 기록의 근거 절을 그대로 잘라낸 10줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a09 발췌 — 10줄" zoom="true" -::: - -## 서명이 endpoint를 덮는 이유가 무력화된다 - -grant를 발급받은 주체가 다른 endpoint로 그것을 쓸 수 있다. - -## 확인하지 못한 것 - -업로드용 허가를 다른 경로에 제시해 통과를 재현하지 않았다. 이 기록은 검증 지점의 필드 목록 대조에 근거한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/objectstorage-staged-lifecycle/case/case-the-last-part-cannot-get-a-grant.md b/docs/clean-architecture-backend-template/tech-log-studio/objectstorage-staged-lifecycle/case/case-the-last-part-cannot-get-a-grant.md deleted file mode 100644 index 4be5623..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/objectstorage-staged-lifecycle/case/case-the-last-part-cannot-get-a-grant.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -kind: CASE -slug: the-last-part-cannot-get-a-grant -title: 직접 multipart의 마지막 part는 grant를 받을 수 없다 -topic: objectstorage-staged-lifecycle -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:the-last-part-cannot-get-a-grant -evidenceCapturedOn: 2026-09-01 -assets: - - key: the-last-part-cannot-get-a-grant - file: ../../../final/evidence/rendered/the-last-part-cannot-get-a-grant.svg -evidence: - - ../../../final/evidence/raw/the-last-part-cannot-get-a-grant.txt -source: - - 원본 분석 절은 final/document.md#a09 §38 이다. ---- - -# 직접 multipart의 마지막 part는 grant를 받을 수 없다 - -클라이언트가 저장소에 직접 올리는 멀티파트 업로드에서 각 파트마다 허가가 필요하다. 발급 경로의 조건이 마지막 파트를 제외한다. - -## 관계 - -- **staged lifecycle과 서명된 grant** - 이 허가가 무엇을 위한 것인지 다룬 개념이다. -- **서명된 grant의 endpoint 검증이 upload 경로에만 있다** - 같은 허가 체계의 다른 공백이다. - -## 문제 - -멀티파트 업로드는 객체를 여러 조각으로 나눠 올린다. 클라이언트가 저장소에 직접 올리는 구성에서는 각 조각마다 서명된 허가가 필요하다. - -허가 발급 경로가 어느 파트까지 발급하는지가 이 사례의 대상이다. - -## 결론 - -마지막 파트가 조건에서 빠진다. - -발급 경로를 따라가면 파트 번호에 대한 조건이 마지막 하나를 제외한다. - -결과적으로 클라이언트는 마지막 조각을 올릴 허가를 받지 못한다. 업로드가 완결되지 않는다. - -이 공백의 성질은 조용하지 않다. 마지막 파트에서 실패하므로 업로드를 시도하면 바로 드러난다. 다만 그 실패가 허가 발급의 조건 문제로 보이지 않고 저장소 거절로 보인다. - -그리고 이 경로가 실제로 배포에서 쓰이는지는 별도 문제다. 직접 업로드 구성이 활성인 배포에서만 나타난다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 허가 발급 경로의 조건 확인 -소스 수정 : x - -## 재현 조건 - -1. 멀티파트 허가 발급 경로를 찾는다. -2. 파트 번호에 대한 조건을 읽는다. -3. 마지막 파트가 그 조건을 만족하는지 확인한다. - -## 본문 - - - -직접 multipart 업로드에서 마지막 part가 grant를 받을 수 없다. - -## grant 가 단계별로 발급된다 - -:::evidence key="the-last-part-cannot-get-a-grant" alt="분석 문서 final/document.md#a09 에서 이 기록의 근거 절을 그대로 잘라낸 7줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a09 발췌 — 7줄" zoom="true" -::: - -## grant 발급 경로를 벗어나는 조건 - -grant가 업로드 단계별로 발급되는 구조인데 마지막 part의 조건이 그 발급 경로를 벗어난다. - -## 확인하지 못한 것 - -실제 저장소에 대고 멀티파트 업로드를 수행해 마지막 파트 실패를 재현하지 않았다. 이 기록은 조건 확인에 근거한다. - -이 경로가 어떤 배포 구성에서 활성인지 확인하지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/objectstorage-staged-lifecycle/decision/decision-legacy-adoption-requires-two-approvers.md b/docs/clean-architecture-backend-template/tech-log-studio/objectstorage-staged-lifecycle/decision/decision-legacy-adoption-requires-two-approvers.md deleted file mode 100644 index 1dae041..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/objectstorage-staged-lifecycle/decision/decision-legacy-adoption-requires-two-approvers.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -kind: PROJECT_DECISION -slug: legacy-adoption-requires-two-approvers -title: legacy 채택은 서로 다른 두 승인자의 서명을 요구한다 -topic: objectstorage-staged-lifecycle -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: decision:legacy-adoption-requires-two-approvers -decisionStatus: ADOPTED -decidedOn: 2026-08-30 -source: - - src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/maintenance/Ed25519LegacyAdoptionApprovalVerifier.java - - src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/maintenance/LegacyObjectAdoptionService.java - - src/application-core/src/main/java/dev/caskeleton/application/storage/migration/LegacyObjectAdoptionApprovalVerifierPort.java - - final/document.md#a09 ---- - -# legacy 채택은 서로 다른 두 승인자의 서명을 요구한다 - -## 결정문 - -레거시 객체를 이 플랫폼의 관리 대상으로 편입하는 연산은 서로 다른 두 승인자의 서명을 요구한다. - -## 판단 이유 - -채택은 되돌리기 어려운 연산이다. 기존 저장소의 객체가 이 플랫폼의 수명주기와 쿼터와 게시 규칙 아래로 들어온다. - -한 사람의 판단으로 그것이 일어나면 실수 하나가 대량으로 확산된다. 잘못된 네임스페이스나 잘못된 목적지를 지목한 채택이 그렇다. - -그래서 두 승인자를 요구한다. 서로 다른 두 서명이 있어야 한다는 조건이다. - -승인 객체에는 연산 키와 매니페스트 해시와 네임스페이스 다이제스트와 대상 목적지와 유효 구간이 담긴다. 즉 승인은 특정 요청에 대한 것이고 다른 요청에 재사용될 수 없다. - -재생 방지 저장소가 같은 승인의 재사용을 막는다. - -## 영향 - -감수하는 것 - -채택 절차가 무겁다. 두 승인자가 서명해야 하고 그 서명을 요청에 실어야 한다. - -승인 키 관리가 필요하다. 두 승인자의 키가 별도로 관리되어야 두 승인자라는 조건이 의미를 갖는다. - -현재 이 결정의 강제가 배선되어 있지 않다. 채택 서비스가 승인의 필드 일관성만 확인하고 서명을 검증하지 않으며, 서명 검증기는 자기 테스트에서만 생성된다. 즉 결정은 채택되었고 강제는 비어 있다. - -얻는 것 - -대량 편입이 한 사람의 판단으로 일어나지 않는다. - -승인이 특정 요청에 묶이므로 재사용되지 않는다. - -## 근거 - -- **APPLY를 켜는 설정은 있고 승인을 검증하는 bean은 없다** - 이 결정의 강제가 비어 있음을 보여 주는 사례다. -- **권한이 센 절반이 설정 한 줄로 켜지면 안 된다** - 이 결정이 지키려는 규칙이다. -- **이진 승인 코덱은 decode 후 재인코딩이 원본과 같아야 한다** - 서명이 유효하려면 함께 성립해야 하는 형식 규칙이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/objectstorage-staged-lifecycle/reference/reference-a-binary-approval-codec-must-round-trip.md b/docs/clean-architecture-backend-template/tech-log-studio/objectstorage-staged-lifecycle/reference/reference-a-binary-approval-codec-must-round-trip.md deleted file mode 100644 index 8d474dd..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/objectstorage-staged-lifecycle/reference/reference-a-binary-approval-codec-must-round-trip.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -kind: REFERENCE -slug: a-binary-approval-codec-must-round-trip -title: 이진 승인 코덱은 decode 후 재인코딩이 원본과 같아야 한다 -topic: objectstorage-staged-lifecycle -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:a-binary-approval-codec-must-round-trip -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# 이진 승인 코덱은 decode 후 재인코딩이 원본과 같아야 한다 - -## 목적 - -서명 대상 바이트와 검증 대상 바이트가 달라, 유효한 승인이 거절되거나 변조된 승인이 통과하는 것을 막는다. - -## 규칙 - -1. 왕복이 항등이어야 한다 - 디코드한 뒤 다시 인코딩한 결과가 원본 바이트와 같아야 한다. - -2. 서명은 정규 형식에 대해 한다 - 같은 내용이 두 가지 바이트로 표현될 수 있으면 서명이 그중 하나에만 유효하다. - -3. 길이 프레이밍을 쓴다 - 가변 길이 필드를 구분자로 이으면 서로 다른 필드 조합이 같은 바이트가 될 수 있다. - -4. 버전을 서명 대상에 포함한다 - 형식이 바뀌면 옛 서명이 새 해석으로 검증되지 않아야 한다. - -5. 왕복 성질을 테스트로 고정한다 - 무작위 입력에 대해 왕복을 확인하는 테스트가 필요하다. - -## 적용 조건 - -서명되는 모든 이진 구조 — 승인 토큰과 허가와 커서 - -두 계층이 같은 바이트를 서로 다른 코드로 다루는 경우 - -## 예외 - -서명 대상이 텍스트이고 정규화 규칙이 외부 표준으로 정해져 있으면 그 표준을 따른다. - -## 예시 - -커서 코덱은 버전과 페이로드를 함께 MAC 으로 덮는다. 페이로드만 덮으면 접두사를 고쳐 옛 형식으로 강등할 수 있다. - -전이 다이제스트는 구성 요소가 가변 길이 텍스트이고 그중 하나는 이 플랫폼이 제약할 수 없는 값이라 길이 프레이밍이 필요하다. - -## 관계 - -- **서명된 커서의 구조와 검증 순서** - 같은 원칙이 커서에 적용된 구조다. -- **digest는 길이 프레이밍하고 버전을 붙인다** - 같은 계열의 형식 규칙이다. -- **APPLY를 켜는 설정은 있고 승인을 검증하는 bean은 없다** - 이 코덱이 쓰일 경로가 배선되지 않은 사례다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/objectstorage-staged-lifecycle/reference/reference-the-powerful-half-must-not-be-one-setting-away.md b/docs/clean-architecture-backend-template/tech-log-studio/objectstorage-staged-lifecycle/reference/reference-the-powerful-half-must-not-be-one-setting-away.md deleted file mode 100644 index 88d243a..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/objectstorage-staged-lifecycle/reference/reference-the-powerful-half-must-not-be-one-setting-away.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -kind: REFERENCE -slug: the-powerful-half-must-not-be-one-setting-away -title: 권한이 센 절반이 설정 한 줄로 켜지면 안 된다 -topic: objectstorage-staged-lifecycle -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:the-powerful-half-must-not-be-one-setting-away -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# 권한이 센 절반이 설정 한 줄로 켜지면 안 된다 - -## 목적 - -권한이 센 연산을 켜는 조건과 그 연산을 통제하는 장치를 조립하는 조건이 달라, 통제 없이 권한만 켜지는 것을 막는다. - -## 규칙 - -1. 켜는 조건과 통제하는 조건을 같은 자리에 둔다 - 설정 하나가 켜는 것이 연산이면 그 설정이 통제 장치도 함께 조립해야 한다. - -2. 통제 장치가 없으면 조립을 거부한다 - 조건부로 조립하되 통제 장치가 없으면 시작에 실패하게 한다. 조용히 통제 없이 켜지는 것보다 낫다. - -3. 일관성 검사와 진정성 검사를 구별한다 - 요청과 승인이 서로 맞는지 확인하는 것과 그 승인을 우리가 발급했는지 확인하는 것은 다른 검사다. - -4. 승인 객체의 출처를 확인한다 - 요청이 들고 온 객체는 요청자가 만든 것일 수 있다. - -5. 검증기의 존재를 배선의 증거로 읽지 않는다 - 검증기 클래스와 그 테스트가 있어도 서비스의 협력자가 아니면 돌지 않는다. - -## 적용 조건 - -데이터 채택과 마이그레이션과 일괄 삭제처럼 되돌리기 어려운 연산 - -승인이나 서명으로 통제되는 모든 관리 경로 - -## 예외 - -개발 환경 전용으로 명시되고 배포 구성에서 구조적으로 차단되는 경로는 예외일 수 있다. 그 차단이 무엇인지 적혀 있어야 한다. - -## 예시 - -레거시 채택 설정이 조건 하나로 채택 포트를 조립한다. 그 서비스는 승인 객체의 필드 일관성만 확인하고 서명을 검증하지 않으며, 서명 검증기는 자기 테스트에서만 생성된다. - -## 관계 - -- **APPLY를 켜는 설정은 있고 승인을 검증하는 bean은 없다** - 이 규칙을 만든 사례다. -- **legacy 채택은 서로 다른 두 승인자의 서명을 요구한다** - 이 경로가 요구하도록 설계된 결정이다. -- **Bean 애너테이션이 있다는 것은 조립 증거가 아니다** - 다섯 번째 규칙의 일반형이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/observability-models/concept/concept-adapter-inbound-web-c15.md b/docs/clean-architecture-backend-template/tech-log-studio/observability-models/concept/concept-adapter-inbound-web-c15.md deleted file mode 100644 index e72e3df..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/observability-models/concept/concept-adapter-inbound-web-c15.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-web-c15 -title: 상관 식별자를 만드는 필터가 세 벌이다 -topic: observability-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-web-c15 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-web-c15 - file: ../../../final/evidence/rendered/adapter-inbound-web-c15.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-web-c15.txt -source: - - 원본 분석 절은 final/document.md#a14#L1108 이다. -module: adapter-inbound-web ---- - -# 상관 식별자를 만드는 필터가 세 벌이다 - -`WebMvcRequestIdFilter`·`RequestLoggingFilter`·`WebFluxRequestContextFilter` 셋이 상관 식별자를 각각 만든다. 세 번째는 전송이 달라 공존이 정상이고, 앞의 둘은 같은 서블릿 체인에서 같은 헤더를 두 번 처리한다. - -## 본문 - - - -상관 식별자를 만드는 메커니즘이 셋이다. - -| 메커니즘 | 헤더 | 저장 위치 | -|---|---|---| -| `WebMvcRequestIdFilter` | `X-Request-Id`, `traceparent` | 요청 속성(`WebRequestId`/`WebTraceId`) | -| `RequestLoggingFilter` | `X-Request-Id`, `X-Correlation-Id`, `traceparent` | MDC | -| `WebFluxRequestContextFilter` | `X-Request-Id`, `traceparent` | Reactor context | - -## RequestLoggingFilter 참조 위치 - -:::evidence key="adapter-inbound-web-c15" alt="코드베이스에서 RequestLoggingFilter 를 검색한 출력 22줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RequestLoggingFilter 코드베이스 검색 — 22줄 · exit 0" zoom="true" -::: - -## 서블릿 체인에서 같은 헤더가 두 번 처리된다 - -세 번째는 전송이 달라 공존이 정상이다. 앞의 둘은 같은 서블릿 체인에서 같은 헤더를 두 번 처리한다. `traceparent`도 마찬가지로 두 번 파싱되며, `RequestLoggingFilter`는 응답에도 `traceparent`를 쓰고(`:68`) `WebMvcRequestIdFilter`는 쓰지 않는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/observability-models/concept/concept-adapter-outbound-persistence-jpa-c44.md b/docs/clean-architecture-backend-template/tech-log-studio/observability-models/concept/concept-adapter-outbound-persistence-jpa-c44.md deleted file mode 100644 index 99da8de..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/observability-models/concept/concept-adapter-outbound-persistence-jpa-c44.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c44 -title: manual audit 경로만 조립되고 Spring Data auditing은 dormant다 -topic: observability-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c44 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c44 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c44.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c44.txt -source: - - 원본 분석 절은 final/document.md#a05#L3123 이다. -module: adapter-outbound-persistence-jpa ---- - -# manual audit 경로만 조립되고 Spring Data auditing은 dormant다 - -manual `AuditableEntity`/`AuditContextPort` 경로와 Spring Data `AuditMetadata`/`JpaAuditingConfiguration`이 함께 존재한다. tests/docs가 후자를 candidate/dormant로 명시하고 default composition도 canonical manual audit 경로만 사용한다. - -## 본문 - - - -manual `AuditableEntity`/`AuditContextPort` 경로와 Spring Data `AuditMetadata`/`JpaAuditingConfiguration`이 함께 존재한다. - -## 중복 활성화로 판정하지 않은 이유 - -tests/docs가 후자를 candidate/dormant로 명시하고 default composition도 canonical manual audit 경로만 사용한다. 현재 중복 활성화 defect로 판정하지 않는다. - -## AuditableEntity 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c44" alt="코드베이스에서 AuditableEntity 를 검색한 출력 27줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="AuditableEntity 코드베이스 검색 — 27줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/observability-models/concept/concept-adapter-outbound-support-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/observability-models/concept/concept-adapter-outbound-support-c02.md deleted file mode 100644 index 8316515..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/observability-models/concept/concept-adapter-outbound-support-c02.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-support-c02 -title: FailOpenDependencyLogger는 실패 정책을 정하지 않는다 -topic: observability-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-support-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-support-c02 - file: ../../../final/evidence/rendered/adapter-outbound-support-c02.svg - - key: adapter-outbound-support-c02-diagram - file: ../../../final/assets/diagrams/adapter-outbound-support-c02.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-support-c02.txt -source: - - 원본 분석 절은 final/document.md#a04#L119 이다. -module: adapter-outbound-support ---- - -# FailOpenDependencyLogger는 실패 정책을 정하지 않는다 - -이 logger는 retry, recovery, fallback을 수행하지 않는다. 실패 정책을 결정하는 주체가 아니라 이미 결정된 fail-open outcome을 관측하는 기술 seam이다. - -## 본문 - - - -`logSuccess(...)`는 DEBUG로 다음을 기록한다. - -- dependency_name -- dependency_type -- operation -- outcome=`SUCCESS` -- correlation_id - -`logFailure(...)`는 WARN으로 outcome=`FAILURE`와 error=`: `를 추가한다. README와 javadoc은 WARN을 선택한 이유를 "optional fail-open dependency가 실패해도 core use case 자체는 성공했기 때문"이라고 설명한다. - -## 판정과 기록이 갈리는 자리 - -:::evidence key="adapter-outbound-support-c02-diagram" alt="fail-open 판정에서 use case 결과와 로거로 각각 화살표가 나가고 로거에서 돌아오는 화살표는 없는 구조" caption="판정과 기록의 갈림" zoom="false" -::: - -이 logger 자체는 retry, recovery, fallback을 수행하지 않는다. **실패 정책을 결정하는 주체가 아니라 이미 결정된 fail-open outcome을 관측하는 기술 seam**이다. - -## FailOpenDependencyLogger 참조 위치 - -:::evidence key="adapter-outbound-support-c02" alt="코드베이스에서 FailOpenDependencyLogger 를 검색한 출력 34줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="FailOpenDependencyLogger 코드베이스 검색 — 34줄 · exit 0" zoom="true" -::: - -## 실제로 support를 import하는 네 파일 - -repository-wide production reference scan에서 support package를 직접 import하는 current production files는 네 개뿐이었다 — `MessagingConfig`, `OutboundMessagePublisher`, `NotificationConfig`, `FailOpenNotificationProvider`. 반대로 support README가 "공유 consumer"로 설명하는 `cache-redis`, `httpclient`는 Gradle dependency는 유지하지만 support production type을 직접 참조하지 않는다. 이 차이는 §8에서 별도로 다룬다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/observability-models/concept/concept-adapter-outbound-support-c06.md b/docs/clean-architecture-backend-template/tech-log-studio/observability-models/concept/concept-adapter-outbound-support-c06.md deleted file mode 100644 index 78a3033..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/observability-models/concept/concept-adapter-outbound-support-c06.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-support-c06 -title: 두 게이트가 증명하는 것과 증명하지 않는 것 -topic: observability-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-support-c06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-support-c06 - file: ../../../final/evidence/rendered/adapter-outbound-support-c06.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-support-c06.txt -source: - - 원본 분석 절은 final/document.md#a04#L558 이다. -module: adapter-outbound-support ---- - -# 두 게이트가 증명하는 것과 증명하지 않는 것 - -`CleanArchitectureTest --rerun-tasks`와 `verifyCleanArchitectureDependencies`가 각각 BUILD SUCCESSFUL이다. 이 둘은 source/package/project dependency constraint를 증명하며 diagnostics runtime failure나 PII behavior를 증명하지 않는다. - -## 본문 - - - -두 게이트의 실행 결과다. - -- `CleanArchitectureTest --rerun-tasks`: BUILD SUCCESSFUL -- `verifyCleanArchitectureDependencies`: BUILD SUCCESSFUL - -## 두 결과가 증명하지 않는 것 - -이 둘은 source/package/project dependency constraint를 증명하며 diagnostics runtime failure나 PII behavior를 증명하지 않는다. - -## 분석 원문의 실행 기록 - -:::evidence key="adapter-outbound-support-c06" alt="분석 문서 final/document.md#a04 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a04 발췌 — 15줄" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/observability-models/concept/concept-grpc-observability-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/observability-models/concept/concept-grpc-observability-c03.md deleted file mode 100644 index c539e71..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/observability-models/concept/concept-grpc-observability-c03.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -kind: CONCEPT -slug: grpc-observability-c03 -title: allowlist가 기본 거절이고 거절 목록은 메시지를 위한 것이다 -topic: observability-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:grpc-observability-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-observability-c03 - file: ../../../final/evidence/rendered/grpc-observability-c03.svg -evidence: - - ../../../final/evidence/raw/grpc-observability-c03.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-observability#L74 이다. -module: grpc-observability ---- - -# allowlist가 기본 거절이고 거절 목록은 메시지를 위한 것이다 - -`violations(Map)` 의 판정 순서가 셋이고, 그중 `FORBIDDEN_TAGS` 분기는 판정을 바꾸지 않고 진단만 바꾼다. 두 번째 분기의 allowlist가 이미 그것들을 거절한다. - -## 본문 - - - -`violations(Map)` 의 판정 순서가 셋이다. - -```java -if (FORBIDDEN_TAGS.contains(key)) → "its value space grows with traffic…" -if (!ALLOWED_TAGS.contains(key)) → "not on the bounded allowlist [...]" -if (UNBOUNDED_VALUE.matcher(value)) → "looks like an identifier or a credential" -``` - -클래스 javadoc 이 두 목록이 겹치는 이유를 적는다 — "Everything unlisted is refused anyway; naming the dangerous ones gives the refusal a message that says why rather than just that." 즉 `FORBIDDEN_TAGS` 는 판정을 바꾸지 않고 진단만 바꾼다. 두 번째 분기가 이미 그것들을 거절한다. - -## 허용 태그 여덟과 명시적 거절 열하나 - -허용 태그: `grpc.service` · `grpc.method` · `grpc.rpc_type` · `grpc.status` · `grpc.channel_profile` · `grpc.completion_outcome` · `grpc.retry_bucket` · `grpc.stream_termination_reason`. - -명시적 거절: `actor_id` · `tenant_id` · `object_id` · `stream_id` · `idempotency_key` · `request` · `response` · `metadata` · `authorization` · `error_detail` · `trace_id`. - -## GrpcRpcObservation 참조 위치 - -:::evidence key="grpc-observability-c03" alt="코드베이스에서 GrpcRpcObservation 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcRpcObservation 코드베이스 검색 — 5줄 · exit 0" zoom="true" -::: - -## 값 검사가 잡지 못하는 것 - -UUID · `sha256:` 접두 · `bearer ` 접두. 숫자 id, 이메일, 호스트명은 잡히지 않는다. 그리고 `.` 은 기본적으로 개행에 맞지 않으므로 값에 개행이 섞이면 `matches()` 가 거짓이 된다. - -## 시도 수를 값이 아니라 버킷으로 접는다 - -`retryBucket(int)` 이 1-based 시도 수를 받아 `0`/`1`/`2`/`3+` 로 접는다. 0 이하는 던진다. javadoc 이 이유를 적는다 — "an attempt count is unbounded in principle and the distinction anyone acts on is first attempt, one retry, several." - -## 재시도 한 번이 호출 하나로 남는 이유 - -`GrpcRpcObservation` javadoc 이 적는다. - -> "A retried call is one observation with a retry bucket, and three attempt events beneath it; recording three separate calls instead makes the success rate read as 33% when the caller in fact got its answer." - -그 분리가 `GrpcObservationConvention.record(GrpcRpcObservation)` 에서 실제로 그렇게 구현되어 있다 — `RPC_DURATION` 타이머는 1회, `RPC_ATTEMPTS` 카운터는 `attempts` 만큼 증가. 같은 태그 집합을 쓴다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/observability-models/concept/concept-messaging-admin-runtime-c05.md b/docs/clean-architecture-backend-template/tech-log-studio/observability-models/concept/concept-messaging-admin-runtime-c05.md deleted file mode 100644 index 85bf428..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/observability-models/concept/concept-messaging-admin-runtime-c05.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-admin-runtime-c05 -title: 실패 사유가 저널에도 감사 이벤트에도 남지 않는다 -topic: observability-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-admin-runtime-c05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-admin-runtime-c05 - file: ../../../final/evidence/rendered/messaging-admin-runtime-c05.svg -evidence: - - ../../../final/evidence/raw/messaging-admin-runtime-c05.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-admin-runtime#L472 이다. -module: messaging-admin-runtime ---- - -# 실패 사유가 저널에도 감사 이벤트에도 남지 않는다 - -`attempt(...)` 가 모든 `RuntimeException` 을 삼키는 것은 근거가 있지만 대가도 있다. 감사 이벤트는 `failed` 개수만 담고, 어떤 메시지가 왜 실패했는지는 어디에도 기록되지 않는다. - -## 본문 - - - -토폴로지 불일치가 두 곳에서 각각 보고된다. - -| 상황 | 처리 | 위치 | -|---|---|---| -| 토폴로지 불일치 (A) | `MessagingConfigurationException("TOPOLOGY_MISMATCH")` | `TopologyValidationReport:78` | -| 토폴로지 불일치 (B) | `MessageTopologyException("TOPOLOGY_MISMATCH")` | `TopologyValidationRuntime:54` | - -이 두 줄이 §12.3(a)의 요약이다 — 같은 코드 문자열, 다른 예외 타입, 다른 판정 규칙. - -## RuntimeException 을 삼키는 자리 - -:::evidence key="messaging-admin-runtime-c05" alt="코드베이스에서 RuntimeException 를 검색한 출력 4줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RuntimeException 코드베이스 검색 — 4줄 · exit 0" zoom="true" -::: - -## 삼킨 예외가 남기지 않는 것 - -`attempt(...)` 가 모든 `RuntimeException` 을 삼키는 것은 근거가 있지만 대가도 있다: 실패 사유가 어디에도 남지 않는다. 감사 이벤트는 `failed` 개수만 담고(`:135`), 어떤 메시지가 왜 실패했는지는 기록되지 않는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/observability-models/concept/concept-messaging-observability-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/observability-models/concept/concept-messaging-observability-c02.md deleted file mode 100644 index 454b04b..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/observability-models/concept/concept-messaging-observability-c02.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-observability-c02 -title: 재료 둘만 bean으로 있고 그것을 조립하는 것이 없다 -topic: observability-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-observability-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-observability-c02 - file: ../../../final/evidence/rendered/messaging-observability-c02.svg -evidence: - - ../../../final/evidence/raw/messaging-observability-c02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-observability#L70 이다. -module: messaging-observability ---- - -# 재료 둘만 bean으로 있고 그것을 조립하는 것이 없다 - -출하 조립에서 bean으로 등록되는 것은 `MessagingRedactor`와 `CardinalityGuard` 둘이다. 둘 다 `MessagingMetrics`의 생성자 인자인데 `MessagingMetrics` bean이 없다. - -## 관계 - -- **타입이 문서화한 불변식은 타입이 강제한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -들어오는 것: `messaging-core-api`(api), `micrometer-core`(api). 나가는 것: `messaging-runtime-core`, `messaging-kafka`, `messaging-rabbit`, `messaging-admin-runtime`, `messaging-pulsar-experimental`, `messaging-nats-experimental`, `messaging-spring-boot-starter`. - -## 출하 조립이 등록하는 bean 둘 - -| bean | 라인 | 소비 | -|---|---:|---| -| `MessagingRedactor` | `MessagingCoreAutoConfiguration:253` | **없음** | -| `CardinalityGuard` | `:264` | **없음** | - -## MessagingMetrics 참조 위치 - -:::evidence key="messaging-observability-c02" alt="코드베이스에서 MessagingMetrics 를 검색한 출력 16줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingMetrics 코드베이스 검색 — 16줄 · exit 0" zoom="true" -::: - -## 재료는 있는데 조립하는 bean이 없다 - -두 클래스는 `MessagingMetrics`의 생성자 인자다. 그런데 `MessagingMetrics` bean이 없다(§12.1). `MessagingTracer`·`MessagingAuditSink`·`DefaultMessagingObservationConvention`은 bean도 없고 소비자도 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/operator-approval-and-destructive-operations/case/case-a-resumed-redrive-skips-what-it-could-not-move.md b/docs/clean-architecture-backend-template/tech-log-studio/operator-approval-and-destructive-operations/case/case-a-resumed-redrive-skips-what-it-could-not-move.md deleted file mode 100644 index 3c67ecf..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/operator-approval-and-destructive-operations/case/case-a-resumed-redrive-skips-what-it-could-not-move.md +++ /dev/null @@ -1,142 +0,0 @@ ---- -kind: CASE -slug: a-resumed-redrive-skips-what-it-could-not-move -title: 재개된 리드라이브가 옮기지 못한 메시지를 건너뛰고 성공으로 닫힌다 -topic: operator-approval-and-destructive-operations -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a-resumed-redrive-skips-what-it-could-not-move -evidenceCapturedOn: 2026-09-02 -assets: - - key: a-resumed-redrive-skips-what-it-could-not-move - file: ../../../final/evidence/rendered/a-resumed-redrive-skips-what-it-could-not-move.svg -evidence: - - ../../../final/evidence/raw/a-resumed-redrive-skips-what-it-could-not-move.txt -source: - - `final/document.md#a19-messaging-admin-runtime` §12.1·§17, 측정 `EVD-306`. ---- - -# 재개된 리드라이브가 옮기지 못한 메시지를 건너뛰고 성공으로 닫힌다 - -재개 지점이 시도한 개수인데 건너뛰는 대상은 매번 새로 조회한 목록이다. 그 목록에서 사라진 것은 성공한 것뿐이므로, 건너뛰기가 정확히 실패분과 미시도분을 대상에서 제외한다. - -## 관계 - -- **승인·검증·실행의 분리와 그것을 타입으로 표현하기** - 이 사례가 속한 구조다. -- **재개는 인덱스가 아니라 신원으로 한다** - 이 사례가 만든 규칙이다. -- **그 속성을 보일 수 없는 대역 위에서 통과한 테스트는 증인이 아니다** - 회귀 테스트가 성립하지 않는 이유다. -- **모르는 것은 성공도 실패도 아닌 세 번째 결과여야 한다** - 결과 보고가 사실이 아니게 되는 형태가 같다. - -## 문제 - -리드라이브는 데드레터에 쌓인 메시지를 원래 목적지로 되돌리는 작업이다. - -이것을 평범한 루프로 돌렸을 때 세 가지가 잘못됐다고 코드가 적는다. 발행자의 동기 실패가 루프 밖으로 전파되어 남은 후보가 시도되지 않고 감사 기록도 남지 않았고, 재시도는 첫 후보부터 다시 발행했으며, 한 메시지가 몇 번까지 리드라이브될 수 있는지를 제한하는 것이 없었다. - -그래서 항목마다 경계를 두고, 저널에 진행을 적고, 그 지점부터 재개하게 됐다. - -## 결론 - -재개 지점의 계약과 실제로 적히는 값이 어긋난다. - -그 값의 의미는 파라미터 javadoc 에 이전 시도가 확실히 옮긴 개수로 적혀 있다. 항목마다 부르는 체크포인트가 올리는 것은 시도한 개수다. 성공과 실패 양쪽에서 같은 카운터가 올라간다. - -재개할 때는 데드레터를 다시 조회한다. 그 목록에서 사라진 것은 정착된 것뿐이다. 실패한 것과 아직 시도하지 않은 것은 여전히 앞쪽에 남아 있다. - -그런데 재개 코드는 그 새 목록의 앞에서 기록된 개수만큼을 잘라낸다. 잘려나가는 것이 정확히 실패분과 미시도분이다. - -그 실행은 성공으로 닫히고 승인이 소진된다. 같은 승인으로는 다시 돌릴 수 없고, 남은 메시지들을 지목하는 기록이 없으므로 새 승인을 받을 근거도 없다. - -이 결함을 잡는 술어가 이미 존재한다. 결과 객체에 전부 정산됐는지 묻는 메서드가 있고, 프로덕션 호출부가 0 이다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Gradle : 9.0.0 -확인 방식 : 재개 지점의 계약과 기록되는 값 대조, 술어와 구현체의 호출자 검색 -소스 수정 : x - -## 재현 조건 - -1. 재개 지점 파라미터의 javadoc 과 건너뛰는 자리의 주석을 읽는다. -2. 체크포인트에 넘기는 카운터가 어디서 올라가는지 확인한다. 성공과 실패 양쪽이다. -3. 그 값이 다음 재개 지점이 되는 경로를 저널 구현까지 추적하고, 완료로 닫힌 승인이 어떻게 다뤄지는지 확인한다. -4. 후보 목록이 매번 다시 조회되는지, 확인되지 않은 재발행을 왜 정착시키지 않는지 확인한다. -5. 전부 정산됐는지 묻는 술어의 호출자를 main 과 test 로 나눠 센다. -6. 이 경로의 오케스트레이터와 조회 구현체가 몇 개인지 센다. - -## 본문 - - - -리드라이브를 평범한 루프로 돌렸을 때 무엇이 잘못됐는지는 코드가 적어 두었다. 발행자가 던지면 남은 후보가 전부 버려지고 감사 기록도 남지 않았고, 재시도는 첫 후보부터 다시 발행했다. - -그래서 항목마다 경계를 두고 저널에 진행을 적게 됐다. 그 진행 값이 이 사례의 대상이다. - -## 계약은 옮긴 개수인데 세는 것은 시도한 개수다 - -:::evidence key="a-resumed-redrive-skips-what-it-could-not-move" alt="코드베이스에서 재개 지점의 계약을 적은 javadoc 과 건너뛰는 자리의 주석, 체크포인트가 성공과 실패 양쪽에서 올라가는 지점, 그 체크포인트 값이 다음 재개 지점이 되는 경로와 두 저널 구현이 각각 값을 되돌아가지 않게 고정하는 줄, 완료로 닫힌 승인이 다시 인수되지 않는 분기, 후보를 매번 다시 조회하는 줄과 확인되지 않은 재발행을 정착시키지 않는 이유, 전부 정산됐는지 묻는 술어와 그 호출자, 그리고 이 경로에 실행 가능한 구현이 없다는 계수와 유일한 테스트 대역의 조회·정착 메서드를 뽑은 출력 88줄. 계약은 옮긴 개수인데 세는 것은 시도한 개수이고 대역의 조회가 목록을 그대로 돌려준다는 것이 그 출력에 보인다." caption="계약과 실제 값 · 체크포인트가 다음 재개 지점이 되는 경로 · 완료된 승인은 재인수 불가 · 술어 호출자 0 · 구현 0 과 대역의 조회 — 88줄" zoom="true" -::: - -재개 지점 파라미터의 javadoc 이 그 값을 이전 시도가 확실히 옮긴 개수라고 적는다. 건너뛰는 자리의 주석도 같은 말을 한다 — 그 앞의 것은 이전 시도가 옮기고 정착시켰으므로 다시 발행하는 것은 재시도가 아니라 중복이라는 것이다. - -실제로 올라가는 값은 다르다. 카운터가 재개 지점에서 시작해서, 성공 분기와 실패 분기를 지난 뒤 공통으로 한 번 올라간다. - -## 그 값이 다음 재개 지점이 되고, 되돌아가지 않는다 - -인수는 이전 레코드의 진행 값을 그대로 이어받아 리스의 재개 지점으로 내놓는다. 그 자리 주석이 이유를 적는다 — 이전 시도가 실패했든 리스가 만료됐든 둘 다 체크포인트에서 재개하며, 새 토큰이 이전 보유자를 막는다. - -두 저널 구현이 각각 그 값을 큰 쪽으로만 고정한다. 하나는 최댓값 함수로, 다른 하나는 SQL 의 같은 함수로. 인터페이스가 요구하지 않는데 둘 다 그렇게 한다. 시도 개수라는 선택이 우연이 아니라는 뜻이다. - -작업이 끝날 때 부르는 완료는 다르다. 그 값은 다음 재개 지점이 되지 못한다. 완료로 닫힌 승인은 인수 자체가 거절되기 때문이다. - -## 목록은 매번 다시 조회되고, 사라지는 것은 성공한 것뿐이다 - -재개할 때 후보를 다시 조회한다. 이전에 옮겨져 정착된 메시지는 데드레터에서 빠졌으니 목록에 없다. - -실패한 메시지는 남아 있다. 결과 타입의 javadoc 이 이유를 적는다 — 확인되지 않은 재발행을 정착시키면 마지막 사본을 지우는 셈이고, 데드레터로 보낼 때 적용되는 규칙이 리드라이브에도 똑같이 적용된다는 것이다. - -그래서 새 목록의 앞쪽은 실패분과 미시도분이다. 거기서 시도한 개수만큼을 잘라내면 정확히 그것들이 사라진다. - -## 메시지 다섯 건에서 어디가 잘리는지 - -데드레터에 다섯이 있고 첫 시도가 첫째를 옮겨 정착시킨다. 둘째는 확인되지 않아 남는다. 여기서 프로세스가 죽는다. 체크포인트에 적힌 진행은 둘이다. - -재개하면 목록은 넷이다. 원래의 첫째만 사라졌으니 남은 것은 원래의 둘째부터 다섯째다. 앞에서 둘을 잘라내면 이 목록의 셋째와 넷째 — 원래의 넷째와 다섯째 — 만 대상이 된다. - -원래의 둘째와 셋째가 잘려나간다. 하나는 실패했던 것이고 하나는 시도조차 되지 않은 것이다. - -둘 다 성공하면 결과는 후보 넷 중 둘을 옮기고 실패 0 으로 닫힌다. 예외도 실패 카운트도 남지 않는다. - -## 승인은 한 번 쓰이고 닫힌다 - -작업이 완료로 닫히면 그 승인은 소진된다. 같은 티켓으로 다시 인수하려 하면 저널이 거절한다 — 승인은 한 번의 실행을 허가하는 것이지 상시 권한이 아니라는 문장과 함께. - -새 승인을 받으면 키가 달라지므로 재개 지점 0 에서 새로 시작할 수 있다. 문제는 그 승인을 받을 근거다. 두 메시지는 데드레터에 남아 있고, 이 실행이 남긴 어떤 기록도 그 둘을 지목하지 않는다. - -## 잡을 수 있는 술어가 이미 있다 - -결과 객체에 전부 정산됐는지 묻는 메서드가 있다. 옮긴 개수와 남아 있는 개수의 합이 후보 수와 같은지 본다. 위 경우에는 둘 더하기 0 이 넷과 같지 않다. - -그 자리 javadoc 은 정산되지 않은 메시지가 부분 성공이 아니라 버그라고 적는다. 재발행되지도 남겨지지도 않았다는 것은 리드라이브가 그것을 놓쳤다는 뜻이다. - -이 술어를 부르는 프로덕션 코드가 0 이다. 부르는 것은 admin-api 타입만 조립하는 테스트 한 건이고, 그 안에서 참과 거짓을 각각 한 번씩 단언한다. - -## 이 경로에는 아직 실행 가능한 구현이 없다 - -재개 지점을 저널과 잇는 오케스트레이터를 생성하는 코드가 저장소 전체에 0 이다. 조회 인터페이스를 구현하는 main 코드도 0 이다. - -유일한 구현이 테스트 대역이고, 그 조회는 담아 둔 목록을 그대로 돌려준다. 정착은 별도 목록에 추가만 한다. - -이 결함의 성립 조건은 정착된 메시지가 다음 조회에서 사라진다는 것이다. 그 대역 위에서는 결함 있는 구현과 올바른 구현이 같은 결과를 낸다. 지금 이 결함을 겨냥한 테스트를 써도 통과한다. - -## 확인하지 못한 것 - -이 경로를 실행한 것이 아니다. 오케스트레이터를 생성하는 코드가 없고, 정착된 메시지가 조회 목록에서 사라진다는 성질을 갖춘 조회 구현도 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/operator-approval-and-destructive-operations/case/case-blocking-means-startup-fails-and-nothing-runs-it.md b/docs/clean-architecture-backend-template/tech-log-studio/operator-approval-and-destructive-operations/case/case-blocking-means-startup-fails-and-nothing-runs-it.md deleted file mode 100644 index da8597e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/operator-approval-and-destructive-operations/case/case-blocking-means-startup-fails-and-nothing-runs-it.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -kind: CASE -slug: blocking-means-startup-fails-and-nothing-runs-it -title: BLOCKING 이면 기동이 실패한다는 보장이 어떤 배선에서도 실행되지 않는다 -topic: operator-approval-and-destructive-operations -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:blocking-means-startup-fails-and-nothing-runs-it -evidenceCapturedOn: 2026-09-01 -assets: - - key: blocking-means-startup-fails-and-nothing-runs-it - file: ../../../final/evidence/rendered/blocking-means-startup-fails-and-nothing-runs-it.svg -evidence: - - ../../../final/evidence/raw/blocking-means-startup-fails-and-nothing-runs-it.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-admin-api §17 P2 이다. ---- - -# BLOCKING 이면 기동이 실패한다는 보장이 어떤 배선에서도 실행되지 않는다 - -토폴로지 검증에서 차단 심각도는 기동을 실패시킨다고 두 javadoc 이 적는다. 그 판정을 부르는 production 코드가 없고, 그것을 부를 수 있는 유일한 진입점도 호출자가 없으며, 서비스 빈을 스타터가 만들지 않는다. - -## 관계 - -- **승인·검증·실행의 분리와 그것을 타입으로 표현하기** - 이 사례가 속한 표면이다. -- **검증기는 발행이 아니라 주입이 강제다** - 이 사례가 어기는 규칙이다. -- **시작 검증기가 도는지는 그 능력에 자동설정 루트가 있는지와 일치한다** - 같은 계열의 판정 규칙이다. -- **시작 검증기 13개 규칙이 유일한 조립 지점에서 호출되지 않는다** - 같은 형태가 gRPC 쪽에서 나타난 사례다. - -## 문제 - -목적지 프로파일은 보장을 선언한다. 내구성이 필요하다거나, 순서가 필요하다거나, 특정 복제 수준이 필요하다는 선언이다. - -브로커의 실제 토폴로지가 그 보장을 줄 수 없으면 그것은 기동 시점에 잡아야 한다. 런타임에 발견하면 이미 메시지를 받은 뒤다. - -## 결론 - -타입은 그 상황을 정확히 표현하고, 표현한 것을 아무도 읽지 않는다. - -심각도 열거형의 차단 값 javadoc 이 적는다. - -> The destination cannot deliver a declared guarantee; startup must fail. - -검증 보고서의 수용 가능성 요구 메서드 javadoc 이 적는다. - -> Fails startup when any blocking issue was found. - -그 메서드의 production 호출부가 0이다. 그것을 부를 수 있는 유일한 진입점인 토폴로지 검증 메서드도 호출부가 0이고, 그 메서드를 가진 서비스 빈을 스타터가 만들지 않는다. - -결과: 복제 계수가 1인 목적지에 내구성을 선언해도 컨텍스트는 정상 기동한다. - -고치는 방법이 같은 리프에 이미 있다. 내구성 검증기가 초기화 콜백으로 저널 내구성을 기동 시점에 검사하고 실패시킨다. 같은 모양의 빈 하나면 된다. 다만 브로커 검사기가 없으면 검증할 수 없으므로 그 조건부는 유지해야 한다. - -이 항목이 무거운 이유는 리프가 출하 애플리케이션 소속이고, 보장이 문서와 타입과 테스트 세 겹으로 존재하는데 배선만 없다는 점이다. 읽는 사람은 보장이 있다고 믿을 근거를 세 개 갖는다. - -그리고 토폴로지 검증 스택이 이 가족에 두 벌 있다. 파티션 스케일업에 대해 한쪽은 권고로 두어 기동을 허용하고 다른 쪽은 차이로 보아 거부한다. 둘 다 호출자가 0이라 지금은 충돌하지 않지만, 배선하는 순간 어느 스택을 고르느냐가 스케일업한 배포의 기동 여부를 가른다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Gradle : 9.0.0 -확인 방식 : 두 메서드의 호출자 전수 검색과 스타터의 빈 목록 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/302-admin-api-topology-guarantee-unwired.txt (EVD-302) 에 있다. - -1. 차단 심각도와 수용 가능성 요구 메서드의 javadoc 을 읽는다. -2. 그 메서드의 호출자를 저장소에서 검색한다. -3. 그것을 부를 수 있는 진입점의 호출자를 검색한다. -4. 스타터가 그 서비스 빈을 만드는지 확인한다. -5. 같은 리프의 내구성 검증기가 어떻게 배선되어 있는지 확인한다. - -## 본문 - - - -`BLOCKING` 의 javadoc 이 "The destination cannot deliver a declared guarantee; startup must fail" 이라고 적고, `requireAcceptable()` 의 javadoc 이 "Fails startup when any blocking issue was found" 라고 적는다. - -## MessagingAdminDurabilityValidator 참조 위치 - -:::evidence key="blocking-means-startup-fails-and-nothing-runs-it" alt="코드베이스에서 MessagingAdminDurabilityValidator 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingAdminDurabilityValidator 코드베이스 검색 — 3줄 · exit 0" zoom="true" -::: - -## 세 겹으로 호출자가 없다 - -그 메서드의 production 호출부가 0 이고, 그것을 부를 수 있는 유일한 진입점도 호출부가 0 이며, 그 서비스 빈을 스타터가 만들지 않는다. 결과적으로 복제 계수 1인 목적지에 내구성을 선언해도 컨텍스트는 정상 기동한다. 타입은 그 상황을 정확히 표현할 수 있고 표현한 것을 아무도 읽지 않는다. - -## 고치는 방법이 같은 리프에 이미 있다 - -`MessagingAdminDurabilityValidator` 가 `InitializingBean` 으로 저널 내구성을 기동 시점에 검사하고 실패시킨다. 같은 모양의 빈 하나면 된다. - -## 이 항목이 무거운 이유 - -이 리프가 `app-bootstrap` 소속이고 보장이 문서·타입·테스트 세 겹으로 존재하는데 배선만 없다 — 읽는 사람은 보장이 있다고 믿을 근거를 세 개 갖는다. 그리고 토폴로지 검증 스택이 이 리프에 두 벌 있고 파티션 스케일업에 대한 판정이 서로 반대라, 배선하는 순간 어느 스택을 고르느냐가 스케일업한 배포의 기동 여부를 가른다. P2. - -## 확인하지 못한 것 - -컨텍스트를 세워 기동 성공을 관측하지 않았다. 호출부 부재로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/operator-approval-and-destructive-operations/case/case-the-forgeable-approval-survived-on-the-irreversible-half.md b/docs/clean-architecture-backend-template/tech-log-studio/operator-approval-and-destructive-operations/case/case-the-forgeable-approval-survived-on-the-irreversible-half.md deleted file mode 100644 index 5d5544e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/operator-approval-and-destructive-operations/case/case-the-forgeable-approval-survived-on-the-irreversible-half.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -kind: CASE -slug: the-forgeable-approval-survived-on-the-irreversible-half -title: 위조 가능한 승인이 하필 되돌릴 수 없는 작업 쪽에만 남았다 -topic: operator-approval-and-destructive-operations -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:the-forgeable-approval-survived-on-the-irreversible-half -evidenceCapturedOn: 2026-09-01 -assets: - - key: the-forgeable-approval-survived-on-the-irreversible-half - file: ../../../final/evidence/rendered/the-forgeable-approval-survived-on-the-irreversible-half.svg -evidence: - - ../../../final/evidence/raw/the-forgeable-approval-survived-on-the-irreversible-half.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-admin-runtime §17 · final/document.md#a19-messaging-admin-api §17 이다. ---- - -# 위조 가능한 승인이 하필 되돌릴 수 없는 작업 쪽에만 남았다 - -승인을 위조할 수 없게 만드는 수정이 복구 가능한 두 작업에는 적용됐고 복구 불가능한 세 작업에는 적용되지 않았다. 방향이 뒤집혀 있다. - -## 관계 - -- **승인·검증·실행의 분리와 그것을 타입으로 표현하기** - 이 사례가 속한 구조다. -- **권한이 센 절반이 설정 한 줄로 켜지면 안 된다** - 같은 계열의 규칙이다. -- **서명 능력과 검증 능력은 같은 객체에 두지 않는다** - 같은 표면의 다른 결함이다. -- **APPLY를 켜는 설정은 있고 승인을 검증하는 bean은 없다** - 같은 형태가 다른 어댑터에서 나타난 사례다. - -## 문제 - -파괴적 작업 인터페이스는 다섯 작업을 다룬다. 재생과 리드라이브는 되돌릴 수 있다. 비우기, 목적지 삭제, 오프셋 되돌리기는 되돌릴 수 없다. - -승인된 계획을 표현하는 타입이 작업마다 있다. - -## 결론 - -수정이 절반에만 적용됐다. - -재생과 리드라이브의 승인된 계획 타입은 검증을 통과한 사실 자체를 담은 타입을 요구한다. 그 타입은 검증기만 만들 수 있다. - -나머지 세 작업의 승인된 계획 타입은 여전히 public 생성자를 가진 평범한 record 다. 생성자가 보는 것은 널과 음수뿐이다. 그 승인이 이 작업을 인가하는지, 이 목적지를 인가하는지, 영향받는 메시지 수가 승인 상한 이하인지 아무것도 검사하지 않고, 계획 다이제스트 필드 자체가 없다. - -방향이 뒤집혀 있다는 것이 이 사례의 요점이다. 실수해도 되돌릴 수 있는 작업이 보호받고, 되돌릴 수 없는 작업이 보호받지 않는다. - -현재 구현체가 0건이라 실행되는 결함은 아니다. 그러나 이 인터페이스는 운영자 도구가 구현하라고 존재하는 것이고, 그 도구가 생기는 순간의 모양이 이것이다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Gradle : 9.0.0 -확인 방식 : 다섯 작업의 승인된 계획 타입 생성자 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/308-admin-runtime-api-and-dependency-defects.txt (EVD-308) 에 있다. - -1. 파괴적 작업 인터페이스에서 다섯 작업의 승인된 계획 타입을 찾는다. -2. 각 타입의 생성자가 무엇을 검사하는지 적는다. -3. 검증을 통과한 사실을 담은 타입을 요구하는 것이 어느 작업인지 센다. -4. 그 작업들이 복구 가능한지 확인한다. - -## 본문 - - - -승인된 계획을 위조할 수 없게 만드는 수정이 `REPLAY` 와 `REDRIVE` 에는 적용됐고 `PURGE` · `DELETE_DESTINATION` · `OFFSET_RESET` 에는 적용되지 않았다. - -## 수정이 적용된 두 작업 - -:::evidence key="the-forgeable-approval-survived-on-the-irreversible-half" alt="분석 문서 final/document.md#a19-messaging-admin-runtime 에서 이 기록의 근거 절을 그대로 잘라낸 18줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a19-messaging-admin-runtime 발췌 — 18줄" zoom="true" -::: - -## 남은 쪽의 생성자가 보는 것 - -`Approved` record 는 public 생성자를 갖고 생성자가 null 과 음수만 본다 — 그 승인이 이 작업을 인가하는지, 이 목적지를 인가하는지, 영향 메시지 수가 승인 상한 이하인지 아무것도 검사하지 않고 계획 다이제스트 필드 자체가 없다. - -## 방향이 뒤집혀 있다 - -적용된 두 작업은 복구 가능하고, 빠진 세 작업은 복구 불가능하다. 현재 구현체가 0 건이라 실행되는 결함은 아니지만, 이 인터페이스는 운영자 도구가 구현하라고 존재하는 것이고 그 도구가 생기는 순간의 모양이 이것이다. - -## 확인하지 못한 것 - -없다. 다섯 타입의 생성자와 적용 범위를 대조했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/operator-approval-and-destructive-operations/concept/concept-approval-verification-execution.md b/docs/clean-architecture-backend-template/tech-log-studio/operator-approval-and-destructive-operations/concept/concept-approval-verification-execution.md deleted file mode 100644 index 405f300..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/operator-approval-and-destructive-operations/concept/concept-approval-verification-execution.md +++ /dev/null @@ -1,95 +0,0 @@ ---- -kind: CONCEPT -slug: approval-verification-execution -title: 승인·검증·실행의 분리와 그것을 타입으로 표현하기 -topic: operator-approval-and-destructive-operations -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:approval-verification-execution -evidenceCapturedOn: 2026-09-01 -assets: - - key: approval-verification-execution - file: ../../../final/evidence/rendered/approval-verification-execution.svg - - key: approval-verification-execution-diagram - file: ../../../final/assets/diagrams/approval-verification-execution.svg -evidence: - - ../../../final/evidence/raw/approval-verification-execution.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-admin-api §4 · §17 이다. ---- - -# 승인·검증·실행의 분리와 그것을 타입으로 표현하기 - -운영자 도구가 파괴적 작업을 부를 때 세 능력이 분리되어야 한다. 승인을 발급하는 능력, 그 승인이 진짜인지 검증하는 능력, 작업을 실행하는 능력. - -## 관계 - -- **위조 가능한 승인이 하필 되돌릴 수 없는 작업 쪽에만 남았다** - 이 분리가 절반만 적용된 사례다. -- **서명 능력과 검증 능력은 같은 객체에 두지 않는다** - 세 능력 중 앞의 둘에 대한 규칙이다. -- **권한이 센 절반이 설정 한 줄로 켜지면 안 된다** - 같은 계열의 기존 규칙이다. - -## 본문 - - - -운영자 도구가 파괴적 작업을 부를 때 세 가지가 분리되어야 한다. 승인을 발급하는 능력, 그 승인이 진짜인지 검증하는 능력, 그리고 작업을 실행하는 능력. - -## 승인이 실행에 닿기까지 - -:::evidence key="approval-verification-execution-diagram" alt="승인 발급에서 가드 검증으로 승인 문서가 건너가고 가드 검증에서 파괴적 실행으로 VerifiedApproval 이 건너간다" caption="승인이 실행에 닿기까지" zoom="false" -::: - -## 호출자가 자기에 대해 하던 주장 - -이 리프가 그 분리를 타입으로 표현한 이력이 javadoc 에 남아 있다 — 예전에는 승인된 계획 타입이 public 생성자를 가진 평범한 record 라서 "이 계획은 승인됐다" 가 호출자가 자기에 대해 한 주장이었고, 실행 메서드에 닿을 수 있는 코드는 무엇이든 승인을 지어낼 수 있었다. 그 수정이 `VerifiedApproval` 이다 — 검증을 통과했다는 사실 자체를 타입으로 만들어 생성자를 막았다. - -## VerifiedApproval 참조 위치 - -:::evidence key="approval-verification-execution" alt="코드베이스에서 VerifiedApproval 를 검색한 출력 12줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="VerifiedApproval 코드베이스 검색 — 12줄 · exit 0" zoom="true" -::: - -## javadoc 목록에 빠진 두 조건 - -가드는 네 조건이 아니라 여섯을 본다 — 작업 종류 일치와 출처 일치가 javadoc 목록에 빠져 있다. 그중 다섯 번째의 인라인 주석이 이 개념의 요점을 말한다 — "A guard that only checks presence and window lets a verified redrive approval authorise a destination deletion." - -:::note - -이 표면 전체에 production 소비자가 없어 실행으로 확인한 것이 없다 - -::: - -## 왜 셋인가 - -파괴적 작업은 되돌릴 수 없다. 목적지를 지우거나 오프셋을 되돌리면 그 전 상태는 없다. - -그러므로 실행하는 쪽이 스스로 승인할 수 없어야 한다. 승인이 별도의 사람이나 시스템에서 오고, 실행 직전에 그것이 진짜인지 확인해야 한다. - -## 승인을 타입으로 만든 이력 - -이 리프의 javadoc 이 이전 형태와 그 결함을 적는다. - -> The approved-plan types used to hold a plain AdminApproval record with a public constructor, so "this plan was approved" was a claim the caller made about itself. Any code that could reach the execute method could write new AdminApproval("TICKET-1", "someone", now, later) and the platform believed it. - -승인된 계획이 public 생성자를 가진 평범한 record 였으므로, 승인됐다는 것은 호출자가 자기에 대해 한 주장이었다는 뜻이다. - -수정은 검증을 통과했다는 사실 자체를 타입으로 만드는 것이었다. 그 타입은 검증기만 만들 수 있고, 실행 메서드는 그 타입을 요구한다. 그러면 승인을 지어내려면 검증기를 통과해야 한다. - -## 가드가 보는 것 - -가드의 클래스 javadoc 은 네 조건을 든다. 실제 본문은 여섯을 본다. 작업 종류 일치와 출처 일치가 목록에서 빠져 있다. - -그 둘이 사소하지 않다는 것을 다섯 번째 분기의 인라인 주석이 말한다. - -> A guard that only checks presence and window lets a verified redrive approval authorise a destination deletion. - -승인이 있고 유효 기간 안이라는 것만 보면, 검증된 리드라이브 승인으로 목적지 삭제를 인가할 수 있다는 뜻이다. 승인의 존재와 그 승인이 이 작업을 인가한다는 것은 다른 사실이다. - -## 이 표면의 현재 상태 - -이 리프 전체에 production 소비자가 없다. 운영자 도구가 아직 없기 때문이다. 그래서 여기 기록된 것들은 전부 그 도구가 생기는 순간의 모양에 대한 것이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/operator-approval-and-destructive-operations/reference/reference-resume-by-identity-not-by-index.md b/docs/clean-architecture-backend-template/tech-log-studio/operator-approval-and-destructive-operations/reference/reference-resume-by-identity-not-by-index.md deleted file mode 100644 index 5a6555d..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/operator-approval-and-destructive-operations/reference/reference-resume-by-identity-not-by-index.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -kind: REFERENCE -slug: resume-by-identity-not-by-index -title: 재개는 인덱스가 아니라 신원으로 한다 -topic: operator-approval-and-destructive-operations -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:resume-by-identity-not-by-index -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# 재개는 인덱스가 아니라 신원으로 한다 - -## 목적 - -중단된 배치 작업의 재개 지점이 목록의 변화에 무너지지 않게 한다. - -## 규칙 - -1. 재개 시점에 목록을 다시 만드는지 본다 - 다시 만들지 않으면 인덱스로 충분하다. - -2. 그 목록이 처리 결과에 따라 줄어드는지 본다 - 줄어든다면 인덱스는 재개 사이에 의미가 바뀐다. - -3. 무엇이 줄어드는지 정확히 적는다 - 성공한 것만 사라지고 실패한 것이 남으면, 인덱스로 건너뛰기는 실패한 것을 지운다. - -4. 재개 지점을 신원이나 오프셋으로 바꾼다 - 이미 처리한 항목의 식별자 집합이거나 브로커가 주는 위치여야 한다. - -## 적용 조건 - -리드라이브, 재생, 마이그레이션처럼 승인과 저널을 갖는 모든 재개 가능 작업. - -## 예외 - -목록이 불변이면 인덱스로 충분하다. 다만 불변이라는 것은 재개 사이에 다른 생산자가 없다는 뜻이며, 데드레터처럼 계속 유입되는 대상에는 성립하지 않는다. - -## 예시 - -리드라이브 저널이 시도한 개수를 기록하고, 재개할 때 새로 조회한 목록의 앞에서 그만큼을 잘라낸다. 그 목록에서 사라지는 것은 성공한 것뿐이므로 잘려나가는 것이 실패분과 미시도분이다. - -이 규칙을 따르려면 저널에 필드를 늘려야 한다. 그것이 이 규칙의 실제 비용이다. - -## 관계 - -- **재개된 리드라이브가 옮기지 못한 메시지를 건너뛰고 성공으로 닫힌다** - 이 규칙을 만든 사례다. -- **모르는 것은 성공도 실패도 아닌 세 번째 결과여야 한다** - 결과 보고가 사실이 아니게 되는 것을 막는 같은 계열의 규칙이다. -- **그 속성을 보일 수 없는 대역 위에서 통과한 테스트는 증인이 아니다** - 이 규칙의 회귀 테스트가 성립하려면 함께 필요한 규칙이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/operator-approval-and-destructive-operations/reference/reference-signing-and-verifying-do-not-share-an-object.md b/docs/clean-architecture-backend-template/tech-log-studio/operator-approval-and-destructive-operations/reference/reference-signing-and-verifying-do-not-share-an-object.md deleted file mode 100644 index 6147f89..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/operator-approval-and-destructive-operations/reference/reference-signing-and-verifying-do-not-share-an-object.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -kind: REFERENCE -slug: signing-and-verifying-do-not-share-an-object -title: 서명 능력과 검증 능력은 같은 객체에 두지 않는다 -topic: operator-approval-and-destructive-operations -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:signing-and-verifying-do-not-share-an-object -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# 서명 능력과 검증 능력은 같은 객체에 두지 않는다 - -## 목적 - -승인을 검증하는 프로세스가 승인을 발급할 수 있게 되는 것을 막는다. - -## 규칙 - -1. 검증하는 쪽이 서명 키를 쥐는지 본다 - 대칭키라면 검증할 수 있다는 것이 곧 서명할 수 있다는 뜻이다. - -2. 그 프로세스가 누구인지 묻는다 - 운영자 도구라면 운영자가 자기 승인을 지어낼 수 있다. - -3. 판정은 객체가 아니라 키의 소재로 한다 - 클래스를 나눠도 같은 키를 쥐면 아무것도 바뀌지 않는다. - -4. 정규 형식은 한 곳에 남긴다 - 클래스를 나눌 때 서명 대상의 직렬화가 두 벌이 되면 그 둘이 드리프트한다. - -## 적용 조건 - -승인, 토큰, 커서처럼 발급자와 검증자가 다른 모든 서명. - -## 예외 - -발급과 검증이 같은 신뢰 경계 안에서만 일어나면 분리가 필요 없다. 다만 그 경계는 코드가 아니라 배포가 정하므로, 경계가 바뀔 수 있으면 지금 분리해 두는 편이 싸다. - -## 예시 - -승인 검증기가 서명과 검증을 같은 키로 제공한다. javadoc 이 위험을 명시한다. - -> Holding this key is what makes a caller an issuer — it is not, and must not become, available to the runtime that executes operations. - -그런데 대칭키에서는 검증하려면 그 키를 가져야 한다. 이 리프의 테스트가 그 구조를 그대로 보여준다. 같은 객체가 발급자이자 검증자이고, 변수 이름이 발급자다. - -두 방향의 해법이 있고 둘 다 javadoc 이 이미 열어 두었다. 발급자 타입을 분리하거나, 공개키 검증자를 구현해 검증 측이 서명 키를 갖지 않게 하는 것이다. - -## 관계 - -- **승인·검증·실행의 분리와 그것을 타입으로 표현하기** - 이 규칙이 나온 구조다. -- **위조 가능한 승인이 하필 되돌릴 수 없는 작업 쪽에만 남았다** - 같은 표면의 다른 결함이다. -- **legacy 채택은 서로 다른 두 승인자의 서명을 요구한다** - 같은 문제를 결정으로 해결한 사례다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/owner-safe-state-machines/case/case-a-digest-that-covered-who-but-not-what.md b/docs/clean-architecture-backend-template/tech-log-studio/owner-safe-state-machines/case/case-a-digest-that-covered-who-but-not-what.md deleted file mode 100644 index 251e1eb..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/owner-safe-state-machines/case/case-a-digest-that-covered-who-but-not-what.md +++ /dev/null @@ -1,116 +0,0 @@ ---- -kind: CASE -slug: a-digest-that-covered-who-but-not-what -title: transition digest가 "누가·언제"만 덮고 "무엇"을 덮지 않아 다른 전이를 같다고 보고했다 -topic: owner-safe-state-machines -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a-digest-that-covered-who-but-not-what -evidenceCapturedOn: 2026-09-02 -assets: - - key: a-digest-that-covered-who-but-not-what - file: ../../../final/evidence/rendered/a-digest-that-covered-who-but-not-what.svg -evidence: - - ../../../final/evidence/raw/a-digest-that-covered-who-but-not-what.txt -source: - - 이 사례의 사실은 분석 문서가 아니라 다이제스트 정책 클래스의 javadoc — 고친 쪽이 남긴 사후 기록 — 과 현재 구현에서 왔다. 후보 원장이 지정한 `final/document.md#a05` 의 절 번호는 그 파일에 존재하지 않는다. 완료 쪽이 아직 열려 있다는 것은 같은 문서 §59.2 이고, 위에 인용한 탐침 값이 그 절에 있다. ---- - -# transition digest가 "누가·언제"만 덮고 "무엇"을 덮지 않아 다른 전이를 같다고 보고했다 - -전이 다이제스트가 전이 종류와 연산 식별자와 소유자와 시도와 상태 리비전을 덮었다. 누가 언제 했는지는 덮고 무엇을 했는지는 덮지 않아서, 결말이 다른 전이가 같은 값을 냈다. - -## 관계 - -- **digest는 길이 프레이밍하고 버전을 붙인다** - 이 사례가 만든 규칙이다. -- **CAS 튜플을 where 절에 전부 반복하고 update count를 답으로 쓴다** - 같은 저장소의 소유자 안전 규칙이다. -- **complete()의 replay 판정이 replayTtl 변경을 무시한다** - 이 다이제스트가 실제 판정에 쓰이지 않는 경로다. - -## 문제 - -다이제스트가 덮던 다섯 성분은 전부 누가 몇 번째로 어느 상태에서 했는지를 말한다. 무엇을 했는지를 말하는 성분이 없었다. - -그래서 재시도 가능한 실패와 포기한 실패가 같은 값을 냈고, 응답이 다른 두 완료와 보존 기간이 다른 두 완료도 그랬다. - -## 결론 - -다이제스트를 비교한 재생 판정이 세 쌍을 같은 전이로 결론지었다. 세 쌍에서 갈린 것은 호출자의 행동이 달라지는 자리뿐이었다. - -수정은 두 층으로 왔다. 호출부마다 의미 인자를 넘기게 했고, 실패 쪽은 전이 종류 자체를 성향별로 갈랐다. - -이어 붙이는 방식도 바뀌었다. 성분마다 앞에 길이를 적고, 버전 상수를 다이제스트 입력에 넣는다. - -다만 완료 쪽 재생 분기는 이 다이제스트를 비교하지 않는다. 그래서 응답이 같고 재생 창만 다른 두 완료는 지금도 같은 결과로 판정된다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 다이제스트 정책과 다섯 호출부, 완료 재생 분기 확인 -소스 수정 : x - -## 재현 조건 - -1. 다이제스트 정책의 javadoc 에서 덮던 성분과 충돌한 쌍을 읽는다. -2. 전이 다이제스트를 부르는 다섯 지점에서 각각 무엇을 넘기는지 확인한다. -3. 실패 쪽 전이 종류가 어떻게 갈라지는지 확인한다. -4. 이어 붙이기가 각 성분 앞에 적는 길이와, 해시가 실제로 먹는 바이트를 확인한다. -5. 완료 재생 분기가 무엇을 비교하는지 확인한다. - -## 본문 - - - -전이 다이제스트가 덮던 것은 전이 종류와 연산 식별자와 소유자 토큰과 시도와 상태 리비전이었다. - -## 결말을 가르는 성분이 하나도 없었다 - -:::evidence key="a-digest-that-covered-who-but-not-what" alt="코드베이스에서 전이 다이제스트가 덮던 다섯 성분과 그때 같은 값을 내던 쌍을 적은 javadoc, 지금 다섯 호출부가 실제로 넘기는 의미 인자, 각 성분 앞에 적는 길이와 해시가 먹는 바이트, 버전 상수, 그리고 완료 재생 분기가 비교하는 값을 뽑은 출력 49줄. 완료 재생 분기가 전이 다이제스트가 아니라 연산 식별자와 응답 다이제스트만 본다는 것이 그 출력에 그대로 보인다." caption="덮던 다섯과 충돌한 쌍 · 호출부별 의미 인자 · 길이 프레이밍과 해시 입력 · 완료 재생 분기가 비교하는 값 — 49줄 · exit 0" zoom="true" -::: - -다섯은 전부 누가 몇 번째로 어느 상태에서 했는지를 말한다. 결말에 가장 가까운 것이 전이 종류인데, 그때는 실패가 성향과 무관하게 하나의 `FAIL` 이었다. - -javadoc 이 같은 값을 내던 쌍 셋을 든다. 재시도 가능한 실패와 포기한 실패, 응답이 다른 두 완료, 보존 기간이 다른 두 완료다. - -세 쌍이 달랐던 부분은 호출자가 실제로 행동을 바꾸는 부분뿐이었다. 다이제스트를 비교한 재생 판정은 그 셋을 전부 같은 전이라고 답했다. - -## 무엇을 넘길지는 호출부가 정한다 - -지금은 다섯 호출부가 각자 인자를 넘긴다. - -갱신은 처리 임차 유효 기간을 넘긴다. 완료는 응답 다이제스트와 재생 유효 기간을 넘기고, 그 자리 주석이 이유를 적는다 — 응답 다이제스트를 전이 다이제스트 대신 쓰면 같은 응답을 낸 서로 다른 연산의 완료가 구별되지 않고, 완료가 함께 결정하는 재생 창도 잃는다. - -실패는 성향과 보존 기간을 넘긴다. 그리고 전이 종류 자체가 `FAIL_RETRYABLE` 과 `FAIL_ABANDONED` 로 갈린다. 첫 번째 쌍은 두 겹으로 갈라진 셈이다. - -시작과 해제는 아무것도 넘기지 않는다. javadoc 이 예시로 든 코덱 신원은 지금 어느 호출부도 넘기지 않는다. - -## 이어 붙이는 방식이 구별을 잃게 할 수 있다 - -성분은 전부 가변 폭 텍스트다. 그중 소유자 토큰은 문자 집합을 이 플랫폼이 정하지 않는다 — 소유자 타입이 강제하는 것은 1자 이상 128자 이하와 공백 불가뿐이다. - -구분자로 이으면 서로 다른 성분 목록이 하나의 문자열로 렌더링될 수 있다. 그래서 각 성분 앞에 길이를 적고 콜론을 찍은 뒤 값을 적는다. - -적는 길이는 자바 문자 수다. 해시가 먹는 것은 UTF-8 바이트다. 같은 저장소의 메시징 파티션 키는 같은 목적에 UTF-8 바이트 수를 쓰고, 비 ASCII 골든 벡터로 그 선택을 고정한다. - -## 버전 상수는 다이제스트 입력 안에 있다 - -상수 값은 2 이고, 이어 붙이기가 그 값을 `v2` 로 맨 앞에 쓴다. 형식이 바뀌면 값이 바뀌므로 옛 형식으로 계산된 다이제스트와 섞이지 않는다. - -저장소 이력에는 이 파일이 지금 형태 그대로 커밋 하나에 들어와 있다. 값이 언제 몇 번 올랐는지는 남아 있지 않다. - -## 완료 쪽은 아직 이 다이제스트를 보지 않는다 - -정책이 고쳐진 것과 그 정책이 판정에 쓰이는 것은 다르다. - -이미 완료된 동일 연산의 재생 분기는 전이 다이제스트를 비교하지 않는다. 상태가 COMPLETED 인지, 마지막 전이 종류가 COMPLETE 인지, 연산 식별자가 같은지를 보고, 그다음 응답 다이제스트만 비교한다. - -그래서 응답은 같고 재생 유효 기간만 다른 두 완료가 같은 결과로 판정된다. 분석 문서가 실제 PostgreSQL 탐침으로 그것을 확인했다 — 첫 호출에 한 시간, 두 번째 호출에 아홉 시간을 넘겼는데 두 번째가 같은 결과로 판정됐고, 저장된 창은 3600초 그대로였다. - -## 확인하지 못한 것 - -수정 이전의 충돌을 실행으로 재현하지 않았다. 지금 정책이 지켜지는지는 다이제스트 정책 전용 테스트가 고정한다 — 성향 차이, 응답 차이, 재생 창 차이, 길이 프레이밍, 그리고 널 인자와 인자 없음의 구분이다. 완료 쪽이 아직 열려 있다는 것은 분석 문서의 실측 탐침이 확인했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/owner-safe-state-machines/case/case-expired-claim-versus-expired-execution.md b/docs/clean-architecture-backend-template/tech-log-studio/owner-safe-state-machines/case/case-expired-claim-versus-expired-execution.md deleted file mode 100644 index 92572e4..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/owner-safe-state-machines/case/case-expired-claim-versus-expired-execution.md +++ /dev/null @@ -1,99 +0,0 @@ ---- -kind: CASE -slug: expired-claim-versus-expired-execution -title: 만료된 CLAIMED는 takeover하고 만료된 EXECUTING은 조정을 요구하도록 갈랐다 -topic: owner-safe-state-machines -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:expired-claim-versus-expired-execution -evidenceCapturedOn: 2026-09-01 -assets: - - key: expired-claim-versus-expired-execution - file: ../../../final/evidence/rendered/expired-claim-versus-expired-execution.svg -evidence: - - ../../../final/evidence/raw/expired-claim-versus-expired-execution.txt -source: - - 원본 분석 절은 final/document.md#a05 §10.1, §10.4 이다. ---- - -# 만료된 CLAIMED는 takeover하고 만료된 EXECUTING은 조정을 요구하도록 갈랐다 - -리스가 만료된 두 상태를 같이 다루면 안 된다. 청구만 하고 실행하지 않은 소유자는 밀어내도 되지만, 실행을 시작한 소유자는 무엇을 했는지 알 수 없으므로 조정으로 넘긴다. - -## 관계 - -- **만료된 claim과 만료된 실행은 다르게 다뤄야 한다** - 이 사례에서 끌어낸 규칙이다. -- **모르는 것은 성공도 실패도 아닌 세 번째 결과여야 한다** - 조정으로 넘기는 판정이 그 규칙의 적용이다. -- **fenced lease — 만료 시각만으로는 부족한 이유** - 리스 만료를 다루는 맥락이다. - -## 문제 - -리스가 만료되면 다른 작업자가 그 레코드를 가져갈 수 있어야 한다. 그러지 않으면 죽은 작업자의 레코드가 영원히 막힌다. - -문제는 만료된 소유자가 무엇을 하다가 만료됐는지에 따라 안전한 처리가 다르다는 점이다. - -## 결론 - -청구 결정 트리가 두 상태를 갈라 다르게 답한다. - -같은 소유자 토큰이면 연산 충돌로 답한다 -상태가 CLAIMED 이고 리스가 지났으면 청구를 재설정한다. 즉 가져간다 -상태가 재시도 가능 실패면 마찬가지로 재설정한다 -상태가 EXECUTING 이고 리스가 지났으면 복구 필요로 답한다 -상태가 포기됨이면 복구 필요로 답한다 -그 외에는 진행 중으로 답하고 재시도 시각을 준다 - -CLAIMED 는 자리를 잡았지만 아직 아무것도 실행하지 않은 상태다. 그 소유자를 밀어내도 외부 효과가 없다. - -EXECUTING 은 실행을 시작한 상태다. 그 소유자가 무엇을 어디까지 했는지 이 저장소는 모른다. 밀어내고 다시 실행하면 그 작업이 두 번 일어날 수 있다. - -그래서 EXECUTING 만료는 자동 처리 대상이 아니라 조정 대상이다. 결과 타입이 복구 필요라는 별도 값을 갖고, 그 값이 시도 번호를 함께 들고 간다. - -같은 구별이 해제 경로에도 있다. 소유자 튜플이 다르면 소유자 아님으로 답하고, 상태가 이미 EXECUTING 이면 실행이 시작되었음으로 답하며, CLAIMED 가 아니면 연산 충돌로 답한다. 해제는 아직 실행하지 않은 청구에 대해서만 허용된다. - -같은 형태가 인박스 어댑터에도 있다. - -## 검증 환경 - -OpenJDK : 21.0.12 -데이터베이스 : PostgreSQL -확인 방식 : 결정 트리와 그 javadoc 확인 -소스 수정 : x - -## 재현 조건 - -1. 소유자 안전 멱등성 저장소의 청구 결정 트리를 읽는다. -2. CLAIMED 만료와 EXECUTING 만료가 각각 어떤 결과를 내는지 확인한다. -3. 해제 경로의 상태 검사를 확인한다. -4. 인박스 어댑터의 같은 형태를 비교한다. - -## 본문 - - - -만료된 lease를 일률적으로 takeover하면 이미 실행이 시작된 작업을 blind retry하게 된다. - -## 이 저장소는 상태로 나눈다 - -만료된 `CLAIMED`는 `resetClaim`으로 takeover하고, 만료된 `EXECUTING`은 `abandonExpiredExecution`으로 `ABANDONED`에 넣고 `RecoveryRequired`를 반환한다. - -## RecoveryRequired 참조 위치 - -:::evidence key="expired-claim-versus-expired-execution" alt="코드베이스에서 RecoveryRequired 를 검색한 출력 10줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RecoveryRequired 코드베이스 검색 — 10줄 · exit 0" zoom="true" -::: - -## inbox도 같은 축을 쓴다 - -`RECEIVED`(takeover 가능)와 `PROCESSING`(→ DEAD, recovery-required)로 나눈다. 즉 "claim만 했다"와 "실행에 들어갔다"가 만료 시 다른 결론을 낳는다. - -## 확인하지 못한 것 - -만료된 EXECUTING 이 실제로 조정 큐로 흘러가 사람이 처리하는 경로를 따라가지 않았다. 확인한 것은 저장소가 그 상태를 별도 결과로 답한다는 것이다. - -컨테이너 레인 미실행 - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/owner-safe-state-machines/decision/decision-state-machines-carry-no-stereotype.md b/docs/clean-architecture-backend-template/tech-log-studio/owner-safe-state-machines/decision/decision-state-machines-carry-no-stereotype.md deleted file mode 100644 index 47da62f..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/owner-safe-state-machines/decision/decision-state-machines-carry-no-stereotype.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -kind: PROJECT_DECISION -slug: state-machines-carry-no-stereotype -title: 상태 기계 구현은 Spring stereotype을 갖지 않는다 -topic: owner-safe-state-machines -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: decision:state-machines-carry-no-stereotype -decisionStatus: ADOPTED -decidedOn: 2026-08-30 -source: - - src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency - - src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/config/JpaAdapterComponentsConfig.java - - final/document.md#a05 ---- - -# 상태 기계 구현은 Spring stereotype을 갖지 않는다 - -## 결정문 - -소유자 안전 상태 기계 구현 클래스에는 컴포넌트 스캔 대상이 되는 애너테이션을 붙이지 않고, 조립은 명시적으로 한다. - -## 판단 이유 - -이 클래스들은 어떤 데이터소스와 어떤 트랜잭션 경계에 묶이는지가 정확해야 한다. 스캔으로 들어오면 그 결정이 스캔 범위와 조건에 흩어진다. - -같은 리프에서 그 흩어짐이 실제로 문제가 된 사례가 있다. 활성 트랜잭션 검사가 어느 데이터소스인지를 묻지 않아, 다른 데이터소스의 트랜잭션 안에서 발행된 변경이 이 저장소의 커넥션에 대해 트랜잭션 밖에서 커밋됐다. - -명시적 조립은 그 결정을 한 자리에 모은다. 어떤 데이터소스가 어떤 저장소에 들어가는지가 코드로 보인다. - -그리고 스캔되지 않으면 조건을 반복할 필요도 없다. 능력이 꺼진 배포에서 이 클래스들이 후보가 되는 일 자체가 없다. - -## 영향 - -감수하는 것 - -조립 코드를 사람이 써야 한다. 새 상태 기계를 추가하면 조립 지점도 함께 고쳐야 한다. - -조립을 잊으면 그 상태 기계가 없는 채로 배포된다. 스캔은 그 실수를 자동으로 막아 주지만 명시 조립은 그렇지 않다. - -얻는 것 - -데이터소스와 트랜잭션 경계가 조립 지점에서 명시된다. - -능력이 꺼진 배포에서 이 클래스들이 조건 평가 대상이 되지 않는다. - -## 근거 - -- **활성 트랜잭션 검사가 data source를 묻지 않아 다른 커넥션에서 커밋됐다** - 조립 결정이 흩어졌을 때의 결과다. -- **꺼짐은 조건의 반복이 아니라 구조여야 한다** - 같은 계열의 조립 원칙이다. -- **Bean 애너테이션이 있다는 것은 조립 증거가 아니다** - 명시 조립을 확인할 때 쓰는 규칙이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/owner-safe-state-machines/reference/reference-digest-must-be-length-framed-and-versioned.md b/docs/clean-architecture-backend-template/tech-log-studio/owner-safe-state-machines/reference/reference-digest-must-be-length-framed-and-versioned.md deleted file mode 100644 index e558bc4..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/owner-safe-state-machines/reference/reference-digest-must-be-length-framed-and-versioned.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -kind: REFERENCE -slug: digest-must-be-length-framed-and-versioned -title: digest는 길이 프레이밍하고 버전을 붙인다 -topic: owner-safe-state-machines -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:digest-must-be-length-framed-and-versioned -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# digest는 길이 프레이밍하고 버전을 붙인다 - -## 목적 - -다이제스트가 서로 다른 입력에 대해 같은 값을 내거나, 구성이 바뀐 뒤 옛 값과 비교되는 것을 막는다. - -## 규칙 - -1. 무엇을 덮는지가 정책이다 - 다이제스트가 빠뜨린 입력은 두 개의 다른 대상이 같은 값을 낼 수 있는 입력이다. 그 목록은 구현 세부가 아니라 정책이므로 자기 타입을 갖는다. - -2. 결과를 덮는다 - 누가 언제 했는지만 덮으면 무엇을 했는지가 다른 두 전이가 같아진다. - -3. 길이 프레이밍한다 - 구성 요소가 가변 길이 텍스트이고 그중 하나라도 이 플랫폼이 제약할 수 없는 값이면, 구분자로 이었을 때 서로 다른 목록이 한 문자열로 렌더링될 수 있다. - -4. 버전을 붙이고 구성이 바뀌면 올린다 - 저장된 다이제스트가 구성 경계를 넘어 비교되지 않게 한다. - -5. 비교 실패를 조용히 처리하지 않는다 - 버전이 다르면 같다고도 다르다고도 결론 내리지 않는다. - -## 적용 조건 - -재생 판정이나 중복 판정에 쓰이는 모든 다이제스트 - -멱등성 키와 요청 지문 - -## 예외 - -캐시 키처럼 충돌이 성능 문제일 뿐 정확성 문제가 아닌 경우는 이 규칙이 과하다. - -## 예시 - -전이 다이제스트가 전이 종류와 연산과 소유자와 시도와 리비전만 덮어, 재시도 가능한 실패와 포기한 실패가 같은 값을 냈다. 서로 다른 응답을 담은 두 완료도 마찬가지였다. - -소유자 토큰은 이 플랫폼이 형식을 제약하는 값이 아니므로 길이 프레이밍이 필요하다. - -## 관계 - -- **transition digest가 누가와 언제만 덮고 무엇을 덮지 않아 다른 전이를 같다고 보고했다** - 이 규칙을 만든 사례다. -- **서명된 커서의 구조와 검증 순서** - 같은 계열의 형식 결정을 다룬다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/owner-safe-state-machines/reference/reference-expired-claim-and-expired-execution-differ.md b/docs/clean-architecture-backend-template/tech-log-studio/owner-safe-state-machines/reference/reference-expired-claim-and-expired-execution-differ.md deleted file mode 100644 index 6a3c45b..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/owner-safe-state-machines/reference/reference-expired-claim-and-expired-execution-differ.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -kind: REFERENCE -slug: expired-claim-and-expired-execution-differ -title: 만료된 claim과 만료된 실행은 다르게 다뤄야 한다 -topic: owner-safe-state-machines -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:expired-claim-and-expired-execution-differ -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# 만료된 claim과 만료된 실행은 다르게 다뤄야 한다 - -## 목적 - -리스 만료를 한 가지로 처리해, 실행을 시작했던 소유자의 작업을 두 번 수행하는 것을 막는다. - -## 규칙 - -1. 만료된 청구는 인계한다 - 자리를 잡았지만 아직 아무것도 실행하지 않은 소유자를 밀어내도 외부 효과가 없다. - -2. 만료된 실행은 조정으로 넘긴다 - 그 소유자가 무엇을 어디까지 했는지 알 수 없다. 다시 실행하면 그 작업이 두 번 일어날 수 있다. - -3. 조정 결과는 별도 값이어야 한다 - 성공이나 실패로 접으면 그 구별이 사라진다. 결과 타입에 세 번째 변형이 필요하다. - -4. 해제도 같은 구별을 따른다 - 실행이 시작된 청구는 해제할 수 없다. 해제는 아직 실행하지 않은 청구에만 허용한다. - -5. 재시도 가능 실패는 인계 대상이다 - 그 상태는 이미 결과가 확정된 것이므로 새 소유자가 처음부터 시작해도 된다. - -## 적용 조건 - -청구와 실행을 별도 상태로 갖는 모든 상태 기계 - -멱등성 저장소와 인박스와 아웃박스 - -## 예외 - -실행이 외부 효과를 남기지 않는 것이 구조적으로 보장되면 두 상태를 같이 다뤄도 된다. 그 보장을 적어 둔다. - -## 예시 - -청구 결정 트리가 만료된 청구는 재설정하고 만료된 실행은 복구 필요로 답한다. 복구 필요 결과는 시도 번호를 함께 들고 간다. - -해제 경로는 이미 실행이 시작된 경우 실행 시작됨으로 답하고 해제하지 않는다. - -## 관계 - -- **만료된 CLAIMED는 takeover하고 만료된 EXECUTING은 조정을 요구하도록 갈랐다** - 이 규칙을 만든 사례다. -- **모르는 것은 성공도 실패도 아닌 세 번째 결과여야 한다** - 세 번째 규칙이 기대는 상위 규칙이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/query-and-pagination-models/concept/concept-adapter-inbound-graphql-c10.md b/docs/clean-architecture-backend-template/tech-log-studio/query-and-pagination-models/concept/concept-adapter-inbound-graphql-c10.md deleted file mode 100644 index 4019ab3..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/query-and-pagination-models/concept/concept-adapter-inbound-graphql-c10.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-graphql-c10 -title: 자동설정이 참조하는 것은 58개 파일 중 둘이다 -topic: query-and-pagination-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-graphql-c10 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-graphql-c10 - file: ../../../final/evidence/rendered/adapter-inbound-graphql-c10.svg - - key: adapter-inbound-graphql-c10-diagram - file: ../../../final/assets/diagrams/adapter-inbound-graphql-c10.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-graphql-c10.txt -source: - - 원본 분석 절은 final/document.md#a16#L719 이다. -module: adapter-inbound-graphql ---- - -# 자동설정이 참조하는 것은 58개 파일 중 둘이다 - -58개 main 파일 중 자동설정이 참조하는 것은 `GraphQlBatchPolicyRegistry`(4)와 `GraphQlDataLoaderFactory`(4) 둘이다. 나머지 56개는 autoconf=0이고, 그중 41개는 어떤 배선 경로에도 놓여 있지 않다. - -## 본문 - - - -58개 main 파일 중 자동설정이 참조하는 것은 **둘**이다 — `GraphQlBatchPolicyRegistry`(4) · `GraphQlDataLoaderFactory`(4). 나머지 56개는 autoconf=0이다. - -## 배선 경로가 닿는 패키지 - -:::evidence key="adapter-inbound-graphql-c10-diagram" alt="자동설정과 배치 로더 등록기에서 dataloader 로만 화살표가 가고, 나머지 세 패키지는 배선 경로 없음 이라고 이름 붙은 별도 영역 안에 화살표 없이 놓인다" caption="패키지별 도달 경로" zoom="false" -::: - -`dataloader`는 `runtime/GraphQlBatchLoaderRegistrar`를 통해 도달하므로 배선돼 있다. `fetch`·`pagination`·`mutation` 41개 파일은 어떤 배선 경로에도 없다. - -## GraphQlBatchPolicyRegistry 참조 위치 - -:::evidence key="adapter-inbound-graphql-c10" alt="코드베이스에서 GraphQlBatchPolicyRegistry 를 검색한 출력 20줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GraphQlBatchPolicyRegistry 코드베이스 검색 — 20줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/query-and-pagination-models/concept/concept-adapter-inbound-web-c06.md b/docs/clean-architecture-backend-template/tech-log-studio/query-and-pagination-models/concept/concept-adapter-inbound-web-c06.md deleted file mode 100644 index f6268ec..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/query-and-pagination-models/concept/concept-adapter-inbound-web-c06.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-web-c06 -title: durable-operation 게이트는 켤 수 없고 켜면 부팅이 실패한다 -topic: query-and-pagination-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-web-c06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-web-c06 - file: ../../../final/evidence/rendered/adapter-inbound-web-c06.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-web-c06.txt -source: - - 원본 분석 절은 final/document.md#a14#L762 이다. -module: adapter-inbound-web ---- - -# durable-operation 게이트는 켤 수 없고 켜면 부팅이 실패한다 - -`app.web-platform.durable-operations` 문자열은 저장소의 어떤 yaml에도 없다. 그리고 켜더라도 컨트롤러 생성자가 요구하는 `OperationQueryService` 빈을 선언하는 코드가 main·app-bootstrap에 없다. - -## 본문 - - - -`app.web-platform.durable-operations` 문자열은 저장소의 어떤 yaml에도 없다. `@ConditionalOnProperty`에 `matchIfMissing`이 없으므로 기본값은 꺼짐이다. - -## 켜더라도 생성자가 요구하는 빈이 없다 - -`OperationHttpController`의 생성자는 `OperationQueryService`를 요구한다. 그 빈을 선언하는 코드가 main·app-bootstrap에 없다(testkit에만 생성). §16.3과 같은 형태 — 게이트를 켜면 부팅이 실패한다. - -## OperationQueryService 참조 위치 - -:::evidence key="adapter-inbound-web-c06" alt="코드베이스에서 OperationQueryService 를 검색한 출력 13줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="OperationQueryService 코드베이스 검색 — 13줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/query-and-pagination-models/concept/concept-adapter-outbound-persistence-jpa-c27.md b/docs/clean-architecture-backend-template/tech-log-studio/query-and-pagination-models/concept/concept-adapter-outbound-persistence-jpa-c27.md deleted file mode 100644 index 4d98d15..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/query-and-pagination-models/concept/concept-adapter-outbound-persistence-jpa-c27.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c27 -title: JpaRepositoryFragmentSupport는 CRUD가 아니라 실행 정책이다 -topic: query-and-pagination-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c27 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c27 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c27.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c27.txt -source: - - 원본 분석 절은 final/document.md#a05#L1860 이다. -module: adapter-outbound-persistence-jpa ---- - -# JpaRepositoryFragmentSupport는 CRUD가 아니라 실행 정책이다 - -`JpaRepositoryFragmentSupport`는 domain-specific repository adapter가 사용할 공통 실행 support이며 범용 business repository contract는 제공하지 않는다. - -## 본문 - - - -`JpaRepositoryFragmentSupport`는 domain-specific repository adapter가 사용할 공통 실행 support다. 제공하는 것은 대략 네 가지다. - -- `EntityManager` access -- query name context -- fetch plan application -- bounded query observation scope - -범용 business repository contract는 제공하지 않는다. - -## JpaRepositoryFragmentSupport 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c27" alt="코드베이스에서 JpaRepositoryFragmentSupport 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="JpaRepositoryFragmentSupport 코드베이스 검색 — 5줄 · exit 0" zoom="true" -::: - -## application-core가 JPA를 알 필요가 없는 이유 - -이 구조는 Clean Architecture 관점에서 의미가 있다. application-core가 `JpaRepository`, `EntityManager`, `Specification`을 알 필요가 없고, 실제 domain repository port를 구현하는 outbound adapter 내부에서만 Spring Data/JPA mechanics를 사용한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/query-and-pagination-models/concept/concept-adapter-outbound-persistence-jpa-c28.md b/docs/clean-architecture-backend-template/tech-log-studio/query-and-pagination-models/concept/concept-adapter-outbound-persistence-jpa-c28.md deleted file mode 100644 index f492701..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/query-and-pagination-models/concept/concept-adapter-outbound-persistence-jpa-c28.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c28 -title: KeysetPredicateBuilder가 만드는 것은 사전식 술어다 -topic: query-and-pagination-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c28 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c28 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c28.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c28.txt -source: - - 원본 분석 절은 final/document.md#a05#L1933 이다. -module: adapter-outbound-persistence-jpa ---- - -# KeysetPredicateBuilder가 만드는 것은 사전식 술어다 - -`KeysetPredicateBuilder`는 conjunction이 아니라 lexicographic predicate를 만든다. 그래서 `(Instant, UUID)`처럼 term type이 다르고 direction도 다른 ordering을 표현할 수 있다. - -## 본문 - - - -`KeysetPredicateBuilder`는 conjunction이 아니라 lexicographic predicate를 만든다. - -## 커서 뒤가 뜻하는 조건 - -`(createdAt ASC, id DESC)`라면 cursor 뒤는 개념적으로 이렇다. - -```text -createdAt > cursorTime -OR -(createdAt = cursorTime AND id < cursorId) -``` - -현재 `KeysetTerm`는 각 term마다 expression, cursor value, direction을 가진다. 그래서 `(Instant, UUID)`처럼 term type이 다르고 direction도 다른 ordering을 표현할 수 있다. source history에는 과거 one-type/one-direction API가 mixed order에서 rows를 skip/repeat했던 이유가 주석으로 남아 있고, 현재 code/test는 이를 보완했다. - -## KeysetPredicateBuilder 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c28" alt="코드베이스에서 KeysetPredicateBuilder 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="KeysetPredicateBuilder 코드베이스 검색 — 3줄 · exit 0" zoom="true" -::: - -## 마지막 term의 유일성은 타입이 아니라 호출자 계약이다 - -builder는 "마지막 term이 unique tie-breaker여야 한다"고 문서화하지만 runtime에서 uniqueness를 증명할 metadata는 받지 않는다. 따라서 uniqueness는 caller/registry contract다. 현재 evidence만으로 이를 defect라 단정하지 않는다. platform이 이를 fail-closed invariant로 승격하려면 unique-key metadata까지 contract에 포함해야 한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/query-and-pagination-models/concept/concept-adapter-outbound-persistence-jpa-c29.md b/docs/clean-architecture-backend-template/tech-log-studio/query-and-pagination-models/concept/concept-adapter-outbound-persistence-jpa-c29.md deleted file mode 100644 index f4dbf5d..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/query-and-pagination-models/concept/concept-adapter-outbound-persistence-jpa-c29.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c29 -title: size + 1로 hasNext를 판정하고 COUNT를 없앤다 -topic: query-and-pagination-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c29 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c29 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c29.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c29.txt -source: - - 원본 분석 절은 final/document.md#a05#L1973 이다. -module: adapter-outbound-persistence-jpa ---- - -# size + 1로 hasNext를 판정하고 COUNT를 없앤다 - -`JpaKeysetQuerySupport`는 `size + 1`을 가져와 최대 `size`개를 반환하고 추가 1개로 `hasNext`를 판단한다. 이 path에는 `COUNT(*)`가 없다. - -## 본문 - - - -`JpaKeysetQuerySupport`의 실행 형태는 이렇다. - -```text -query.setMaxResults(page.fetchSize()) // size + 1 - -> result - -> KeysetSliceAssembler -``` - -반환은 최대 `size`개이고 추가 1개로 `hasNext`를 판단한다. 이 path에는 `COUNT(*)`가 없다. - -## JpaKeysetQuerySupport 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c29" alt="코드베이스에서 JpaKeysetQuerySupport 를 검색한 출력 6줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="JpaKeysetQuerySupport 코드베이스 검색 — 6줄 · exit 0" zoom="true" -::: - -## COUNT를 다시 붙이지 않는 이유 - -keyset을 도입해 OFFSET full-walk 비용을 줄여 놓고 total count로 다시 full-work를 추가하는 구조를 피한다. - -## 별도 레인에서만 확인한 것 - -실제 PostgreSQL readiness query도 `(occurred_at,id) > (?,?) ORDER BY ... LIMIT ?` 형태와 representative index 사용을 별도 integration lane에서 검증한다. 해당 entire integration lane 자체는 later sub-scope 11의 denominator이므로 여기서는 cross-scope evidence로만 사용한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/query-and-pagination-models/concept/concept-application-core-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/query-and-pagination-models/concept/concept-application-core-c01.md deleted file mode 100644 index cf06d65..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/query-and-pagination-models/concept/concept-application-core-c01.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -kind: CONCEPT -slug: application-core-c01 -title: application-core가 아는 유일한 프로젝트 의존은 shared-contract다 -topic: query-and-pagination-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:application-core-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: application-core-c01 - file: ../../../final/evidence/rendered/application-core-c01.svg - - key: application-core-c01-diagram - file: ../../../final/assets/diagrams/application-core-c01.svg -evidence: - - ../../../final/evidence/raw/application-core-c01.txt -source: - - 원본 분석 절은 final/document.md#a03#L58 이다. -module: application-core ---- - -# application-core가 아는 유일한 프로젝트 의존은 shared-contract다 - -`build.gradle`의 production project dependency는 `:shared-contract` 하나뿐이다. 실행 정책은 use case 타입이 아니라 `@UseCaseCapability`에 선언되고, `CleanArchitectureTest`가 그 선언과 실제 호출의 일치를 검사한다. - -## 관계 - -- **legacy storage/notification compatibility surface의 제거 조건 추적** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -**Observed.** `build.gradle`의 production project dependency는 `:shared-contract` 하나뿐이다. application-core가 Spring, JPA, Redis, Kafka, filesystem provider 같은 구현 모듈을 직접 참조하지 않고, 외부 구현은 composition root와 adapter가 역으로 이 모듈의 port를 구현한다. - -## 의존이 흐르는 방향 - -:::evidence key="application-core-c01-diagram" alt="어댑터와 application-core 와 shared-contract 가 위에서 아래로 쌓이고 의존 방향 화살표가 아래쪽 하나로만 그려진 구조" caption="의존이 흐르는 한 방향" zoom="false" -::: - -## 실행 정책은 애너테이션에 따로 선언된다 - -`CommandUseCase`와 `QueryUseCase`는 `UseCase.handle(I)`를 write/read intent에 맞게 타입으로 좁힌다. 자체적으로 transaction을 열거나 security interceptor를 실행하지 않는다. 실행 정책은 `@UseCaseCapability`에 별도로 선언된다 — runtime TYPE annotation이며 `transactionMode`, `idempotency`, `repositoryAccess`를 필수로 받고 `externalOutboundAllowed`, `sensitiveRead`, `bulkWrite`, `crossTenantAdmin`을 추가 선언한다. - -## CleanArchitectureTest 참조 위치 - -:::evidence key="application-core-c01" alt="코드베이스에서 CleanArchitectureTest 를 검색한 출력 6줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="CleanArchitectureTest 코드베이스 검색 — 6줄 · exit 0" zoom="true" -::: - -## 적합성 함수가 검사하는 일곱 가지 - -annotation 자체는 metadata에 불과하지만 `CleanArchitectureTest`가 concrete Command/Query use case에 annotation 존재를 강제한다. 그 위에서 architecture fitness function이 다음 coherence를 직접 검사한다. - -- `READ_ONLY + READ_REPOSITORY`는 `TransactionPort.inRead`를 직접 호출해야 한다. -- `WRITE + WRITE_REPOSITORY`는 `inWrite` 또는 `inRootWrite`를 직접 호출해야 한다. -- `REQUIRES_NEW`는 `inNew`를 직접 호출해야 한다. -- `repositoryAccess != WRITE_REPOSITORY`인 use case가 repository write verb를 직접 호출하면 실패한다. -- `bulkWrite=true`는 `WRITE_REPOSITORY`를 요구한다. -- mutating use case는 type-level `@RequiresPermission`을 선언해야 한다. -- application/domain은 Spring Security에 의존할 수 없다. - -## 이 강제가 잡지 못하는 것 - -이 enforcement에는 의도적으로 한계가 있다. ArchUnit의 direct-call 분석이므로 helper 뒤에 숨은 repository mutation/transaction call은 잡지 못하고, AOP self-invocation/non-bean path도 static rule만으로 보장하지 않는다. 이 제한은 테스트 설명 자체에 명시돼 있어 최종 계약의 일부로 봐야 한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/redis-command-admission/case/case-a-build-gate-that-is-not-in-the-build.md b/docs/clean-architecture-backend-template/tech-log-studio/redis-command-admission/case/case-a-build-gate-that-is-not-in-the-build.md index a482f1c..a707e46 100644 --- a/docs/clean-architecture-backend-template/tech-log-studio/redis-command-admission/case/case-a-build-gate-that-is-not-in-the-build.md +++ b/docs/clean-architecture-backend-template/tech-log-studio/redis-command-admission/case/case-a-build-gate-that-is-not-in-the-build.md @@ -1,7 +1,7 @@ --- kind: CASE slug: a-build-gate-that-is-not-in-the-build -title: '"build gate"라 불리는 catalog drift 검사가 어디에서도 실행되지 않는다' +title: "build gate"라 불리는 catalog drift 검사가 어디에서도 실행되지 않는다 topic: redis-command-admission project: clean-architecture-backend-template status: 게시 전 diff --git a/docs/clean-architecture-backend-template/tech-log-studio/redis-command-admission/decision/decision-unclassified-commands-are-refused.md b/docs/clean-architecture-backend-template/tech-log-studio/redis-command-admission/decision/decision-unclassified-commands-are-refused.md deleted file mode 100644 index b261411..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/redis-command-admission/decision/decision-unclassified-commands-are-refused.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -kind: PROJECT_DECISION -slug: unclassified-commands-are-refused -title: 분류되지 않은 명령은 fail-closed로 거부한다 -topic: redis-command-admission -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: decision:unclassified-commands-are-refused -decisionStatus: ADOPTED -decidedOn: 2026-08-30 -source: - - src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/RedisCommandCatalog.java - - src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/CommandPolicyGuard.java - - final/document.md#a10 ---- - -# 분류되지 않은 명령은 fail-closed로 거부한다 - -## 결정문 - -명령 카탈로그가 분류하지 못하는 명령은 통과시키지 않고 거부한다. - -## 판단 이유 - -승인 아홉 단계는 명령이 무엇인지 아는 것을 전제한다. 위험 등급도 키 스펙도 슬롯 계산도 카탈로그의 정의에서 나온다. - -분류되지 않은 명령을 통과시키면 그 단계들이 적용되지 않은 채 실행된다. 위험 등급을 모르므로 허가 요구도 걸 수 없고, 키 스펙을 모르므로 네임스페이스 검사도 슬롯 계산도 할 수 없다. - -즉 통과는 검사를 건너뛰는 것과 같다. 그리고 그 사실이 호출자에게 보이지 않는다. - -거부는 시끄럽다. 새 명령을 쓰려면 카탈로그에 먼저 넣어야 한다. 그 마찰이 이 결정의 목적이다. - -카탈로그의 정의가 서버 메타데이터에서 온다는 것과 함께 보면 구조가 완성된다. 서버가 아는 명령만 카탈로그에 있고, 카탈로그에 있는 명령만 실행된다. - -## 영향 - -감수하는 것 - -새 Redis 명령을 쓰려면 카탈로그 갱신이 선행되어야 한다. 서버가 지원해도 바로 쓸 수 없다. - -카탈로그가 뒤처지면 정상적인 명령이 거부된다. 그래서 드리프트 검사가 필요하고, 그 검사가 현재 빌드에 없다. - -우회 경로가 있으면 이 결정이 그 경로에 적용되지 않는다. 다섯 어댑터가 게이트웨이를 직접 부르는 경로가 그렇다. - -얻는 것 - -정책이 적용되지 않은 명령이 실행되지 않는다. - -새 명령의 도입이 명시적 행위가 된다. - -## 근거 - -- **서버 메타데이터가 명령의 정의이고 정책 파일은 허용 범위다** - 이 결정이 기대는 관계다. -- **명령 카탈로그와 admission 아홉 단계** - 카탈로그가 없으면 적용될 수 없는 단계들이다. -- **의미 어댑터 다섯이 gateway를 직접 불러 admission 아홉 단계를 건너뛴다** - 이 결정이 적용되지 않는 경로다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/redis-command-admission/reference/reference-a-single-admission-point-must-count-its-bypasses.md b/docs/clean-architecture-backend-template/tech-log-studio/redis-command-admission/reference/reference-a-single-admission-point-must-count-its-bypasses.md deleted file mode 100644 index e6aaab1..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/redis-command-admission/reference/reference-a-single-admission-point-must-count-its-bypasses.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -kind: REFERENCE -slug: a-single-admission-point-must-count-its-bypasses -title: 단일 admission point는 우회 경로를 세어야 성립한다 -topic: redis-command-admission -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:a-single-admission-point-must-count-its-bypasses -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# 단일 admission point는 우회 경로를 세어야 성립한다 - -## 목적 - -단일 승인 지점이라는 선언을 그 지점이 실제로 유일하다는 증거로 읽는 것을 막는다. - -## 규칙 - -1. 선언은 두 가지를 함께 주장한다 - 지나는 것이 전부 검사된다는 것과 모든 것이 지난다는 것이다. 코드가 보장하는 것은 대개 첫 번째뿐이다. - -2. 하위 계층 타입을 직접 참조하는 곳을 센다 - 승인 지점이 감싸고 있는 타입을 상위 코드가 직접 부르면 그것이 우회다. - -3. 임포트 목록이 빠른 지표다 - 어떤 패키지에서 무엇을 가져오는지 집계하면 우회 여부가 드러난다. - -4. 우회가 있으면 선언을 좁히거나 경로를 막는다 - 둘 중 하나를 하지 않으면 다음 사람이 같은 오해를 한다. - -5. 컴파일 시점에 막을 수 있으면 그렇게 한다 - 하위 타입을 패키지 밖에서 볼 수 없게 하면 우회 경로가 생기지 않는다. - -## 적용 조건 - -단일 진입점이나 단일 승인 지점을 표방하는 모든 계층 - -정책과 실행이 분리된 구조 - -## 예외 - -성능이나 특수 목적으로 의도적으로 우회를 허용하는 경로가 있을 수 있다. 그 경우 어떤 검사가 생략되는지가 그 자리에 적혀 있어야 한다. - -## 예시 - -명령 정책 가드가 자기를 모든 명령이 지나는 단일 승인 지점이라고 적는다. 의미 어댑터 다섯이 가드도 실행기도 타입 API 도 참조하지 않고 게이트웨이를 30 회 직접 부른다. - -허가 출처 확인이 그 우회로 함께 건너뛰어진다. 가드는 애플리케이션이 허가 인터페이스를 직접 구현하는 경우까지 막도록 설계되어 있다. - -## 관계 - -- **의미 어댑터 다섯이 gateway를 직접 불러 admission 아홉 단계를 건너뛴다** - 이 규칙을 만든 사례다. -- **명령 카탈로그와 admission 아홉 단계** - 우회되는 대상이다. -- **중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다** - 같은 계열의 확인 규칙이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/redis-command-admission/reference/reference-server-metadata-defines-the-command.md b/docs/clean-architecture-backend-template/tech-log-studio/redis-command-admission/reference/reference-server-metadata-defines-the-command.md deleted file mode 100644 index ba60f41..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/redis-command-admission/reference/reference-server-metadata-defines-the-command.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -kind: REFERENCE -slug: server-metadata-defines-the-command -title: 서버 메타데이터가 명령의 정의이고 정책 파일은 허용 범위다 -topic: redis-command-admission -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:server-metadata-defines-the-command -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# 서버 메타데이터가 명령의 정의이고 정책 파일은 허용 범위다 - -## 목적 - -정책 파일이 명령의 정의 노릇을 해서, 서버가 실제로 하는 일과 어긋난 채 허용 판정이 내려지는 것을 막는다. - -## 규칙 - -1. 정의는 서버에서 온다 - 명령의 키 스펙과 플래그와 인자 구조는 서버 메타데이터가 정본이다. - -2. 정책 파일은 그 위에서 범위만 정한다 - 무엇이 허용되고 무엇이 위험한지를 적는다. 명령이 무엇인지를 다시 적지 않는다. - -3. 두 쪽의 드리프트를 검사한다 - 서버 버전이 올라가면 정의가 바뀔 수 있다. 정책 파일이 옛 정의 위에 서 있는지 확인하는 검사가 필요하다. - -4. 그 검사를 실제로 돌린다 - 게이트라고 부르는 것과 빌드에 있는 것은 다르다. - -5. 분류되지 않은 명령은 거부한다 - 카탈로그가 답하지 못하는 명령을 통과시키면 정책이 적용되지 않은 명령이 실행된다. - -## 적용 조건 - -명령 단위로 허용 여부를 판정하는 모든 데이터 저장소 클라이언트 - -서버 버전에 따라 명령 정의가 달라지는 환경 - -## 예외 - -서버 메타데이터를 조회할 수 없는 구성에서는 정의를 고정할 수밖에 없다. 그 경우 고정한 버전을 명시하고, 다른 버전에 붙었을 때의 동작을 정해 둔다. - -## 예시 - -명령 메타데이터 드리프트를 검사하는 코드가 있고 정책 파일 머리 주석이 그것을 빌드 게이트라고 부르지만, 그 검사를 실행하는 태스크도 CI 단계도 없다. - -## 관계 - -- **명령 카탈로그와 admission 아홉 단계** - 카탈로그가 승인에서 하는 역할이다. -- **build gate라 불리는 catalog drift 검사가 어디에서도 실행되지 않는다** - 네 번째 규칙이 필요한 사례다. -- **분류되지 않은 명령은 fail-closed로 거부한다** - 다섯 번째 규칙을 채택한 결정이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-adapter-inbound-grpc-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-adapter-inbound-grpc-c01.md deleted file mode 100644 index 1407f6e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-adapter-inbound-grpc-c01.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-grpc-c01 -title: 인증이 예외 변환 바깥에 있어도 진단이 새지 않는다 -topic: result-and-failure-algebra -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-grpc-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-grpc-c01 - file: ../../../final/evidence/rendered/adapter-inbound-grpc-c01.svg - - key: adapter-inbound-grpc-c01-diagram - file: ../../../final/assets/diagrams/adapter-inbound-grpc-c01.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-grpc-c01.txt -source: - - 원본 분석 절은 final/document.md#a15#L140 이다. -module: adapter-inbound-grpc ---- - -# 인증이 예외 변환 바깥에 있어도 진단이 새지 않는다 - -`ServerInterceptors.intercept(service, exceptionInterceptor, authenticationInterceptor)`는 인증을 예외 변환보다 바깥에 놓는다. 그런데도 인증 실패가 변환되지 않은 예외로 새지 않는 이유는 인증 인터셉터가 자기 예외를 스스로 삼키기 때문이다. - -## 본문 - - - -`ServerInterceptors.intercept(service, exceptionInterceptor, authenticationInterceptor)` — gRPC 규약상 **마지막 인터셉터의 `interceptCall`이 먼저** 호출되므로 인증이 바깥, 예외 처리가 안쪽이다. 등록할 때 적은 순서와 실제로 요청을 감싸는 순서가 뒤집혀 있다. - -## 바깥에서 안으로 놓이는 순서 - -:::evidence key="adapter-inbound-grpc-c01-diagram" alt="인증과 업무 처리와 예외 변환이 위에서 아래로 이어지는 구조" caption="인터셉터의 순서" zoom="false" -::: - -## 인증이 예외 변환 바깥에 있어도 되는 이유 - -인증 인터셉터가 예외 처리 바깥에 있는데도 안전한 이유는 그것이 스스로 예외를 삼키기 때문이다. CLAUDE.md의 약속("정책이 `false`를 반환하거나 예외를 던진 요청은 ... 안정적인 `UNAUTHENTICATED` status/code/category로 종료된다")이 코드와 일치하고, 정책이 `false`를 돌려준 경우와 예외를 던진 경우가 모두 같은 `call.close(Status.UNAUTHENTICATED.withDescription(OperationalError.UNAUTHENTICATED.code()), trailersFor(...))`로 끝난다. 정책 진단은 클라이언트에 닿지 않는다. - -두 인터셉터가 같은 일을 두 번 하는 구조가 아니다. 중복 아님. - -## 분석 원문이 적은 순서 판정 - -:::evidence key="adapter-inbound-grpc-c01" alt="분석 문서 final/document.md#a15 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a15 발췌 — 15줄" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-adapter-outbound-cache-redis-c09.md b/docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-adapter-outbound-cache-redis-c09.md deleted file mode 100644 index 553f001..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-adapter-outbound-cache-redis-c09.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-cache-redis-c09 -title: 구독 경로가 guard를 건너뛰고 대신 세우는 검사 -topic: result-and-failure-algebra -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-cache-redis-c09 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-cache-redis-c09 - file: ../../../final/evidence/rendered/adapter-outbound-cache-redis-c09.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-cache-redis-c09.txt -source: - - 원본 분석 절은 final/document.md#a10#L477 이다. -module: adapter-outbound-cache-redis ---- - -# 구독 경로가 guard를 건너뛰고 대신 세우는 검사 - -`PubSubOperationRequests`의 javadoc이 guard를 지나지 않는 경로를 스스로 밝히고, 그 자리에 어떤 검사를 대신 두었는지도 함께 적는다. 대체 검사는 실제로 존재한다. - -## 본문 - - - -`PubSubOperationRequests`의 javadoc이 예외를 스스로 밝힌다. 발행은 보통의 명령이라 guard를 지나지만 구독은 지나지 않고, 경계 지을 응답도 적용할 타임아웃도 없어서 guard가 검사할 대상 자체가 없다는 것이 그 이유다. 대체 검사가 실제로 있다. - -## 구독이 guard 대신 통과하는 세 검사 - -`channelTargets`·`shardTargets`·`patternTargets` 셋 다 빈 컬렉션을 거부하고 **모든 대상의 네임스페이스가 이 프로세스의 것과 같은지** 확인한다(`requireNamespace`, 다르면 "channel belongs to a namespace this process may not use"). `patternTargets`는 추가로 `context.sdkPermit(PATTERN_SUBSCRIBE)`를 호출한다. 팬아웃 폭을 요청이 아니라 서버가 정하기 때문이다. - -## 코드베이스에 남은 PubSubOperationRequests 참조 - -:::evidence key="adapter-outbound-cache-redis-c09" alt="코드베이스에서 PubSubOperationRequests 를 검색한 출력 10줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="PubSubOperationRequests 코드베이스 검색 — 10줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-domain-core-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-domain-core-c03.md deleted file mode 100644 index 7e46b5a..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-domain-core-c03.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -kind: CONCEPT -slug: domain-core-c03 -title: 실패가 런타임이 아니라 빌드에서 발생한다 -topic: result-and-failure-algebra -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:domain-core-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: domain-core-c03 - file: ../../../final/evidence/rendered/domain-core-c03.svg -evidence: - - ../../../final/evidence/raw/domain-core-c03.txt -source: - - 원본 분석 절은 final/document.md#a01#L158 이다. -module: domain-core ---- - -# 실패가 런타임이 아니라 빌드에서 발생한다 - -이 module의 runtime executable behavior는 매우 작다. annotation 자체에는 success/failure path가 없고 interface도 implementation을 가지지 않으므로, 주요 failure mechanics는 build-time architecture violation이다. - -## 본문 - - - -이 module의 runtime executable behavior는 매우 작다. annotation 자체에는 success/failure path가 없고, interface도 implementation을 가지지 않는다. 그래서 주요 failure mechanics는 **build-time architecture violation**이다. - -## 위반이 걸리는 규칙 이름 - -| 위반 | 잡는 규칙 | -|---|---| -| forbidden framework/domain dependency | `DOMAIN_IS_PURE` | -| domain logger dependency | `DOMAIN_HAS_NO_LOGGER` | -| public no-arg value object | `VALUE_OBJECTS_HAVE_NO_PUBLIC_NO_ARG_CONSTRUCTOR` | -| public `set*` aggregate mutator | `AGGREGATE_ROOT_SETTERS_ARE_NOT_PUBLIC` | -| non-record domain event | `DOMAIN_EVENTS_ARE_RECORDS` | -| enumerated transport dependency | `DOMAIN_EVENTS_ARE_TRANSPORT_FREE` | -| `ResourceId`에 할당할 수 없는 `id` field raw type | `NO_LONG_ID_PK` | -| registry에 없는 project dependency | `verifyCleanArchitectureDependencies` | -| production -> `sample-portfolio` edge | settings registry validation · root dependency verification · cross-module ArchUnit rule | - -## 코드베이스의 ResourceId 참조 - -:::evidence key="domain-core-c03" alt="코드베이스에서 ResourceId 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ResourceId 코드베이스 검색 — 2줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-messaging-cloudevents-c08.md b/docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-messaging-cloudevents-c08.md deleted file mode 100644 index a1bc490..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-messaging-cloudevents-c08.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-cloudevents-c08 -title: public 시그니처에 나오는 타입은 api로 선언한다 -topic: result-and-failure-algebra -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-cloudevents-c08 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-cloudevents-c08 - file: ../../../final/evidence/rendered/messaging-cloudevents-c08.svg -evidence: - - ../../../final/evidence/raw/messaging-cloudevents-c08.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-cloudevents#L451 이다. -module: messaging-cloudevents ---- - -# public 시그니처에 나오는 타입은 api로 선언한다 - -build.gradle 주석이 이전 결함 하나를 보존한다. `implementation`으로 선언한 탓에 이 module의 public API에 등장하는 타입이 소비자에게는 숨겨졌던 사례이고, 그것이 `src/messaging/CLAUDE.md:40-43`의 게이트를 만든 사례군이다. - -## 관계 - -- **왕복이라 부르는 테스트는 무엇을 보존하지 않는지도 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **문서가 범위를 제한하면 그 제한을 누가 강제하는지 같이 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -build.gradle 주석이 이전 결함 하나를 보존한다. 의존성을 `implementation`으로 선언했는데 그 타입이 이 module의 public API에 등장하고 있었다. - -> "Declared `implementation`, the type appeared in this module's public API while the dependency was hidden from consumers: an adopter calling the documented method could not name its return type without adding CloudEvents to their own build, and Gradle gave them no hint why. A type in a public signature is part of the artifact's contract." - -이것이 `src/messaging/CLAUDE.md:40-43`의 게이트를 만든 사례군에 속한다 — "source에서 public/protected 시그니처에 등장하는 vendor 라이브러리를 뽑아 그 leaf의 `build.gradle`이 `api`로 선언했는지 대조". 이 leaf는 그 게이트를 두 좌표로 나눠 통과한 모범 사례다. - -## 하지 않기로 기록된 두 매핑 - -코드 주석이 남긴 두 매핑 결정(§4.2)도 실패 이력의 성격을 갖는다 — "defaulted to the production instant"와 "inventing a tombstone"은 하지 않기로 한 것들이다. - -## 이 기록이 다루는 파일 범위 - -:::evidence key="messaging-cloudevents-c08" alt="코드베이스에서 파일 목록을 만든 출력 3줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 3줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-messaging-inbox-jdbc-postgresql-c07.md b/docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-messaging-inbox-jdbc-postgresql-c07.md deleted file mode 100644 index 630cacc..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-messaging-inbox-jdbc-postgresql-c07.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-inbox-jdbc-postgresql-c07 -title: 커넥션을 요청하기 전에 거절하도록 고정한 테스트 -topic: result-and-failure-algebra -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-inbox-jdbc-postgresql-c07 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-inbox-jdbc-postgresql-c07 - file: ../../../final/evidence/rendered/messaging-inbox-jdbc-postgresql-c07.svg -evidence: - - ../../../final/evidence/raw/messaging-inbox-jdbc-postgresql-c07.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-inbox-jdbc-postgresql#L581 이다. -module: messaging-inbox-jdbc-postgresql ---- - -# 커넥션을 요청하기 전에 거절하도록 고정한 테스트 - -한 결함이 두 파일에 기록돼 있고, 그중 하나가 그것을 막는 테스트다. 그 테스트는 거절 시점까지 고정한다 — DataSource가 커넥션을 요청받는 순간 테스트가 실패하도록 만들어 두었다. - -## 관계 - -- **컬럼 폭은 애플리케이션 검증과 짝을 이룬다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 안전 규칙은 한 공식과 한 강제 시점을 갖는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -**한 결함이 두 파일에 기록돼 있고, 그중 하나가 그것을 막는 테스트다.** `JdbcInboxRepository.reserve(Connection,…)`의 javadoc과 `JdbcInboxTransactionRequirementTest`의 javadoc이 같은 결함을 각각 구현 쪽과 테스트 쪽에서 서술한다. - -## 거절 시점을 데이터 소스로 고정한다 - -그 테스트가 "hermetic: the refusal has to happen before any connection is requested, and the data source below fails the test by being asked for one"이라고 자기 설계를 적는다. 거절이 커넥션 요청보다 먼저 일어나야 한다는 요구를, 요청받는 것만으로 실패하는 데이터 소스를 아래에 두어 검사 순서까지 고정한 것이다. - -## 이 기록이 다루는 파일 범위 - -:::evidence key="messaging-inbox-jdbc-postgresql-c07" alt="코드베이스에서 파일 목록을 만든 출력 6줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 6줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-messaging-pulsar-experimental-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-messaging-pulsar-experimental-c01.md deleted file mode 100644 index 8fc00a4..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-messaging-pulsar-experimental-c01.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-pulsar-experimental-c01 -title: 알 수 없는 실패의 기본값을 모호로 둔다 -topic: result-and-failure-algebra -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-pulsar-experimental-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-pulsar-experimental-c01 - file: ../../../final/evidence/rendered/messaging-pulsar-experimental-c01.svg - - key: messaging-pulsar-experimental-c01-diagram - file: ../../../final/assets/diagrams/messaging-pulsar-experimental-c01.svg -evidence: - - ../../../final/evidence/raw/messaging-pulsar-experimental-c01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-pulsar-experimental#L50 이다. -module: messaging-pulsar-experimental ---- - -# 알 수 없는 실패의 기본값을 모호로 둔다 - -분류는 타입 있는 신호만 본다. `PulsarPreSendRejection`이면 REJECTED, `TimeoutException`이면 시간 초과, 나머지는 전부 AMBIGUOUS다. javadoc이 이전 구현과 그 결함을 적는다. - -## 본문 - - - -javadoc 이 이전 구현과 그 결함을 적는다. 예전에는 예외의 클래스 단순 이름을 읽어서 `"Timeout"`이면 모호, 그 밖이면 거절로 분류했다. - -> "Classification used to read the exception's class simple name: `"Timeout"` meant ambiguous, anything else meant rejected. A class name is not part of Pulsar's contract — it changes between client versions — and defaulting the unknown case to `REJECTED` tells the caller nothing was transmitted, which is how the same entry is published to the bookies twice." - -기본값이 모호로 바뀐 것이 핵심이다. 알 수 없는 실패에서 안전한 방향은 모호다. - -## 분류가 갈라지는 자리 - -:::evidence key="messaging-pulsar-experimental-c01-diagram" alt="사전 거절과 그 밖의 실패가 나란히 놓이고 모호가 아래에 놓인 구조" caption="분류의 기본값" zoom="false" -::: - -## 성공을 영수증이 아니라 복제 증거로 적는다 - -확인된 성공은 복제 증거로 기록된다 — 전송 미래가 설정된 수의 저장 노드에 기록된 뒤에야 해소되므로 영수증이 아니라 복제 증거다. - -## 이 기록이 다루는 파일 범위 - -:::evidence key="messaging-pulsar-experimental-c01" alt="코드베이스에서 파일 목록을 만든 출력 8줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 8줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-messaging-schema-api-c07.md b/docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-messaging-schema-api-c07.md deleted file mode 100644 index 8e64eb5..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-messaging-schema-api-c07.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-schema-api-c07 -title: 검사의 위치와 키가 틀렸던 두 결함 -topic: result-and-failure-algebra -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-schema-api-c07 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-schema-api-c07 - file: ../../../final/evidence/rendered/messaging-schema-api-c07.svg -evidence: - - ../../../final/evidence/raw/messaging-schema-api-c07.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-schema-api#L432 이다. -module: messaging-schema-api ---- - -# 검사의 위치와 키가 틀렸던 두 결함 - -코드 주석이 이전 결함 둘을 보존한다. 두 사례 다 형태가 같다 — 검사가 없었던 게 아니라 검사의 위치/키가 틀렸다. - -## 관계 - -- **port 계약은 동시성 요구를 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **도달성 판정은 단어가 아니라 import로 확인한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -코드 주석이 보존한 이전 결함 둘은 형태가 같다 — **검사가 없었던 게 아니라 검사의 위치/키가 틀렸다.** - -| 위치 | 이전 상태 | 그것이 만든 실패 | -|---|---|---| -| `BoundedByteSink` javadoc | 각 codec이 무제한 버퍼에 직렬화 후 길이 비교 | 한도가 **보고 임계값**일 뿐 할당 경계가 아님 → 팽창하는 payload 하나가 consumer 프로세스를 죽임 | -| `MessageContractKey` javadoc | 타입만으로 registry 키 | v999가 v1 클래스로 디코딩되고 v999 라벨을 유지 → 하위 게이트·감사 기록이 등록된 적 없는 버전을 서술 | - -`messaging-core-api` §13의 "문자 vs 바이트, 정확일치 vs 세그먼트" 목록과 같은 계열이다. - -## 이 기록이 다루는 파일 범위 - -:::evidence key="messaging-schema-api-c07" alt="코드베이스에서 파일 목록을 만든 출력 10줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 10줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-messaging-testkit-c06.md b/docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-messaging-testkit-c06.md deleted file mode 100644 index 8b8b56b..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-messaging-testkit-c06.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-testkit-c06 -title: 아는 척하지 않기 위해 던지는 자리와 삼키는 자리 -topic: result-and-failure-algebra -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-testkit-c06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-testkit-c06 - file: ../../../final/evidence/rendered/messaging-testkit-c06.svg -evidence: - - ../../../final/evidence/raw/messaging-testkit-c06.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-testkit#L492 이다. -module: messaging-testkit ---- - -# 아는 척하지 않기 위해 던지는 자리와 삼키는 자리 - -이 리프의 실패 처리 원칙은 하나다: 모르는 것을 아는 척하지 않는다. 그 원칙이 어떤 자리에서는 예외를 던지는 방향으로, 다른 자리에서는 예외를 삼키는 방향으로 나타난다. - -## 본문 - - - -이 리프의 실패 처리 원칙은 하나다: **모르는 것을 아는 척하지 않는다.** 같은 원칙이 어느 자리에서는 예외를 던지는 쪽으로, 다른 자리에서는 예외를 삼키는 쪽으로 나타난다. - -## 등록되지 않은 어댑터와 기록 없는 커버리지 - -`CompatibilityMatrix.of("messaging-artemis")` 는 던지고(`anUnknownAdapterIsNotSilentlyTreatedAsSupported`), `matrix.coverageOf("messaging-artemis", …)` 는 `NOT_COVERED` 를 돌려준다(`aFaultThatWasNeverRecordedReadsAsUncoveredRatherThanPassing`). 전자는 "지원 목록에 없는 것을 지원인 척"을 막고, 후자는 "기록 없음"이 곧 "커버 안 됨"이라는 자연스러운 읽기다. - -## DockerAvailability가 어떤 예외든 삼키는 이유 - -`Class.forName("org.testcontainers.DockerClientFactory")` 를 리플렉션으로 부르고 어떤 예외든 `false` 로 삼킨다(`:26-34`). 이 리프가 testcontainers 에 의존하지 않으면서 그 존재를 물어볼 수 있게 하는 유일한 방법이고, 결과를 `static final` 로 1회만 캐시한다. - -## DockerAvailability 참조 위치 - -:::evidence key="messaging-testkit-c06" alt="코드베이스에서 DockerAvailability 를 검색한 출력 16줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DockerAvailability 코드베이스 검색 — 16줄 · exit 0" zoom="true" -::: - -## 인증 레인만 가드 없이 실패한다 - -**"skip 은 성공이 아니다"** 라는 반대 규칙이 인증 레인에는 적용되어 있다. 일반 컨테이너 스위트는 `DockerAvailability` 로 skip 하고, 인증 레인만 **가드 없이 실패**한다. 대신 `test` 태그에서 빼서 노트북 빌드를 깨지 않는다. 두 규칙이 충돌하지 않게 배치되어 있다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-messaging-transport-spi-c07.md b/docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-messaging-transport-spi-c07.md deleted file mode 100644 index 53b26e5..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-messaging-transport-spi-c07.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-transport-spi-c07 -title: 오래 돌 때만 드러나는 수명주기 결함 네 개 -topic: result-and-failure-algebra -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-transport-spi-c07 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-transport-spi-c07 - file: ../../../final/evidence/rendered/messaging-transport-spi-c07.svg -evidence: - - ../../../final/evidence/raw/messaging-transport-spi-c07.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-transport-spi#L579 이다. -module: messaging-transport-spi ---- - -# 오래 돌 때만 드러나는 수명주기 결함 네 개 - -코드 주석이 네 결함을 보존한다. 전부 장기 실행에서만 드러나는 종류다 — 세대 회전이 겹칠 때, 닫힌 세대가 목록에 남을 때, 회전 없이 종료할 때, 이중 해제가 계수를 음수로 만들 때. - -## 관계 - -- **멱등 종료를 보장하는 컴포넌트는 종료 이후의 등록도 정의한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 개념의 sentinel은 계층을 넘어 하나로 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -코드 주석이 네 결함을 보존한다. 전부 **장기 실행에서만 드러나는** 종류다. - -| 위치 | 이전 상태 | 그것이 만든 실패 | -|---|---|---| -| `Generation.retiredAt` javadoc | 호출자가 넘긴 하나의 `retiredAt`을 전체 draining 목록에 적용 | 드레인 중 회전이 겹치면, 방금 은퇴한 세대를 강제 종료하거나 오래된 세대에 새 마감을 주거나 — 호출자가 우연히 넘긴 타임스탬프에 좌우 | -| `closeExpiredDraining` 주석 | 이미 닫힌 세대가 목록에 잔류 | `drainingCount()`가 끝난 작업을 영원히 보고 | -| `close()` javadoc | 회전이 은퇴시킨 것만 닫음 | 회전 없이 종료한 프로세스가 브로커 연결을 JVM 종료에 맡김 → 미전송 producer 배치 소실, consumer 세션이 그룹을 떠나지 않고 브로커에서 타임아웃 | -| `endWork` javadoc | clamp 없음 | 이중 해제가 계수를 음수로 → 작업이 도는 중에 드레인 완료로 보고 | - -## 0번 해제와 2번 해제가 같은 등급인 이유 - -네 번째와 `LeakTrackingRuntime.closeCount()` javadoc("Closing twice is as much a defect as never closing")이 같은 주제를 반대편에서 말한다 — **해제는 정확히 한 번이어야 하고, 0번도 2번도 결함이다.** - -## 이 기록이 다루는 파일 범위 - -:::evidence key="messaging-transport-spi-c07" alt="코드베이스에서 파일 목록을 만든 출력 13줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 13줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-shared-contract-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-shared-contract-c03.md deleted file mode 100644 index 2561580..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/result-and-failure-algebra/concept/concept-shared-contract-c03.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: shared-contract-c03 -title: 추정하지 않는 두 경계 — 스위치 해석과 상태 투영 -topic: result-and-failure-algebra -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:shared-contract-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: shared-contract-c03 - file: ../../../final/evidence/rendered/shared-contract-c03.svg -evidence: - - ../../../final/evidence/raw/shared-contract-c03.txt -source: - - 원본 분석 절은 final/document.md#a02#L108 이다. -module: shared-contract ---- - -# 추정하지 않는 두 경계 — 스위치 해석과 상태 투영 - -`MasterSwitchParser`는 operator configuration을 permissive coercion하지 않는 fail-closed contract다. `RedisHealthSnapshotProvider`는 공유 경계로 내보낼 필드를 좁히고, 조회하지 않은 값을 관측인 것처럼 적지 않는다. - -## 본문 - - - -`MasterSwitchParser`는 unset=false, true/false case-insensitive만 허용하며 `yes`, `1`, `on`, whitespace-padded value를 invalid로 처리한다. canonical+legacy가 동시에 있으면 값이 같아도 ambiguous로 실패하고 legacy-only는 replacement property를 반환한다. 이는 operator configuration을 permissive coercion하지 않는 fail-closed contract다. - -## MasterSwitchParser 참조 위치 - -:::evidence key="shared-contract-c03" alt="코드베이스에서 MasterSwitchParser 를 검색한 출력 11줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MasterSwitchParser 코드베이스 검색 — 11줄 · exit 0" zoom="true" -::: - -## 상태 스냅샷이 공유 경계로 내보내는 필드 - -`RedisHealthSnapshotProvider`는 Redis client/connection/credential을 shared boundary로 새지 않게 role/capability/state/reason/semantic freshness만 projection한다. - -## eviction policy가 관측이 아니라 기대인 이유 - -eviction policy는 runtime CONFIG 조회 증명이 아니라 configured expectation임을 enum 이름과 javadoc으로 명시한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/retention-and-unbounded-growth/case/case-a-replay-store-with-no-eviction-path.md b/docs/clean-architecture-backend-template/tech-log-studio/retention-and-unbounded-growth/case/case-a-replay-store-with-no-eviction-path.md deleted file mode 100644 index a29b595..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/retention-and-unbounded-growth/case/case-a-replay-store-with-no-eviction-path.md +++ /dev/null @@ -1,117 +0,0 @@ ---- -kind: CASE -slug: a-replay-store-with-no-eviction-path -title: 재생 저장소에 제거 경로가 없고, 형제 맵의 상한은 세션 경계 하나다 -topic: retention-and-unbounded-growth -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a-replay-store-with-no-eviction-path -evidenceCapturedOn: 2026-09-02 -assets: - - key: a-replay-store-with-no-eviction-path - file: ../../../final/evidence/rendered/a-replay-store-with-no-eviction-path.svg -evidence: - - ../../../final/evidence/raw/a-replay-store-with-no-eviction-path.txt -source: - - 분석 문서는 grpc-policy 편 §17.3 과 grpc-advanced-streaming 편 §17.1 이다. 앞의 절이 재생 저장소의 제거 경로 부재를 P2 로 판정하고, 두 모듈의 대조가 "저쪽은 풀었고 이쪽은 안 풀었다"가 아니라는 정정도 그 절에 있다. 뒤의 절이 형제 맵의 증가를 P3 으로 기록하고, 필요한 창이 좁다는 근거를 함께 적는다. - - 같은 절이 이 저장소가 커밋될 때마다 항목이 쌓인다고 적는데, 이 클래스를 잡는 프로덕션 코드는 아직 없다. 위 터미널 출력의 참조 수가 그것이다. ---- - -# 재생 저장소에 제거 경로가 없고, 형제 맵의 상한은 세션 경계 하나다 - -결과 재생 저장소가 항목을 넣기만 하고 지우지 않는다. 형제 모듈의 중복 제거기가 같은 형태이고, 그 클래스는 같은 종류의 무제한 증가를 비판하는 javadoc 을 갖고 있다. - -## 관계 - -- **bounded 와 unbounded 오버로드를 나란히 둔 포트** - 같은 Topic 의 보존 경계 문제다. -- **같은 안전 규칙은 하나의 공식과 하나의 강제 시점을 갖는다** - 보존 경계를 어디서 정하는지에 대한 규칙이다. -- **runtime_memberships를 먼저 읽고 심각도를 정한다** - 이 사례가 오늘의 사고가 아닌 이유를 판정하는 규칙이다. - -## 문제 - -정확히 한 번을 보장하려면 과거를 기억해야 한다. 같은 요청이 다시 오면 저장된 결과를 돌려주고, 같은 메시지가 다시 오면 건너뛴다. - -그 기억이 자료구조다. 요청 식별자나 메시지 식별자를 키로, 결과나 체크포인트를 값으로 갖는다. - -## 결론 - -결과 재생 저장소에는 넣는 경로만 있다. 항목 하나의 크기는 제한하고 개수는 제한하지 않는다. 제거도 비우기도 축출도 만료도 없다. - -형제 모듈의 중복 제거기는 같은 형태에 상한 하나를 갖는다. 세션을 끝낼 때 그 세션 키를 지운다. 그래서 이쪽의 증가는 세션 수명에 묶이고, 저장소 쪽은 프로세스 수명에 묶인다. - -두 모듈의 대조는 한쪽이 풀고 한쪽이 안 푼 것이 아니다. 같은 형태의 무제한 증가를 둘 다 갖고 있고, 한쪽만 부분적 상한을 갖는다. 무거운 쪽은 저장소다. - -이 종류의 자료구조에서 보존 경계는 기능이 아니라 전제다. 무엇을 언제까지 기억하는지가 정해지지 않으면 그 보장은 메모리가 버티는 동안만 성립한다. - -두 모듈 모두 런타임 소속이 비어 있고, 저장소 쪽은 이 클래스를 잡는 프로덕션 코드도 없다. 지금 도는 배포에서 일어나는 문제는 아니다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Gradle : 9.0.0 -확인 방식 : 두 자료구조의 삽입 경로와 제거 경로 대조 -소스 수정 : x - -## 재현 조건 - -1. 재생 저장소의 맵 선언과 항목을 넣는 메서드를 읽는다. -2. 같은 파일에서 제거·비우기·축출·만료를 검색하고, 이 클래스를 잡는 프로덕션 코드를 센다. -3. 개수를 노출하는 메서드가 있는지, 그 값을 읽는 프로덕션 코드가 있는지 센다. -4. 중복 제거기의 클래스 javadoc 과 두 맵의 증가·감소 지점을 확인한다. -5. 중복 제거 키의 형식을 확인해 세션 종료가 실제로 그 세션 항목만 지우는지 본다. -6. 두 모듈의 런타임 소속을 모듈 레지스트리에서 읽는다. - -## 본문 - - - -## 크기는 제한하고 개수는 제한하지 않는다 - -:::evidence key="a-replay-store-with-no-eviction-path" alt="코드베이스에서 결과 재생 저장소의 맵 선언과 항목 크기 제한, 제거·비우기·축출·만료 매치 수, 개수를 노출하는 메서드와 그 값을 읽는 코드 수, 이 클래스를 잡는 코드 수, javadoc 이 저장소를 부르는 이름을 뽑은 출력. 이어서 형제 모듈 중복 제거기의 클래스 javadoc 과 두 맵의 선언, 항목이 쌓이는 지점, 재생 판정이 보는 값, 이 파일의 제거 매치 수와 그 둘이 있는 자리, 중복 제거 키의 형식, 그리고 두 모듈의 런타임 소속이 나온다. 저장소 쪽 제거 매치가 0 이고 형제 맵 쪽은 세션 종료 하나뿐이라는 것이 그 출력에 보인다." caption="저장소: 크기 제한만 · 제거 0 · 잡는 코드 0 / 형제 맵: 메시지마다 한 항목 · 제거는 세션 종료 하나 · 키는 세션 접두 — 54줄" zoom="true" -::: - -결과 재생 저장소는 결과 참조를 키로, 직렬화된 응답을 값으로 갖는 맵이다. - -넣는 메서드가 크기를 검사한다. 인라인 한도를 넘는 응답은 예외로 거절하고, 객체 참조 뒤에 두라고 메시지에 적는다. - -개수를 검사하는 코드는 없다. 제거도 비우기도 축출도 만료도 파일 전체에서 0 이다. - -이 클래스를 잡는 프로덕션 코드도 아직 0 이다. 배선되면 멱등 키가 필요한 커밋 하나당 항목 하나가 프로세스 수명 동안 남는 형태다. - -## 개수를 볼 수는 있는데 보는 곳이 없다 - -개수를 돌려주는 메서드가 하나 있다. 그것을 부르는 프로덕션 코드가 0 이고, 부르는 것은 테스트 하나다. - -javadoc 은 이 저장소를 배포가 고를 수 있는 선택지 중 하나로 소개하면서 작은 인라인 저장소라고 부른다. 작게 유지하는 장치는 그 안에 없다. - -## 형제 모듈이 같은 문제를 문장으로 적어 두었다 - -중복 제거기의 클래스 javadoc 이 설계 선택을 설명한다. 본 키를 모으는 집합을 거부한 이유가 셋인데, 첫 번째가 집합은 세션 수명 동안 무제한으로 증가한다는 것이다. - -체크포인트 맵은 그 비판을 지킨다. 세션당 항목 하나이고 순번만 앞으로 간다. - -형제 맵은 지키지 않는다. 적용된 메시지마다 결과 참조를 하나씩 넣는다. - -같은 javadoc 에는 그 맵에 대해 재생 결과가 체크포인트 뒤의 좁은 창 동안만 유지된다고 한 줄 적혀 있다. 창을 닫는 코드가 없다 — 체크포인트가 그 순번을 지나가도 항목은 남는다. - -필요한 창이 실제로 좁다는 것도 코드에 있다. 재생 판정은 체크포인트의 순번 비교가 내리고, 재생 응답에 쓰이는 것은 그 순번의 결과 하나다. 체크포인트보다 한참 뒤처진 순번의 결과가 필요한 구간은 재개 직후뿐이다. - -## 줄어드는 지점은 세션 종료 하나다 - -이 파일에서 무언가를 지우는 지점은 둘이고 둘 다 같은 메서드 안에 있다. 세션을 끝내면서 체크포인트를 지우고, 재생 결과 맵에서 그 세션 접두사를 가진 키를 지운다. - -중복 제거 키는 세션 값과 세대와 순번을 막대로 이어 만든다. 접두사 제거가 그 세션의 모든 세대를 함께 지운다. - -그래서 이쪽의 증가는 세션 수명에 묶인다. 저장소 쪽은 프로세스 수명에 묶인다. - -두 모듈 모두 모듈 레지스트리의 런타임 소속이 비어 있다. 배선 경로가 없으니 오늘 이것으로 무너지는 배포는 없다. - -## 확인하지 못한 것 - -오래 돌려 증가를 확인해 보지는 않았다. 배선 경로가 없어 실행 대상이 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/retention-and-unbounded-growth/case/case-the-cleanup-that-causes-the-outage-it-prevents.md b/docs/clean-architecture-backend-template/tech-log-studio/retention-and-unbounded-growth/case/case-the-cleanup-that-causes-the-outage-it-prevents.md deleted file mode 100644 index 0e11fdc..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/retention-and-unbounded-growth/case/case-the-cleanup-that-causes-the-outage-it-prevents.md +++ /dev/null @@ -1,98 +0,0 @@ ---- -kind: CASE -slug: the-cleanup-that-causes-the-outage-it-prevents -title: cleanup 이 스스로 막겠다고 적은 장애를 일으키는 형태로 호출된다 -topic: retention-and-unbounded-growth -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:the-cleanup-that-causes-the-outage-it-prevents -evidenceCapturedOn: 2026-09-01 -assets: - - key: the-cleanup-that-causes-the-outage-it-prevents - file: ../../../final/evidence/rendered/the-cleanup-that-causes-the-outage-it-prevents.svg -evidence: - - ../../../final/evidence/raw/the-cleanup-that-causes-the-outage-it-prevents.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-inbox-jdbc-postgresql §17 P1 · final/document.md#a19-messaging-outbox-jdbc-postgresql §17 P1 이다. ---- - -# cleanup 이 스스로 막겠다고 적은 장애를 일으키는 형태로 호출된다 - -inbox 와 outbox 의 정리 잡이 무제한 삭제 오버로드를 부른다. 배치 제한 구현은 두 리프 모두에 있고 호출 지점이 0이다. 잡의 javadoc 이 실행되는 코드의 동작을 그대로 서술한다. - -## 관계 - -- **bounded 와 unbounded 오버로드를 나란히 둔 포트** - 이 사례가 속한 구조다. -- **두 형태를 나란히 내놓는 포트는 이미 안전하지 않은 쪽을 고른 것이다** - 이 사례가 만든 규칙이다. -- **그 속성을 보일 수 없는 대역 위에서 통과한 테스트는 증인이 아니다** - 회귀 테스트가 성립하지 않는 이유다. - -## 문제 - -inbox 와 outbox 는 처리된 행을 보존 기간 동안 남긴다. 그 기간이 지나면 정리 잡이 지운다. - -한 번에 얼마나 지우는지가 이 잡의 안전을 정한다. 몇 주 동안 쌓인 테이블에서 조건에 맞는 행 전부를 한 문장으로 지우면, 그 문장이 끝날 때까지 락을 잡고 백로그 크기에 비례해 WAL 을 쓴다. - -## 결론 - -두 정리 잡이 무제한 오버로드를 부른다. - -배치 제한 구현은 두 리프 모두에 있다. 건수 제한과 잠긴 행 건너뛰기를 쓰는 SQL 이고, 저장소 전체에서 그 시그니처가 등장하는 곳은 선언 둘, 구현 둘, 테스트 대역 다섯이며 호출 지점이 0이다. - -inbox 정리 잡의 javadoc 이 실행되는 코드의 동작을 그대로 서술한다. - -> A single unbounded DELETE over a table that has been accumulating for weeks holds locks long enough to block the very reservations the inbox exists to serve, so the cleanup would cause the outage it is meant to prevent. - -정리가 막으려던 장애를 정리가 일으킨다는 문장이고, 그것이 지금 호출되는 형태다. - -두 리프 다 출하 애플리케이션 소속이고 두 잡 다 스타터 빈이다. 다만 스케줄러가 등록되지 않으며 그것은 의도된 설계다. 그래서 상시 결함이 아니라 잠재 결함이고, 애플리케이션이 문서 지시대로 잡을 스케줄하는 순간 첫 스윕에서 발현한다. - -회귀 테스트를 쓰려면 대역도 함께 고쳐야 한다. 현재 대역의 배치 제한 구현은 무제한 결과와 제한값 중 작은 쪽을 돌려주는 형태다. 전부 지우고 숫자만 깎으므로 두 형태의 차이를 재현하지 못한다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Gradle : 9.0.0 -확인 방식 : 두 오버로드의 선언과 구현 위치 확인, 저장소 전역 호출 지점 검색 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/294-bounded-purge-never-called.txt 에 있다. - -1. 두 포트에서 정리 연산의 두 오버로드 선언을 확인한다. -2. 각 구현의 SQL 에서 건수 제한과 잠금 건너뛰기 유무를 확인한다. -3. 배치 제한 시그니처를 저장소 전역에서 검색해 호출 지점을 센다. -4. 두 정리 잡이 어느 오버로드를 부르는지 확인한다. - -## 본문 - - - -두 cleanup 잡이 무제한 오버로드를 부른다. bounded 구현은 `LIMIT` + `FOR UPDATE SKIP LOCKED` 로 두 리프 모두에 존재하고 호출 지점이 0 이다. - -## InboxCleanupJob 참조 위치 - -:::evidence key="the-cleanup-that-causes-the-outage-it-prevents" alt="코드베이스에서 InboxCleanupJob 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="InboxCleanupJob 코드베이스 검색 — 3줄 · exit 0" zoom="true" -::: - -## javadoc 이 실행되는 코드의 동작을 그대로 서술한다 - -"A single unbounded DELETE over a table that has been accumulating for weeks holds locks long enough to block the very reservations the inbox exists to serve, so the cleanup would cause the outage it is meant to prevent." bounded 쪽 javadoc 은 한 발 더 나가, 배치 크기로 제한된다는 잡의 자기 서술을 참으로 만드는 것이 바로 이 파라미터라고 적는다. 그 파라미터를 아무도 넘기지 않는다. - -## 상시 결함이 아니라 잠재 결함이다 - -두 리프 다 `app-bootstrap` 소속이고 두 잡 다 스타터 빈이지만 스케줄러가 등록되지 않는다 — 그것이 의도된 설계다. 애플리케이션이 문서 지시대로 잡을 스케줄하는 순간 첫 스윕에서 발현한다. - -## 회귀 테스트가 성립하려면 대역도 고쳐야 한다 - -현재 대역의 bounded 구현은 전부 지우고 숫자만 깎는 형태라 차이를 재현하지 못한다. - -## 확인하지 못한 것 - -백로그가 쌓인 실제 테이블에서 두 형태의 락 보유 시간을 측정하지 않았다. 호출 지점 부재와 두 SQL 의 형태로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/retention-and-unbounded-growth/concept/concept-bounded-and-unbounded-side-by-side.md b/docs/clean-architecture-backend-template/tech-log-studio/retention-and-unbounded-growth/concept/concept-bounded-and-unbounded-side-by-side.md deleted file mode 100644 index 14d2f26..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/retention-and-unbounded-growth/concept/concept-bounded-and-unbounded-side-by-side.md +++ /dev/null @@ -1,91 +0,0 @@ ---- -kind: CONCEPT -slug: bounded-and-unbounded-side-by-side -title: bounded 와 unbounded 오버로드를 나란히 둔 포트 -topic: retention-and-unbounded-growth -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:bounded-and-unbounded-side-by-side -evidenceCapturedOn: 2026-09-01 -assets: - - key: bounded-and-unbounded-side-by-side - file: ../../../final/evidence/rendered/bounded-and-unbounded-side-by-side.svg - - key: bounded-and-unbounded-side-by-side-diagram - file: ../../../final/assets/diagrams/bounded-and-unbounded-side-by-side.svg -evidence: - - ../../../final/evidence/raw/bounded-and-unbounded-side-by-side.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-reliability-api §17 · final/document.md#a19-messaging-inbox-jdbc-postgresql §17 이다. ---- - -# bounded 와 unbounded 오버로드를 나란히 둔 포트 - -두 포트가 각각 정리 연산의 무제한 형태와 배치 제한 형태를 오버로드로 나란히 선언한다. 두 형태를 가르는 표시가 없어서 호출자가 인자가 적은 쪽을 골랐다. - -## 관계 - -- **두 형태를 나란히 내놓는 포트는 이미 안전하지 않은 쪽을 고른 것이다** - 이 구조에서 나온 규칙이다. -- **cleanup 이 스스로 막겠다고 적은 장애를 일으키는 형태로 호출된다** - 이 구조가 실제로 발현한 사례다. -- **같은 개념의 두 어휘가 공존하면 하나를 죽은 것으로 표시한다** - 같은 계열의 기존 규칙이다. - -## 본문 - - - -두 포트가 각각 `purge*Before(Instant)` 와 `purge*Before(Instant, int)` 를 나란히 선언한다. 뒤쪽이 안전한 형태이고 그 이유가 javadoc 에 있다 — "The cleanup jobs describe themselves as bounded by batch size; this is the parameter that makes that true." 두 형태를 가르는 표시는 없다. - -## 두 오버로드를 가르는 표시 - -:::evidence key="bounded-and-unbounded-side-by-side-diagram" alt="무제한 오버로드와 가르는 표시 없음이 왼쪽에, 배치 제한 오버로드와 javadoc 근거가 오른쪽에 같은 축으로 놓인다" caption="두 오버로드를 가르는 표시" zoom="false" -::: - -## 호출자가 인자가 적은 쪽을 고른다 - -`@Deprecated` 도, 이름 차이도, 호출을 막는 가시성 차이도 없다. - -## 두 오버로드의 javadoc - -:::evidence key="bounded-and-unbounded-side-by-side" alt="분석 문서 final/document.md#a19-messaging-reliability-api 에서 이 기록의 근거 절을 그대로 잘라낸 18줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a19-messaging-reliability-api 발췌 — 18줄" zoom="true" -::: - -## 결함이 호출자에게 있지 않다 - -같은 리프가 다른 곳에서 같은 형태를 이미 기록했다 — 한 인터페이스가 같은 전이의 두 세대를 갖고 안전하지 않은 쪽에 표시가 없다. 포트의 형태가 오용을 가능하게 했고, 두 리프에서 같은 방향으로 발생했다. - -:::note - -없음 — 두 포트의 선언과 저장소 전역 호출 지점을 전수 확인했다 - -::: - -## 두 형태 - -정리 연산은 기준 시각 이전의 행을 지운다. 무제한 형태는 시각만 받고, 배치 제한 형태는 시각과 최대 건수를 받는다. - -두 형태의 차이는 SQL 에서 드러난다. 제한 형태는 건수 제한과 잠긴 행 건너뛰기를 함께 쓴다. 한 번에 지우는 양이 정해져 있으므로 락 보유 시간과 WAL 기록량이 백로그 크기와 무관해진다. - -## 안전한 쪽이 무엇을 참으로 만드는가 - -배치 제한 오버로드의 javadoc 이 자기 존재 이유를 적는다. - -> The cleanup jobs describe themselves as bounded by batch size; this is the parameter that makes that true. - -정리 잡이 자신을 배치 크기로 제한된다고 서술하는데, 그것을 참으로 만드는 것이 바로 이 파라미터라는 뜻이다. - -## 표시가 없다 - -두 형태를 가르는 것이 인자 개수뿐이다. 폐기 표시도, 이름 차이도, 호출을 막는 가시성 차이도 없다. - -그래서 호출자는 짧은 쪽을 고른다. 그것은 호출자의 부주의가 아니라 포트가 만든 기본값이다. 인자를 하나 더 넘기려면 그 값을 어디서 가져올지 정해야 하고, 그 결정을 미루는 것이 자연스러운 선택이 된다. - -## 같은 리프가 같은 형태를 이미 기록했다 - -이 포트에는 같은 전이의 두 세대 메서드도 나란히 있다. 안전하지 않은 쪽에 폐기 표시가 없고, production 은 신세대만 쓰지만 그것을 강제하는 것은 없다. - -두 사례가 한 인터페이스 안에 있다는 점이 이 개념의 요지다. 포트의 형태가 오용을 가능하게 했고, 같은 방향으로 두 번 발생했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/retention-and-unbounded-growth/reference/reference-a-fake-that-cannot-show-the-property-is-not-a-witness.md b/docs/clean-architecture-backend-template/tech-log-studio/retention-and-unbounded-growth/reference/reference-a-fake-that-cannot-show-the-property-is-not-a-witness.md deleted file mode 100644 index 7593159..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/retention-and-unbounded-growth/reference/reference-a-fake-that-cannot-show-the-property-is-not-a-witness.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -kind: REFERENCE -slug: a-fake-that-cannot-show-the-property-is-not-a-witness -title: 그 속성을 보일 수 없는 대역 위에서 통과한 테스트는 증인이 아니다 -topic: retention-and-unbounded-growth -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:a-fake-that-cannot-show-the-property-is-not-a-witness -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# 그 속성을 보일 수 없는 대역 위에서 통과한 테스트는 증인이 아니다 - -## 목적 - -테스트 대역이 실제 구현의 불변식을 재현하지 못하면 그 위에서 통과한 단언은 그 불변식에 대해 아무 말도 하지 않는다. - -## 규칙 - -1. 단언하는 속성이 협력자의 상태 변화에 달려 있는지 본다 - 달려 있지 않으면 이 규칙은 해당하지 않는다. - -2. 대역이 결함 있는 구현과 올바른 구현을 구분할 수 있는지 묻는다 - 구분하지 못하면 그 테스트는 회귀를 잡지 못한다. - -3. 대역이 흉내 내는 불변식을 문장으로 쓴다 - 쓸 수 없으면 대역이 무엇을 재현하는지 저자도 모르는 상태다. - -4. 이름이 속성을 주장하면 대역을 먼저 읽는다 - 이름이 강할수록 그 공백이 눈에 띄지 않는다. - -## 적용 조건 - -저장소, 브로커, 파일 시스템처럼 상태를 갖는 협력자의 인메모리 대역 전부. - -## 예외 - -협력자의 존재만 필요한 테스트에는 해당하지 않는다. 호출이 일어났는지만 확인하는 경우가 그렇다. - -## 예시 - -배치 제한 정리 대역이 무제한 결과와 제한값 중 작은 쪽을 돌려준다. 전부 지우고 숫자만 깎으므로 두 형태의 차이를 보일 수 없다. - -리드라이브 대역의 정착 메서드가 정착 목록에 추가만 하고 준비 목록을 줄이지 않는다. 실제 불변식은 정착된 것이 다음 조회에서 사라지는 것이고, 재개 지점 계산이 정확히 그 성질에 달려 있다. - -청구 대역의 저장 메서드가 삽입을 흉내 내서, 실제 구현이 갱신으로 동작한다는 차이를 가린다. - -## 관계 - -- **cleanup 이 스스로 막겠다고 적은 장애를 일으키는 형태로 호출된다** - 이 규칙이 회귀 테스트를 막는 사례다. -- **재개된 리드라이브가 옮기지 못한 메시지를 건너뛰고 성공으로 닫힌다** - 대역이 불변식을 재현하지 못해 결함이 오래 남은 사례다. -- **배정 식별자 때문에 insert-first 청구가 UPSERT 가 되어 커밋된 결과를 덮었다** - 같은 형태의 세 번째 사례다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/retention-and-unbounded-growth/reference/reference-a-port-that-offers-both-forms-has-chosen-the-unsafe-one.md b/docs/clean-architecture-backend-template/tech-log-studio/retention-and-unbounded-growth/reference/reference-a-port-that-offers-both-forms-has-chosen-the-unsafe-one.md deleted file mode 100644 index 4e061f5..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/retention-and-unbounded-growth/reference/reference-a-port-that-offers-both-forms-has-chosen-the-unsafe-one.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -kind: REFERENCE -slug: a-port-that-offers-both-forms-has-chosen-the-unsafe-one -title: 두 형태를 나란히 내놓는 포트는 이미 안전하지 않은 쪽을 고른 것이다 -topic: retention-and-unbounded-growth -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:a-port-that-offers-both-forms-has-chosen-the-unsafe-one -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# 두 형태를 나란히 내놓는 포트는 이미 안전하지 않은 쪽을 고른 것이다 - -## 목적 - -인터페이스가 만든 기본값을 호출자의 선택으로 오인하지 않는다. 안전하지 않은 형태가 컴파일되면 그것은 언젠가 호출된다. - -## 규칙 - -1. 안전하지 않은 쪽을 부르는 것이 컴파일되는지 본다 - 컴파일되면 그 형태는 기본값이다. - -2. 두 형태를 가르는 표시가 있는지 본다 - 폐기 표시, 이름 차이, 가시성 차이 중 아무것도 없으면 호출자는 짧은 쪽을 고른다. - -3. 두 형태가 진짜로 다른 용도인지 묻는다 - 다르면 이름이 그 차이를 말해야 한다. 오버로드로 두는 것은 차이를 이름에서 지우는 선택이다. - -4. 같은 용도라면 하나를 지운다 - 지울 수 없으면 폐기 표시와 함께 부를 조건을 javadoc 에 적는다. - -## 적용 조건 - -정리, 삭제, 조회처럼 결과 크기가 데이터 양에 비례하는 모든 연산. 그리고 같은 전이의 두 세대가 공존하는 인터페이스. - -## 예외 - -두 형태가 서로 다른 호출자 집단을 위한 것이면 공존이 맞다. 그때 판정 기준은 이름이 그 구분을 담고 있는지다. - -## 예시 - -inbox 와 outbox 포트가 정리 연산의 무제한 형태와 배치 제한 형태를 오버로드로 나란히 선언한다. 두 정리 잡이 무제한 쪽을 부르고, 배치 제한 쪽은 호출 지점이 0이다. - -같은 포트에 같은 전이의 두 세대 메서드가 있다. 안전하지 않은 쪽에 폐기 표시가 없다. - -## 관계 - -- **bounded 와 unbounded 오버로드를 나란히 둔 포트** - 이 규칙이 나온 구조다. -- **cleanup 이 스스로 막겠다고 적은 장애를 일으키는 형태로 호출된다** - 이 규칙을 어긴 결과다. -- **같은 개념의 두 어휘가 공존하면 하나를 죽은 것으로 표시한다** - 같은 계열의 규칙이고, 이쪽은 어휘가 아니라 시그니처에 대한 것이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/retention-and-unbounded-growth/reference/reference-one-formula-and-one-enforcement-point-per-safety-rule.md b/docs/clean-architecture-backend-template/tech-log-studio/retention-and-unbounded-growth/reference/reference-one-formula-and-one-enforcement-point-per-safety-rule.md deleted file mode 100644 index ad646ae..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/retention-and-unbounded-growth/reference/reference-one-formula-and-one-enforcement-point-per-safety-rule.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -kind: REFERENCE -slug: one-formula-and-one-enforcement-point-per-safety-rule -title: 같은 안전 규칙은 하나의 공식과 하나의 강제 시점을 갖는다 -topic: retention-and-unbounded-growth -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:one-formula-and-one-enforcement-point-per-safety-rule -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# 같은 안전 규칙은 하나의 공식과 하나의 강제 시점을 갖는다 - -## 목적 - -같은 종류의 안전 여유가 여러 곳에서 독립적으로 정해지면 공식과 강제 시점이 갈린다. 값의 주인을 한 곳으로 정한다. - -## 규칙 - -1. 이 값을 바꾸려면 몇 군데를 고쳐야 하는지 센다 - 하나가 아니면 나머지는 조용히 옛 값으로 남는다. - -2. 공식이 같은지 본다 - 같은 관계를 곱셈과 덧셈으로 다르게 표현하고 있으면 둘 중 하나는 근거가 없다. - -3. 강제 시점이 같은지 본다 - 한쪽은 항상 검증하고 다른 쪽은 특정 객체를 만들 때만 검증하면, 그 객체를 만들지 않는 배포는 검증을 받지 않는다. - -4. 값이 아니라 관계가 소유자를 가져야 하는 경우를 구분한다 - 층마다 다른 값이 정당하면 값이 아니라 그 관계를 한 곳에 두고 검증한다. - -## 적용 조건 - -보존 기간, 마감, 재시도 상한, 배치 크기처럼 안전을 위해 고른 모든 수치. - -## 예외 - -클라이언트 마감이 서버 마감보다 짧아야 하는 것처럼 층마다 다른 값이 필요한 경우. 그때는 값이 아니라 관계가 한 곳에 있어야 하고, 그 관계를 검증하는 코드가 있어야 한다. - -## 예시 - -inbox 보존 여유가 한 곳에서는 곱셈으로, 다른 곳에서는 덧셈으로 표현된다. 한쪽은 정리 잡을 만들 때만 검증하고 다른 쪽은 항상 검증한다. - -드레인 마감 30초가 세 곳에서 각자 정해진다. 공개 상수 하나, 다른 클래스의 비공개 복사본 하나, 생성자 인자 하나. 테스트가 붙드는 것은 공개 상수뿐이고, 살아 있는 값은 비공개 복사본이다. - -## 관계 - -- **재생 저장소에 제거 경로가 없고, 형제 맵의 상한은 세션 경계 하나다** - 보존 경계가 아예 없는 쪽의 사례다. -- **순서 계약을 구현하는 것이 없고, 그것을 검증한다는 테스트는 enum 선언 순서만 본다** - 드레인 마감 세 곳이 그 사건의 일부다. -- **문서의 수치는 세지 말고 파생하거나 게이트로 붙든다** - 같은 규칙의 문서 판이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/case/case-a05-f005-jparetrypolicy.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/case/case-a05-f005-jparetrypolicy.md deleted file mode 100644 index bee9779..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/case/case-a05-f005-jparetrypolicy.md +++ /dev/null @@ -1,132 +0,0 @@ ---- -kind: CASE -slug: a05-f005-jparetrypolicy -title: 애플리케이션이 넣은 재시도 정책이 한 번도 불리지 않고 작업만 두 번 돈다 -topic: runtime-contract-correctness -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a05-f005-jparetrypolicy -evidenceCapturedOn: 2026-09-02 -body: case-a05-f005-jparetrypolicy.body.md -assets: - - key: a05-f005-jparetrypolicy - file: ../../../final/evidence/rendered/a05-f005-jparetrypolicy.svg - - key: a05-f005-jparetrypolicy-probe - file: ../../../final/evidence/rendered/a05-f005-jparetrypolicy-probe.svg -evidence: - - ../../../final/evidence/raw/a05-f005-jparetrypolicy.txt - - ../../../final/evidence/raw/a05-f005-jparetrypolicy-probe.txt -source: - - 분석 문서는 persistence-jpa 편 §20 이다. 이것을 단순한 죽은 필드가 아니라 공개 조립 팩토리가 제공하는 기능이 동작하지 않는 계약 결함으로 규정하고 P2 로 둔다. §7.6 이 API 스코프에서 먼저 도달 불가 후보로 올리고 트랜잭션 스코프로 넘긴 흔적이 남아 있다. ---- - -# 애플리케이션이 넣은 재시도 정책이 한 번도 불리지 않고 작업만 두 번 돈다 - -조립 팩토리가 애플리케이션이 제공한 재시도 정책을 받는 오버로드를 공개한다. 그 정책은 대체 정책 필드에 담기고, 그 필드를 읽는 분기는 호출자 프로파일의 재시도 프로파일이 널일 때만 열린다. 프로파일 타입이 그것을 널로 두지 못하게 한다. - -## 관계 - -- **재시도 구현이 둘이고 정교한 쪽을 아무도 호출하지 않는다** - 같은 코디네이터의 다른 공백이다. -- **재시도 단위는 statement가 아니라 유스케이스 전체다** - 이 정책이 참여하는 결정이다. -- **@Bean이 있다는 것은 조립 증거가 아니다** - 빈으로 등록된 확장점이라도 실제로 불리는지는 따로 세어 봐야 한다. - -## 문제 - -트랜잭션 자동설정 팩토리는 오버로드를 둘 갖는다. 정책을 스스로 만드는 쪽과 밖에서 받는 쪽이다. - -## 결론 - -정책 오버로드는 실제로 매번 불린다. 프로파일 오버로드가 하는 일이 그 호출 한 줄뿐이라서다. 그 자리를 채우는 호출자가 저장소 어디에도 없다. - -넣으면 어떻게 되는지 실행으로 확인했다. 언제나 재시도 금지를 돌려주는 정책과 호출 카운터를 넣고, 두 번까지 허용하는 유효한 프로파일로 직렬화 실패를 던지는 작업을 돌렸다. 정책은 0 번, 작업은 2 번 불렸다. 정책은 말할 기회를 얻지 못했고 작업은 두 번 실행됐다. - -그 정책이 담기는 필드의 javadoc 이 사연을 적는다. 예전 구현은 시도 횟수만 호출자에게서 받고 적격성과 백오프는 이 정책으로 덮었다. 재시도 하나를 프로파일 둘이 나눠 결정하게 되면서, 어느 쪽을 읽어도 실제 동작을 못 맞히게 됐다. 현재 형태가 그 수정의 결과다. - -같은 javadoc 이 이 필드를 호출이 자기 재시도 프로파일을 주지 않을 때만 쓰인다고 적는다. 프로파일 타입의 컴팩트 생성자가 그 필드에 널 아님을 요구하므로, 문서에 남은 발동 조건이 코드에서는 성립하지 않는다. - -앞쪽 오버로드의 javadoc 에는 두 값을 따로 두면 서로 어긋난 조합이 만들어지고 운영자 눈에는 그것이 설정 실수가 아니라 고장으로 보이기 때문에, 정책과 예산을 같은 프로파일에서 만든다고 적혀 있다. 그 문단 바로 아래에 정책만 따로 받는 오버로드가 놓여 있다. - -플랫폼 자신도 같은 문에 걸린다. 런타임 자동설정이 시도 3회짜리 기본 프로파일로 정책을 만들어 넘기고, 그 정책은 대체 정책 필드에 담긴 뒤 읽히지 않는다. - -예외도 경고도 없다. 코디네이터는 정책을 받았는지 널 검사만 하고, 그 정책이 쓰이지 않는다는 것은 어디서도 드러나지 않는다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 두 오버로드와 분기 열람, 애플리케이션 정책을 넣은 코디네이터 실행, 호출 계수 -소스 수정 : x - -## 재현 조건 - -1. 자동설정의 두 오버로드를 읽는다. 앞의 것이 뒤의 것을 부르는지 확인한다. -2. 뒤의 오버로드가 넘긴 정책이 담기는 필드와, 그 필드를 읽는 분기를 찾는다. -3. 프로파일 타입의 컴팩트 생성자가 그 분기의 조건을 허용하는지 확인한다. -4. 호출 카운터를 단 정책과 유효한 프로파일로 코디네이터를 세워 실패하는 작업을 실행한다. -5. 정책 호출과 작업 호출을 센다. - -## 본문 - - - -트랜잭션 자동설정 팩토리는 코디네이터를 만드는 오버로드를 둘 갖는다. 하나는 재시도 프로파일을 받고, 다른 하나는 애플리케이션이 준 정책을 받는다. - -## 그 정책은 대체 정책 필드로 간다 - -:::evidence key="a05-f005-jparetrypolicy" alt="자동설정의 두 조립 팩토리와 앞쪽의 설계 근거 javadoc, 뒤쪽 오버로드가 넘긴 정책이 담기는 필드와 그 필드의 javadoc, 그 필드를 읽는 유일한 분기, 프로파일 타입의 컴팩트 생성자, 그리고 조립 팩토리 호출 전수와 코디네이터를 직접 생성하는 곳이 넘기는 정책을 출력한 터미널 기록." caption="두 오버로드와 앞쪽의 설계 근거 · 정책이 담기는 대체 정책 필드와 그 사연 · 분기는 널일 때만 열림 · 프로파일 생성자가 널을 금지 · 밖에서 넣은 정책 0 — 55줄 · exit 0" zoom="true" -::: - -앞쪽 오버로드의 javadoc 이 이유를 적는다. 정책과 예산은 같은 프로파일에서 만든다, 일부러 그렇게 한다. 따로 설정하면 재시도하라는 정책과 한 번만 허용하는 예산이 만나고, 그것은 오설정이 아니라 재시도가 고장 난 것처럼 보인다는 것이다. - -바로 그 아래에 정책만 따로 받는 오버로드가 있다. 그 오버로드가 넘긴 정책은 `fallbackRetryPolicy` 필드에 담긴다. - -그 필드를 읽는 자리는 하나다. - -```java -JpaRetryPolicy retryPolicy = - profile.retryProfile() == null - ? fallbackRetryPolicy - : DefaultJpaRetryPolicy.forProfile(profile.retryProfile()); -``` - -그리고 프로파일 타입의 컴팩트 생성자가 `retryProfile` 에 널 아님을 요구한다. 왼쪽 가지가 열리는 조건을 타입이 금지한다. - -## 그 필드는 잊힌 것이 아니라 강등된 것이다 - -필드 javadoc 이 사연을 적는다. 코디네이터는 예전에 이 정책의 적격성과 백오프를 모든 호출에 적용하면서 시도 횟수만 호출자 프로파일에서 가져왔다. 프로파일 둘이 재시도 하나를 나눠 결정했고, 어느 쪽을 읽든 실제 동작을 잘못 예측하게 됐다. - -지금 형태는 그 수정의 결과다. 같은 javadoc 이 이 필드를 호출이 자기 재시도 프로파일을 주지 않을 때만 쓰인다고 적는데, 그 조건이 곧 타입이 금지하는 것이다. 발동 조건이 문서에 남고 코드에서 사라졌다. - -## 그 자리에 애플리케이션 정책이 들어온 적이 없다 - -정책 오버로드 자체는 매번 불린다. 프로파일 오버로드의 몸통이 그것을 부르는 한 줄이다. - -밖에서 자기 정책을 넣는 호출은 프로덕션에도 테스트에도 없다. 유일한 프로덕션 진입은 프로파일 쪽이고, 테스트는 코디네이터를 직접 세우면서도 프로파일에서 만든 기본 정책만 넘긴다. - -## 넣으면 어떻게 되는지 실행으로 확인했다 - -:::evidence key="a05-f005-jparetrypolicy-probe" alt="애플리케이션이 준 정책과 호출 카운터, 두 번까지 허용하는 유효한 프로파일을 코디네이터에 넣고 직렬화 실패를 던지는 작업을 실행한 결과와, 플랫폼 런타임 자동설정이 자기 빈에 넣는 기본 프로파일을 출력한 터미널 기록." caption="언제나 재시도 금지를 돌려주는 정책과 카운터 투입 · 정책 호출 0 · 작업 호출 2 · 플랫폼 기본 프로파일도 같은 자리로 — 30줄 · exit 0" zoom="true" -::: - -언제나 재시도 금지를 돌려주는 정책에 호출 카운터를 달았다. 프로파일은 두 번까지 허용하는 유효한 것이고, 작업은 직렬화 실패를 던진다. - -정책 호출 0, 작업 호출 2 다. 정책은 재시도하지 말라고 말할 기회를 얻지 못했고, 작업은 호출자 프로파일에 따라 두 번 실행됐다. 예외도 경고도 없었다. 코디네이터가 하는 검증은 정책을 받았다는 널 아님 확인뿐이고, 그것이 쓰이지 않는다는 신호는 아니다. - -## 플랫폼 자신도 같은 문에 걸린다 - -런타임 자동설정이 `jpa-platform-default` 라는 이름과 시도 3회짜리 프로파일로 정책을 만들어 넘긴다. 그 정책도 대체 정책 필드에 담긴 뒤 읽히지 않는다. - -실행마다 쓰이는 것은 호출자가 준 프로파일에서 그 자리에서 만든 정책이다. 도달하지 못하는 것은 공개된 확장점만이 아니라 플랫폼이 자기 빈에 넣은 기본값이기도 하다. - -## 고칠 방향은 셋이다 - -생성자가 받은 정책을 권위로 삼고 예산만 프로파일에서 계산하거나, 오버로드를 없애고 재시도 프로파일이 유일한 출처임을 API 로 못박거나, 정책과 프로파일을 하나의 객체로 합치는 것이다. 분석 문서는 지금 형태가 가장 나쁘다고 적는다. 설정 출처를 둘 받아 놓고 한쪽을 조용히 버리기 때문이다. - -## 확인하지 못한 것 - -실제 데이터베이스를 붙인 통합 실행에서 같은 결과가 나오는지는 확인하지 않았다. 탐침은 트랜잭션 관리자를 대역으로 세운 단위 실행이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/case/case-grpc-core-api-f04.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/case/case-grpc-core-api-f04.md deleted file mode 100644 index 9994bfe..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/case/case-grpc-core-api-f04.md +++ /dev/null @@ -1,84 +0,0 @@ ---- -kind: CASE -slug: grpc-core-api-f04 -title: 하나의 상태 코드가 같은 메서드 안에서 두 답을 갖는다 -topic: runtime-contract-correctness -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-core-api-f04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-core-api-f04 - file: ../../../final/evidence/rendered/grpc-core-api-f04.svg -evidence: - - ../../../final/evidence/raw/grpc-core-api-f04.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-core-api#L200 이다. -module: grpc-core-api -priority: P3 ---- - -# 하나의 상태 코드가 같은 메서드 안에서 두 답을 갖는다 - -forMutation 은 스위치에 닿기 전에 OK 를 먼저 처리한다. 스위치의 OK 분기는 도달하지 않는다. - -## 문제 - -forMutation 은 스위치에 닿기 전에 OK 를 먼저 처리한다. - -스위치의 OK 분기는 도달하지 않는다. - -## 결론 - -열거형 전수 처리를 컴파일러가 요구하므로 항목 자체는 필요하지만, 그 값이 위의 가드와 반대다. - -결과는 잠재적 함정이다. - -누군가 위의 OK 가드를 "중복이니까" 지우면 컴파일은 통과하고 OK 인 변경이 COMPLETION_UNKNOWN 이 된다 — 성공한 변경마다 대사(reconciliation)를 요구하게 된다. - -이 리프의 다른 자리들은 그런 편집이 눈에 띄도록 설계되어 있다(예: 승격 메서드를 하나로 좁힌 것). - -수정은 한 글자다. - -스위치의 OK 를 COMPLETED 로 옮기면 두 자리의 답이 같아지고, 가드가 사라져도 결과가 바뀌지 않는다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : forMutation 의 선행 처리와 스위치 분기의 도달성 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-core-api#L200 에 있다. - -## 본문 - - - -`forMutation` 은 스위치에 닿기 전에 `OK` 를 먼저 처리한다. 스위치의 `OK` 분기는 도달하지 않는다. - -## 스위치에 닿기 전의 처리 - -:::evidence key="grpc-core-api-f04" alt="분석 문서 final/document.md#a20-grpc-core-api 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-core-api 발췌 — 15줄" zoom="true" -::: - -## 도달하지 않는 분기의 값이 가드와 반대다 - -열거형 전수 처리를 컴파일러가 요구하므로 항목 자체는 필요하다. - -## 결과는 잠재적 함정이다 - -누군가 위의 `OK` 가드를 "중복이니까" 지우면 컴파일은 통과하고 `OK` 인 변경이 `COMPLETION_UNKNOWN` 이 된다 — 성공한 변경마다 대사(reconciliation)를 요구하게 된다. 이 리프의 다른 자리들은 그런 편집이 눈에 띄도록 설계되어 있다(예: 승격 메서드를 하나로 좁힌 것). - -## 수정은 한 글자다 - -스위치의 `OK` 를 `COMPLETED` 로 옮기면 두 자리의 답이 같아지고, 가드가 사라져도 결과가 바뀌지 않는다. - -## 확인하지 못한 것 - -도달하지 않는 분기를 실행으로 확인하지 않았다. 스위치에 닿기 전에 같은 코드가 처리된다는 제어 흐름으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/case/case-grpc-observability-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/case/case-grpc-observability-f03.md deleted file mode 100644 index 1a7cfa2..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/case/case-grpc-observability-f03.md +++ /dev/null @@ -1,84 +0,0 @@ ---- -kind: CASE -slug: grpc-observability-f03 -title: 허용 태그 8개 중 둘은 값이 자유 문자열이고, 그중 하나는 bounded 열거형이 이미 존재한다 -topic: runtime-contract-correctness -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-observability-f03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-observability-f03 - file: ../../../final/evidence/rendered/grpc-observability-f03.svg -evidence: - - ../../../final/evidence/raw/grpc-observability-f03.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-observability#L238 이다. -module: grpc-observability -priority: P3 ---- - -# 허용 태그 8개 중 둘은 값이 자유 문자열이고, 그중 하나는 bounded 열거형이 이미 존재한다 - -값 검사는 키가 allowlist 를 통과한 뒤 UNBOUNDED_VALUE 세 형태만 본다. 그런데 태그 값의 출처는 균일하지 않다. - -## 문제 - -값 검사는 키가 allowlist 를 통과한 뒤 UNBOUNDED_VALUE 세 형태만 본다. - -그런데 태그 값의 출처는 균일하지 않다. - -## 결론 - -GrpcStreamObservation 의 검증은 terminationReason 이 널이 아니고 공백이 아닌지만 본다. - -호출자가 예외 메시지나 원격 상태 문자열을 그대로 넣으면 그 태그의 값 공간이 트래픽과 함께 자란다 — 이 클래스가 존재하는 이유로 든 바로 그 실패다. - -그리고 그 개념의 bounded 열거형이 이미 저장소에 있다 — grpc-policy 의 GrpcStreamTerminationReason. - -쓰지 않은 이유는 의존 방향으로 설명된다. - -이 리프의 allowed_dependencies 는 ["grpc-core-api"] 뿐이고 그 열거형은 grpc-policy 에 있다. - -그래서 수정은 열거형을 grpc-core-api 로 옮기거나, violations 가 두 자유 문자열 태그에 대해 허용값 집합을 받도록 서명을 넓히는 것이다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : 허용 태그 8개의 값 출처 추적과 UNBOUNDED_VALUE 가 보는 세 형태 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-observability#L238 에 있다. - -## 본문 - - - -값 검사는 키가 allowlist 를 통과한 뒤 `UNBOUNDED_VALUE` 세 형태만 본다. 그런데 태그 값의 출처는 균일하지 않다. - -## GrpcStreamObservation 참조 위치 - -:::evidence key="grpc-observability-f03" alt="코드베이스에서 GrpcStreamObservation 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcStreamObservation 코드베이스 검색 — 5줄 · exit 0" zoom="true" -::: - -## terminationReason 은 공백 여부만 본다 - -`GrpcStreamObservation` 의 검증은 널이 아니고 공백이 아닌지만 본다. 호출자가 예외 메시지나 원격 상태 문자열을 그대로 넣으면 그 태그의 값 공간이 트래픽과 함께 자란다 — 이 클래스가 존재하는 이유로 든 바로 그 실패다. - -## bounded 열거형이 이미 저장소에 있다 - -`grpc-policy` 의 `GrpcStreamTerminationReason` 이다. 쓰지 않은 이유는 의존 방향으로 설명된다 — 이 리프의 `allowed_dependencies` 는 `["grpc-core-api"]` 뿐이고 그 열거형은 `grpc-policy` 에 있다. - -## 수정 - -열거형을 `grpc-core-api` 로 옮기거나, `violations` 가 두 자유 문자열 태그에 대해 허용값 집합을 받도록 서명을 넓히는 것이다. - -## 확인하지 못한 것 - -UNBOUNDED_VALUE 를 우회하는 값 형태(숫자 id·이메일 등)를 실행으로 확인하지 않았다. 정규식 형태로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/case/case-grpc-spring-boot-starter-f04.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/case/case-grpc-spring-boot-starter-f04.md deleted file mode 100644 index b5a0a7a..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/case/case-grpc-spring-boot-starter-f04.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -kind: CASE -slug: grpc-spring-boot-starter-f04 -title: 반사 모드를 명시하면 서비스·역할 허용 목록이 조용히 하드코딩으로 바뀐다 -topic: runtime-contract-correctness -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-spring-boot-starter-f04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-spring-boot-starter-f04 - file: ../../../final/evidence/rendered/grpc-spring-boot-starter-f04.svg -evidence: - - ../../../final/evidence/raw/grpc-spring-boot-starter-f04.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-spring-boot-starter#L249 이다. -module: grpc-spring-boot-starter -priority: P3 ---- - -# 반사 모드를 명시하면 서비스·역할 허용 목록이 조용히 하드코딩으로 바뀐다 - -두 갈래가 만드는 것이 같은 종류의 값이 아니다. 설정하지 않으면 defaultFor(environment) — 환경이 서비스 목록과 역할 목록을 함께 결정한다. - -## 문제 - -두 갈래가 만드는 것이 같은 종류의 값이 아니다. - -설정하지 않으면 defaultFor(environment) — 환경이 서비스 목록과 역할 목록을 함께 결정한다. - -## 결론 - -설정하면 모드만 운영자 것이고, 허용 서비스와 허용 역할은 이 자동 설정에 박힌 리터럴이 된다. - -운영자가 조정한다고 생각하는 것은 노출 수위 하나인데, 실제로는 노출 대상 집합까지 바뀐다. - -그리고 그 두 리터럴은 설정 표면에 노출되어 있지 않으므로 되돌릴 방법이 reflection-mode 를 다시 비우는 것뿐이다. - -ca-skeleton.grpc.platform 은 ignoreUnknownFields = false 를 걸어 "오타가 조용히 기본값으로 남지 않게" 한 설정 표면이다. - -같은 규율로 보면, 값을 하나 설정했을 때 설정하지 않은 두 값이 함께 바뀌는 것도 같은 종류의 침묵이다. - -허용 서비스·역할을 GrpcPlatformProperties 에 올리거나, 명시 모드에서도 defaultFor(environment) 가 만든 정책의 모드만 바꾼 사본을 쓴다. - -후자가 이 저장소의 다른 곳에서 쓰는 형태다(GrpcProtoStyleManifest.allowingWellKnownTypes 처럼 넓힌 사본). - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : 반사 모드 설정 유무에 따른 두 갈래가 만드는 값의 종류 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-spring-boot-starter#L249 에 있다. - -## 본문 - - - -두 갈래가 만드는 것이 같은 종류의 값이 아니다. 설정하지 않으면 `defaultFor(environment)` — 환경이 서비스 목록과 역할 목록을 함께 결정한다. 설정하면 모드만 운영자 것이고, **허용 서비스와 허용 역할은 이 자동 설정에 박힌 리터럴이 된다.** - -## GrpcPlatformProperties 참조 위치 - -:::evidence key="grpc-spring-boot-starter-f04" alt="코드베이스에서 GrpcPlatformProperties 를 검색한 출력 19줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcPlatformProperties 코드베이스 검색 — 19줄 · exit 0" zoom="true" -::: - -## 운영자가 조정한다고 생각하는 것과 실제로 바뀌는 것 - -조정한다고 생각하는 것은 노출 수위 하나인데, 실제로는 노출 대상 집합까지 바뀐다. 그리고 그 두 리터럴은 설정 표면에 노출되어 있지 않으므로 되돌릴 방법이 `reflection-mode` 를 다시 비우는 것뿐이다. - -## 같은 규율로 보면 같은 종류의 침묵이다 - -`ca-skeleton.grpc.platform` 은 `ignoreUnknownFields = false` 를 걸어 "오타가 조용히 기본값으로 남지 않게" 한 설정 표면이다. 값을 하나 설정했을 때 설정하지 않은 두 값이 함께 바뀌는 것도 같은 종류다. - -## 수정 - -허용 서비스·역할을 `GrpcPlatformProperties` 에 올리거나, 명시 모드에서도 `defaultFor(environment)` 가 만든 정책의 모드만 바꾼 사본을 쓴다. 후자가 이 저장소의 다른 곳에서 쓰는 형태다(`GrpcProtoStyleManifest.allowingWellKnownTypes` 처럼 넓힌 사본). - -## 확인하지 못한 것 - -반사 모드를 명시한 컨텍스트를 세워 허용 목록의 변화를 관측하지 않았다. 두 갈래의 생성 코드로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/case/case-messaging-claim-check-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/case/case-messaging-claim-check-f03.md deleted file mode 100644 index e5bf303..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/case/case-messaging-claim-check-f03.md +++ /dev/null @@ -1,75 +0,0 @@ ---- -kind: CASE -slug: messaging-claim-check-f03 -title: 예외 승격이 에러 코드 문자열 접미사에 의존한다 -topic: runtime-contract-correctness -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-claim-check-f03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-claim-check-f03 - file: ../../../final/evidence/rendered/messaging-claim-check-f03.svg -evidence: - - ../../../final/evidence/raw/messaging-claim-check-f03.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-claim-check#L525 이다. -module: messaging-claim-check -priority: P3 ---- - -# 예외 승격이 에러 코드 문자열 접미사에 의존한다 - -ClaimCheckResolver.verify가 validation.failure().code().endsWith("_MISMATCH")로 ClaimCheckIntegrityException 승격을 결정한다. ClaimCheckIntegrityGuard의 세 코드 중 둘이 그 접미사를 갖는다. - -## 관계 - -- **leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 튜닝 값이 두 계층에 있으면 어느 쪽이 이기는지 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 문제 - -ClaimCheckResolver.verify가 validation.failure().code().endsWith("_MISMATCH")로 ClaimCheckIntegrityException 승격을 결정한다. - -ClaimCheckIntegrityGuard의 세 코드 중 둘이 그 접미사를 갖는다. - -## 결론 - -두 클래스 사이의 계약이 문자열 명명 규약이고 어디에도 선언되지 않았다. - -guard가 코드를 바꾸면(예: CLAIM_CHECK_DIGEST_INVALID) 승격이 조용히 멈추고 poison message가 PERMANENT_BUSINESS로 분류된다 — 재시도 정책이 달라진다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : ClaimCheckResolver 참조 8건 검색과 승격 조건이 보는 문자열 접미사 및 guard 의 코드 집합 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-claim-check#L525 에 있다. - -## 본문 - - - -`ClaimCheckResolver.verify`가 `validation.failure().code().endsWith("_MISMATCH")`로 `ClaimCheckIntegrityException` 승격을 결정한다. `ClaimCheckIntegrityGuard`의 세 코드 중 둘이 그 접미사를 갖는다. - -## ClaimCheckResolver 참조 위치 - -:::evidence key="messaging-claim-check-f03" alt="코드베이스에서 ClaimCheckResolver 를 검색한 출력 8줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ClaimCheckResolver 코드베이스 검색 — 8줄 · exit 0" zoom="true" -::: - -## 두 클래스 사이의 계약이 문자열 명명 규약이다 - -어디에도 선언되지 않았다. guard가 코드를 바꾸면(예: `CLAIM_CHECK_DIGEST_INVALID`) 승격이 조용히 멈추고 poison message가 `PERMANENT_BUSINESS`로 분류된다 — 재시도 정책이 달라진다. - -## 확인하지 못한 것 - -guard 의 코드를 바꿔 승격이 조용히 멈추는 것을 재현하지 않았다. 두 클래스 사이의 계약이 문자열 명명 규약이라는 코드 형태로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/case/case-messaging-core-api-f04.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/case/case-messaging-core-api-f04.md deleted file mode 100644 index 207a910..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/case/case-messaging-core-api-f04.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -kind: CASE -slug: messaging-core-api-f04 -title: MessagingRedactor가 상수 대신 문자열 리터럴을 쓴다 -topic: runtime-contract-correctness -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-core-api-f04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-core-api-f04 - file: ../../../final/evidence/rendered/messaging-core-api-f04.svg -evidence: - - ../../../final/evidence/raw/messaging-core-api-f04.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-core-api#L871 이다. -module: messaging-core-api -priority: P3 ---- - -# MessagingRedactor가 상수 대신 문자열 리터럴을 쓴다 - -messaging-observability/.../MessagingRedactor.java:24가 "msg.id"를 리터럴로 갖는다. ReservedHeaders.MESSAGE_ID 상수가 있다. - -## 문제 - -messaging-observability/.../MessagingRedactor.java:24가 "msg.id"를 리터럴로 갖는다. - -ReservedHeaders.MESSAGE_ID 상수가 있다. - -## 결론 - -상수가 바뀌면 redaction이 조용히 대상을 잃는다. - -컴파일러가 잡지 않는다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : MessagingRedactor 참조 13건 검색과 리터럴 값 대비 상수 값의 일치 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-core-api#L871 에 있다. - -## 본문 - - - -`messaging-observability/.../MessagingRedactor.java:24`가 `"msg.id"`를 리터럴로 갖는다. `ReservedHeaders.MESSAGE_ID` 상수가 있다. - -## MessagingRedactor 참조 위치 - -:::evidence key="messaging-core-api-f04" alt="코드베이스에서 MessagingRedactor 를 검색한 출력 13줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingRedactor 코드베이스 검색 — 13줄 · exit 0" zoom="true" -::: - -## 상수가 바뀌면 조용히 대상을 잃는다 - -컴파일러가 잡지 않는다. `git grep '"msg\.'` 로 확인하면 production 매치는 이 한 곳뿐이다. 후보는 리터럴을 `ReservedHeaders.MESSAGE_ID`로 교체하는 것이고, 소유는 `messaging-observability` leaf SSOT다. - -## 확인하지 못한 것 - -이 리터럴이 실제로 어긋난 적이 있는지 확인하지 못했다. 현재는 상수와 값이 같다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/case/case-messaging-outbox-jdbc-postgresql-f05.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/case/case-messaging-outbox-jdbc-postgresql-f05.md deleted file mode 100644 index 04060eb..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/case/case-messaging-outbox-jdbc-postgresql-f05.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -kind: CASE -slug: messaging-outbox-jdbc-postgresql-f05 -title: 구세대 전이 메서드가 신세대와 다른 행 상태를 남긴다 -topic: runtime-contract-correctness -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-outbox-jdbc-postgresql-f05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-outbox-jdbc-postgresql-f05 - file: ../../../final/evidence/rendered/messaging-outbox-jdbc-postgresql-f05.svg -evidence: - - ../../../final/evidence/raw/messaging-outbox-jdbc-postgresql-f05.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-outbox-jdbc-postgresql#L917 이다. -module: messaging-outbox-jdbc-postgresql -priority: P3 ---- - -# 구세대 전이 메서드가 신세대와 다른 행 상태를 남긴다 - -markPublished(MessageId, Instant) 는 lease_owner 와 next_attempt_at 을 지우지 않는다. markPublished(OutboxLease, Instant) 는 지운다. - -## 문제 - -markPublished(MessageId, Instant) 는 lease_owner 와 next_attempt_at 을 지우지 않는다. - -markPublished(OutboxLease, Instant) 는 지운다. - -## 결론 - -markAmbiguous/markFailed 도 같다. - -두 컬럼은 청구 술어와 부분 인덱스가 읽는 값이다. - -이 저장소에 구세대를 부르는 프로덕션 코드는 없다. - -그러나 포트에 남아 있고 @Deprecated 도 아니므로, 외부 구현이나 향후 코드가 부를 수 있다. - -최소한 @Deprecated 와 "신세대를 쓰라"는 문장이 필요하고, 더 나은 것은 제거다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : 두 오버로드의 SET 절 대조와 그 컬럼을 읽는 청구 술어·부분 인덱스 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-outbox-jdbc-postgresql#L917 에 있다. - -## 본문 - - - -`markPublished(MessageId, Instant)` 는 `lease_owner` 와 `next_attempt_at` 을 지우지 않는다. `markPublished(OutboxLease, Instant)` 는 지운다. `markAmbiguous`/`markFailed` 도 같다. - -## 두 오버로드가 지우는 컬럼 - -:::evidence key="messaging-outbox-jdbc-postgresql-f05" alt="분석 문서 final/document.md#a19-messaging-outbox-jdbc-postgresql 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a19-messaging-outbox-jdbc-postgresql 발췌 — 15줄" zoom="true" -::: - -## 두 컬럼은 청구 술어와 부분 인덱스가 읽는 값이다 - -이 저장소에 구세대를 부르는 프로덕션 코드는 없다. 그러나 포트에 남아 있고 `@Deprecated` 도 아니므로, 외부 구현이나 향후 코드가 부를 수 있다. 최소한 `@Deprecated` 와 "신세대를 쓰라"는 문장이 필요하고, 더 나은 것은 제거다. - -## 확인하지 못한 것 - -구세대 메서드가 실제로 호출되는 배포가 있는지 확인하지 않았다. 이 저장소에는 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/case/case-messaging-outbox-jdbc-postgresql-f08.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/case/case-messaging-outbox-jdbc-postgresql-f08.md deleted file mode 100644 index d34b173..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/case/case-messaging-outbox-jdbc-postgresql-f08.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -kind: CASE -slug: messaging-outbox-jdbc-postgresql-f08 -title: maxBatches 가 하드코딩이고 현재는 의미가 없다 -topic: runtime-contract-correctness -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-outbox-jdbc-postgresql-f08 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-outbox-jdbc-postgresql-f08 - file: ../../../final/evidence/rendered/messaging-outbox-jdbc-postgresql-f08.svg -evidence: - - ../../../final/evidence/raw/messaging-outbox-jdbc-postgresql-f08.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-outbox-jdbc-postgresql#L935 이다. -module: messaging-outbox-jdbc-postgresql -priority: P3 ---- - -# maxBatches 가 하드코딩이고 현재는 의미가 없다 - -starter 가 20 을 박아 넣는다(:141, :170). P1 을 고치기 전에는 이 값이 아무 일도 하지 않고, 고친 뒤에는 배치 크기와 함께 조정 대상이 된다. - -## 문제 - -starter 가 20 을 박아 넣는다(:141, :170). - -P1 을 고치기 전에는 이 값이 아무 일도 하지 않고, 고친 뒤에는 배치 크기와 함께 조정 대상이 된다. - -## 결론 - -OutboxProperties/InboxRetentionPolicy 로 옮기는 것이 맞다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : OutboxProperties 참조 28건 검색과 starter 가 넘기는 리터럴 값 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-outbox-jdbc-postgresql#L935 에 있다. - -## 본문 - - - -starter 가 `20` 을 박아 넣는다(`:141`, `:170`). - -## OutboxProperties 참조 위치 - -:::evidence key="messaging-outbox-jdbc-postgresql-f08" alt="코드베이스에서 OutboxProperties 를 검색한 출력 28줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="OutboxProperties 코드베이스 검색 — 28줄 · exit 0" zoom="true" -::: - -## 지금은 아무 일도 하지 않는다 - -P1 을 고치기 전에는 이 값이 아무 일도 하지 않고, 고친 뒤에는 배치 크기와 함께 조정 대상이 된다. `OutboxProperties`/`InboxRetentionPolicy` 로 옮기는 것이 맞다. - -## 확인하지 못한 것 - -이 값을 바꿔 배치 동작이 달라지는지 실행으로 확인하지 않았다. 무제한 DELETE 가 고쳐지기 전에는 값이 소비되지 않는다는 것으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/case/case-messaging-spring-boot-starter-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/case/case-messaging-spring-boot-starter-f03.md deleted file mode 100644 index 30f9092..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/case/case-messaging-spring-boot-starter-f03.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -kind: CASE -slug: messaging-spring-boot-starter-f03 -title: 죽은 매개변수 하나가 유일한 비기본값에서 NPE 를 낳는다 -topic: runtime-contract-correctness -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-spring-boot-starter-f03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-spring-boot-starter-f03 - file: ../../../final/evidence/rendered/messaging-spring-boot-starter-f03.svg -evidence: - - ../../../final/evidence/raw/messaging-spring-boot-starter-f03.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-spring-boot-starter#L319 이다. -module: messaging-spring-boot-starter -priority: P3 ---- - -# 죽은 매개변수 하나가 유일한 비기본값에서 NPE 를 낳는다 - -호출처가 셋이고 전부 () -> true 다. 그래서 이 매개변수는 값을 하나만 갖는다. - -## 관계 - -- **검증기는 발행이 아니라 주입이 강제다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 문제 - -호출처가 셋이고 전부 () -> true 다. - -그래서 이 매개변수는 값을 하나만 갖는다. - -## 결론 - -그리고 그것이 죽어 있다는 것보다 나쁜 성질이 있다 — 이 매개변수가 존재하는 이유("이 역할은 선택적이다")대로 () -> false 를 넘기면 credential == null 인 경로가 가드를 지나 다음 줄의 credential.type() 에서 NPE 로 죽는다. - -즉 이 매개변수의 유일한 비기본값이 의도한 동작이 아니라 널 역참조다. - -수정은 매개변수를 지우고 널 검사를 무조건으로 만드는 것이다. - -선택적 역할이 필요해지는 날에는 Optional 을 돌려주는 별도 메서드가 그 자리다 — admin 이 이미 호출처에서 그렇게 다뤄진다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : 이 매개변수의 호출처 셋을 전수 확인해 넘어오는 값의 종류 집계 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-spring-boot-starter#L319 에 있다. - -## 본문 - - - -호출처가 셋이고 전부 `() -> true` 다. 그래서 이 매개변수는 값을 하나만 갖는다. - -## 매개변수가 갖는 값의 개수 - -:::evidence key="messaging-spring-boot-starter-f03" alt="분석 문서 final/document.md#a19-messaging-spring-boot-starter 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a19-messaging-spring-boot-starter 발췌 — 15줄" zoom="true" -::: - -## 죽어 있다는 것보다 나쁜 성질 - -이 매개변수가 존재하는 이유("이 역할은 선택적이다")대로 `() -> false` 를 넘기면 `credential == null` 인 경로가 가드를 지나 다음 줄의 `credential.type()` 에서 NPE 로 죽는다. 즉 이 매개변수의 유일한 비기본값이 의도한 동작이 아니라 널 역참조다. - -## 수정 - -매개변수를 지우고 널 검사를 무조건으로 만든다. 선택적 역할이 필요해지는 날에는 `Optional` 을 돌려주는 별도 메서드가 그 자리다 — `admin` 이 이미 호출처에서 그렇게 다뤄진다. - -## 확인하지 못한 것 - -기본값이 아닌 값을 넘겨 NPE 를 재현하지 않았다. 호출처 셋이 전부 같은 값을 넘긴다는 것과 본문의 널 취급으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/reference/reference-messaging-kafka-share-experimental-f04.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/reference/reference-messaging-kafka-share-experimental-f04.md deleted file mode 100644 index 0d84718..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/reference/reference-messaging-kafka-share-experimental-f04.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-kafka-share-experimental-f04 -title: 구성 오류는 한 예외 타입과 안정 코드로 보고한다 -topic: runtime-contract-correctness -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-kafka-share-experimental-f04 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-kafka-share-experimental#L494 ---- - -# 구성 오류는 한 예외 타입과 안정 코드로 보고한다 - -## 관계 - -- **"등록"이 아무것도 등록하지 않고 성공을 반환한다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. - -## 목적 - -같은 시작 시점에 같은 종류의 설정 오류를 거절하면서 한쪽만 FailureDescriptor 를 갖게 되면, 운영자가 받는 두 실패 중 하나에만 코드와 카테고리가 붙는다. - -## 규칙 - -1. 같은 검증기 안의 거절들이 같은 예외 타입인지 본다 - KafkaShareProfileValidator.java:31-40 에서 !enabled 는 MessagingCapabilityUnavailableException(안정 코드 KAFKA_SHARE_DISABLED)이고, orderingScope != NONE 은 IllegalArgumentException(코드 없음)이다. - -2. 안정 코드가 없는 거절을 찾아 코드를 준다 - 코드가 없으면 그 실패는 문자열로만 식별된다. - -3. 같은 가족 안에서 갈라진 사례를 함께 본다 - messaging-security 의 MessageSecurityValidator 는 전부 IllegalArgumentException 이고 BrokerTlsPolicy 는 전부 MessagingConfigurationException 이다. 같은 형태의 분기가 이미 두 곳에 있다. - -## 적용 조건 - -시작 시점에 설정을 거절하는 검증기 전부. 특히 한 클래스가 여러 규칙을 검사하는 자리. - -## 예외 - -플랫폼 예외 계층이 닿지 않는 프레임워크 경계에서는 표준 예외가 불가피할 수 있다. 이 검증기는 그 경계가 아니다. - -## 예시 - -KafkaShareProfileValidator.java:31-40 의 두 throw 문. 확인 방법은 그 둘을 나란히 대조하는 것이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/reference/reference-messaging-policy-f05.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/reference/reference-messaging-policy-f05.md deleted file mode 100644 index c273231..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/reference/reference-messaging-policy-f05.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-policy-f05 -title: 부팅 경로의 알고리즘 복잡도는 문서화한다 -topic: runtime-contract-correctness -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-policy-f05 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-policy#L817 ---- - -# 부팅 경로의 알고리즘 복잡도는 문서화한다 - -## 관계 - -- **재시도 엔진과 DLQ 조정자가 bean으로 만들어지고 주입되는 곳이 없다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **재시도와 DLQ 각각에 두 개의 구현이 있고 정본이 표시되지 않았다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **DLQ 메타데이터의 두 시각이 항상 같다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. - -## 목적 - -walk 가 각 분기마다 new LinkedHashSet<>(onPath) 와 new ArrayList<>(path) 를 만든다. 비용이 경로 수에 비례하고 경로 수는 분기 계수에 지수적이다. 정상 구성에서는 무해하지만 validateAll 은 부팅 경로이고, 목적지가 수백 개인 배포에서 부팅이 느려지면 원인을 찾을 단서가 어디에도 없다. - -## 규칙 - -1. 부팅 경로에서 도는 순회의 비용을 코드 옆에 적는다 - DestinationProfileValidator.java:196-198 이 그 지점이다. - -2. 무해한 이유가 입력 크기 가정이면 그 가정을 적는다 - 여기서는 목적지 수십 개, 목적지당 간선 0–2개다. - -3. 더 싼 형태가 있으면 그것도 적는다 - 방문 상태를 white/gray/black 색칠로 바꾸면 복사 없이 O(V+E)가 된다. - -## 적용 조건 - -애플리케이션 시작 중에 도는 검증·그래프 순회·전수 스캔. - -## 예외 - -입력 크기가 코드로 상한이 걸려 있으면 문서화 없이도 안전하다. 이 순회의 입력은 목적지 프로파일이고 상한이 없다. - -## 예시 - -DestinationProfileValidator.java:196-198 의 복사 두 줄. 확인 방법은 목적지 수를 늘려가며 validateAll 시간을 재는 것이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/reference/reference-messaging-runtime-core-f05.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/reference/reference-messaging-runtime-core-f05.md deleted file mode 100644 index bf7c40c..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/reference/reference-messaging-runtime-core-f05.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-runtime-core-f05 -title: CompletionStage를 반환하는 메서드는 동기적으로 던지지 않는다 -topic: runtime-contract-correctness -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-runtime-core-f05 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-runtime-core#L745 ---- - -# CompletionStage를 반환하는 메서드는 동기적으로 던지지 않는다 - -## 관계 - -- **관측이 구현·호출부·주입 자리를 모두 갖추고도 출하에서 no-op이다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **소비 오케스트레이터가 조립되지 않는다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **선언된 content type과 실제 인코딩이 조용히 갈라질 수 있다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. - -## 목적 - -.publish(...).exceptionally(...) 로만 실패를 처리하는 호출자는 MessageTooLargeException 과 MessageBackpressureException 을 놓친다. 두 예외 다 MessagingException 이라 FailureDescriptor 는 갖지만, 전달 경로가 나머지 실패와 다르다. - -## 규칙 - -1. 비동기 반환 메서드 안에서 try 블록 밖에 있는 단계를 찾는다 - 발행 8단계 중 admission(:24)만 밖이다. 나머지 실패는 전부 CompletionStage 로 정규화된다. - -2. 그 단계가 던지는 예외를 센다 - 여기서는 둘이다. - -3. 정규화하거나 동기 throw 를 계약에 적는다 - admission 을 try 안으로 넣어 rejected(...) 로 만들거나, javadoc 이 동기 throw 를 명시한다. - -## 적용 조건 - -CompletionStage 나 CompletableFuture 를 반환하면서 본문 앞부분에 검증 단계를 두는 모든 메서드. - -## 예외 - -인자 자체가 계약 위반인 경우(널 등)를 동기 throw 로 두는 것은 통상적이다. 상한 초과와 backpressure 는 정상 운영 중에 발생하는 실패라 그 예외에 들지 않는다. - -## 예시 - -DefaultMessagePublisher.java:198 의 admit 호출 위치와 그 앞뒤 try 블록 범위. 확인 방법은 상한 초과 payload 로 publish 를 불러 호출 자체가 던지는지 보는 것이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-a05-f007-transactionprofileregistry.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-a05-f007-transactionprofileregistry.md deleted file mode 100644 index b62afeb..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-a05-f007-transactionprofileregistry.md +++ /dev/null @@ -1,163 +0,0 @@ ---- -kind: CASE -slug: a05-f007-transactionprofileregistry -title: 오타를 막는 근거만 남기고, 오타가 들어올 자리를 지웠다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a05-f007-transactionprofileregistry -evidenceCapturedOn: 2026-09-02 -assets: - - key: a05-f007-transactionprofileregistry-adr - file: ../../../final/evidence/rendered/a05-f007-transactionprofileregistry-adr.svg - - key: a05-f007-transactionprofileregistry - file: ../../../final/evidence/rendered/a05-f007-transactionprofileregistry.svg -evidence: - - ../../../final/evidence/raw/a05-f007-transactionprofileregistry-adr.txt - - ../../../final/evidence/raw/a05-f007-transactionprofileregistry.txt -source: - - 원본 분석 절은 `final/document.md#a05` §25 다. 프로덕션 참조 0, 전용 단위 테스트만 존재, 비공개 API 패키지, 루트 빈 배선 없음이 그 절의 판정이고 등급은 P3 다. ADR 대조와 형제 레지스트리 비교는 이 기록에서 새로 확인했다. 바로 앞 §24 는 같은 리프에서 트랜잭션 스택이 둘로 갈린 문제를 다룬다. ---- - -# 오타를 막는 근거만 남기고, 오타가 들어올 자리를 지웠다 - -프로파일을 이름으로 찾고 등록되지 않은 이름을 거절하는 레지스트리가 남았다. 이름을 건네던 유일한 호출부는 한 커밋에서 지워졌고, 같은 커밋이 그 레지스트리의 javadoc 에서 지워진 애너테이션 이름만 빼고 오타 예시와 논거는 남겼다. - -## 관계 - -- **재시도 구현이 둘이고 정교한 쪽을 아무도 호출하지 않는다** - 같은 스택의 다른 마디가 같은 형태로 남은 사례다. -- **legacy compatibility surface의 제거 조건을 세 가지로 고정한다** - 이 후보를 그 세 조건에 대 봤다. -- **같은 개념의 두 어휘가 공존하면 하나를 죽은 것으로 표시한다** - 같은 개념을 가리키는 어휘가 둘 남았을 때 어느 쪽이 죽었는지 표시하라는 규칙이다. - -## 문제 - -이 레지스트리는 등록되지 않은 이름에 기본값을 주지 않고 던진다. javadoc 이 그 이유로 프로파일 이름 오타 하나가 남의 격리 수준과 타임아웃과 재시도 예산을 물려받는 상황을 든다. - -그런 방어가 값을 하려면 이름을 건네는 쪽이 있어야 한다. - -## 결론 - -이름을 건네던 곳은 하나였다. 삭제된 인터셉터의 66행이 애너테이션에 적힌 프로파일 이름을 레지스트리에 넘기고, 69행이 그렇게 얻은 프로파일로 코디네이터를 불렀다. - -애너테이션 44줄, 인터셉터 109줄, 그 테스트 211줄이 한 커밋에서 함께 지워졌다. 파일 909개를 옮긴 기능 커밋 하나에 이 삭제가 섞여 들어갔다. - -같은 커밋이 레지스트리 javadoc 도 한 줄 고쳤다. 오타 예시에서 애너테이션 표기를 빼고 이름만 남겼다. 방어의 근거는 손질해서 남기고, 방어할 대상은 같은 커밋으로 지운 것이다. - -지금 세면 자기 파일을 뺀 프로덕션 참조 0, 자동설정 등록 0, 설정 키 0 이다. 패키지는 스캔 범위에 들어가는데 이 클래스에는 스테레오타입 애너테이션이 없다. 남은 참조는 전용 단위 테스트 한 파일의 셋이다. - -레지스트리가 담는 타입도 사정이 다르지 않다. TransactionProfile 은 네 파일에 나타나고 공개 포트가 그것을 두 번째 인자로 받는다. 그런데 그 포트를 참조하는 main 파일은 포트 자신과 구현체뿐이고, 자기 파일 밖에서 프로파일을 만드는 main 코드도 없다. 참조가 네 파일 안에서만 돌고 바깥에서 들어오는 길이 없다. - -프로덕션 트랜잭션은 다른 타입으로 열린다. 같은 패키지의 SpringTransactionPort 는 애플리케이션 계층 요청 타입을 받는데, 그 파일에서 TransactionProfile 을 부르는 곳이 없다. - -그래서 이름 조회가 객체 전달로 대체된 것으로 읽으면 틀린다. 이름을 받던 입력이 사라졌고, 객체를 받는 쪽에도 프로덕션 호출자가 붙지 않았다. 레지스트리는 그 스택에서 가장 먼저 잘려 나간 마디다. - -제거 조건 셋 중 둘은 확인된다. 외부 프로덕션 참조가 0 이고 설정 경로가 없다. 세 번째인 대체 경로의 특성화는 확인되지 않는다. 대체 경로가 같은 동작을 다르게 하는 것이 아니라, 이름 조회를 수행하는 프로덕션 경로가 없다. - -이런 모양 자체가 버려진 것은 아니다. 문자열 이름을 받아 등록되지 않았으면 던지는 레지스트리를 정렬 필드 매퍼가 지금도 호출한다. 형제로 셀 수 있는 것은 결국 하나다. gRPC 채널 레지스트리는 검증된 값 객체를 키로 쓰고, Mongo 일관성 레지스트리는 enum 을 키로 써서 조회가 실패할 수 없다고 javadoc 이 적는다. - -ADR 은 아직 지워진 것을 전제한다. 강제 절이 삭제된 클래스의 상수를 근거로 지목하고, 결정 절은 재시도 어드바이스가 스프링 트랜잭션 어드바이스 바깥에 정렬된다고 적는다. 그 상수 이름은 소스 전체에 없고, JPA 리프 main 에 어드바이저도 없다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 단어 경계 참조 계수, 삭제 커밋의 부모와 diff 열람, 형제 레지스트리의 키 타입 대조, ADR 대조 -소스 수정 : x - -## 재현 조건 - -1. 레지스트리의 패키지와 이 리프의 공개 API 패키지를 비교한다. -2. 자기 파일을 뺀 프로덕션 참조와 자동설정 등록과 설정 키를 센다. 스캔 대상 패키지인지, 스테레오타입이 있는지도 본다. -3. 레지스트리가 담는 타입의 참조를 세고, 그 타입을 만드는 코드와 포트를 부르는 코드를 따로 센다. -4. 프로덕션 트랜잭션이 실제로 지나는 포트를 찾아 그 타입을 확인한다. -5. 삭제 커밋의 부모에서 인터셉터를 열고, 같은 커밋의 레지스트리 diff 를 본다. -6. 형제 레지스트리들의 require 키 타입을 읽는다. -7. ADR 의 결정 절과 강제 절이 지목한 것을 소스에서 찾는다. - -## 본문 - - - -`TransactionProfileRegistry` 는 이름을 받아 트랜잭션 프로파일을 돌려주고, 등록되지 않은 이름이면 기본값으로 넘어가는 대신 예외를 던진다. - -클래스의 javadoc 이 그 완고함의 근거를 적는다. 기본값이 있으면 `"order-wrtie"` 같은 오타가 남의 격리 수준과 타임아웃과 재시도 예산으로 조용히 실행된다는 것이다. - -그런 방어는 이름을 건네는 쪽이 있을 때만 값을 한다. - -## 이름을 건네던 코드는 한 번 있었고, 지워졌다 - -:::evidence key="a05-f007-transactionprofileregistry-adr" alt="삭제 커밋의 규모와 지워진 세 파일, 그 커밋의 부모에서 열어 본 인터셉터의 해당 줄, 같은 커밋이 레지스트리 javadoc 에 낸 diff, 문자열 키를 쓰는 형제 레지스트리와 다른 키 타입을 쓰는 둘, 그리고 ADR 의 결정 절과 강제 절 원문을 차례로 출력한 터미널 기록." caption="삭제 규모와 지워진 세 파일 · 삭제 직전의 호출부 두 줄 · 같은 커밋의 javadoc diff · 문자열 키 형제는 하나 · ADR 두 절이 지목한 것 — 43줄 · exit 0" zoom="true" -::: - -한 커밋이 애너테이션 44줄과 인터셉터 109줄과 그 테스트 211줄을 지웠다. 909개 파일을 손댄 큰 기능 커밋이고, 이 삭제는 그 안에 있다. - -지운 파일을 그 커밋의 부모에서 열면 레지스트리를 어떻게 썼는지 보인다. - -```java -41: private final TransactionProfileRegistry profiles; -66: TransactionProfile profile = profiles.require(policy.profile()); -69: return coordinator.execute(operation, profile, () -> proceed(invocation), transactionKey); -``` - -`policy.profile()` 은 애너테이션에 적힌 문자열이다. 오타가 들어올 수 있는 자리가 거기였다. - -## 같은 커밋이 근거만 손질해서 남겼다 - -```text -- * default would mean a typo in {@code @RetryableJpaTransaction(profile = "order-wrtie")} silently -+ * default would mean a typo in a profile name of {@code "order-wrtie"} silently runs with someone -``` - -지워지는 애너테이션의 이름만 문장에서 빼고, 오타 예시와 논거는 그대로 뒀다. 방어할 대상을 지우는 커밋이 방어의 근거는 문법만 고쳐 남긴 것이다. - -## 이름이 사라진 자리를 객체가 넘겨받지도 않았다 - -:::evidence key="a05-f007-transactionprofileregistry" alt="레지스트리의 패키지 위치와 공개 API 패키지 파일 수, 자기 파일을 제외한 프로덕션 참조와 자동설정 등록과 설정 키의 계수, 컴포넌트 스캔 대상 여부와 스테레오타입 유무, 레지스트리가 담는 타입의 참조 계수와 그 타입을 만드는 코드 수, 포트를 참조하는 파일, 그리고 프로덕션 트랜잭션이 실제로 지나는 포트와 그 타입을 출력한 터미널 기록." caption="구현 패키지 · 프로덕션 참조 0 과 설정 키 0 · 스캔 대상이나 스테레오타입 없음 · 담긴 타입은 네 파일에 있으나 만드는 코드 0 · 실제 경로는 다른 포트 — 37줄 · exit 0" zoom="true" -::: - -레지스트리 쪽 수는 전부 0 이다. 자기 파일을 뺀 프로덕션 참조 0, 자동설정 등록 0, 출하 설정의 프로파일 키 0. 리프의 공개 API 패키지에는 파일이 마흔아홉 개 있지만 이것은 그중에 없다. 패키지 자체는 컴포넌트 스캔 대상인데, 이 클래스에 스테레오타입 애너테이션이 없어 후보가 되지 않는다. - -레지스트리가 담는 타입 쪽은 수가 다르다. `TransactionProfile` 은 네 파일에서 열한 줄에 나타나고, import 와 javadoc 을 빼면 일곱 줄이 타입 사용이다. 코디네이터, 정의 매퍼, 실행기 구현, 그리고 공개 포트다. 포트는 이름 대신 객체를 받는다. - -```java - T execute(PersistenceOperationName operation, TransactionProfile profile, Supplier work); -``` - -두 번째 인자가 프로파일 객체다. 다만 이 포트를 참조하는 main 파일은 포트 자신과 그 구현체뿐이고, 자기 파일 밖에서 `TransactionProfile` 을 만드는 main 코드도 없다. 네 파일은 서로를 참조할 뿐이고 바깥에서 들어오는 진입점이 없다. - -프로덕션 트랜잭션은 같은 패키지의 다른 클래스가 연다. `SpringTransactionPort` 가 애플리케이션 계층의 요청 타입을 받고, 그 파일 안의 `TransactionProfile` 참조는 0 이다. - -그러니 이름 조회가 객체 전달로 대체됐다고는 말할 수 없다. 이름을 받던 입력이 사라진 뒤 객체를 받는 쪽에도 호출자가 붙지 않았고, 레지스트리는 그 스택에서 가장 먼저 잘려 나간 마디다. - -## 제거 조건 세 가지에 대 보면 - -두 조건은 확인된다. 외부 프로덕션 참조가 0 이고, 운영자가 켤 수 있는 설정 경로가 없다. - -세 번째 조건인 대체 경로의 특성화는 확인되지 않는데, 이유가 규칙이 상정한 것과 다르다. 규칙은 대체 경로가 같은 동작을 증명하지 못하면 지우는 순간 동작이 바뀐다고 말한다. 여기서는 이름 조회를 수행하는 프로덕션 경로가 없다. 지워도 바뀔 동작이 없는 대신, 무엇이 대체했는지도 말할 수 없다. - -이 모양이 폐기된 것도 아니다. `SafeSortRegistry` 는 지금도 문자열 이름을 받아 등록되지 않았으면 던지고, 정렬 파라미터를 파싱하는 매퍼가 그것을 호출한다. - -형제라 부를 만한 것은 그 하나다. gRPC 채널 레지스트리는 검증된 값 객체를 키로 쓰고, 던지는 이유도 오타가 아니라 아직 설치되지 않았다는 것이다. Mongo 일관성 레지스트리는 enum 을 키로 쓰고, 그 javadoc 이 모든 enum 상수가 언제나 있다고 적는다. 컴파일러가 키를 강제하므로 오타가 들어올 자리가 없다. - -## ADR 이 지워진 것을 아직 전제한다 - -전체 트랜잭션 재시도 ADR 의 Enforcement 절이 두 가지를 근거로 지목한다. - -```text -`FullTransactionRetryCoordinatorTest`; `RetryableJpaTransactionInterceptor.DEFAULT_ORDER`. -``` - -앞의 것은 있다. 뒤의 것은 삭제된 클래스의 상수이고, `DEFAULT_ORDER` 라는 이름은 소스 전체에 한 줄도 없다. - -Decision 절도 마찬가지다. 재시도 어드바이스가 스프링 트랜잭션 어드바이스 바깥에 정렬되어 각 시도가 새 트랜잭션을 연다고 적는데, JPA 리프 main 에 어드바이저는 없다. 코드는 정리됐고 결정 기록은 그대로 남았다. - -## 확인하지 못한 것 - -저장소 밖의 리플렉션이나 외부 직접 생성 여부는 소스 검색으로 알 수 없다. - -포크가 이름 기반 조회를 다시 붙일 의도인지도 알 수 없다. 테스트 머리글이 계획 문서의 과제 번호와 설계 절을 가리키지만, 그 계획이 지금도 유효한지는 이 저장소가 말하지 않는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-a05-f018-sql.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-a05-f018-sql.md deleted file mode 100644 index 11a2a33..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-a05-f018-sql.md +++ /dev/null @@ -1,138 +0,0 @@ ---- -kind: CASE -slug: a05-f018-sql -title: 쿼리 이름을 SQL 로 옮기는 두 경로가 양쪽 끝만 있고 가운데가 없다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a05-f018-sql -evidenceCapturedOn: 2026-09-02 -assets: - - key: a05-f018-sql - file: ../../../final/evidence/rendered/a05-f018-sql.svg -evidence: - - ../../../final/evidence/raw/a05-f018-sql.txt -source: - - 원본 분석 절은 `final/document.md#a05` §55 의 backlog 안, 「Cross-scope P1/P2 — query SQL naming/observability composition 부재」 항목이다. 검사기의 런타임 등록이 0 이라는 판정, SQL 계층으로 잇는 다리가 확인되지 않았다는 판정, 그리고 최종 판정을 트랜잭션 관측 배선 공백과 함께 관측·설정 범위로 미룬다는 기록이 거기 있다. - - 다리가 확인되지 않았다는 판정은 맞다. `QueryNameContext` 가 이름을 스레드에 묶고 검사기가 그것을 읽지만, 어떤 SessionFactory 도 그 검사기를 설치하지 않으므로 `inspect()` 는 호출되지 않는다. 코드에 있는 것은 다리가 아니라 양쪽 끝이다. 두 번째 경로인 힌트도 마찬가지다. ---- - -# 쿼리 이름을 SQL 로 옮기는 두 경로가 양쪽 끝만 있고 가운데가 없다 - -쿼리 이름을 SQL 주석으로 실어 나르는 문장 검사기가 있다. 어떤 SessionFactory 도 그것을 설치하지 않아 `inspect()` 가 호출되지 않는다. 두 번째 경로인 `org.hibernate.comment` 힌트도 그것을 SQL 로 내보내는 설정이 꺼져 있다. 이름을 넣는 프로덕션 클래스 셋은 자기 단위 테스트 밖에서 쓰이지 않는다. - -## 관계 - -- **@Bean이 있다는 것은 조립 증거가 아니다** - 구현의 존재를 조립의 증거로 읽는다는 점이 같다. -- **이름은 값이 아니라 registry key다** - 쿼리 이름이 등록된 식별자로 쓰이는 맥락이다. -- **카디널리티 경계를 타입으로 표현하기** - 그 이름이 메트릭 태그가 되는 경계다. - -## 문제 - -주석에 이름이 실려야 DBA 가 눈앞의 문장을 어느 유스케이스에서 나온 것인지 되짚는다. 검사기 javadoc 이 그 이유를 적고, 없으면 어느 엔드포인트가 이 쿼리를 내는지를 SQL 조각으로 코드베이스를 뒤져 답하게 된다고 덧붙인다. - -## 결론 - -inspect() 는 다 쓰여 있다. 현재 스레드에 묶인 이름을 꺼내 /* 이름 */ 을 앞에 붙이고, 이름에 주석 종료자가 있으면 원본을 돌려준다. 그 분기는 도달하지 않는다. QueryName 의 형식 [a-z][a-z0-9.-]{2,95} 가 이미 * 와 / 를 막고, javadoc 도 그 검사를 additionally 라고 적는다. - -단위 테스트는 없다. 검사기에도 컨텍스트에도 테스트 파일이 없다. - -SessionFactory 가 설치해 주지 않으면 inspect() 는 불리지 않는다. 네 경로는 저장소 루트에서 확장자 제한 없이 훑어 얻었다. 설정 키 0, HibernatePropertiesCustomizer 와 SessionFactoryBuilder 와 ServiceRegistry 와 Integrator 0, persistence.xml 0, 런타임 리소스의 FQCN 0 이다. - -이름을 넣는 쪽은 배선되어 있다. QueryNameContext.with(...) 를 부르는 프로덕션 코드가 셋이다. 그 컨텍스트를 읽는 쪽은 검사기 하나뿐이다. - -SQL 로 이름을 옮기는 길이 그것 말고 또 있다. org.hibernate.comment 힌트를 거는 것은 리포지토리 조각 지원과 Querydsl 지원이다. 그 힌트도 스위치 하나에 달려 있는데 저장소에 그 키가 0 이다. - -두 경로 다 이름을 넣는 코드와 그것을 읽는 자리는 있는데, 그 사이를 잇는 설치와 스위치가 없다. - -넣는 세 클래스도 실행되지 않는다. 각 타입을 쓰는 파일은 자기 자신과 자기 단위 테스트뿐이고, 리포지토리 조각 지원을 상속하는 유일한 코드도 그 테스트 안의 중첩 클래스다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 설치 경로 넷을 저장소 루트에서 계수, 이름을 넣고 읽는 지점 추적, 두 번째 경로의 활성 설정 확인 -소스 수정 : x - -## 재현 조건 - -1. 검사기 구현과 javadoc, 그리고 QueryName 의 형식 정규식을 읽는다. -2. 저장소 루트에서 확장자 제한 없이 설치 경로 넷을 각각 센다. -3. 그 클래스의 단위 테스트가 있는지 찾는다. -4. 이름을 넣는 호출과 읽는 호출을 각각 센다. -5. org.hibernate.comment 힌트를 거는 곳과, use_sql_comments 를 켜는 곳을 센다. -6. 넣는 세 클래스를 쓰는 파일과 상속하는 코드를 전부 나열한다. - -## 본문 - - - -쿼리 이름이 SQL 주석으로 실려야 DBA 가 `pg_stat_activity` 나 느린 쿼리 로그의 문장을 유스케이스로 되돌릴 수 있다. 검사기 javadoc 이 그 이유를 적는다. - -## inspect() 가 하는 일 - -:::evidence key="a05-f018-sql" alt="문장 검사기의 javadoc 앞부분과 클래스 본문, 쿼리 이름의 형식 정규식, 하이버네이트가 이 구현을 설치하는 경로 넷을 저장소 루트에서 확장자 제한 없이 센 결과와 그 클래스의 단위 테스트 수, 이름을 컨텍스트에 넣는 세 곳과 읽는 곳, 이름을 SQL 로 옮기는 두 번째 경로와 그것을 활성화하는 설정의 수, 그리고 넣는 세 클래스를 쓰는 파일과 상속하는 코드를 출력한 터미널 기록." caption="검사기 javadoc 과 본문 · 이름 형식이 이미 * 와 / 를 막음 · 설치 경로 넷 전부 0 · 단위 테스트 0 · 넣는 곳 셋과 읽는 곳 하나 · 두 번째 경로의 use_sql_comments 0 · 세 클래스는 자기 테스트만 — 69줄 · exit 0" zoom="true" -::: - -```java -public String inspect(String sql) { - if (sql == null) { - return null; - } - Optional queryName = QueryNameContext.current(); - if (queryName.isEmpty()) { - return sql; - } - String value = queryName.get().value(); - if (value.contains(COMMENT_TERMINATOR)) { - return sql; - } - return "/* " + value + " */ " + sql; -} -``` - -현재 스레드에 묶인 이름을 꺼내 주석으로 붙인다. 이름에 `*/` 가 있으면 원본을 돌려주는 분기가 하나 더 있는데, `QueryName` 의 형식 `[a-z][a-z0-9.-]{2,95}` 가 이미 `*` 와 `/` 를 막으므로 도달하지 않는다. javadoc 도 그 검사를 additionally 라고 적는다. - -이 클래스에는 단위 테스트가 없다. 컨텍스트 쪽도 없다. - -## 검사기를 설치하는 설정이 없다 - -하이버네이트가 `inspect()` 를 부르려면 SessionFactory 가 이 구현을 설치해야 한다. 설치 경로 넷을 저장소 루트에서 확장자 제한 없이 셌다. - -```text -statement_inspector 설정 키 : 0 -HibernatePropertiesCustomizer / SessionFactoryBuilder / Integrator : 0 -persistence.xml : 0 -런타임 리소스의 FQCN : 0 -``` - -## 이름을 컨텍스트에 넣는 세 곳 - -`JpaStreamExecutor:63`, `JpaKeysetQuerySupport:49`, `JpaRepositoryFragmentSupport:72` 가 작업을 감싸며 이름을 컨텍스트에 넣는다. 그 컨텍스트를 읽는 코드는 검사기 하나다. - -이름을 SQL 로 옮기는 경로는 하나가 더 있다. 리포지토리 조각 지원과 Querydsl 지원이 `org.hibernate.comment` 힌트를 건다. - -```java -return entityManager.createQuery(jpql, resultType).setHint(COMMENT_HINT, name.value()); -``` - -그 힌트가 SQL 로 나오려면 `hibernate.use_sql_comments` 가 켜져야 한다. 저장소에 그 키는 0 이다. - -두 경로 모두 양쪽 끝만 있고 가운데가 없다. - -## 세 클래스의 프로덕션 참조 - -각 타입을 쓰는 파일은 자기 자신과 자기 단위 테스트뿐이다. 리포지토리 조각 지원을 상속하는 유일한 코드도 그 테스트 안의 중첩 클래스다. - -스프링 데이터의 조각 규약으로 연결될 여지도 없다. 그 클래스는 인터페이스가 아니라 추상 클래스이고, `repositoryBaseClass` 도 `@NoRepositoryBean` 도 저장소에 없다. - -이름은 실행 경로에 오르지 않는다. - -## 확인하지 못한 것 - -애플리케이션을 부팅해 실제 문장에 주석이 붙지 않는 것을 관측하지 않았다. 설치 경로와 호출 지점을 센 것까지가 확인 범위다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-a06-f003-change-streams-true.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-a06-f003-change-streams-true.md deleted file mode 100644 index 5fd9566..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-a06-f003-change-streams-true.md +++ /dev/null @@ -1,157 +0,0 @@ ---- -kind: CASE -slug: a06-f003-change-streams-true -title: 거부라고 적힌 처리가 폐기이고, 그 값을 읽는 시작 검사는 켤 방법이 없다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a06-f003-change-streams-true -evidenceCapturedOn: 2026-09-02 -body: case-a06-f003-change-streams-true.body.md -assets: - - key: a06-f003-change-streams-true - file: ../../../final/evidence/rendered/a06-f003-change-streams-true.svg - - key: a06-f003-change-streams-true-probe - file: ../../../final/evidence/rendered/a06-f003-change-streams-true-probe.svg - - key: a06-f003-change-streams-true-shipped - file: ../../../final/evidence/rendered/a06-f003-change-streams-true-shipped.svg -evidence: - - ../../../final/evidence/raw/a06-f003-change-streams-true.txt - - ../../../final/evidence/raw/a06-f003-change-streams-true-probe.txt - - ../../../final/evidence/raw/a06-f003-change-streams-true-shipped.txt -source: - - 원본 분석 절은 `final/document.md#a06#L156` 이다. - - 그 절의 판정은 P3 이고, 근거로 변경 스트림 실행체가 애초에 출하되지 않는다는 것을 든다. 같은 문서 `#L1069` 이 이 리비전에서 그 전제를 부정한다. 그래서 P3 에 붙은 "현재 잘못된 동작을 만들지는 않는다"는 그대로 두기 어렵지만, 등급 재조정은 이 기록이 하지 않는다. `#L1069` 의 P2 는 별도 기록이 다룬다. - - 네 호출이 각각 어디서 멈추는지, 그 분기가 내는 문장이 저장소에 몇 번 나오는지, 소비자 조건 다섯의 구현체가 전부 시험 픽스처라는 것, 그리고 세 입력의 바인딩 결과와 단독 서버에서의 두 배선 비교와 복구 정책 판정은 이 기록에서 확인했다. ---- - -# 거부라고 적힌 처리가 폐기이고, 그 값을 읽는 시작 검사는 켤 방법이 없다 - -설정 타입의 컴팩트 생성자가 변경 스트림 플래그를 예외 없이 거짓으로 덮어쓴다. 그 자리의 주석은 이 처리를 거부라고 부른다. 거부와 폐기는 운영자에게 다른 사건이다. 하나는 기동이 멈추고 하나는 아무 일도 일어나지 않는다. - -## 관계 - -- **하위 시스템 전체가 미배선인데 그것을 켜는 플래그는 시작 검사를 수행한다** - 그 기록에서는 플래그가 켜져 있어 요구를 만드는데 실행체가 없다. -- **sanitize가 아니라 reject가 기본이다** - 이 주석이 따르겠다고 선언한 규칙이다. -- **플래그는 고정 거짓이라 능력 검사를 끄지만, 조립 조건이 아니라서 소비자 빈은 그대로 생성된다** - 같은 플래그를 다른 절에서 다룬 기록이다. 등급 재조정은 그 기록이 한다. - -## 문제 - -레코드 컴포넌트는 넷이고 생성자는 넷을 서로 다르게 다룬다. 프로파일은 널이면 빈 맵으로 흡수한다. 필요 보조 노드 수는 널이면 기본값을 넣고 음수면 예외로 거부한다. 형제 불리언인 트랜잭션 플래그는 그대로 보존한다. 변경 스트림 플래그만 예외 없이 거짓이 된다. - -## 결론 - -이 플래그가 실제로 끄는 것은 실행체가 아니라 검사다. 시작 검증기는 변경 스트림이 켜져 있을 때 토폴로지 능력을 확인하는 분기를 갖는다. 그 인자를 넘기는 프로덕션 지점은 자동 구성 한 줄이고, 그 줄이 읽는 값은 생성자가 이미 거짓으로 덮어쓴 뒤다. - -시험도 그 분기 본문에 닿지 않는다. 검증기를 만드는 시험은 하나이고 호출은 넷인데, 둘은 거짓을 넘기고, 하나는 앞 단계인 토폴로지 검사에서 먼저 멈추며, 나머지 하나는 복제 셋이라 능력이 안정으로 나온다. 그 분기가 내는 문장은 저장소 안에서 자기 throw 자리 한 곳에만 있고, 그것을 단언하는 시험은 없다. - -오플로그가 없는 단독 서버로 확인했다. 출하 배선의 시작 검증은 통과하고, 같은 검증기에 리터럴 참을 넘기면 토폴로지를 지목하는 예외가 난다. 그 뒤 커서를 열면 드라이버가 명령 단계에서 거절하고, 복구 정책은 그것을 실패로 확정하며 프라이머리 장애 조치 런북을 붙인다. 그 런북에 오플로그 없는 토폴로지 항목은 없다. - -주석은 이 처리의 근거로 드라이버 쪽 구현이 출하되지 않아 빈이 0 이라는 것을 든다. 그 근거는 이 리비전에서 성립하지 않는다. 다만 그 사실이 바꾸는 것은 원본 절의 등급 판단이고, 이 기록의 관찰은 거부와 폐기의 차이 그대로다. - -수정은 둘 중 하나다. 값을 정말로 거부하거나, 플래그를 레코드 컴포넌트에서 빼 존재하지 않는 스위치로 만드는 것이다. - -## 검증 환경 - -OpenJDK : 21.0.12 -MongoDB : 8.0.16 단독 서버 -확인 방식 : 실제 바인딩에 세 입력 통과, 실제 서버에 시작 검증기와 커서 열기와 복구 정책 실행 -소스 수정 : x - -## 재현 조건 - -1. 컴팩트 생성자에서 네 컴포넌트의 처리를 비교하고 변경 스트림 자리의 주석을 읽는다. -2. 시작 검증기의 변경 스트림 분기, 그 인자를 넘기는 곳, 검증기를 만드는 곳을 전수로 센다. -3. 그 분기가 내는 문장이 저장소 어디에 나오는지 센다. -4. 검증기를 만드는 시험의 네 호출이 각각 어디서 멈추는지 읽는다. -5. 오플로그가 없는 단독 서버를 띄운다. -6. 세 입력을 실제 바인딩에 통과시키고 각각의 결과를 읽는다. -7. 같은 서버에 대해 출하 배선과 리터럴 참 배선으로 시작 검증을 각각 돌린다. -8. 같은 클래스로 커서를 열고, 그 실패를 복구 정책에 넘긴다. - -## 본문 - - - -설정 타입의 주석은 이 값을 저장하지 않고 거부한다고 적으면서, 근거로 드라이버 쪽 구현이 없다는 것을 든다. - -## 네 값 중 하나만 삼켜진다 - -:::evidence key="a06-f003-change-streams-true" alt="설정 타입의 컴팩트 생성자에서 네 컴포넌트가 각각 흡수·보존·덮어쓰기·예외로 처리되는 구간과 변경 스트림 자리의 주석, 시작 검증기의 변경 스트림 분기, 그 분기가 내는 문장이 저장소에 나오는 곳 전수, changeStreams 라는 이름이 나오는 곳 전수, 검증기를 만드는 곳 전수를 출력한 터미널 기록." caption="프로파일은 빈 맵으로 흡수 · 음수 보조 노드 수는 예외 · 트랜잭션은 보존 · 변경 스트림만 예외 없이 거짓 · 분기가 내는 문장은 자기 throw 자리 한 곳뿐 · 검증기 생성 지점은 프로덕션 1 시험 1 — 43줄 · exit 0" zoom="true" -::: - -```java -// Experimental, and therefore not a switch (MNG-INT-003). The driver-side source — watch, -// resumeAfter/startAfter, cursor lifetime, reconnection — is not shipped; what exists is policy -// and value objects that do not add up to a running consumer. Accepting the flag and ignoring -// it -// would leave an operator believing it took effect, so the value is refused rather than stored: -// zero beans, zero threads, and a `true` that cannot be honoured never becomes one that looks -// honoured. -changeStreams = false; -``` - -여덟 줄 아래에서 같은 생성자가 음수 보조 노드 수를 예외로 던진다. 거부가 어떤 모양인지 같은 생성자 안에 있고, 이 줄은 그 모양이 아니다. 형제 불리언인 `transactions` 는 손대지 않는다. - -## 검증기가 받는 값은 언제나 거짓이다 - -```java -if (changeStreamsEnabled && !capabilities.isStable(MongoCapability.CHANGE_STREAM)) { -``` - -좌항을 넘기는 프로덕션 지점은 자동 구성 한 줄뿐이다. 검증기를 만드는 곳은 그 줄과 시험 하나이고, 시험은 리터럴을 넘긴다. 그런데 그 시험의 네 호출 어느 것도 이 `if` 의 본문을 실행하지 않는다. - -- 단독 서버에 프로덕션 프로파일을 놓는 호출은 참 둘을 넘기지만, `validate()` 가 능력 검사보다 먼저 부르는 토폴로지 검사에서 멈춘다 -- 선언과 실제가 어긋나는 호출은 거짓 둘을 넘긴다 -- 트랜잭션만 켜는 호출은 변경 스트림에 거짓을 넘긴다 -- 정상 복제 셋 호출은 참을 넘기지만 그 토폴로지에서는 능력이 안정으로 나온다 - -이 `if` 가 만드는 문장은 저장소 전체에서 자기 `throw` 자리 한 곳에만 있다. - -## 실제 서버에서 - -:::evidence key="a06-f003-change-streams-true-probe" alt="오플로그가 없는 단독 MongoDB 를 띄우고 세 입력을 실제 바인딩에 통과시킨 결과, 관측 토폴로지와 변경 스트림 능력 판정, 출하 배선과 리터럴 참 배선으로 각각 돌린 시작 검증 결과, 같은 클래스를 직접 만들어 커서를 연 결과, 그리고 그 실패를 복구 정책에 넘긴 판정을 출력한 터미널 기록." caption="단독 서버에서 능력 판정은 isStable=false · 출하 배선의 시작 검증은 통과, 리터럴 참 배선은 토폴로지를 지목하며 거절 · 커서를 열면 드라이버가 40573 으로 거절 · 복구 정책은 FAILED 와 failover 런북 — 20줄 · exit 0" zoom="true" -::: - -오플로그가 없는 단독 서버를 띄우고 같은 클래스들을 그대로 썼다. 리터럴 참을 넘긴 배선의 예외 메시지는 이 배포에 없는 것과 필요한 것을 함께 적는다. - -```text -change streams are enabled but unavailable here: {reason=topology is STANDALONE, which has no oplog}; a REPLICA_SET, SHARDED or ATLAS topology is required -``` - -출하 배선에서는 이 예외가 만들어지지 않는다. 같은 클래스를 직접 만들어 커서를 열면 드라이버가 명령 단계에서 거절한다. - -```text -Command execution failed on MongoDB server with error 40573 (Location40573): 'The $changeStream stage is only supported on replica sets' on server 127.0.0.1:57017. -``` - -그 실패를 복구 정책에 넘기면 판정이 나온다. - -```text -MongoChangeStreamRecoveryDecision[state=FAILED, autoResume=false, requiredRunbook=docs/mongodb/runbooks/failover.md] -``` - -정책은 서버 코드가 이력 소실이면 전용 런북을, 라벨이 재개 가능이면 재개를 고르고, 그 밖은 실패로 확정한다. 40573 은 셋째 갈래다. 붙는 런북은 프라이머리 선출과 서버 선택 지연을 다루고, 이 서버에 오플로그가 없다는 경우는 다루지 않는다. - -## 주석이 근거로 든 사실 - -:::evidence key="a06-f003-change-streams-true-shipped" alt="드라이버 쪽 구현을 만드는 빈 위에 겹쳐 있는 조건 넷(모듈 opt-in, 블로킹 템플릿 클래스, 리액티브 템플릿 클래스와 빈), 그 빈이 커서를 여는 호출 사슬, 소비자 빈이 요구하는 조건 다섯, 그 다섯을 구현하는 클래스 전수, 저장소 자신의 배선 시험 네 개, 그리고 이 자동 구성 파일에서 해당 플래그가 나오는 유일한 줄을 출력한 터미널 기록." caption="source 빈의 조건은 모듈 opt-in 과 리액티브 템플릿, 그리고 @ConditionalOnMissingBean · 소비자 조건 다섯의 구현체 열은 전부 시험 픽스처 · 배선 시험이 조립·미조립·미배선을 각각 고정 · 플래그는 검증기 인자 한 줄뿐 — 56줄 · exit 0" zoom="true" -::: - -드라이버 쪽 구현에는 빈 선언이 있다. 그 빈의 메서드에 붙은 조건은 `@ConditionalOnMissingBean` 하나지만, 그것을 감싼 중첩 설정 클래스가 리액티브 템플릿을 클래스로도 빈으로도 요구하고, 다시 그 바깥이 모듈 opt-in 을 요구한다. 무조건은 아니고 리액티브 템플릿이 있는 배포에서는 언제나다. 그 조건 넷 어디에도 변경 스트림 플래그는 없다. - -소비자 빈의 선언도 있다. 조건이 다섯인데 다섯 다 배포가 공급해야 하는 타입이고, 이 저장소는 다섯 중 어느 것도 빈으로 만들지 않는다. 열 개의 구현체가 전부 시험 안의 `private static final class` 다. - -저장소 자신의 배선 시험이 세 상태를 각각 고정한다. 리액티브 템플릿이 없으면 아무것도 만들지 않고, 있으면 source 는 만들고 투영기가 없으면 소비자는 만들지 않고, 다섯을 주면 소비자가 조립된다. - -주석이 없다고 적은 넷 가운데 `watch` 와 `resumeAfter`/`startAfter` 는 이 클래스에 있고, 커서 수명과 재연결은 같은 패키지의 소비자에 있다. - -## 확인하지 못한 것 - -소비자 빈의 조건 다섯을 공급하는 포크의 배포는 다루지 않았다. 이 저장소 안에서 확인한 것은 다섯 타입의 구현체가 전부 시험 픽스처라는 것, 조건 목록에 이 플래그가 없다는 것, 그리고 저장소 자신의 배선 시험이 그 세 상태를 각각 고정한다는 것이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-grpc-advanced-compat-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-grpc-advanced-compat-f01.md deleted file mode 100644 index 5bffb70..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-grpc-advanced-compat-f01.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -kind: CASE -slug: grpc-advanced-compat-f01 -title: 통합 다리의 메타데이터 조립이 메타데이터 예산을 검사하지 않는다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-advanced-compat-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-advanced-compat-f01 - file: ../../../final/evidence/rendered/grpc-advanced-compat-f01.svg -evidence: - - ../../../final/evidence/raw/grpc-advanced-compat-f01.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-advanced-compat#L120 이다. -module: grpc-advanced-compat -priority: P3 ---- - -# 통합 다리의 메타데이터 조립이 메타데이터 예산을 검사하지 않는다 - -허용 목록으로 키를 거르지만 값의 크기도, 합계도 보지 않는다. 클래스 javadoc 자신이 예산을 이 정책의 이유 중 하나로 든다 — 흐름의 내부 배관을 네트워크로 보내면 "it counts against the metadata budget." 그리고 같은 저장소의 다른 메타데이터 경로는 예산을 검사한다. - -## 문제 - -허용 목록으로 키를 거르지만 값의 크기도, 합계도 보지 않는다. - -클래스 javadoc 자신이 예산을 이 정책의 이유 중 하나로 든다 — 흐름의 내부 배관을 네트워크로 보내면 "it counts against the metadata budget." 그리고 같은 저장소의 다른 메타데이터 경로는 예산을 검사한다. - -## 결론 - -String.valueOf(value) 이므로 헤더 값이 임의의 객체일 때 그 문자열 표현이 그대로 실린다. - -Spring Integration 헤더에는 컬렉션이나 도메인 객체가 흔히 들어가므로 값 하나가 클 수 있다. - -수정은 이 record 에 GrpcMetadataBudget 를 성분으로 추가하고 metadataFrom 끝에서 검사하는 것이다. - -형태가 이미 옆 리프에 있다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : GrpcMetadataBudget 참조 26건 전수 검색과 다리의 메타데이터 조립 경로 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-advanced-compat#L120 에 있다. - -## 본문 - - - -허용 목록으로 키를 거르지만 값의 크기도, 합계도 보지 않는다. - -## GrpcMetadataBudget 참조 위치 - -:::evidence key="grpc-advanced-compat-f01" alt="코드베이스에서 GrpcMetadataBudget 를 검색한 출력 26줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcMetadataBudget 코드베이스 검색 — 26줄 · exit 0" zoom="true" -::: - -## 클래스 javadoc 자신이 예산을 이유로 든다 - -흐름의 내부 배관을 네트워크로 보내면 "it counts against the metadata budget." 그리고 같은 저장소의 다른 메타데이터 경로는 예산을 검사한다. - -## 값 하나가 클 수 있다 - -`String.valueOf(value)` 이므로 헤더 값이 임의의 객체일 때 그 문자열 표현이 그대로 실린다. Spring Integration 헤더에는 컬렉션이나 도메인 객체가 흔히 들어간다. - -## 수정 - -이 record 에 `GrpcMetadataBudget` 를 성분으로 추가하고 `metadataFrom` 끝에서 검사하는 것이다. 형태가 이미 옆 리프에 있다. - -## 확인하지 못한 것 - -실제 브라우저·프록시·서블릿 컨테이너로 이 다리를 돌리지 않았다. 그 인프라가 없다는 것이 이 가족의 기록이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-grpc-core-api-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-grpc-core-api-f01.md deleted file mode 100644 index 41cc5a0..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-grpc-core-api-f01.md +++ /dev/null @@ -1,91 +0,0 @@ ---- -kind: CASE -slug: grpc-core-api-f01 -title: 정책 목록의 가장 강한 성질을 이 저장소에서는 쓸 수 없다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-core-api-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-core-api-f01 - file: ../../../final/evidence/rendered/grpc-core-api-f01.svg -evidence: - - ../../../final/evidence/raw/grpc-core-api-f01.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-core-api#L140 이다. -module: grpc-core-api -priority: P3 ---- - -# 정책 목록의 가장 강한 성질을 이 저장소에서는 쓸 수 없다 - -withDescriptorMethods 를 부르는 곳은 이 리프의 테스트 두 줄뿐이다. GrpcMethodPolicyCatalog.builder() 를 부르는 곳은 저장소 전체에서 전부 테스트다. - -## 문제 - -withDescriptorMethods 를 부르는 곳은 이 리프의 테스트 두 줄뿐이다. - -GrpcMethodPolicyCatalog.builder() 를 부르는 곳은 저장소 전체에서 전부 테스트다. - -## 결론 - -그리고 그중 어느 것도 서술자 집합을 선언하지 않는다(위 두 줄 제외). - -자바독이 그 상태를 미리 서술한다 — 서술자가 없으면 "the catalog is materially weaker … there is nothing to compare a policy's method name against." 그리고 서술자가 없는 이유는 옆 리프에 있다. - -grpc-codegen 이 서술자 산출물을 정의하지만 저장소에 protobuf 플러그인이 없어 protoc 이 돌지 않는다. - -즉 이름 변경을 잡는 성질은 코드 생성 레인이 켜지기 전까지 성립할 수 없다. - -기록하는 이유는 이것이 이 클래스가 존재하는 첫 번째 이유로 적혀 있기 때문이다. - -수정은 코드 생성 레인이 생길 때 그 서술자를 목록 조립에 연결하는 것이고, 그때까지는 자바독이 그 조건을 명시하는 편이 낫다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : withDescriptorMethods 와 builder() 호출처 전수 검색으로 production 호출 유무 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-core-api#L140 에 있다. - -## 본문 - - - -`withDescriptorMethods` 를 부르는 곳은 이 리프의 테스트 두 줄뿐이다. - -``` -grpc-core-api/src/test/.../GrpcMethodPolicyCatalogTest.java:145 -grpc-core-api/src/test/.../GrpcMethodPolicyCatalogTest.java:175 -``` - -`GrpcMethodPolicyCatalog.builder()` 를 부르는 곳은 저장소 전체에서 전부 테스트이고, 그중 어느 것도 서술자 집합을 선언하지 않는다(위 두 줄 제외). - -## withDescriptorMethods 를 부르는 곳 - -:::evidence key="grpc-core-api-f01" alt="분석 문서 final/document.md#a20-grpc-core-api 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-core-api 발췌 — 15줄" zoom="true" -::: - -## 자바독이 그 상태를 미리 서술한다 - -서술자가 없으면 "the catalog is materially weaker … there is nothing to compare a policy's method name against." - -## 서술자가 없는 이유는 옆 리프에 있다 - -`grpc-codegen` 이 서술자 산출물을 정의하지만 저장소에 protobuf 플러그인이 없어 `protoc` 이 돌지 않는다. 즉 이름 변경을 잡는 성질은 코드 생성 레인이 켜지기 전까지 성립할 수 없다. - -## 기록하는 이유 - -이것이 이 클래스가 존재하는 첫 번째 이유로 적혀 있다. 수정은 코드 생성 레인이 생길 때 그 서술자를 목록 조립에 연결하는 것이고, 그때까지는 자바독이 그 조건을 명시하는 편이 낫다. P3. - -## 확인하지 못한 것 - -서술자 대조 경로를 실제 스키마로 돌려 보지 않았다. 저장소에 컴파일된 서술자가 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-grpc-observability-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-grpc-observability-f01.md deleted file mode 100644 index 2858cbf..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-grpc-observability-f01.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -kind: CASE -slug: grpc-observability-f01 -title: queueHighWatermark 는 요구되고 검증되지만 아무도 읽지 않는다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-observability-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-observability-f01 - file: ../../../final/evidence/rendered/grpc-observability-f01.svg -evidence: - - ../../../final/evidence/raw/grpc-observability-f01.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-observability#L197 이다. -module: grpc-observability -priority: P3 ---- - -# queueHighWatermark 는 요구되고 검증되지만 아무도 읽지 않는다 - -GrpcStreamObservation 의 7성분 중 queueHighWatermark 만 소비자가 없다. tags() 에 없고, GrpcObservationConvention.record(GrpcStreamObservation) 이 등록하는 세 meter(STREAM_LIFETIME·STREAM_MESSAGES·STREAM_FLOW_CONTROL_STALLS) 어디에도 들어가지 않는다. - -## 문제 - -GrpcStreamObservation 의 7성분 중 queueHighWatermark 만 소비자가 없다. - -tags() 에 없고, GrpcObservationConvention.record(GrpcStreamObservation) 이 등록하는 세 meter(STREAM_LIFETIME·STREAM_MESSAGES·STREAM_FLOW_CONTROL_STALLS) 어디에도 들어가지 않는다. - -## 결론 - -테스트도 250L 을 넘기고 그 값에 대해 아무것도 단언하지 않는다. - -클래스 javadoc 이 "what is recorded instead is …" 로 세 가지를 열거하는데 그 목록에도 없다. - -즉 서술과 구현은 일치하고, 어긋난 것은 필수 생성자 인자라는 점이다. - -호출자는 측정해서 넘겨야 하고 그 값은 버려진다. - -수정은 둘 중 하나다 — STREAM_QUEUE_HIGH_WATERMARK gauge/counter 를 추가하거나, 성분에서 뺀다. - -큐 최고 수위는 소비자 지연의 직접 지표이므로 전자가 이 클래스의 목적에 맞는다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : GrpcStreamObservation 참조 5건 검색과 record 오버로드가 등록하는 meter 목록 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-observability#L197 에 있다. - -## 본문 - - - -`GrpcStreamObservation` 의 7성분 중 `queueHighWatermark` 만 소비자가 없다. - -``` -GrpcStreamObservation.java:23 long queueHighWatermark, ← 선언 -GrpcStreamObservation.java:31 … || queueHighWatermark < 0 ← 검증 -그 외 저장소 전체 매치 0 -``` - -## GrpcStreamObservation 참조 위치 - -:::evidence key="grpc-observability-f01" alt="코드베이스에서 GrpcStreamObservation 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcStreamObservation 코드베이스 검색 — 5줄 · exit 0" zoom="true" -::: - -## 세 meter 어디에도 들어가지 않는다 - -`tags()` 에 없고, `GrpcObservationConvention.record(GrpcStreamObservation)` 이 등록하는 `STREAM_LIFETIME`·`STREAM_MESSAGES`·`STREAM_FLOW_CONTROL_STALLS` 어디에도 없다. 테스트도 `250L` 을 넘기고 그 값에 대해 아무것도 단언하지 않는다. - -## 서술과 구현은 일치한다 - -클래스 javadoc 이 "what is recorded instead is …" 로 세 가지를 열거하는데 그 목록에도 없다. 어긋난 것은 **필수 생성자 인자**라는 점이다 — 호출자는 측정해서 넘겨야 하고 그 값은 버려진다. - -## 수정 - -`STREAM_QUEUE_HIGH_WATERMARK` gauge/counter 를 추가하거나, 성분에서 뺀다. 큐 최고 수위는 소비자 지연의 직접 지표이므로 전자가 이 클래스의 목적에 맞는다. - -## 확인하지 못한 것 - -이 리프를 실제 MeterRegistry 에 배선해 돌린 적이 없다. 배선 자체가 없으므로 런타임 관측이 불가능하다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-grpc-policy-f07.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-grpc-policy-f07.md deleted file mode 100644 index b7263bc..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-grpc-policy-f07.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -kind: CASE -slug: grpc-policy-f07 -title: clearAfterTask 는 합법 값이 하나뿐인 성분이고, 아무도 읽지 않는다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-policy-f07 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-policy-f07 - file: ../../../final/evidence/rendered/grpc-policy-f07.svg -evidence: - - ../../../final/evidence/raw/grpc-policy-f07.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-policy#L324 이다. -module: grpc-policy -priority: P3 ---- - -# clearAfterTask 는 합법 값이 하나뿐인 성분이고, 아무도 읽지 않는다 - -false 를 무조건 거부하므로 이 성분이 가질 수 있는 값은 true 하나다. 그리고 저장소 전체에서 clearAfterTask() 를 읽는 production 코드가 없다 — 호출처는 이 생성자의 가드와 테스트의 단언 한 줄뿐이다. - -## 문제 - -false 를 무조건 거부하므로 이 성분이 가질 수 있는 값은 true 하나다. - -그리고 저장소 전체에서 clearAfterTask() 를 읽는 production 코드가 없다 — 호출처는 이 생성자의 가드와 테스트의 단언 한 줄뿐이다. - -## 결론 - -읽지 않아도 되는 이유는 GrpcContextBinder 가 옳게 쓰였기 때문이다. - -runWith·callWith·wrap 이 전부 finally 에서 detach 한다. - -불변식이 이미 구조로 지켜진다. - -그래서 이 성분은 설정처럼 보이지만 설정이 아니다. - -읽는 사람은 정책으로 끌 수 있는 것이라고 읽고, 테스트는 그 가드를 시험한다. - -수정은 성분을 지우고 javadoc 에 "always cleared" 를 남기는 것이다. - -그러면 backgroundWork()·stable() 이 인자 하나가 되고, 불변식은 검증이 아니라 구조가 된다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : GrpcContextBinder 참조 11건 검색과 생성자 가드가 허용하는 값 범위 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-policy#L324 에 있다. - -## 본문 - - - -`false` 를 무조건 거부하므로 이 성분이 가질 수 있는 값은 `true` 하나다. 그리고 저장소 전체에서 `clearAfterTask()` 를 읽는 production 코드가 없다 — 호출처는 이 생성자의 가드와 테스트의 단언 한 줄뿐이다. - -## GrpcContextBinder 참조 위치 - -:::evidence key="grpc-policy-f07" alt="코드베이스에서 GrpcContextBinder 를 검색한 출력 11줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcContextBinder 코드베이스 검색 — 11줄 · exit 0" zoom="true" -::: - -## 읽지 않아도 되는 이유 - -`GrpcContextBinder` 가 옳게 쓰였다 — `runWith`·`callWith`·`wrap` 이 전부 `finally` 에서 detach 한다. 불변식이 이미 구조로 지켜진다. - -## 그래서 설정처럼 보이지만 설정이 아니다 - -읽는 사람은 정책으로 끌 수 있는 것이라고 읽고, 테스트는 그 가드를 시험한다. 수정은 성분을 지우고 javadoc 에 "always cleared" 를 남기는 것이다. 그러면 `backgroundWork()`·`stable()` 이 인자 하나가 되고, 불변식은 검증이 아니라 구조가 된다. - -## 확인하지 못한 것 - -이 성분을 false 로 만들어 동작 차이를 관측하지 않았다. 생성자가 false 를 무조건 거부한다는 것으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-grpc-server-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-grpc-server-f03.md deleted file mode 100644 index 944b8ac..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-grpc-server-f03.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -kind: CASE -slug: grpc-server-f03 -title: 빌더 경로에서 순서 규칙 넷 중 셋이 발화할 수 없다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-server-f03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-server-f03 - file: ../../../final/evidence/rendered/grpc-server-f03.svg -evidence: - - ../../../final/evidence/raw/grpc-server-f03.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-server#L196 이다. -module: grpc-server -priority: P3 ---- - -# 빌더 경로에서 순서 규칙 넷 중 셋이 발화할 수 없다 - -byStage 는 EnumMap 이므로 keySet() 은 언제나 열거형 선언 순서다. 그리고 stage(...) 가 같은 단계의 두 번째 등록을 이미 거부한다. - -## 문제 - -byStage 는 EnumMap 이므로 keySet() 은 언제나 열거형 선언 순서다. - -그리고 stage(...) 가 같은 단계의 두 번째 등록을 이미 거부한다. - -## 결론 - -따라서 빌더가 만드는 목록에서는 중복도, 역순도, 예외 경계가 최외곽이 아닌 경우도 발생할 수 없다. - -발화 가능한 규칙은 필수 단계 누락 하나다. - -결함은 아니다 — 나머지 셋은 violations(List) 를 직접 부르는 외부 호출자를 위한 것이고, 테스트가 그 경로로 셋을 모두 확인한다. - -기록하는 이유는 빌더를 쓰는 조립 코드가 그 셋의 보호를 받는다고 읽기 쉽기 때문이다. - -실제 보호는 자료구조가 준다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : EnumMap 의 keySet 순서 보장과 stage 등록 가드가 이미 막는 경우의 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-server#L196 에 있다. - -## 본문 - - - -`byStage` 는 `EnumMap` 이므로 `keySet()` 은 언제나 열거형 선언 순서다. 그리고 `stage(...)` 가 같은 단계의 두 번째 등록을 이미 거부한다. - -## byStage 가 EnumMap 이다 - -:::evidence key="grpc-server-f03" alt="분석 문서 final/document.md#a20-grpc-server 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-server 발췌 — 15줄" zoom="true" -::: - -## 빌더 경로에서는 셋이 발생할 수 없다 - -중복도, 역순도, 예외 경계가 최외곽이 아닌 경우도 발생할 수 없다. 발화 가능한 규칙은 필수 단계 누락 하나다. - -## 결함은 아니다 - -나머지 셋은 `violations(List)` 를 직접 부르는 외부 호출자를 위한 것이고, 테스트가 그 경로로 셋을 모두 확인한다. 기록하는 이유는 빌더를 쓰는 조립 코드가 그 셋의 보호를 받는다고 읽기 쉽기 때문이다 — 실제 보호는 자료구조가 준다. - -## 확인하지 못한 것 - -빌더 경로로 네 규칙을 실제로 발화시켜 보지 않았다. 자료구조의 순서 보장과 선행 가드로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-grpc-spring-boot-starter-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-grpc-spring-boot-starter-f01.md deleted file mode 100644 index 5f72f30..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-grpc-spring-boot-starter-f01.md +++ /dev/null @@ -1,97 +0,0 @@ ---- -kind: CASE -slug: grpc-spring-boot-starter-f01 -title: 시작 검증기가 시작 시 실행되지 않는다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-spring-boot-starter-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-spring-boot-starter-f01 - file: ../../../final/evidence/rendered/grpc-spring-boot-starter-f01.svg - - key: grpc-spring-boot-starter-f01-diagram - file: ../../../final/assets/diagrams/grpc-spring-boot-starter-f01.svg -evidence: - - ../../../final/evidence/raw/grpc-spring-boot-starter-f01.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-spring-boot-starter#L183 이다. -module: grpc-spring-boot-starter -priority: P2 ---- - -# 시작 검증기가 시작 시 실행되지 않는다 - -GrpcPlatformStartupValidator 를 이름으로 부르는 파일은 둘뿐이다 — 자기 자신과 자기 테스트. GrpcPlatformAutoConfiguration 은 빈 9개를 만들고 requireValid 를 부르지 않는다. - -## 문제 - -GrpcPlatformStartupValidator 를 이름으로 부르는 파일은 둘뿐이다 — 자기 자신과 자기 테스트. - -GrpcPlatformAutoConfiguration 은 빈 9개를 만들고 requireValid 를 부르지 않는다. - -## 결론 - -초기화 콜백도, @PostConstruct 도, ApplicationRunner 도 없다. - -그래서 클래스 javadoc 이 약속한 성질이 성립하지 않는다 — "Refuses to start on a configuration that would be wrong in a way nobody would notice." 지금은 그 설정으로 그냥 시작한다. - -검증기가 유일한 소비자인 설정 키가 넷이다. - -transport — production 이 아닌 전송을 거부할 곳이 없다. - -게다가 자동 설정은 이 값을 보지 않고 GrpcServerProfile.stableNetty(...) 를 하드코딩한다(§17.2). - -tls-enabled · trust-all-certificates — 배포 환경의 TLS 바닥을 강제할 곳이 없다. - -operation-ledger-enabled — 멱등 키 필수 메서드가 원장 없이 열리는 것을 막을 곳이 없다. - -같은 저장소가 이 형태를 두 번 기록했다 — WebPlatformStartupValidator 가 시작 시 실행되지 않고, BrokerAclManifest 의 시작 자기점검이 없다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : GrpcPlatformStartupValidator 참조 15건 검색과 자동 설정이 만드는 빈 목록 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-spring-boot-starter#L183 에 있다. - -## 본문 - - - -`GrpcPlatformStartupValidator` 를 이름으로 부르는 파일은 둘뿐이다 — 자기 자신과 자기 테스트. - -## 검증기가 도는 데 빠진 것 - -:::evidence key="grpc-spring-boot-starter-f01-diagram" alt="설정 프로퍼티 셋만 검증기 입력 안에 놓이고 모듈 id 집합 생산자와 requireValid 호출 지점이 바깥에 빗금으로 놓인다" caption="검증기가 도는 데 빠진 것" zoom="false" -::: - -`GrpcPlatformAutoConfiguration` 은 빈 9개를 만들고 `requireValid` 를 부르지 않는다. 초기화 콜백도, `@PostConstruct` 도, `ApplicationRunner` 도 없다. 그래서 클래스 javadoc 이 약속한 성질이 성립하지 않는다 — "Refuses to start on a configuration that would be wrong in a way nobody would notice." - -## GrpcPlatformStartupValidator 참조 위치 - -:::evidence key="grpc-spring-boot-starter-f01" alt="코드베이스에서 GrpcPlatformStartupValidator 를 검색한 출력 15줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcPlatformStartupValidator 코드베이스 검색 — 15줄 · exit 0" zoom="true" -::: - -## 함께 사라지는 설정 키 넷 - -`transport` — production 이 아닌 전송을 거부할 곳이 없다(게다가 자동 설정은 이 값을 보지 않고 `GrpcServerProfile.stableNetty(...)` 를 하드코딩한다, §17.2). `tls-enabled` · `trust-all-certificates` — 배포 환경의 TLS 바닥을 강제할 곳이 없다. `operation-ledger-enabled` — 멱등 키 필수 메서드가 원장 없이 열리는 것을 막을 곳이 없다. - -## 정본이 저장소 안에 둘 있다 - -messaging 의 `StartupProfileValidation` 은 `InitializingBean.afterPropertiesSet` 으로 돌려 그 문제를 이미 한 번 해결했고, fileserver 는 `attestMapping()` 을 app-bootstrap 의 `@Bean` 으로 연결했다. 반대로 같은 저장소가 이 형태를 두 번 기록했다 — `WebPlatformStartupValidator` 가 시작 시 실행되지 않고, `BrokerAclManifest` 의 시작 자기점검이 없다. - -## 왜 배선되지 않았는지가 서명에 보인다 - -`violations` 는 넷을 받고 그중 셋에 생산자가 없다. 특히 마지막은 "스타터가 해석한 모듈 id 집합" 인데 그것을 실행 중에 산출하는 코드가 없다. P2. - -## 확인하지 못한 것 - -스타터를 실제 애플리케이션에 올려 컨텍스트를 세우지 않았다. build-only 이고 이 스타터를 의존하는 모듈이 저장소에 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-grpc-spring-boot-starter-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-grpc-spring-boot-starter-f03.md deleted file mode 100644 index 7507c47..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-grpc-spring-boot-starter-f03.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -kind: CASE -slug: grpc-spring-boot-starter-f03 -title: default-unary-deadline 은 읽는 코드가 저장소에 없다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-spring-boot-starter-f03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-spring-boot-starter-f03 - file: ../../../final/evidence/rendered/grpc-spring-boot-starter-f03.svg -evidence: - - ../../../final/evidence/raw/grpc-spring-boot-starter-f03.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-spring-boot-starter#L236 이다. -module: grpc-spring-boot-starter -priority: P3 ---- - -# default-unary-deadline 은 읽는 코드가 저장소에 없다 - -getDefaultUnaryDeadline() 의 호출자가 0 이다. 검증기도 이 값을 쓰지 않는다 — 검증기가 보는 것은 정책 목록의 policy.deadline().usable() 이고 그 값이 0 이면 위반을 낸다. - -## 문제 - -getDefaultUnaryDeadline() 의 호출자가 0 이다. - -검증기도 이 값을 쓰지 않는다 — 검증기가 보는 것은 정책 목록의 policy.deadline().usable() 이고 그 값이 0 이면 위반을 낸다. - -## 결론 - -즉 자바독이 말하는 "선언하지 않은 메서드에 적용되는 기본 마감" 을 적용하는 코드가 없다. - -ignoreUnknownFields = false 라서 이 키를 설정하는 것은 성공하고 아무 효과가 없다. - -수정은 그 기본값을 실제로 적용하는 지점을 만들거나(정책 목록 조립 시), 필드를 제거하는 것이다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : getDefaultUnaryDeadline 호출처 검색과 검증기가 실제로 읽는 값 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-spring-boot-starter#L236 에 있다. - -## 본문 - - - -`getDefaultUnaryDeadline()` 의 호출자가 0 이다. 검증기도 이 값을 쓰지 않는다 — 검증기가 보는 것은 정책 목록의 `policy.deadline().usable()` 이고 그 값이 0 이면 위반을 낸다. - -## 검증기가 실제로 보는 값 - -:::evidence key="grpc-spring-boot-starter-f03" alt="분석 문서 final/document.md#a20-grpc-spring-boot-starter 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-spring-boot-starter 발췌 — 15줄" zoom="true" -::: - -## 자바독이 말하는 기본 마감을 적용하는 코드가 없다 - -"선언하지 않은 메서드에 적용되는 기본 마감" 이다. `ignoreUnknownFields = false` 라서 이 키를 설정하는 것은 성공하고 아무 효과가 없다. - -## 수정 - -그 기본값을 실제로 적용하는 지점을 만들거나(정책 목록 조립 시), 필드를 제거하는 것이다. - -## 확인하지 못한 것 - -이 값을 바꿔 동작 차이가 없음을 실행으로 확인하지 않았다. 호출자가 0 이라는 것으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-grpc-testkit-f05.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-grpc-testkit-f05.md deleted file mode 100644 index 4a5176f..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-grpc-testkit-f05.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -kind: CASE -slug: grpc-testkit-f05 -title: 계약 스위트 둘이 결과를 만드는 코드를 갖지 않는다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-testkit-f05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-testkit-f05 - file: ../../../final/evidence/rendered/grpc-testkit-f05.svg -evidence: - - ../../../final/evidence/raw/grpc-testkit-f05.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-testkit#L245 이다. -module: grpc-testkit -priority: P3 ---- - -# 계약 스위트 둘이 결과를 만드는 코드를 갖지 않는다 - -GrpcUnaryReliabilityContract 와 GrpcServerStreamingContract 는 순수 평가기다 — List 를 받아 위반을 돌려준다. 시나리오 정의(단항 3 · 스트리밍 5)와 그 정합성 검사는 훌륭하다. - -## 문제 - -GrpcUnaryReliabilityContract 와 GrpcServerStreamingContract 는 순수 평가기다 — List 를 받아 위반을 돌려준다. - -시나리오 정의(단항 3 · 스트리밍 5)와 그 정합성 검사는 훌륭하다. - -## 결론 - -스위트가 자기 커버리지를 열거하고, 돌지 않은 시나리오를 침묵이 아니라 위반으로 만든다. - -빠진 것은 그 시나리오를 돌리는 쪽이다. - -GrpcUnaryContractResult·GrpcStreamingContractResult 를 만드는 코드는 저장소 전체에서 두 테스트뿐이고, 둘 다 리터럴로 만든다. - -그래서 "이 플랫폼은 비멱등 변경을 재시도하지 않는다" 를 뒷받침하는 것은, 그 문장을 리터럴로 적은 뒤 평가기가 그것을 읽고 위반이 없다고 답하는 절차다. - -평가기의 산술은 옳고, 대상이 관측이 아니다. - -in-process 픽스처(§1)는 이 시나리오들을 돌릴 재료를 이미 갖고 있다 — 인터셉터를 끼운 서버, 상태 매핑, 스트리밍 핸들러. - -수정은 픽스처 위에서 세 시나리오를 실행해 attempts·businessInvocations 를 세는 러너를 두는 것이다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : GrpcUnaryReliabilityContract 참조 10건 검색과 결과를 만드는 코드의 존재 여부 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-testkit#L245 에 있다. - -## 본문 - - - -`GrpcUnaryReliabilityContract` 와 `GrpcServerStreamingContract` 는 순수 평가기다 — `List` 를 받아 위반을 돌려준다. 시나리오 정의(단항 3 · 스트리밍 5)와 그 정합성 검사는 훌륭하다. 스위트가 자기 커버리지를 열거하고, 돌지 않은 시나리오를 침묵이 아니라 위반으로 만든다. - -## GrpcUnaryReliabilityContract 참조 위치 - -:::evidence key="grpc-testkit-f05" alt="코드베이스에서 GrpcUnaryReliabilityContract 를 검색한 출력 10줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcUnaryReliabilityContract 코드베이스 검색 — 10줄 · exit 0" zoom="true" -::: - -## 빠진 것은 그 시나리오를 돌리는 쪽이다 - -`GrpcUnaryContractResult`·`GrpcStreamingContractResult` 를 만드는 코드는 저장소 전체에서 두 테스트뿐이고, 둘 다 리터럴로 만든다. 그래서 "이 플랫폼은 비멱등 변경을 재시도하지 않는다" 를 뒷받침하는 것은, 그 문장을 리터럴로 적은 뒤 평가기가 그것을 읽고 위반이 없다고 답하는 절차다. - -## 평가기의 산술은 옳고 대상이 관측이 아니다 - -in-process 픽스처(§1)는 이 시나리오들을 돌릴 재료를 이미 갖고 있다 — 인터셉터를 끼운 서버, 상태 매핑, 스트리밍 핸들러. 수정은 픽스처 위에서 세 시나리오를 실행해 `attempts`·`businessInvocations` 를 세는 러너를 두는 것이다. - -## 확인하지 못한 것 - -두 계약 스위트를 실제 결과로 돌려 보지 않았다. 순수 평가기이고 입력을 만드는 코드가 없다는 것으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-admin-api-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-admin-api-f03.md deleted file mode 100644 index 610e69d..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-admin-api-f03.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -kind: CASE -slug: messaging-admin-api-f03 -title: TopologyManagementMode 가 어디에도 연결되어 있지 않다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-admin-api-f03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-admin-api-f03 - file: ../../../final/evidence/rendered/messaging-admin-api-f03.svg -evidence: - - ../../../final/evidence/raw/messaging-admin-api-f03.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-admin-api#L952 이다. -module: messaging-admin-api -priority: P3 ---- - -# TopologyManagementMode 가 어디에도 연결되어 있지 않다 - -자기 선언과 테스트 4건이 전부다. 이 enum 을 읽는 프로덕션 코드도, 이것으로 매핑되는 설정 프로퍼티도 없다(EVD-302). - -## 문제 - -자기 선언과 테스트 4건이 전부다. - -이 enum 을 읽는 프로덕션 코드도, 이것으로 매핑되는 설정 프로퍼티도 없다(EVD-302). - -## 결론 - -두 선택지가 있다: 실제로 배선하거나(선언된 토폴로지 관리 모드를 설정에서 읽고 requireSafeFor(isProduction) 를 기동 시 호출), 제거한다. - -지금 상태는 "규칙이 코드에 있다" 는 인상만 준다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : TopologyManagementMode 참조 5건 검색으로 프로덕션 읽기와 설정 프로퍼티 매핑 유무 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-admin-api#L952 에 있다. - -## 본문 - - - -자기 선언과 테스트 4건이 전부다. 이 enum 을 읽는 프로덕션 코드도, 이것으로 매핑되는 설정 프로퍼티도 없다(`EVD-302`). - -## TopologyManagementMode 참조 위치 - -:::evidence key="messaging-admin-api-f03" alt="코드베이스에서 TopologyManagementMode 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="TopologyManagementMode 코드베이스 검색 — 5줄 · exit 0" zoom="true" -::: - -## 두 선택지 - -실제로 배선하거나(선언된 토폴로지 관리 모드를 설정에서 읽고 `requireSafeFor(isProduction)` 를 기동 시 호출), 제거한다. 지금 상태는 "규칙이 코드에 있다" 는 인상만 준다. - -## 확인하지 못한 것 - -부팅된 컨텍스트에서 이 enum 이 어떤 경로로도 읽히지 않는 것을 런타임으로 확인하지 않았다. 참조 전수로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-admin-api-f04.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-admin-api-f04.md deleted file mode 100644 index d3934e2..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-admin-api-f04.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -kind: CASE -slug: messaging-admin-api-f04 -title: 운영자용 표면 전체에 프로덕션 소비자가 없다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-admin-api-f04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-admin-api-f04 - file: ../../../final/evidence/rendered/messaging-admin-api-f04.svg -evidence: - - ../../../final/evidence/raw/messaging-admin-api-f04.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-admin-api#L956 이다. -module: messaging-admin-api -priority: P3 ---- - -# 운영자용 표면 전체에 프로덕션 소비자가 없다 - -ReplayPlan.describeImpact, RedrivePlan.describeImpact, ReplayResult.fellShortOfTheEstimate, RedriveResult.isFullyAccounted, AdminOperationLease.isResumption — 다섯 개가 전부 테스트에서만 호출된다(EVD-302). 이것들은 잉여 코드가 아니라 아직 소비자가 없는 잘 설계된 표면이다. - -## 문제 - -ReplayPlan.describeImpact, RedrivePlan.describeImpact, ReplayResult.fellShortOfTheEstimate, RedriveResult.isFullyAccounted, AdminOperationLease.isResumption — 다섯 개가 전부 테스트에서만 호출된다(EVD-302). - -이것들은 잉여 코드가 아니라 아직 소비자가 없는 잘 설계된 표면이다. - -## 결론 - -describeImpact 의 javadoc 이 "operator-facing" 이라고 쓰고 ApprovedPlanExecutionTest.aReplayIntoTheLiveGroupSaysSoInCapitals 가 대문자 LIVE 까지 검증한다. - -문제는 그 문자열이 도달할 화면이 없다는 것이다. - -admin API·CLI 계층을 만들 때 이 다섯이 그 계층의 명세라는 점을 문서에 남겨 두는 것이 낫다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : ReplayPlan 참조 9건 검색으로 다섯 표면의 호출처가 테스트뿐임을 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-admin-api#L956 에 있다. - -## 본문 - - - -`ReplayPlan.describeImpact`, `RedrivePlan.describeImpact`, `ReplayResult.fellShortOfTheEstimate`, `RedriveResult.isFullyAccounted`, `AdminOperationLease.isResumption` — 다섯 개가 전부 테스트에서만 호출된다(`EVD-302`). - -## ReplayPlan 참조 위치 - -:::evidence key="messaging-admin-api-f04" alt="코드베이스에서 ReplayPlan 를 검색한 출력 9줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ReplayPlan 코드베이스 검색 — 9줄 · exit 0" zoom="true" -::: - -## 잉여 코드가 아니라 아직 소비자가 없는 표면이다 - -`describeImpact` 의 javadoc 이 "operator-facing" 이라고 쓰고 `ApprovedPlanExecutionTest.aReplayIntoTheLiveGroupSaysSoInCapitals` 가 대문자 `LIVE` 까지 검증한다. 문제는 그 문자열이 도달할 화면이 없다는 것이다. - -## 남겨 둘 것 - -admin API·CLI 계층을 만들 때 이 다섯이 그 계층의 명세라는 점을 문서에 남겨 두는 것이 낫다. - -## 확인하지 못한 것 - -운영자 도구가 이 저장소 밖에 존재하는지 확인할 수 없었다. 저장소 안의 참조로만 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-admin-api-f06.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-admin-api-f06.md deleted file mode 100644 index 2830074..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-admin-api-f06.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -kind: CASE -slug: messaging-admin-api-f06 -title: messaging-policy 의존이 import 0건이다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-admin-api-f06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-admin-api-f06 - file: ../../../final/evidence/rendered/messaging-admin-api-f06.svg -evidence: - - ../../../final/evidence/raw/messaging-admin-api-f06.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-admin-api#L968 이다. -module: messaging-admin-api -priority: P3 ---- - -# messaging-policy 의존이 import 0건이다 - -선언만 남아 있다. 제거 후보. - -## 문제 - -선언만 남아 있다. - -제거 후보. - -## 결론 - -선언만 남아 있다. - -제거 후보. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : 선언된 의존에 대한 패키지 이름 import 전수 검색 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-admin-api#L968 에 있다. - -## 본문 - - - -`messaging-policy` 의존이 선언만 남아 있고 import 는 0건이다. - -## 선언만 남은 의존 - -:::evidence key="messaging-admin-api-f06" alt="분석 문서 final/document.md#a19-messaging-admin-api 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a19-messaging-admin-api 발췌 — 15줄" zoom="true" -::: - -## 제거 후보 - -## 확인하지 못한 것 - -의존을 제거하고 빌드를 돌려 보지 않았다. import 0 건으로 판정했으므로 간접 사용은 배제하지 못했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-admin-runtime-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-admin-runtime-f03.md deleted file mode 100644 index 731d9f2..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-admin-runtime-f03.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -kind: CASE -slug: messaging-admin-runtime-f03 -title: 오케스트레이터가 어디에서도 실행되지 않는다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-admin-runtime-f03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-admin-runtime-f03 - file: ../../../final/evidence/rendered/messaging-admin-runtime-f03.svg - - key: messaging-admin-runtime-f03-diagram - file: ../../../final/assets/diagrams/messaging-admin-runtime-f03.svg -evidence: - - ../../../final/evidence/raw/messaging-admin-runtime-f03.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-admin-runtime#L918 이다. -module: messaging-admin-runtime -priority: P2 ---- - -# 오케스트레이터가 어디에서도 실행되지 않는다 - -DefaultMessagingAdminService 257줄과 ReplayService 99줄이 프로덕션에서도 테스트에서도 인스턴스화되지 않는다(EVD-307). 검사 순서·저널 시퀀스·실패 시 재던짐·실패 코드 정제가 전부 미검증이다. - -## 문제 - -DefaultMessagingAdminService 257줄과 ReplayService 99줄이 프로덕션에서도 테스트에서도 인스턴스화되지 않는다(EVD-307). - -검사 순서·저널 시퀀스·실패 시 재던짐·실패 코드 정제가 전부 미검증이다. - -## 결론 - -DefaultMessagingAdminService 의 생성자는 10개 인자를 받고 그중 8개가 SPI 또는 Supplier 이므로, 대역으로 조립하는 테스트를 쓰는 비용은 낮다. - -§12.1(a)의 회귀 테스트도 이 층에서 쓰는 것이 자연스럽다 — 저널·리스·리드라이브 루프가 함께 도는 것이 결함이 나타나는 조건이기 때문이다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : DefaultMessagingAdminService 참조 2건 검색으로 프로덕션·테스트 양쪽 인스턴스화 지점 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-admin-runtime#L918 에 있다. - -## 본문 - - - -`DefaultMessagingAdminService` 257줄과 `ReplayService` 99줄이 프로덕션에서도 테스트에서도 인스턴스화되지 않는다(`EVD-307`). - -## 실행되지 않는 계층 - -:::evidence key="messaging-admin-runtime-f03-diagram" alt="SPI 각각의 단위 테스트만 검증된 범위 안에 놓이고 검사 순서와 저널 시퀀스, 실패 재던짐과 코드 정제가 바깥에 빗금으로 놓인다" caption="실행되지 않는 계층" zoom="false" -::: - -검사 순서·저널 시퀀스·실패 시 재던짐·실패 코드 정제가 전부 미검증이다. - -## DefaultMessagingAdminService 참조 위치 - -:::evidence key="messaging-admin-runtime-f03" alt="코드베이스에서 DefaultMessagingAdminService 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DefaultMessagingAdminService 코드베이스 검색 — 2줄 · exit 0" zoom="true" -::: - -## 대역으로 조립하는 비용이 낮다 - -`DefaultMessagingAdminService` 의 생성자는 10개 인자를 받고 그중 8개가 SPI 또는 `Supplier` 다. §12.1(a)의 회귀 테스트도 이 층에서 쓰는 것이 자연스럽다 — 저널·리스·리드라이브 루프가 함께 도는 것이 결함이 나타나는 조건이기 때문이다. - -## 확인하지 못한 것 - -부팅된 컨텍스트에서 admin 평면을 켜고 빈 그래프를 관측하지 않았다. 런타임 관측을 수행하지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-admin-runtime-f04.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-admin-runtime-f04.md deleted file mode 100644 index 4eda8b8..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-admin-runtime-f04.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -kind: CASE -slug: messaging-admin-runtime-f04 -title: public 인터페이스를 패키지 밖에서 구현할 수 없다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-admin-runtime-f04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-admin-runtime-f04 - file: ../../../final/evidence/rendered/messaging-admin-runtime-f04.svg -evidence: - - ../../../final/evidence/raw/messaging-admin-runtime-f04.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-admin-runtime#L924 이다. -module: messaging-admin-runtime -priority: P3 ---- - -# public 인터페이스를 패키지 밖에서 구현할 수 없다 - -RedriveEstimator(public)의 반환 타입 RedriveEstimate 가 package-private 이다(EVD-308). DefaultMessagingAdminService 의 public 생성자가 그 인터페이스를 요구하므로, 외부 조립이 불가능하다. - -## 문제 - -RedriveEstimator(public)의 반환 타입 RedriveEstimate 가 package-private 이다(EVD-308). - -DefaultMessagingAdminService 의 public 생성자가 그 인터페이스를 요구하므로, 외부 조립이 불가능하다. - -## 결론 - -RedriveEstimate 를 public 으로 올리는 것이 최소 수정이다. - -더 나은 방향은 DefaultMessagingAdminService 밖의 최상위 record 로 꺼내는 것 — 지금은 오케스트레이터의 내부 타입이 SPI 계약의 일부가 되어 있다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : RedriveEstimator 참조 3건 검색과 반환 타입 및 생성자 인자의 접근 제한자 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-admin-runtime#L924 에 있다. - -## 본문 - - - -`RedriveEstimator`(public)의 반환 타입 `RedriveEstimate` 가 package-private 이다(`EVD-308`). `DefaultMessagingAdminService` 의 public 생성자가 그 인터페이스를 요구하므로, 외부 조립이 불가능하다. - -## RedriveEstimator 참조 위치 - -:::evidence key="messaging-admin-runtime-f04" alt="코드베이스에서 RedriveEstimator 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RedriveEstimator 코드베이스 검색 — 3줄 · exit 0" zoom="true" -::: - -## 최소 수정과 더 나은 방향 - -`RedriveEstimate` 를 public 으로 올리는 것이 최소 수정이다. 더 나은 방향은 `DefaultMessagingAdminService` 밖의 최상위 record 로 꺼내는 것 — 지금은 오케스트레이터의 내부 타입이 SPI 계약의 일부가 되어 있다. - -## 확인하지 못한 것 - -패키지 밖에서 실제로 구현을 시도해 컴파일 실패를 관측하지 않았다. 접근 제한자 조합으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-admin-runtime-f09.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-admin-runtime-f09.md deleted file mode 100644 index 66455ab..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-admin-runtime-f09.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -kind: CASE -slug: messaging-admin-runtime-f09 -title: 선언된 의존 6개 중 3개가 import 0건 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-admin-runtime-f09 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-admin-runtime-f09 - file: ../../../final/evidence/rendered/messaging-admin-runtime-f09.svg -evidence: - - ../../../final/evidence/raw/messaging-admin-runtime-f09.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-admin-runtime#L957 이다. -module: messaging-admin-runtime -priority: P3 ---- - -# 선언된 의존 6개 중 3개가 import 0건 - -messaging-policy, messaging-transport-spi, messaging-security. 제거 후보. - -## 문제 - -messaging-policy, messaging-transport-spi, messaging-security. - -제거 후보. - -## 결론 - -messaging-policy, messaging-transport-spi, messaging-security. - -제거 후보. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : 선언된 의존 6개에 대한 패키지 이름 import 전수 검색 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-admin-runtime#L957 에 있다. - -## 본문 - - - -선언된 의존 6개 중 3개가 import 0건이다 — `messaging-policy`, `messaging-transport-spi`, `messaging-security`. - -## import 0 건인 세 의존 - -:::evidence key="messaging-admin-runtime-f09" alt="분석 문서 final/document.md#a19-messaging-admin-runtime 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a19-messaging-admin-runtime 발췌 — 15줄" zoom="true" -::: - -## 제거 후보 - -## 확인하지 못한 것 - -세 의존을 제거하고 빌드를 돌려 보지 않았다. import 0 건으로 판정했으므로 리플렉션이나 문자열을 통한 간접 사용은 배제하지 못했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-claim-check-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-claim-check-f01.md deleted file mode 100644 index b77fdbd..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-claim-check-f01.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -kind: CASE -slug: messaging-claim-check-f01 -title: 배포 아티팩트가 싣지만 아무도 부르지 않고, 다른 곳의 에러 메시지가 이 경로를 권한다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-claim-check-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-claim-check-f01 - file: ../../../final/evidence/rendered/messaging-claim-check-f01.svg - - key: messaging-claim-check-f01-diagram - file: ../../../final/assets/diagrams/messaging-claim-check-f01.svg -evidence: - - ../../../final/evidence/raw/messaging-claim-check-f01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-claim-check#L507 이다. -module: messaging-claim-check -priority: P2 ---- - -# 배포 아티팩트가 싣지만 아무도 부르지 않고, 다른 곳의 에러 메시지가 이 경로를 권한다 - -여섯 타입 전부 leaf 밖 참조 0, ClaimCheckStore 구현이 테스트 fake뿐, 조립 0건. 그런데 runtime_memberships가 ["app-bootstrap"]이고 starter의 allowed_dependencies에 포함된다. - -## 관계 - -- **leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 튜닝 값이 두 계층에 있으면 어느 쪽이 이기는지 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 문제 - -여섯 타입 전부 leaf 밖 참조 0, ClaimCheckStore 구현이 테스트 fake뿐, 조립 0건. - -그런데 runtime_memberships가 ["app-bootstrap"]이고 starter의 allowed_dependencies에 포함된다. - -## 결론 - -그리고 messaging-policy의 PayloadLimitGuard가 상한 초과 payload를 거절하며 "payload of %d bytes exceeds the %d byte limit for %s; use claim check"라고 안내한다. - -운영자가 상한 초과 오류를 보고 안내대로 claim check를 켜려 해도 켤 것이 없다 — 저장소 구현도, bean도, 오프로드를 부르는 발행 경로도 없다. - -그리고 DestinationProfile이 claimCheckThresholdBytes를 선언하고 검증까지 하므로 설정 표면은 존재한다. - -설정할 수 있고 아무 효과가 없는 값이다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : ClaimCheckStore 참조 6건 검색과 runtime_memberships 값 및 다른 리프의 오류 메시지 안내 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-claim-check#L507 에 있다. - -## 본문 - - - -여섯 타입 전부 leaf 밖 참조 0, `ClaimCheckStore` 구현이 테스트 fake뿐, 조립 0건. 그런데 `runtime_memberships`가 `["app-bootstrap"]`이고 starter의 `allowed_dependencies`에 포함된다. - -## 안내가 가리키는 빈자리 - -:::evidence key="messaging-claim-check-f01-diagram" alt="설정 표면과 오류 메시지 안내가 운영자가 켤 수 있는 것 안에 놓이고 저장소 구현과 bean 이 바깥에 빗금으로 놓인다" caption="안내가 가리키는 빈자리" zoom="false" -::: - -## ClaimCheckStore 참조 위치 - -:::evidence key="messaging-claim-check-f01" alt="코드베이스에서 ClaimCheckStore 를 검색한 출력 6줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ClaimCheckStore 코드베이스 검색 — 6줄 · exit 0" zoom="true" -::: - -## 다른 곳의 에러 메시지가 이 경로를 권한다 - -`messaging-policy`의 `PayloadLimitGuard`가 상한 초과 payload를 거절하며 `"payload of %d bytes exceeds the %d byte limit for %s; use claim check"`라고 안내한다. 운영자가 그 안내대로 claim check를 켜려 해도 켤 것이 없다 — 저장소 구현도, bean도, 오프로드를 부르는 발행 경로도 없다. - -## 설정 표면은 존재한다 - -`DestinationProfile`이 `claimCheckThresholdBytes`를 선언하고 검증까지 한다. 설정할 수 있고 아무 효과가 없는 값이다. - -## 확인하지 못한 것 - -ClaimCheckStore 를 구현할 계획이 있는지 확인할 수 없었다. objectstorage 어댑터가 후보이지만 두 리프가 registry 에서 연결되지 않는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-cloudevents-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-cloudevents-f02.md deleted file mode 100644 index ccf1ea2..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-cloudevents-f02.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -kind: CASE -slug: messaging-cloudevents-f02 -title: 배포 아티팩트가 싣지만 아무도 부르지 않는다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-cloudevents-f02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-cloudevents-f02 - file: ../../../final/evidence/rendered/messaging-cloudevents-f02.svg - - key: messaging-cloudevents-f02-diagram - file: ../../../final/assets/diagrams/messaging-cloudevents-f02.svg -evidence: - - ../../../final/evidence/raw/messaging-cloudevents-f02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-cloudevents#L529 이다. -module: messaging-cloudevents -priority: P2 ---- - -# 배포 아티팩트가 싣지만 아무도 부르지 않는다 - -세 타입의 leaf 밖 참조가 0인데 runtime_memberships가 ["app-bootstrap"]이다. messaging-spring-boot-starter의 의존 목록에 있어 cloudevents-api·cloudevents-core 두 jar가 런타임 classpath에 오른다. - -## 관계 - -- **왕복이라 부르는 테스트는 무엇을 보존하지 않는지도 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **문서가 범위를 제한하면 그 제한을 누가 강제하는지 같이 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 문제 - -세 타입의 leaf 밖 참조가 0인데 runtime_memberships가 ["app-bootstrap"]이다. - -messaging-spring-boot-starter의 의존 목록에 있어 cloudevents-api·cloudevents-core 두 jar가 런타임 classpath에 오른다. - -## 결론 - -starter에 CloudEventMapper를 만드는 @Bean이 없다. - -형제 Avro·Protobuf는 소비자 0과 membership []이 일치하는 정합적 incubating 상태다. - -이 leaf만 어긋난다. - -오늘 실행되는 코드가 없으므로 사고는 아니지만, 아티팩트 크기와 "이 의존성이 왜 있지"의 조사 비용이 남는다. - -그리고 support-matrix.md:23이 "모든 messaging leaf가 unwired"라고 적고 있어 문서에서도 이 사실을 알 수 없다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : CloudEventMapper 참조 3건 검색과 배포 아티팩트에 실리는 jar·타입 목록 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-cloudevents#L529 에 있다. - -## 본문 - - - -세 타입의 leaf 밖 참조가 0인데 `runtime_memberships`가 `["app-bootstrap"]`이다. - -## 싣고 쓰지 않는 구조 - -:::evidence key="messaging-cloudevents-f02-diagram" alt="cloudevents 두 jar 와 leaf 세 타입이 배포 아티팩트 안에 놓이고 CloudEventMapper 빈이 바깥에 빗금으로 놓인다" caption="싣고 쓰지 않는 구조" zoom="false" -::: - -`messaging-spring-boot-starter`의 의존 목록에 있어 `cloudevents-api`·`cloudevents-core` 두 jar가 런타임 classpath에 오른다. starter에 `CloudEventMapper`를 만드는 `@Bean`이 없다. - -## CloudEventMapper 참조 위치 - -:::evidence key="messaging-cloudevents-f02" alt="코드베이스에서 CloudEventMapper 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="CloudEventMapper 코드베이스 검색 — 3줄 · exit 0" zoom="true" -::: - -## 형제 둘은 정합적이다 - -Avro·Protobuf는 소비자 0과 membership `[]`이 일치하는 incubating 상태다. 이 leaf만 어긋난다. - -## 오늘 사고는 아니고 조사 비용이 남는다 - -실행되는 코드가 없으므로 사고는 아니지만, 아티팩트 크기와 "이 의존성이 왜 있지"의 조사 비용이 남는다. 그리고 `support-matrix.md:23`이 "모든 messaging leaf가 unwired"라고 적고 있어 문서에서도 이 사실을 알 수 없다. - -## 확인하지 못한 것 - -이 리프가 starter 의존 목록에 들어간 시점과 이유를 확인할 수 없었다. 커밋이 대량 커밋 4개뿐이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-inbox-jdbc-postgresql-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-inbox-jdbc-postgresql-f01.md deleted file mode 100644 index d72ae10..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-inbox-jdbc-postgresql-f01.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -kind: CASE -slug: messaging-inbox-jdbc-postgresql-f01 -title: bounded purge가 구현돼 있고 호출되지 않아, cleanup이 스스로 막겠다고 한 장애를 일으킨다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-inbox-jdbc-postgresql-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-inbox-jdbc-postgresql-f01 - file: ../../../final/evidence/rendered/messaging-inbox-jdbc-postgresql-f01.svg - - key: messaging-inbox-jdbc-postgresql-f01-diagram - file: ../../../final/assets/diagrams/messaging-inbox-jdbc-postgresql-f01.svg -evidence: - - ../../../final/evidence/raw/messaging-inbox-jdbc-postgresql-f01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-inbox-jdbc-postgresql#L647 이다. -module: messaging-inbox-jdbc-postgresql -priority: P1 ---- - -# bounded purge가 구현돼 있고 호출되지 않아, cleanup이 스스로 막겠다고 한 장애를 일으킨다 - -InboxRepository·OutboxRepository 둘 다 purge*Before(Instant, int) 오버로드를 선언하고, JdbcInboxRepository:141·JdbcOutboxRepository:486이 LIMIT + FOR UPDATE SKIP LOCKED로 구현한다. 저장소 전체에서 그 시그니처가 등장하는 9곳은 선언 2 + 구현 2 + 테스트 fake override 5이고 호출 지점이 0이다. - -## 관계 - -- **컬럼 폭은 애플리케이션 검증과 짝을 이룬다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 안전 규칙은 한 공식과 한 강제 시점을 갖는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 문제 - -InboxRepository·OutboxRepository 둘 다 purge*Before(Instant, int) 오버로드를 선언하고, JdbcInboxRepository:141·JdbcOutboxRepository:486이 LIMIT + FOR UPDATE SKIP LOCKED로 구현한다. - -저장소 전체에서 그 시그니처가 등장하는 9곳은 선언 2 + 구현 2 + 테스트 fake override 5이고 호출 지점이 0이다. - -## 결론 - -InboxCleanupJob:56과 OutboxCleanupJob:50이 무제한 오버로드를 부른다. - -InboxCleanupJob.DEFAULT_BATCH_SIZE = 1_000은 자기 선언 한 줄만 존재한다. - -InboxCleanupJob의 javadoc이 스스로 적는다 — "A single unbounded DELETE over a table that has been accumulating for weeks holds locks long enough to block the very reservations the inbox exists to serve, so the cleanup would cause the outage it is meant to prevent." 실행되는 코드가 정확히 그 문장이 서술하는 동작이다. - -OutboxRepository의 bounded 오버로드 javadoc은 한 발 더 나간다 — "The cleanup jobs describe themselves as bounded by batch size; this is the parameter that makes that true." 그 파라미터를 아무도 넘기지 않는다. - -그리고 두 leaf가 동일한 형태로 그렇다. - -왜 P1인가. - -두 leaf 다 runtime_memberships: ["app-bootstrap"]이고 두 cleanup job이 starter에서 bean으로 만들어진다(MessagingReliabilityAutoConfiguration의 inboxCleanupJob·outboxCleanupJob). 즉 출하 구성에서 실행되는 경로이며, 백로그가 쌓인 뒤 첫 스윕에서 발현한다. 다른 미배선 발견들과 성격이 다르다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : InboxRepository 참조 20건 검색으로 bounded 시그니처가 등장하는 9곳을 선언·구현·호출로 분류 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-inbox-jdbc-postgresql#L647 에 있다. - -## 본문 - - - -`InboxRepository`·`OutboxRepository` 둘 다 `purge*Before(Instant, int)` 오버로드를 선언하고, `JdbcInboxRepository:141`·`JdbcOutboxRepository:486`이 `LIMIT` + `FOR UPDATE SKIP LOCKED`로 구현한다. 저장소 전체에서 그 시그니처가 등장하는 9곳은 **선언 2 + 구현 2 + 테스트 fake override 5**이고 **호출 지점이 0**이다. `InboxCleanupJob:56`과 `OutboxCleanupJob:50`이 무제한 오버로드를 부른다. - -## 정리가 일으키는 장애 - -:::evidence key="messaging-inbox-jdbc-postgresql-f01-diagram" alt="쌓인 백로그가 무제한 DELETE 와 락 장기 보유를 지나 예약이 막히는 결과로 이어진다" caption="정리가 일으키는 장애" zoom="false" -::: - -## InboxRepository 참조 위치 - -:::evidence key="messaging-inbox-jdbc-postgresql-f01" alt="코드베이스에서 InboxRepository 를 검색한 출력 20줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="InboxRepository 코드베이스 검색 — 20줄 · exit 0" zoom="true" -::: - -## javadoc 자신이 이 동작을 서술한다 - -`InboxCleanupJob`의 javadoc — "A single unbounded DELETE over a table that has been accumulating for weeks holds locks long enough to block the very reservations the inbox exists to serve, so the cleanup would cause the outage it is meant to prevent." 실행되는 코드가 정확히 그 문장이 서술하는 동작이다. `OutboxRepository`의 bounded 오버로드 javadoc은 한 발 더 나간다 — "The cleanup jobs describe themselves as bounded by batch size; **this is the parameter that makes that true**." 그 파라미터를 아무도 넘기지 않는다. `InboxCleanupJob.DEFAULT_BATCH_SIZE = 1_000`은 자기 선언 한 줄만 존재한다. - -## 왜 P1인가 - -두 leaf 다 `runtime_memberships: ["app-bootstrap"]`이고 두 cleanup job이 starter에서 bean으로 만들어진다(`MessagingReliabilityAutoConfiguration`의 `inboxCleanupJob`·`outboxCleanupJob`). 즉 **출하 구성에서 실행되는 경로**이며, 백로그가 쌓인 뒤 첫 스윕에서 발현한다. 다른 미배선 발견들과 성격이 다르다. - -## 후보 - -두 job이 bounded 오버로드에 배치 크기를 넘기게 한다 — `InboxCleanupJob`은 이미 `DEFAULT_BATCH_SIZE`를 갖고 있다. - -## 확인하지 못한 것 - -무제한 DELETE 가 실제 규모의 테이블에서 얼마나 오래 락을 잡는지 측정하지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-observability-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-observability-f02.md deleted file mode 100644 index be008bb..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-observability-f02.md +++ /dev/null @@ -1,84 +0,0 @@ ---- -kind: CASE -slug: messaging-observability-f02 -title: 관측 구현이 조립되지 않고, 그 재료 둘만 bean으로 존재한다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-observability-f02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-observability-f02 - file: ../../../final/evidence/rendered/messaging-observability-f02.svg - - key: messaging-observability-f02-diagram - file: ../../../final/assets/diagrams/messaging-observability-f02.svg -evidence: - - ../../../final/evidence/raw/messaging-observability-f02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-observability#L682 이다. -module: messaging-observability -priority: P2 ---- - -# 관측 구현이 조립되지 않고, 그 재료 둘만 bean으로 존재한다 - -MessagingMetrics는 MessagingObservation의 유일한 구현이고 저장소 전체에서 자기 테스트에서만 생성된다. starter는 그 생성자 인자 둘(MessagingRedactor:253, CardinalityGuard:264)을 bean으로 만들고 MessagingMetrics bean은 만들지 않는다. - -## 관계 - -- **타입이 문서화한 불변식은 타입이 강제한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 문제 - -MessagingMetrics는 MessagingObservation의 유일한 구현이고 저장소 전체에서 자기 테스트에서만 생성된다. - -starter는 그 생성자 인자 둘(MessagingRedactor:253, CardinalityGuard:264)을 bean으로 만들고 MessagingMetrics bean은 만들지 않는다. - -## 결론 - -DefaultMessagePublisher는 NO_OBSERVATION을 쓰는 6인자 생성자로 조립된다. - -재료·구현·seam·호출부가 전부 있고 조립 한 줄이 없다. - -그리고 두 재료 bean은 주입처가 0이므로 컨텍스트에 앉아만 있다 — bean 존재 검사는 통과한다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : MessagingMetrics 참조 16건 검색과 starter 가 만드는 bean 목록 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-observability#L682 에 있다. - -## 본문 - - - -`MessagingMetrics`는 `MessagingObservation`의 유일한 구현이고 저장소 전체에서 자기 테스트에서만 생성된다. - -## 재료와 조립의 거리 - -:::evidence key="messaging-observability-f02-diagram" alt="MessagingRedactor 와 CardinalityGuard 가 starter 가 만드는 bean 안에 놓이고 MessagingMetrics 가 바깥에 빗금으로 놓인다" caption="재료와 조립의 거리" zoom="false" -::: - -starter는 그 생성자 인자 둘(`MessagingRedactor:253`, `CardinalityGuard:264`)을 bean으로 만들고 `MessagingMetrics` bean은 만들지 않는다. - -## MessagingMetrics 참조 위치 - -:::evidence key="messaging-observability-f02" alt="코드베이스에서 MessagingMetrics 를 검색한 출력 16줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingMetrics 코드베이스 검색 — 16줄 · exit 0" zoom="true" -::: - -## 조립 한 줄이 없다 - -`DefaultMessagePublisher`는 `NO_OBSERVATION`을 쓰는 6인자 생성자로 조립된다. 재료·구현·seam·호출부가 전부 있다. 그리고 두 재료 bean은 주입처가 0이므로 컨텍스트에 앉아만 있다 — bean 존재 검사는 통과한다. - -## 확인하지 못한 것 - -이 bean 이 없는 것이 미완인지 확장점인지 확인하지 못했다. 저장소 안에 답이 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-outbox-jdbc-postgresql-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-outbox-jdbc-postgresql-f01.md deleted file mode 100644 index b288dea..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-outbox-jdbc-postgresql-f01.md +++ /dev/null @@ -1,95 +0,0 @@ ---- -kind: CASE -slug: messaging-outbox-jdbc-postgresql-f01 -title: 정리 작업이 무제한 DELETE 를 쏘고, 그것을 막는 오버로드는 호출되지 않는다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-outbox-jdbc-postgresql-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-outbox-jdbc-postgresql-f01 - file: ../../../final/evidence/rendered/messaging-outbox-jdbc-postgresql-f01.svg - - key: messaging-outbox-jdbc-postgresql-f01-diagram - file: ../../../final/assets/diagrams/messaging-outbox-jdbc-postgresql-f01.svg -evidence: - - ../../../final/evidence/raw/messaging-outbox-jdbc-postgresql-f01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-outbox-jdbc-postgresql#L876 이다. -module: messaging-outbox-jdbc-postgresql -priority: P1 ---- - -# 정리 작업이 무제한 DELETE 를 쏘고, 그것을 막는 오버로드는 호출되지 않는다 - -OutboxCleanupJob:50 과 InboxCleanupJob:56 이 무제한 오버로드를 부른다. bounded 오버로드(purgePublishedBefore(Instant, int) / purgeProcessedBefore(Instant, int))는 두 포트에 선언되고 두 구현에 구현되어 있으며 호출부가 0건이다(EVD-294, EVD-311). - -## 문제 - -OutboxCleanupJob:50 과 InboxCleanupJob:56 이 무제한 오버로드를 부른다. - -bounded 오버로드(purgePublishedBefore(Instant, int) / purgeProcessedBefore(Instant, int))는 두 포트에 선언되고 두 구현에 구현되어 있으며 호출부가 0건이다(EVD-294, EVD-311). - -## 결론 - -두 잡 모두 starter 빈이지만 스케줄러는 등록되지 않으며, 그것은 의도된 설계다(EVD-316). - -즉 기본 배포에서는 아무 일도 일어나지 않고, 애플리케이션이 문서 지시대로 잡을 스케줄하는 순간 무제한 DELETE 가 발동한다. - -잠재 결함이지 상시 결함이 아니다. - -bounded 구현의 주석이 결과를 명시한다: "An unbounded DELETE holds locks and writes WAL in proportion to the whole backlog, which stalls the relay and the business writes behind retention." 3일치 백로그가 쌓인 테이블에서 이것은 릴레이 정지와 비즈니스 쓰기 정체를 뜻한다. - -두 리프 모두 runtime_memberships: ["app-bootstrap"] 이고 두 잡 모두 starter 빈이다. - -수정은 한 줄이다 — purgePublishedBefore(cutoff, batchLimit). - -maxBatches 가 그제서야 의미를 갖는다. - -배치 크기는 새 파라미터가 필요하고, OutboxProperties.batchSize(100)를 재사용하거나 별도 값을 둔다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : OutboxProperties 참조 28건 검색과 두 오버로드의 SQL·호출자·starter 배선 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-outbox-jdbc-postgresql#L876 에 있다. - -## 본문 - - - -`OutboxCleanupJob:50` 과 `InboxCleanupJob:56` 이 무제한 오버로드를 부른다. bounded 오버로드(`purgePublishedBefore(Instant, int)` / `purgeProcessedBefore(Instant, int)`)는 두 포트에 선언되고 두 구현에 구현되어 있으며 호출부가 **0건**이다(`EVD-294`, `EVD-311`). - -## 두 형태의 락 구간 - -:::evidence key="messaging-outbox-jdbc-postgresql-f01-diagram" alt="호출되는 오버로드 쪽에 무제한 DELETE 와 전체 백로그 락이 빗금으로 놓이고 호출되지 않는 오버로드 쪽에 LIMIT 배치와 배치 단위 락이 놓인다" caption="두 형태의 락 구간" zoom="false" -::: - -## OutboxProperties 참조 위치 - -:::evidence key="messaging-outbox-jdbc-postgresql-f01" alt="코드베이스에서 OutboxProperties 를 검색한 출력 28줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="OutboxProperties 코드베이스 검색 — 28줄 · exit 0" zoom="true" -::: - -## 잠재 결함이지 상시 결함이 아니다 - -두 잡 모두 starter 빈이지만 스케줄러는 등록되지 않으며, 그것은 의도된 설계다(`EVD-316`). 즉 기본 배포에서는 아무 일도 일어나지 않고, 애플리케이션이 문서 지시대로 잡을 스케줄하는 순간 무제한 DELETE 가 발동한다. - -## bounded 구현의 주석이 결과를 명시한다 - -"An unbounded DELETE holds locks and writes WAL in proportion to the whole backlog, which stalls the relay and the business writes behind retention." 3일치 백로그가 쌓인 테이블에서 이것은 릴레이 정지와 비즈니스 쓰기 정체를 뜻한다. 두 리프 모두 `runtime_memberships: ["app-bootstrap"]` 이다. - -## 수정은 한 줄이고 대역도 함께 고쳐야 한다 - -`purgePublishedBefore(cutoff, batchLimit)` 로 바꾸면 `maxBatches` 가 그제서야 의미를 갖는다. 배치 크기는 `OutboxProperties.batchSize`(100)를 재사용하거나 별도 값을 둔다. 그리고 회귀 테스트가 성립하려면 `RecordingRepository` 를 고쳐야 한다 — 현재 대역의 bounded 구현은 `Math.min(unbounded(), limit)` 로 전부 지우고 숫자만 깎는다. - -## 확인하지 못한 것 - -백로그가 쌓인 실제 테이블에서 무제한 DELETE 의 락 보유 시간을 측정하지 않았다. 두 SQL 과 호출부 부재로 도출했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-outbox-jdbc-postgresql-f04.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-outbox-jdbc-postgresql-f04.md deleted file mode 100644 index e9843f1..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-outbox-jdbc-postgresql-f04.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -kind: CASE -slug: messaging-outbox-jdbc-postgresql-f04 -title: 두 릴레이 상호배제가 기동에서 강제되지 않는다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-outbox-jdbc-postgresql-f04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-outbox-jdbc-postgresql-f04 - file: ../../../final/evidence/rendered/messaging-outbox-jdbc-postgresql-f04.svg - - key: messaging-outbox-jdbc-postgresql-f04-diagram - file: ../../../final/assets/diagrams/messaging-outbox-jdbc-postgresql-f04.svg -evidence: - - ../../../final/evidence/raw/messaging-outbox-jdbc-postgresql-f04.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-outbox-jdbc-postgresql#L909 이다. -module: messaging-outbox-jdbc-postgresql -priority: P2 ---- - -# 두 릴레이 상호배제가 기동에서 강제되지 않는다 - -DebeziumOutboxProfile.requireExactlyOneRelay(...) 는 프로덕션 호출부가 0건이다. 클래스 javadoc 은 "the incompatibility is therefore enforced at startup instead of documented" 라고 쓴다. - -## 문제 - -DebeziumOutboxProfile.requireExactlyOneRelay(...) 는 프로덕션 호출부가 0건이다. - -클래스 javadoc 은 "the incompatibility is therefore enforced at startup instead of documented" 라고 쓴다. - -## 결론 - -properties 파일도 같은 경고를 반복한다("Enable this OR the in-process polling relay, never both"). - -같은 리프에 정확히 이 형태를 고친 선례가 있다 — OutboxRelayWorker 가 "nothing ever called runOnce" 를 고치고 MessagingOutboxRelayLifecycle 로 배선까지 마쳤다. - -같은 방식으로 MessagingReliabilityAutoConfiguration 에 프로필 빈과 InitializingBean 검사를 두면 된다. - -배선하려면 CDC 모드를 선택할 프로퍼티도 필요하다 — 지금은 DebeziumOutboxProfile 을 만드는 설정 경로 자체가 없다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : OutboxRelayWorker 참조 17건 검색과 상호배제 검사 메서드의 프로덕션 호출부 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-outbox-jdbc-postgresql#L909 에 있다. - -## 본문 - - - -`DebeziumOutboxProfile.requireExactlyOneRelay(...)` 는 프로덕션 호출부가 0건이다. 클래스 javadoc 은 "the incompatibility is therefore enforced at startup instead of documented" 라고 쓴다. - -## 배제가 강제되지 않는 자리 - -:::evidence key="messaging-outbox-jdbc-postgresql-f04-diagram" alt="문서의 경고만 기동에서 강제되는 것 안에 놓이고 requireExactlyOneRelay 호출이 바깥에 빗금으로 놓인다" caption="배제가 강제되지 않는 자리" zoom="false" -::: - -properties 파일도 같은 경고를 반복한다("Enable this OR the in-process polling relay, never both"). - -## OutboxRelayWorker 참조 위치 - -:::evidence key="messaging-outbox-jdbc-postgresql-f04" alt="코드베이스에서 OutboxRelayWorker 를 검색한 출력 17줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="OutboxRelayWorker 코드베이스 검색 — 17줄 · exit 0" zoom="true" -::: - -## 같은 리프에 선례가 있다 - -`OutboxRelayWorker` 가 "nothing ever called `runOnce`" 를 고치고 `MessagingOutboxRelayLifecycle` 로 배선까지 마쳤다. 같은 방식으로 `MessagingReliabilityAutoConfiguration` 에 프로필 빈과 `InitializingBean` 검사를 두면 된다. 배선하려면 CDC 모드를 선택할 프로퍼티도 필요하다 — 지금은 `DebeziumOutboxProfile` 을 만드는 설정 경로 자체가 없다. - -## 확인하지 못한 것 - -두 릴레이를 동시에 켠 배포를 만들어 관측하지 않았다. 검사 메서드의 호출부가 0 건이라는 것으로 도출했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-rabbit-f04.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-rabbit-f04.md deleted file mode 100644 index 77ee145..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-rabbit-f04.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -kind: CASE -slug: messaging-rabbit-f04 -title: 능력 상수의 delayedDelivery 가 무조건 참이고, 그 지연을 제공할 토폴로지는 조립되지 않는다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-rabbit-f04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-rabbit-f04 - file: ../../../final/evidence/rendered/messaging-rabbit-f04.svg -evidence: - - ../../../final/evidence/raw/messaging-rabbit-f04.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-rabbit#L307 이다. -module: messaging-rabbit -priority: P3 ---- - -# 능력 상수의 delayedDelivery 가 무조건 참이고, 그 지연을 제공할 토폴로지는 조립되지 않는다 - -그런데 지연을 실제로 만드는 것은 RabbitRetryQueueTopology 이고, 그 클래스는 자기 파일과 시험 하나 밖에서 참조되지 않는다. 어떤 production 코드도 그 큐를 선언하지 않는다. - -## 문제 - -그런데 지연을 실제로 만드는 것은 RabbitRetryQueueTopology 이고, 그 클래스는 자기 파일과 시험 하나 밖에서 참조되지 않는다. - -어떤 production 코드도 그 큐를 선언하지 않는다. - -## 결론 - -그리고 그 클래스의 javadoc 이 이 지연의 성질을 정확히 적는다. - -즉 제공되는 것은 "메시지별 지연" 이 아니라 "재시도 큐 하나당 TTL 하나" 다. - -능력 모델에는 그 구분을 표현하는 자리가 없고, 상수는 프로파일과 무관하게 참을 답한다. - -Kafka 는 같은 칸을 false 로 둔다. - -그래서 이 플래그의 두 값이 "지연 있음/없음" 이 아니라 "지연을 흉내낼 토폴로지를 선언할 수 있음/없음" 을 뜻하게 된다. - -수정은 능력을 전송 상수가 아니라 목적지의 재시도 큐 선언에서 파생시키는 것이다. - -이 리프가 조립되지 않는 동안에는 P3 이고, RabbitChannelPublisher 구현이 생기는 날 함께 봐야 한다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : RabbitRetryQueueTopology 참조 4건 검색으로 그 큐를 선언하는 production 코드 유무 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-rabbit#L307 에 있다. - -## 본문 - - - -능력 상수의 `delayedDelivery` 가 무조건 참이다. 그런데 지연을 실제로 만드는 것은 `RabbitRetryQueueTopology` 이고, 그 클래스는 자기 파일과 시험 하나 밖에서 참조되지 않는다. 어떤 production 코드도 그 큐를 선언하지 않는다. - -## RabbitRetryQueueTopology 참조 위치 - -:::evidence key="messaging-rabbit-f04" alt="코드베이스에서 RabbitRetryQueueTopology 를 검색한 출력 4줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RabbitRetryQueueTopology 코드베이스 검색 — 4줄 · exit 0" zoom="true" -::: - -## 제공되는 것은 메시지별 지연이 아니다 - -그 클래스의 javadoc 이 이 지연의 성질을 정확히 적는다 — "재시도 큐 하나당 TTL 하나" 다. 능력 모델에는 그 구분을 표현하는 자리가 없고, 상수는 프로파일과 무관하게 참을 답한다. Kafka 는 같은 칸을 `false` 로 둔다. - -## 플래그의 두 값이 다른 뜻이 된다 - -"지연 있음/없음" 이 아니라 "지연을 흉내낼 토폴로지를 선언할 수 있음/없음" 이다. 수정은 능력을 전송 상수가 아니라 목적지의 재시도 큐 선언에서 파생시키는 것이다. 이 리프가 조립되지 않는 동안에는 P3 이고, `RabbitChannelPublisher` 구현이 생기는 날 함께 봐야 한다. - -## 확인하지 못한 것 - -지연 재시도 큐 토폴로지를 실제로 선언해 보지 않았다. 참조가 자기 파일과 시험 하나뿐이라는 것으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-reliability-api-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-reliability-api-f02.md deleted file mode 100644 index 30354b9..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-reliability-api-f02.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -kind: CASE -slug: messaging-reliability-api-f02 -title: fencing token 경로가 실제 데이터베이스에 대해 실행되지 않는다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-reliability-api-f02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-reliability-api-f02 - file: ../../../final/evidence/rendered/messaging-reliability-api-f02.svg - - key: messaging-reliability-api-f02-diagram - file: ../../../final/assets/diagrams/messaging-reliability-api-f02.svg -evidence: - - ../../../final/evidence/raw/messaging-reliability-api-f02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-reliability-api#L698 이다. -module: messaging-reliability-api -priority: P2 ---- - -# fencing token 경로가 실제 데이터베이스에 대해 실행되지 않는다 - -OutboxRelay는 claimBatch/lease 기반 전이만 쓴다. OutboxPostgresIT는 leaseBatch/MessageId 기반 전이만 쓴다. - -## 관계 - -- **계약만 담는 leaf도 계약의 거절 조건은 자기 레인에서 검증한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **record의 `equals`를 좁히면 이유를 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **호출 컨텍스트가 계약이면 그 컨텍스트를 검증할 수단을 함께 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 문제 - -OutboxRelay는 claimBatch/lease 기반 전이만 쓴다. - -OutboxPostgresIT는 leaseBatch/MessageId 기반 전이만 쓴다. - -## 결론 - -신세대를 쓰는 다른 테스트는 InMemoryOutboxRepository와 RecordingRepository — SQL이 없는 fake다. - -fencing의 정확성은 구현의 조건부 UPDATE가 영향 행 수를 정확히 세는지에 달려 있다. - -OutboxTransitionResult.STALE_LEASE는 "its update matches zero rows"에서 나오고, 그것은 SQL의 성질이지 Java의 성질이 아니다. - -in-memory fake는 그 SQL을 실행하지 않는다. - -즉 이중 발행을 막는 장치가 그것을 검증할 수 있는 유일한 환경에서 실행되지 않는다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : OutboxRelay 참조 27건 검색과 릴레이·컨테이너 테스트가 각각 쓰는 전이 세대 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-reliability-api#L698 에 있다. - -## 본문 - - - -`OutboxRelay`는 `claimBatch`/lease 기반 전이만 쓴다. `OutboxPostgresIT`는 `leaseBatch`/`MessageId` 기반 전이만 쓴다. - -## 검증이 닿은 범위 - -:::evidence key="messaging-reliability-api-f02-diagram" alt="in-memory fake 만 펜싱 경로를 실행하는 것 안에 놓이고 실 데이터베이스 위의 펜싱 경로가 바깥에 빗금으로 놓인다" caption="검증이 닿은 범위" zoom="false" -::: - -신세대를 쓰는 다른 테스트는 `InMemoryOutboxRepository`와 `RecordingRepository` — SQL이 없는 fake다. - -## OutboxRelay 참조 위치 - -:::evidence key="messaging-reliability-api-f02" alt="코드베이스에서 OutboxRelay 를 검색한 출력 27줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="OutboxRelay 코드베이스 검색 — 27줄 · exit 0" zoom="true" -::: - -## fencing 의 정확성은 SQL 의 성질이다 - -구현의 조건부 UPDATE가 영향 행 수를 정확히 세는지에 달려 있다. `OutboxTransitionResult.STALE_LEASE`는 "its update matches zero rows"에서 나오고, in-memory fake는 그 SQL을 실행하지 않는다. 즉 **이중 발행을 막는 장치가 그것을 검증할 수 있는 유일한 환경에서 실행되지 않는다.** - -## 확인하지 못한 것 - -fencing token SQL 이 실제 PostgreSQL 에서 정확한지 확인하지 못했다. 그것을 검증할 레인이 다른 세대를 쓴다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-runtime-core-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-runtime-core-f01.md deleted file mode 100644 index 907be1c..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-runtime-core-f01.md +++ /dev/null @@ -1,104 +0,0 @@ ---- -kind: CASE -slug: messaging-runtime-core-f01 -title: 관측이 구현·호출부·주입 자리를 모두 갖추고도 출하에서 no-op이다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-runtime-core-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-runtime-core-f01 - file: ../../../final/evidence/rendered/messaging-runtime-core-f01.svg - - key: messaging-runtime-core-f01-diagram - file: ../../../final/assets/diagrams/messaging-runtime-core-f01.svg -evidence: - - ../../../final/evidence/raw/messaging-runtime-core-f01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-runtime-core#L710 이다. -module: messaging-runtime-core -priority: P2 ---- - -# 관측이 구현·호출부·주입 자리를 모두 갖추고도 출하에서 no-op이다 - -DefaultMessagePublisher가 모든 발행 결과를 observation.recordPublish(...)로 기록하고, 관측을 "constructor argument rather than an optional decorator"로 받는다. MessagingMetrics가 MessagingObservation을 구현한다. - -## 관계 - -- **만들어 두고 흘리지 않는 진단값은 진단이 아니다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **증가한다고 문서화한 값이 리터럴이면 그 사실을 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **안정 코드는 운영자의 행동이 갈리는 지점마다 나눈다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **`CompletionStage`를 반환하는 메서드는 동기적으로 던지지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 문제 - -DefaultMessagePublisher가 모든 발행 결과를 observation.recordPublish(...)로 기록하고, 관측을 "constructor argument rather than an optional decorator"로 받는다. - -MessagingMetrics가 MessagingObservation을 구현한다. - -## 결론 - -그런데 출하 조립(MessagingCoreAutoConfiguration:446)은 6인자 생성자를 써서 NO_OBSERVATION을 넣고, MessagingMetrics는 저장소 전체에서 자기 테스트에서만 생성된다. - -starter는 MessagingMetrics의 협력자 둘(MessagingRedactor:253, CardinalityGuard:264)을 bean으로 만든다. - -이 필드의 javadoc이 정확히 이 상황을 막으려고 쓰였다 — "an unobserved publish path is how 'the dashboards were empty during the incident' happens". - -그리고 같은 javadoc이 이전 결함을 "bean은 있고 호출 경로가 없었다"로 기록한다. - -지금은 반대다 — 호출 경로가 있고 bean이 없다. - -관측 결과는 같다. - -고침이 간극을 닫은 게 아니라 반대편으로 옮겼다. - -"decorator가 아니라 생성자 인자"라는 선택도 막지 못했는데, 인자를 기본값으로 채우는 짧은 생성자가 함께 있기 때문이다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : DefaultMessagePublisher 참조 22건 검색과 출하 조립이 고르는 생성자의 인자 수 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-runtime-core#L710 에 있다. - -## 본문 - - - -`DefaultMessagePublisher`가 모든 발행 결과를 `observation.recordPublish(...)`로 기록하고, 관측을 "constructor argument rather than an optional decorator"로 받는다. `MessagingMetrics`가 `MessagingObservation`을 구현한다. - -## 조립이 되돌린 것 - -:::evidence key="messaging-runtime-core-f01-diagram" alt="MessagingMetrics 구현과 recordPublish 호출부와 생성자 인자 자리가 갖춰진 것 안에 놓이고 출하 조립의 6인자 생성자가 바깥에 빗금으로 놓인다" caption="조립이 되돌린 것" zoom="false" -::: - -그런데 출하 조립(`MessagingCoreAutoConfiguration:446`)은 **6인자 생성자**를 써서 `NO_OBSERVATION`을 넣고, `MessagingMetrics`는 저장소 전체에서 자기 테스트에서만 생성된다. - -## DefaultMessagePublisher 참조 위치 - -:::evidence key="messaging-runtime-core-f01" alt="코드베이스에서 DefaultMessagePublisher 를 검색한 출력 22줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DefaultMessagePublisher 코드베이스 검색 — 22줄 · exit 0" zoom="true" -::: - -## 협력자 둘은 bean 으로 만들어진다 - -starter가 `MessagingRedactor:253`, `CardinalityGuard:264` 를 만든다. 이 필드의 javadoc이 정확히 이 상황을 막으려고 쓰였다 — "an unobserved publish path is how 'the dashboards were empty during the incident' happens". - -## 고침이 간극을 반대편으로 옮겼다 - -같은 javadoc이 이전 결함을 "bean은 있고 호출 경로가 없었다"로 기록한다. 지금은 반대다 — 호출 경로가 있고 bean이 없다. 관측 결과는 같다. "decorator가 아니라 생성자 인자"라는 선택도 막지 못했는데, 인자를 기본값으로 채우는 짧은 생성자가 함께 있기 때문이다. - -## 확인하지 못한 것 - -6인자 생성자 선택이 의도인지 확인할 수 없었다. 커밋이 대량 커밋 4개뿐이고 이 선택을 설명하는 기록이 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-runtime-core-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-runtime-core-f02.md deleted file mode 100644 index 3b77a6f..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-runtime-core-f02.md +++ /dev/null @@ -1,84 +0,0 @@ ---- -kind: CASE -slug: messaging-runtime-core-f02 -title: 소비 오케스트레이터가 조립되지 않는다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-runtime-core-f02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-runtime-core-f02 - file: ../../../final/evidence/rendered/messaging-runtime-core-f02.svg - - key: messaging-runtime-core-f02-diagram - file: ../../../final/assets/diagrams/messaging-runtime-core-f02.svg -evidence: - - ../../../final/evidence/raw/messaging-runtime-core-f02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-runtime-core#L719 이다. -module: messaging-runtime-core -priority: P2 ---- - -# 소비 오케스트레이터가 조립되지 않는다 - -DefaultDeliveryProcessor는 leaf 밖 참조 0, src/main 생성 0, src/test 생성 1이다. 이 클래스가 고친 문제("각 어댑터가 retry/dead-letter의 뜻을 각자 결정")가 배선 없이는 그대로 남는다. - -## 관계 - -- **만들어 두고 흘리지 않는 진단값은 진단이 아니다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **증가한다고 문서화한 값이 리터럴이면 그 사실을 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **안정 코드는 운영자의 행동이 갈리는 지점마다 나눈다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **`CompletionStage`를 반환하는 메서드는 동기적으로 던지지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 문제 - -DefaultDeliveryProcessor는 leaf 밖 참조 0, src/main 생성 0, src/test 생성 1이다. - -이 클래스가 고친 문제("각 어댑터가 retry/dead-letter의 뜻을 각자 결정")가 배선 없이는 그대로 남는다. - -## 결론 - -그리고 DeclaredDestinationAccess가 consume 권한을 빈 집합으로 두는 것과 정합적이다 — 기본 구성은 소비를 상정하지 않는다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : DefaultDeliveryProcessor 참조 7건 검색으로 leaf 밖 참조와 src/main 생성 수 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-runtime-core#L719 에 있다. - -## 본문 - - - -`DefaultDeliveryProcessor`는 leaf 밖 참조 0, `src/main` 생성 0, `src/test` 생성 1이다. - -## 경로가 열리지 않는 이유 - -:::evidence key="messaging-runtime-core-f02-diagram" alt="발행 경로만 조립되는 것 안에 놓이고 DefaultDeliveryProcessor 가 바깥에 빗금으로 놓인다" caption="경로가 열리지 않는 이유" zoom="false" -::: - -## DefaultDeliveryProcessor 참조 위치 - -:::evidence key="messaging-runtime-core-f02" alt="코드베이스에서 DefaultDeliveryProcessor 를 검색한 출력 7줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DefaultDeliveryProcessor 코드베이스 검색 — 7줄 · exit 0" zoom="true" -::: - -## 이 클래스가 고친 문제가 그대로 남는다 - -"각 어댑터가 retry/dead-letter의 뜻을 각자 결정" 이다. 그리고 `DeclaredDestinationAccess`가 consume 권한을 빈 집합으로 두는 것과 정합적이다 — 기본 구성은 소비를 상정하지 않는다. - -## 확인하지 못한 것 - -파생 프로젝트가 이 오케스트레이터를 직접 조립하는지 확인할 방법이 이 저장소 안에 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-schema-api-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-schema-api-f01.md deleted file mode 100644 index c4f500c..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-schema-api-f01.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -kind: CASE -slug: messaging-schema-api-f01 -title: 포맷 독립 진화 규칙이 호출되지 않고, 그것이 막으려던 중복이 실제로 생겼다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-schema-api-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-schema-api-f01 - file: ../../../final/evidence/rendered/messaging-schema-api-f01.svg - - key: messaging-schema-api-f01-diagram - file: ../../../final/assets/diagrams/messaging-schema-api-f01.svg -evidence: - - ../../../final/evidence/raw/messaging-schema-api-f01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-schema-api#L494 이다. -module: messaging-schema-api -priority: P2 ---- - -# 포맷 독립 진화 규칙이 호출되지 않고, 그것이 막으려던 중복이 실제로 생겼다 - -SchemaCompatibilityValidator의 저장소 전체 참조가 자기 선언과 자기 테스트뿐이다. 동시에 AvroCompatibilityGate가 isTransitive를 글자 그대로 복사했고 방향 판정 둘은 허용목록/거부목록으로 형태가 반대다. - -## 관계 - -- **port 계약은 동시성 요구를 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **도달성 판정은 단어가 아니라 import로 확인한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 문제 - -SchemaCompatibilityValidator의 저장소 전체 참조가 자기 선언과 자기 테스트뿐이다. - -동시에 AvroCompatibilityGate가 isTransitive를 글자 그대로 복사했고 방향 판정 둘은 허용목록/거부목록으로 형태가 반대다. - -## 결론 - -오늘은 7개 모드 전부에서 두 구현의 결과가 같다(NONE_EXPERIMENTAL은 gate의 early return이 가린다). - -그러나 enum에 값이 하나 추가되면 허용목록은 "검사 안 함", 거부목록은 "양방향 검사"로 반대 방향 기본값을 갖는다. - -그리고 requireProductionMode — 검사 없는 스키마가 보존 로그를 뒷받침하는 것을 막는 게이트 — 는 호출되는 곳이 없다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : SchemaCompatibilityValidator 참조 11건 검색과 Avro 게이트의 중복 구현 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-schema-api#L494 에 있다. - -## 본문 - - - -`SchemaCompatibilityValidator`의 저장소 전체 참조가 자기 선언과 자기 테스트뿐이다. - -## 규칙이 막지 못한 것 - -:::evidence key="messaging-schema-api-f01-diagram" alt="자기 테스트만 규칙을 부르는 것 안에 놓이고 production 호출자가 바깥에 빗금으로 놓인다" caption="규칙이 막지 못한 것" zoom="false" -::: - -동시에 `AvroCompatibilityGate`가 `isTransitive`를 글자 그대로 복사했고 방향 판정 둘은 허용목록/거부목록으로 형태가 반대다. - -## SchemaCompatibilityValidator 참조 위치 - -:::evidence key="messaging-schema-api-f01" alt="코드베이스에서 SchemaCompatibilityValidator 를 검색한 출력 11줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="SchemaCompatibilityValidator 코드베이스 검색 — 11줄 · exit 0" zoom="true" -::: - -## 오늘은 결과가 같다 - -7개 모드 전부에서 두 구현의 결과가 같다(`NONE_EXPERIMENTAL`은 gate의 early return이 가린다). 그러나 enum에 값이 하나 추가되면 허용목록은 "검사 안 함", 거부목록은 "양방향 검사"로 **반대 방향** 기본값을 갖는다. - -## 게이트 하나도 호출되지 않는다 - -`requireProductionMode` — 검사 없는 스키마가 보존 로그를 뒷받침하는 것을 막는 게이트 — 를 부르는 곳이 없다. - -## 확인하지 못한 것 - -port 구현의 스레드 안전성 요구를 관측할 대상이 없다. javadoc 에 없고 이 저장소에 production 구현이 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-security-f04.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-security-f04.md deleted file mode 100644 index 95db90d..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-security-f04.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -kind: CASE -slug: messaging-security-f04 -title: 종료 시 자격증명 소거가 호출되지 않는다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-security-f04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-security-f04 - file: ../../../final/evidence/rendered/messaging-security-f04.svg -evidence: - - ../../../final/evidence/raw/messaging-security-f04.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-security#L661 이다. -module: messaging-security -priority: P3 ---- - -# 종료 시 자격증명 소거가 호출되지 않는다 - -CredentialRuntimeRegistry.clearAll()의 javadoc이 "Clears every held credential, for shutdown"이라고 하고, 호출자가 저장소에 없다. 이 leaf 전체가 "비밀이 힙에 남지 않게 한다"를 목적으로 하고(char[], clear(), 회전 시 즉시 소거), 종료 경로에서 그 마지막 단계가 빠져 있다. - -## 관계 - -- **배선된 게이트는 자기 leaf 레인에서 검증한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 술어가 두 타입에 있으면 하나가 다른 하나를 부른다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **가변 필드로 상태 전이를 표현하면 가시성을 함께 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **맵 갱신 함수 안에서 I/O를 하면 그 지연이 락 범위가 된다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 문제 - -CredentialRuntimeRegistry.clearAll()의 javadoc이 "Clears every held credential, for shutdown"이라고 하고, 호출자가 저장소에 없다. - -이 leaf 전체가 "비밀이 힙에 남지 않게 한다"를 목적으로 하고(char[], clear(), 회전 시 즉시 소거), 종료 경로에서 그 마지막 단계가 빠져 있다. - -## 결론 - -프로세스가 끝나면 힙도 사라지지만, 종료가 느리거나 힙 덤프가 뜨는 경우가 정확히 이 통제가 노리는 상황이다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : clearAll 호출자 전수 검색과 종료 계약의 자격증명 소거 단계 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-security#L661 에 있다. - -## 본문 - - - -`CredentialRuntimeRegistry.clearAll()`의 javadoc이 "Clears every held credential, for shutdown"이라고 하고, 호출자가 저장소에 없다. - -## clearAll() 의 javadoc 과 호출자 수 - -:::evidence key="messaging-security-f04" alt="분석 문서 final/document.md#a19-messaging-security 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a19-messaging-security 발췌 — 15줄" zoom="true" -::: - -## 이 leaf 전체의 목적에서 마지막 단계가 빠졌다 - -"비밀이 힙에 남지 않게 한다"를 위해 `char[]`, `clear()`, 회전 시 즉시 소거를 두었는데 종료 경로가 비어 있다. - -## 프로세스가 끝나면 힙도 사라지지만 - -종료가 느리거나 힙 덤프가 뜨는 경우가 정확히 이 통제가 노리는 상황이다. - -## 확인하지 못한 것 - -힙 덤프로 자격증명 잔존을 확인하지 않았다. 호출자 부재로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-spring-boot-starter-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-spring-boot-starter-f01.md deleted file mode 100644 index 4a2d8f3..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-spring-boot-starter-f01.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -kind: CASE -slug: messaging-spring-boot-starter-f01 -title: 같은 자동 설정 안에서 검증기 하나만 감싸이지 않는다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-spring-boot-starter-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-spring-boot-starter-f01 - file: ../../../final/evidence/rendered/messaging-spring-boot-starter-f01.svg - - key: messaging-spring-boot-starter-f01-diagram - file: ../../../final/assets/diagrams/messaging-spring-boot-starter-f01.svg -evidence: - - ../../../final/evidence/raw/messaging-spring-boot-starter-f01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-spring-boot-starter#L281 이다. -module: messaging-spring-boot-starter -priority: P2 ---- - -# 같은 자동 설정 안에서 검증기 하나만 감싸이지 않는다 - -KafkaMessagingAutoConfiguration 은 검증기 셋을 만든다. KafkaTransactionProfileValidator 에는 대응하는 StartupProfileValidation 이 없다. - -## 관계 - -- **검증기는 발행이 아니라 주입이 강제다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 문제 - -KafkaMessagingAutoConfiguration 은 검증기 셋을 만든다. - -KafkaTransactionProfileValidator 에는 대응하는 StartupProfileValidation 이 없다. - -## 결론 - -즉 컨텍스트가 그 검증기를 발행하고 아무도 주입하지 않는다 — StartupProfileValidation 의 javadoc 이 서술한 이전 상태와 정확히 같은 형태다. - -RabbitMessagingAutoConfiguration 은 검증기 하나이고 그것을 감싼다. - -그러므로 이 가족에서 감싸이지 않은 검증기는 이 하나다. - -트랜잭션 프로파일 검증이 무엇을 막는지는 그 클래스가 안다 — 비트랜잭션 생산자 위의 정확히 한 번 주장 같은 조합이다. - -그 검증이 지금 돌지 않는다. - -수정은 한 블록이다. - -같은 파일의 kafkaProfileStartupValidation 형태를 복사해 세 번째 검증기를 감싼다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : KafkaMessagingAutoConfiguration 참조 6건 검색과 세 검증기의 감싸기 여부 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-spring-boot-starter#L281 에 있다. - -## 본문 - - - -`KafkaMessagingAutoConfiguration` 은 검증기 셋을 만든다. `KafkaTransactionProfileValidator` 에는 대응하는 `StartupProfileValidation` 이 없다. - -## 한 곳만 빠진 감싸기 - -:::evidence key="messaging-spring-boot-starter-f01-diagram" alt="KafkaProfileValidator 와 RabbitProfileValidator 가 감싸인 것 안에 놓이고 KafkaTransactionProfileValidator 가 바깥에 빗금으로 놓인다" caption="한 곳만 빠진 감싸기" zoom="false" -::: - -즉 컨텍스트가 그 검증기를 발행하고 아무도 주입하지 않는다 — `StartupProfileValidation` 의 javadoc 이 서술한 이전 상태와 정확히 같은 형태다. - -## KafkaMessagingAutoConfiguration 참조 위치 - -:::evidence key="messaging-spring-boot-starter-f01" alt="코드베이스에서 KafkaMessagingAutoConfiguration 를 검색한 출력 6줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="KafkaMessagingAutoConfiguration 코드베이스 검색 — 6줄 · exit 0" zoom="true" -::: - -## 이 가족에서 감싸이지 않은 검증기는 이 하나다 - -`RabbitMessagingAutoConfiguration` 은 검증기 하나이고 그것을 감싼다. 트랜잭션 프로파일 검증이 무엇을 막는지는 그 클래스가 안다 — 비트랜잭션 생산자 위의 정확히 한 번 주장 같은 조합이다. - -## 수정은 한 블록이다 - -같은 파일의 `kafkaProfileStartupValidation` 형태를 복사해 세 번째 검증기를 감싼다. - -## 확인하지 못한 것 - -TLS·SASL 을 요구하는 실제 브로커에 붙여 재현하지 않았다. 조립되는 설정 맵 성분과 보안 설정기가 만드는 성분의 교집합이 0 이라는 것으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-testkit-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-testkit-f01.md deleted file mode 100644 index 99c25a5..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-testkit-f01.md +++ /dev/null @@ -1,95 +0,0 @@ ---- -kind: CASE -slug: messaging-testkit-f01 -title: FaultController 의 5개 중 2개가 구현만 3벌 있고 호출부가 0건이다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-testkit-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-testkit-f01 - file: ../../../final/evidence/rendered/messaging-testkit-f01.svg - - key: messaging-testkit-f01-diagram - file: ../../../final/assets/diagrams/messaging-testkit-f01.svg -evidence: - - ../../../final/evidence/raw/messaging-testkit-f01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-testkit#L940 이다. -module: messaging-testkit -priority: P2 ---- - -# FaultController 의 5개 중 2개가 구현만 3벌 있고 호출부가 0건이다 - -rejectPublish() 와 reset() 은 인터페이스에 선언되어 세 하니스가 전부 구현하지만, 계약 스위트를 포함해 어디에서도 호출되지 않는다(EVD-299). rejectPublish 는 심지어 세 하니스의 publish() 경로에 완전히 배선되어 있다(KafkaContractHarness:119, RabbitContractHarness:85, InMemoryMessagingHarness:66) — 켜는 스위치만 아무도 누르지 않는다. - -## 문제 - -rejectPublish() 와 reset() 은 인터페이스에 선언되어 세 하니스가 전부 구현하지만, 계약 스위트를 포함해 어디에서도 호출되지 않는다(EVD-299). - -rejectPublish 는 심지어 세 하니스의 publish() 경로에 완전히 배선되어 있다(KafkaContractHarness:119, RabbitContractHarness:85, InMemoryMessagingHarness:66) — 켜는 스위치만 아무도 누르지 않는다. - -## 결론 - -이것이 단순한 미사용 코드가 아닌 이유: 미사용 경로가 틀린 값을 인코딩하고 있다. - -InMemoryMessagingHarness 에서 rejectPublish 는 rejected("BROKER_REJECTED", …) 를 돌려주고, 그 헬퍼는 PublishEvidence.notTransmitted() 를 쓴다(:184-193). - -TransmissionEvidence.NOT_TRANSMITTED 의 javadoc 은 "Nothing was written to the broker connection." 이다. - -그런데 FaultController.rejectPublish 의 javadoc 은 "refused outright by the broker" 다 — 브로커가 거절하려면 바이트가 나갔어야 하므로 TRANSMITTED 여야 한다. - -이 플랫폼은 전송 증거를 세 값으로 구분하는 것을 핵심 가치로 삼는데, 유일하게 실행되지 않는 경로에 그 구분의 오류가 들어 있다. - -connection-refused 시나리오(유일하게 증거가 없는 시나리오, Expectation.REJECTED)와 이 미사용 결함이 같은 빈칸을 가리킨다. - -둘 중 하나를 택해야 한다: 계약에 rejectsWhenBrokerRefusesBeforeTransmission 를 추가하고 전송 증거를 바로잡거나, rejectPublish 를 인터페이스에서 제거해 세 하니스의 구현 부담을 없애거나. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : FaultController 참조 19건 검색과 세 하니스의 구현·배선 지점 대비 호출부 수 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-testkit#L940 에 있다. - -## 본문 - - - -`rejectPublish()` 와 `reset()` 은 인터페이스에 선언되어 세 하니스가 전부 구현하지만, 계약 스위트를 포함해 어디에서도 호출되지 않는다(`EVD-299`). - -## 구현만 있는 고장 종류 - -:::evidence key="messaging-testkit-f01-diagram" alt="세 하니스의 구현과 publish 경로 배선이 고장 종류에 있는 것 안에 놓이고 호출부가 바깥에 빗금으로 놓인다" caption="구현만 있는 고장 종류" zoom="false" -::: - -`rejectPublish` 는 심지어 세 하니스의 `publish()` 경로에 완전히 배선되어 있다(`KafkaContractHarness:119`, `RabbitContractHarness:85`, `InMemoryMessagingHarness:66`) — 켜는 스위치만 아무도 누르지 않는다. - -## FaultController 참조 위치 - -:::evidence key="messaging-testkit-f01" alt="코드베이스에서 FaultController 를 검색한 출력 19줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="FaultController 코드베이스 검색 — 19줄 · exit 0" zoom="true" -::: - -## 미사용 경로가 틀린 값을 인코딩하고 있다 - -`InMemoryMessagingHarness` 에서 `rejectPublish` 는 `rejected("BROKER_REJECTED", …)` 를 돌려주고, 그 헬퍼는 `PublishEvidence.notTransmitted()` 를 쓴다(`:184-193`). `TransmissionEvidence.NOT_TRANSMITTED` 의 javadoc 은 "Nothing was written to the broker connection." 인데, `FaultController.rejectPublish` 의 javadoc 은 "refused outright by **the broker**" 다 — 브로커가 거절하려면 바이트가 나갔어야 하므로 `TRANSMITTED` 여야 한다. - -## 유일하게 실행되지 않는 경로에 그 구분의 오류가 있다 - -이 플랫폼은 전송 증거를 세 값으로 구분하는 것을 핵심 가치로 삼는다. `connection-refused` 시나리오(유일하게 증거가 없는 시나리오, `Expectation.REJECTED`)와 이 미사용 결함이 같은 빈칸을 가리킨다. 둘 중 하나를 택해야 한다 — 계약에 `rejectsWhenBrokerRefusesBeforeTransmission` 를 추가하고 전송 증거를 바로잡거나, `rejectPublish` 를 인터페이스에서 제거해 세 하니스의 구현 부담을 없애거나. - -## reset 은 별개다 - -세 구현 모두 결함 플래그를 one-shot 으로 소비하므로(`consumeXxx` 가 읽고 즉시 false) 리셋이 필요 없는 구조다. 계약이 테스트마다 새 하니스를 만드는 것도 같은 이유다. 제거 후보다. - -## 확인하지 못한 것 - -두 고장을 실제로 주입해 계약 스위트가 어떻게 반응하는지 관측하지 않았다. 호출부가 0 건이라는 것으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-testkit-f04.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-testkit-f04.md deleted file mode 100644 index 89d7c60..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-testkit-f04.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -kind: CASE -slug: messaging-testkit-f04 -title: 1 MiB 한도가 PayloadPolicy 를 두고 리터럴로 재선언된다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-testkit-f04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-testkit-f04 - file: ../../../final/evidence/rendered/messaging-testkit-f04.svg -evidence: - - ../../../final/evidence/raw/messaging-testkit-f04.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-testkit#L966 이다. -module: messaging-testkit -priority: P3 ---- - -# 1 MiB 한도가 PayloadPolicy 를 두고 리터럴로 재선언된다 - -PayloadPolicy.DEFAULT_MAX_BYTES = 1_048_576 이 정본인데 같은 값이 최소 8곳에 다시 있고(§12.3(b)), 그중 둘이 이 리프다(InMemoryMessagingHarness:31, ContractMessage:50). messaging-testkit 은 api project(':messaging:messaging-policy') 를 이미 선언하고 있으므로 import 한 줄이면 된다. - -## 문제 - -PayloadPolicy.DEFAULT_MAX_BYTES = 1_048_576 이 정본인데 같은 값이 최소 8곳에 다시 있고(§12.3(b)), 그중 둘이 이 리프다(InMemoryMessagingHarness:31, ContractMessage:50). - -messaging-testkit 은 api project(':messaging:messaging-policy') 를 이미 선언하고 있으므로 import 한 줄이면 된다. - -## 결론 - -지금은 messaging-policy 의존이 import 0건이라 선언만 남아 있는데(§12.4(b)), 이 자리가 그 의존이 실제로 쓰여야 할 곳이다. - -ContractMessage.oversized() 의 1_048_577 은 PayloadPolicy.DEFAULT_MAX_BYTES + 1 로 쓰면 "한도 바로 위 한 바이트" 라는 의도가 코드에 드러난다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : PayloadPolicy 참조 25건 검색과 같은 값이 리터럴로 재선언된 지점 집계 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-testkit#L966 에 있다. - -## 본문 - - - -`PayloadPolicy.DEFAULT_MAX_BYTES = 1_048_576` 이 정본인데 같은 값이 최소 8곳에 다시 있고(§12.3(b)), 그중 둘이 이 리프다(`InMemoryMessagingHarness:31`, `ContractMessage:50`). - -## PayloadPolicy 참조 위치 - -:::evidence key="messaging-testkit-f04" alt="코드베이스에서 PayloadPolicy 를 검색한 출력 25줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="PayloadPolicy 코드베이스 검색 — 25줄 · exit 0" zoom="true" -::: - -## import 한 줄이면 된다 - -`messaging-testkit` 은 `api project(':messaging:messaging-policy')` 를 이미 선언하고 있다. 지금은 그 의존이 import 0건이라 선언만 남아 있는데(§12.4(b)), 이 자리가 그 의존이 실제로 쓰여야 할 곳이다. - -## 의도가 코드에 드러나게 하는 방법 - -`ContractMessage.oversized()` 의 `1_048_577` 은 `PayloadPolicy.DEFAULT_MAX_BYTES + 1` 로 쓰면 "한도 바로 위 한 바이트" 가 읽힌다. - -## 확인하지 못한 것 - -리터럴을 정본 상수로 바꿔 빌드를 돌려 보지 않았다. 값이 같다는 것과 의존 선언이 이미 존재한다는 것으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-testkit-f05.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-testkit-f05.md deleted file mode 100644 index b062a78..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-testkit-f05.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -kind: CASE -slug: messaging-testkit-f05 -title: messaging-transport-spi 의존이 import 0건이다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-testkit-f05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-testkit-f05 - file: ../../../final/evidence/rendered/messaging-testkit-f05.svg -evidence: - - ../../../final/evidence/raw/messaging-testkit-f05.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-testkit#L972 이다. -module: messaging-testkit -priority: P3 ---- - -# messaging-transport-spi 의존이 import 0건이다 - -policy 와 달리 transport-spi 는 쓸 자리가 보이지 않는다. 제거 후보. - -## 문제 - -policy 와 달리 transport-spi 는 쓸 자리가 보이지 않는다. - -제거 후보. - -## 결론 - -policy 와 달리 transport-spi 는 쓸 자리가 보이지 않는다. - -제거 후보. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : 선언된 의존에 대한 패키지 이름 import 전수 검색 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-testkit#L972 에 있다. - -## 본문 - - - -`messaging-transport-spi` 의존이 import 0건이다. - -## import 0 건인 의존 - -:::evidence key="messaging-testkit-f05" alt="분석 문서 final/document.md#a19-messaging-testkit 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a19-messaging-testkit 발췌 — 15줄" zoom="true" -::: - -## policy 와 다르다 - -policy 쪽은 쓸 자리가 분명한데(§P3의 1 MiB 한도) transport-spi 는 쓸 자리가 보이지 않는다. 제거 후보. - -## 확인하지 못한 것 - -의존을 제거하고 빌드를 돌려 보지 않았다. import 0 건으로 판정했으므로 간접 사용은 배제하지 못했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-testkit-f06.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-testkit-f06.md deleted file mode 100644 index 6269e01..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-messaging-testkit-f06.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -kind: CASE -slug: messaging-testkit-f06 -title: BrokerFailureMatrix.adapters() 는 호출부가 0건이다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-testkit-f06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-testkit-f06 - file: ../../../final/evidence/rendered/messaging-testkit-f06.svg -evidence: - - ../../../final/evidence/raw/messaging-testkit-f06.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-testkit#L976 이다. -module: messaging-testkit -priority: P3 ---- - -# BrokerFailureMatrix.adapters() 는 호출부가 0건이다 - -public 메서드이나 아무도 쓰지 않는다. 이 리프의 다른 public 표면은 전부 소비자가 있다. - -## 문제 - -public 메서드이나 아무도 쓰지 않는다. - -이 리프의 다른 public 표면은 전부 소비자가 있다. - -## 결론 - -제거하거나, 진단용이라면 그렇게 적는다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : adapters() 호출부 검색과 이 리프의 다른 public 표면의 소비자 유무 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-testkit#L976 에 있다. - -## 본문 - - - -`BrokerFailureMatrix.adapters()` 는 public 메서드이나 아무도 쓰지 않는다. - -## adapters() 의 호출부 - -:::evidence key="messaging-testkit-f06" alt="분석 문서 final/document.md#a19-messaging-testkit 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a19-messaging-testkit 발췌 — 15줄" zoom="true" -::: - -## 이 리프의 다른 public 표면은 전부 소비자가 있다 - -제거하거나, 진단용이라면 그렇게 적는다. - -## 확인하지 못한 것 - -리플렉션이나 서비스 로더로 부르는 형태는 배제하지 못했다. 이름 기반 검색으로만 확인했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-startup-validator-is-the-only-reader-of-four-keys.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-startup-validator-is-the-only-reader-of-four-keys.md deleted file mode 100644 index 015b804..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-startup-validator-is-the-only-reader-of-four-keys.md +++ /dev/null @@ -1,103 +0,0 @@ ---- -kind: CASE -slug: startup-validator-is-the-only-reader-of-four-keys -title: 시작 검증기가 유일한 소비자인 설정 키 넷이 아무것도 게이트하지 않는다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:startup-validator-is-the-only-reader-of-four-keys -evidenceCapturedOn: 2026-09-01 -body: case-startup-validator-is-the-only-reader-of-four-keys.body.md -assets: - - key: startup-validator-is-the-only-reader-of-four-keys - file: ../../../final/evidence/rendered/startup-validator-is-the-only-reader-of-four-keys.svg -evidence: - - ../../../final/evidence/raw/startup-validator-is-the-only-reader-of-four-keys.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-spring-boot-starter#L139 이다. ---- - -# 시작 검증기가 유일한 소비자인 설정 키 넷이 아무것도 게이트하지 않는다 - -검증기를 이름으로 부르는 파일은 자기 자신과 자기 테스트뿐이다. 자동 설정은 빈 아홉을 만들고 검증을 부르지 않는다. 그리고 그 검증기가 유일한 소비자인 설정 키가 넷이다. - -## 관계 - -- **시작 검증기가 시작 시 실행되지 않는다** - 다른 가족의 같은 형태다. -- **검증기는 발행이 아니라 주입이 강제다** - 이 사례에서 뽑은 규칙이다. -- **같은 자동 설정 안에서 검증기 하나만 감싸이지 않아 트랜잭션 조건이 검사되지 않는다** - 같은 통독에서 나온 짝이다. - -## 문제 - -시작 검증기의 자바독이 선정 기준을 적는다. - -여기 담긴 모든 규칙은 실행 시 증상이 침묵이거나 오귀인인 실수라는 것이다. 마감 없는 단항 메서드는 클라이언트 자신의 마감까지 매달리고, 무제한 실행기는 과부하를 무제한 지연으로 바꾸고, 운영의 전체 신뢰는 전송 보안을 보고하면서 제공하지 않고, 운영의 반사 공개는 스키마를 게시하고, 원장 없는 키 필수 메서드는 지킬 수 없는 멱등 키를 받아들인다는 것이다. - -그리고 어느 것도 연기 테스트를 실패시키지 않는다는 것이다. - -## 결론 - -그 검증이 돌지 않는다. - -검증기를 이름으로 부르는 파일이 둘뿐이다. 자기 자신과 자기 테스트다. - -자동 설정은 실행기 프로파일과 서버 프로파일과 승인 제어기와 상태 레지스트리와 반사 정책과 관리 노출 정책과 배수 정책과 문맥 결속기와 오류 사상기, 아홉 빈을 만든다. 검증 호출이 없고 초기화 콜백도 없다. - -그래서 클래스 자바독이 약속한 성질이 성립하지 않는다. 아무도 눈치채지 못할 방식으로 틀린 설정에서 시작을 거부한다는 것이다. 지금은 그냥 시작한다. - -함께 사라지는 것이 있다. - -검증기가 유일한 소비자인 설정 키가 넷이다. 전송과 TLS 사용 여부와 전체 신뢰와 원장 활성화다. - -전송 키는 자동 설정이 아예 보지 않는다. 서버 프로파일 팩토리가 안정 전송을 하드코딩한다. - -TLS 두 키는 배포 환경의 바닥을 강제할 곳이 없다. - -원장 키는 멱등 키 필수 메서드가 원장 없이 열리는 것을 막을 곳이 없다. - -같은 저장소가 정본을 둘 갖고 있다. 메시징 가족은 시작 프로파일 검증을 초기화 콜백으로 돌려 이 문제를 이미 한 번 해결했고, 파일 서버 하위 트리는 증명 메서드를 부트스트랩의 빈으로 연결했다. - -## 검증 환경 - -Spring Boot : 4.0.8 -확인 방식 : 호출자 전수 검색과 설정 키별 소비자 계수 -소스 수정 : x - -## 재현 조건 - -원문은 document-detail 의 final/document.md#a20-grpc-spring-boot-starter 에 있다. - -1. 검증기 클래스 이름을 저장소 전체에서 검색한다. -2. 자동 설정의 빈 목록을 읽고 검증 호출이 있는지 본다. -3. 설정 속성의 각 접근자를 저장소 전체에서 검색한다. -4. 검증기 밖에 소비자가 없는 키를 가려낸다. -5. 서버 프로파일 팩토리가 전송 키를 읽는지 확인한다. - -## 본문 - - - -검증기를 이름으로 부르는 파일은 자기 자신과 자기 테스트뿐이다. 자동 설정은 빈 아홉 개를 만들고 `requireValid` 를 부르지 않으며 초기화 콜백도 없다. - -## StartupProfileValidation 참조 위치 - -:::evidence key="startup-validator-is-the-only-reader-of-four-keys" alt="코드베이스에서 StartupProfileValidation 를 검색한 출력 4줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="StartupProfileValidation 코드베이스 검색 — 4줄 · exit 0" zoom="true" -::: - -## 그 검증기가 유일한 소비자인 키 넷 - -`transport`·`tls-enabled`·`trust-all-certificates`·`operation-ledger-enabled`. 따라서 운영 환경의 TLS 바닥도, 비운영 전송 거부도, 멱등 키 필수 메서드의 원장 요구도 강제되지 않는다. - -## 같은 저장소가 정본을 둘 갖고 있다 - -messaging 의 `StartupProfileValidation` 과 fileserver 의 증명 호출이다. - -## 확인하지 못한 것 - -스타터를 애플리케이션에 올려 컨텍스트를 세우지 않았다. 빌드 전용 리프라 그 배포가 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-validator-declared-and-never-injected.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-validator-declared-and-never-injected.md deleted file mode 100644 index a7166df..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/case/case-validator-declared-and-never-injected.md +++ /dev/null @@ -1,101 +0,0 @@ ---- -kind: CASE -slug: validator-declared-and-never-injected -title: 같은 자동 설정 안에서 검증기 하나만 감싸이지 않아 트랜잭션 조건이 검사되지 않는다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:validator-declared-and-never-injected -evidenceCapturedOn: 2026-09-01 -body: case-validator-declared-and-never-injected.body.md -assets: - - key: validator-declared-and-never-injected - file: ../../../final/evidence/rendered/validator-declared-and-never-injected.svg -evidence: - - ../../../final/evidence/raw/validator-declared-and-never-injected.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-spring-boot-starter#L148 이다. ---- - -# 같은 자동 설정 안에서 검증기 하나만 감싸이지 않아 트랜잭션 조건이 검사되지 않는다 - -한 자동 설정이 검증기 둘을 만든다. 하나는 시작 검증 도우미로 감싸여 컨텍스트 구성 중에 돌고, 다른 하나는 빈으로 발행만 된다. 그 도우미의 자바독이 서술한 이전 결함이 정확히 그 형태다. - -## 관계 - -- **시작 검증기가 유일한 소비자인 설정 키 넷이 아무것도 게이트하지 않는다** - 같은 통독에서 나온 짝이다. -- **검증기는 발행이 아니라 주입이 강제다** - 이 사례에서 뽑은 규칙이다. -- **능력 상수가 프로파일 조건보다 오래 살아서 중복 제거 없는 목적지가 가드를 통과한다** - 같은 어댑터 계열의 짝이 되는 사례다. - -## 문제 - -시작 검증 도우미가 이 가족에서 이미 한 번 고쳐진 결함을 자바독에 기록한다. - -브로커 두 종과 보안 검증기가 전부 빈이었는데 아무 데도 주입되지 않았다는 것이다. 컨텍스트가 브로커마다 검증기를 발행하고 아무것도 검증하지 않았다는 것이다. - -그 수정이 적용된 뒤의 상태를 확인했다. - -## 결론 - -같은 자동 설정 안에서 하나가 빠져 있다. - -브로커 프로파일 검증기는 도우미로 감싸여 초기화 콜백에서 돈다. - -같은 파일의 트랜잭션 프로파일 검증기는 빈으로 발행만 된다. 대응하는 도우미 선언이 없다. - -다른 브로커의 자동 설정은 검증기가 하나뿐이고 그것을 감싼다. 그러므로 이 가족에서 감싸이지 않은 검증기는 이 하나다. - -그 검증기가 무엇을 막는지는 자기 자바독이 적는다. - -트랜잭션 식별자 접두와 멱등 생산자와 모든 복제 확인과 수동 오프셋 커밋을 요구하고, 마지막 규칙이 핵심이다. 부수효과가 데이터베이스에 사는 목적지가 브로커 트랜잭션을 함께 선언하면, 팀이 트랜잭션이라는 낱말을 두 번 읽고 전체 경로가 원자적이라고 결론짓는다는 것이다. 두 절반은 여전히 갈라질 수 있다. - -그 검증이 지금 돌지 않는다. - -그리고 같은 어댑터의 능력 선언은 브로커 트랜잭션을 무조건 참으로 답한다. 두 겹이 함께 비어 있다. - -여기서 리프 경계를 넘어야만 보이는 것이 하나 있다. 검증기는 어댑터 리프가 소유하고, 그것을 시작 시 부르는 배선은 스타터가 소유한다. 어느 쪽 문서도 혼자서는 이 검증이 실행되지 않는다는 것을 말할 수 없다. - -## 검증 환경 - -Spring Boot : 4.0.8 -확인 방식 : 자동 설정의 빈 선언 대조와 형제 자동 설정 비교 -소스 수정 : x - -## 재현 조건 - -원문은 document-detail 의 final/document.md#a19-messaging-spring-boot-starter 에 있다. - -1. 시작 검증 도우미의 자바독을 읽는다. -2. 브로커 자동 설정의 빈 선언을 순서대로 읽는다. -3. 각 검증기에 대응하는 도우미 선언이 있는지 확인한다. -4. 다른 브로커의 자동 설정과 비교한다. -5. 감싸이지 않은 검증기가 무엇을 요구하는지 읽는다. - -## 본문 - - - -`KafkaProfileValidator` 는 `StartupProfileValidation` 으로 감싸여 `afterPropertiesSet` 에서 돌고, 같은 파일의 `KafkaTransactionProfileValidator` 는 빈으로 발행만 된다. Rabbit 쪽은 하나뿐인 검증기를 감싼다. - -## KafkaProfileValidator 참조 위치 - -:::evidence key="validator-declared-and-never-injected" alt="코드베이스에서 KafkaProfileValidator 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="KafkaProfileValidator 코드베이스 검색 — 3줄 · exit 0" zoom="true" -::: - -## 검사되지 않는 네 조건 - -트랜잭션 식별자 접두·멱등 생산자·`acks=all`·수동 커밋 요구가 시작 시 검사되지 않고, 어댑터는 `brokerTransaction=true` 를 무조건 답한다. - -## 어느 문서도 혼자서는 이 사실을 말할 수 없다 - -검증기의 절반은 `messaging-kafka` 가, 배선의 절반은 스타터가 소유한다. - -## 확인하지 못한 것 - -조건을 어긴 프로파일로 컨텍스트를 세워 검증이 돌지 않는 것을 재현하지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/question/openquestion-messaging-core-api-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/question/openquestion-messaging-core-api-f01.md deleted file mode 100644 index 1119b8a..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/question/openquestion-messaging-core-api-f01.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -kind: QUESTION -slug: messaging-core-api-f01 -title: 선언된 핸들러 계약이 배선된 것과 다르다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: open-question:messaging-core-api-f01 -questionStatus: OPEN -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-core-api#L835 ---- - -# 선언된 핸들러 계약이 배선된 것과 다르다 - -공개 API 가 먼저 보여 주는 핸들러 인터페이스를 구현해도 아무 데도 꽂히지 않는다. 실제 배선은 다른 타입을 받는다. - -## 사실 - -MessageHandler(delivery/MessageHandler.java:14)의 저장소 전체 참조가 0 이다. git grep -n -w MessageHandler -- src ':!src/messaging/messaging-core-api' 가 exit 1 이다. - -핸들러 결과를 정산으로 바꾸는 유일한 지점 DefaultDeliveryProcessor 는 Function, HandleResult> 를 받는다(:40,47). - -MessageDelivery 가 빠지면서 deliveryAttempt · redelivered · handlerDeadline · shutdownRequested 가 핸들러에 도달할 수 없다. DeliveryContext javadoc 이 설명하는 graceful drain 협력은 현재 배선으로 성립하지 않는다. - -## 미지수 - -저장소 밖에 이 계약의 소비자가 있는가. 세 선택지 모두 그 답에 걸린다. - -## 선택지 - -DefaultDeliveryProcessor 시그니처를 계약에 맞춘다 - MessageDelivery 를 조립해야 하므로 DeliveryContext 생성 책임이 runtime 으로 간다. - -두 타입을 이 leaf 에서 제거한다 - 실제 계약만 남고 공개 API 가 배선과 일치한다. - -확장점임을 명시한다 - 파생 프로젝트가 구현하는 자리라면 javadoc 과 support-matrix.md 에 그 사실을 적는다. - -## 다음 검증 - -git grep -n -w MessageHandler 와 DefaultDeliveryProcessor.java:40,47 로 현재 배선은 확정된다. 저장소 밖 소비자의 존재는 이 저장소 안에서 확인할 수 없다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/question/openquestion-messaging-core-api-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/question/openquestion-messaging-core-api-f03.md deleted file mode 100644 index 66b2b4e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/question/openquestion-messaging-core-api-f03.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -kind: QUESTION -slug: messaging-core-api-f03 -title: 12개 예외가 선언만 되어 있다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: open-question:messaging-core-api-f03 -questionStatus: OPEN -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-core-api#L862 ---- - -# 12개 예외가 선언만 되어 있다 - -23개 구체 예외 중 12개가 leaf 밖에서 한 번도 참조되지 않는다. 깨지는 것은 없고, 대신 정리 비용이 계속 커진다. - -## 사실 - -23개 구체 예외 중 12개가 leaf 밖 참조 0 이다(§6.2 표). 원문 근거는 evidence/raw/269 §B 이다. - -MessagePublishAmbiguousException 처럼 설계의 중심 개념에 이름을 준 타입이 던져지지 않으면, 그 개념이 실제로 어떤 경로로 표현되는지(결과 record)를 읽는 사람이 스스로 알아내야 한다. - -src/messaging/CLAUDE.md:44 가 "새 public 타입은 그 모듈의 계약이다. 삭제·시그니처 변경은 breaking change 로 취급한다" 라고 적는다. 그래서 나중에 정리하는 비용이 시간에 비례해 커진다. - -## 미지수 - -저장소 밖 소비자가 있는가. 같은 leaf 의 핸들러 계약 질문과 같은 미지수를 공유한다. - -## 선택지 - -어댑터가 결과 record 대신 예외를 던질 지점을 정한다 - 선언과 실행이 만난다. - -미사용 예외를 제거한다 - CLAUDE.md:44 의 breaking change 규칙을 지금 한 번 치른다. - -파생 프로젝트용 어휘임을 명시한다 - 제거하지 않는 이유가 코드 옆에 남는다. - -## 다음 검증 - -evidence/raw/269 §B 재실행으로 참조 수는 다시 셀 수 있다. 저장소 밖 소비자 여부는 그 재실행으로 답해지지 않는다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/question/openquestion-messaging-kafka-share-experimental-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/question/openquestion-messaging-kafka-share-experimental-f03.md deleted file mode 100644 index 14c046d..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/question/openquestion-messaging-kafka-share-experimental-f03.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -kind: QUESTION -slug: messaging-kafka-share-experimental-f03 -title: 형제 어댑터 넷이 구현하는 SPI를 이 leaf만 구현하지 않는다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: open-question:messaging-kafka-share-experimental-f03 -questionStatus: OPEN -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-kafka-share-experimental#L485 ---- - -# 형제 어댑터 넷이 구현하는 SPI를 이 leaf만 구현하지 않는다 - -capability 선언은 있는데 그것을 읽어 갈 통로가 없다. 이 leaf 만 `MessagingTransport` 를 구현하지 않기 때문이다. - -## 관계 - -- **허용 의존 목록은 상한이므로 미사용을 잡지 않는다 — 그것을 잡으려면 별도 검사가 필요하다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **에러 메시지가 지시하는 설정은 그 설정을 읽는 코드와 함께 존재해야 한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **구성 오류는 한 예외 타입과 안정 코드로 보고한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 사실 - -KafkaMessagingTransport · RabbitMessagingTransport · PulsarMessagingTransport · NatsJetStreamTransport 가 전부 MessagingTransport 를 구현한다. 이 leaf 는 TransportConsumerRegistration 만 부분 구현한다. - -KafkaShareWorkQueueCapability 가 존재하는 이유는 "shared validators refuse … before a message is ever produced" 다. 그것이 실현되려면 MessagingTransport.capabilities(DestinationName) 를 통해 값이 전달돼야 한다. 그 인터페이스를 구현하지 않으므로 capability 는 아무도 읽지 않는 상수다. - -두 experimental 형제(pulsar, nats)는 구현한다. "experimental 이라서" 가 이유가 되지 않는다. - -## 미지수 - -이 leaf 를 완성할 것인가. 그 판정이 §17 첫 항목에 걸려 있고, 완성 여부가 정해지기 전에는 SPI 구현 여부도 정할 수 없다. - -## 선택지 - -MessagingTransport 를 구현한다 - capability 가 실제 검증기에 도달하고 형제 넷과 형태가 같아진다. - -capability 전달 경로를 따로 정한다 - 구현하지 않기로 하면 그 상수를 누가 읽는지가 정해져야 한다. - -## 다음 검증 - -git grep -n 'implements MessagingTransport' -- 'src/messaging/**/*.java' 로 형제들의 구현 상태는 확정된다. 완성 여부의 결정은 저장소 안의 사실로 닫히지 않는다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/question/openquestion-messaging-policy-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/question/openquestion-messaging-policy-f02.md deleted file mode 100644 index c37ee0f..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/question/openquestion-messaging-policy-f02.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -kind: QUESTION -slug: messaging-policy-f02 -title: 출하 컨텍스트가 발행은 하고 소비는 하지 못한다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: open-question:messaging-policy-f02 -questionStatus: OPEN -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-policy#L790 ---- - -# 출하 컨텍스트가 발행은 하고 소비는 하지 못한다 - -소비 경로의 여덟 클래스가 `src/main` 어디에서도 생성되지 않는다. 발행 경로는 생성된다. 이것이 미완인지 의도된 확장점인지가 정해지지 않았다. - -## 관계 - -- **구성 오류는 한 예외 타입과 안정 코드로 보고한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **저장소 밖 문서를 절 번호로 인용하지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **부팅 경로의 알고리즘 복잡도는 문서화한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 사실 - -KafkaConsumerRegistrar · RabbitConsumerRegistrar · KafkaBatchConsumerRegistrar · RabbitBatchConsumerRegistrar · DefaultDeliveryProcessor · KafkaRetryExecutor · KafkaDeadLetterPublisher · RabbitDeadLetterPublisher 가 전부 src/main 생성 0 이다. - -대조군인 발행 경로는 생성된다 — DefaultMessagePublisher 와 TransportMessagingRuntime 이 MessagingCoreAutoConfiguration:446,476 에서 만들어진다. - -이것이 messaging-policy 의 두 축이 미배선인 근본 원인이고, final/document.md#a19-messaging-core-api §12.1 이 관측한 MessageHandler 참조 0 의 조립 쪽 설명이다. - -docs/messaging/support-matrix.md 의 브로커 등급표가 소비 측 보장(순서 · 정산 · 재시도)을 서술하는데, 그 보장을 수행할 코드가 조립되지 않는다. - -## 미지수 - -소비 경로 조립이 미완인가, 파생 프로젝트의 조립 책임으로 남긴 확장점인가. 이 질문의 소유는 이 leaf 가 아니라 cross-scope 또는 messaging-spring-boot-starter 쪽이다. - -## 선택지 - -소비자 등록을 자동설정에 추가한다 - 지원 매트릭스가 서술하는 소비 측 보장이 실행 가능해진다. - -파생 프로젝트의 조립 책임임을 문서화한다 - 현 상태를 유지하되 지원 매트릭스가 그 경계를 밝힌다. - -## 다음 검증 - -evidence/raw/281 §F 를 재실행하면 생성 0 은 다시 확정된다. 의도 여부는 그 재실행으로 답해지지 않는다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/question/openquestion-messaging-reliability-api-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/question/openquestion-messaging-reliability-api-f03.md deleted file mode 100644 index c9ceddd..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/question/openquestion-messaging-reliability-api-f03.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -kind: QUESTION -slug: messaging-reliability-api-f03 -title: dual-write의 답이라고 선언한 진입점에 구현이 없다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: open-question:messaging-reliability-api-f03 -questionStatus: OPEN -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-reliability-api#L707 ---- - -# dual-write의 답이라고 선언한 진입점에 구현이 없다 - -Outbox 절반이 "릴레이가 읽는 쪽" 만 배선돼 있고 "애플리케이션이 쓰는 쪽" 이 비어 있다. - -## 관계 - -- **계약만 담는 leaf도 계약의 거절 조건은 자기 레인에서 검증한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **record의 `equals`를 좁히면 이유를 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **호출 컨텍스트가 계약이면 그 컨텍스트를 검증할 수단을 함께 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 사실 - -ReliableMessagePublisher 가 구현 0, 참조 0 이다. git grep -n -E 'implements .*ReliableMessagePublisher' -- src 가 아무것도 돌려주지 않는다. javadoc 은 "This is the answer to the dual-write problem" 이라고 적는다. - -OutboxRepository.append 가 있으므로 outbox 에 행을 넣을 방법 자체가 없는 것은 아니다. 다만 그 포트는 저장소 계약이고, ReliableMessagePublisher 는 애플리케이션이 저장소를 직접 만지지 않게 하려고 존재한다. - -애플리케이션은 ArchUnit 규칙 때문에 이 leaf 를 참조할 수 없다. 그래서 브리지 어댑터가 필요한데 그것이 없다. - -## 미지수 - -두 outbox 모델 중 어느 쪽이 정본인가. 이 질문은 application-core 와 cross-scope 가 함께 답한다. - -## 선택지 - -브리지 어댑터를 만든다 - 애플리케이션이 ArchUnit 규칙을 지키면서 이 진입점에 도달한다. - -애플리케이션 outbox 모델을 정본으로 삼는다 - 이 인터페이스를 제거하거나 파생 프로젝트의 확장점임을 명시한다. - -## 다음 검증 - -구현 부재는 evidence/raw/289 §B · §C · §D 로 확정된다. 어느 모델이 정본인가는 이 leaf 안에서 답해지지 않는다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/question/openquestion-messaging-schema-json-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/question/openquestion-messaging-schema-json-f03.md deleted file mode 100644 index 1dcefab..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/question/openquestion-messaging-schema-json-f03.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -kind: QUESTION -slug: messaging-schema-json-f03 -title: 빈 registry로 조립되면 모든 메시지가 거절된다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: open-question:messaging-schema-json-f03 -questionStatus: OPEN -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-schema-json#L481 ---- - -# 빈 registry로 조립되면 모든 메시지가 거절된다 - -기본값이 빈 registry 이므로 계약 bean 이 없으면 codec 이 시작에 성공하고 첫 publish 에서 실패한다. 그런 조립이 실제로 발생하는지가 이 leaf 밖에 있다. - -## 관계 - -- **실패 코드는 운영자의 다음 행동이 갈리는 지점마다 나눈다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 사실 - -contracts.getIfAvailable(MessageContracts::none) 이 기본값이므로 MessageContracts bean 이 없으면 빈 registry 로 codec 이 만들어진다(MessagingCoreAutoConfiguration:362-365). - -그 codec 은 시작에 성공하고 첫 publish 에서 UNKNOWN_MESSAGE_TYPE 으로 실패한다. - -messaging-core-api 계열의 다른 leaf 에서 관측된 것과 같은 형태다 — "시작은 하고 첫 쓰기에서 실패한다". - -## 미지수 - -MessageContracts 의 production 구현이 존재하는가. 존재하지 않으면 기본 조립이 곧 이 상태다. - -## 선택지 - -계약 bean 이 없을 때 시작을 거부한다 - 실패가 첫 publish 가 아니라 부팅으로 옮겨 간다. - -빈 registry 를 유효한 조립으로 유지한다 - 그때는 그 조합이 무엇을 뜻하는지가 문서에 있어야 한다. - -## 다음 검증 - -messaging-spring-boot-starter leaf 에서 MessageContracts production 구현의 존재 여부를 확인한다. 그 leaf SSOT 가 답을 갖는다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/reference/reference-a-validator-is-enforced-by-injection.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/reference/reference-a-validator-is-enforced-by-injection.md deleted file mode 100644 index d29e328..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/reference/reference-a-validator-is-enforced-by-injection.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -kind: REFERENCE -slug: a-validator-is-enforced-by-injection -title: 검증기는 발행이 아니라 주입이 강제다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:a-validator-is-enforced-by-injection -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# 검증기는 발행이 아니라 주입이 강제다 - -## 목적 - -검증기를 빈으로 만든 것을 그 검증이 돈다는 증거로 읽어, 아무것도 검사하지 않는 컨텍스트를 검사되는 컨텍스트로 착각하는 것을 막는다. - -## 규칙 - -1. 검증기의 존재와 실행은 다른 사실이다 - 컨테이너가 발행한 객체는 누군가 그것을 부를 때만 판정을 만든다. 발행 자체는 아무 판정도 아니다. - -2. 실행 지점을 이름으로 지목할 수 없으면 돌지 않는다고 본다 - 초기화 콜백이든 조립 코드의 호출이든, 그 지점을 파일과 줄로 댈 수 없으면 검증은 없는 것이다. - -3. 검증은 컨텍스트가 만들어지는 중에 실패해야 한다 - 응용 이벤트로 늦추면 실패 시점에 이미 빈이 다 만들어져 있고, 원인이 된 설정 객체가 스택에서 사라진다. - -4. 검증기가 유일한 소비자인 설정 키를 센다 - 그 수가 곧 검증이 돌지 않을 때 조용해지는 설정의 수다. - -5. 검증기가 여럿이면 각각의 실행 지점을 따로 확인한다 - 같은 파일 안에서 하나만 감싸이는 형태가 실제로 나타난다. - -## 적용 조건 - -시작 시점에 설정을 판정하는 모든 검증기 - -자동 설정이 만드는 정책·프로파일·능력 객체 - -## 예외 - -의도적으로 호출자에게 판정을 맡기는 순수 함수형 규칙 객체는 여기 해당하지 않는다. 다만 그 경우 호출자가 어디인지가 자바독에 있어야 한다. - -## 예시 - -한 가족이 이 결함을 이미 한 번 겪고 고쳤다. 브로커마다 검증기를 발행하면서 아무 데도 주입하지 않아 컨텍스트가 아무것도 검증하지 않았고, 수정은 검증기를 초기화 콜백을 구현한 얇은 타입으로 감싸는 것이었다. 그 타입의 자바독이 이전 상태를 기록으로 남긴다. - -같은 형태가 네 곳에 남아 있다. 플랫폼 시작 검증기의 호출자가 0 이고, 같은 자동 설정 안에서 검증기 하나만 감싸이지 않고, 두 아키텍처 규칙이 저장소 소스에 적용되지 않는다. - -## 관계 - -- **같은 자동 설정 안에서 검증기 하나만 감싸이지 않아 트랜잭션 조건이 검사되지 않는다** - 이 규칙이 나온 사례다. -- **시작 검증기가 유일한 소비자인 설정 키 넷이 아무것도 게이트하지 않는다** - 규칙 4 가 나온 사례다. -- **시작 검증기가 시작 시 실행되지 않는다** - 다른 가족의 같은 형태다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/reference/reference-messaging-claim-check-f04.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/reference/reference-messaging-claim-check-f04.md deleted file mode 100644 index c89405d..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/reference/reference-messaging-claim-check-f04.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-claim-check-f04 -title: leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-claim-check-f04 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-claim-check#L534 ---- - -# leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다 - -## 관계 - -- **배포 아티팩트가 싣지만 아무도 부르지 않고, 다른 곳의 에러 메시지가 이 경로를 권한다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **예외 승격이 에러 코드 문자열 접미사에 의존한다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. - -## 목적 - -테스트가 클래스 단위로 붙지 않으면 어떤 결정이 검증되지 않았는지를 셀 수 없다. messaging-claim-check 의 테스트 세 개가 guard·resolver·policy 를 겨냥하고 publisher 를 겨냥하는 것이 없어서, publisher 가 혼자 소유한 결정들이 통째로 미검증으로 남았다. - -## 규칙 - -1. leaf 의 public 클래스 목록과 테스트 클래스 목록을 나란히 놓는다 - 짝이 없는 이름이 곧 미검증 표면이다. - -2. 그 클래스가 혼자 소유한 결정을 센다 - publisher 의 경우 오프로드 판정(shouldOffload), 오프로드 시 payload 를 비우는 것, Offloaded 의 양방향 방어 복사, 그리고 "저장이 발행보다 먼저" 라는 순서다. 마지막 것은 publisher 의 계약인데 그것을 확인하는 테스트가 없다. - -3. 다른 클래스의 테스트가 대신 덮고 있는지 확인한다 - 덮고 있다면 그 사실을 적고, 덮지 않으면 테스트를 만든다. - -## 적용 조건 - -leaf 하나가 여러 public 클래스를 갖고 그중 일부만 테스트 이름에 등장하는 자리. - -## 예외 - -테스트가 소비자 leaf 에 있는 경우는 예외로 볼 수 있다. 다만 그때는 어느 레인이 그 계약을 붙드는지가 기록돼 있어야 하고, 여기서는 그런 기록이 없다. - -## 예시 - -find src/test -name '*Test.java' 가 세 개를 돌려주고 그 셋이 guard·resolver·policy 라는 것. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/reference/reference-messaging-kafka-share-experimental-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/reference/reference-messaging-kafka-share-experimental-f02.md deleted file mode 100644 index 7c160df..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/reference/reference-messaging-kafka-share-experimental-f02.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-kafka-share-experimental-f02 -title: 허용 의존 목록은 상한이므로 미사용을 잡지 않는다 — 그것을 잡으려면 별도 검사가 필요하다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-kafka-share-experimental-f02 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-kafka-share-experimental#L476 ---- - -# 허용 의존 목록은 상한이므로 미사용을 잡지 않는다 — 그것을 잡으려면 별도 검사가 필요하다 - -## 관계 - -- **"등록"이 아무것도 등록하지 않고 성공을 반환한다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. - -## 목적 - -verifyCleanArchitectureDependencies 는 allowed_dependencies 를 상한으로만 검사한다. 그래서 쓰지 않는 의존이 남아 있어도 초록불이고, build closure 는 실제 필요보다 넓어진다. 여기서는 messaging-kafka 34파일과 그 전이 의존이 함께 딸려 온다. - -## 규칙 - -1. 선언된 의존과 실제 import 를 각각 센다 - build.gradle 이 org.apache.kafka:kafka-clients 를 선언하는데 import org.apache.kafka 가 0건이다. registry 가 messaging-policy 와 messaging-kafka 의존을 허용하는데 두 패키지의 import 도 0건이다. - -2. 상한 검사만으로는 이 차이가 드러나지 않는다는 것을 안다 - 허용 목록을 통과했다는 사실은 "선언이 과하지 않다" 를 뜻하지 않는다. - -3. 미사용을 잡으려면 import 를 세는 검사를 따로 둔다 - 그때까지는 선언 옆에 미완 상태임을 적어 둔다. - -## 적용 조건 - -registry 나 build 파일이 의존을 선언하고, 그 선언이 아키텍처 검사의 입력이 되는 모든 leaf. - -## 예외 - -곧 쓰일 예정이라 미리 선언해 둔 경우는 예외가 될 수 있다. 다만 그 의도가 주석에 없으면 읽는 쪽에서는 "이 leaf 가 Kafka 를 쓴다" 는 인상만 남는다. - -## 예시 - -evidence/raw/290 §D 의 import 전수와 build.gradle 의 선언. 확인 방법은 grep -rn 'import org.apache.kafka\|import dev.caskeleton.messaging.policy\|import dev.caskeleton.messaging.kafka\.' src/messaging/messaging-kafka-share-experimental/src 다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/reference/reference-messaging-runtime-core-f07.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/reference/reference-messaging-runtime-core-f07.md deleted file mode 100644 index 28e45c5..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/reference/reference-messaging-runtime-core-f07.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-runtime-core-f07 -title: 만들어 두고 흘리지 않는 진단값은 진단이 아니다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-runtime-core-f07 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-runtime-core#L763 ---- - -# 만들어 두고 흘리지 않는 진단값은 진단이 아니다 - -## 관계 - -- **관측이 구현·호출부·주입 자리를 모두 갖추고도 출하에서 no-op이다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **소비 오케스트레이터가 조립되지 않는다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **선언된 content type과 실제 인코딩이 조용히 갈라질 수 있다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. - -## 목적 - -핸들러가 null 을 반환한 경우와 HandleResult.Retry 를 반환한 경우가 정산 수준에서 구분되지 않는다. 전자는 프로그래밍 오류이고 후자는 정상 흐름인데 같은 requeue 로 끝난다. descriptor 는 만들어졌으나 어디로도 흐르지 않는다. - -## 규칙 - -1. descriptor 를 만드는 코드와 그것을 소비하는 코드를 짝지어 본다 - DefaultDeliveryProcessor.missingResult() 가 HANDLER_RETURNED_NOTHING descriptor 를 만든다. result == null 분기는 그것을 쓰지 않고 바로 settlement.requeue(retryDelay) 를 부른다. - -2. 소비자가 없으면 둘 중 하나를 고른다 - 값을 관측이나 로그로 흘리거나, 메서드를 제거한다. - -3. 남겨 둘 이유가 있으면 그 이유를 적는다 - package-private static 이라 외부 호출자가 생길 수 없다는 점이 판정을 단순하게 만든다. - -## 적용 조건 - -실패 원인을 값으로 표현해 두고 그 값이 정산·로그·메트릭 중 어디로도 나가지 않을 수 있는 자리. - -## 예외 - -SSOT 가 이 규칙의 반례를 적지 않았다. 테스트에서만 쓰는 진단 팩토리라면 그 사실이 이름이나 주석에 있어야 한다. - -## 예시 - -DefaultDeliveryProcessor.java:77-79 의 팩토리와 :146-154 의 null 분기. 확인 방법은 git grep -n 'missingResult' -- src 다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/reference/reference-messaging-security-f07.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/reference/reference-messaging-security-f07.md deleted file mode 100644 index 3a8af01..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/reference/reference-messaging-security-f07.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-security-f07 -title: 배선된 게이트는 자기 leaf 레인에서 검증한다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-security-f07 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-security#L688 ---- - -# 배선된 게이트는 자기 leaf 레인에서 검증한다 - -## 관계 - -- **종료 시 자격증명 소거가 호출되지 않는다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **같은 TLS posture를 두 클래스가 다른 엄격도로 검사한다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **권한 거부가 `AUTHORIZATION`이 아니라 `CONFIGURATION`으로 기록된다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. - -## 목적 - -BrokerTlsPolicy 는 실제로 배선된 클래스다 — 어댑터 둘이 호출한다. 그 네 거절 조건과 허용목록 판정이 이 leaf 의 레인에서 검증되지 않는다. 어댑터 테스트가 간접적으로 지나가더라도 그것은 다른 목표를 가진 레인이다. - -## 규칙 - -1. leaf 의 타입 중 테스트에 이름이 없는 것을 센다 - 다섯이다. BrokerTlsPolicy · BrokerAclManifest · DestinationAccessPolicy · DestinationAccessValidator · BrokerCredentialProfile. - -2. 그중 실제 호출자가 있는 것을 먼저 고른다 - 배선되지 않은 타입의 미검증과 배선된 게이트의 미검증은 무게가 다르다. - -3. 거절 조건과 경계를 겨냥한 테스트를 자기 레인에 둔다 - BrokerTlsPolicy 의 네 거절 조건과 허용·거부 경계가 그 대상이다. - -## 적용 조건 - -보안·승인 판정을 수행하고 다른 leaf 가 호출하는 모든 게이트 클래스. - -## 예외 - -다른 레인이 그 게이트를 명시적 목표로 검증하고 그 사실이 기록돼 있으면 예외가 될 수 있다. 어댑터 테스트는 그 조건을 만족하지 않는다. - -## 예시 - -세 테스트 클래스 전수. 확인 방법은 find src/test -name '*Test.java' 가 셋을 돌려준다는 것이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/reference/reference-messaging-spring-cloud-stream-bridge-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/reference/reference-messaging-spring-cloud-stream-bridge-f01.md deleted file mode 100644 index 3e08378..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/reference/reference-messaging-spring-cloud-stream-bridge-f01.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-spring-cloud-stream-bridge-f01 -title: 허용 의존 목록은 상한이므로 미사용을 잡지 않는다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-spring-cloud-stream-bridge-f01 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-spring-cloud-stream-bridge#L552 ---- - -# 허용 의존 목록은 상한이므로 미사용을 잡지 않는다 - -## 목적 - -spring-context 선언이 "이 leaf 가 Spring 과 통합돼 있다" 는 인상을 주는데, main 소스는 Spring 타입을 한 번도 이름 부르지 않는다. 바인더 접촉면 전체가 자체 함수형 인터페이스다. - -## 규칙 - -1. 선언과 import 를 각각 센다 - registry 가 messaging-transport-spi 를 허용하고 build.gradle 이 spring-context 를 선언한다. main 소스의 비-JDK import 9개는 전부 messaging-core-api 와 messaging-policy 에서 온다. - -2. 검색 결과가 비었는지 확인한다 - import dev.caskeleton.messaging.transport 와 import org.springframework 검색이 exit 1 이다. - -3. 상한 검사로는 이 차이가 드러나지 않는다는 것을 안다 - verifyCleanArchitectureDependencies 는 허용 목록을 상한으로만 본다. 같은 형태가 messaging-kafka-share-experimental 에도 있다 — incubating leaf 둘이 같은 방식으로 미사용 의존을 선언했다. - -## 적용 조건 - -registry 나 build 파일이 의존을 선언하고 그 선언이 아키텍처 검사의 입력이 되는 모든 leaf. - -## 예외 - -완성 시 필요해질 의존을 미리 선언해 둔 경우는 예외가 될 수 있다. 그때는 그 사실이 build.gradle 주석에 있어야 한다. - -## 예시 - -evidence/raw/296 §B 의 import 전수와 두 검색의 exit 1. 확인 방법은 그 §B 를 재실행하는 것이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/reference/reference-messaging-spring-cloud-stream-bridge-f05.md b/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/reference/reference-messaging-spring-cloud-stream-bridge-f05.md deleted file mode 100644 index 1f365e3..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/runtime-reachability-and-composition/reference/reference-messaging-spring-cloud-stream-bridge-f05.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-spring-cloud-stream-bridge-f05 -title: 등록을 받는 컴포넌트는 해제도 제공한다 -topic: runtime-reachability-and-composition -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-spring-cloud-stream-bridge-f05 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-spring-cloud-stream-bridge#L588 ---- - -# 등록을 받는 컴포넌트는 해제도 제공한다 - -## 목적 - -바인딩이 재구성되거나 컨텍스트가 종료될 때 맵이 비워지지 않는다. 오늘은 조립되지 않아 무해하다. 같은 가족의 messaging-transport-spi 는 TransportConsumerRegistration 을 AutoCloseable 로 두어 반대편을 이미 갖췄다. - -## 규칙 - -1. 등록 메서드가 있으면 짝이 되는 해제를 찾는다 - SpringCloudStreamConsumerBridge 에 unregister 도 close 도 없다. SpringCloudStreamPublisherBridge 도 마찬가지다. - -2. 없으면 등록이 프로세스 수명과 같아지는지 확인한다 - 여기서는 바인딩 재구성이 그 가정을 깬다. - -3. unregister(bindingName) 이나 AutoCloseable 중 하나를 둔다 - 둘 중 어느 쪽이든 해제 시점이 코드에 생긴다. - -## 적용 조건 - -핸들러·리스너·바인딩을 맵에 담아 두는 모든 등록 컴포넌트. - -## 예외 - -등록이 애플리케이션 수명과 정확히 같고 재구성 경로가 없으면 대상이 아니다. 이 브리지는 재구성을 전제하는 자리에 있다. - -## 예시 - -두 클래스의 public 메서드 전수. 확인 방법은 그 목록을 보는 것이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-grpc-advanced-bootstrap-f05.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-grpc-advanced-bootstrap-f05.md deleted file mode 100644 index c8f2408..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-grpc-advanced-bootstrap-f05.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -kind: CASE -slug: grpc-advanced-bootstrap-f05 -title: 예외가 들고 있는 능력이 transient 라 역직렬화 뒤 사라진다 -topic: schema-and-data-contracts -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-advanced-bootstrap-f05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-advanced-bootstrap-f05 - file: ../../../final/evidence/rendered/grpc-advanced-bootstrap-f05.svg -evidence: - - ../../../final/evidence/raw/grpc-advanced-bootstrap-f05.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-advanced-bootstrap#L312 이다. -module: grpc-advanced-bootstrap -priority: P3 ---- - -# 예외가 들고 있는 능력이 transient 라 역직렬화 뒤 사라진다 - -transient 는 보통 직렬화 가능하지 않은 필드를 담은 Serializable 클래스에 대한 정적 분석 경고를 끄려고 붙인다. 그런데 열거형은 언제나 직렬화 가능하다 — 여기서 transient 가 막을 문제가 애초에 없다. - -## 문제 - -transient 는 보통 직렬화 가능하지 않은 필드를 담은 Serializable 클래스에 대한 정적 분석 경고를 끄려고 붙인다. - -그런데 열거형은 언제나 직렬화 가능하다 — 여기서 transient 가 막을 문제가 애초에 없다. - -## 결론 - -대가는 있다. - -예외가 직렬화를 거쳐 오면 capability() 가 null 이다. - -메시지 문자열은 살아남으므로 사람이 읽는 데는 지장이 없고, 그래서 눈에 띄지 않는다. - -이 예외를 던지는 require 자체가 리프 밖에서 불리지 않으므로(§12.1) 오늘 도달하지 않는다. - -transient 를 지우는 것이 수정 전부다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : Serializable 참조 8건 검색과 예외 필드의 transient 선언 및 열거형 직렬화 성질 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-advanced-bootstrap#L312 에 있다. - -## 본문 - - - -`transient` 는 보통 직렬화 가능하지 않은 필드를 담은 `Serializable` 클래스에 대한 정적 분석 경고를 끄려고 붙인다. 그런데 열거형은 언제나 직렬화 가능하다 — 여기서 `transient` 가 막을 문제가 애초에 없다. - -## Serializable 참조 위치 - -:::evidence key="grpc-advanced-bootstrap-f05" alt="코드베이스에서 Serializable 를 검색한 출력 8줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="Serializable 코드베이스 검색 — 8줄 · exit 0" zoom="true" -::: - -## 대가는 있다 - -예외가 직렬화를 거쳐 오면 `capability()` 가 `null` 이다. 메시지 문자열은 살아남으므로 사람이 읽는 데는 지장이 없고, 그래서 눈에 띄지 않는다. 이 예외를 던지는 `require` 자체가 리프 밖에서 불리지 않으므로(§12.1) 오늘 도달하지 않는다. - -## 수정 - -`transient` 를 지우는 것이 전부다. - -## 확인하지 못한 것 - -실제로 직렬화·역직렬화해 능력이 사라지는 것을 관측하지 않았다. 열거형이 언제나 직렬화 가능하다는 것과 transient 선언의 대조로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-grpc-advanced-edition-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-grpc-advanced-edition-f01.md deleted file mode 100644 index eb8625c..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-grpc-advanced-edition-f01.md +++ /dev/null @@ -1,97 +0,0 @@ ---- -kind: CASE -slug: grpc-advanced-edition-f01 -title: 비교 픽스처에 비교 대상이 없다 -topic: schema-and-data-contracts -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-advanced-edition-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-advanced-edition-f01 - file: ../../../final/evidence/rendered/grpc-advanced-edition-f01.svg - - key: grpc-advanced-edition-f01-diagram - file: ../../../final/assets/diagrams/grpc-advanced-edition-f01.svg -evidence: - - ../../../final/evidence/raw/grpc-advanced-edition-f01.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-advanced-edition#L180 이다. -module: grpc-advanced-edition -priority: P2 ---- - -# 비교 픽스처에 비교 대상이 없다 - -compatibility.proto 의 주석이 존재 이유를 적는다. 그 쌍둥이가 저장소에 없다. - -## 문제 - -compatibility.proto 의 주석이 존재 이유를 적는다. - -그 쌍둥이가 저장소에 없다. - -## 결론 - -한 곳뿐이다. - -같은 필드와 번호를 proto3 로 선언한 파일이 없으므로 비교가 성립하지 않는다. - -그리고 두 번째 전제도 없다. - -이 저장소에는 protobuf 플러그인이 어디에도 없다 — grpc-proto-contract 와 adapter-inbound-grpc 의 build.gradle 이 그 사실을 주석으로 명시한다. - -그러므로 편집 파일도 proto3 파일도 컴파일되지 않고, 유선 바이트와 JSON 을 비교할 산출물 자체가 만들어지지 않는다. - -결과적으로 GrpcEditionCompatibilityReport 는 사람이 손으로 채우는 기록이 된다. - -승격 게이트가 그것을 읽어 판정하므로, 게이트의 입력이 측정이 아니라 선언이다. - -Advanced 가족이라 오늘의 배포에는 영향이 없다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : GrpcEditionCompatibilityReport 참조 7건 검색과 픽스처 디렉터리의 파일 목록 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-advanced-edition#L180 에 있다. - -## 본문 - - - -`compatibility.proto` 의 주석이 존재 이유를 적는다. 그 쌍둥이가 저장소에 없다. - -## 비교에 필요한 것 - -:::evidence key="grpc-advanced-edition-f01-diagram" alt="edition 픽스처만 저장소 안에 놓이고 proto3 짝 픽스처와 컴파일 산출물이 바깥에 빗금으로 놓인다" caption="비교에 필요한 것" zoom="false" -::: - -같은 필드와 번호를 proto3 로 선언한 파일이 없으므로 비교가 성립하지 않는다. - -## GrpcEditionCompatibilityReport 참조 위치 - -:::evidence key="grpc-advanced-edition-f01" alt="코드베이스에서 GrpcEditionCompatibilityReport 를 검색한 출력 7줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcEditionCompatibilityReport 코드베이스 검색 — 7줄 · exit 0" zoom="true" -::: - -## 두 번째 전제도 없다 - -이 저장소에는 protobuf 플러그인이 어디에도 없다 — `grpc-proto-contract` 와 `adapter-inbound-grpc` 의 build.gradle 이 그 사실을 주석으로 명시한다. 그러므로 편집 파일도 proto3 파일도 컴파일되지 않고, 유선 바이트와 JSON 을 비교할 산출물 자체가 만들어지지 않는다. - -## 게이트의 입력이 측정이 아니라 선언이다 - -`GrpcEditionCompatibilityReport` 는 사람이 손으로 채우는 기록이 되고, 승격 게이트가 그것을 읽어 판정한다. Advanced 가족이라 오늘의 배포에는 영향이 없다. - -## 기록하는 이유 - -이 리프의 목적이 "공개 서비스가 옮겨 가기 전에 그 실패를 찾는 것" 이고, 그 실패를 찾을 장치가 픽스처 하나만 있고 짝이 없다. `compatibility_proto3.proto` 를 같은 디렉터리에 두어 필드·번호·JSON 이름을 맞추고, 두 파일을 컴파일해 산출물을 비교하는 레인을 만든다. 그 레인이 생기기 전까지는 이 보고서가 측정이 아니라 선언이라는 것을 자바독에 적는 편이 낫다. - -## 확인하지 못한 것 - -protoc 을 돌려 이 편집 파일이 실제로 컴파일되는지 확인하지 않았다. 저장소에 protobuf 플러그인이 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-grpc-codegen-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-grpc-codegen-f03.md deleted file mode 100644 index ae110a7..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-grpc-codegen-f03.md +++ /dev/null @@ -1,84 +0,0 @@ ---- -kind: CASE -slug: grpc-codegen-f03 -title: 픽스처의 메서드 경로가 서비스 × 메서드 교차곱이다 -topic: schema-and-data-contracts -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-codegen-f03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-codegen-f03 - file: ../../../final/evidence/rendered/grpc-codegen-f03.svg -evidence: - - ../../../final/evidence/raw/grpc-codegen-f03.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-codegen#L239 이다. -module: grpc-codegen -priority: P3 ---- - -# 픽스처의 메서드 경로가 서비스 × 메서드 교차곱이다 - -stub.( 호출 하나가 그 파일이 import 한 모든 서비스에 대해 메서드 경로를 만든다. javadoc 의 규칙 서술은 단수형이다 — "a method is a stub.( call, mapped to /". - -## 문제 - -stub.( 호출 하나가 그 파일이 import 한 모든 서비스에 대해 메서드 경로를 만든다. - -javadoc 의 규칙 서술은 단수형이다 — "a method is a stub.( call, mapped to /". - -## 결론 - -서비스가 둘 이상일 때 어느 서비스인지는 소스 텍스트만으로 알 수 없고, 코드는 전부에 붙이는 쪽을 골랐다. - -결과는 존재하지 않는 메서드 경로를 요구하는 픽스처다. - -서비스 둘과 메서드 셋이면 요구 경로가 여섯 개가 되고, 그중 셋은 어떤 후보 스키마에도 없으므로 breaksAgainst 가 항상 METHOD_PATH 파괴를 보고한다. - -그러면 GrpcSchemaArtifactPublisher.evaluate 가 모든 발행을 거부한다. - -커밋된 픽스처는 서비스가 하나(DocumentServiceGrpc)라 지금은 정확하다. - -두 번째 소비자 픽스처를 추가하는 순간 성립한다. - -수정은 호출자 변수의 선언 타입을 함께 읽어 메서드를 서비스에 귀속시키거나, 서비스가 둘 이상인 픽스처를 거부하는 것이다. - -후자는 지금 형태의 근사를 명시적으로 만든다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : GrpcSchemaArtifactPublisher 참조 19건 검색과 메서드 경로 생성 규칙 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-codegen#L239 에 있다. - -## 본문 - - - -`stub.(` 호출 하나가 그 파일이 import 한 **모든** 서비스에 대해 메서드 경로를 만든다. javadoc 의 규칙 서술은 단수형이다 — "a method is a `stub.(` call, mapped to `/`". - -## GrpcSchemaArtifactPublisher 참조 위치 - -:::evidence key="grpc-codegen-f03" alt="코드베이스에서 GrpcSchemaArtifactPublisher 를 검색한 출력 19줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcSchemaArtifactPublisher 코드베이스 검색 — 19줄 · exit 0" zoom="true" -::: - -## 소스 텍스트만으로는 알 수 없어 전부에 붙였다 - -서비스가 둘 이상일 때 어느 서비스인지는 소스 텍스트만으로 알 수 없고, 코드는 전부에 붙이는 쪽을 골랐다. 결과는 존재하지 않는 메서드 경로를 요구하는 픽스처다 — 서비스 둘과 메서드 셋이면 요구 경로가 여섯 개가 되고, 그중 셋은 어떤 후보 스키마에도 없으므로 `breaksAgainst` 가 항상 `METHOD_PATH` 파괴를 보고한다. 그러면 `GrpcSchemaArtifactPublisher.evaluate` 가 모든 발행을 거부한다. - -## 지금 픽스처는 서비스가 하나라 정확하다 - -`DocumentServiceGrpc` 하나다. 두 번째 소비자 픽스처를 추가하는 순간 성립한다. 수정은 호출자 변수의 선언 타입을 함께 읽어 메서드를 서비스에 귀속시키거나, 서비스가 둘 이상인 픽스처를 거부하는 것이다. - -## 확인하지 못한 것 - -서비스가 둘 이상인 픽스처를 만들어 교차곱을 재현하지 않았다. 유도 코드로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-grpc-core-api-f06.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-grpc-core-api-f06.md deleted file mode 100644 index c6839c9..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-grpc-core-api-f06.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -kind: CASE -slug: grpc-core-api-f06 -title: 직렬화 가능하다고 선언한 예외가 자기 내용을 직렬화하지 않는다 -topic: schema-and-data-contracts -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-core-api-f06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-core-api-f06 - file: ../../../final/evidence/rendered/grpc-core-api-f06.svg -evidence: - - ../../../final/evidence/raw/grpc-core-api-f06.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-core-api#L241 이다. -module: grpc-core-api -priority: P3 ---- - -# 직렬화 가능하다고 선언한 예외가 자기 내용을 직렬화하지 않는다 - -serialVersionUID 는 이 타입이 직렬화된다는 선언이고, transient 는 유일한 필드가 그 직렬화에서 빠진다는 선언이다. 둘이 함께 있으면 역직렬화된 예외는 context == null 이고, 공개 메서드 둘 중 하나(requiresReconciliation())가 NPE 를 던진다. - -## 문제 - -serialVersionUID 는 이 타입이 직렬화된다는 선언이고, transient 는 유일한 필드가 그 직렬화에서 빠진다는 선언이다. - -둘이 함께 있으면 역직렬화된 예외는 context == null 이고, 공개 메서드 둘 중 하나(requiresReconciliation())가 NPE 를 던진다. - -## 결론 - -transient 자체는 강제된 선택이다 — GrpcFailureContext 가 Serializable 을 구현하지 않으므로 필드를 남기면 예외가 직렬화되지 않는다. - -기록하는 이유는 이 리프의 서술 규율과 대비되기 때문이다. - -다른 자리에서는 부재마다 이유가 붙어 있다("There is no factory that takes raw metadata, and that absence is the design"). - -여기에는 transient 의 이유도, 역직렬화 뒤의 계약도 적혀 있지 않다. - -도달성은 낮다. - -gRPC 예외가 자바 직렬화를 지나는 경로는 이 저장소에 없다. - -수정은 셋 중 하나다 — GrpcFailureContext 와 그 구성 요소를 Serializable 로 만들거나, serialVersionUID 를 지워 직렬화를 지원하지 않음을 명시하거나, context() 와 requiresReconciliation() 이 null 문맥을 다루도록 하고 그 이유를 적는 것. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : GrpcFailureContext 참조 34건 검색과 예외 필드의 transient·serialVersionUID 선언 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-core-api#L241 에 있다. - -## 본문 - - - -`serialVersionUID` 는 이 타입이 직렬화된다는 선언이고, `transient` 는 유일한 필드가 그 직렬화에서 빠진다는 선언이다. 둘이 함께 있으면 역직렬화된 예외는 `context == null` 이고, 공개 메서드 둘 중 하나(`requiresReconciliation()`)가 NPE 를 던진다. - -## GrpcFailureContext 참조 위치 - -:::evidence key="grpc-core-api-f06" alt="코드베이스에서 GrpcFailureContext 를 검색한 출력 34줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcFailureContext 코드베이스 검색 — 34줄 · exit 0" zoom="true" -::: - -## transient 자체는 강제된 선택이다 - -`GrpcFailureContext` 가 `Serializable` 을 구현하지 않으므로 필드를 남기면 예외가 직렬화되지 않는다. - -## 이 리프의 서술 규율과 대비된다 - -다른 자리에서는 부재마다 이유가 붙어 있다("There is no factory that takes raw metadata, and that absence is the design"). 여기에는 `transient` 의 이유도, 역직렬화 뒤의 계약도 적혀 있지 않다. - -## 도달성은 낮다 - -gRPC 예외가 자바 직렬화를 지나는 경로는 이 저장소에 없다. 수정은 셋 중 하나다 — `GrpcFailureContext` 와 그 구성 요소를 `Serializable` 로 만들거나, `serialVersionUID` 를 지워 직렬화를 지원하지 않음을 명시하거나, 두 메서드가 null 문맥을 다루도록 하고 그 이유를 적는 것. - -## 확인하지 못한 것 - -실제로 직렬화·역직렬화해 NPE 를 재현하지 않았다. 두 선언이 함께 있다는 것과 공개 메서드의 필드 접근으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-grpc-policy-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-grpc-policy-f03.md deleted file mode 100644 index cb60610..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-grpc-policy-f03.md +++ /dev/null @@ -1,95 +0,0 @@ ---- -kind: CASE -slug: grpc-policy-f03 -title: 직렬 스트림 기록기의 가장 오래된 것 버리기가 잘못된 메시지의 바이트를 뺀다 -topic: schema-and-data-contracts -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-policy-f03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-policy-f03 - file: ../../../final/evidence/rendered/grpc-policy-f03.svg - - key: grpc-policy-f03-diagram - file: ../../../final/assets/diagrams/grpc-policy-f03.svg -evidence: - - ../../../final/evidence/raw/grpc-policy-f03.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-policy#L249 이다. -module: grpc-policy -priority: P2 ---- - -# 직렬 스트림 기록기의 가장 오래된 것 버리기가 잘못된 메시지의 바이트를 뺀다 - -버려지는 것은 꺼낸 봉투인데 빼는 값은 새 메시지의 크기다. 봉투는 크기를 성분으로 담지 않으므로 이 지점에서 버려지는 크기를 알 방법이 없다. - -## 문제 - -버려지는 것은 꺼낸 봉투인데 빼는 값은 새 메시지의 크기다. - -봉투는 크기를 성분으로 담지 않으므로 이 지점에서 버려지는 크기를 알 방법이 없다. - -## 결론 - -계산을 따라가면 이렇다. - -한 번의 DROP_OLDEST 마다 queuedBytes 는 nextBytes 만큼 빠졌다가 enqueue 에서 같은 값만큼 다시 더해진다 — 순변화 0. - -그런데 큐의 실제 내용은 nextBytes - droppedBytes 만큼 바뀐다. - -그 차이가 매 낙차마다 쌓인다. - -방향은 둘 다 틀렸다. - -들어오는 메시지가 버려지는 것보다 크면 추적값이 실제보다 낮아져 바이트 경계가 늦게 발화한다(메모리). - -반대면 실제보다 높아져 경계가 이르게 발화한다(불필요한 종료·낙차). - -누적 바이트는 흐름 제어 정책의 판정 입력이고, 바이트 경계의 존재 이유가 javadoc 에 있다 — 개수 경계만 있으면 메모리 한도를 가장 큰 메시지가 정한다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : 큐에서 빼는 대상과 계수기에서 차감하는 값의 대조, 봉투 성분 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-policy#L249 에 있다. - -## 본문 - - - -버려지는 것은 꺼낸 봉투인데 빼는 값은 새 메시지의 크기다. 봉투는 크기를 성분으로 담지 않으므로 이 지점에서 버려지는 크기를 알 방법이 없다. - -## 두 자리가 가리키는 대상 - -:::evidence key="grpc-policy-f03-diagram" alt="큐에서 빠지는 것 쪽에 가장 오래된 봉투와 그 봉투의 바이트가 놓이고 차감되는 값 쪽에 새 메시지와 그 바이트가 놓인다" caption="두 자리가 가리키는 대상" zoom="false" -::: - -## 계산을 따라가면 - -한 번의 DROP_OLDEST 마다 `queuedBytes` 는 `nextBytes` 만큼 빠졌다가 `enqueue` 에서 같은 값만큼 다시 더해진다 — **순변화 0**. 그런데 큐의 실제 내용은 `nextBytes - droppedBytes` 만큼 바뀐다. 그 차이가 매 낙차마다 쌓인다. - -## 빼는 값과 버리는 대상 - -:::evidence key="grpc-policy-f03" alt="분석 문서 final/document.md#a20-grpc-policy 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-policy 발췌 — 15줄" zoom="true" -::: - -## 방향은 둘 다 틀렸다 - -들어오는 메시지가 버려지는 것보다 크면 추적값이 실제보다 **낮아져** 바이트 경계가 늦게 발화한다(메모리). 반대면 실제보다 **높아져** 경계가 이르게 발화한다(불필요한 종료·낙차). 누적 바이트는 흐름 제어 정책의 판정 입력이고, 바이트 경계의 존재 이유가 javadoc 에 있다 — 개수 경계만 있으면 메모리 한도를 가장 큰 메시지가 정한다. - -## flush 가 오차를 끊는다 - -`flush()` 가 큐를 비우면서 `queuedBytes = 0L` 로 되돌리므로 오차가 flush 를 건너 누적되지는 않는다. 그래서 이것은 영구 드리프트가 아니라 한 flush 주기 안의 폭주 구간에서 바이트 경계를 잘못 판정하는 결함이다. 낙차가 일어나는 상황이 곧 소비자가 못 따라가는 상황이고, 그때 flush 간격이 가장 길어진다. - -## 확인하지 못한 것 - -실제 스트림으로 버리기를 유발해 바이트 오차를 관측하지 않았다. 봉투가 크기를 성분으로 담지 않는다는 것으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-grpc-proto-contract-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-grpc-proto-contract-f01.md deleted file mode 100644 index 6aac48d..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-grpc-proto-contract-f01.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -kind: CASE -slug: grpc-proto-contract-f01 -title: reserved 2 to 5; 범위가 개별 숫자로만 수집되어 RESERVED_HISTORY 오탐이 된다 -topic: schema-and-data-contracts -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-proto-contract-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-proto-contract-f01 - file: ../../../final/evidence/rendered/grpc-proto-contract-f01.svg -evidence: - - ../../../final/evidence/raw/grpc-proto-contract-f01.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-proto-contract#L175 이다. -module: grpc-proto-contract -priority: P3 ---- - -# reserved 2 to 5; 범위가 개별 숫자로만 수집되어 RESERVED_HISTORY 오탐이 된다 - -reserved 2 to 5; 는 그룹이 "2 to 5" 이고 수집되는 것은 {2, 5} 다. 3·4 는 들어가지 않는다. - -## 문제 - -reserved 2 to 5; 는 그룹이 "2 to 5" 이고 수집되는 것은 {2, 5} 다. - -3·4 는 들어가지 않는다. - -## 결론 - -reserved 9 to max; 는 {9} 만 남는다. - -그러면 삭제 이력이 3 을 담고 스키마가 reserved 2 to 5; 로 정확히 예약했는데도 RESERVED_HISTORY 위반이 보고된다. - -범위 예약은 표준 문법이고 여러 필드를 한 번에 지울 때 쓰는 형태이므로 도달 가능하다. - -수정은 to 를 인식해 범위를 펼치는 것이다. - -max 는 상한 상수로 다루거나 그 메시지에 대해 검사를 통과시킨다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : reserved 범위 표기의 정규식 그룹과 수집 코드가 남기는 값의 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-proto-contract#L175 에 있다. - -## 본문 - - - -`reserved 2 to 5;` 는 그룹이 `"2 to 5"` 이고 수집되는 것은 `{2, 5}` 다. `3`·`4` 는 들어가지 않는다. `reserved 9 to max;` 는 `{9}` 만 남는다. - -## 범위 표기가 수집되는 방식 - -:::evidence key="grpc-proto-contract-f01" alt="분석 문서 final/document.md#a20-grpc-proto-contract 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-proto-contract 발췌 — 15줄" zoom="true" -::: - -## 정확히 예약한 스키마가 위반으로 보고된다 - -삭제 이력이 `3` 을 담고 스키마가 `reserved 2 to 5;` 로 예약했는데도 `RESERVED_HISTORY` 위반이 나온다. 범위 예약은 표준 문법이고 여러 필드를 한 번에 지울 때 쓰는 형태이므로 도달 가능하다. - -## 수정 - -`to` 를 인식해 범위를 펼치는 것이다. `max` 는 상한 상수로 다루거나 그 메시지에 대해 검사를 통과시킨다. - -## 확인하지 못한 것 - -범위 문법의 오탐을 실행으로 재현하지 않았다. 정규식과 수집 코드로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-grpc-proto-contract-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-grpc-proto-contract-f03.md deleted file mode 100644 index c1b8aef..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-grpc-proto-contract-f03.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -kind: CASE -slug: grpc-proto-contract-f03 -title: 커밋 스키마 게이트가 파일 목록을 하드코딩한다 -topic: schema-and-data-contracts -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-proto-contract-f03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-proto-contract-f03 - file: ../../../final/evidence/rendered/grpc-proto-contract-f03.svg -evidence: - - ../../../final/evidence/raw/grpc-proto-contract-f03.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-proto-contract#L203 이다. -module: grpc-proto-contract -priority: P3 ---- - -# 커밋 스키마 게이트가 파일 목록을 하드코딩한다 - -리소스 디렉터리를 훑지 않는다. 이 리프에 세 번째 .proto 를 추가하면 이 테스트를 함께 고치기 전까지 판정되지 않고, 빌드는 초록으로 남는다. - -## 문제 - -리소스 디렉터리를 훑지 않는다. - -이 리프에 세 번째 .proto 를 추가하면 이 테스트를 함께 고치기 전까지 판정되지 않고, 빌드는 초록으로 남는다. - -## 결론 - -같은 저장소가 다른 곳에서 이 형태를 이미 경계했다 — 빠뜨림이 통과가 되는 게이트다. - -수정은 proto/** 아래 .proto 를 전부 열거해 돌리는 것이다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : 게이트가 판정하는 파일 목록과 리소스 디렉터리의 .proto 목록 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-proto-contract#L203 에 있다. - -## 본문 - - - -게이트가 파일 목록을 하드코딩한다. - -```java -List files = List.of( - "proto/hyeonworks/grpc/common/v1/error.proto", - "proto/hyeonworks/grpc/common/v1/stream.proto"); -``` - -## 게이트가 하드코딩한 목록 - -:::evidence key="grpc-proto-contract-f03" alt="분석 문서 final/document.md#a20-grpc-proto-contract 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-proto-contract 발췌 — 15줄" zoom="true" -::: - -## 세 번째 파일은 판정되지 않는다 - -리소스 디렉터리를 훑지 않으므로, 이 리프에 세 번째 `.proto` 를 추가하면 이 테스트를 함께 고치기 전까지 판정되지 않고 빌드는 초록으로 남는다. - -## 같은 저장소가 이미 경계한 형태다 - -빠뜨림이 통과가 되는 게이트다. 수정은 `proto/**` 아래 `.proto` 를 전부 열거해 돌리는 것이다. - -## 확인하지 못한 것 - -세 번째 .proto 를 추가해 게이트가 침묵하는 것을 재현하지 않았다. 목록이 하드코딩이고 디렉터리를 훑지 않는다는 것으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-grpc-proto-contract-f04.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-grpc-proto-contract-f04.md deleted file mode 100644 index 069e8d7..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-grpc-proto-contract-f04.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -kind: CASE -slug: grpc-proto-contract-f04 -title: 열거형 안의 reserved 는 수집되지 않는다 -topic: schema-and-data-contracts -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-proto-contract-f04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-proto-contract-f04 - file: ../../../final/evidence/rendered/grpc-proto-contract-f04.svg -evidence: - - ../../../final/evidence/raw/grpc-proto-contract-f04.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-proto-contract#L265 이다. -module: grpc-proto-contract -priority: P3 ---- - -# 열거형 안의 reserved 는 수집되지 않는다 - -scan 은 스코프 종류로 갈라진다. reserved 수집은 scanMessageMember 안에만 있다. - -## 문제 - -scan 은 스코프 종류로 갈라진다. - -reserved 수집은 scanMessageMember 안에만 있다. - -## 결론 - -proto3 는 열거형에도 reserved 2, 15; 와 reserved "OLD_VALUE"; 를 허용하고, 열거형 값을 지울 때 번호를 예약하는 것은 필드와 같은 이유로 필요하다 — 예약하지 않고 재사용하면 옛 클라이언트가 보낸 정수가 다른 뜻으로 해석된다. - -지금 SchemaHistory 에 열거형 이름으로 삭제 이력을 넣으면, 스키마가 정확히 예약했더라도 scan.reservedNumbers 에 그 이름이 없으므로 RESERVED_HISTORY 오탐이 난다. - -§17.1 의 범위 문법 문제와 같은 방향(fail-closed)이고 같은 자리에서 고칠 수 있다. - -reserved 수집을 스코프 종류와 무관하게 먼저 시도한 뒤 나머지 판정을 갈래로 보낸다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : SchemaHistory 참조 10건 검색과 scan 의 스코프 분기별 reserved 수집 위치 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-proto-contract#L265 에 있다. - -## 본문 - - - -`scan` 은 스코프 종류로 갈라진다. `reserved` 수집은 `scanMessageMember` 안에만 있다. - -## SchemaHistory 참조 위치 - -:::evidence key="grpc-proto-contract-f04" alt="코드베이스에서 SchemaHistory 를 검색한 출력 10줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="SchemaHistory 코드베이스 검색 — 10줄 · exit 0" zoom="true" -::: - -## proto3 는 열거형에도 reserved 를 허용한다 - -`reserved 2, 15;` 와 `reserved "OLD_VALUE";` 다. 열거형 값을 지울 때 번호를 예약하는 것은 필드와 같은 이유로 필요하다 — 예약하지 않고 재사용하면 옛 클라이언트가 보낸 정수가 다른 뜻으로 해석된다. - -## 그래서 오탐이 난다 - -`SchemaHistory` 에 열거형 이름으로 삭제 이력을 넣으면, 스키마가 정확히 예약했더라도 `scan.reservedNumbers` 에 그 이름이 없으므로 `RESERVED_HISTORY` 오탐이 난다. - -## 같은 자리에서 고칠 수 있다 - -§17.1 의 범위 문법 문제와 같은 방향(fail-closed)이다. `reserved` 수집을 스코프 종류와 무관하게 먼저 시도한 뒤 나머지 판정을 갈래로 보낸다. - -## 확인하지 못한 것 - -열거형 reserved 오탐을 실행으로 재현하지 않았다. 스코프 분기 코드로 판정했다. 블록 주석 안의 선언이 스캔되는지도 확인하지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-messaging-observability-f04.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-messaging-observability-f04.md deleted file mode 100644 index bf42945..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-messaging-observability-f04.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -kind: CASE -slug: messaging-observability-f04 -title: 감사 sink 인터페이스가 사용처에서 다시 선언된다 -topic: schema-and-data-contracts -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-observability-f04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-observability-f04 - file: ../../../final/evidence/rendered/messaging-observability-f04.svg -evidence: - - ../../../final/evidence/raw/messaging-observability-f04.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-observability#L699 이다. -module: messaging-observability -priority: P3 ---- - -# 감사 sink 인터페이스가 사용처에서 다시 선언된다 - -MessagingAuditSink.record(MessagingAuditEvent)와 같은 시그니처를 RedriveService:208이 자기 중첩 인터페이스로 선언한다. messaging-admin-runtime은 messaging-observability에 의존할 수 있다(registry 확인). - -## 관계 - -- **타입이 문서화한 불변식은 타입이 강제한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 문제 - -MessagingAuditSink.record(MessagingAuditEvent)와 같은 시그니처를 RedriveService:208이 자기 중첩 인터페이스로 선언한다. - -messaging-admin-runtime은 messaging-observability에 의존할 수 있다(registry 확인). - -## 결론 - -MessagingAuditSink.inMemory()가 제공하는 구현을 admin-runtime이 쓸 수 없다. - -그리고 감사 sink의 계약(분리된 보존·접근·무결성 요구)이 문서화된 곳과 실제로 구현되는 곳이 다르다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : 두 인터페이스의 시그니처 대조와 registry 의 의존 허용 여부 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-observability#L699 에 있다. - -## 본문 - - - -`MessagingAuditSink.record(MessagingAuditEvent)`와 같은 시그니처를 `RedriveService:208`이 자기 중첩 인터페이스로 선언한다. `messaging-admin-runtime`은 `messaging-observability`에 의존할 수 있다(registry 확인). - -## 같은 시그니처가 선언된 두 곳 - -:::evidence key="messaging-observability-f04" alt="분석 문서 final/document.md#a19-messaging-observability 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a19-messaging-observability 발췌 — 15줄" zoom="true" -::: - -## 두 결과 - -`MessagingAuditSink.inMemory()`가 제공하는 구현을 admin-runtime이 쓸 수 없다. 그리고 감사 sink의 계약(분리된 보존·접근·무결성 요구)이 문서화된 곳과 실제로 구현되는 곳이 다르다. - -## 확인하지 못한 것 - -레닥션이 빠진 감사 이벤트가 실제로 기록되는 것을 관측하지 않았다. 시그니처 대조와 의존 선언으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-messaging-runtime-core-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-messaging-runtime-core-f03.md deleted file mode 100644 index 5e5bfcf..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-messaging-runtime-core-f03.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -kind: CASE -slug: messaging-runtime-core-f03 -title: 선언된 content type과 실제 인코딩이 조용히 갈라질 수 있다 -topic: schema-and-data-contracts -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-runtime-core-f03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-runtime-core-f03 - file: ../../../final/evidence/rendered/messaging-runtime-core-f03.svg -evidence: - - ../../../final/evidence/raw/messaging-runtime-core-f03.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-runtime-core#L727 이다. -module: messaging-runtime-core -priority: P3 ---- - -# 선언된 content type과 실제 인코딩이 조용히 갈라질 수 있다 - -encode가 codecs.find(message.contentType()).orElseGet(codecs::defaultCodec)으로 폴백한다. 출하 registry에는 JSON codec 하나만 등록된다. - -## 관계 - -- **만들어 두고 흘리지 않는 진단값은 진단이 아니다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **증가한다고 문서화한 값이 리터럴이면 그 사실을 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **안정 코드는 운영자의 행동이 갈리는 지점마다 나눈다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **`CompletionStage`를 반환하는 메서드는 동기적으로 던지지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 문제 - -encode가 codecs.find(message.contentType()).orElseGet(codecs::defaultCodec)으로 폴백한다. - -출하 registry에는 JSON codec 하나만 등록된다. - -## 결론 - -봉투가 application/avro를 선언해도 JSON으로 인코딩되고, EncodedMessage의 content type은 codec이 정하므로 application/json이 된다. - -실패하지 않고 다른 포맷으로 성공한다. - -소비 측이 봉투의 원래 선언을 믿고 디코더를 고르면 어긋난다. - -DestinationProfile.schema().codec()이 목적지의 codec을 선언하는데 그 값과 대조하는 코드가 이 경로에 없다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : encode 의 폴백 코드와 출하 registry 에 등록되는 codec 목록 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-runtime-core#L727 에 있다. - -## 본문 - - - -`encode`가 `codecs.find(message.contentType()).orElseGet(codecs::defaultCodec)`으로 폴백한다. 출하 registry에는 JSON codec 하나만 등록된다. - -## encode 의 폴백과 출하 registry - -:::evidence key="messaging-runtime-core-f03" alt="분석 문서 final/document.md#a19-messaging-runtime-core 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a19-messaging-runtime-core 발췌 — 15줄" zoom="true" -::: - -## 실패하지 않고 다른 포맷으로 성공한다 - -봉투가 `application/avro`를 선언해도 JSON으로 인코딩되고, `EncodedMessage`의 content type은 codec이 정하므로 `application/json`이 된다. - -## 소비 측이 원래 선언을 믿으면 어긋난다 - -`DestinationProfile.schema().codec()`이 목적지의 codec을 선언하는데 그 값과 대조하는 코드가 이 경로에 없다. - -## 확인하지 못한 것 - -실제 브로커로 선언과 다른 포맷이 나가는 것을 관측하지 않았다. 폴백 코드와 등록 목록의 대조로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-messaging-schema-avro-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-messaging-schema-avro-f02.md deleted file mode 100644 index 4b604bd..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-messaging-schema-avro-f02.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -kind: CASE -slug: messaging-schema-avro-f02 -title: 진화 판단이 두 곳에 있고 형태가 반대다 -topic: schema-and-data-contracts -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-schema-avro-f02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-schema-avro-f02 - file: ../../../final/evidence/rendered/messaging-schema-avro-f02.svg - - key: messaging-schema-avro-f02-diagram - file: ../../../final/assets/diagrams/messaging-schema-avro-f02.svg -evidence: - - ../../../final/evidence/raw/messaging-schema-avro-f02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-schema-avro#L533 이다. -module: messaging-schema-avro -priority: P2 ---- - -# 진화 판단이 두 곳에 있고 형태가 반대다 - -isTransitive는 SchemaCompatibilityValidator(public static)와 이 leaf(private static)에 글자까지 같은 복사본이 있다. 방향 판정은 전자가 허용목록, 후자가 거부목록이다. - -## 관계 - -- **모드 enum을 분기 조건으로 쓰면 각 분기에 테스트를 둔다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **컬렉션 순서가 계약이면 양쪽에서 같은 방향으로 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **안정 코드는 판단 단위로 정하고 구현 단위로 정하지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 문제 - -isTransitive는 SchemaCompatibilityValidator(public static)와 이 leaf(private static)에 글자까지 같은 복사본이 있다. - -방향 판정은 전자가 허용목록, 후자가 거부목록이다. - -## 결론 - -오늘 7개 모드에서 결과는 같지만 형태가 반대이므로 SchemaCompatibility에 값이 추가되는 순간 갈라진다 — 허용목록은 "검사 안 함", 거부목록은 "양방향 검사". - -그리고 이 중복은 schema-api의 javadoc이 명시적으로 막으려던 것이다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : SchemaCompatibilityValidator 참조 11건 검색과 두 구현의 판정 방향(허용목록·거부목록) 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-schema-avro#L533 에 있다. - -## 본문 - - - -`isTransitive`는 `SchemaCompatibilityValidator`(public static)와 이 leaf(private static)에 글자까지 같은 복사본이 있다. - -## 판정이 갈리는 자리 - -:::evidence key="messaging-schema-avro-f02-diagram" alt="포트 판정 쪽에 isTransitive 복사본과 허용목록 방향이 놓이고 게이트 판정 쪽에 같은 복사본과 거부목록 방향이 빗금으로 놓인다" caption="판정이 갈리는 자리" zoom="false" -::: - -방향 판정은 전자가 허용목록, 후자가 거부목록이다. - -## SchemaCompatibilityValidator 참조 위치 - -:::evidence key="messaging-schema-avro-f02" alt="코드베이스에서 SchemaCompatibilityValidator 를 검색한 출력 11줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="SchemaCompatibilityValidator 코드베이스 검색 — 11줄 · exit 0" zoom="true" -::: - -## 값이 추가되는 순간 갈라진다 - -오늘 7개 모드에서 결과는 같지만 형태가 반대이므로 `SchemaCompatibility`에 값이 추가되면 허용목록은 "검사 안 함", 거부목록은 "양방향 검사"가 된다. 그리고 이 중복은 schema-api의 javadoc이 명시적으로 막으려던 것이다. - -## 확인하지 못한 것 - -실제 Avro 스키마 진화 사례에서 두 판정이 갈리는지 확인하지 않았다. 테스트는 defaulted 필드 추가·미추가 두 경우만 본다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-messaging-schema-json-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-messaging-schema-json-f01.md deleted file mode 100644 index 697ba90..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-messaging-schema-json-f01.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -kind: CASE -slug: messaging-schema-json-f01 -title: 포맷 중립 payload 정책이, 자기 상수를 두고 JSON codec의 상수를 참조한다 -topic: schema-and-data-contracts -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-schema-json-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-schema-json-f01 - file: ../../../final/evidence/rendered/messaging-schema-json-f01.svg - - key: messaging-schema-json-f01-diagram - file: ../../../final/assets/diagrams/messaging-schema-json-f01.svg -evidence: - - ../../../final/evidence/raw/messaging-schema-json-f01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-schema-json#L463 이다. -module: messaging-schema-json -priority: P2 ---- - -# 포맷 중립 payload 정책이, 자기 상수를 두고 JSON codec의 상수를 참조한다 - -MessagingCoreAutoConfiguration:410-413이 new PayloadPolicy(JacksonMessageCodec.DEFAULT_MAX_BYTES, JacksonMessageCodec.DEFAULT_MAX_BYTES / 2)를 만든다. 그런데 PayloadPolicy 자신이 같은 값의 public 상수 PayloadPolicy.DEFAULT_MAX_BYTES(messaging-policy/PayloadPolicy.java:17)를 갖고 있다. - -## 관계 - -- **실패 코드는 운영자의 다음 행동이 갈리는 지점마다 나눈다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 문제 - -MessagingCoreAutoConfiguration:410-413이 new PayloadPolicy(JacksonMessageCodec.DEFAULT_MAX_BYTES, JacksonMessageCodec.DEFAULT_MAX_BYTES / 2)를 만든다. - -그런데 PayloadPolicy 자신이 같은 값의 public 상수 PayloadPolicy.DEFAULT_MAX_BYTES(messaging-policy/PayloadPolicy.java:17)를 갖고 있다. - -## 결론 - -MessagingAdmissionController는 목적지의 codec이 무엇이든 지나는 관문이다. - -그 상한이 한 포맷 클래스의 상수에서 나오면 두 가지가 깨진다. - -(1) @ConditionalOnMissingBean이 허용하는 대로 애플리케이션이 자기 MessageCodecRegistry를 내놓아 JSON codec을 대체해도, 정책은 여전히 JSON codec의 값을 읽는다. - -(2) 다섯 곳의 리터럴 중 하나만 바뀌면 조용히 갈라지고, RawBytesMessageCodec javadoc이 이미 "shared with the Stable codecs"라고 사실과 다르게 부르고 있다. - -정책 소유자가 이미 존재하는데 배선이 그것을 지나쳤다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : PayloadPolicy 참조 25건 검색과 정책이 읽는 상수의 소유 클래스 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-schema-json#L463 에 있다. - -## 본문 - - - -`MessagingCoreAutoConfiguration:410-413`이 `new PayloadPolicy(JacksonMessageCodec.DEFAULT_MAX_BYTES, JacksonMessageCodec.DEFAULT_MAX_BYTES / 2)`를 만든다. 그런데 `PayloadPolicy` 자신이 같은 값의 public 상수 `PayloadPolicy.DEFAULT_MAX_BYTES`(`messaging-policy/PayloadPolicy.java:17`)를 갖고 있다. - -## 참조가 뒤집힌 자리 - -:::evidence key="messaging-schema-json-f01-diagram" alt="JacksonMessageCodec 상수만 정책이 읽는 상수 안에 놓이고 PayloadPolicy 자기 상수가 바깥에 빗금으로 놓인다" caption="참조가 뒤집힌 자리" zoom="false" -::: - -`MessagingAdmissionController`는 목적지의 codec이 무엇이든 지나는 관문이다. - -## PayloadPolicy 참조 위치 - -:::evidence key="messaging-schema-json-f01" alt="코드베이스에서 PayloadPolicy 를 검색한 출력 25줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="PayloadPolicy 코드베이스 검색 — 25줄 · exit 0" zoom="true" -::: - -## 한 포맷 클래스의 상수에서 나오면 둘이 깨진다 - -(1) `@ConditionalOnMissingBean`이 허용하는 대로 애플리케이션이 자기 `MessageCodecRegistry`를 내놓아 JSON codec을 대체해도, 정책은 여전히 JSON codec의 값을 읽는다. (2) 다섯 곳의 리터럴 중 하나만 바뀌면 조용히 갈라지고, `RawBytesMessageCodec` javadoc이 이미 "shared with the Stable codecs"라고 사실과 다르게 부르고 있다. 정책 소유자가 이미 존재하는데 배선이 그것을 지나쳤다. - -## 확인하지 못한 것 - -실제 배포에서 MessageContracts bean 이 채워지는지 확인하지 못했다. 그 답은 starter 리프가 소유한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-messaging-schema-protobuf-f04.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-messaging-schema-protobuf-f04.md deleted file mode 100644 index 5050cb2..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/case/case-messaging-schema-protobuf-f04.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -kind: CASE -slug: messaging-schema-protobuf-f04 -title: registry 조회 로직이 세 codec에 복제돼 있다 -topic: schema-and-data-contracts -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-schema-protobuf-f04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-schema-protobuf-f04 - file: ../../../final/evidence/rendered/messaging-schema-protobuf-f04.svg -evidence: - - ../../../final/evidence/raw/messaging-schema-protobuf-f04.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-schema-protobuf#L554 이다. -module: messaging-schema-protobuf -priority: P3 ---- - -# registry 조회 로직이 세 codec에 복제돼 있다 - -requireRegistered(JSON/Protobuf)와 schemaFor(Avro)가 같은 3단 판단을 각자 구현한다. JSON과 Protobuf는 registeredVersions 헬퍼까지 사실상 동일하다. - -## 관계 - -- **검증되지 않는 스키마 파일은 문서임을 파일 안에 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **신뢰할 수 없는 입력 쪽 경계를 먼저 테스트한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 문제 - -requireRegistered(JSON/Protobuf)와 schemaFor(Avro)가 같은 3단 판단을 각자 구현한다. - -JSON과 Protobuf는 registeredVersions 헬퍼까지 사실상 동일하다. - -## 결론 - -판단은 MessageContractKey의 성질이지 포맷의 성질이 아니다. - -그리고 실제로 갈라졌다 — Avro만 AVRO_ 접두 코드를 쓰고 등록 버전 목록을 메시지에 넣지 않는다. - -messaging-schema-api가 흡수할 수 있는 형태다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : MessageContractKey 참조 34건 검색과 세 codec 의 조회 메서드 본문 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-schema-protobuf#L554 에 있다. - -## 본문 - - - -`requireRegistered`(JSON/Protobuf)와 `schemaFor`(Avro)가 같은 3단 판단을 각자 구현한다. JSON과 Protobuf는 `registeredVersions` 헬퍼까지 사실상 동일하다. - -## MessageContractKey 참조 위치 - -:::evidence key="messaging-schema-protobuf-f04" alt="코드베이스에서 MessageContractKey 를 검색한 출력 34줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessageContractKey 코드베이스 검색 — 34줄 · exit 0" zoom="true" -::: - -## 판단은 키의 성질이지 포맷의 성질이 아니다 - -그리고 실제로 갈라졌다 — Avro만 `AVRO_` 접두 코드를 쓰고 등록 버전 목록을 메시지에 넣지 않는다. `messaging-schema-api`가 흡수할 수 있는 형태다. - -## 확인하지 못한 것 - -파생 프로젝트가 이 codec 을 쓰는지 확인할 수 없었다. 세 구현의 코드 대조로만 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/question/openquestion-messaging-cloudevents-f04.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/question/openquestion-messaging-cloudevents-f04.md deleted file mode 100644 index 6b8241b..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/question/openquestion-messaging-cloudevents-f04.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -kind: QUESTION -slug: messaging-cloudevents-f04 -title: dataschema가 채워질 경로가 없다 -topic: schema-and-data-contracts -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: open-question:messaging-cloudevents-f04 -questionStatus: OPEN -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-cloudevents#L547 ---- - -# dataschema가 채워질 경로가 없다 - -`dataschema` 를 채우는 코드는 있고 그 값을 만들어 주는 경로가 없다. 이 프로파일이 만드는 CloudEvent 는 스키마 위치를 알리지 않는다. - -## 관계 - -- **왕복이라 부르는 테스트는 무엇을 보존하지 않는지도 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **문서가 범위를 제한하면 그 제한을 누가 강제하는지 같이 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 사실 - -toCloudEvent 가 encoded.schemaReference().flatMap(SchemaReference::schemaUri).ifPresent(builder::withDataSchema) 로 dataschema 를 채운다. - -세 codec(JSON · Avro · Protobuf)이 모두 SchemaReference.of(subject, version) 로 참조를 만들고, 그 factory 는 schemaUri 를 Optional.empty() 로 둔다. - -dataschema 는 CloudEvents 소비자가 페이로드를 해석하는 데 쓰는 표준 속성이다. schemaversion 확장이 그 자리를 대신하지만 그것은 비표준 확장이다. - -## 미지수 - -이 저장소가 외부 schema registry 를 쓸 것인가. messaging-schema-api 의 SchemaRegistry port 가 구현 0 인 것과 같은 뿌리다. - -## 선택지 - -registry URI 를 갖는 배포에서 3인자 생성자를 쓴다 - dataschema 가 채워지고 표준 속성이 제 역할을 한다. - -현재 도달 불가임을 주석으로 남긴다 - 분기를 지우지 않고 그것이 실행되지 않는 이유를 코드 옆에 둔다. - -## 다음 검증 - -git grep -n 'new SchemaReference(' -- 'src/messaging/**/*.java' 로 3인자 생성자를 부르는 production 코드가 있는지 확인한다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/question/openquestion-messaging-schema-protobuf-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/question/openquestion-messaging-schema-protobuf-f03.md deleted file mode 100644 index be747d8..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/question/openquestion-messaging-schema-protobuf-f03.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -kind: QUESTION -slug: messaging-schema-protobuf-f03 -title: protobuf-java 버전이 저장소에 셋이고 전역 정책이 없다 -topic: schema-and-data-contracts -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: open-question:messaging-schema-protobuf-f03 -questionStatus: OPEN -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-schema-protobuf#L545 ---- - -# protobuf-java 버전이 저장소에 셋이고 전역 정책이 없다 - -세 버전이 오늘 한 classpath 를 공유하지 않아 사고가 아니다. 이 leaf 를 런타임에 편입하는 순간 버전 판정이 필요해지고, 그때 참조할 전역 정책이 없다. - -## 관계 - -- **검증되지 않는 스키마 파일은 문서임을 파일 안에 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **신뢰할 수 없는 입력 쪽 경계를 먼저 테스트한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 사실 - -ext.protobufVersion = 3.25.5(grpc 모듈 범위로 한정), 이 leaf 4.29.3, websocket 4.33.2. lockfile 들이 세 값을 모두 고정한다. - -근거 위치는 src/build.gradle:174-180 · messaging-schema-protobuf/build.gradle:9 · adapter/inbound/websocket/build.gradle:44,46 와 각 gradle.lockfile 이다. - -이 leaf 의 runtime_memberships 가 [] 이라 세 버전이 한 classpath 를 공유하지 않는다. 채택 시점의 부채다. - -src/build.gradle 의 "the single SSOT" 라는 표현이 전역 정책의 존재를 시사하는데, 실제 범위는 그 문장 안에서 grpc 모듈로 한정된다. - -## 미지수 - -이 leaf 를 런타임에 편입할 것인가. 저장소 안에 답이 없다. - -## 선택지 - -편입 전까지 현 상태를 유지한다 - src/messaging/CLAUDE.md 에 "편입 시 버전 정합을 먼저 판정한다" 를 적어 판정 시점을 예약한다. - -ext.protobufVersion 의 범위를 넓힌다 - 주석의 "single SSOT" 표현이 실제 범위와 맞아진다. - -## 다음 검증 - -git grep -n 'protobuf-java\|protobufVersion' -- src --include='*.gradle' 로 세 값은 다시 확정된다. 편입 여부의 결정은 그 검색으로 답해지지 않는다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/reference/reference-messaging-schema-avro-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/reference/reference-messaging-schema-avro-f03.md deleted file mode 100644 index 7ea4646..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/reference/reference-messaging-schema-avro-f03.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-schema-avro-f03 -title: 컬렉션 순서가 계약이면 양쪽에서 같은 방향으로 적는다 -topic: schema-and-data-contracts -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-schema-avro-f03 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-schema-avro#L542 ---- - -# 컬렉션 순서가 계약이면 양쪽에서 같은 방향으로 적는다 - -## 관계 - -- **진화 판단이 두 곳에 있고 형태가 반대다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **CI에서 돈다고 선언한 게이트를 부르는 CI가 없다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. - -## 목적 - -둘을 잇는 코드가 없어 지금은 무해하다. 이으면서 reversed() 를 빠뜨리면 pairwise 모드가 가장 오래된 스키마를 직전 버전으로 비교한다. 실패하지 않고 통과할 수 있는 오류라는 점이 이 규칙의 이유다. - -## 규칙 - -1. 같은 리스트를 주고받는 두 쪽의 순서 문장을 대조한다 - SchemaRegistry.history javadoc 은 oldest first, AvroCompatibilityGate.check 의 @param history 는 newest first 다. - -2. 위험이 이미 문서화돼 있는지 본다 - port javadoc 이 그 위험을 직접 적는다 — "an ordering mistake here silently converts a transitive check into a pairwise one." - -3. 한 방향으로 통일하고 뒤집기를 안쪽으로 넣는다 - 게이트가 oldest-first 를 받고 내부에서 뒤집는 형태다. - -## 적용 조건 - -정렬된 컬렉션이 port 와 그 소비자 사이를 오가고, 순서가 결과를 바꾸는 모든 경계. - -## 예외 - -SSOT 가 이 규칙의 반례를 적지 않았다. 두 방향이 의도적이라면 변환 지점이 코드로 존재해야 하는데, 지금은 잇는 코드 자체가 없다. - -## 예시 - -두 javadoc 의 순서 문장. 확인 방법은 그 둘을 나란히 대조하는 것이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/reference/reference-messaging-schema-avro-f05.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/reference/reference-messaging-schema-avro-f05.md deleted file mode 100644 index fcc03ac..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-data-contracts/reference/reference-messaging-schema-avro-f05.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-schema-avro-f05 -title: 안정 코드는 판단 단위로 정하고 구현 단위로 정하지 않는다 -topic: schema-and-data-contracts -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-schema-avro-f05 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-schema-avro#L560 ---- - -# 안정 코드는 판단 단위로 정하고 구현 단위로 정하지 않는다 - -## 관계 - -- **진화 판단이 두 곳에 있고 형태가 반대다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **CI에서 돈다고 선언한 게이트를 부르는 CI가 없다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. - -## 목적 - -FailureDescriptor.code 는 "stable, machine-readable code" 이고 대시보드와 재시도 정책이 그것으로 집계한다. 같은 판단이 두 어휘로 나뉘면 Avro 만 별도 계열이 되고, 집계는 포맷 수만큼 갈라진다. - -## 규칙 - -1. 같은 판단에 형제 구현들이 어떤 코드를 쓰는지 모은다 - 미등록 타입과 미등록 버전이라는 같은 판단에 JSON 과 Protobuf 는 UNKNOWN_MESSAGE_TYPE 과 SCHEMA_VERSION_NOT_REGISTERED 를, Avro 는 AVRO_TYPE_NOT_REGISTERED 와 AVRO_VERSION_NOT_REGISTERED 를 쓴다. - -2. 코드가 갈라진 이유가 판단인지 구현인지 묻는다 - 포맷 이름이 접두로 붙은 것은 구현 단위다. - -3. 포맷 구분은 코드가 아니라 메시지로 옮긴다 - 공통 코드를 쓰고 sanitizedMessage 로 포맷을 구분한다. - -## 적용 조건 - -여러 구현이 같은 port 를 구현하면서 각자 실패 코드를 정하는 모든 가족. - -## 예외 - -판단 자체가 그 구현에만 존재하는 경우는 고유 코드가 맞다. 여기 두 판단은 세 codec 모두에 있다. - -## 예시 - -세 codec 의 requireRegistered 와 schemaFor. 확인 방법은 git grep -n 'NOT_REGISTERED' -- 'src/messaging/**/*.java' 다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-inbound-graphql-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-inbound-graphql-c02.md deleted file mode 100644 index 06ee9ba..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-inbound-graphql-c02.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-graphql-c02 -title: 스키마 해시의 생산자도 소비자도 빈이 없다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-graphql-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-graphql-c02 - file: ../../../final/evidence/rendered/adapter-inbound-graphql-c02.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-graphql-c02.txt -source: - - 원본 분석 절은 final/document.md#a16#L221 이다. -module: adapter-inbound-graphql ---- - -# 스키마 해시의 생산자도 소비자도 빈이 없다 - -`GraphQlSchemaHash`의 유일한 생산 경로와 그 소비처가 둘 다 프로덕션 호출자를 갖지 않는다. - -## 본문 - - - -`GraphQlSchemaHash`의 유일한 생산 경로는 `GraphQlSchemaAssemblyResult.schemaHash()`(`:94-95`)이고, 그 결과 타입은 `GraphQlSchemaAssembler.assemble(...)`만 만든다. 둘 다 프로덕션 호출자가 없다(§7.1). - -## GraphQlSchemaHash 참조 위치 - -:::evidence key="adapter-inbound-graphql-c02" alt="코드베이스에서 GraphQlSchemaHash 를 검색한 출력 20줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GraphQlSchemaHash 코드베이스 검색 — 20줄 · exit 0" zoom="true" -::: - -## 소비 쪽도 빈이 없다 - -소비 쪽은 `GraphQlPlatformActuatorEndpoint`가 생성자로 받는다. 그런데 `GraphQlPlatformAutoConfiguration`의 39개 `@Bean` 중 이것을 만드는 것이 없다. §8.1. - -*(이 파일은 `autoconfigure` 패키지에 있어 sub-scope 01의 분모에 포함된다. 스키마 해시 사슬의 소비 쪽이므로 여기서 함께 다룬다.)* - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-inbound-graphql-c07.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-inbound-graphql-c07.md deleted file mode 100644 index 22c98d2..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-inbound-graphql-c07.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-graphql-c07 -title: 파싱·검증 실패의 와이어 형식은 이 플랫폼이 정하지 않는다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-graphql-c07 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-graphql-c07 - file: ../../../final/evidence/rendered/adapter-inbound-graphql-c07.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-graphql-c07.txt -source: - - 원본 분석 절은 final/document.md#a16#L604 이다. -module: adapter-inbound-graphql ---- - -# 파싱·검증 실패의 와이어 형식은 이 플랫폼이 정하지 않는다 - -`GraphQlRequestErrorMapper`의 진단은 정확하지만 배선된 것은 리졸버 쪽뿐이다. - -## 본문 - - - -`GraphQlRequestErrorMapper`의 javadoc이 자기 존재 이유를 적는다. 진단이 정확하고, 배선된 것은 리졸버 쪽(`GraphQlExceptionResolver`, autoconf=4)뿐이다. 파싱·검증 실패의 와이어 형식을 이 플랫폼이 정하지 않는다는 뜻이다. - -## GraphQlRequestErrorMapper 참조 위치 - -:::evidence key="adapter-inbound-graphql-c07" alt="코드베이스에서 GraphQlRequestErrorMapper 를 검색한 출력 6줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GraphQlRequestErrorMapper 코드베이스 검색 — 6줄 · exit 0" zoom="true" -::: - -## 플랫폼이 거부하는 실패는 안정 코드로 매핑된다 - -`runtime/GraphQlWireErrorMapper`(autoconf=4)와 `runtime/GraphQlPlatformRejectionMapper`(main_other=3)가 배선돼 있어 익명 연산, 복잡도 초과 등은 안정 코드로 매핑된다. 덮이지 않는 것은 graphql-java 자신이 만드는 구문/검증 오류다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-inbound-graphql-c11.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-inbound-graphql-c11.md deleted file mode 100644 index 67633a5..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-inbound-graphql-c11.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-graphql-c11 -title: 키를 요구하는 코드는 둘이고 서명하는 코드는 없다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-graphql-c11 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-graphql-c11 - file: ../../../final/evidence/rendered/adapter-inbound-graphql-c11.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-graphql-c11.txt -source: - - 원본 분석 절은 final/document.md#a16#L725 이다. -module: adapter-inbound-graphql ---- - -# 키를 요구하는 코드는 둘이고 서명하는 코드는 없다 - -`backend.graphql.cursor.key-ids`를 읽는 프로덕션 코드는 시작 검증기와 액추에이터 둘이다. 그 키로 커서에 서명하는 코드는 없다. - -## 본문 - - - -`backend.graphql.cursor.key-ids`를 읽는 프로덕션 코드는 둘이다. - -```text -autoconfigure/GraphQlPlatformStartupValidator.java:42-43 - if (properties.production() && properties.cursor().keyIds().isEmpty()) { - problems.add("a cursor signing key is required; unsigned cursors are client-editable"); - -autoconfigure/GraphQlPlatformActuatorEndpoint.java (보고서에 포함) -``` - -## HmacGraphQlCursorCodec 참조 위치 - -:::evidence key="adapter-inbound-graphql-c11" alt="코드베이스에서 HmacGraphQlCursorCodec 를 검색한 출력 16줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="HmacGraphQlCursorCodec 코드베이스 검색 — 16줄 · exit 0" zoom="true" -::: - -## 서명하는 코드가 없다 - -`HmacGraphQlCursorCodec`과 `GraphQlCursorKeyRing`은 autoconf=0 · main_other=0이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-inbound-web-c08.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-inbound-web-c08.md deleted file mode 100644 index 61fb129..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-inbound-web-c08.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-web-c08 -title: 페이지네이션 어휘가 한 번도 컨트롤러에 붙어 본 적이 없다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-web-c08 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-web-c08 - file: ../../../final/evidence/rendered/adapter-inbound-web-c08.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-web-c08.txt -source: - - 원본 분석 절은 final/document.md#a14#L843 이다. -module: adapter-inbound-web ---- - -# 페이지네이션 어휘가 한 번도 컨트롤러에 붙어 본 적이 없다 - -다섯 패키지 중 소비 모듈이 실제로 부르는 것은 `ETags` 하나다. 나머지는 전부 테스트 전용이다. - -## 본문 - - - -다섯 패키지 중 소비 모듈이 실제로 부르는 것은 `ETags` 하나다 — `sample-portfolio`의 `WorkLogController`가 세 곳에서 쓴다(`:144` `If-None-Match` 비교, `:213` 버전에서 약한 ETag 생성, `:226` `If-Match` 검사). 나머지는 전부 테스트 전용이다. - -## WorkLogController 참조 위치 - -:::evidence key="adapter-inbound-web-c08" alt="코드베이스에서 WorkLogController 를 검색한 출력 18줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="WorkLogController 코드베이스 검색 — 18줄 · exit 0" zoom="true" -::: - -## 라이브러리인데도 기록해 두는 이유 - -라이브러리이므로 그 자체가 결함은 아니지만, 페이지네이션 어휘 19개 파일·버전 관리 7개 파일이 **한 번도 컨트롤러에 붙어 본 적이 없다**는 사실은 기록해 둘 값이 있다 — 이 저장소가 다른 곳에서 "타입은 있고 호출자가 없다"를 반복해서 결함으로 취급했기 때문이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-inbound-web-c10.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-inbound-web-c10.md deleted file mode 100644 index 06dfea2..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-inbound-web-c10.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-web-c10 -title: 어느 쪽이 정본인지 코드로 판정할 수 없다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-web-c10 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-web-c10 - file: ../../../final/evidence/rendered/adapter-inbound-web-c10.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-web-c10.txt -source: - - 원본 분석 절은 final/document.md#a14#L889 이다. -module: adapter-inbound-web ---- - -# 어느 쪽이 정본인지 코드로 판정할 수 없다 - -커서 코덱이 두 벌이고 `WebStableModule`이 둘을 다른 모듈로 선언한다. 둘 다 프로덕션 소비자가 없다. - -## 본문 - - - -커서 코덱이 두 벌이다 — `pagination/WebCursorCodec`(인터페이스) + `pagination/HmacWebCursorCodec`(135, 서명된 구현) + `pagination/WebCursorPayload` + `pagination/WebCursorKeyRing`, 그리고 별도로 `cursor/CursorCodec`(100) + `cursor/CursorException`. `WebStableModule`은 둘을 다른 모듈로 선언한다(`CURSOR` = "Opaque keyset cursor encoding and its failure type", `PAGINATION`). - -## WebStableModule 참조 위치 - -:::evidence key="adapter-inbound-web-c10" alt="코드베이스에서 WebStableModule 를 검색한 출력 14줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="WebStableModule 코드베이스 검색 — 14줄 · exit 0" zoom="true" -::: - -## 코드로 정본을 판정할 수 없다 - -둘 다 프로덕션 소비자가 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-inbound-web-c11.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-inbound-web-c11.md deleted file mode 100644 index d903074..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-inbound-web-c11.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-web-c11 -title: 기여자가 커스터마이저에 도달하지 않아 스키마가 문서에 없다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-web-c11 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-web-c11 - file: ../../../final/evidence/rendered/adapter-inbound-web-c11.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-web-c11.txt -source: - - 원본 분석 절은 final/document.md#a14#L978 이다. -module: adapter-inbound-web ---- - -# 기여자가 커스터마이저에 도달하지 않아 스키마가 문서에 없다 - -배선된 `OpenApiCustomizer`는 익명 람다 하나이고, 607줄짜리 미배선 쪽이 담은 기여자들은 커스터마이저에 도달하지 않는다. - -## 본문 - - - -배선된 것은 `config/OpenApiContractConfig`(37줄, `@Configuration`)가 익명 람다 `OpenApiCustomizer` 하나를 빈으로 등록하는 것뿐이다. 하는 일은 `ApiError.details` 스키마를 `ObjectSchema`로 되돌리는 것 한 가지다. - -## OpenApiCustomizer 참조 위치 - -:::evidence key="adapter-inbound-web-c11" alt="코드베이스에서 OpenApiCustomizer 를 검색한 출력 7줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="OpenApiCustomizer 코드베이스 검색 — 7줄 · exit 0" zoom="true" -::: - -## 배선되지 않은 607줄 - -`openapi/WebOpenApiCustomizer`(74줄)와 그것이 쓰는 `ProblemSchemaContributor`(88) · `CursorSchemaContributor`(47) · `WebOpenApiProfile`(72) · `WebOpenApiBreakingPolicy`(187) · `WebOpenApiReleaseGate`(100) · `WebOpenApiDiffResult`(39). 빈으로 등록하는 코드가 main·app-bootstrap에 없고, 참조는 자기들끼리와 테스트뿐이다. - -## 문서에 기여되지 않는 스키마 - -springdoc이 생성하는 문서에는 RFC 9457 problem 스키마도 커서 스키마도 기여되지 않는다 — 그 기여자들이 커스터마이저에 도달하지 않기 때문이다. SS2에서 확인한 "problem 계약이 문서에 없다"(§7.3)와 같은 방향의 사실이 스키마 쪽에서도 성립한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-inbound-websocket-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-inbound-websocket-c03.md deleted file mode 100644 index b4095af..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-inbound-websocket-c03.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-websocket-c03 -title: 오류 형식이 세 벌이다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-websocket-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-websocket-c03 - file: ../../../final/evidence/rendered/adapter-inbound-websocket-c03.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-websocket-c03.txt -source: - - 원본 분석 절은 final/document.md#a17#L267 이다. -module: adapter-inbound-websocket ---- - -# 오류 형식이 세 벌이다 - -`WebSocketFailureCategory`(main_other=10)가 이 sub-scope에서 가장 널리 참조되는 타입인데, 실제 STOMP 오류는 세 번째 형식이 만든다. - -## 본문 - - - -`error` 패키지의 `WebSocketFailureCategory`(main_other=10)가 이 sub-scope에서 가장 널리 참조되는 타입이고, `WebSocketErrorMessage`(75, main 참조 0)와 `WebSocketErrorTransport`가 그것을 전송으로 옮긴다. - -## WebSocketFailureCategory 참조 위치 - -:::evidence key="adapter-inbound-websocket-c03" alt="코드베이스에서 WebSocketFailureCategory 를 검색한 출력 15줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="WebSocketFailureCategory 코드베이스 검색 — 15줄 · exit 0" zoom="true" -::: - -## 실제 STOMP 오류를 만드는 세 번째 형식 - -`stomp/SafeStompSubProtocolErrorHandler`(32)가 만든다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-inbound-websocket-c04.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-inbound-websocket-c04.md deleted file mode 100644 index 3d0c0ad..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-inbound-websocket-c04.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-websocket-c04 -title: 키를 요구하는 검증기가 없어서 잘못된 확인 신호도 없다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-websocket-c04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-websocket-c04 - file: ../../../final/evidence/rendered/adapter-inbound-websocket-c04.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-websocket-c04.txt -source: - - 원본 분석 절은 final/document.md#a17#L381 이다. -module: adapter-inbound-websocket ---- - -# 키를 요구하는 검증기가 없어서 잘못된 확인 신호도 없다 - -`ResumeTokenCodec`(217) + `ResumeTokenKeyRing`(87) 조합은 graphql §24.1의 커서 코덱 조합과 같은 형태인데, 차이가 하나 있다. - -## 본문 - - - -`ResumeTokenCodec`(217) + `ResumeTokenKeyRing`(87)이 서명된 재개 토큰을 만든다. 이 조합은 graphql §24.1의 `HmacGraphQlCursorCodec` + `GraphQlCursorKeyRing`과 같은 형태다. - -## ResumeTokenCodec 참조 위치 - -:::evidence key="adapter-inbound-websocket-c04" alt="코드베이스에서 ResumeTokenCodec 를 검색한 출력 26줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ResumeTokenCodec 코드베이스 검색 — 26줄 · exit 0" zoom="true" -::: - -## 잘못된 확인 신호가 없는 이유 - -**차이는 이쪽에는 그 키를 요구하는 시작 검증기가 없다는 것** — 즉 "키를 요구하고 서명하지 않는" 잘못된 확인 신호가 없다. 면책 목록에 replay/resume이 있으므로 문서·코드·검증이 일치한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-cache-redis-c07.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-cache-redis-c07.md deleted file mode 100644 index fdad730..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-cache-redis-c07.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-cache-redis-c07 -title: guard 아래에서는 명령 이름으로 위험을 다시 유도할 수 없다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-cache-redis-c07 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-cache-redis-c07 - file: ../../../final/evidence/rendered/adapter-outbound-cache-redis-c07.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-cache-redis-c07.txt -source: - - 원본 분석 절은 final/document.md#a10#L350 이다. -module: adapter-outbound-cache-redis ---- - -# guard 아래에서는 명령 이름으로 위험을 다시 유도할 수 없다 - -`RedisCommandDescriptor`가 공식 서버 메타데이터와 조직 정책의 접합점이고, 생성자가 그 접합의 모순 네 가지를 거부한다. - -## 본문 - - - -`RedisCommandDescriptor`는 "the join between official server metadata and organization policy"이고, 그 아래를 못박는다 — "Nothing downstream of the guard is allowed to re-derive risk, access, or timeout from a command name." 생성자가 그 접합의 모순 네 가지를 거부한다. - -| 불변식 | 의미 | -|---|---| -| `BLOCKED` ⇒ `access == NONE` | 차단된 명령은 ACL 계정을 갖지 않는다 | -| `R4` ⇒ `BLOCKED` | 최고 위험은 반드시 차단 | -| `R3` ⇒ `ADMIN_ONLY` 또는 `BLOCKED` | 관리 위험은 애플리케이션에 열리지 않는다 | -| 쓰기 ∧ `retrySafe` ∧ `mayBeAmbiguous` ⇒ 거부 | 모호할 수 있는 쓰기를 재시도 안전으로 선언 불가 | - -마지막 하나가 §23의 런타임 불변식과 같은 규칙의 **선언 시점** 짝이다 — 하나는 정책 파일이 거짓말하지 못하게 하고, 하나는 실패 객체가 거짓말하지 못하게 한다. - -## RedisCommandDescriptor 참조 위치 - -:::evidence key="adapter-outbound-cache-redis-c07" alt="코드베이스에서 RedisCommandDescriptor 를 검색한 출력 28줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RedisCommandDescriptor 코드베이스 검색 — 28줄 · exit 0" zoom="true" -::: - -## KeySpec과 CommandId가 없애는 불일치 - -`KeySpec`은 공식 Redis 규약(1-based, 음수 lastKey는 뒤에서부터, `movable`은 정적 유도 불가)을 그대로 따르고, `movable`이면 `resolvePositions`가 던진다 — "movable key specification must be resolved by the server". `CommandId`는 항상 대문자로 정규화해 "a policy file, a server metadata reply, and an SDK call site cannot disagree because of casing." - -## permit이 절대 넓히지 못하는 것 - -permit 세 종은 인터페이스이고 javadoc이 경계를 명확히 한다 — "Application code may implement this interface, but a self-made instance never passes `RedisPermitVerifier`… The final enforcement boundary remains the Redis ACL account, which a permit never widens." `PersistentKeyPermit`은 한 줄 더 붙인다: "Cache, session, lock, idempotency, and rate-limit APIs never accept this permit." - -## 예산이 선택이 아닌 이유 - -`OperationBudget`도 규칙을 문서로 못박는다 — "Every R2 API requires a budget. The budget is never optional and never defaulted, because the whole point is that the caller states the cost it is prepared to pay before Redis is asked." - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-cache-redis-c11.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-cache-redis-c11.md deleted file mode 100644 index 60713d1..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-cache-redis-c11.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-cache-redis-c11 -title: R4인데 BLOCKED이 아닌 정책 파일은 읽히지 않는다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-cache-redis-c11 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-cache-redis-c11 - file: ../../../final/evidence/rendered/adapter-outbound-cache-redis-c11.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-cache-redis-c11.txt -source: - - 원본 분석 절은 final/document.md#a10#L579 이다. -module: adapter-outbound-cache-redis ---- - -# R4인데 BLOCKED이 아닌 정책 파일은 읽히지 않는다 - -`RedisCommandPolicyLoader`가 일반 YAML 파서를 쓰지 않고, 교차 필드 불변식이 로딩 시점에 적용된다. - -## 본문 - - - -`RedisCommandPolicyLoader`의 javadoc이 일반 YAML 파서를 쓰지 않는 이유를 적는다. 파서는 그만큼 좁다 — 탭 금지, 들여쓰기 0/2/4만 허용, `commands:` 루트 정확히 하나, 명령 블록 중복 금지, 필드 이름 allowlist(12종) 밖이면 거부, 빈 값 거부, 필드 중복 거부. test가 `rejectsUnknownFieldsEnumsAndDuplicates`로 잡는다. - -## RedisCommandPolicyLoader 참조 위치 - -:::evidence key="adapter-outbound-cache-redis-c11" alt="코드베이스에서 RedisCommandPolicyLoader 를 검색한 출력 6줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RedisCommandPolicyLoader 코드베이스 검색 — 6줄 · exit 0" zoom="true" -::: - -## 모순된 정책 파일은 읽히지 않는다 - -`RedisCommandPolicy`/`RedisCommandDescriptor`의 교차 필드 불변식(§24)이 로딩 시점에 적용되므로, "R4인데 BLOCKED이 아닌" 정책 파일은 **읽히지 않는다**. - -## 집합을 고정하는 테스트 - -`everyDestructiveCommandIsBlockedAndUnreachable`·`deprecatedCommandNamesAreNotReachable`·`arbitraryScriptSourceExecutionIsBlocked`·`theCatalogFailsClosedForAnUnclassifiedCommand`. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-fileserver-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-fileserver-c02.md deleted file mode 100644 index e8e00b0..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-fileserver-c02.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-fileserver-c02 -title: 파싱은 되지만 우리가 쓰지 않았을 형태를 전부 거부한다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-fileserver-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-fileserver-c02 - file: ../../../final/evidence/rendered/adapter-outbound-fileserver-c02.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-fileserver-c02.txt -source: - - 원본 분석 절은 final/document.md#a08#L168 이다. -module: adapter-outbound-fileserver ---- - -# 파싱은 되지만 우리가 쓰지 않았을 형태를 전부 거부한다 - -`FileserverControlRecordCodec`이 decode 직후 재encode해 바이트를 비교한다(`requireCanonical(bytes, encodeOperation(record))`). - -## 본문 - - - -`FileserverControlRecordCodec`은 세 레코드와 receipt snapshot에 대해 **decode 직후 재encode해 바이트를 비교**한다(`requireCanonical(bytes, encodeOperation(record))`). 그래서 "파싱은 되지만 우리가 쓰지 않았을 형태"가 전부 거부된다 — 공백, 필드 재배열, 이스케이프, `-0`·선행 0 같은 숫자 표기, 후행 콘텐츠. - -## FileserverControlRecordCodec 참조 위치 - -:::evidence key="adapter-outbound-fileserver-c02" alt="코드베이스에서 FileserverControlRecordCodec 를 검색한 출력 12줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="FileserverControlRecordCodec 코드베이스 검색 — 12줄 · exit 0" zoom="true" -::: - -## 파서 자체도 좁다 - -- 필드 집합을 **정확히 일치**시킨다(`values.keySet().equals(allowedFields)`) — 누락도 미지 필드도 거부 -- 중복 키를 거부한다(`putIfAbsent`) -- UTF-8 디코딩이 `REPORT` 모드라 malformed 바이트가 대체문자로 조용히 바뀌지 않는다 -- `\b \f \n \r \t` 이스케이프를 **문법 수준에서 거부**한다("control characters are forbidden") -- 짝 없는 서로게이트를 거부한다(`requireWellFormedUnicode`) -- `Instant.parse` 후 `result.toString().equals(value)`로 **canonical UTC 표기**만 받는다 -- receipt snapshot은 `rsv1.` 접두사 + unpadded base64url이고, 디코딩 후 **재인코딩 문자열 비교**로 alias(후행 비트가 0이 아닌 변형)를 거부한다 - -## 오버플로가 상한 검사를 무력화하지 못한다 - -`requireFormulaCountWithinCells`가 `rowCount * columnCount` 곱을 하기 전에 `rowCount <= Long.MAX_VALUE / columnCount`를 먼저 본다. test가 그 하나하나를 이름으로 고정한다 — `canonicalDecoderRejectsWhitespaceReorderingEscapesNumbersUtf8AndTrailingContent`, `receiptSnapshotRejectsBase64urlAliasWithNonZeroTrailingBits`, `canonicalCodecRoundTripsSupplementaryUnicodeInOpaqueText`, `formulaMitigationCountCannotExceedCellsAndUsesOverflowSafeBounds`. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-httpclient-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-httpclient-c03.md deleted file mode 100644 index cf30d3a..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-httpclient-c03.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-httpclient-c03 -title: 검사할 수 없는 성분을 가진 본문은 재생 가능으로 인증되지 않는다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-httpclient-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-httpclient-c03 - file: ../../../final/evidence/rendered/adapter-outbound-httpclient-c03.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-httpclient-c03.txt -source: - - 원본 분석 절은 final/document.md#a11#L237 이다. -module: adapter-outbound-httpclient ---- - -# 검사할 수 없는 성분을 가진 본문은 재생 가능으로 인증되지 않는다 - -`ObjectBody`의 재생 가능성은 값의 성질로 구조적으로 판정되고, 판정할 수 없으면 `ONE_SHOT`으로 떨어진다. - -## 본문 - - - -이 sub-scope에서 가장 신중한 코드다. 과거 동작과 그 결과가 적혀 있다. - -## ObjectBody 참조 위치 - -:::evidence key="adapter-outbound-httpclient-c03" alt="코드베이스에서 ObjectBody 를 검색한 출력 29줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ObjectBody 코드베이스 검색 — 29줄 · exit 0" zoom="true" -::: - -## 값의 성질로 판정한다 - -지금은 `deeplyImmutable(value)`가 구조적으로 판정한다 — 문자열·숫자·불리언·문자·enum·UUID·`Temporal`은 통과, 컬렉션과 맵은 **JDK의 불변 뷰인지 이름으로 확인**하고 원소까지 재귀, record는 모든 성분을 반사로 재귀 확인, 그 외는 전부 `ONE_SHOT`. - -## 인증할 수 없으면 재생하지 않는다 - -반사가 실패하면 "A component the platform cannot inspect cannot be certified, and an uncertified body is one-shot rather than optimistically replayable." 컬렉션 판정이 이름 기반인 이유도 적혀 있다 — "`List.of(...)` and `Collections.unmodifiableList(...)` return package-private classes with no shared marker interface. An ordinary `ArrayList` the caller still holds is exactly the case this must not accept." test 넷이 네 갈래를 고정한다 — `anImmutableRecordReplays`, `aMutableValueIsOneShot`, `aRecordWrappingMutableStateIsOneShot`, `anArbitraryBeanIsOneShot`. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-httpclient-c06.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-httpclient-c06.md deleted file mode 100644 index b60837c..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-httpclient-c06.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-httpclient-c06 -title: 와이어 바이트와 디코드 바이트를 따로 세고, 읽는 도중에 상한을 건다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-httpclient-c06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-httpclient-c06 - file: ../../../final/evidence/rendered/adapter-outbound-httpclient-c06.svg - - key: adapter-outbound-httpclient-c06-diagram - file: ../../../final/assets/diagrams/adapter-outbound-httpclient-c06.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-httpclient-c06.txt -source: - - 원본 분석 절은 final/document.md#a11#L403 이다. -module: adapter-outbound-httpclient ---- - -# 와이어 바이트와 디코드 바이트를 따로 세고, 읽는 도중에 상한을 건다 - -예산이 둘로 나뉘어 있고 강제 시점이 버퍼링 이후가 아니라 읽는 도중이며, 원격이 준 status는 채택되지 않는다. - -## 본문 - - - -`ResponseSizeLimiter`가 **와이어 바이트와 디코드 바이트를 따로** 센다 — "a compressed payload passes a wire check and then expands, so a single limit either rejects legitimate traffic or lets a **decompression bomb** through." - -## 예산이 강제되는 자리 - -:::evidence key="adapter-outbound-httpclient-c06-diagram" alt="와이어 바이트 예산, 읽는 도중 상한 강제, 디코드 바이트 예산, 오류 본문 제한 읽기가 읽는 순서대로 쌓여 있다" caption="응답 크기 예산이 강제되는 자리" zoom="false" -::: - -## 버퍼링이 끝난 뒤에는 늦다 - -`CountingBoundedInputStream`이 상한을 **읽는 도중에** 적용한다 — "a response that is discovered to be too large only once it is fully buffered has already cost the memory the limit exists to protect." `BoundedErrorBody`는 오류 본문을 RFC 9457 문서를 해독할 만큼만 읽고, "The bytes never reach an exception message or a log." `toString()`은 길이와 truncated 여부만 낸다. - -## 원격이 준 status는 버린다 - -`RemoteProblemDecoder`의 규칙 한 줄이 이 계층의 성격을 요약한다 — "**The wire status wins.** A remote `status` member is read and discarded, because trusting it would let an upstream **relabel a 503 as a 400 and change our retry behaviour from its own body.**" 확장 속성도 allowlist로 걸러 "an upstream cannot inject unbounded attributes into our telemetry." test 넷이 그 갈래를 고정한다(`mapsProblemJsonWithoutTrustingBodyStatus`·`dropsExtensionsThatAreNotAllowlisted`·`treatsANonProblemContentTypeAsAnEmptyProblem`·`survivesAnUnparseableProblemDocument`). - -## ResponseSizeLimiter 참조 위치 - -:::evidence key="adapter-outbound-httpclient-c06" alt="코드베이스에서 ResponseSizeLimiter 를 검색한 출력 20줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ResponseSizeLimiter 코드베이스 검색 — 20줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-messaging-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-messaging-c01.md deleted file mode 100644 index e4f3bcc..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-messaging-c01.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-messaging-c01 -title: 구성이 끝난 뒤에는 스키마를 가져올 방법이 남아 있지 않다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-messaging-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-messaging-c01 - file: ../../../final/evidence/rendered/adapter-outbound-messaging-c01.svg - - key: adapter-outbound-messaging-c01-diagram - file: ../../../final/assets/diagrams/adapter-outbound-messaging-c01.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-messaging-c01.txt -source: - - 원본 분석 절은 final/document.md#a12#L174 이다. -module: adapter-outbound-messaging ---- - -# 구성이 끝난 뒤에는 스키마를 가져올 방법이 남아 있지 않다 - -`LocalJsonSchemaRegistry`의 닫힘이 어휘·키워드·참조·메타스키마 권위 네 겹으로 표현되고, 매니페스트 핀이 실제 파일을 가리키는 것까지 probe로 확인했다. - -## 본문 - - - -`LocalJsonSchemaRegistry`의 한 줄 요약이 계약이다 — "Immutable, startup-compiled Draft 2020-12 registry backed **only by explicitly supplied bytes**. Every reference is checked before NetworkNT compilation. After construction this type exposes **no loader, URL, file or classpath fetch operation**." - -## 레지스트리가 닫힌 방식 - -:::evidence key="adapter-outbound-messaging-c01-diagram" alt="넘겨받은 바이트와 동봉된 메타스키마와 허용 어휘가 레지스트리 안에 놓이고 원격 참조 로더와 동적 키워드가 바깥에 빗금으로 놓인다" caption="레지스트리가 닫힌 방식" zoom="false" -::: - -## 닫힘이 네 겹으로 표현된다 - -1. **어휘 allowlist** — `KNOWN_VOCABULARIES` 8종(core·applicator·unevaluated·validation·meta-data·format-annotation·format-assertion·content) 밖의 `$vocabulary` 항목은 거부된다. -2. **키워드 부분집합** — `$anchor`·`$dynamicRef`·`$dynamicAnchor`·`$recursiveRef`·`$recursiveAnchor` 다섯이 `UNSUPPORTED_CLOSED_SUBSET_KEYWORDS`로 **문서 어디에서든** 거부된다(test `rejectsDynamicRecursiveAndAnchorKeywordsEverywhereInTheClosedSubset`). -3. **참조 사전 검사** — `validateAllReferences`가 NetworkNT 컴파일 **전에** 모든 `$ref`를 확인하고, 원격 참조와 설정된 깊이를 넘는 참조 그래프를 거부한다. -4. **핀 고정된 메타스키마 권위** — 9개 Draft 2020-12 메타 문서를 리소스로 동봉하고 `authority.sha256` 매니페스트로 해시를 고정하며, 도메인 분리 상수(`ca-skeleton.messaging.draft-2020-12-authority.v1`)를 섞는다. 매니페스트는 UTF-8 디코딩을 `REPORT` 모드로 읽어 잘못된 바이트를 조용히 대체하지 않는다. - -## 핀이 실제 파일을 가리키는지 실행해서 봤다 - -동봉된 9개 파일의 SHA-256이 `authority.sha256`의 아홉 줄과 **전부 일치한다**(`178-...` §8.3). - -## LocalJsonSchemaRegistry 참조 위치 - -:::evidence key="adapter-outbound-messaging-c01" alt="코드베이스에서 LocalJsonSchemaRegistry 를 검색한 출력 37줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="LocalJsonSchemaRegistry 코드베이스 검색 — 37줄 · exit 0" zoom="true" -::: - -## 식별자는 정확한 URN 스킴만 받는다 - -`$id`는 정확한 URN 스킴만 허용하고(`acceptsOnlyExactUrnSchemeForRootIdentifiersAndAbsoluteReferences`), 중첩 `$id`는 상대·절대 어느 쪽도 허용하지 않으며 **값 타입과 무관하게 키 자체를** 거부한다(`rejectsNestedSchemaIdentifierKeysRegardlessOfValueType`). - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-messaging-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-messaging-c02.md deleted file mode 100644 index 4eba58d..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-messaging-c02.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-messaging-c02 -title: 봉투를 쓰는 경로에 JSON 파서도 생성기도 없다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-messaging-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-messaging-c02 - file: ../../../final/evidence/rendered/adapter-outbound-messaging-c02.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-messaging-c02.txt -source: - - 원본 분석 절은 final/document.md#a12#L189 이다. -module: adapter-outbound-messaging ---- - -# 봉투를 쓰는 경로에 JSON 파서도 생성기도 없다 - -`DeterministicEnvelopeWriter`가 페이로드를 선언된 shape대로 스냅샷해 그 바이트를 리터럴로 이어 붙이므로, 같은 키로 다른 바이트가 나오는 경로가 구조적으로 없다. - -## 본문 - - - -`DeterministicEnvelopeWriter`는 페이로드를 **선언된 shape을 따라 스냅샷**한 뒤 그 정확한 바이트를 봉투에 끼워 넣는다 — "those exact trusted bytes are then embedded in the envelope **without any raw JSON parser or generator API**." `embedExactPayload`가 `,"payload":` 리터럴로 이어 붙이는 방식이다. - -## DeterministicEnvelopeWriter 참조 위치 - -:::evidence key="adapter-outbound-messaging-c02" alt="코드베이스에서 DeterministicEnvelopeWriter 를 검색한 출력 11줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DeterministicEnvelopeWriter 코드베이스 검색 — 11줄 · exit 0" zoom="true" -::: - -## 입력이 통과해야 하는 것 - -draft의 페이로드가 **정확히 등록된 final record 클래스**여야 하고(`exactPayloadClassIsRequiredAndNoAssignableTypeSearchOccurs`), contractId와 payloadVersion이 컴파일된 계약과 같아야 하며, 레코드 성분 수·문자열 UTF-8 길이·배열/객체 크기·깊이가 모두 `EnvelopeAdmissionLimits`로 유계다. 그리고 **쓰는 도중에** 출력 크기를 본다(`boundsJsonOutputDuringWritesInsteadOfOnlyInspectingTheCompletedBuffer`). - -## 접근자를 한 번만 부른다 - -가변 페이로드 처리도 명시적이다 — `snapshotsStatefulMutablePayloadAccessorsOnceAndEmbedsThoseExactBytes`. 접근자를 한 번만 부르고 그 바이트를 고정하므로, httpclient의 `ObjectBody` 문제(같은 키로 다른 바이트)가 여기서는 구조적으로 불가능하다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-objectstorage-c04.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-objectstorage-c04.md deleted file mode 100644 index 65d6aa7..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-objectstorage-c04.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-objectstorage-c04 -title: 새 레코드를 추가하면 컴파일이 codec 갱신을 강제한다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-objectstorage-c04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-objectstorage-c04 - file: ../../../final/evidence/rendered/adapter-outbound-objectstorage-c04.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-objectstorage-c04.txt -source: - - 원본 분석 절은 final/document.md#a09#L173 이다. -module: adapter-outbound-objectstorage ---- - -# 새 레코드를 추가하면 컴파일이 codec 갱신을 강제한다 - -`ObjectControlRecord`가 sealed이고 codec이 exhaustive switch를 쓰므로 계열이 닫혀 있고, 모르는 스키마는 덮어쓰지 않고 격리된다. - -## 본문 - - - -`ObjectControlRecord`는 열한 개 구현만 허용하는 `sealed interface`이고, javadoc이 규칙을 적는다 — "Unknown families and schemas fail closed." codec의 `payload(...)`가 그 계열에 대해 **exhaustive switch**를 쓰므로, 새 레코드를 추가하면 컴파일이 강제로 codec을 갱신하게 만든다. - -## ObjectControlRecord 참조 위치 - -:::evidence key="adapter-outbound-objectstorage-c04" alt="코드베이스에서 ObjectControlRecord 를 검색한 출력 35줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ObjectControlRecord 코드베이스 검색 — 35줄 · exit 0" zoom="true" -::: - -## 모르는 스키마는 덮어쓰지 않고 격리한다 - -스키마 버전은 `ControlRecordSupport.header`가 `schemaVersion != 1`을 거부한다 — "only control schema version 1 is writable". 더 새로운 스키마를 만나면 `UnsupportedObjectControlSchemaException`으로 격리한다("A newer or unknown durable schema that must be quarantined rather than overwritten"). - -## 누출 금지가 가시성으로 성립한다 - -`objectstorage.control` 패키지를 leaf 밖에서 참조하는 코드는 **0**이다(`151-...` §8.1, exit=1). CLAUDE.md의 "control-record types leaking into application-core" 금지가 가시성으로 성립한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-objectstorage-c06.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-objectstorage-c06.md deleted file mode 100644 index efaca39..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-objectstorage-c06.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-objectstorage-c06 -title: 다른 route의 identity로 만든 키는 파싱 단계에서 거부된다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-objectstorage-c06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-objectstorage-c06 - file: ../../../final/evidence/rendered/adapter-outbound-objectstorage-c06.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-objectstorage-c06.txt -source: - - 원본 분석 절은 final/document.md#a09#L273 이다. -module: adapter-outbound-objectstorage ---- - -# 다른 route의 identity로 만든 키는 파싱 단계에서 거부된다 - -키를 만드는 인코더가 각 계열마다 하나뿐이고, route 격리와 핸들 계열 분리가 파싱 시점 검사로 성립한다. - -## 본문 - - - -`ObjectControlKeyCodec`과 `ObjectDataKeyCodec`이 각각 "Sole encoder"를 자칭하고, 저장소에서 `"control/v1/"`·`"data/v1/"` 리터럴은 이 두 파일에만 있다(`152-...` §8.3). 키 형태는 `control/v1////`이고 shard는 identity의 SHA-256 앞 두 자리다. - -## ObjectControlKeyCodec 참조 위치 - -:::evidence key="adapter-outbound-objectstorage-c06" alt="코드베이스에서 ObjectControlKeyCodec 를 검색한 출력 15줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ObjectControlKeyCodec 코드베이스 검색 — 15줄 · exit 0" zoom="true" -::: - -## route 격리가 구조적이다 - -`requireMatchingRoute(route, routedIdentity)`가 routed identity의 route 구획을 파싱해 현재 route와 다르면 거부한다("routed identity belongs to a different route"). reference·session·stage handle 키 모두 이 검사를 지난다. - -## 핸들 계열이 접두사로 분리된다 - -`osh1`(stage), `osu1`(direct upload), `osm1`(multipart), `osv1`(version), `osr1`(reference). `ObjectNamespaceCodecTest.referenceAndHandleFamiliesRemainSeparated`가 그 분리를 고정하고, `dataKeyApiHasNoRawNameStringParameter`는 **API 서명 자체에 raw 이름 문자열이 없음**을 단언한다. - -## 잘림을 조용히 넘기지 않는다 - -`CrockfordBase32`는 소문자 정규 알파벳(`0123456789abcdefghjkmnpqrstvwxyz` — I·L·O·U 제외)을 쓰고, 인코딩 후 남은 값이 있으면 거부한다("base32 output length is too small"). - -## 무엇이 이 성질을 고정하고 있나 - -property-based 검사가 있다 — `routeParserRejectsArbitraryNonCanonicalText(@ForAll String candidate)`(jqwik), `namespaceRejectsAliasesAndTraversalInputs`. fingerprint codec에는 **golden vector**가 고정돼 있다(`canonicalIntentHasAFrozenGoldenVector`, `sameIntentIsStableAndEverySemanticChangeChangesTheFingerprint`). `policySnapshotCodecIsCanonicalAndContainsNoCredentialSurface`는 정책 스냅샷에 자격증명 표면이 없음을 test로 고정한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-persistence-jpa-c05.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-persistence-jpa-c05.md deleted file mode 100644 index d5f068a..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-persistence-jpa-c05.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c05 -title: 자기가 만든 토큰을 자기가 거부하는 경계값 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c05 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c05.svg - - key: adapter-outbound-persistence-jpa-c05-diagram - file: ../../../final/assets/diagrams/adapter-outbound-persistence-jpa-c05.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c05.txt -source: - - 원본 분석 절은 final/document.md#a05#L358 이다. -module: adapter-outbound-persistence-jpa ---- - -# 자기가 만든 토큰을 자기가 거부하는 경계값 - -trust boundary 방어는 촘촘한데, decode 전 크기 추정 helper가 padding을 가정해 encode 허용 영역과 decode 허용 영역이 어긋난다. - -## 본문 - - - -codec은 다음 형태의 token을 만든다. - -```text -v1.. -``` - -확인한 방어는 다음과 같다. - -- signing key 최소 32 bytes -- URL-safe Base64 / no padding -- version까지 MAC input에 포함 -- token 전체 길이 4096-character cap -- payload 2048-byte cap -- presented MAC 32-byte exact length 확인 -- `MessageDigest.isEqual` constant-time comparison -- MAC 검증 전에 application payload decoder를 호출하지 않음 -- oversized public input을 substring/decode/MAC allocation 전에 거부하려는 선행 check - -기존 dedicated tests도 tampering, foreign key, unknown version, short key, oversized token, wrong-length MAC, oversized payload 등을 폭넓게 검증한다. - -## SignedJsonCursorCodec 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c05" alt="코드베이스에서 SignedJsonCursorCodec 를 검색한 출력 15줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="SignedJsonCursorCodec 코드베이스 검색 — 15줄 · exit 0" zoom="true" -::: - -## 추정 helper가 padding을 가정한다 - -문제는 decoded payload size를 decode **전에** 추정하는 helper다. 이 함수는 "최대 decoded size"를 빠르게 계산하려는 의도로 commit `2f5d2fc`에서 hostile-input bounds와 함께 추가됐다. 그러나 codec은 **unpadded Base64URL**을 사용한다. - -## encode와 decode의 허용 범위 - -:::evidence key="adapter-outbound-persistence-jpa-c05-diagram" alt="상한 크기의 payload가 base64url 인코드를 지나 크기 사전 추정 단계에서 decode 쪽 거부로 이어지는 경로" caption="encode와 decode의 허용 범위" zoom="false" -::: - -실제 self-round-trip probe 결과 현재 accepted encode domain과 accepted decode domain이 다르다. 이건 hostile token을 더 엄격히 거부하는 정도가 아니다. **codec의 자기 round-trip contract를 깨는 boundary defect**다. 실행 evidence는 `evidence/raw/035a-jpa-cursor-boundary-probe.java`와 `evidence/raw/035-jpa-cursor-boundary-probe.txt`에 있다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-persistence-jpa-c25.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-persistence-jpa-c25.md deleted file mode 100644 index 3b3cc69..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-persistence-jpa-c25.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c25 -title: 문서 상수와 같은 상수를 assert하는 test를 피한 이유 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c25 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c25 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c25.svg - - key: adapter-outbound-persistence-jpa-c25-diagram - file: ../../../final/assets/diagrams/adapter-outbound-persistence-jpa-c25.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c25.txt -source: - - 원본 분석 절은 final/document.md#a05#L1643 이다. -module: adapter-outbound-persistence-jpa ---- - -# 문서 상수와 같은 상수를 assert하는 test를 피한 이유 - -`HibernateProviderPolicy`가 선언된 baseline 상수와 classpath에서 읽은 runtime version을 분리해, drift를 보고할 수 있게 한다. - -## 본문 - - - -`HibernateProviderPolicy`는 상수로 선언된 Stable provider baseline과 실제 classpath에서 읽은 runtime version을 구분한다. 이 설계가 필요한 이유는 repository가 과거 "7.4를 Stable baseline이라고 문서화하면서 실제 Spring Boot BOM은 7.1.x를 resolve"한 상태를 경험했기 때문이다. - -## 선언 기준과 런타임 판본의 분리 - -:::evidence key="adapter-outbound-persistence-jpa-c25-diagram" alt="왼쪽에 정책 상수 baseline과 문서가 선언한 판본이 오른쪽에 org.hibernate.Version 조회와 BOM이 해석한 판본이 같은 축에 놓인다" caption="선언 기준과 런타임 판본의 분리" zoom="false" -::: - -## 두 값을 비교하는 지점 - -declared baseline은 policy constant이고, runtime provider는 `org.hibernate.Version`에서 읽는다. drift 여부는 `driftsFromDeclaredBaseline()`이 판정하며, app-bootstrap capability/report가 runtime value를 사용한다. 즉 "문서 상수와 같은 상수를 assert해서 green"인 self-fulfilling test는 피한다. - -## HibernateProviderPolicy 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c25" alt="코드베이스에서 HibernateProviderPolicy 를 검색한 출력 25줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="HibernateProviderPolicy 코드베이스 검색 — 25줄 · exit 0" zoom="true" -::: - -## 이 sub-scope에서 도달성이 확실한 타입 - -outside-leaf production consumer가 명확히 존재하는 핵심 Hibernate type도 `HibernateProviderPolicy`다. app-bootstrap이 이를 composition/report에 사용한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-persistence-jpa-c45.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-persistence-jpa-c45.md deleted file mode 100644 index 7ce4e9e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-persistence-jpa-c45.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c45 -title: V3와 V4는 registry revision을 올리지 않는다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c45 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c45 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c45.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c45.txt -source: - - 원본 분석 절은 final/document.md#a05#L3166 이다. -module: adapter-outbound-persistence-jpa ---- - -# V3와 V4는 registry revision을 올리지 않는다 - -Fileserver persistence는 opt-in production capability이고, migration V3·V4가 registry revision을 갱신하지 않는 차이가 뒤따르는 startup fail-open finding의 직접 원인이다. - -## 본문 - - - -Fileserver persistence는 latent helper가 아니라 실제 opt-in production capability다. `PersistenceJpaRootAutoConfiguration`이 `FileserverJpaPersistenceConfig`를 import한다. - -## PersistenceJpaRootAutoConfiguration 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c45" alt="코드베이스에서 PersistenceJpaRootAutoConfiguration 를 검색한 출력 10줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="PersistenceJpaRootAutoConfiguration 코드베이스 검색 — 10줄 · exit 0" zoom="true" -::: - -## 켜진 배포에서 무엇이 도달 가능해지나 - -`app.fileserver-platform.enabled=true`이면 Fileserver entity/repository/component scan이 열린다. `FileserverStorageConfiguration.fileserverSchemaActivation()`은 `JdbcOperations`가 있으면 startup에서 `requireActive()`를 호출한다. 따라서 schema activation, quota, cleanup, recovery adapter는 Fileserver capability가 켜진 배포에서 production-reachable하다. - -## revision이 2에서 멈춘다 - -V1은 registry에 `jpa-fileserver-metadata-v1`, `feature_revision=1`, `INSTALLED_INACTIVE`를 기록하고, V2는 recovery schema를 추가한 뒤 revision을 2로 올린다. 이후 V3는 fenced cleanup lease column을, V4는 upload terminal lifecycle column을 추가하지만 registry revision은 더 이상 갱신하지 않는다. 이 차이는 아래 startup fail-open finding의 직접 원인이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-persistence-jpa-c49.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-persistence-jpa-c49.md deleted file mode 100644 index b3807f5..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-persistence-jpa-c49.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c49 -title: 평문처럼 보이는 envelope를 DB constraint가 거부한다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c49 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c49 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c49.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c49.txt -source: - - 원본 분석 절은 final/document.md#a05#L3561 이다. -module: adapter-outbound-persistence-jpa ---- - -# 평문처럼 보이는 envelope를 DB constraint가 거부한다 - -request variable payload는 보호 collaborator를 필수로 받고, contact point는 네 조각으로 분리되며, V10이 같은 규칙을 스키마에 넣는다. - -## 본문 - - - -request variable payload는 `NotificationPayloadProtection`을 필수 collaborator로 받아 보호된 envelope를 저장하고, contact point는 ciphertext/nonce/lookup HMAC/key id로 분리된다. V10은 plaintext-looking request envelope를 **DB constraint로도** 거부한다. - -## NotificationPayloadProtection 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c49" alt="코드베이스에서 NotificationPayloadProtection 를 검색한 출력 37줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="NotificationPayloadProtection 코드베이스 검색 — 37줄 · exit 0" zoom="true" -::: - -## 확인하지 못한 것 - -이번 완독에서 이 경계 자체를 우회하는 production write path는 확인하지 못했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-persistence-mongo-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-persistence-mongo-c02.md deleted file mode 100644 index ac0d0b6..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-persistence-mongo-c02.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-mongo-c02 -title: 선언된 축이 컴파일되지 않으면 startup에서 거부된다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-mongo-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-mongo-c02 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-mongo-c02.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-mongo-c02.txt -source: - - 원본 분석 절은 final/document.md#a06#L493 이다. -module: adapter-outbound-persistence-mongo ---- - -# 선언된 축이 컴파일되지 않으면 startup에서 거부된다 - -이 package의 나머지는 api manifest를 말이 아니라 코드로 만든다 — 조립 순서, startup 거부, 축 컴파일, 실제 등록된 변환기로 만든 guard, 정밀도 검사. - -## 본문 - - - -P1과 별개로, 이 package의 나머지는 api manifest를 말이 아니라 코드로 만든다. - -## 변환기 선택이 실행마다 달라지지 않는다 - -`MongoCustomConversionsFactory.converters(...)`가 변환기를 **명시적 List 순서로** 조립한다. 이유가 주석에 있다 — Spring의 conversion service는 첫 매칭 변환기를 쓰므로 `Set`이나 classpath 스캔에서 조립하면 JVM 실행마다 다른 변환기가 선택될 수 있다. `fingerprint(manifest)`가 manifest fingerprint에 변환기 클래스 이름을 이어 붙여 golden BSON snapshot이 비교할 identity를 만든다. - -## 등록하지 않은 축은 startup에서 걸린다 - -같은 factory가 `requireEveryAxisImplemented(...)`로 `LOCAL_DATE_TIME_WITH_REGISTERED_CONVERTER`를 startup에서 거부한다. enum 상수 자신이 "selecting this without registering the named converter is a startup failure"라고 적어 둔 규칙을 실제로 집행하는 지점이다. - -## LocalDateTimeMappingGuard 참조 위치 - -:::evidence key="adapter-outbound-persistence-mongo-c02" alt="코드베이스에서 LocalDateTimeMappingGuard 를 검색한 출력 14줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="LocalDateTimeMappingGuard 코드베이스 검색 — 14줄 · exit 0" zoom="true" -::: - -## 선언만 있고 컴파일되지 않았던 축 - -`BigIntegerRepresentationConverters.forRepresentation(...)`은 manifest의 BigInteger 축을 세 변환기 쌍으로 컴파일한다. 주석이 과거 상태를 기록한다 — 이 축은 선언만 있고 컴파일되지 않아 `STRING`과 `DECIMAL128`이 동일한 document를 만들었고, 하나는 사전식으로 다른 하나는 수치로 정렬된다. - -## guard를 실제 등록된 변환기로 만든다 - -`LocalDateTimeMappingGuard`는 `MongoMappingConfiguration`이 **실제 등록된 변환기**로 만든다. javadoc이 이전 결함을 적는다 — guard를 `withoutConverters()`로 만들고 manifest를 검증하게 해서, 명명된 변환기를 등록한 배포와 등록하지 않은 배포를 똑같이 거부했다. - -## 조용한 반올림이 금액을 바꾸지 못한다 - -`BigDecimalToDecimal128Converter`는 driver 호출 전에 34 유효숫자·지수 범위를 검사한다. `Decimal128`은 초과 정밀도를 조용히 반올림하므로, 검사가 없으면 금액이 다른 값으로 저장되고 아무 오류도 나지 않는다. - -## 저장된 문자열이 클래스를 고르지 못한다 - -`PolicyAwareMongoTypeMapper`는 alias에 점을 금지하고, 읽을 때 점의 유무로 "legacy class name"과 "alias"를 구분한다 — 그래서 미등록 alias가 class loading으로 fallback하는 일이 없다. `readType(source, basicType)`은 저장된 타입이 caller의 기대 타입과 호환되지 않으면 조용히 caller 타입으로 읽지 않고 schema 오류를 던진다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-persistence-mongo-c05.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-persistence-mongo-c05.md deleted file mode 100644 index aa1737b..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-persistence-mongo-c05.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-mongo-c05 -title: 배선된 enforcer를 부르는 코드가 미배선이다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-mongo-c05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-mongo-c05 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-mongo-c05.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-mongo-c05.txt -source: - - 원본 분석 절은 final/document.md#a06#L709 이다. -module: adapter-outbound-persistence-mongo ---- - -# 배선된 enforcer를 부르는 코드가 미배선이다 - -auto-configuration이 이 sub-scope에서 만드는 bean은 하나이고, 그 하나의 유일한 production 소비자가 미배선이라 아무도 호출하지 않는다. - -## 본문 - - - -auto-configuration이 이 sub-scope에서 만드는 bean은 **`MongoBudgetEnforcer` 하나**다(`132-...` §8.1). `MongoQueryPolicy`·`PolicyAwareMongoQueryBuilder`·`MongoRegexPolicy`·`MongoBudgetPolicyRegistry`·`MongoKeysetCursorCodec`·`PolicyAwareMongoAggregationExecutor`는 bean도 아니고 `main` 안에 소비자도 없다(§8.1 세 번째 검색 exit=1). - -## MongoBudgetEnforcer 참조 위치 - -:::evidence key="adapter-outbound-persistence-mongo-c05" alt="코드베이스에서 MongoBudgetEnforcer 를 검색한 출력 9줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MongoBudgetEnforcer 코드베이스 검색 — 9줄 · exit 0" zoom="true" -::: - -## 그 하나조차 짝이 없다 - -`MongoBudgetEnforcer`의 유일한 production 소비자는 `PolicyAwareMongoAggregationExecutor`인데 그것이 미배선이므로, 배선된 enforcer는 현재 아무도 호출하지 않는다. `MongoKeysetCursorCodec`은 32바이트 이상 서명 키를 요구하는데 그 키를 공급하는 production 코드가 없다 — 생성자 호출은 test 3곳뿐이다. - -## 이것 자체는 결함이 아니다 - -이 leaf는 가짜 도메인을 두지 않고 collection profile·field descriptor·budget을 fork가 선언하도록 설계돼 있으며, CLAUDE.md가 "Real forks add their own document, repository, mapper"라고 명시한다. - -## 그래도 기록하는 두 이유 - -(a) README의 D1/D2 표는 "typed query, mapping manifest, atomic update, optimistic revision"을 노출 계층의 내용으로 제시하는데, 그중 typed query 계열은 배선 없이 fork가 조립해야 한다는 사실이 그 표에 없다. (b) §41의 다음 항목이 그 조립 시점에만 문제가 된다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-persistence-mongo-c09.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-persistence-mongo-c09.md deleted file mode 100644 index bc9ac02..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-adapter-outbound-persistence-mongo-c09.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-mongo-c09 -title: @Configuration을 게이팅하면 게이트를 공급하는 것을 게이팅한다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-mongo-c09 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-mongo-c09 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-mongo-c09.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-mongo-c09.txt -source: - - 원본 분석 절은 final/document.md#a06#L1269 이다. -module: adapter-outbound-persistence-mongo ---- - -# @Configuration을 게이팅하면 게이트를 공급하는 것을 게이팅한다 - -advanced 분류 불변식이 문서가 아니라 코드로 서 있고, 제외 항목마다 근거가 붙어 있다. - -## 본문 - - - -세어 봤다(`137-...` §8.1b·§8.1c). - -- `@MongoAdvancedEntryPoint` **7개**: `MongoChangeMessagingBridge`(CHANGE_STREAM), `MongoCsfleClientFactory`(CSFLE), `MongoQueryableEncryptionCollectionManager`(QUERYABLE_ENCRYPTION), `MongoGridFsMigrationJob`(GRIDFS_COMPATIBILITY), `MongoShardingAdminGateway`(SHARDING), `MongoTenantClientRegistry`·`MongoTenantMigrationCoordinator`(DATABASE_PER_TENANT). -- `@MongoAdvancedPolicy` **11개**. -- 어느 쪽도 아닌 구체 클래스 **1개**: `MongoAdvancedConfiguration`. - -## MongoChangeMessagingBridge 참조 위치 - -:::evidence key="adapter-outbound-persistence-mongo-c09" alt="코드베이스에서 MongoChangeMessagingBridge 를 검색한 출력 16줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MongoChangeMessagingBridge 코드베이스 검색 — 16줄 · exit 0" zoom="true" -::: - -## 그 하나가 누락이 아닌 이유 - -`MongoAdvancedRules.concreteClass()`가 `@Configuration`을 명시적으로 제외하며 이유를 적는다: "A `@Configuration` class is the package's composition root: it builds entry points through the guard rather than being one, and **gating it would gate the thing that supplies the guard**." interface·enum·record·익명·private 중첩·abstract도 같은 방식으로 제외되고 각각 근거가 붙어 있다. 즉 §83이 말하는 불변식은 문서가 아니라 코드로 서 있다. 이 leaf에서 "문서가 주장하고 코드가 지키지 않는다"를 여러 번 본 뒤라, 여기서는 그 반대가 성립한다는 것을 명시해 둘 가치가 있다. - -## 던지기만 하는 메서드를 값으로 바꾼 두 수리 - -`MongoTimeSeriesCapabilityValidator`는 네 개의 던지기만 하는 메서드를 `supportFor(capability) → MongoTimeSeriesSupport`(지원 여부 + 이유)로 바꿨고, `MongoQueryableEncryptionProfile`은 세 개의 던지기만 하는 static factory를 `supportFor(MongoQueryShape) → MongoQueryShapeSupport`로 바꿨다. 근거도 동일하다 — "A factory that never returns is not an API: it cannot appear in working code, so its only reachable use is a test asserting that it throws, and the design-time question it was meant to answer is only answered by running it." - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-application-core-c10.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-application-core-c10.md deleted file mode 100644 index fd32b89..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-application-core-c10.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -kind: CONCEPT -slug: application-core-c10 -title: 임의 Object 대신 sealed 변수 대수를 쓴 이유 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:application-core-c10 -evidenceCapturedOn: 2026-09-01 -assets: - - key: application-core-c10 - file: ../../../final/evidence/rendered/application-core-c10.svg - - key: application-core-c10-diagram - file: ../../../final/assets/diagrams/application-core-c10.svg -evidence: - - ../../../final/evidence/raw/application-core-c10.txt -source: - - 원본 분석 절은 final/document.md#a03#L230 이다. -module: application-core ---- - -# 임의 Object 대신 sealed 변수 대수를 쓴 이유 - -가변·임의 변수가 serialization/fingerprint drift와 `toString` 충돌을 만들 수 있었던 것이 변경 근거이고, 그 뒤로 비밀 경계가 계약 바깥에 따로 서 있다. - -## 관계 - -- **legacy storage/notification compatibility surface의 제거 조건 추적** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -public contract는 arbitrary `Object`/`Map`를 허용하지 않고 sealed `NotificationVariable` algebra를 사용한다. 과거 mutable/arbitrary variable 때문에 serialization/fingerprint drift와 `toString` collision이 가능했던 것이 변경 근거다. - -## 공개 계약과 비밀이 나뉘는 자리 - -:::evidence key="application-core-c10-diagram" alt="sealed 변수 대수와 고정된 템플릿 판본과 유계 수신자 수가 공개 알림 계약 안에 놓이고 임의 Object 변수와 연락처 원문이 바깥에 빗금으로 놓인다" caption="공개 계약과 비밀이 나뉘는 자리" zoom="false" -::: - -## 이 기록이 다루는 범위 - -:::evidence key="application-core-c10" alt="코드베이스에서 파일 목록을 만든 출력 25줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 25줄 · exit 0" zoom="true" -::: - -## vacuous pass가 남긴 regression guard - -structural test는 public API에 arbitrary Object가 다시 들어오지 않는지 검사하며, 과거 잘못된 test root로 vacuous pass했던 문제도 regression guard로 남아 있다. `NotificationPlan`은 exact template version을 pin한다. recipient/metadata/variable count/depth가 bounded되어 있고, receipt는 "durable logical acceptance"이지 provider delivery를 의미하지 않는다. - -## decrypt 실패를 빈 값으로 떨어뜨리지 않는다 - -contact point는 encrypted value + keyed fingerprint로 분리되고 protected contact rendering은 원문을 노출하지 않는다. template variable 자체에 reset token 같은 secret이 들어갈 수 있어 payload protection이 존재한다. contact lookup/provider request/callback fingerprint는 HMAC purpose를 분리해 동일 secret-purpose reuse를 피한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-grpc-policy-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-grpc-policy-c01.md deleted file mode 100644 index c23911b..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-grpc-policy-c01.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: grpc-policy-c01 -title: 재개 토큰 — 서명하고, 구분자를 봉인한다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:grpc-policy-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-policy-c01 - file: ../../../final/evidence/rendered/grpc-policy-c01.svg -evidence: - - ../../../final/evidence/raw/grpc-policy-c01.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-policy#L73 이다. -module: grpc-policy ---- - -# 재개 토큰 — 서명하고, 구분자를 봉인한다 - -GrpcResumeToken 의 아홉 성분 각각이 왜 필요한지가 javadoc 에 있다 — 스냅숏 판본 없이는 사라진 뷰의 위치에서 재개하고, 만료 없이는 이력이 사라진 커서에서 재개하고, 필터 지문 없이는 남의 필터를 자기 위치에서 재개해 요청하지 않은 행을 받는다. 그리고 문자열 성분이 구분자를 담지 못하게 생성자가 거부한다. - -## 본문 - - - -`GrpcResumeToken` 의 아홉 성분 각각이 왜 필요한지가 javadoc 에 있다 — 스냅숏 판본 없이는 사라진 뷰의 위치에서 재개하고, 만료 없이는 이력이 사라진 커서에서 재개하고, 필터 지문 없이는 남의 필터를 자기 위치에서 재개해 요청하지 않은 행을 받는다. - -## GrpcResumeToken 참조 위치 - -:::evidence key="grpc-policy-c01" alt="코드베이스에서 GrpcResumeToken 를 검색한 출력 19줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcResumeToken 코드베이스 검색 — 19줄 · exit 0" zoom="true" -::: - -## 구분자를 담지 못하게 생성자가 거부한다 - -문자열 성분이 구분자를 담으면 생성자에서 거부된다. - -## 검증이 지키는 세 성질 - -`GrpcResumeTokenCodec` 의 검증은 상수 시간 비교(`MessageDigest.isEqual`), 알 수 없는 키 식별자 거부, 세 실패의 구분 불가를 지킨다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-admin-api-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-admin-api-c03.md deleted file mode 100644 index 5dbb8d6..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-admin-api-c03.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-admin-api-c03 -title: 서명 대상 객체가 존재한다는 것이 4-eyes 통과를 뜻한다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-admin-api-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-admin-api-c03 - file: ../../../final/evidence/rendered/messaging-admin-api-c03.svg - - key: messaging-admin-api-c03-diagram - file: ../../../final/assets/diagrams/messaging-admin-api-c03.svg -evidence: - - ../../../final/evidence/raw/messaging-admin-api-c03.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-admin-api#L185 이다. -module: messaging-admin-api ---- - -# 서명 대상 객체가 존재한다는 것이 4-eyes 통과를 뜻한다 - -8개 필드. 그중 requestedBy 가 생성자에서 4-eyes 를 강제한다. - -## 본문 - - - -`ApprovalGrant` 는 8개 필드다. 그중 `requestedBy` 가 생성자에서 4-eyes 를 강제한다. "우회할 수 없는 자리에 표현했다" — 검사기가 아니라 **record 생성자**에 두었으므로, 서명 대상 객체가 존재하는 것 자체가 4-eyes 통과를 뜻한다. - -## canonicalForm 이 길이 접두 인코딩인 것 - -버전 접두 `"v1"` 이 앞에 있어 형식 교체 여지를 남긴 것도 의도적으로 보인다. 이 규칙이 계획 다이제스트 쪽에는 적용되지 않았다 — §12.3(b). - -## 승인 검증이 도는 순서 - -:::evidence key="messaging-admin-api-c03-diagram" alt="다이제스트 대조에서 윈도우로 일치가 건너가고 윈도우에서 서명 파싱으로 기간 안이 건너가고 서명 파싱에서 HMAC 으로 형식 통과가 건너간다" caption="승인 검증이 도는 순서" zoom="false" -::: - -**다이제스트 대조 → 윈도우 → 16진 파싱 → HMAC**. 다이제스트를 먼저 보는 이유가 주석에 있다. - -## ApprovedReplayPlan 참조 위치 - -:::evidence key="messaging-admin-api-c03" alt="코드베이스에서 ApprovedReplayPlan 를 검색한 출력 11줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ApprovedReplayPlan 코드베이스 검색 — 11줄 · exit 0" zoom="true" -::: - -## 검증하는 쪽은 반드시 서명할 수도 있다 - -`sign(...)` 이 같은 클래스에 public 으로 있고, javadoc 이 그 위험을 스스로 명시한다. 이 문장과 대칭키 선택이 만드는 구조적 결과가 §17 의 한 항목이다. - -## javadoc 이 세지 않는 두 검사 - -실제 분기는 여섯이다. 5번에는 별도 인라인 주석이 있어 의도된 검사임이 분명하다. "검사는 있는데 클래스 javadoc 이 세지 않는" 두 항목이, 동시에 **어떤 테스트에도 도달하지 않는** 두 항목이다(`EVD-303`, §12.1). - -## dry run 을 항상 허용하는 근거 - -"the way to make operators plan before they act is to make planning free". 이것은 보안 완화가 아니라 행동 설계다. - -## 토폴로지를 두 번 보는 이유 - -`ApprovedReplayPlan` 생성자(`:19-52`)와 `requireExecutable(now, currentTopologyVersion)`(`:54-85`)이 토폴로지를 **두 번** 본다. 승인이 서명된 토폴로지와 계획이 계산된 토폴로지가 다를 수 있으므로 둘 다 현재와 대조한다. - -## 진행처럼 보이는 루프 - -`ApprovedRedrivePlan` 은 같은 네 검사에 더해 `loopAcknowledged` 를 별도 필드로 갖고, `requireExecutable` 의 마지막 분기가 그것을 강제한다. "진행처럼 보이는 루프" 는 이 리프에서 반복되는 관점이다 — 대시보드에서 옳아 보이는 실패를 타입으로 막는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-cloudevents-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-cloudevents-c01.md deleted file mode 100644 index 7835208..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-cloudevents-c01.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-cloudevents-c01 -title: 싣고 쓰지 않는다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-cloudevents-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-cloudevents-c01 - file: ../../../final/evidence/rendered/messaging-cloudevents-c01.svg - - key: messaging-cloudevents-c01-diagram - file: ../../../final/assets/diagrams/messaging-cloudevents-c01.svg -evidence: - - ../../../final/evidence/raw/messaging-cloudevents-c01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-cloudevents#L48 이다. -module: messaging-cloudevents ---- - -# 싣고 쓰지 않는다 - -플랫폼 봉투와 CloudEvents 1.0.2 사이의 양방향 매퍼. 적용 범위를 인터페이스 javadoc이 한정한다. - -## 관계 - -- **왕복이라 부르는 테스트는 무엇을 보존하지 않는지도 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **문서가 범위를 제한하면 그 제한을 누가 강제하는지 같이 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -플랫폼 봉투와 CloudEvents 1.0.2 사이의 양방향 매퍼. 적용 범위를 인터페이스 javadoc이 한정한다. - -## 싣기와 쓰기가 갈리는 자리 - -:::evidence key="messaging-cloudevents-c01-diagram" alt="messaging-cloudevents 적재만 배포 아티팩트 안에 놓이고 이 리프 타입의 호출자가 바깥에 빗금으로 놓인다" caption="싣기와 쓰기가 갈리는 자리" zoom="false" -::: - -`runtime_memberships`가 `["app-bootstrap"]`이다 — 배포 아티팩트가 싣는다. 그런데 소비자가 하나도 없다(§12.1). Avro·Protobuf는 "싣지도 않고 쓰지도 않는다"로 정합하지만, 이 leaf는 **싣고 쓰지 않는다.** - -## CloudEvent 참조 위치 - -:::evidence key="messaging-cloudevents-c01" alt="코드베이스에서 CloudEvent 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="CloudEvent 코드베이스 검색 — 5줄 · exit 0" zoom="true" -::: - -## 두 좌표를 나눠 지킨 의존성 선언 - -의존성 선언에 이 저장소에서 가장 자세한 근거 주석이 붙어 있다. **둘의 scope가 다른 것이 정확하다** — `cloudevents-api`(`CloudEvent`, `CloudEventData`)는 public 시그니처에 나오므로 `api`, `cloudevents-core`(`CloudEventBuilder`, `BytesCloudEventData`)는 구현 안에서만 쓰이므로 `implementation`이다. `src/messaging/CLAUDE.md:40-43`의 게이트가 잡는 구분이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-cloudevents-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-cloudevents-c02.md deleted file mode 100644 index 799cefa..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-cloudevents-c02.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-cloudevents-c02 -title: 런타임 classpath에는 오르고 어느 @Bean도 만들지 않는다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-cloudevents-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-cloudevents-c02 - file: ../../../final/evidence/rendered/messaging-cloudevents-c02.svg -evidence: - - ../../../final/evidence/raw/messaging-cloudevents-c02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-cloudevents#L80 이다. -module: messaging-cloudevents ---- - -# 런타임 classpath에는 오르고 어느 @Bean도 만들지 않는다 - -들어오는 것: messaging-core-api(api), messaging-schema-api(api), cloudevents-api:4.0.1(api), cloudevents-core:4.0.1(implementation). 나가는 것: messaging-spring-boot-starter의 allowed_dependencies에 포함된다. - -## 관계 - -- **왕복이라 부르는 테스트는 무엇을 보존하지 않는지도 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **문서가 범위를 제한하면 그 제한을 누가 강제하는지 같이 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -들어오는 것은 `messaging-core-api`(api), `messaging-schema-api`(api), `cloudevents-api:4.0.1`(api), `cloudevents-core:4.0.1`(implementation)이다. 나가는 것은 `messaging-spring-boot-starter`의 `allowed_dependencies`에 포함된다. - -## CloudEventMapper 참조 위치 - -:::evidence key="messaging-cloudevents-c02" alt="코드베이스에서 CloudEventMapper 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="CloudEventMapper 코드베이스 검색 — 3줄 · exit 0" zoom="true" -::: - -## classpath에 오르는 경로 - -`app-bootstrap` → starter → 이 leaf 경로로 런타임 classpath에 오른다. - -## 그런데 아무도 부르지 않는다 - -starter의 어느 `@Bean`도 `CloudEventMapper`를 만들지 않고, 어느 클래스도 import하지 않는다(§12.1). bean 없음(Spring 주석 0개). - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-cloudevents-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-cloudevents-c03.md deleted file mode 100644 index 7c9bec7..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-cloudevents-c03.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-cloudevents-c03 -title: 확장 이름이 봉투 필드명과 다른 이유 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-cloudevents-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-cloudevents-c03 - file: ../../../final/evidence/rendered/messaging-cloudevents-c03.svg -evidence: - - ../../../final/evidence/raw/messaging-cloudevents-c03.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-cloudevents#L92 이다. -module: messaging-cloudevents ---- - -# 확장 이름이 봉투 필드명과 다른 이유 - -CloudEventExtensions의 javadoc이 이름이 봉투 필드명과 다른 이유를 적는다 — "CloudEvents requires extension names to be lowercase alphanumeric". - -## 관계 - -- **왕복이라 부르는 테스트는 무엇을 보존하지 않는지도 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **문서가 범위를 제한하면 그 제한을 누가 강제하는지 같이 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -이 leaf의 타입 구성은 다음과 같다. - -```text -CloudEventMapper (interface) -├── toCloudEvent(MessageEnvelope, URI) → CloudEvent -└── fromCloudEvent(CloudEvent) → MessageEnvelope - -DefaultCloudEventMapper (구현) -├── toCloudEvent : occurredAt 필수, payload는 이미 인코딩된 것만 -├── fromCloudEvent : time 필수, schemaversion 확장 필수 -├── stringExtension / intExtension -└── producerFrom(URI) : 마지막 세그먼트를 producer id로 - -CloudEventExtensions (상수 4개) -correlationid · causationid · schemaversion · tenantcontext -``` - -## CloudEventExtensions 참조 위치 - -:::evidence key="messaging-cloudevents-c03" alt="코드베이스에서 CloudEventExtensions 를 검색한 출력 14줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="CloudEventExtensions 코드베이스 검색 — 14줄 · exit 0" zoom="true" -::: - -## 이름이 소문자 영숫자인 이유 - -`CloudEventExtensions`의 javadoc이 이름이 봉투 필드명과 다른 이유를 적는다 — "CloudEvents requires extension names to be lowercase alphanumeric". - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-cloudevents-c05.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-cloudevents-c05.md deleted file mode 100644 index fc3a25e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-cloudevents-c05.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-cloudevents-c05 -title: 두 방향 모두 시각이 없으면 거절한다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-cloudevents-c05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-cloudevents-c05 - file: ../../../final/evidence/rendered/messaging-cloudevents-c05.svg -evidence: - - ../../../final/evidence/raw/messaging-cloudevents-c05.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-cloudevents#L290 이다. -module: messaging-cloudevents ---- - -# 두 방향 모두 시각이 없으면 거절한다 - -나가는 방향: occurredAt 확인(없으면 거절) → CloudEventBuilder.v1()에 id·source·type·time·datacontenttype·schemaversion → 선택 확장 셋 → payload 종류 판정 → dataschema(있을 때) → build() 들어오는 방향: time 확인(없으면 거절) → datacontenttype(기본 application/json) → data(없으면 빈 배열) → MessageId·MessageType·SchemaVersion·ProducerId·확장 셋 → MessageEnvelope 조립 - -## 관계 - -- **왕복이라 부르는 테스트는 무엇을 보존하지 않는지도 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **문서가 범위를 제한하면 그 제한을 누가 강제하는지 같이 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -**나가는 방향:** `occurredAt` 확인(없으면 거절) → `CloudEventBuilder.v1()`에 id·source·type·time·datacontenttype·schemaversion → 선택 확장 셋 → payload 종류 판정 → `dataschema`(있을 때) → `build()` - -**들어오는 방향:** `time` 확인(없으면 거절) → `datacontenttype`(기본 `application/json`) → `data`(없으면 빈 배열) → `MessageId`·`MessageType`·`SchemaVersion`·`ProducerId`·확장 셋 → `MessageEnvelope` 조립 - -## MessageId 참조 위치 - -:::evidence key="messaging-cloudevents-c05" alt="코드베이스에서 MessageId 를 검색한 출력 9줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessageId 코드베이스 검색 — 9줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-cloudevents-c06.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-cloudevents-c06.md deleted file mode 100644 index a5c980b..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-cloudevents-c06.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-cloudevents-c06 -title: 외부 데이터가 들어오는 유일한 진입점의 실패가 분류되지 않는다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-cloudevents-c06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-cloudevents-c06 - file: ../../../final/evidence/rendered/messaging-cloudevents-c06.svg -evidence: - - ../../../final/evidence/raw/messaging-cloudevents-c06.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-cloudevents#L298 이다. -module: messaging-cloudevents ---- - -# 외부 데이터가 들어오는 유일한 진입점의 실패가 분류되지 않는다 - -분류된 실패 넷, 분류되지 않은 실패 여덟. 매퍼가 직접 던지는 것은 전부 MessageValidationException이지만, 값 객체 생성자에 위임한 검증은 전부 raw IllegalArgumentException이다. - -## 관계 - -- **왕복이라 부르는 테스트는 무엇을 보존하지 않는지도 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **문서가 범위를 제한하면 그 제한을 누가 강제하는지 같이 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -**분류된 실패 넷, 분류되지 않은 실패 여덟.** 매퍼가 직접 던지는 것은 전부 `MessageValidationException`이지만, 값 객체 생성자에 위임한 검증은 전부 raw `IllegalArgumentException`이다. - -## MessageValidationException 참조 위치 - -:::evidence key="messaging-cloudevents-c06" alt="코드베이스에서 MessageValidationException 를 검색한 출력 9줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessageValidationException 코드베이스 검색 — 9줄 · exit 0" zoom="true" -::: - -## 하필 그 진입점이다 - -`fromCloudEvent`는 **외부에서 온 데이터**를 다루는 유일한 진입점인데, 그 진입점의 실패 대부분이 플랫폼 실패 어휘 밖이다. - -## 설계 의도와 어긋나는 지점 - -`messaging-core-api`의 `FailureDescriptor` 설계 전체가 "예외 클래스로 분기하지 말고 선언된 분류로 판단하라"였다. 이 경로는 그 분류를 만들지 않는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-core-api-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-core-api-c02.md deleted file mode 100644 index c8a72c2..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-core-api-c02.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-core-api-c02 -title: 의존이 하나도 없고 나머지 24개 전부가 이것을 의존한다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-core-api-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-core-api-c02 - file: ../../../final/evidence/rendered/messaging-core-api-c02.svg -evidence: - - ../../../final/evidence/raw/messaging-core-api-c02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-core-api#L101 이다. -module: messaging-core-api ---- - -# 의존이 하나도 없고 나머지 24개 전부가 이것을 의존한다 - -들어오는 것: 없음. registry allowed_dependencies: []이고 build.gradle에 선언이 없다. - -## 본문 - - - -들어오는 것은 없다 — registry `allowed_dependencies: []`이고 `build.gradle`에 선언이 없다. - -## 이 기록이 다루는 범위 - -:::evidence key="messaging-core-api-c02" alt="코드베이스에서 파일 목록을 만든 출력 25줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 25줄 · exit 0" zoom="true" -::: - -## 나가는 쪽은 messaging family 전부다 - -`messaging-schema-api`, `messaging-schema-json`, `messaging-schema-avro`, `messaging-schema-protobuf`, `messaging-cloudevents`, `messaging-policy`, `messaging-transport-spi`, `messaging-runtime-core`, `messaging-observability`, `messaging-security`, `messaging-kafka`, `messaging-kafka-share-experimental`, `messaging-rabbit`, `messaging-reliability-api`, `messaging-outbox-jdbc-postgresql`, `messaging-inbox-jdbc-postgresql`, `messaging-claim-check`, `messaging-admin-api`, `messaging-admin-runtime`, `messaging-pulsar-experimental`, `messaging-nats-experimental`, `messaging-spring-cloud-stream-bridge`, `messaging-spring-boot-starter`, `messaging-testkit` — 나머지 **24개 전부**. - -## 편입이 전이로 일어난다 - -`runtime_memberships: ["app-bootstrap"]`이고, 그 편입은 직접 선언이 아니라 **전이(transitive)**로 일어난다. `src/app-bootstrap/build.gradle:87`이 선언하는 것은 starter 하나이고, starter의 `allowed_dependencies`가 17개 leaf를 끌고 오며 그 closure에 `messaging-core-api`가 있다. 즉 **배포 아티팩트가 이 leaf를 싣는다.** 실행 여부는 별개이고 master switch `app.messaging.enabled`(기본 `false`)가 결정한다(`src/messaging/CLAUDE.md:56-57`). - -## 비교할 sibling bean이 없다 - -이 leaf 자체는 bean을 하나도 만들지 않는다. Spring stereotype·`@Bean`·`@Conditional`·`@Profile` 주석이 leaf 전체에 0개다(`evidence/raw/269` §F, `git grep` exit=1). 따라서 §12.2의 conditional sibling 비교는 이 leaf에 **적용 대상이 없다**. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-core-api-c05.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-core-api-c05.md deleted file mode 100644 index 946f05e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-core-api-c05.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-core-api-c05 -title: 코드가 실제로 도는 네 지점 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-core-api-c05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-core-api-c05 - file: ../../../final/evidence/rendered/messaging-core-api-c05.svg -evidence: - - ../../../final/evidence/raw/messaging-core-api-c05.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-core-api#L406 이다. -module: messaging-core-api ---- - -# 코드가 실제로 도는 네 지점 - -이 leaf에는 실행 경로가 거의 없다. 실제로 코드가 도는 지점은 넷이다. - -## 본문 - - - -이 leaf에는 실행 경로가 거의 없다. 실제로 코드가 도는 지점은 넷이다. - -## 이 기록이 다루는 범위 - -:::evidence key="messaging-core-api-c05" alt="코드베이스에서 파일 목록을 만든 출력 25줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 25줄 · exit 0" zoom="true" -::: - -## 네 지점 - -1. **봉투 생성** — `new MessageEnvelope<>(...)` → 14개 non-null 검사 + partitionKey/orderingKey wire 검사 -2. **헤더 생성** — `MessageHeaders.application/platform(Map)` → 개수(≤64) → 이름별 예약/자격증명/중복 검사 → 총 바이트(≤32,768) -3. **식별자 생성** — `MessageId.newId()` → `UuidV7.next()` → `AtomicLong.updateAndGet(advance)` -4. **결과 조립** — `new PublishResult(...)` / `new SettlementResult(...)` → 조합 검증 - -## 나머지가 있는 곳 - -나머지는 전부 인터페이스 선언이고, 구현은 `messaging-runtime-core`·`messaging-kafka`·`messaging-rabbit` 등 다른 leaf가 소유한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-api-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-api-c02.md deleted file mode 100644 index 8365af7..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-api-c02.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-schema-api-c02 -title: Spring 주석 0개인 leaf가 배포 아티팩트에 실린다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-schema-api-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-schema-api-c02 - file: ../../../final/evidence/rendered/messaging-schema-api-c02.svg -evidence: - - ../../../final/evidence/raw/messaging-schema-api-c02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-schema-api#L79 이다. -module: messaging-schema-api ---- - -# Spring 주석 0개인 leaf가 배포 아티팩트에 실린다 - -들어오는 것: messaging-core-api(api 노출). 나가는 것: messaging-schema-json, messaging-schema-avro, messaging-schema-protobuf, messaging-cloudevents, messaging-policy, messaging-transport-spi, messaging-runtime-core, messaging-kafka, messaging-rabbit, messaging-pulsar-experimental, messaging-nats-experimental, messaging-spring-boot-starter, messaging-testkit. - -## 관계 - -- **port 계약은 동시성 요구를 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **도달성 판정은 단어가 아니라 import로 확인한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -들어오는 것은 `messaging-core-api`(api 노출)다. 나가는 것은 `messaging-schema-json`, `messaging-schema-avro`, `messaging-schema-protobuf`, `messaging-cloudevents`, `messaging-policy`, `messaging-transport-spi`, `messaging-runtime-core`, `messaging-kafka`, `messaging-rabbit`, `messaging-pulsar-experimental`, `messaging-nats-experimental`, `messaging-spring-boot-starter`, `messaging-testkit`이다. - -## 이 기록이 다루는 범위 - -:::evidence key="messaging-schema-api-c02" alt="코드베이스에서 파일 목록을 만든 출력 10줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 10줄 · exit 0" zoom="true" -::: - -## 편입 경로가 core-api 와 같다 - -`app-bootstrap`이 `messaging-spring-boot-starter`를 선언하고 그 closure가 이 leaf를 끌어온다. 이 leaf는 bean을 만들지 않는다 — Spring 주석 0개. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-api-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-api-c03.md deleted file mode 100644 index c7245bf..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-api-c03.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-schema-api-c03 -title: 아무것도 실패하지 않고 잘못된 성공이 나온다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-schema-api-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-schema-api-c03 - file: ../../../final/evidence/rendered/messaging-schema-api-c03.svg - - key: messaging-schema-api-c03-diagram - file: ../../../final/assets/diagrams/messaging-schema-api-c03.svg -evidence: - - ../../../final/evidence/raw/messaging-schema-api-c03.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-schema-api#L113 이다. -module: messaging-schema-api ---- - -# 아무것도 실패하지 않고 잘못된 성공이 나온다 - -이 leaf에서 가장 밀도 높은 javadoc이다. 핵심은 "Nothing failed"다. - -## 관계 - -- **port 계약은 동시성 요구를 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **도달성 판정은 단어가 아니라 import로 확인한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -이 leaf에서 가장 밀도 높은 javadoc이다. 타입만으로 키를 잡으면 실패가 발생하지 않고 **잘못된 성공**이 발생한다. 그리고 그 결과에는 등록된 적 없는 버전 라벨이 붙어 하위 감사 기록까지 오염된다. - -## 타입만 키로 잡은 경로 - -:::evidence key="messaging-schema-api-c03-diagram" alt="미등록 버전 라벨이 타입만으로 조회와 이전 파서로 디코딩을 지나 잘못된 성공으로 이어진다" caption="타입만 키로 잡은 경로" zoom="false" -::: - -## 세 codec이 두 실패를 구분한다 - -`JacksonMessageCodec.requireRegistered`, `AvroMessageCodec.schemaFor`, `ProtobufMessageCodec.requireRegistered`가 모두 "타입은 아는데 버전을 모른다"와 "타입 자체를 모른다"를 **다른 에러 코드**로 구분한다(`SCHEMA_VERSION_NOT_REGISTERED` vs `UNKNOWN_MESSAGE_TYPE`). 그 구분이 있어야 운영자가 "등록을 빠뜨렸다"와 "오타다"를 나눌 수 있다. - -## JacksonMessageCodec 참조 위치 - -:::evidence key="messaging-schema-api-c03" alt="코드베이스에서 JacksonMessageCodec 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="JacksonMessageCodec 코드베이스 검색 — 5줄 · exit 0" zoom="true" -::: - -## 한도를 미리 잡지 않는다 - -`new ByteArrayOutputStream(Math.min(maxBytes, 8_192))` — 주석: "a 1 GiB bound must not pre-allocate 1 GiB." - -## codec 자기 에러 코드를 그대로 던진다 - -`errorCode`가 생성자 인자다. 그래서 Avro는 `AVRO_PAYLOAD_TOO_LARGE`, JSON은 `PAYLOAD_TOO_LARGE`가 나온다. 테스트가 이 성질을 직접 단언한다(`BoundedByteSinkTest.java:69-77`, `as("the sink reports the codec's own code, not a generic one")`). - -## requireFits 는 예산을 소비하지 않는다 - -Protobuf는 직렬화 크기를 미리 알므로 첫 바이트 전에 거절할 수 있다. 그 뒤의 쓰기도 여전히 경계 안이다 — 주석: "this is a cheaper refusal, not a replacement for the bound." `refuseIfBeyondLimit`가 `size > maxBytes - written`으로 비교하는 것도 의도적이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-api-c04.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-api-c04.md deleted file mode 100644 index c27277d..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-api-c04.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-schema-api-c04 -title: 진화 검사 경로는 이 저장소에서 실행되지 않는다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-schema-api-c04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-schema-api-c04 - file: ../../../final/evidence/rendered/messaging-schema-api-c04.svg -evidence: - - ../../../final/evidence/raw/messaging-schema-api-c04.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-schema-api#L255 이다. -module: messaging-schema-api ---- - -# 진화 검사 경로는 이 저장소에서 실행되지 않는다 - -1. 경계 있는 인코딩 — codec이 BoundedByteSink.of(maxBytes, code)를 만들고 → 포맷 라이브러리가 sink에 쓰고 → 한도를 넘는 write에서 MessageTooLargeException → 아니면 sink.toByteArray()로 EncodedMessage 조립 2. - -## 관계 - -- **port 계약은 동시성 요구를 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **도달성 판정은 단어가 아니라 import로 확인한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -세 경로가 있다. - -1. **경계 있는 인코딩** — codec이 `BoundedByteSink.of(maxBytes, code)`를 만들고 → 포맷 라이브러리가 sink에 쓰고 → 한도를 넘는 write에서 `MessageTooLargeException` → 아니면 `sink.toByteArray()`로 `EncodedMessage` 조립 -2. **계약 조회** — `new MessageContractKey(type, version)` → registry lookup → 미스면 "타입 미등록" vs "버전 미등록" 구분 -3. **진화 검사** — `registry.compatibilityOf(subject)` → `versionsToCheck` → (포맷별 게이트가 실제 비교) - -## MessageTooLargeException 참조 위치 - -:::evidence key="messaging-schema-api-c04" alt="코드베이스에서 MessageTooLargeException 를 검색한 출력 13줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessageTooLargeException 코드베이스 검색 — 13줄 · exit 0" zoom="true" -::: - -## 셋째만 실행되지 않는다 - -3번은 이 저장소에서 실행되지 않는다(§12.1). - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-avro-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-avro-c01.md deleted file mode 100644 index e0e4b64..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-avro-c01.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-schema-avro-c01 -title: 형제 leaf가 implementation이고 이쪽이 api인 것이 규칙의 증거다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-schema-avro-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-schema-avro-c01 - file: ../../../final/evidence/rendered/messaging-schema-avro-c01.svg -evidence: - - ../../../final/evidence/raw/messaging-schema-avro-c01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-schema-avro#L49 이다. -module: messaging-schema-avro ---- - -# 형제 leaf가 implementation이고 이쪽이 api인 것이 규칙의 증거다 - -선택적(optional) Avro codec. Stable이 아니고 registry membership이 비어 있다 — build-only / incubating이며, docs/messaging/support-matrix.md의 등급과는 다른 축이다. - -## 관계 - -- **모드 enum을 분기 조건으로 쓰면 각 분기에 테스트를 둔다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **컬렉션 순서가 계약이면 양쪽에서 같은 방향으로 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **안정 코드는 판단 단위로 정하고 구현 단위로 정하지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -선택적(optional) Avro codec. Stable이 아니고 registry membership이 비어 있다 — **build-only / incubating**이며, `docs/messaging/support-matrix.md`의 등급과는 다른 축이다. - -## 이 기록이 다루는 범위 - -:::evidence key="messaging-schema-avro-c01" alt="코드베이스에서 파일 목록을 만든 출력 2줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 2줄 · exit 0" zoom="true" -::: - -## Avro를 api로 선언한 근거 - -build.gradle 주석에 있다. `src/messaging/CLAUDE.md:40-43`이 기술하는 게이트 — public/protected 시그니처에 나오는 vendor 라이브러리가 `api`로 선언됐는지 대조 — 를 이 leaf가 통과한다. 형제 `messaging-schema-json`은 Jackson 타입이 시그니처에 없으므로 `implementation`이고, 그 판정 차이가 규칙이 실제로 작동한다는 증거다. - -## 클래스 둘의 실행 시점이 다르다 - -게이트의 javadoc이 그 이유를 적는다 — "By the time a producer has published one incompatible record, the damage is durable: the record sits in a retained log that every current and future consumer must be able to read." - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-avro-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-avro-c02.md deleted file mode 100644 index 69161c4..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-avro-c02.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-schema-avro-c02 -title: 소비자 없음과 membership 없음이 일치한다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-schema-avro-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-schema-avro-c02 - file: ../../../final/evidence/rendered/messaging-schema-avro-c02.svg -evidence: - - ../../../final/evidence/raw/messaging-schema-avro-c02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-schema-avro#L75 이다. -module: messaging-schema-avro ---- - -# 소비자 없음과 membership 없음이 일치한다 - -들어오는 것: messaging-core-api(api), messaging-schema-api(api), avro:1.12.0(api). 나가는 것: 없다. - -## 관계 - -- **모드 enum을 분기 조건으로 쓰면 각 분기에 테스트를 둔다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **컬렉션 순서가 계약이면 양쪽에서 같은 방향으로 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **안정 코드는 판단 단위로 정하고 구현 단위로 정하지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -들어오는 것은 `messaging-core-api`(api), `messaging-schema-api`(api), `avro:1.12.0`(api)다. 나가는 것은 **없다** — 어떤 leaf의 `allowed_dependencies`에도 `messaging-schema-avro`가 없고, `messaging-spring-boot-starter`의 17개 의존 목록에도 없다. - -## 이 기록이 다루는 범위 - -:::evidence key="messaging-schema-avro-c02" alt="코드베이스에서 파일 목록을 만든 출력 2줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 2줄 · exit 0" zoom="true" -::: - -## 런타임 배선 없음 - -`runtime_memberships: []`이므로 배포 아티팩트에 실리지 않는다. bean도 없다(Spring 주석 0개). - -## 정합적인 incubating 상태 - -소비자 없음과 membership 없음이 일치한다. `messaging-cloudevents`와 대비된다 — 그쪽은 membership이 있고 소비자가 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-avro-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-avro-c03.md deleted file mode 100644 index 233324e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-avro-c03.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-schema-avro-c03 -title: 틀린 스키마로 디코딩해도 실패하지 않는다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-schema-avro-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-schema-avro-c03 - file: ../../../final/evidence/rendered/messaging-schema-avro-c03.svg - - key: messaging-schema-avro-c03-diagram - file: ../../../final/assets/diagrams/messaging-schema-avro-c03.svg -evidence: - - ../../../final/evidence/raw/messaging-schema-avro-c03.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-schema-avro#L106 이다. -module: messaging-schema-avro ---- - -# 틀린 스키마로 디코딩해도 실패하지 않는다 - -"does not fail — it produces plausible garbage"가 이 leaf의 모든 방어의 전제다. JSON이나 Protobuf와 달리 Avro는 잘못된 스키마로 디코딩해도 예외를 던지지 않는 경우가 있다. - -## 관계 - -- **모드 enum을 분기 조건으로 쓰면 각 분기에 테스트를 둔다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **컬렉션 순서가 계약이면 양쪽에서 같은 방향으로 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **안정 코드는 판단 단위로 정하고 구현 단위로 정하지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -"does not fail — it produces plausible garbage"가 이 leaf의 모든 방어의 전제다. JSON이나 Protobuf와 달리 Avro는 잘못된 스키마로 디코딩해도 예외를 던지지 않는 경우가 있다. single-object encoding에 헤더를 붙이지 않는 것도 명시적 결정이다 — "The framing that would carry a schema fingerprint belongs to the transport headers, where the platform already carries schema identity for every format, rather than being duplicated inside the Avro payload for this one format." - -## 얕은 복사가 만든 구멍 - -:::evidence key="messaging-schema-avro-c03-diagram" alt="중첩 맵 쪽에 바깥 맵만 복사와 안쪽 맵은 호출자 소유가 빗금으로 놓이고 평탄화된 키 쪽에 두 레벨 모두 복사와 버전이 키의 일부가 놓인다" caption="얕은 복사가 만든 구멍" zoom="false" -::: - -생성자가 받는 것은 중첩 맵 `Map>`이고, `Map.copyOf`는 **바깥 레벨만** 복사한다 — 안쪽 맵은 호출자 객체로 남아, 참조를 쥔 호출자가 생성 후에 버전을 추가·교체·제거하면 codec이 조용히 그것으로 인코딩하기 시작했다. `(type, version)` 키로 평탄화하면 두 레벨이 모두 복사되고 버전이 조회 identity의 일부가 된다. 이 결함이 위험했던 이유는 위와 곱해진다 — 스키마가 바뀌어도 디코딩이 실패하지 않고 그럴듯한 쓰레기를 낸다. - -## AvroRegistryBoundsTest 참조 위치 - -:::evidence key="messaging-schema-avro-c03" alt="코드베이스에서 AvroRegistryBoundsTest 를 검색한 출력 1줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="AvroRegistryBoundsTest 코드베이스 검색 — 1줄 · exit 0" zoom="true" -::: - -`mutatingTheCallersMapAfterConstructionChangesNothing`이 세 가지를 한 번에 확인한다 — 생성 후 추가한 버전은 미등록, 생성 후 추가한 타입도 미등록, 원래 등록한 스키마는 그대로. 평탄화가 `MessageContractKey`(schema-api)를 키로 쓰므로 §4.5의 2단 에러 구분도 자연히 따라온다. - -## direct encoder 한 줄에 방어가 걸려 있다 - -`BoundedByteSink`(schema-api)의 경계가 실제로 작동하려면 인코더가 증분적으로 써야 한다. `EncoderFactory.get().binaryEncoder(...)`는 버퍼링하므로 sink가 첫 write를 보기 전에 큰 레코드가 이미 할당된다. 즉 **schema-api의 방어가 이 한 줄에 의존한다.** - -## 인코딩 전 검사 둘 - -payload가 `GenericRecord`인가 → `AVRO_PAYLOAD_NOT_A_RECORD`. `schema.equals(record.getSchema())`인가 → `AVRO_SCHEMA_MISMATCH`. 두 번째는 테스트가 이유를 적는다 — `as("encoding v2 data under the v1 version would produce bytes nothing can decode")`. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-avro-c04.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-avro-c04.md deleted file mode 100644 index 202660a..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-avro-c04.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-schema-avro-c04 -title: 쓰기 스키마와 읽기 스키마를 둘 다 넘기는 경로 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-schema-avro-c04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-schema-avro-c04 - file: ../../../final/evidence/rendered/messaging-schema-avro-c04.svg -evidence: - - ../../../final/evidence/raw/messaging-schema-avro-c04.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-schema-avro#L254 이다. -module: messaging-schema-avro ---- - -# 쓰기 스키마와 읽기 스키마를 둘 다 넘기는 경로 - -encode: schemaFor → GenericRecord 확인 → 스키마 동일성 확인 → BoundedByteSink + direct encoder → writer.write + flush → EncodedMessage(bytes, AVRO, SchemaReference) decode(동일 버전): requireWithinLimit → schemaFor → 대상 타입이 GenericRecord 계열인지 → boundedReader(writer, writer) → reader.read decodeEvolved: requireWithinLimit → schemaFor(writer) + schemaFor(reader) → boundedReader(writer, reader) → reader.read CI 게이트: check(candidate, history, mode) → 모드에 따라 비교 대상 선정 → 방향별 checkReaderWriterCompatibility - -## 관계 - -- **모드 enum을 분기 조건으로 쓰면 각 분기에 테스트를 둔다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **컬렉션 순서가 계약이면 양쪽에서 같은 방향으로 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **안정 코드는 판단 단위로 정하고 구현 단위로 정하지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -네 경로가 있다. - -**encode:** `schemaFor` → `GenericRecord` 확인 → 스키마 동일성 확인 → `BoundedByteSink` + direct encoder → `writer.write` + `flush` → `EncodedMessage(bytes, AVRO, SchemaReference)` - -**decode(동일 버전):** `requireWithinLimit` → `schemaFor` → 대상 타입이 `GenericRecord` 계열인지 → `boundedReader(writer, writer)` → `reader.read` - -**decodeEvolved:** `requireWithinLimit` → `schemaFor(writer)` + `schemaFor(reader)` → `boundedReader(writer, reader)` → `reader.read` - -**CI 게이트:** `check(candidate, history, mode)` → 모드에 따라 비교 대상 선정 → 방향별 `checkReaderWriterCompatibility` - -## GenericRecord 참조 위치 - -:::evidence key="messaging-schema-avro-c04" alt="코드베이스에서 GenericRecord 를 검색한 출력 29줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GenericRecord 코드베이스 검색 — 29줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-avro-c05.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-avro-c05.md deleted file mode 100644 index 4f5b2a3..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-avro-c05.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-schema-avro-c05 -title: 크기 실패가 인코딩 실패로 접히지 않게 하는 재던지기 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-schema-avro-c05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-schema-avro-c05 - file: ../../../final/evidence/rendered/messaging-schema-avro-c05.svg -evidence: - - ../../../final/evidence/raw/messaging-schema-avro-c05.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-schema-avro#L266 이다. -module: messaging-schema-avro ---- - -# 크기 실패가 인코딩 실패로 접히지 않게 하는 재던지기 - -예외 재던지기 패턴이 세 곳에 반복된다. BoundedByteSink가 던지는 MessageTooLargeException은 RuntimeException이므로 catch에 걸린다. - -## 관계 - -- **모드 enum을 분기 조건으로 쓰면 각 분기에 테스트를 둔다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **컬렉션 순서가 계약이면 양쪽에서 같은 방향으로 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **안정 코드는 판단 단위로 정하고 구현 단위로 정하지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -**예외 재던지기 패턴이 세 곳에 반복된다.** `BoundedByteSink`가 던지는 `MessageTooLargeException`은 `RuntimeException`이므로 catch에 걸린다. 그것을 그대로 통과시키지 않으면 크기 실패가 인코딩 실패로 접힌다 — JSON codec의 `unwrapTooLarge`와 같은 문제를 다른 방식(원인 사슬 탐색이 아니라 즉시 `instanceof`)으로 푼다. §12.3. - -## BoundedByteSink 참조 위치 - -:::evidence key="messaging-schema-avro-c05" alt="코드베이스에서 BoundedByteSink 를 검색한 출력 15줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="BoundedByteSink 코드베이스 검색 — 15줄 · exit 0" zoom="true" -::: - -## 방어를 우회하는 값이 존재하지만 읽기 실패로 끝난다 - -`AvroHostileInputTest.aCountBeyondIntRangeFailsWhileReadingRatherThanWhileReserving`가 흥미로운 경계를 잡는다 — 2³²을 주장하면 int로 잘려 무해한 값이 되고, 그 다음 읽기가 입력 부족으로 실패해 `MessageSerializationException`이 된다. 즉 `newArray` 방어를 우회하는 값이 존재하지만 그 우회는 할당이 아니라 읽기 실패로 끝난다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-json-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-json-c02.md deleted file mode 100644 index cd9c45f..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-json-c02.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-schema-json-c02 -title: 출하 구성의 codec registry에는 JSON 하나만 들어간다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-schema-json-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-schema-json-c02 - file: ../../../final/evidence/rendered/messaging-schema-json-c02.svg -evidence: - - ../../../final/evidence/raw/messaging-schema-json-c02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-schema-json#L71 이다. -module: messaging-schema-json ---- - -# 출하 구성의 codec registry에는 JSON 하나만 들어간다 - -들어오는 것: messaging-core-api(api), messaging-schema-api(api), jackson-databind(implementation). 나가는 것: messaging-spring-boot-starter(registry allowed_dependencies에 포함). - -## 관계 - -- **실패 코드는 운영자의 다음 행동이 갈리는 지점마다 나눈다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -들어오는 것은 `messaging-core-api`(api), `messaging-schema-api`(api), `jackson-databind`(implementation)다. 나가는 것은 `messaging-spring-boot-starter`(registry `allowed_dependencies`에 포함)다. **실제 배선 지점이 하나 있다** — 이 플랫폼에서 유일하게 조립되는 codec이다. - -## MessageContracts 참조 위치 - -:::evidence key="messaging-schema-json-c02" alt="코드베이스에서 MessageContracts 를 검색한 출력 8줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessageContracts 코드베이스 검색 — 8줄 · exit 0" zoom="true" -::: - -## varargs 자리가 비어 있다 - -`RegisteredMessageCodecs.of(defaultCodec, codecs...)`의 varargs 자리가 비어 있다. 즉 **출하 구성의 codec registry에는 JSON 하나만 들어간다.** Avro·Protobuf·raw bytes는 등록되지 않는다. - -## 포맷 중립이어야 할 정책이 한 포맷 상수를 참조한다 - -두 번째 배선 지점은 상수 참조다. payload 정책의 상한이 **JSON codec의 상수에서 파생된다** — §17에서 다룬다. - -## bean이 없으면 빈 registry로 만들어진다 - -`contracts.getIfAvailable(MessageContracts::none)`이 기본값이므로, 애플리케이션이 `MessageContracts` bean을 내놓지 않으면 **빈 registry**로 codec이 만들어진다. 그 codec은 모든 `encode`/`decode`를 `UNKNOWN_MESSAGE_TYPE`으로 거절한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-json-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-json-c03.md deleted file mode 100644 index 3f7b88c..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-json-c03.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-schema-json-c03 -title: 인코딩 상한과 파서 상한이 하나의 값에서 나온다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-schema-json-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-schema-json-c03 - file: ../../../final/evidence/rendered/messaging-schema-json-c03.svg -evidence: - - ../../../final/evidence/raw/messaging-schema-json-c03.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-schema-json#L124 이다. -module: messaging-schema-json ---- - -# 인코딩 상한과 파서 상한이 하나의 값에서 나온다 - -여섯 가지 방어가 한 곳에 있다. 그리고 polymorphic default typing을 켜지 않는다. - -## 관계 - -- **실패 코드는 운영자의 다음 행동이 갈리는 지점마다 나눈다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -여섯 가지 방어가 한 곳(`strictMapper`)에 있다 — `maxNestingDepth`(100), `maxDocumentLength`(codec 상한), `maxNumberLength`(1,000), `maxStringLength`·`maxNameLength`(5,000,000), `STRICT_DUPLICATE_DETECTION`, 그리고 `FAIL_ON_READING_DUP_TREE_KEY`·`FAIL_ON_TRAILING_TOKENS`·`FAIL_ON_UNKNOWN_PROPERTIES`. 그리고 **polymorphic default typing을 켜지 않는다** — javadoc이 그것이 대부분의 JSON gadget chain의 기반이라고 적는다. - -## 두 상한이 갈라지지 않는다 - -`maxDocumentLength`가 `maxBytes`와 같다는 점이 중요하다 — 인코딩 상한과 디코딩 파서 상한이 하나의 값에서 나온다. 따로 두면 둘이 갈라진다. - -## MessageTooLargeException 참조 위치 - -:::evidence key="messaging-schema-json-c03" alt="코드베이스에서 MessageTooLargeException 를 검색한 출력 8줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessageTooLargeException 코드베이스 검색 — 8줄 · exit 0" zoom="true" -::: - -## 증분 쓰기라 한도에서 멈춘다 - -주석이 이유를 적는다 — "Jackson writes incrementally, so a payload whose serialized form is far larger than the limit stops at the limit instead of after the whole graph has been rendered into a buffer nobody bounded." - -## unwrapTooLarge 가 원인 사슬을 훑는다 - -`for (Throwable cause = exception; cause != null; cause = cause.getCause())` — 끝까지 훑어 `MessageTooLargeException`을 찾는다. 못 찾으면 `MessageSerializationException("JSON_ENCODE_FAILED")`. - -## 에러 메시지만으로 운영자가 판단할 수 있다 - -`SCHEMA_VERSION_NOT_REGISTERED` 메시지에는 `registeredVersions(type)`가 정렬되어 포함된다. 테스트가 그 내용을 직접 단언한다 — `hasMessageContaining("order.created v999").hasMessageContaining("[1, 2]")`(`JsonContractRegistryTest.java:58-61`). - -## 인코딩과 디코딩의 비대칭 - -인코딩에서 `OrderCreated`의 하위 타입을 넘기면 Jackson이 등록된 형태로 직렬화한다. 디코딩에서 하위 타입을 허용하면 등록된 계약과 다른 클래스로 역직렬화되므로 정확 일치여야 한다. 다만 이 비대칭은 주석으로 설명되지 않았다 — §15의 추론 항목이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-json-c04.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-json-c04.md deleted file mode 100644 index 81056fa..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-json-c04.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-schema-json-c04 -title: 디코딩은 등록 클래스와 정확히 같은지를 본다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-schema-json-c04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-schema-json-c04 - file: ../../../final/evidence/rendered/messaging-schema-json-c04.svg -evidence: - - ../../../final/evidence/raw/messaging-schema-json-c04.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-schema-json#L238 이다. -module: messaging-schema-json ---- - -# 디코딩은 등록 클래스와 정확히 같은지를 본다 - -encode: requireRegistered(type, version) → payload가 등록 타입의 인스턴스인지 → BoundedByteSink 생성 → mapper.writeValue(sink, payload) → 실패 시 unwrapTooLarge → EncodedMessage(bytes, JSON, SchemaReference) decode: requireRegistered(type, version) → 요청 클래스가 등록 클래스와 정확히 같은지 → encoded.length 상한 → mapper.readValue → JacksonException이면 JSON_DECODE_FAILED - -## 관계 - -- **실패 코드는 운영자의 다음 행동이 갈리는 지점마다 나눈다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -**encode:** `requireRegistered(type, version)` → payload가 등록 타입의 인스턴스인지 → `BoundedByteSink` 생성 → `mapper.writeValue(sink, payload)` → 실패 시 `unwrapTooLarge` → `EncodedMessage(bytes, JSON, SchemaReference)` - -**decode:** `requireRegistered(type, version)` → 요청 클래스가 등록 클래스와 정확히 같은지 → `encoded.length` 상한 → `mapper.readValue` → `JacksonException`이면 `JSON_DECODE_FAILED` - -## BoundedByteSink 참조 위치 - -:::evidence key="messaging-schema-json-c04" alt="코드베이스에서 BoundedByteSink 를 검색한 출력 15줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="BoundedByteSink 코드베이스 검색 — 15줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-protobuf-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-protobuf-c01.md deleted file mode 100644 index 8f9ac16..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-protobuf-c01.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-schema-protobuf-c01 -title: 소비자가 계약을 등록하려면 vendor 타입을 이름 불러야 한다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-schema-protobuf-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-schema-protobuf-c01 - file: ../../../final/evidence/rendered/messaging-schema-protobuf-c01.svg -evidence: - - ../../../final/evidence/raw/messaging-schema-protobuf-c01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-schema-protobuf#L49 이다. -module: messaging-schema-protobuf ---- - -# 소비자가 계약을 등록하려면 vendor 타입을 이름 불러야 한다 - -선택적 Protobuf codec. runtime_memberships: []이고 starter의 codec registry에도 등록되지 않는다 — build-only / incubating. - -## 관계 - -- **검증되지 않는 스키마 파일은 문서임을 파일 안에 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **신뢰할 수 없는 입력 쪽 경계를 먼저 테스트한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -선택적 Protobuf codec. `runtime_memberships: []`이고 starter의 codec registry에도 등록되지 않는다 — build-only / incubating. - -## 이 기록이 다루는 범위 - -:::evidence key="messaging-schema-protobuf-c01" alt="코드베이스에서 파일 목록을 만든 출력 2줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 2줄 · exit 0" zoom="true" -::: - -## protobuf를 api로 선언한 근거 - -build.gradle 주석에 있다. `src/messaging/CLAUDE.md:40-43`의 vendor `api` 게이트를 통과한다 — `ProtobufMessageContract(Class, Parser)`가 public record이므로 소비자가 그 타입을 이름 부르지 않고는 계약을 등록할 수 없다. - -## 이 leaf의 핵심 문제 인식 - -클래스 javadoc이 한 문장으로 적는다. `messaging-schema-avro`의 "does not fail — it produces plausible garbage"와 같은 성질이다. **JSON은 틀린 스키마로 디코딩하면 대개 실패하고, Avro와 Protobuf는 실패하지 않는다.** 그래서 두 leaf 모두 registry를 계약의 중심에 둔다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-protobuf-c04.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-protobuf-c04.md deleted file mode 100644 index 7498b89..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-protobuf-c04.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-schema-protobuf-c04 -title: 계약 등록 실패가 시작 시점에 난다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-schema-protobuf-c04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-schema-protobuf-c04 - file: ../../../final/evidence/rendered/messaging-schema-protobuf-c04.svg -evidence: - - ../../../final/evidence/raw/messaging-schema-protobuf-c04.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-schema-protobuf#L229 이다. -module: messaging-schema-protobuf ---- - -# 계약 등록 실패가 시작 시점에 난다 - -계약 등록: new ProtobufMessageContract(class, parser) → 빈 입력 파싱 → 클래스 일치 확인 → 실패 시 MessagingConfigurationException encode: requireRegistered → Message이고 등록 클래스인지 → requireFits(getSerializedSize()) → writeTo(sink) → EncodedMessage(bytes, PROTOBUF, SchemaReference) decode: requireRegistered → 요청 클래스 정확 일치 → encoded.length 상한 → parser.parseFrom - -## 관계 - -- **검증되지 않는 스키마 파일은 문서임을 파일 안에 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **신뢰할 수 없는 입력 쪽 경계를 먼저 테스트한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -세 경로가 있다. - -**계약 등록:** `new ProtobufMessageContract(class, parser)` → 빈 입력 파싱 → 클래스 일치 확인 → 실패 시 `MessagingConfigurationException` - -**encode:** `requireRegistered` → `Message`이고 등록 클래스인지 → `requireFits(getSerializedSize())` → `writeTo(sink)` → `EncodedMessage(bytes, PROTOBUF, SchemaReference)` - -**decode:** `requireRegistered` → 요청 클래스 정확 일치 → `encoded.length` 상한 → `parser.parseFrom` - -## MessagingConfigurationException 참조 위치 - -:::evidence key="messaging-schema-protobuf-c04" alt="코드베이스에서 MessagingConfigurationException 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingConfigurationException 코드베이스 검색 — 5줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-protobuf-c05.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-protobuf-c05.md deleted file mode 100644 index fb02a95..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-protobuf-c05.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-schema-protobuf-c05 -title: 세 codec이 같은 문제를 라이브러리에 맞춰 다르게 푼다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-schema-protobuf-c05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-schema-protobuf-c05 - file: ../../../final/evidence/rendered/messaging-schema-protobuf-c05.svg -evidence: - - ../../../final/evidence/raw/messaging-schema-protobuf-c05.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-schema-protobuf#L239 이다. -module: messaging-schema-protobuf ---- - -# 세 codec이 같은 문제를 라이브러리에 맞춰 다르게 푼다 - -Avro와 다른 점 하나. Avro는 catch (IOException | RuntimeException) 안에서 MessageTooLargeException을 instanceof로 통과시킨다. - -## 관계 - -- **검증되지 않는 스키마 파일은 문서임을 파일 안에 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **신뢰할 수 없는 입력 쪽 경계를 먼저 테스트한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -**Avro와 다른 점 하나.** Avro는 `catch (IOException | RuntimeException)` 안에서 `MessageTooLargeException`을 `instanceof`로 통과시킨다. Protobuf는 `catch (IOException failure)`만 잡으므로 sink가 던지는 `MessageTooLargeException`(`RuntimeException`)이 그대로 전파된다. - -## MessageTooLargeException 참조 위치 - -:::evidence key="messaging-schema-protobuf-c05" alt="코드베이스에서 MessageTooLargeException 를 검색한 출력 4줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessageTooLargeException 코드베이스 검색 — 4줄 · exit 0" zoom="true" -::: - -## 별도 통과 로직이 필요 없는 이유 - -protobuf-java가 예외를 감싸지 않기 때문이다. 세 codec이 같은 문제를 세 가지로 푸는데(JSON은 원인 사슬 탐색, Avro는 즉시 `instanceof`, Protobuf는 아무것도 안 함) 각각 라이브러리 동작에 맞는 최소 해법이다. 다만 그 이유가 코드에 적혀 있지 않다. - -## 계약 위반은 메시지 실패가 아니다 - -`ProtobufMessageContract` 생성 실패는 registry를 조립하는 시점, 즉 시작 시점에 난다. 카테고리는 `CONFIGURATION`이고, `MessagingConfigurationException` javadoc이 그 의도를 적는다 — "Raised at startup wherever possible." - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-protobuf-c07.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-protobuf-c07.md deleted file mode 100644 index 6c5b6a5..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-schema-protobuf-c07.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-schema-protobuf-c07 -title: 실패의 종류가 바뀌면 운영자가 읽을 정보가 사라진다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-schema-protobuf-c07 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-schema-protobuf-c07 - file: ../../../final/evidence/rendered/messaging-schema-protobuf-c07.svg -evidence: - - ../../../final/evidence/raw/messaging-schema-protobuf-c07.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-schema-protobuf#L458 이다. -module: messaging-schema-protobuf ---- - -# 실패의 종류가 바뀌면 운영자가 읽을 정보가 사라진다 - -ProtobufMessageContract javadoc이 두 결함을 보존한다. 두 번째가 특히 이 저장소의 반복 주제다 — 실패의 종류가 바뀌면 운영자가 읽을 정보가 사라진다. - -## 관계 - -- **검증되지 않는 스키마 파일은 문서임을 파일 안에 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **신뢰할 수 없는 입력 쪽 경계를 먼저 테스트한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -`ProtobufMessageContract` javadoc이 두 결함을 보존한다. 두 번째가 특히 이 저장소의 반복 주제다 — **실패의 종류가 바뀌면 운영자가 읽을 정보가 사라진다.** `messaging-core-api`의 `FailureDescriptor` 설계, `MessageContractKey`의 2단 에러, JSON codec의 `unwrapTooLarge`가 전부 같은 관심사다. - -## ProtobufMessageContract 참조 위치 - -:::evidence key="messaging-schema-protobuf-c07" alt="코드베이스에서 ProtobufMessageContract 를 검색한 출력 12줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ProtobufMessageContract 코드베이스 검색 — 12줄 · exit 0" zoom="true" -::: - -## proto 파일의 주석이 남긴 규칙 셋 - -"Field numbers are the contract, not the field names ... Tags are never reused, and removed fields are reserved so that a later edit cannot take the number back." 이 규칙 셋 중 둘(개명 안전, 태그 재사용 위험)이 테스트로 증명되고 하나(reserved)는 증명되지 않는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-testkit-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-testkit-c03.md deleted file mode 100644 index a03e572..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-messaging-testkit-c03.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-testkit-c03 -title: 등급이 증거를 만들지 않고 증거가 등급을 만든다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-testkit-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-testkit-c03 - file: ../../../final/evidence/rendered/messaging-testkit-c03.svg -evidence: - - ../../../final/evidence/raw/messaging-testkit-c03.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-testkit#L140 이다. -module: messaging-testkit ---- - -# 등급이 증거를 만들지 않고 증거가 등급을 만든다 - -단일 패키지 dev.caskeleton.messaging.testkit. 세 소스셋이 같은 패키지를 공유하므로 InMemoryMessagingHarness(test)가 FaultController(main)를 package-private 없이 구현할 수 있고, EnvelopeCodecBenchmark(jmh)도 같은 패키지에 있다. - -## 본문 - - - -단일 패키지 `dev.caskeleton.messaging.testkit`. 세 소스셋이 같은 패키지를 공유하므로 `InMemoryMessagingHarness`(test)가 `FaultController`(main)를 package-private 없이 구현할 수 있고, `EnvelopeCodecBenchmark`(jmh)도 같은 패키지에 있다. - -## InMemoryMessagingHarness 참조 위치 - -:::evidence key="messaging-testkit-c03" alt="코드베이스에서 InMemoryMessagingHarness 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="InMemoryMessagingHarness 코드베이스 검색 — 5줄 · exit 0" zoom="true" -::: - -## 화살표 방향이 한 번도 역전되지 않는다 - -데이터 흐름은 한 방향이다. 등급이 증거를 만들지 않고 증거가 등급을 만든다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-shared-contract-c04.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-shared-contract-c04.md deleted file mode 100644 index d3f9869..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-and-wire-models/concept/concept-shared-contract-c04.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: shared-contract-c04 -title: digest는 고정하지만 validator 상호운용은 증명하지 않는다 -topic: schema-and-wire-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:shared-contract-c04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: shared-contract-c04 - file: ../../../final/evidence/rendered/shared-contract-c04.svg -evidence: - - ../../../final/evidence/raw/shared-contract-c04.txt -source: - - 원본 분석 절은 final/document.md#a02#L114 이다. -module: shared-contract ---- - -# digest는 고정하지만 validator 상호운용은 증명하지 않는다 - -contracts/messaging/envelope/v1.schema.json은 Draft 2020-12 schema resource이며 envelopeVersion/eventId/contractId/payloadVersion/logicalDestination/aggregate/occurredAt/correlationId/contentType/payload를 required로 고정하고 top-level/aggregate에 unevaluatedProperties:false를 둔다. checked-in SHA-256은 bf6f2e13fafe01b8ef4cbb73d7ba3f5703bfc68d145bdfe43190bf606dbd00b1이다. - -## 본문 - - - -`contracts/messaging/envelope/v1.schema.json`은 Draft 2020-12 schema resource이며 envelopeVersion/eventId/contractId/payloadVersion/logicalDestination/aggregate/occurredAt/correlationId/contentType/payload를 required로 고정하고 top-level/aggregate에 `unevaluatedProperties:false`를 둔다. checked-in SHA-256은 `bf6f2e13fafe01b8ef4cbb73d7ba3f5703bfc68d145bdfe43190bf606dbd00b1`이다. - -## MessagingEnvelopeSchemaResourceTest 참조 위치 - -:::evidence key="shared-contract-c04" alt="코드베이스에서 MessagingEnvelopeSchemaResourceTest 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingEnvelopeSchemaResourceTest 코드베이스 검색 — 2줄 · exit 0" zoom="true" -::: - -## 테스트가 JDK API로 확인하는 것 - -schema text 자체, identifier regex parity, Java int/long 경계 vector, strict UTF-8, exact digest. - -## 그것이 증명하지 않는 것 - -resource drift와 digest mismatch는 강하게 막지만, README가 명시하듯 실제 Draft 2020-12 validator interoperability나 broker runtime discovery는 증명하지 않는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-ownership-and-capability-streams/case/case-a-customizer-that-discarded-the-bound-property.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-ownership-and-capability-streams/case/case-a-customizer-that-discarded-the-bound-property.md deleted file mode 100644 index 5d61a44..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-ownership-and-capability-streams/case/case-a-customizer-that-discarded-the-bound-property.md +++ /dev/null @@ -1,110 +0,0 @@ ---- -kind: CASE -slug: a-customizer-that-discarded-the-bound-property -title: Flyway location customizer가 운영자가 바인딩한 값을 덮어썼다 -topic: schema-ownership-and-capability-streams -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a-customizer-that-discarded-the-bound-property -evidenceCapturedOn: 2026-09-02 -assets: - - key: a-customizer-that-discarded-the-bound-property - file: ../../../final/evidence/rendered/a-customizer-that-discarded-the-bound-property.svg -evidence: - - ../../../final/evidence/raw/a-customizer-that-discarded-the-bound-property.txt -source: - - 원본 분석에서 이 커스터마이저를 다루는 곳은 `final/document.md#a19` §7.2 이고, 합성 루트의 Flyway 기본 위치를 이 파일이 고정한다고 적는다. 사건의 사후 기록은 벤더 설정의 javadoc 이 갖고 있으며 이 기록에 붙은 자산에서 확인할 수 있다. ---- - -# Flyway location customizer가 운영자가 바인딩한 값을 덮어썼다 - -커스터마이저는 벤더 위치를 넣어 주면서 위치 설정을 조건 없이 불렀다. Flyway 의 그 메서드는 더하는 것이 아니라 대체하고, 커스터마이저는 속성 바인딩이 끝난 뒤에 돈다. - -## 관계 - -- **중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다** - 같은 설정을 두 주체가 정하려 한 사례다. -- **독립 Flyway 스트림과 baseline version 0** - 운영자가 위치 목록에 더하려던 것이 그 스트림들이다. - -## 문제 - -Flyway 의 마이그레이션 위치는 두 곳에서 정해질 수 있다. 운영자가 속성으로 지정하거나, 벤더 설정이 커스터마이저로 넣어 주거나. - -기본값을 주려는 코드와 값을 정하는 코드는 형태가 거의 같다. 둘을 가르는 것은 이미 값이 있는지 보느냐뿐이다. - -## 결론 - -벤더 설정은 고쳤다. 고친 뒤로는 바인딩된 값이 비었을 때만 벤더 위치가 들어간다. - -같은 이름의 빈이 sample 모듈에 하나 더 있는데, 그 빈은 아직 무조건 호출한다. 그 파일의 javadoc 은 이 기록과 반대되는 결론을 적는다. - -증상이 왜 조용했는지와 두 구현의 차이는 본문이 다룬다. - -## 검증 환경 - -Spring Boot : 4.0.8 -마이그레이션 도구 : Flyway -확인 방식 : 두 커스터마이저 구현과 javadoc 대조, H2 쪽 부재 확인 -소스 수정 : x - -## 재현 조건 - -1. PostgreSqlPersistenceConfig 의 커스터마이저 javadoc 을 읽는다. 어느 레인이 몇 개를 설정하고 몇 개를 적용했는지 적혀 있다. -2. 그 파일의 현재 구현이 환경에서 속성을 읽고 비어 있을 때만 설정하는지 확인한다. -3. SamplePostgreSqlPersistenceConfig 의 같은 이름 빈이 어떤 형태인지 확인한다. -4. H2PersistenceConfig 가 커스터마이저를 두는지 확인한다. - -## 본문 - - - -벤더의 마이그레이션 위치를 넣어 주는 커스터마이저가 위치 설정을 무조건 호출했다. Flyway 의 그 메서드는 더하는 것이 아니라 **대체한다.** - -커스터마이저는 스프링이 속성 바인딩을 끝낸 뒤에 돈다. 운영자가 지정한 값은 읽히고 바인딩되고, 그다음에 버려졌다. - -## 실패가 아니라 잘못된 성공이었다 - -운영자가 환경 변수로 마이그레이션 스트림들을 위치 목록에 더한다. Flyway 는 성공을 보고했고, 실제로 적용된 것은 벤더 스트림 하나였다. 없어지는 것은 운영자의 의도뿐이고 그것을 알려 주는 신호가 없었다. - -## 일곱을 설정하고 하나를 적용했다 - -:::evidence key="a-customizer-that-discarded-the-bound-property" alt="코드베이스에서 벤더 설정의 커스터마이저 javadoc 과 현재 구현, 같은 빈 이름을 쓰는 sample 쪽 구현, H2 쪽 주석을 잘라낸 출력 48줄. 벤더 쪽은 값이 비어 있을 때만 설정하고 sample 쪽은 아직 무조건 호출한다는 것이 그 출력에 그대로 보인다." caption="벤더 커스터마이저 javadoc · 현재 구현 · sample 쪽 구현 · H2 쪽 주석 — 48줄 · exit 0" zoom="true" -::: - -javadoc 이 그 경우를 이름으로 적는다 — `local-notification-ingest` 다. 로컬에서 애플리케이션과 데이터베이스와 마이그레이션 단계들을 한 번에 띄우는 시작 레인이다. - -그 레인은 위치 일곱 개를 설정했고 하나가 적용됐다. 나머지 여섯 스트림의 마이그레이션은 돌지 않았다. - -## 같은 형태가 로컬 프로파일에도 있었다 - -javadoc 이 그 대비를 직접 든다. 로컬 프로파일 설정 파일이 값들을 리터럴로 고정해서 호출자가 공급한 모든 환경을 눌렀다. - -그 파일은 지금 전부 자리표시자로 바뀌었고, 주석이 당시 상태를 남긴다 — 스모크 레인이 PostgreSQL 을 띄우고 애플리케이션을 거기 물리고 풀이 열리는 것과 헬스 체크가 통과하는 것을 보면서, 실제로는 이 파일이 고른 인메모리 데이터베이스를 상대하고 있었다는 것이다. 보고서가 둘을 구별하지 못했고, 그래서 실패가 아니라 거짓 초록이었다. - -## 벤더 쪽 수정은 기본값 제공자가 되는 것이었다 - -지금 구현은 해석된 환경에서 그 속성을 읽고, 비어 있을 때만 벤더 위치를 설정한다. 운영자가 골랐으면 아무것도 하지 않는다. - -javadoc 이 책임 이동까지 적는다 — 자기 위치를 지정한 배포는 벤더 위치를 포함할 책임을 진다. 위치를 지정함으로써 맡은 책임이라는 것이다. - -## sample 모듈의 같은 이름 빈은 고쳐지지 않았다 - -sample 모듈에 `postgreSqlFlywayLocationCustomizer` 라는 같은 이름의 빈이 있다. 환경을 받지 않고, 두 위치를 무조건 설정한다. - -그 파일의 javadoc 은 이 기록과 정반대의 결론을 적는다 — `locations(...)` 가 대체하므로 **이것이** 속성이 아니라 유효한 정본이라는 것이다. 벤더 쪽 javadoc 이 같은 성질을 결함의 원인으로 적은 문장과 같은 사실을 두고, 한쪽은 고칠 이유로 쓰고 다른 쪽은 설계 근거로 쓴다. - -sample 모듈은 분석 대상에서 제외된 모듈이라 원본 분석이 그 형태를 판정한 적이 없다. 코드가 그렇다는 것까지만 확인했다. - -## H2 쪽은 커스터마이저를 아예 두지 않는다 - -`H2PersistenceConfig` 는 커스터마이저를 등록하지 않고, 그 결정을 javadoc 에 굵게 적는다. 로컬 프로파일이 Flyway 를 끄고 Hibernate 가 엔티티에서 스키마를 만들기 때문에 H2 에 대응하는 마이그레이션 트리가 없다는 것이다. - -세 설정이 같은 문제에 서로 다른 답을 갖고 있다 — 조건부로 기여하거나, 무조건 정하거나, 아예 두지 않거나. - -## 확인하지 못한 것 - -여러 위치를 환경 변수로 지정해 재현해 보지는 않았다. sample 모듈은 분석 범위에서 제외된 모듈이라, 그 모듈의 같은 빈에 대한 판정은 원본 분석에 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-ownership-and-capability-streams/case/case-registry-column-too-short-for-its-own-path.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-ownership-and-capability-streams/case/case-registry-column-too-short-for-its-own-path.md deleted file mode 100644 index 68af991..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-ownership-and-capability-streams/case/case-registry-column-too-short-for-its-own-path.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -kind: CASE -slug: registry-column-too-short-for-its-own-path -title: 레지스트리 컬럼이 38자 경로에서 짧아 "더 짧은 경로를 적는" 우회를 유혹했다 -topic: schema-ownership-and-capability-streams -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:registry-column-too-short-for-its-own-path -evidenceCapturedOn: 2026-09-01 -assets: - - key: registry-column-too-short-for-its-own-path - file: ../../../final/evidence/rendered/registry-column-too-short-for-its-own-path.svg -evidence: - - ../../../final/evidence/raw/registry-column-too-short-for-its-own-path.txt -source: - - 원본 분석 절은 final/document.md#a05 §8.4 이다. ---- - -# 레지스트리 컬럼이 38자 경로에서 짧아 "더 짧은 경로를 적는" 우회를 유혹했다 - -능력 스키마 레지스트리의 스트림 경로 컬럼이 32자였다. 실제 스트림 경로가 그보다 길어 거절됐고, 오류 메시지는 컬럼 이름을 부르지 이유를 말하지 않았다. - -## 관계 - -- **독립 Flyway 스트림과 baseline version 0** - 이 레지스트리가 기록하는 대상이다. -- **두 트리가 다 V1부터 번호를 매겨 공유 history가 하나를 건너뛸 수 있었다** - 스트림 경로가 길어진 이유와 연결된다. - -## 문제 - -능력 스키마 레지스트리는 각 능력의 스키마 스트림 경로를 기록한다. 그 컬럼이 32자였다. - -능력별 스트림 경로는 그보다 길다. 능력 이름과 디렉터리 구조가 경로에 들어가기 때문이다. - -## 결론 - -컬럼이 자기가 기록해야 할 경로보다 짧았다. - -이 상태의 위험은 실패 자체가 아니라 우회의 유혹이다. 오류가 컬럼 길이를 말하므로, 가장 쉬운 대응은 경로를 짧게 바꾸는 것이다. 그러면 경로가 구조를 반영하지 않게 되고 다음 능력에서 같은 문제가 다시 난다. - -수정은 컬럼을 넓히는 것이었다. 그리고 두 위치가 같은 테이블을 만들기 때문에 양쪽 다 넓혀야 했다. - -마이그레이션 헤더가 그 이유를 적는다. 한쪽으로만 마이그레이션한 배포는 여전히 32자를 넘는 스트림 경로를 거절하고, 그 오류는 이유가 아니라 컬럼 이름을 부른다는 것이다. - -번호 선택에도 근거가 있다. 두 트리가 샘플 컴포지션에서 하나의 위치 목록으로 병합되므로 버전 공간을 공유한다. 샘플이 이미 몇 개 번호를 갖고 있어서 그것들을 피해야 했다. 중복 버전은 Flyway 가 해결하는 병합 충돌이 아니라 기동 거부다. - -## 검증 환경 - -데이터베이스 : PostgreSQL -마이그레이션 도구 : Flyway -확인 방식 : 마이그레이션 헤더의 사후 기록 확인 -소스 수정 : x - -## 재현 조건 - -1. 컬럼을 넓히는 마이그레이션의 헤더를 읽는다. 번호 선택 근거와 두 위치 문제가 적혀 있다. -2. 원래 컬럼 정의를 확인한다. -3. 같은 테이블을 만드는 다른 위치를 확인한다. - -## 본문 - - - -`schema_stream`이 `varchar(32)`였고 작성 당시 모든 스트림에 맞았으며 `'db/migration/jpa/notification-platform'`(38자)에서 안 맞기 시작했다. - -## 컬럼 폭이 맞지 않기 시작한 경로 - -:::evidence key="registry-column-too-short-for-its-own-path" alt="분석 문서 final/document.md#a05 에서 이 기록의 근거 절을 그대로 잘라낸 3줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a05 발췌 — 3줄" zoom="true" -::: - -## 실패 모드가 나쁜 종류다 - -모든 면에서 올바른 등록이 `value too long`으로 마이그레이션 타임에 실패하고, **뻔한 우회책은 스트림의 실제 경로가 아닌 더 짧은 경로를 기록하는 것**이며, 스키마가 어디서 왔는지에 대해 거짓말하는 레지스트리는 없는 것보다 나쁘다. - -## 128로 넓힌 이유 - -`capability_id`가 이미 `varchar(128)`이고 **하나의 bound가 두 개보다 추론하기 쉽다.** - -## 확인하지 못한 것 - -32자를 넘는 경로로 삽입해 거절을 재현하지 않았다. 이 기록은 마이그레이션 헤더에 근거한다. - -없음 - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-ownership-and-capability-streams/decision/decision-repair-is-not-a-mode.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-ownership-and-capability-streams/decision/decision-repair-is-not-a-mode.md deleted file mode 100644 index 3ac91bb..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-ownership-and-capability-streams/decision/decision-repair-is-not-a-mode.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -kind: PROJECT_DECISION -slug: repair-is-not-a-mode -title: Repair는 모드가 아니라 운영자가 호출하는 작업이다 -topic: schema-ownership-and-capability-streams -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: decision:repair-is-not-a-mode -decisionStatus: ADOPTED -decidedOn: 2026-08-30 -source: - - src/adapter/outbound/persistence-jpa/src/main/resources/db/migration - - src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlPersistenceConfig.java - - final/document.md#a05 ---- - -# Repair는 모드가 아니라 운영자가 호출하는 작업이다 - -## 결정문 - -마이그레이션 이력 복구는 애플리케이션 기동 시 자동으로 수행하지 않고, 운영자가 명시적으로 호출하는 작업으로 둔다. - -## 판단 이유 - -복구는 이력 테이블을 고치는 작업이다. 체크섬이 맞지 않거나 실패한 항목이 남아 있을 때 그것을 정리한다. - -그 작업이 기동 시 자동으로 돈다면, 체크섬 불일치가 발견되는 대신 지워진다. 그리고 체크섬 불일치는 대개 누군가 이미 적용된 마이그레이션 파일을 고쳤다는 신호다. - -즉 자동 복구는 알아야 할 사실을 감춘다. - -그리고 복구는 되돌릴 수 없다. 이력을 고치고 나면 원래 어떤 상태였는지 알 수 없다. - -그래서 운영자가 호출한다. 그 시점에 무엇이 어긋났는지 보고, 왜 어긋났는지 판단한 뒤에 실행한다. - -## 영향 - -감수하는 것 - -체크섬 불일치가 생기면 기동이 실패하고 사람이 개입해야 한다. 배포가 멈춘다. - -긴급 상황에서 복구 절차를 아는 사람이 필요하다. - -얻는 것 - -이미 적용된 마이그레이션이 수정되었다는 사실이 감춰지지 않는다. - -이력이 자동으로 고쳐지지 않으므로, 이력이 말하는 것과 실제가 다른 상태가 조용히 만들어지지 않는다. - -## 근거 - -- **적용된 마이그레이션의 checksum은 그것을 돌린 모든 배포에 대한 약속이다** - 이 결정이 지키려는 규칙이다. -- **Flyway가 스키마를 소유하고 런타임 롤은 DDL 권한을 갖지 않는다** - 같은 소유 원칙의 다른 면이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-ownership-and-capability-streams/reference/reference-an-applied-checksum-is-a-promise.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-ownership-and-capability-streams/reference/reference-an-applied-checksum-is-a-promise.md deleted file mode 100644 index c29c739..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-ownership-and-capability-streams/reference/reference-an-applied-checksum-is-a-promise.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -kind: REFERENCE -slug: an-applied-checksum-is-a-promise -title: 적용된 마이그레이션의 checksum은 그것을 돌린 모든 배포에 대한 약속이다 -topic: schema-ownership-and-capability-streams -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:an-applied-checksum-is-a-promise -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# 적용된 마이그레이션의 checksum은 그것을 돌린 모든 배포에 대한 약속이다 - -## 목적 - -이미 적용된 마이그레이션 파일을 수정해, 그것을 적용한 배포와 앞으로 적용할 배포가 서로 다른 스키마를 갖게 되는 것을 막는다. - -## 규칙 - -1. 적용된 파일은 수정하지 않는다 - 체크섬이 바뀌면 그 파일을 이미 적용한 배포에서 검증이 실패한다. - -2. 고쳐야 할 것은 새 마이그레이션으로 고친다 - 앞의 것을 되돌리거나 보정하는 마이그레이션을 새로 만든다. - -3. 새 마이그레이션은 순서에 무관하게 동작하도록 가드한다 - 여러 스트림이 같은 대상을 만들 수 있으면, 어느 쪽이 먼저 돌든 올바르게 끝나야 한다. - -4. 반복 비용을 두 번 내지 않는다 - 이미 처리된 대상은 건너뛴다. 테이블 재작성 같은 비싼 작업일수록 중요하다. - -5. 수정 이력을 헤더에 남긴다 - 왜 새 번호가 필요했는지가 그 파일에 있어야 다음 사람이 앞의 것을 고치려 하지 않는다. - -## 적용 조건 - -버전 기반 마이그레이션을 쓰는 모든 스키마 관리 - -여러 배포가 서로 다른 시점에 마이그레이션을 적용하는 환경 - -## 예외 - -아직 어떤 환경에도 적용되지 않은 마이그레이션은 수정할 수 있다. 그 판단에는 모든 환경을 확인했다는 근거가 필요하다. - -## 예시 - -컬럼 타입을 바꾸는 마이그레이션이 앞의 마이그레이션을 고치는 대신 새 번호로 추가되었고, 두 스트림의 순서가 고정되어 있지 않아 아직 옛 타입인 컬럼만 변환하도록 가드했다. - -컬럼을 넓히는 마이그레이션이 두 위치 모두에 필요했다. 한쪽만 적용한 배포는 옛 제약을 유지한다. - -## 관계 - -- **char(64)와 varchar(64) 불일치를 H2가 가리고 있었다** - 이 규칙을 따른 수정의 사례다. -- **레지스트리 컬럼이 38자 경로에서 짧아 더 짧은 경로를 적는 우회를 유혹했다** - 다섯 번째 규칙의 사례다. -- **Repair는 모드가 아니라 운영자가 호출하는 작업이다** - 체크섬 불일치를 다루는 결정이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/schema-ownership-and-capability-streams/reference/reference-each-stream-owns-its-history-table.md b/docs/clean-architecture-backend-template/tech-log-studio/schema-ownership-and-capability-streams/reference/reference-each-stream-owns-its-history-table.md deleted file mode 100644 index acab1fe..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/schema-ownership-and-capability-streams/reference/reference-each-stream-owns-its-history-table.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -kind: REFERENCE -slug: each-stream-owns-its-history-table -title: 마이그레이션 스트림은 자기 history 테이블을 갖는다 -topic: schema-ownership-and-capability-streams -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:each-stream-owns-its-history-table -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# 마이그레이션 스트림은 자기 history 테이블을 갖는다 - -## 목적 - -독립적으로 번호를 매기는 마이그레이션 트리들이 하나의 버전 공간을 공유해, 같은 번호가 충돌하거나 조용히 건너뛰어지는 것을 막는다. - -## 규칙 - -1. 스트림마다 위치와 히스토리 테이블을 함께 준다 - 위치만 나누고 히스토리를 공유하면 버전 공간은 여전히 하나다. - -2. 각 스트림은 자기 V1 부터 시작한다 - 다른 트리의 번호를 신경 쓰지 않아도 된다는 것이 이 구조의 목적이다. - -3. 위치 목록을 병합하는 구성이 있으면 그 목록이 하나의 버전 공간이다 - 병합되는 트리들끼리는 번호를 겹치지 않게 골라야 하고, 그 사실을 마이그레이션 헤더에 적는다. - -4. 중복 버전은 기동 거부다 - Flyway 가 병합해 주지 않는다. 배포 시점에 처음 발견된다. - -5. 여러 위치가 같은 테이블을 만들면 변경도 모든 위치에 적용한다 - 한쪽으로만 마이그레이션한 배포가 옛 제약을 유지하고, 그때 오류는 이유가 아니라 컬럼 이름을 부른다. - -## 적용 조건 - -능력별로 나뉜 마이그레이션 트리 - -리프마다 자기 마이그레이션을 갖는 모듈 구조 - -## 예외 - -한 팀이 하나의 트리만 관리하고 병합될 다른 트리가 없다면 단일 히스토리로 충분하다. - -## 예시 - -알림 스키마가 자기 위치와 자기 히스토리 테이블을 갖는다. 그래서 알림을 켜지 않은 배포에는 알림 히스토리 테이블도 없다. - -메시징 인박스와 아웃박스 리프가 같은 디렉터리 이름을 쓰고 둘 다 V2 를 만든다. - -샘플 컴포지션이 두 위치를 병합하므로 번호를 고를 때 다른 트리를 봐야 한다는 사실이 마이그레이션 헤더에 적혀 있다. - -## 관계 - -- **독립 Flyway 스트림과 baseline version 0** - 이 규칙이 나온 개념이다. -- **두 트리가 다 V1부터 번호를 매겨 공유 history가 하나를 건너뛸 수 있었다** - 이 규칙이 없을 때의 결과다. -- **messaging 마이그레이션 두 leaf가 같은 디렉터리에서 V2를 둘 만들었다** - 같은 형태가 다른 가족에서 남아 있는 사례다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/case/case-a06-f020-tls-stable.md b/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/case/case-a06-f020-tls-stable.md deleted file mode 100644 index 9134e7c..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/case/case-a06-f020-tls-stable.md +++ /dev/null @@ -1,199 +0,0 @@ ---- -kind: CASE -slug: a06-f020-tls-stable -title: 프로파일이 선언한 여섯 값이 드라이버에 하나도 도달하지 않는다 -topic: security-and-trust-boundaries -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a06-f020-tls-stable -evidenceCapturedOn: 2026-09-02 -body: case-a06-f020-tls-stable.body.md -assets: - - key: a06-f020-tls-stable - file: ../../../final/evidence/rendered/a06-f020-tls-stable.svg - - key: a06-f020-tls-stable-probe - file: ../../../final/evidence/rendered/a06-f020-tls-stable-probe.svg -evidence: - - ../../../final/evidence/raw/a06-f020-tls-stable.txt - - ../../../final/evidence/raw/a06-f020-tls-stable-probe.txt -source: - - 원본 분석 절은 final/document.md#a06#L1161 이다. 등급은 P1 이다. 팩토리의 javadoc, 호출자가 없다는 계수, 자격증명 해석기의 미도달, 실제 클라이언트를 Boot 가 만든다는 사실, 탐침이 잰 값들, 이미 고쳐진 형제, 그리고 기존 시험 둘이 이 경계를 보지 못한다는 판정이 그 절에 있다. - - 팩토리가 만들었을 설정의 여섯 값, 커스터마이저를 적용한 뒤에도 실제값이 그대로라는 것, ssl 속성이 두 번째 경로라는 것, README 의 속성 키가 이 Boot 버전에서 바인딩되지 않는다는 것, 그리고 프로파일을 선언하려면 토폴로지 프로브가 함께 있어야 한다는 것은 이 기록에서 확인했다. ---- - -# 프로파일이 선언한 여섯 값이 드라이버에 하나도 도달하지 않는다 - -프로파일의 값을 드라이버 설정으로 옮기는 클래스가 있고, 저장소 어디에도 그것을 부르는 코드가 없다. Boot 가 클라이언트를 만들 때 쓰는 설정은 TLS 가 꺼져 있고 타임아웃과 풀과 서버 API 와 UUID 표현이 전부 드라이버 기본값이다. - -## 관계 - -- **검증기가 운영에 TLS를 요구하고, 실제로 조립되는 생산자에는 그 설정이 없다** - 다른 리프에서 같은 형태가 난 사례다. -- **@Bean이 있다는 것은 조립 증거가 아니다** - 수리 코드가 배선되지 않은 형태다. -- **조립 결함을 판정하려면 조립하는 쪽을 먼저 읽어야 한다** - 이 사례를 찾는 방법이다. - -## 문제 - -값을 옮기는 클래스는 존재하고, 자기 javadoc 에 왜 만들어졌는지 적어 두었다. 프로파일과 자격증명 해석기와 TLS 와 Stable API 플래그와 풀과 타임아웃 정책이 전부 있었고 전부 단위 시험돼 있었는데 그중 어느 것도 설정에 도달하지 않았다는 것이다. - -같은 형태가 한 단계 뒤에서 반복된다. 그 클래스는 작성됐고, 그것을 부르는 배선은 없다. - -## 결론 - -참조가 자기 파일과 자기 시험 밖으로 나가지 않는다. 저장소 전체에서 남는 언급 넷은 안내 문서, 옛 리뷰, 생성된 API 표면 기준선이며 호출은 하나도 없다. 유일하게 그 클래스만 쓰는 자격증명 해석기도 같다. - -Boot 가 클라이언트를 만들 때 쓰는 설정을 재었다. 기본 빈에 등록된 커스터마이저 둘을 적용한 결과가 그 설정이다. 프로덕션 프로파일은 TLS 요구, 엄격한 서버 API 고정, 연결 2초, 서버 선택 3초, 풀 상한 40, 고정 UUID 표현을 말한다. 실제 설정은 TLS 꺼짐, 연결 10초, 서버 선택 30초, 풀 상한 100, 서버 API 없음, UUID 표현 미지정이다. 여섯이 전부 다르다. 같은 프로파일로 팩토리를 부르면 여섯이 전부 선언대로 나온다. - -첫 값만 성질이 다르다. 타임아웃과 풀 상한이 성능 문제라면 TLS 는 노출 문제다. - -TLS 를 켜는 경로는 둘이다. 배포가 URI 에 그 옵션을 넣거나, spring.mongodb.ssl 속성을 켜는 것이다. 어느 쪽이든 프로파일이 선언한 값과는 무관하게 배포가 한 번 더 말해야 한다. - -이 선언이 실제로 검증되는 배포는 아직 없다. 이 모듈 아래 출하 설정이 여는 키는 둘 — 활성화 여부와 활성 프로파일 이름이다. 프로파일 본체를 채워 주는 자리는 없다. 넣으려면 포크가 토폴로지 프로브 빈까지 함께 공급해야 하고, 넣지 않으면 기동이 그 사실을 말하며 거부한다. 지금 도는 배포에서 터지는 결함이 아니다. 프로파일이 선언되는 순간부터 성립한다. - -이 경계를 보는 시험은 없다. 팩토리 시험은 팩토리를 직접 만들어 그 출력이 프로파일대로인지 확인한다. 그 시험의 javadoc 은 타이핑된 프로파일이 드라이버가 실제로 만들어지는 설정에 도달한다고 적는데, 확인하는 것은 팩토리의 출력이고 그 팩토리는 아무도 부르지 않는다. TLS 레인 시험은 ssl.enabled(true) 를 손수 붙인 클라이언트로 서버가 TLS 를 강제하는지 확인한다. - -수리 수단은 이 리프에 이미 있다. 관측 쪽에도 설정 빌더 적용 지점에 호출자가 없던 시기가 있었다. 관측 쪽은 자동 구성이 Boot 커스터마이저를 빈으로 올려 해결했다. 프로덕션에서 그 수단이 등록되는 자리는 하나다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Spring Boot : 4.0.8 -확인 방식 : 참조 전수 계수, 실제 자동 구성을 올린 컨텍스트에서 커스터마이저 적용 후 설정 값 비교 -소스 수정 : x - -## 재현 조건 - -1. 클라이언트 설정 팩토리의 javadoc 을 읽는다. -2. 그 타입과 자격증명 해석기를 언급하는 곳을 저장소 전체에서 센다. -3. 프로파일 검증이 production 에 TLS 를 요구하는 자리와, 그 검증이 켜지려면 무엇이 있어야 하는지를 읽는다. -4. 출하 설정이 이 모듈 아래 노출하는 키와 프로파일을 넣는 곳의 수를 센다. -5. 이 리프의 자동 구성과 Boot 의 Mongo 자동 구성을 올린 컨텍스트를 띄운다. -6. 기본 설정 빈에 등록된 커스터마이저를 전부 적용하고 값을 읽는다. -7. URI 를 건드리지 않고 spring.mongodb.ssl.enabled 만 켜서 같은 값을 읽는다. -8. 같은 프로파일로 팩토리를 불러 만들어졌을 설정의 값을 읽는다. -9. 이 경계를 보는 시험이 있는지 확인한다. - -## 본문 - - - -값을 옮기는 클래스는 있다. 자기 javadoc 에 왜 만들어졌는지도 적혀 있다. - -## 설정을 만드는 팩토리 - -:::evidence key="a06-f020-tls-stable" alt="클라이언트 설정 팩토리의 javadoc, 저장소 전체에서 그 타입과 자격증명 해석기를 언급하는 곳 전수, 프로파일 검증이 production 에 TLS 를 요구하는 자리, 그 검증이 켜지려면 토폴로지 프로브가 필요하다는 기동 가드, 출하 설정이 이 모듈 아래 노출하는 키와 프로파일을 넣는 곳의 수, 이 리프가 관측 쪽에 쓴 Boot 빌더 커스터마이저 등록과 그 수단의 프로덕션 등록 수, 그리고 팩토리 시험의 javadoc 과 TLS 레인 시험이 쓰는 손수 만든 클라이언트를 출력한 터미널 기록." caption="팩토리와 해석기를 언급하는 곳은 문서 넷뿐이고 호출은 없음 · 검증은 프로파일이 선언됐을 때만 켜지고 토폴로지 프로브가 없으면 기동이 거부 · 출하 키는 enabled 와 active-profile 둘, 프로파일을 넣는 곳 0 · 빌더 커스터마이저의 프로덕션 등록은 1 · 두 시험 다 팩토리 출력이나 손수 만든 설정을 본다 — 69줄 · exit 0" zoom="true" -::: - -```java - *

The profile, the credential resolver, the TLS and Stable-API flags and the pool and timeout - * policy all existed and were all unit-tested. None of them reached a {@link MongoClientSettings}: - * the values were checked as intermediate objects and whatever the driver ended up configured with - * was decided elsewhere, by defaults nobody had chosen. A policy that nothing applies reads exactly - * like a policy that is applied — the tests pass, the record is populated, and the client connects - * with a three-second timeout it inherited from the driver rather than the two the profile states. -``` - -이 클래스를 참조하는 곳은 자기 파일과 자기 시험뿐이다. 저장소 전체를 훑으면 안내 문서와 옛 리뷰 문서와 생성된 API 표면 기준선의 언급 넷이 남고, 어느 것도 호출이 아니다. - -## 선언값과 실제값 - -:::evidence key="a06-f020-tls-stable-probe" alt="같은 URI 를 두 속성 키로 각각 넣어 얻은 접속 호스트, 프로덕션 프로파일이 선언하는 TLS 요구와 서버 API 고정과 두 타임아웃과 풀 상한과 UUID 표현, 그 선언을 설정 검증이 통과시키는 결과, 이 리프와 Boot 의 자동 구성을 올린 컨텍스트에서 커스터마이저를 전부 적용한 뒤 읽은 같은 값들, URI 를 건드리지 않고 ssl 속성만 켰을 때의 결과, 그리고 같은 프로파일로 팩토리를 불러 만들어졌을 설정의 같은 값들을 출력한 터미널 기록." caption="README 가 보여 주는 키는 호스트를 바꾸지 못하고 다른 키만 바꾼다 · 선언은 TLS 요구·엄격 서버 API·2초·3초·40·STANDARD · 커스터마이저 둘을 적용한 실제 설정은 TLS 꺼짐·10초·30초·100·없음·UNSPECIFIED · ssl 속성만 켜도 TLS 는 켜짐 · 팩토리가 만들었을 설정은 여섯이 전부 선언대로 — 21줄 · exit 0" zoom="true" -::: - -이 리프의 자동 구성과 Boot 의 Mongo 자동 구성을 올리고, 기본 설정 빈에 등록된 커스터마이저를 Boot 가 하는 대로 전부 적용한 뒤 값을 읽었다. - -```text -[선언] production 프로파일이 말하는 값 - tlsRequired=true stableApiStrict=true connectTimeout=2000ms serverSelection=3000ms poolMax=40 uuid=STANDARD - 이 선언을 설정 검증이 통과시킨다 - -[실제] Boot 가 클라이언트를 만들 때 쓰는 설정 (커스터마이저 적용 후) - 커스터마이저 2개 적용 - sslEnabled=false - connectTimeoutMs=10000 serverSelectionTimeoutMs=30000 - poolMaxSize=100 serverApi=null uuidRepresentation=UNSPECIFIED - -[미도달] 팩토리가 만들었을 설정 - sslEnabled=true - connectTimeoutMs=2000 serverSelectionTimeoutMs=3000 - poolMaxSize=40 serverApi=ServerApi{version=V1, deprecationErrors=true, strict=true} uuidRepresentation=STANDARD -``` - -여섯이 전부 다르다. 관측 커스터마이저가 등록된 상태에서도 같다 — 그 커스터마이저는 리스너만 붙이고 나머지에 손대지 않는다. `serverApi=null` 은 엄격한 서버 API 고정이 없다는 뜻이고, `uuidRepresentation=UNSPECIFIED` 는 팩토리가 저장된 문서 아래서 값이 움직이는 것을 아무도 쓰지 않은 마이그레이션이라 부르며 고정하려던 값이다. - -첫 값은 나머지와 성질이 다르다. 타임아웃과 풀 상한은 성능이고, TLS 는 노출이다. - -## TLS 를 켜는 경로는 프로파일이 아니다 - -```text - spring.mongodb.ssl.enabled=true -> sslEnabled=true -``` - -URI 를 건드리지 않고 속성만 켜도 TLS 가 켜진다. URI 에 옵션을 직접 넣는 경로도 있다. 어느 쪽이든 배포가 프로파일과 별개로 한 번 더 말해야 한다. - -덧붙여, 모듈 README 가 보여 주는 속성 키는 이 Boot 버전에서 바인딩되지 않는다. 같은 URI 를 두 키로 넣어 접속 호스트를 읽으면 갈린다. - -```text - spring.mongodb.uri -> [canary-host:31337] - spring.data.mongodb.uri -> [localhost:27017] -``` - -README 의 키를 그대로 넣은 배포는 기본 호스트로 뜬다. - -## 이 선언이 검증되는 배포는 아직 없다 - -프로파일 검증은 프로덕션 프로파일이 TLS 를 요구하지 않으면 거부하고, 그 메시지가 같은 결함의 이전 판을 적고 있다. - -```java -"a production MongoDB profile requires TLS; the flag existed and nothing checked it, so a" - + " deployment could carry tls-required=false and still be called production" -``` - -다만 그 검증은 프로파일이 선언됐을 때만 켜진다. 출하 설정이 이 모듈 아래 노출하는 키는 활성화 여부와 활성 프로파일 이름 둘뿐이고, 프로파일 자체를 넣는 곳은 저장소에 없다. 넣으려면 토폴로지 프로브 빈이 함께 있어야 한다. - -```java -if (probe.getIfAvailable() == null) { - throw new IllegalStateException( - "the Mongo platform has configured profiles but no MongoTopologyProbe bean, so the " - + "startup validator has nothing to ask about the server: … -``` - -그래서 이것은 지금 도는 배포의 결함이 아니라 조립하는 쪽이 프로파일을 선언하는 순간 성립하는 결함이다. - -## 이 경계를 보는 시험이 없다 - -```java -/** - * The typed profile reaches the settings the driver is actually built from (MNG-INT-002). -``` - -팩토리 시험의 javadoc 이다. 확인하는 것은 팩토리의 출력이고, 그 팩토리는 아무도 부르지 않는다. TLS 레인 시험은 반대쪽에서 같은 자리를 비껴간다. - -```java -MongoClientSettings.builder() - .applyConnectionString(new ConnectionString(connectionString)) - .applyToSslSettings(ssl -> ssl.enabled(true).context(trustOnly(authorityPem))) -``` - -손수 붙인 설정으로 서버가 TLS 를 강제하는지 확인한다. 어느 쪽도 프로파일의 선언이 실제 연결을 TLS 로 만드는지 묻지 않는다. - -## 이 리프가 이미 쓰는 배선 수단 - -```java -/** The customizer Boot applies when it builds the client. */ -@Bean -@ConditionalOnBean(MeterRegistry.class) -@ConditionalOnMissingBean(name = "mongoDriverObservabilityCustomizer") -public MongoClientSettingsBuilderCustomizer mongoDriverObservabilityCustomizer( -``` - -관측 쪽도 설정 빌더에 적용하는 자리에 호출자가 없는 같은 형태였고, 자동 구성이 Boot 의 빌더 커스터마이저를 등록해 고쳤다. 프로덕션에서 그 수단을 등록하는 곳은 이 한 자리뿐이다. 설정 쪽에는 쓰이지 않았다. - -## 확인하지 못한 것 - -실제 서버에 평문으로 연결되는 것을 관측하지 않았다. 확인한 것은 Boot 가 클라이언트를 만들 때 넘기는 설정 객체의 값이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-inbound-graphql-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-inbound-graphql-c01.md deleted file mode 100644 index daf53b8..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-inbound-graphql-c01.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-graphql-c01 -title: off 계약의 두 절반과 리터럴 404를 피한 단언 -topic: security-and-trust-boundaries -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-graphql-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-graphql-c01 - file: ../../../final/evidence/rendered/adapter-inbound-graphql-c01.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-graphql-c01.txt -source: - - 원본 분석 절은 final/document.md#a16#L130 이다. -module: adapter-inbound-graphql ---- - -# off 계약의 두 절반과 리터럴 404를 피한 단언 - -꺼짐 계약이 두 절반으로 나뉜다 — 이 leaf의 빈을 막는 절반과 프레임워크가 발행하는 라우트를 막는 절반. 뒤쪽 테스트는 리터럴 404가 아니라 매핑된 적 없는 경로의 상태 코드와 같은지를 본다. - -## 본문 - - - -꺼짐 계약이 두 절반으로 나뉜다. - -| 절반 | 무엇을 막는가 | 검증 | -|---|---|---| -| `GraphQlRootAutoConfiguration`의 `@ConditionalOnProperty` | 이 leaf의 39개 빈 | `GraphQlShippedAndGatedTest.graphQlOffHoldsNothing` — 빈 인벤토리 | -| `GraphQlOffAutoConfigurationImportFilter` | 프레임워크가 발행하는 `/graphql` 라우트 | `GraphQlShippedAndGatedTest.graphQlOffPublishesNoEndpoint` — 실제 포트 | - -## 리터럴 404를 단언하지 않는 이유 - -두 번째 테스트가 특히 정교하다. 리터럴 404를 단언하지 않고, 매핑된 적 없는 경로의 상태 코드와 **같은지**를 본다 — 주석이 그 이유를 적는다: "Asserting a literal 404 would have been wrong: the security filter chain runs before ...". - -## 그 테스트가 app-bootstrap에 있는 이유 - -그리고 그 테스트는 이 leaf가 아니라 app-bootstrap에 있다. off 계약은 출하 조립에서만 검증할 수 있으므로 옳은 위치다. - -## 분석 원문의 두 절반 표 - -:::evidence key="adapter-inbound-graphql-c01" alt="분석 문서 final/document.md#a16 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a16 발췌 — 15줄" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-inbound-web-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-inbound-web-c03.md deleted file mode 100644 index 2188be3..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-inbound-web-c03.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-web-c03 -title: security 패키지가 자기 안에서만 서로를 부른다 -topic: security-and-trust-boundaries -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-web-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-web-c03 - file: ../../../final/evidence/rendered/adapter-inbound-web-c03.svg - - key: adapter-inbound-web-c03-diagram - file: ../../../final/assets/diagrams/adapter-inbound-web-c03.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-web-c03.txt -source: - - 원본 분석 절은 final/document.md#a14#L426 이다. -module: adapter-inbound-web ---- - -# security 패키지가 자기 안에서만 서로를 부른다 - -신원 모델 타입들의 프로덕션 참조를 전수 세면 서로를 가리키는 것뿐이고 패키지 바깥에서 들어오는 화살표가 없다. 교차 테넌트 가드 `rejectTenantInput`도 그 섬 안에만 있다. - -## 본문 - - - -신원 모델 타입들의 프로덕션 참조를 전수 세면 이렇다. - -```text -WebSecurityContextBridge : main_refs=0 test_refs=1 -WebActorContextResolver : main_refs=1 test_refs=0 ← 참조자는 WebSecurityContextBridge 하나 -WebTenantContextResolver : main_refs=1 test_refs=1 ← 같음 -AuthenticationView : main_refs=3 test_refs=1 ← 전부 위 세 파일 -SecurityIdentity : main_refs=1 test_refs=1 -rejectTenantInput : main_refs=2 test_refs=1 ← 선언 + 브리지 오버로드. 세 번째 호출자 없음 -WebCorsPolicyValidator : main_refs=0 test_refs=1 -WebCsrfPolicyResolver : main_refs=0 test_refs=1 -``` - -`AuthenticationView`를 만드는 코드도 테스트뿐이다 — `WebSecurityContextBridgeTest`의 다섯 줄이 전부다. - -## 신원 모델이 닿는 범위 - -:::evidence key="adapter-inbound-web-c03-diagram" alt="security 패키지 경계 안에 세 타입이 들어 있고 바깥 프로덕션 호출자 상자가 빗금으로 경계 밖에 놓인 구조" caption="신원 모델이 닿는 범위" zoom="false" -::: - -즉 `security` 패키지 전체가 **자기 안에서만 서로를 부르는 닫힌 섬**이고, 바깥에서 들어오는 화살표가 없다. 교차 테넌트 가드 `rejectTenantInput`은 그 섬 안에만 있다. - -## AuthenticationView 참조 위치 - -:::evidence key="adapter-inbound-web-c03" alt="코드베이스에서 AuthenticationView 를 검색한 출력 21줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="AuthenticationView 코드베이스 검색 — 21줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-inbound-web-c04.md b/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-inbound-web-c04.md deleted file mode 100644 index 6978b33..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-inbound-web-c04.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-web-c04 -title: publicPaths가 먼저 등록되어 제한 경로 규칙을 덮는다 -topic: security-and-trust-boundaries -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-web-c04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-web-c04 - file: ../../../final/evidence/rendered/adapter-inbound-web-c04.svg - - key: adapter-inbound-web-c04-diagram - file: ../../../final/assets/diagrams/adapter-inbound-web-c04.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-web-c04.txt -source: - - 원본 분석 절은 final/document.md#a14#L473 이다. -module: adapter-inbound-web ---- - -# publicPaths가 먼저 등록되어 제한 경로 규칙을 덮는다 - -`SecurityConfig`가 `publicPaths`를 `RestrictedPathRule`보다 먼저 등록한다. Spring Security는 첫 일치가 이기므로, 주석이 말하는 순서는 `anyRequest()`에 대해서만 성립하고 `publicPaths`에 대해서는 반대다. - -## 본문 - - - -매처가 등록되는 순서는 이렇다. - -```java -// SecurityConfig.java:83-94 -.authorizeHttpRequests(auth -> { - if (publicPaths.length > 0) { auth.requestMatchers(publicPaths).permitAll(); } // ← 먼저 - for (RestrictedPathRule rule : restricted) { // ← 나중 - auth.requestMatchers(rule.pathPattern()).hasAnyAuthority(rule.authorities()); - } - auth.anyRequest().authenticated(); -}) -``` - -## 매처가 등록되는 순서 - -:::evidence key="adapter-inbound-web-c04-diagram" alt="공개 경로 매처와 제한 경로 규칙과 인증 요구 매처가 왼쪽에서 오른쪽으로 이어지고 화살표에 등록 순서가 붙은 구조" caption="매처가 등록되는 순서" zoom="false" -::: - -Spring Security는 첫 일치가 이긴다. 주석은 "Ordered before the authenticated catch-all: **a management path must be refused at the transport**"라고 하는데, 그 순서는 `anyRequest()`에 대해서만 성립하고 `publicPaths`에 대해서는 반대다. §12.3. - -## RestrictedPathRule 참조 위치 - -:::evidence key="adapter-inbound-web-c04" alt="코드베이스에서 RestrictedPathRule 를 검색한 출력 8줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RestrictedPathRule 코드베이스 검색 — 8줄 · exit 0" zoom="true" -::: - -## 실제로 등록되는 값 - -프로덕션 `RestrictedPathRule` 생산자는 하나다 — `FileserverAdminPlaneConfiguration:36`이 fileserver 관리 경로를 등록한다. `publicPaths`의 기본값은 `${SECURITY_PUBLIC_PATHS:${PRESENTATION_API_BASE_PATH:/v1}/healthcheck}`로, 환경변수 하나로 전체가 대체된다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-inbound-web-c07.md b/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-inbound-web-c07.md deleted file mode 100644 index cbb13d3..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-inbound-web-c07.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-web-c07 -title: 지문이 길이 프레이밍 없이 구분자로만 만들어진다 -topic: security-and-trust-boundaries -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-web-c07 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-web-c07 - file: ../../../final/evidence/rendered/adapter-inbound-web-c07.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-web-c07.txt -source: - - 원본 분석 절은 final/document.md#a14#L785 이다. -module: adapter-inbound-web ---- - -# 지문이 길이 프레이밍 없이 구분자로만 만들어진다 - -`SemanticRequestFingerprintFactory`는 U+001F 한 글자를 구분자로 쓰고 값에 이스케이프나 길이 접두사를 붙이지 않는다. 같은 저장소의 다른 다이제스트들은 4바이트 길이 프레이밍을 쓴다. - -## 본문 - - - -`SemanticRequestFingerprintFactory`는 U+001F 한 글자를 구분자로 쓰고, 경로 변수는 `SEP + name + "=" + value`, 헤더는 `SEP + name + ":" + value`로 이어붙인다. 값에 대한 이스케이프나 길이 접두사가 없다. - -## SemanticRequestFingerprintFactory 참조 위치 - -:::evidence key="adapter-inbound-web-c07" alt="코드베이스에서 SemanticRequestFingerprintFactory 를 검색한 출력 18줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="SemanticRequestFingerprintFactory 코드베이스 검색 — 18줄 · exit 0" zoom="true" -::: - -## 같은 저장소의 다른 다이제스트는 길이 프레이밍을 쓴다 - -notification `NotificationCatalogException.update`, messaging의 도메인 분리 상수는 **4바이트 길이 프레이밍**을 쓰고, 그 이유를 "인접 필드 연결로 인한 충돌이 구조적으로 불가능"으로 적는다. §20.3. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-outbound-httpclient-c07.md b/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-outbound-httpclient-c07.md deleted file mode 100644 index 4d0978e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-outbound-httpclient-c07.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-httpclient-c07 -title: 절대 URI를 정화하지 않고 거부한다 -topic: security-and-trust-boundaries -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-httpclient-c07 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-httpclient-c07 - file: ../../../final/evidence/rendered/adapter-outbound-httpclient-c07.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-httpclient-c07.txt -source: - - 원본 분석 절은 final/document.md#a11#L474 이다. -module: adapter-outbound-httpclient ---- - -# 절대 URI를 정화하지 않고 거부한다 - -`TrustedTargetPolicy`는 절대 URI를 정화하는 대신 거부한다. 목적지를 바꾸는 것은 다른 계층의 일이고 그 계층은 자기 정책·자격증명·DNS 검증을 갖는다. - -## 본문 - - - -`TrustedTargetPolicy`의 규칙 — "An absolute URI is **rejected here rather than sanitised**: H2 exists to vary method, relative path, query, approved headers, and body — not the destination. Changing the destination is what H3 is for, and H3 has its own policy, credentials, and DNS validation." - -## 템플릿이 거부되는 조건 - -`requireRelativeTemplate`가 빈 템플릿, `//` 시작, `://` 포함, `/`로 시작하지 않음을 거부한다. 확장은 문자열 연결이 아니라 Spring `DefaultUriBuilderFactory`의 `TEMPLATE_AND_VALUES` 인코딩이라 "a value containing `/`, `?`, or `#` cannot change the shape of the request." 그리고 확장 **후에** `requireAllowedOrigin`이 host/port allowlist를 다시 본다. - -## TrustedTargetPolicy 참조 위치 - -:::evidence key="adapter-outbound-httpclient-c07" alt="코드베이스에서 TrustedTargetPolicy 를 검색한 출력 12줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="TrustedTargetPolicy 코드베이스 검색 — 12줄 · exit 0" zoom="true" -::: - -## 리다이렉트 hop에 allowlist를 다시 적용하는 이유 - -멱등성 키 처리에 수정 이력 둘이 붙어 있다. 그리고 리다이렉트 hop에 allowlist를 다시 적용하는 `requireAllowedTarget`이 public인 이유도 적혀 있다 — 조정자가 이전에는 리다이렉트 정책만 보고 프로파일 allowlist를 보지 않아 "An upstream could therefore redirect a trusted profile to any origin the redirect policy tolerated, including one the operator had explicitly excluded." - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-outbound-httpclient-c09.md b/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-outbound-httpclient-c09.md deleted file mode 100644 index f413766..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-outbound-httpclient-c09.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-httpclient-c09 -title: TLS 실패가 CONNECT로 분류된 원인은 픽스처의 듀얼스택 호스트명이다 -topic: security-and-trust-boundaries -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-httpclient-c09 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-httpclient-c09 - file: ../../../final/evidence/rendered/adapter-outbound-httpclient-c09.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-httpclient-c09.txt -source: - - 원본 분석 절은 final/document.md#a11#L627 이다. -module: adapter-outbound-httpclient ---- - -# TLS 실패가 CONNECT로 분류된 원인은 픽스처의 듀얼스택 호스트명이다 - -이 절은 이전 사이클이 여기에 적었던 P1 진단을 철회하고 교체한다. 관측된 실패는 그대로 재현되지만, 원인으로 지목했던 기전은 측정으로 반증되었다. 근거는 `EVD-332`다. - -## 본문 - - - -이 절은 이전 사이클이 여기에 적었던 **P1 진단을 철회하고 교체한다**. 관측된 실패는 그대로 재현되지만, 그 원인으로 지목했던 기전은 측정으로 반증되었다. 근거는 `EVD-332`다. - -## 관측은 그대로다 - -`:adapter:outbound:httpclient:test` 는 HEAD 에서도 **283 중 3건 실패**한다. 세 건 모두 `MutualTlsHandshakeContractTest.java:168` — `assertThat(classified.stage()).isEqualTo(TLS_HANDSHAKE)` 다. 바로 앞줄인 167행(`evidence == NOT_SENT`)은 통과한다. - -## 철회하는 진단 - -이전 진단은 `ApacheFailureClassifier.recognize`의 분기 순서(pool → DNS → CONNECT → TLS)를 읽고 사슬의 모양을 추론한 것이지, 사슬을 실제로 떠본 것이 아니다. 잡힌 예외를 그대로 출력하면 사슬에 `SSLHandshakeException`이 **없다**. 그림자에 가려진 것이 아니라 애초에 도착하지 않았다. 분류기는 자기가 받은 것을 정확히 분류했다. - -## ApacheFailureClassifier 참조 위치 - -:::evidence key="adapter-outbound-httpclient-c09" alt="코드베이스에서 ApacheFailureClassifier 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ApacheFailureClassifier 코드베이스 검색 — 5줄 · exit 0" zoom="true" -::: - -## 두 주소 중 마지막 것만 호출자에게 도달한다 - -동일한 서버 객체, 동일한 클라이언트 신뢰재료. `baseUrl`의 호스트 문자열만 바꿨다. 이 컨테이너의 `/etc/hosts`는 `localhost`를 두 패밀리에 준다. `MockWebServer`는 IPv4 루프백에만 바인딩하고, `MockHttpServer.uri()`는 호스트명 `localhost`를 돌려준다 (`MockHttpServer.java:70-72`). - -```text -127.0.0.1 → TCP 성공 → TLS 핸드셰이크 실패(진짜 실패) → 삼켜짐 -::1 → TCP 거부(듣는 소켓 없음) → 마지막 주소 → HttpHostConnectException 으로 승격 -``` - -Apache HttpClient 5의 연결 오퍼레이터는 해석된 주소를 순회하면서 **마지막이 아닌 주소의 실패를 삼킨다**. 호출자에게 도달하는 유일한 예외는 두 번째 주소의 연결 거부다. - -## 성공하는 테스트가 통과하는 이유도 같은 루프다 - -127.0.0.1 에서 성공하면 루프가 즉시 반환하므로 `::1`을 시도하지 않는다. 따라서 처음 눈에 띄었던 `startTls(..., true/false)` 차이는 원인이 아니라 상관관계였다 — 실패하는 케이스가 곧 두 번째 주소까지 가는 케이스다. `startTls(..., false)`에서 서버가 뜨지 않는다는 가설도 함께 기각했다. 두 경우 모두 원시 소켓 접속이 성공한다(`raw 127.0.0.1: OK`, `raw localhost: OK`). - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-outbound-notification-c04.md b/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-outbound-notification-c04.md deleted file mode 100644 index 0c80c48..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-outbound-notification-c04.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-notification-c04 -title: unhealthy 조건 넷에 DRAINING이 없다 -topic: security-and-trust-boundaries -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-notification-c04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-notification-c04 - file: ../../../final/evidence/rendered/adapter-outbound-notification-c04.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-notification-c04.txt -source: - - 원본 분석 절은 final/document.md#a13#L619 이다. -module: adapter-outbound-notification ---- - -# unhealthy 조건 넷에 DRAINING이 없다 - -`NotificationHealthReporter.snapshot()`이 `healthy = false`로 넘어가는 조건은 넷이고, `DRAINING`은 그 목록에 없다. 로테이션 중 드레인은 정상 운영이므로 그 자체로는 옳다. - -## 본문 - - - -`NotificationHealthReporter.snapshot()`이 `healthy = false`로 넘어가는 조건은 넷이다. - -| 조건 | 위치 | -|---|---| -| 감시 대상 프로파일이 레지스트리에 없음 (`UNREGISTERED`) | `:57-62` | -| 상태가 `AUTHENTICATION_FAILED` 또는 `DISABLED` | `:64-67` | -| provider는 있는데 라우팅된 채널이 하나도 없음 | `:78-80` | -| 서빙 상태가 선언된 임계치를 넘음 | `:83-85` | - -## 세 번째 조건에 붙은 자기고발 - -"A platform with providers but no route accepts every request and delivers none. It was reported healthy because every runtime was healthy — **which was true and beside the point**." - -## 분석 원문의 조건 표 - -:::evidence key="adapter-outbound-notification-c04" alt="분석 문서 final/document.md#a13 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a13 발췌 — 15줄" zoom="true" -::: - -## DRAINING이 목록에 없다 - -여기서 눈에 띄는 것은 **`DRAINING`이 목록에 없다**는 점이다. 로테이션 중 드레인은 정상 운영이므로 그 자체로는 옳다. 그러나 §13의 P2와 겹치면 부작용이 하나 더 생긴다 — 아래 §21.2. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-outbound-objectstorage-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-outbound-objectstorage-c03.md deleted file mode 100644 index ea70039..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-outbound-objectstorage-c03.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-objectstorage-c03 -title: ObjectReference가 보장하는 route 구획 -topic: security-and-trust-boundaries -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-objectstorage-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-objectstorage-c03 - file: ../../../final/evidence/rendered/adapter-outbound-objectstorage-c03.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-objectstorage-c03.txt -source: - - 원본 분석 절은 final/document.md#a09#L135 이다. -module: adapter-outbound-objectstorage ---- - -# ObjectReference가 보장하는 route 구획 - -`RoutingObjectReadAdapter.load`는 `canonicalText`를 분리한 뒤 두 번째 구획을 직접 읽는다. 별도 인덱스 검사는 없지만 `ObjectReference` 생성자가 `ObjectIdentitySupport.requireRouted(canonicalText, "osr1")`로 형식을 강제하므로, 유효한 참조에는 route 구획이 항상 존재한다. - -## 본문 - - - -## 직접 인덱싱과 입력 불변식 - -`RoutingObjectReadAdapter.load`는 `reference.canonicalText().split("\\.", -1)[1]`로 route token을 읽는다. 이 코드만 보면 두 번째 구획이 없을 때 `ArrayIndexOutOfBoundsException`이 발생할 수 있다. - -## RoutingObjectReadAdapter 참조 위치 - -:::evidence key="adapter-outbound-objectstorage-c03" alt="코드베이스에서 RoutingObjectReadAdapter 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RoutingObjectReadAdapter 코드베이스 검색 — 5줄 · exit 0" zoom="true" -::: - -## ObjectReference가 보장하는 형식 - -`ObjectReference` 생성자는 `ObjectIdentitySupport.requireRouted(canonicalText, "osr1")`를 통해 routed 형식을 먼저 검증한다. 따라서 정상적으로 생성된 `ObjectReference`가 `load`에 전달되는 경로에서는 두 번째 구획이 값 타입의 불변식으로 보장된다. 현재 계약에서는 이 split 자체를 결함으로 분류할 근거가 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-outbound-persistence-jpa-c53.md b/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-outbound-persistence-jpa-c53.md deleted file mode 100644 index c32b6cc..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-outbound-persistence-jpa-c53.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c53 -title: 가드의 존재가 곧 테넌트 격리 보장은 아니다 -topic: security-and-trust-boundaries -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c53 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c53 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c53.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c53.txt -source: - - 원본 분석 절은 final/document.md#a05#L3784 이다. -module: adapter-outbound-persistence-jpa ---- - -# 가드의 존재가 곧 테넌트 격리 보장은 아니다 - -`TenantAwareRepositoryGuard`와 `TenantEntityListenerGuard`의 local behavior는 fail-closed다. 그러나 현재 production repository/entity에 연결된 caller/listener registration이 없다. - -## 본문 - - - -`TenantAwareRepositoryGuard`와 `TenantEntityListenerGuard`의 local behavior는 fail-closed다. - -## TenantAwareRepositoryGuard 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c53" alt="코드베이스에서 TenantAwareRepositoryGuard 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="TenantAwareRepositoryGuard 코드베이스 검색 — 3줄 · exit 0" zoom="true" -::: - -## 존재만으로 격리를 주장하지 않는 이유 - -현재 production repository/entity에 연결된 caller/listener registration은 없다. 따라서 이 type들이 존재한다는 이유만으로 현재 application의 tenant isolation이 보장된다고 쓰지 않는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-outbound-support-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-outbound-support-c03.md deleted file mode 100644 index a6839d9..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-outbound-support-c03.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-support-c03 -title: 로그 마스킹은 PII 차단 보장을 복구하지 않는다 -topic: security-and-trust-boundaries -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-support-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-support-c03 - file: ../../../final/evidence/rendered/adapter-outbound-support-c03.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-support-c03.txt -source: - - 원본 분석 절은 final/document.md#a04#L207 이다. -module: adapter-outbound-support ---- - -# 로그 마스킹은 PII 차단 보장을 복구하지 않는다 - -`app-bootstrap`의 `LogMaskingPatterns`는 방어 심층화로 몇 가지 secret 형태를 mask하지만 arbitrary email address나 free-form body PII를 일반적으로 제거하는 규칙은 없다. - -## 본문 - - - -`app-bootstrap`의 `LogMaskingPatterns`는 방어 심층화로 다음과 같은 secret 형태를 mask한다. - -- password/secret/token/api-key 계열 key=value -- Authorization credentials -- standalone Bearer token - -## LogMaskingPatterns 참조 위치 - -:::evidence key="adapter-outbound-support-c03" alt="코드베이스에서 LogMaskingPatterns 를 검색한 출력 29줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="LogMaskingPatterns 코드베이스 검색 — 29줄 · exit 0" zoom="true" -::: - -## 마스킹 규칙이 덮지 않는 것 - -arbitrary email address나 free-form body PII를 일반적으로 제거하는 규칙은 없다. app-bootstrap README 자체도 regex masking을 **보증이 아니라 defence-in-depth**라고 설명한다. 따라서 현재 "logger signature 때문에 PII가 들어올 수 없다"는 1차 방어선 설명은 사실과 맞지 않는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-outbound-support-c05.md b/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-outbound-support-c05.md deleted file mode 100644 index 6696979..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-adapter-outbound-support-c05.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-support-c05 -title: outbound 어댑터끼리는 support를 통해서만 공유한다 -topic: security-and-trust-boundaries -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-support-c05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-support-c05 - file: ../../../final/evidence/rendered/adapter-outbound-support-c05.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-support-c05.txt -source: - - 원본 분석 절은 final/document.md#a04#L368 이다. -module: adapter-outbound-support ---- - -# outbound 어댑터끼리는 support를 통해서만 공유한다 - -`CleanArchitectureTest.OUTBOUND_ADAPTERS_ARE_PEERS_SHARING_ONLY_SUPPORT`가 outbound adapter family를 slice로 나누고 서로 직접 의존하지 못하게 한다. 유일한 shared-code 예외는 target package가 `..adapter.outbound.support..`인 dependency다. - -## 본문 - - - -`CleanArchitectureTest.OUTBOUND_ADAPTERS_ARE_PEERS_SHARING_ONLY_SUPPORT`는 outbound adapter family를 slice로 나누고 서로 직접 의존하지 못하게 한다. 유일한 shared-code 예외는 target package가 이것인 dependency다. - -```text -..adapter.outbound.support.. -``` - -## 규칙이 허용하는 것과 금지하는 것 - -따라서 messaging → notification 같은 peer coupling은 금지하지만 messaging → support는 허용한다. fresh `CleanArchitectureTest --rerun-tasks`도 통과했다. - -## 분석 원문의 규칙 서술 - -:::evidence key="adapter-outbound-support-c05" alt="분석 문서 final/document.md#a04 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a04 발췌 — 15줄" zoom="true" -::: - -## 이 규칙이 고정하는 것 - -support 모듈이 단순 편의 library가 아니라 **outbound family에서 sanctioned shared dependency point**라는 점을 build-time fitness function으로 고정한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-app-bootstrap-c05.md b/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-app-bootstrap-c05.md deleted file mode 100644 index df80af3..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-app-bootstrap-c05.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: app-bootstrap-c05 -title: 레지스트리 계약 테스트가 실제 파일을 읽어 대조한다 -topic: security-and-trust-boundaries -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:app-bootstrap-c05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: app-bootstrap-c05 - file: ../../../final/evidence/rendered/app-bootstrap-c05.svg -evidence: - - ../../../final/evidence/raw/app-bootstrap-c05.txt -source: - - 원본 분석 절은 final/document.md#a18#L460 이다. -module: app-bootstrap ---- - -# 레지스트리 계약 테스트가 실제 파일을 읽어 대조한다 - -`MasterSwitchRegistryContractTest`가 `docs/registries/env-keys.yaml`과 `src/app-bootstrap/src/main/resources/application.yml`을 실제로 읽어 대조한다. 파일 기반 SSOT가 테스트로 고정돼 있다. - -## 본문 - - - -`MasterSwitchRegistryContractTest`가 `docs/registries/env-keys.yaml`과 `src/app-bootstrap/src/main/resources/application.yml`을 실제로 읽어 대조한다(§2). `ErrorCodeRegistryMappingTest`·`SecretsClassificationRegistryTest`도 `docs/registries/` 아래 파일을 읽는다. - -## MasterSwitchRegistryContractTest 참조 위치 - -:::evidence key="app-bootstrap-c05" alt="코드베이스에서 MasterSwitchRegistryContractTest 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MasterSwitchRegistryContractTest 코드베이스 검색 — 3줄 · exit 0" zoom="true" -::: - -## 파일 기반 SSOT가 테스트로 고정돼 있다 - -레지스트리 파일이 문서로만 존재하지 않고, 그 내용과 실제 설정 파일이 어긋나면 테스트가 먼저 깨진다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-application-core-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-application-core-c02.md deleted file mode 100644 index 329ce5f..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-application-core-c02.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -kind: CONCEPT -slug: application-core-c02 -title: 권한 판정과 객체 접근 판정을 분리한다 -topic: security-and-trust-boundaries -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:application-core-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: application-core-c02 - file: ../../../final/evidence/rendered/application-core-c02.svg - - key: application-core-c02-diagram - file: ../../../final/assets/diagrams/application-core-c02.svg -evidence: - - ../../../final/evidence/raw/application-core-c02.txt -source: - - 원본 분석 절은 final/document.md#a03#L78 이다. -module: application-core ---- - -# 권한 판정과 객체 접근 판정을 분리한다 - -`AuthorizationPort`는 "이 종류의 작업을 수행할 수 있는가"를 판정하고, object-level access는 별도 `ObjectAccessPolicy`가 담당한다. 같은 permission을 가진 사용자라도 ownership, membership, workflow state에 따라 결과가 달라질 수 있기 때문이다. - -## 관계 - -- **legacy storage/notification compatibility surface의 제거 조건 추적** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -`AuthorizationPort`는 principal의 raw role/permission을 기준으로 "이 종류의 작업을 수행할 수 있는가"를 판정하는 framework-free PEP다. `AuthorizationPrincipal`은 role set을 defensive copy + unmodifiable로 만들고 null roles는 empty set으로 정규화한다. `AuthorizationDeniedException`은 Spring `AccessDeniedException` 대신 application-owned failure를 사용한다. - -## 두 판정이 갈리는 자리 - -:::evidence key="application-core-c02-diagram" alt="요청에서 권한 포트와 객체 접근 정책으로 각각 화살표가 나가고 화살표에 작업 종류와 개별 객체가 붙은 구조" caption="두 판정이 갈리는 자리" zoom="false" -::: - -object-level access는 별도 `ObjectAccessPolicy`가 담당한다. 같은 permission을 가진 사용자라도 ownership, membership, workflow state에 따라 특정 object 접근 결과가 달라질 수 있기 때문이다. `ObjectAccessDecision`은 denial에 stable code를 요구하고 `hideExistence`를 별도 boolean으로 보존해 transport가 403/404 disclosure 정책을 추측하지 않게 한다. - -## AuthorizationPort 참조 위치 - -:::evidence key="application-core-c02" alt="코드베이스에서 AuthorizationPort 를 검색한 출력 28줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="AuthorizationPort 코드베이스 검색 — 28줄 · exit 0" zoom="true" -::: - -## 이 계약이 adapter에서 core로 옮겨 온 이력 - -`ObjectAccessPolicyTest`에는 이 계약이 과거 inbound GraphQL adapter에 있었고 GraphQL request context를 signature에 포함해 application-core가 구현하려면 transport에 역의존해야 했던 문제가 기록돼 있다. 현재 regression test는 policy/request/decision signature에 `dev.caskeleton.adapter.*` 타입이 다시 등장하면 실패한다. 이 프로젝트에서 "여러 호출자가 공유해야 하는 계약을 inbound adapter가 소유하면 Core가 Adapter에 의존하게 된다"는 문제가 실제로 있었던 근거다. - -## 계약에 명시된 한계 - -`decideAll()`의 default는 요청 순서를 보존하지만 object마다 `decide()`를 호출한다. set-based authorization을 제공하는 구현체가 override하지 않으면 batched loading 안에서 authorization N+1을 다시 만들 수 있다는 제한도 계약에 명시돼 있다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-grpc-advanced-bootstrap-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-grpc-advanced-bootstrap-c03.md deleted file mode 100644 index df7543c..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-grpc-advanced-bootstrap-c03.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -kind: CONCEPT -slug: grpc-advanced-bootstrap-c03 -title: 승격 게이트는 능력마다 따로 기록된 증거를 본다 -topic: security-and-trust-boundaries -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:grpc-advanced-bootstrap-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-advanced-bootstrap-c03 - file: ../../../final/evidence/rendered/grpc-advanced-bootstrap-c03.svg -evidence: - - ../../../final/evidence/raw/grpc-advanced-bootstrap-c03.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-advanced-bootstrap#L89 이다. -module: grpc-advanced-bootstrap ---- - -# 승격 게이트는 능력마다 따로 기록된 증거를 본다 - -증거를 능력마다 따로 기록하는 이유가 적혀 있다 — 공유 기록은 하나를 승격할 때 같은 시점에 측정된 다른 것들까지 함께 승격시킨다. - -## 본문 - - - -증거는 능력마다 따로 기록된다. - -> "a shared record makes promoting one of them promote whichever others happened to be measured at the same time." - -일곱 항목(호환성·보안 검토·고장·성능·ADR·런북·실환경 테스트)과 담금 기간을 본다. - -## 담금 기간이 둘인 이유 - -```text -ADVANCED_STABLE_SOAK = 7일 -STABLE_DEFAULT_SOAK = 30일 -``` - -> "becoming a Stable default means every deployment gets it, which additionally puts its dependencies on every classpath and its failure modes in every on-call rotation." - -## GrpcAdvancedSupportMatrix 참조 위치 - -:::evidence key="grpc-advanced-bootstrap-c03" alt="코드베이스에서 GrpcAdvancedSupportMatrix 를 검색한 출력 6줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcAdvancedSupportMatrix 코드베이스 검색 — 6줄 · exit 0" zoom="true" -::: - -## 시작 등급을 다시 확인하는 이유 - -그리고 `WATCH` 는 `EXPERIMENTAL` 을 먼저 거쳐야 한다. `GrpcAdvancedSupportMatrix.apply` 는 결정의 시작 등급이 현재 등급과 다르면 거부한다 — 두 승격이 경합했거나 하나가 재생된 경우다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-grpc-advanced-diagnostics-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-grpc-advanced-diagnostics-c01.md deleted file mode 100644 index af83be4..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-grpc-advanced-diagnostics-c01.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -kind: CONCEPT -slug: grpc-advanced-diagnostics-c01 -title: Channelz는 유용해서 위험한 표면이다 -topic: security-and-trust-boundaries -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:grpc-advanced-diagnostics-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-advanced-diagnostics-c01 - file: ../../../final/evidence/rendered/grpc-advanced-diagnostics-c01.svg - - key: grpc-advanced-diagnostics-c01-diagram - file: ../../../final/assets/diagrams/grpc-advanced-diagnostics-c01.svg -evidence: - - ../../../final/evidence/raw/grpc-advanced-diagnostics-c01.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-advanced-diagnostics#L41 이다. -module: grpc-advanced-diagnostics ---- - -# Channelz는 유용해서 위험한 표면이다 - -이 리프는 진단 표면(Channelz·CSDS)과 그것을 게시 가능하게 만드는 편집기, 그리고 고급 능력의 검증 대상을 이름 짓는 테스트킷 계약을 담는다. 편집기가 필요한 이유가 편집기 자신의 javadoc에 적혀 있다. - -## 본문 - - - -진단 표면(Channelz·CSDS)과 그것을 게시 가능하게 만드는 편집기, 그리고 고급 능력이 무엇을 상대로 검증되어야 하는지를 이름 짓는 테스트킷 계약을 담는다. - -## 이 리프가 담는 것 - -:::evidence key="grpc-advanced-diagnostics-c01-diagram" alt="리프 경계 안에 진단 표면과 편집기와 테스트킷 계약 세 상자가 나란히 들어 있는 구조" caption="이 리프가 담는 것" zoom="false" -::: - -## 편집기가 필요한 이유 - -편집기 javadoc 이 왜 이것이 필요한지 적는다. - -> "Channelz is unusually dangerous to expose because it is genuinely useful: it holds every socket's local and remote address, the security details of each connection, and per-call state… once the endpoint exists the whole of it is one authorization mistake away from being readable." - -## 이 기록이 다루는 파일 범위 - -:::evidence key="grpc-advanced-diagnostics-c01" alt="코드베이스에서 파일 목록을 만든 출력 4줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 4줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-grpc-observability-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-grpc-observability-c01.md deleted file mode 100644 index 59d5543..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-grpc-observability-c01.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -kind: CONCEPT -slug: grpc-observability-c01 -title: 논리 RPC와 물리 시도와 스트림 수명주기를 구별한다 -topic: security-and-trust-boundaries -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:grpc-observability-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-observability-c01 - file: ../../../final/evidence/rendered/grpc-observability-c01.svg - - key: grpc-observability-c01-diagram - file: ../../../final/assets/diagrams/grpc-observability-c01.svg -evidence: - - ../../../final/evidence/raw/grpc-observability-c01.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-observability#L46 이다. -module: grpc-observability ---- - -# 논리 RPC와 물리 시도와 스트림 수명주기를 구별한다 - -관측을 세 층위로 나눈다. Micrometer를 `api`로 노출하는 이유도 build.gradle에 적혀 있고 코드와 일치한다. - -## 본문 - - - -build.gradle 주석이 이 리프의 범위를 적는다. - -```groovy -// build.gradle:3-5 -// Bounded observability: logical RPC vs physical attempt vs stream lifecycle, with a cardinality -// policy that refuses payload, raw metadata and any actor/tenant/object/stream/idempotency -// identifier as a tag. -``` - -## 구별하는 세 층위 - -:::evidence key="grpc-observability-c01-diagram" alt="리프 경계 안에 논리 RPC 와 물리 시도와 스트림 수명주기 세 상자가 나란히 들어 있는 구조" caption="구별하는 세 층위" zoom="false" -::: - -## Micrometer를 api로 노출하는 이유 - -build.gradle 에 적혀 있다 — "the observation convention's public signatures name Micrometer types, so wiring it requires naming them." 실제로 `GrpcObservationConvention` 의 생성자와 `boundedTags` 반환형이 Micrometer 타입(`MeterRegistry`, `Tags`)이므로 그 서술은 코드와 일치한다. - -## GrpcObservationConvention 참조 위치 - -:::evidence key="grpc-observability-c01" alt="코드베이스에서 GrpcObservationConvention 를 검색한 출력 12줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcObservationConvention 코드베이스 검색 — 12줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-grpc-spring-boot-starter-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-grpc-spring-boot-starter-c01.md deleted file mode 100644 index fb9f570..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-grpc-spring-boot-starter-c01.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -kind: CONCEPT -slug: grpc-spring-boot-starter-c01 -title: Advanced 의존 금지를 세 겹으로 강제한다 -topic: security-and-trust-boundaries -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:grpc-spring-boot-starter-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-spring-boot-starter-c01 - file: ../../../final/evidence/rendered/grpc-spring-boot-starter-c01.svg - - key: grpc-spring-boot-starter-c01-diagram - file: ../../../final/assets/diagrams/grpc-spring-boot-starter-c01.svg -evidence: - - ../../../final/evidence/raw/grpc-spring-boot-starter-c01.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-spring-boot-starter#L42 이다. -module: grpc-spring-boot-starter ---- - -# Advanced 의존 금지를 세 겹으로 강제한다 - -이 리프가 `:grpc-advanced:*`에 닿으면 안 된다는 규칙이 주석이 아니라 세 지점에서 각각 강제된다 — 레지스트리, 빌드 검증 태스크, 자바 쪽 단언. - -## 본문 - - - -build.gradle 주석이 이 리프의 경계를 적는다. - -```groovy -// build.gradle:3-7 -// The platform's composition boundary: typed properties, auto-configuration and the startup -// validator that refuses a deployment whose configuration contradicts a Stable invariant. -// -// It must never reach `:grpc-advanced:*`. That is not a comment — the registry's -// allowed_dependencies for this leaf omits every advanced id, `verifyCleanArchitectureDependencies` -// enforces it, and GrpcPlatformStartupValidatorTest asserts the same rule from the Java side. -``` - -## 한 규칙을 지키는 세 지점 - -:::evidence key="grpc-spring-boot-starter-c01-diagram" alt="Advanced 의존 금지 규칙에서 레지스트리와 빌드 검증 태스크와 자바 단언 세 상자로 화살표가 나가는 구조" caption="한 규칙을 지키는 세 지점" zoom="false" -::: - -격리 규칙은 세 겹이다 — 레지스트리, 빌드 검증 태스크, 그리고 자바 쪽 단언. 세 번째는 `validateAdvancedIsolation` 이 `GrpcStableBuildInvariant.requireNoAdvancedDependency` 를 부르는 형태다. - -## GrpcStableBuildInvariant 참조 위치 - -:::evidence key="grpc-spring-boot-starter-c01" alt="코드베이스에서 GrpcStableBuildInvariant 를 검색한 출력 10줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcStableBuildInvariant 코드베이스 검색 — 10줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-messaging-admin-api-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-messaging-admin-api-c02.md deleted file mode 100644 index f797f79..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-messaging-admin-api-c02.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-admin-api-c02 -title: 부팅된 애플리케이션에서 살아나는 타입은 둘뿐이다 -topic: security-and-trust-boundaries -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-admin-api-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-admin-api-c02 - file: ../../../final/evidence/rendered/messaging-admin-api-c02.svg -evidence: - - ../../../final/evidence/raw/messaging-admin-api-c02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-admin-api#L98 이다. -module: messaging-admin-api ---- - -# 부팅된 애플리케이션에서 살아나는 타입은 둘뿐이다 - -leaf 밖 소비자는 21개 파일 4개 모듈이지만, 부팅된 애플리케이션에서 실제로 살아나는 것은 둘뿐이다. `MessagingAdminService` 빈도 `ApprovalVerifier` 빈도 없다. - -## 본문 - - - -`messaging-core-api` 에서 쓰는 것: `DestinationName`, `MessageAuthorizationException`, `MessagingConfigurationException`. `messaging-policy` 는 `api` 로 선언돼 있지만 import 0건이다(§12.4). - -## leaf 밖 소비자 21개 파일 - -| 모듈 | src/main | src/test | 역할 | -|---|---:|---:|---| -| `messaging-admin-runtime` | 10 | 6 | 실제 실행·검증 서비스 | -| `messaging-spring-boot-starter` | 2 | 0 | 빈 배선 + 저널 내구성 검사 | -| `messaging-outbox-jdbc-postgresql` | 1 | 1 | `JdbcAdminOperationJournal` | -| `messaging-kafka` | 0 | 1 | `KafkaTopologyValidationIT` | - -## DestinationName 참조 위치 - -:::evidence key="messaging-admin-api-c02" alt="코드베이스에서 DestinationName 를 검색한 출력 16줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DestinationName 코드베이스 검색 — 16줄 · exit 0" zoom="true" -::: - -## 빈이 없는 것이 설계인 범위 - -부팅된 애플리케이션에서 이 리프의 타입 중 실제로 살아나는 것은 **둘뿐**이다(`EVD-302`, `EVD-303`). `MessagingAdminService` 빈은 없고 `ApprovalVerifier` 빈도 없다. 이는 명시된 설계다. 그러나 이 스탠스가 **토폴로지 검증까지 덮지는 않는다** — §12.1 과 §17 의 첫 항목이 그것이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-messaging-admin-api-c05.md b/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-messaging-admin-api-c05.md deleted file mode 100644 index a2926c9..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-messaging-admin-api-c05.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-admin-api-c05 -title: 인가 실패와 프로그래밍 오류를 예외 타입으로 가른다 -topic: security-and-trust-boundaries -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-admin-api-c05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-admin-api-c05 - file: ../../../final/evidence/rendered/messaging-admin-api-c05.svg -evidence: - - ../../../final/evidence/raw/messaging-admin-api-c05.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-admin-api#L543 이다. -module: messaging-admin-api ---- - -# 인가 실패와 프로그래밍 오류를 예외 타입으로 가른다 - -거절은 전부 `MessageAuthorizationException` 또는 `MessagingConfigurationException`이고 안정 코드가 붙는다. `IllegalArgumentException`은 구조적으로 불가능한 값에만 쓴다. - -## 본문 - - - -거절은 전부 `MessageAuthorizationException` 또는 `MessagingConfigurationException` 이고, 코드가 붙어 있다. 메시지가 전부 "무엇이 왜 거절되었는가" 를 서술형으로 쓴다. - -| 코드 | 던지는 곳 | 의미 | -|---|---|---| -| `APPROVAL_PLAN_MISMATCH` | `HmacApprovalVerifier:74`, `ApprovedReplayPlan:26`, `ApprovedRedrivePlan:28` | 진짜 승인, 다른 계획 | -| `APPROVAL_EXPIRED` | `HmacApprovalVerifier:81`, `DestructiveOperationGuard:69` | 윈도우 밖 | -| `APPROVAL_SIGNATURE_INVALID` | `HmacApprovalVerifier:89, :93` | 16진 아님 / HMAC 불일치 | -| `ADMIN_CREDENTIAL_REQUIRED` | `DestructiveOperationGuard:58` | 애플리케이션 런타임 | -| `APPROVAL_REQUIRED` | `DestructiveOperationGuard:65` | 승인 없음 | -| `APPROVAL_OPERATION_MISMATCH` | `DestructiveOperationGuard:75` | 다른 작업의 승인 | -| `APPROVAL_SOURCE_MISMATCH` | `DestructiveOperationGuard:85` | 다른 목적지의 승인 | -| `APPROVAL_IMPACT_EXCEEDED` | `Approved*Plan:47/52` | 승인 상한 초과 | -| `TOPOLOGY_CHANGED_SINCE_APPROVAL` | `Approved*Plan:70/74, :79/82` | 추정치 무효 | -| `REDRIVE_LOOP_NOT_ACKNOWLEDGED` | `ApprovedRedrivePlan:88` | 루프 위험 미승인 | -| `AUTO_CREATE_IN_PRODUCTION` | `TopologyManagementMode:30` | 프로덕션 자동 생성 | -| `TOPOLOGY_MISMATCH` | `TopologyValidationReport:78` | BLOCKING 존재 | - -## MessageAuthorizationException 참조 위치 - -:::evidence key="messaging-admin-api-c05" alt="코드베이스에서 MessageAuthorizationException 를 검색한 출력 38줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessageAuthorizationException 코드베이스 검색 — 38줄 · exit 0" zoom="true" -::: - -## IllegalArgumentException을 쓰는 자리 - -`IllegalArgumentException` 은 **구조적으로 불가능한 값**에만 쓴다 — 음수 카운터, 빈 문자열, 역전된 윈도우, 4-eyes 위반. 인가 실패와 프로그래밍 오류가 예외 타입으로 갈린다. - -## 로그 위생이 예외마다 같지 않다 - -`toString()` 하나가 로그 위생을 명시적으로 다룬다. `ApprovalGrant` 는 record 라 기본 `toString()` 이 전 필드를 찍는다는 점은 대비된다 — 다만 `ApprovalGrant` 자체가 로그에 닿는 경로는 확인되지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-messaging-rabbit-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-messaging-rabbit-c02.md deleted file mode 100644 index 67a7a09..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/concept/concept-messaging-rabbit-c02.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-rabbit-c02 -title: 자격증명은 연결 시도마다 해석된다 -topic: security-and-trust-boundaries -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-rabbit-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-rabbit-c02 - file: ../../../final/evidence/rendered/messaging-rabbit-c02.svg -evidence: - - ../../../final/evidence/raw/messaging-rabbit-c02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-rabbit#L106 이다. -module: messaging-rabbit ---- - -# 자격증명은 연결 시도마다 해석된다 - -RabbitMQ 클라이언트 연결은 오래 살고 스스로 재연결하므로, 시작 시점의 자격증명을 들고 있는 팩토리는 폐기된 자격증명으로 계속 재연결한다. 재연결 시점이 곧 회전된 자격증명이 적용되어야 할 시점이다. - -## 본문 - - - -자격증명을 연결 시도마다 다시 해석하는 이유가 적혀 있다. - -> "RabbitMQ client connections are long-lived and reconnect on their own, so a factory holding a credential from startup will happily reconnect with a revoked one for as long as the process runs — the reconnect is exactly the moment a rotated credential should take effect." - -## AmqpCredentials가 record가 아닌 이유 - -비밀을 지우려면 가변이어야 하고, record 가 `char[]` 를 동등성에 쓰면 같은 자재를 가진 둘이 서로 다르다고 판정된다. - -## AmqpCredentials 참조 위치 - -:::evidence key="messaging-rabbit-c02" alt="코드베이스에서 AmqpCredentials 를 검색한 출력 9줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="AmqpCredentials 코드베이스 검색 — 9줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-a06-f011-mongoregexpolicy-forbidden.md b/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-a06-f011-mongoregexpolicy-forbidden.md deleted file mode 100644 index 1595ae6..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-a06-f011-mongoregexpolicy-forbidden.md +++ /dev/null @@ -1,169 +0,0 @@ ---- -kind: CASE -slug: a06-f011-mongoregexpolicy-forbidden -title: MongoRegexPolicy.forbidden()은 금지하지 않는다 -topic: security-policy-enforcement -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a06-f011-mongoregexpolicy-forbidden -evidenceCapturedOn: 2026-09-02 -assets: - - key: a06-f011-mongoregexpolicy-forbidden-probe - file: ../../../final/evidence/rendered/a06-f011-mongoregexpolicy-forbidden-probe.svg - - key: a06-f011-mongoregexpolicy-forbidden - file: ../../../final/evidence/rendered/a06-f011-mongoregexpolicy-forbidden.svg -evidence: - - ../../../final/evidence/raw/a06-f011-mongoregexpolicy-forbidden-probe.txt - - ../../../final/evidence/raw/a06-f011-mongoregexpolicy-forbidden.txt -source: - - 원본 분석 절은 final/document.md#a06#L740 이다. 등급은 P3 이다. 금지 팩토리가 길이 1 정책이라는 관찰, 앵커 문자가 네 검사를 통과한다는 판정, 두 도우미만 막힌다는 대비, 그리고 필드가 정규식 연산자를 등록해야 도달한다는 조건이 그 절에 있다. - - 그 절은 두 도우미의 이스케이프 결과가 항상 다섯 자 이상이라고 적는데, 포함 쪽 최단은 네 자다. 결론은 같다. - - 통과하는 조합이 정확히 하나라는 것, 최대 길이 0 이 생성자에서 막힌다는 것, 매치되는 것이 그 필드의 문자열 값이라는 것, 그리고 이 팩토리의 호출 지점이 없다는 것은 이 기록에서 확인했다. ---- - -# MongoRegexPolicy.forbidden()은 금지하지 않는다 - -금지가 별도 상태가 아니라 최대 길이 1 로 표현되어 있다. 그 정책을 통과하는 패턴과 플래그의 조합은 하나뿐이고, 그 하나는 대상 필드에 든 문자열 값을 전부 매치한다. - -## 관계 - -- **위험한 조합은 정책이 아니라 생성자가 거부하게 만든다** - 금지를 별도 상태로 두어야 하는 이유다. -- **sanitize가 아니라 reject가 기본이다** - 정화가 아니라 거절이 기본이어야 한다는 규칙이다. -- **접두사 시작 매칭은 시그니처에는 맞고 스니핑 패턴에는 맞지 않는다** - 두 사례 모두 검사 방식이 막으려는 대상과 맞지 않는다. - -## 문제 - -정규식 정책은 최대 길이와 허용 플래그와 앵커 요구 셋으로 이루어진 레코드다. 금지를 뜻하는 자리는 그 셋에 없다. - -컴팩트 생성자는 최대 길이가 0 이하면 거부한다. 음수 길이는 관대한 정책이 아니라 오타라는 판단이고 그 판단은 옳다. 다만 그 때문에 정규식 없음을 뜻할 수 있는 값이 표현 불가능해지고, 금지 팩토리는 표현 가능한 가장 작은 값을 쓴다. - -## 결론 - -길이 1 이하이면서 앵커로 시작하는 문자열은 앵커 문자 하나뿐이다. 허용 플래그 집합이 비어 있으므로 플래그도 빈 문자열이어야 한다. 통과하는 조합은 그 둘의 짝 하나다. - -그 짝은 대상 필드에 문자열이 든 문서를 전부 매치한다. 값이 문자열이 아니거나 필드가 없으면 매치되지 않는다. 무력화되는 것은 그 컬렉션에서 정규식 검색의 대상이 되는 값 전부다. - -이스케이프 도우미 둘은 어떤 입력으로도 이 정책을 통과할 수 없다. 입력이 길수록 출력도 길어지는데, 가장 짧은 것이 다섯 자와 네 자다. 금지 정책이 실제로 막는 것은 호출자가 문법을 기여할 수 없는 두 경로이고, 남기는 것은 문법을 그대로 받는 경로의 그 한 짝이다. - -도달에는 선택 둘이 겹쳐야 한다. 이 팩토리를 부르는 것과, 어떤 필드가 정규식 연산자를 명시로 등록하는 것이다. 기본 연산자 집합에 정규식은 없다. 그리고 지금 저장소에는 앞의 선택이 없다. - -이 파일의 javadoc 은 대체로 자기 한계를 함께 적는다. 중첩 수량자 검사는 안전 증명이 아니라 필터라고 스스로 밝힌다. 조건 없이 단언하는 문장은 금지 팩토리 위의 한 줄뿐이고, 그 한 줄이 참이 아니다. - -수정은 정책에 명시적인 정규식 불허 상태를 두고 검증이 그것을 먼저 보게 하는 것이다. - -## 검증 환경 - -OpenJDK : 21.0.12 -MongoDB : 8.0.16 단독 서버 -확인 방식 : 팩토리 값과 검증 경로 실행, 실제 서버에 질의 실행 -소스 수정 : x - -## 재현 조건 - -1. 두 팩토리가 만드는 값과 컴팩트 생성자의 길이 검사를 읽는다. -2. 검증 메서드의 네 검사와 길이 비교를 읽는다. -3. 앵커 문자와 빈 플래그, 앵커 문자와 i 플래그, 빈 문자열, 다른 한 글자, 두 글자, 두 도우미의 최단 결과를 금지 정책에 통과시킨다. -4. 길이 검사를 얹은 인스턴스 도우미를 서로 다른 입력으로 부른다. -5. 기본 연산자 집합에 정규식이 있는지 확인한다. -6. 정책을 갈아 끼우고 어떤 필드에 정규식 연산자를 등록해 질의를 조립한다. -7. 문자열과 비문자열과 필드 없는 문서를 섞어 넣고 그 질의를 실제 서버에서 실행한다. -8. 이 팩토리와 정책 교체 메서드와 질의 정책 생성을 부르는 곳을 전수로 센다. - -## 본문 - - - -정규식 정책은 최대 길이와 허용 플래그와 앵커 요구 세 자리다. 금지를 뜻하는 자리는 없고, 금지 팩토리는 첫 자리를 1 로 잡는다. - -## 0 이 아니라 1 인 이유 - -:::evidence key="a06-f011-mongoregexpolicy-forbidden-probe" alt="금지 정책과 기본 정책의 값, 최대 길이 0 인 정책이 생성자에서 거부되는 결과, 패턴과 플래그 일곱 조합을 금지 정책의 검증에 통과시킨 결과와 각각의 거절 사유, 길이 검사를 얹은 인스턴스 도우미가 서로 다른 입력에서 던지는 결과, 기본 연산자 집합에 정규식이 없다는 확인, 조립된 질의의 문서, 그리고 문자열과 비문자열과 필드 없는 문서를 섞어 넣은 실제 서버에서 그 질의가 매치한 것과 매치하지 않은 것을 출력한 터미널 기록." caption="최대 길이 0 은 생성자가 거부 · 통과하는 조합은 앵커 문자와 빈 플래그 하나 · 인스턴스 도우미는 입력이 무엇이든 길이에서 거절 · 기본 연산자 집합에 REGEX 없음 · 저장 8건에 매치 4건, 매치된 것은 전부 문자열 값 — 33줄 · exit 0" zoom="true" -::: - -```java -/** A policy that forbids regular expressions entirely. */ -public static MongoRegexPolicy forbidden() { - return new MongoRegexPolicy(1, Set.of(), true); -} -``` - -같은 레코드의 컴팩트 생성자가 0 이하를 막는다. 음수 길이를 오타로 보는 검사인데, 정규식 없음을 뜻할 수 있는 값도 같이 막는다. - -```text -길이 0 정책 : IllegalArgumentException: a regex policy needs a positive maximum length -``` - -## 통과하는 조합이 하나 있다 - -검증은 길이·플래그·앵커·중첩 수량자를 이 순서로 검사한다. 길이 1 이하와 앵커 요구를 함께 만족하는 문자열은 하나뿐이고, 허용 플래그 집합이 비어 있으므로 플래그 자리도 빈 문자열이어야 한다. - -```text - ("^", "") -> 수용 - ("^", "i") -> 거절, regex flag 'i' is not allowed by this collection's policy - ("", "") -> 거절, the pattern is not anchored at the start, so it cannot use an index and will scan the collection - ("a", "") -> 거절, the pattern is not anchored at the start, so it cannot use an index and will scan the collection - ("^a", "") -> 거절, the pattern is 2 characters, above the limit of 1 -``` - -그 짝을 넘겨 질의를 조립하면 이런 문서가 나온다. - -```text -{"$and": [{"name": {"$regularExpression": {"pattern": "^", "options": ""}}}]} -``` - -## 매치되는 것은 그 필드의 문자열 값이다 - -문자열과 비문자열과 필드 없는 문서를 섞어 여덟 건을 넣고 실행한 결과다. - -```text -[실행] 저장 8건 · 매치 4건 - 매치 {"name": "ana"} - 매치 {"name": ""} - 매치 {"name": "\ud83d\ude00"} - 매치 {"name": ["zz"]} - 미매치 {"name": 12345} - 미매치 {"name": true} - 미매치 {"name": null} - 미매치 {"other": "x"} -``` - -빈 문자열도 이모지도 배열 안의 문자열도 걸린다. 숫자와 불리언과 널과 필드 부재는 걸리지 않는다. 정규식 검색의 대상이 되는 값만 골라서 전부 매치한다는 뜻이다. - -## 두 도우미는 어떤 입력으로도 통과할 수 없다 - -두 이스케이프 도우미는 호출자의 텍스트를 인용부호 안에 그대로 넣으므로 호출자가 문법을 기여할 수 없다. 출력 길이는 입력을 따라 늘고, 최단이 다섯 자와 네 자다. - -```text - literalPrefix("") = ^\Q\E (5자) - prefixPattern("") -> 거절, the pattern is 5 characters, above the limit of 1 - literalPrefix("ab") = ^\Qab\E (7자) - prefixPattern("ab") -> 거절, the pattern is 7 characters, above the limit of 1 - escapedContains("") = \Q\E (4자) -``` - -금지 정책이 막는 것은 문법을 받지 않는 두 경로이고, 남기는 것은 문법을 그대로 받는 경로의 한 짝이다. - -## 호출 지점은 선언 자리뿐이다 - -:::evidence key="a06-f011-mongoregexpolicy-forbidden" alt="정규식 정책의 두 팩토리가 만드는 값, 검증 메서드의 네 검사와 길이 비교, 금지 팩토리를 부르는 곳 전수와 정책 교체 메서드를 부르는 곳 전수, 질의 정책을 만드는 곳 전수, 그리고 자동 구성이 질의 계열에서 만드는 빈을 출력한 터미널 기록." caption="금지 팩토리는 길이 1·플래그 없음·앵커 요구 참 · 검증 네 검사 어디에도 정규식 자체를 막는 갈래가 없음 · 금지 팩토리 호출 0 · 정책 교체는 시험 한 곳에서 기본 정책으로 · 질의 정책 생성 다섯 곳은 전부 시험 · 질의 계열의 빈은 예산 집행기 하나 — 58줄 · exit 0" zoom="true" -::: - -기록의 뒤 네 갈래가 호출 지점을 센다. `forbidden()` 은 선언 자리 말고 없고, `withRegexPolicy` 는 시험 한 곳이 기본 정책으로 부르며, 질의 정책을 만드는 다섯 곳은 전부 시험이고, 자동 구성이 질의 계열에서 등록하는 빈은 `MongoBudgetEnforcer` 하나다. - -배선해도 곧장 도달하지는 않는다. 기본 연산자 집합에 정규식이 없어서, 어떤 필드가 `withOperators` 로 정규식을 명시해 등록해야 한다. - -```text -[전제] 기본 연산자 집합에 REGEX 가 있는가 : false [GT, GTE, LT, IN, EQ, LTE, EXISTS] -``` - -필요한 것은 두 선택이 겹치는 일이다. - -## 확인하지 못한 것 - -이 팩토리를 부르는 배포는 없으므로, 어떤 필드가 정규식 연산자를 등록할지는 다루지 않았다. 확인한 것은 두 선택이 겹쳤을 때 검증이 무엇을 통과시키고 그 결과가 서버에서 무엇을 매치하는지다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-grpc-admin-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-grpc-admin-f02.md deleted file mode 100644 index 797ea79..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-grpc-admin-f02.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -kind: CASE -slug: grpc-admin-f02 -title: 비밀 필드 검사가 스냅숏의 네 구획 중 하나에만 적용된다 -topic: security-policy-enforcement -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-admin-f02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-admin-f02 - file: ../../../final/evidence/rendered/grpc-admin-f02.svg -evidence: - - ../../../final/evidence/raw/grpc-admin-f02.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-admin#L162 이다. -module: grpc-admin -priority: P3 ---- - -# 비밀 필드 검사가 스냅숏의 네 구획 중 하나에만 적용된다 - -메시지는 "a platform snapshot must not carry …" 로 스냅숏 전체를 말한다. 검사 대상은 channelProfileHashes 하나다. - -## 문제 - -메시지는 "a platform snapshot must not carry …" 로 스냅숏 전체를 말한다. - -검사 대상은 channelProfileHashes 하나다. - -## 결론 - -같은 채널 이름 공간을 쓰는 두 맵이 더 있다 — resolverAndLoadBalancerByChannel, retryOwnerByChannel. - -그리고 registeredServices 목록과 serviceHealth 맵이 있다. - -어느 것도 검사되지 않는다. - -세 맵의 키 집합이 같아야 한다는 요구가 없으므로, 어떤 채널이 나머지 두 맵에만 있으면 그 이름은 검사를 지나지 않는다. - -grpc-advanced-diagnostics 의 스냅숏은 같은 형태의 자기 검사를 두 구획(주소 목록, 자원 판본 키)에 적용한다. - -두 리프의 규율이 갈린다. - -수정은 네 구획 전부를 같은 검사에 넣는 것이다. - -값이 아니라 키를 보는 검사이므로 비용이 낮다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : 검사에 들어가는 구획과 스냅숏이 담는 네 구획의 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-admin#L162 에 있다. - -## 본문 - - - -검사는 한 구획에만 걸린다. - -```java -List forbidden = GrpcAdminExposurePolicy.forbiddenFields(channelProfileHashes); -if (!forbidden.isEmpty()) { - throw new IllegalArgumentException("a platform snapshot must not carry " + forbidden + "; hashes and names only"); -} -``` - -메시지는 "a platform snapshot must not carry …" 로 스냅숏 전체를 말한다. 검사 대상은 `channelProfileHashes` 하나다. - -## 검사가 걸리는 구획 - -:::evidence key="grpc-admin-f02" alt="분석 문서 final/document.md#a20-grpc-admin 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-admin 발췌 — 15줄" zoom="true" -::: - -## 검사되지 않는 세 구획 - -같은 채널 이름 공간을 쓰는 두 맵(`resolverAndLoadBalancerByChannel`, `retryOwnerByChannel`)과 `registeredServices` 목록·`serviceHealth` 맵이다. 세 맵의 키 집합이 같아야 한다는 요구가 없으므로, 어떤 채널이 나머지 두 맵에만 있으면 그 이름은 검사를 지나지 않는다. - -## 두 리프의 규율이 갈린다 - -`grpc-advanced-diagnostics` 의 스냅숏은 같은 형태의 자기 검사를 두 구획(주소 목록, 자원 판본 키)에 적용한다. 수정은 네 구획 전부를 같은 검사에 넣는 것이다 — 값이 아니라 키를 보는 검사이므로 비용이 낮다. - -## 확인하지 못한 것 - -검사되지 않는 세 구획에 실제로 비밀 형태의 키를 넣어 통과를 관측하지 않았다. 검사 인자와 구획 목록의 대조로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-grpc-advanced-diagnostics-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-grpc-advanced-diagnostics-f01.md deleted file mode 100644 index 96a9643..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-grpc-advanced-diagnostics-f01.md +++ /dev/null @@ -1,97 +0,0 @@ ---- -kind: CASE -slug: grpc-advanced-diagnostics-f01 -title: 마스킹이 IPv4 만 알고, 그 결과 "마스킹되지 않은 주소" 검사가 나머지 형태를 전부 통과시킨다 -topic: security-policy-enforcement -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-advanced-diagnostics-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-advanced-diagnostics-f01 - file: ../../../final/evidence/rendered/grpc-advanced-diagnostics-f01.svg - - key: grpc-advanced-diagnostics-f01-diagram - file: ../../../final/assets/diagrams/grpc-advanced-diagnostics-f01.svg -evidence: - - ../../../final/evidence/raw/grpc-advanced-diagnostics-f01.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-advanced-diagnostics#L167 이다. -module: grpc-advanced-diagnostics -priority: P2 ---- - -# 마스킹이 IPv4 만 알고, 그 결과 "마스킹되지 않은 주소" 검사가 나머지 형태를 전부 통과시킨다 - -IPv4 가 아닌 주소는 패턴에 맞지 않아 입력 그대로 반환된다. 그리고 스냅숏 생성자의 검사는 이렇게 되어 있다. - -## 문제 - -IPv4 가 아닌 주소는 패턴에 맞지 않아 입력 그대로 반환된다. - -그리고 스냅숏 생성자의 검사는 이렇게 되어 있다. - -## 결론 - -마스킹 결과가 입력과 같으면 이미 마스킹된 것으로 판정한다. - -그러므로 IPv4 가 아닌 주소는 전부 이 검사를 통과한다. - -세 번째와 네 번째가 문제다. - -이 플랫폼이 겨냥하는 배포 형태가 쿠버네티스이고(grpc-discovery 전체가 그 주제다), 헤드리스 레코드의 엔드포인트는 파드 DNS 이름이며 이중 스택 클러스터에서는 IPv6 주소다. - -편집기가 막으려 한 것이 정확히 그것이다 — "a diagnostics endpoint that publishes peer addresses publishes every tenant's connection." unix 소켓 경로도 통과한다. - -그것은 호스트 파일 시스템 경로다. - -테스트의 주소 리터럴이 전부 IPv4 다 — 10.4.13.201:9090 · 10.9.13.201 · 10.4.x.x · 10.5.x.x. - -IPv6 도 호스트 이름도 없다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : 마스킹 정규식이 받는 주소 형태와 스냅숏 생성자의 판정 조건 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-advanced-diagnostics#L167 에 있다. - -## 본문 - - - -IPv4 가 아닌 주소는 패턴에 맞지 않아 **입력 그대로 반환된다.** 그리고 스냅숏 생성자의 검사는 마스킹 결과가 입력과 같으면 이미 마스킹된 것으로 판정한다. - -## 마스킹이 아는 형태 - -:::evidence key="grpc-advanced-diagnostics-f01-diagram" alt="IPv4 점 십진만 마스킹 대상 안에 놓이고 IPv6 주소와 호스트 이름 및 unix 경로가 바깥에 빗금으로 놓인다" caption="마스킹이 아는 형태" zoom="false" -::: - -그러므로 IPv4 가 아닌 주소는 전부 이 검사를 통과한다. - -## 마스킹되지 않은 입력이 통과하는 이유 - -:::evidence key="grpc-advanced-diagnostics-f01" alt="분석 문서 final/document.md#a20-grpc-advanced-diagnostics 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-advanced-diagnostics 발췌 — 15줄" zoom="true" -::: - -## 셋째와 넷째가 문제다 - -이 플랫폼이 겨냥하는 배포 형태가 쿠버네티스이고(`grpc-discovery` 전체가 그 주제다), 헤드리스 레코드의 엔드포인트는 파드 DNS 이름이며 이중 스택 클러스터에서는 IPv6 주소다. 편집기가 막으려 한 것이 정확히 그것이다 — "a diagnostics endpoint that publishes peer addresses publishes every tenant's connection." `unix` 소켓 경로도 통과하는데, 그것은 호스트 파일 시스템 경로다. - -## 테스트의 주소 리터럴이 전부 IPv4 다 - -`10.4.13.201:9090` · `10.9.13.201` · `10.4.x.x` · `10.5.x.x`. IPv6 도 호스트 이름도 없다. - -## 수정 - -마스킹을 형태별로 나눈다 — IPv6 는 앞 두 그룹만 남기고 나머지를 `:x:x` 로, 호스트 이름은 최상위 라벨 몇 개만 남기고, 그 밖의 형태는 `unknown` 으로 접는다. 그리고 검사를 "결과가 입력과 같으면 통과" 가 아니라 "알려진 마스킹 형태와 일치해야 통과" 로 뒤집는다. 지금 형태는 마스킹이 모르는 입력을 전부 안전하다고 판정한다. - -## 확인하지 못한 것 - -IPv6 주소로 스냅숏을 만들어 실행으로 재현하지 않았다. 정규식과 생성자 검사로 판정했다. 실제 Channelz 서비스를 띄우지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-grpc-advanced-diagnostics-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-grpc-advanced-diagnostics-f02.md deleted file mode 100644 index 624bb38..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-grpc-advanced-diagnostics-f02.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -kind: CASE -slug: grpc-advanced-diagnostics-f02 -title: 금지 필드 검사가 키에만 적용되고 값에는 적용되지 않는다 -topic: security-policy-enforcement -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-advanced-diagnostics-f02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-advanced-diagnostics-f02 - file: ../../../final/evidence/rendered/grpc-advanced-diagnostics-f02.svg -evidence: - - ../../../final/evidence/raw/grpc-advanced-diagnostics-f02.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-advanced-diagnostics#L206 이다. -module: grpc-advanced-diagnostics -priority: P3 ---- - -# 금지 필드 검사가 키에만 적용되고 값에는 적용되지 않는다 - -redact(...) 도 같다 — 금지 이름의 키를 버리고, 남은 값은 주소 필드일 때만 마스킹한다. 값 자체가 자격증명 형태인지는 보지 않는다. - -## 문제 - -redact(...) 도 같다 — 금지 이름의 키를 버리고, 남은 값은 주소 필드일 때만 마스킹한다. - -값 자체가 자격증명 형태인지는 보지 않는다. - -## 결론 - -grpc-observability 의 태그 정책은 값도 본다(UUID·sha256:·bearer 패턴). - -같은 저장소의 두 관측 편집기가 값 검사에서 갈린다. - -xDS 자원 버전은 보통 짧은 숫자나 해시라 도달성이 낮다. - -기록하는 이유는 두 편집기의 규율이 다르다는 점이다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : redact 가 검사하는 대상(키)과 검사하지 않는 대상(값)의 코드 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-advanced-diagnostics#L206 에 있다. - -## 본문 - - - -`redact(...)` 는 금지 이름의 키를 버리고, 남은 값은 주소 필드일 때만 마스킹한다. 값 자체가 자격증명 형태인지는 보지 않는다. - -## redact 가 보는 것 - -:::evidence key="grpc-advanced-diagnostics-f02" alt="분석 문서 final/document.md#a20-grpc-advanced-diagnostics 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-advanced-diagnostics 발췌 — 15줄" zoom="true" -::: - -## 같은 저장소의 다른 편집기는 값도 본다 - -`grpc-observability` 의 태그 정책은 UUID·`sha256:`·`bearer ` 패턴을 본다. 두 관측 편집기가 값 검사에서 갈린다. - -## 도달성은 낮다 - -xDS 자원 버전은 보통 짧은 숫자나 해시다. 기록하는 이유는 두 편집기의 규율이 다르다는 점이다. - -## 확인하지 못한 것 - -자격증명 형태의 값을 실제로 넣어 통과를 관측하지 않았다. 배선 경로가 없어 실제 Channelz 스냅숏을 만들 수 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-grpc-advanced-edition-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-grpc-advanced-edition-f03.md deleted file mode 100644 index 6604c3d..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-grpc-advanced-edition-f03.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -kind: CASE -slug: grpc-advanced-edition-f03 -title: 정책의 자바독이 하지 않는 거부를 한다고 적고, 승격 승인이 두 곳에 따로 있다 -topic: security-policy-enforcement -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-advanced-edition-f03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-advanced-edition-f03 - file: ../../../final/evidence/rendered/grpc-advanced-edition-f03.svg -evidence: - - ../../../final/evidence/raw/grpc-advanced-edition-f03.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-advanced-edition#L216 이다. -module: grpc-advanced-edition -priority: P3 ---- - -# 정책의 자바독이 하지 않는 거부를 한다고 적고, 승격 승인이 두 곳에 따로 있다 - -첫째, 서술과 코드가 어긋난다. "refuses an approval nobody recorded" 에 해당하는 검사가 없다. - -## 문제 - -첫째, 서술과 코드가 어긋난다. - -"refuses an approval nobody recorded" 에 해당하는 검사가 없다. - -## 결론 - -promotionApproved 는 읽히지도 검증되지도 않고 그대로 저장된다. - -new GrpcEdition2024Policy(Set.of(), Set.of(), true) — 옵트인한 모듈도 공개 서비스도 없는데 승인만 참인 정책 — 이 아무 저항 없이 만들어지고, serviceMayMove 는 모든 서비스에 참을 답한다. - -둘째, 같은 사실이 두 곳에 따로 있다. - -게이트는 정책을 인자로 받지도, 참조하지도 않는다. - -그래서 "ADR 이 없다"고 판정한 게이트와 "승인되었다"고 답하는 정책이 동시에 성립할 수 있고, 둘을 맞추는 코드가 없다. - -§17.2 가 지적한 "두 게이트가 서로를 부르지 않는다" 와 같은 구조가 정책과 게이트 사이에도 있다. - -정책도 게이트도 production 호출자가 없고(§12.1), 승격은 사람이 수행하는 절차다. - -다만 이 리프가 존재하는 이유가 "그 절차를 코드로 적어 두는 것" 이므로, 적힌 절차 안에서 같은 사실이 둘로 갈라져 있는 것은 그 목적에 어긋난다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : GrpcEdition2024Policy 참조 11건 검색과 자바독 서술 대비 실제 거부 조건 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-advanced-edition#L216 에 있다. - -## 본문 - - - -서술과 코드가 어긋난다 — "refuses an approval nobody recorded" 에 해당하는 검사가 없다. - -## GrpcEdition2024Policy 참조 위치 - -:::evidence key="grpc-advanced-edition-f03" alt="코드베이스에서 GrpcEdition2024Policy 를 검색한 출력 11줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcEdition2024Policy 코드베이스 검색 — 11줄 · exit 0" zoom="true" -::: - -## 승인만 참인 정책이 저항 없이 만들어진다 - -`promotionApproved` 는 읽히지도 검증되지도 않고 그대로 저장된다. `new GrpcEdition2024Policy(Set.of(), Set.of(), true)` — 옵트인한 모듈도 공개 서비스도 없는데 승인만 참인 정책 — 이 만들어지고, `serviceMayMove` 는 모든 서비스에 참을 답한다. - -## 같은 사실이 두 곳에 따로 있다 - -게이트는 정책을 인자로 받지도, 참조하지도 않는다. 그래서 "ADR 이 없다"고 판정한 게이트와 "승인되었다"고 답하는 정책이 동시에 성립할 수 있고, 둘을 맞추는 코드가 없다. §17.2 가 지적한 "두 게이트가 서로를 부르지 않는다" 와 같은 구조다. - -## 왜 그래도 기록하나 - -정책도 게이트도 production 호출자가 없고(§12.1), 승격은 사람이 수행하는 절차다. 다만 이 리프가 존재하는 이유가 "그 절차를 코드로 적어 두는 것" 이므로, 적힌 절차 안에서 같은 사실이 둘로 갈라져 있는 것은 그 목적에 어긋난다. 수정은 `promotionBlockers` 가 정책을 받아 `promotionApproved` 를 `promotionAdr` 자리에 쓰고, 정책 생성자가 자바독대로 승인의 근거를 요구하는 것이다. 어느 쪽도 하지 않겠다면 자바독의 그 문장을 지운다. - -## 확인하지 못한 것 - -편집 기능이 proto3 의 optional 과 같은 유선 결과를 내는지 확인하지 않았다. 그것이 이 레인의 질문이고 이 결함이 그 질문에 답할 수 없는 이유다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-grpc-core-api-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-grpc-core-api-f03.md deleted file mode 100644 index 1ee46b9..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-grpc-core-api-f03.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -kind: CASE -slug: grpc-core-api-f03 -title: RESOURCE_EXHAUSTED 매핑이 그 상태의 두 출처 중 하나만 가정한다 -topic: security-policy-enforcement -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-core-api-f03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-core-api-f03 - file: ../../../final/evidence/rendered/grpc-core-api-f03.svg -evidence: - - ../../../final/evidence/raw/grpc-core-api-f03.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-core-api#L180 이다. -module: grpc-core-api -priority: P3 ---- - -# RESOURCE_EXHAUSTED 매핑이 그 상태의 두 출처 중 하나만 가정한다 - -이 분기는 전송이 미시작을 증명하지 못한 뒤에 도달한다. 즉 "보냈는지 모르지만 이 상태 코드는 거절을 뜻한다" 는 판정이다. - -## 문제 - -이 분기는 전송이 미시작을 증명하지 못한 뒤에 도달한다. - -즉 "보냈는지 모르지만 이 상태 코드는 거절을 뜻한다" 는 판정이다. - -## 결론 - -목록의 나머지 일곱은 서버가 일을 시작하기 전에 답하는 상태다. - -RESOURCE_EXHAUSTED 는 두 출처를 갖는다. - -이 플랫폼 자신의 승인 제어기가 부하를 흘려보낼 때 — 일을 쓰기 전이므로 거절이 맞다. - -원격 서버가 작업 중 자원(할당량·디스크)을 소진했을 때 — 부분 커밋이 있을 수 있다. - -이 클래스의 원칙은 보수적이다. - -자바독이 두 기본값(DEADLINE_EXCEEDED·UNAVAILABLE 를 모호로)을 계획의 전역 제약이라 부르고, 그 이유는 "보냈는지 모르면 모호" 다. - -RESOURCE_EXHAUSTED 는 그 원칙에서 벗어난 유일한 항목이다. - -ABORTED 가 모호에 있는 것과 대비된다 — 트랜잭션 충돌은 서버가 일을 시작한 뒤의 상태이고, 그래서 모호다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : 이 분기에 도달하는 선행 조건 추적과 상태 코드의 두 출처 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-core-api#L180 에 있다. - -## 본문 - - - -이 분기는 전송이 미시작을 증명하지 못한 뒤에 도달한다 — "보냈는지 모르지만 이 상태 코드는 거절을 뜻한다" 는 판정이다. 목록의 나머지 일곱은 서버가 일을 시작하기 전에 답하는 상태다. - -## 이 분기에 도달하는 조건 - -:::evidence key="grpc-core-api-f03" alt="분석 문서 final/document.md#a20-grpc-core-api 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-core-api 발췌 — 15줄" zoom="true" -::: - -## RESOURCE_EXHAUSTED 는 두 출처를 갖는다 - -이 플랫폼 자신의 승인 제어기가 부하를 흘려보낼 때 — 일을 쓰기 전이므로 거절이 맞다. 원격 서버가 작업 중 자원(할당량·디스크)을 소진했을 때 — 부분 커밋이 있을 수 있다. - -## 이 클래스의 원칙은 보수적이다 - -자바독이 두 기본값(`DEADLINE_EXCEEDED`·`UNAVAILABLE` 를 모호로)을 계획의 전역 제약이라 부르고, 그 이유는 "보냈는지 모르면 모호" 다. `RESOURCE_EXHAUSTED` 는 그 원칙에서 벗어난 유일한 항목이다. `ABORTED` 가 모호에 있는 것과 대비된다 — 트랜잭션 충돌은 서버가 일을 시작한 뒤의 상태이고, 그래서 모호다. - -## 수정 - -`RESOURCE_EXHAUSTED` 를 모호로 옮기거나, 그 상태를 이 플랫폼이 발행한 것과 원격이 발행한 것으로 구분해 전자만 거절로 두는 것이다. 후자는 증거 축에 발신자 정보를 요구하므로 전자가 현실적이다. - -## 확인하지 못한 것 - -상태 코드별 매핑을 실제 서버 응답으로 재현하지 않았다. 표와 근거 문장으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-grpc-policy-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-grpc-policy-f01.md deleted file mode 100644 index 94e191d..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-grpc-policy-f01.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -kind: CASE -slug: grpc-policy-f01 -title: 스트림 승인의 경계가 동시성 아래에서 새고, caller별 맵이 줄지 않는다 -topic: security-policy-enforcement -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-policy-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-policy-f01 - file: ../../../final/evidence/rendered/grpc-policy-f01.svg - - key: grpc-policy-f01-diagram - file: ../../../final/assets/diagrams/grpc-policy-f01.svg -evidence: - - ../../../final/evidence/raw/grpc-policy-f01.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-policy#L184 이다. -module: grpc-policy -priority: P2 ---- - -# 스트림 승인의 경계가 동시성 아래에서 새고, caller별 맵이 줄지 않는다 - -읽고 비교한 뒤 별도로 증가한다. 경계에 있는 N 개 스레드가 모두 통과한다. - -## 문제 - -읽고 비교한 뒤 별도로 증가한다. - -경계에 있는 N 개 스레드가 모두 통과한다. - -## 결론 - -이 클래스의 javadoc 이 서술하는 실패 상황이 곧 고동시성이다 — "a client that reconnects on every error opens streams faster than the old ones close." 재접속 폭풍에서 경계가 가장 많이 샌다. - -release 도 같은 형태라 음수로 갈 수 있다. - -그리고 perCaller 에서 항목이 제거되지 않는다. - -computeIfAbsent 가 호출자 지문마다 계수기를 만들고 release 는 값만 줄인다. - -서로 다른 호출자 수만큼 맵이 자란다 — grpc-observability 의 GrpcMetricCardinalityPolicy 가 지표 태그에 대해 명시적으로 막는 것과 같은 종류의 증가이고, 여기에는 그 가드가 없다. - -정본이 같은 리프에 있다 — GrpcRetryBudget.tryConsume 의 비교 후 교체 루프. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : GrpcMetricCardinalityPolicy 참조 20건 검색과 승인 경로의 읽기·증가 원자성 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-policy#L184 에 있다. - -## 본문 - - - -읽고 비교한 뒤 별도로 증가한다. 경계에 있는 N 개 스레드가 모두 통과한다. - -## 두 값이 함께 커지는 조건 - -:::evidence key="grpc-policy-f01-diagram" alt="재접속 폭주가 같은 값 읽고 통과와 계수기 증가를 지나 경계 초과와 맵 증가로 이어진다" caption="두 값이 함께 커지는 조건" zoom="false" -::: - -이 클래스의 javadoc 이 서술하는 실패 상황이 곧 고동시성이다 — "a client that reconnects on every error opens streams faster than the old ones close." 재접속 폭풍에서 경계가 가장 많이 샌다. `release` 도 같은 형태라 음수로 갈 수 있다. - -## GrpcMetricCardinalityPolicy 참조 위치 - -:::evidence key="grpc-policy-f01" alt="코드베이스에서 GrpcMetricCardinalityPolicy 를 검색한 출력 20줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcMetricCardinalityPolicy 코드베이스 검색 — 20줄 · exit 0" zoom="true" -::: - -## perCaller 에서 항목이 제거되지 않는다 - -`computeIfAbsent` 가 호출자 지문마다 계수기를 만들고 `release` 는 값만 줄인다. 서로 다른 호출자 수만큼 맵이 자란다 — `grpc-observability` 의 `GrpcMetricCardinalityPolicy` 가 지표 태그에 대해 명시적으로 막는 것과 같은 종류의 증가이고, 여기에는 그 가드가 없다. - -## 정본이 같은 리프에 있다 - -`GrpcRetryBudget.tryConsume` 의 비교 후 교체 루프. - -## 확인하지 못한 것 - -동시 승인을 실행으로 재현하지 않았다. 원자성 분석과 JMM 으로 판정했고, 어떤 인터셉터도 실제 서버에 걸어 돌리지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-grpc-policy-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-grpc-policy-f02.md deleted file mode 100644 index d60f2ff..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-grpc-policy-f02.md +++ /dev/null @@ -1,97 +0,0 @@ ---- -kind: CASE -slug: grpc-policy-f02 -title: 자격증명 회전이 비교 후 교체가 아니고, 배수 완료가 진행 중인 회전을 되돌릴 수 있다 -topic: security-policy-enforcement -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-policy-f02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-policy-f02 - file: ../../../final/evidence/rendered/grpc-policy-f02.svg - - key: grpc-policy-f02-diagram - file: ../../../final/assets/diagrams/grpc-policy-f02.svg -evidence: - - ../../../final/evidence/raw/grpc-policy-f02.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-policy#L204 이다. -module: grpc-policy -priority: P2 ---- - -# 자격증명 회전이 비교 후 교체가 아니고, 배수 완료가 진행 중인 회전을 되돌릴 수 있다 - -AtomicReference 를 쓰면서 두 메서드 모두 읽고 나서 조건 없이 쓴다. 두 회전이 같은 observed 를 읽으면 둘 다 승계 검사를 통과할 수 있고, 나중 set 이 앞의 것을 덮는다. - -## 문제 - -AtomicReference 를 쓰면서 두 메서드 모두 읽고 나서 조건 없이 쓴다. - -두 회전이 같은 observed 를 읽으면 둘 다 승계 검사를 통과할 수 있고, 나중 set 이 앞의 것을 덮는다. - -## 결론 - -덮인 회전이 배수 대상으로 기록해 둔 세대가 상태에서 사라진다. - -그 세대 위의 호출은 아무도 배수하지 않는다. - -javadoc 이 이 상황을 이미 알고 있다 — 승계 검사의 존재 이유로 "the usual reason for one is two rotators racing" 를 든다. - -검사는 있고 원자성이 없다. - -completeDrain() 이 자기가 읽은 observed.current() 로 새 상태를 만든다. - -읽기와 쓰기 사이에 회전이 일어나면, 그 회전이 활성화한 세대가 지워지고 이전 세대가 다시 현재가 된다. - -즉 방금 교체된 자격증명이 되살아난다. - -클래스의 존재 이유가 "in-flight 작업을 떨어뜨리지 않고 자격 자재를 교체하는 것" 인데, 이 경로는 교체 자체를 되돌린다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : AtomicReference 를 다루는 두 메서드의 읽기·쓰기 순서 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-policy#L204 에 있다. - -## 본문 - - - -`AtomicReference` 를 쓰면서 두 메서드 모두 읽고 나서 조건 없이 쓴다. - -## 두 연산이 겹치는 결과 - -:::evidence key="grpc-policy-f02-diagram" alt="회전과 배수 완료가 각자 읽은 상태와 조건 없는 set 을 지나 이전 세대가 현재가 되는 결과로 이어진다" caption="두 연산이 겹치는 결과" zoom="false" -::: - -두 회전이 같은 `observed` 를 읽으면 둘 다 승계 검사를 통과할 수 있고, 나중 `set` 이 앞의 것을 덮는다. 덮인 회전이 배수 대상으로 기록해 둔 세대가 상태에서 사라지고, 그 세대 위의 호출은 아무도 배수하지 않는다. - -## 두 메서드가 읽고 쓰는 방식 - -:::evidence key="grpc-policy-f02" alt="분석 문서 final/document.md#a20-grpc-policy 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-policy 발췌 — 15줄" zoom="true" -::: - -## javadoc 이 이 상황을 이미 알고 있다 - -승계 검사의 존재 이유로 "the usual reason for one is two rotators racing" 를 든다. 검사는 있고 원자성이 없다. - -## 배수 완료가 회전을 되돌린다 - -`completeDrain()` 이 자기가 읽은 `observed.current()` 로 새 상태를 만든다. 읽기와 쓰기 사이에 회전이 일어나면, 그 회전이 활성화한 세대가 지워지고 **이전 세대가 다시 현재가 된다** — 방금 교체된 자격증명이 되살아난다. 클래스의 존재 이유가 "in-flight 작업을 떨어뜨리지 않고 자격 자재를 교체하는 것" 인데, 이 경로는 교체 자체를 되돌린다. - -## 수정 - -`rotate` 는 `compareAndSet(observed, next)` 가 실패하면 다시 읽어 판정하고, `completeDrain` 은 `updateAndGet(s -> new State(s.current(), null, null))` 로 현재 값을 원자적으로 읽어 쓰면 된다. 후자는 한 줄이다. 같은 형태가 `grpc-client` 의 `GrpcChannelRuntimeRegistry.rotate` 에도 있다. - -## 확인하지 못한 것 - -동시 회전과 동시 배수 완료를 실행으로 재현하지 않았다. 조건 없는 set 이라는 코드 형태로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-grpc-server-f04.md b/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-grpc-server-f04.md deleted file mode 100644 index 2460262..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-grpc-server-f04.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -kind: CASE -slug: grpc-server-f04 -title: 승인 제어기의 세 메서드가 원자적이지 않고, 큐 계수기를 되돌리는 경로가 없다 -topic: security-policy-enforcement -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-server-f04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-server-f04 - file: ../../../final/evidence/rendered/grpc-server-f04.svg - - key: grpc-server-f04-diagram - file: ../../../final/assets/diagrams/grpc-server-f04.svg -evidence: - - ../../../final/evidence/raw/grpc-server-f04.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-server#L211 이다. -module: grpc-server -priority: P2 ---- - -# 승인 제어기의 세 메서드가 원자적이지 않고, 큐 계수기를 되돌리는 경로가 없다 - -이 리프가 SSOT 이므로 여기에 적는다. grpc-policy §17.1 이 이 클래스를 대조군으로 지목하는데, 지목된 쪽 문서에 판정이 없었다. - -## 문제 - -이 리프가 SSOT 이므로 여기에 적는다. - -grpc-policy §17.1 이 이 클래스를 대조군으로 지목하는데, 지목된 쪽 문서에 판정이 없었다. - -## 결론 - -첫째, 읽고 나서 따로 증가시킨다. - -경계에 있는 N 개 스레드가 모두 통과한다. - -AtomicInteger 를 쓰면서 비교와 증가를 나눈 형태이고, 같은 가족의 정본이 GrpcRetryBudget.tryConsume 의 비교 후 교체 루프다. - -release()·promoteFromQueue() 도 같다 — get() > 0 을 확인한 뒤 별도로 감소시키므로, 두 스레드가 같은 마지막 하나를 보고 둘 다 감소시켜 음수가 될 수 있다. - -클래스가 Math.max(0, …) 같은 하한도 두지 않는다. - -둘째, 큐 계수기를 되돌리는 경로가 없다. - -큐에 들어간 호출도 admitted=true 를 받는다. - -그런데 그 경로는 queued 만 올리고 inFlight 는 올리지 않는다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : 두 계수기의 증가·감소 지점 대조와 큐 계수기 반납 메서드 유무 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-server#L211 에 있다. - -## 본문 - - - -이 리프가 SSOT 이므로 여기에 적는다. `grpc-policy` §17.1 이 이 클래스를 대조군으로 지목하는데, 지목된 쪽 문서에 판정이 없었다. - -## 첫째 — 읽고 나서 따로 증가시킨다 - -```java -public Decision tryAdmit() { - int running = inFlight.get(); - if (running < maxConcurrentCalls) { - inFlight.incrementAndGet(); // ← 읽기와 증가 사이에 다른 스레드가 들어온다 - return new Decision(true, …); - } - int waiting = queued.get(); - if (waiting < maxQueuedCalls) { - queued.incrementAndGet(); // ← 같은 형태 -``` - -경계에 있는 N 개 스레드가 모두 통과한다. 같은 가족의 정본이 `GrpcRetryBudget.tryConsume` 의 비교 후 교체 루프다. `release()`·`promoteFromQueue()` 도 같다 — `get() > 0` 을 확인한 뒤 별도로 감소시키므로 두 스레드가 같은 마지막 하나를 보고 둘 다 감소시켜 음수가 될 수 있고, `Math.max(0, …)` 같은 하한도 없다. - -## 계수기 쌍의 비대칭 - -:::evidence key="grpc-server-f04-diagram" alt="진행 계수기 쪽에 tryAdmit 증가와 release 감소가 놓이고 큐 계수기 쪽에 tryAdmit 증가와 반납 메서드 없음이 놓인다" caption="계수기 쌍의 비대칭" zoom="false" -::: - -## 대조군으로 지목된 클래스 - -:::evidence key="grpc-server-f04" alt="분석 문서 final/document.md#a20-grpc-server 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-server 발췌 — 15줄" zoom="true" -::: - -## 둘째 — 큐 계수기를 되돌리는 경로가 없다 - -큐에 들어간 호출도 `admitted=true` 를 받는데 그 경로는 `queued` 만 올리고 `inFlight` 는 올리지 않는다. 그리고 끝난 호출을 반납하는 메서드는 하나뿐이다. 따라서 호출자가 `promoteFromQueue()` 를 정확히 한 번 끼워 넣지 않으면 계수기가 어긋난다 — 큐에서 실행된 호출이 끝나면 `queued` 는 그대로이고 `inFlight` 만 줄어든다. `releaseQueued()` 같은 메서드도, 그 짝짓기를 요구하는 서술도 없다. - -## 시험이 짝지어 부른다 - -두 시험 모두 단일 스레드이고, `releaseAndPromotionTrackCapacity` 는 `release()` 와 `promoteFromQueue()` 를 짝지어 부른다. 짝짓지 않는 경로는 시험되지 않는다. - -## 배선하는 순간 P1 이다 - -오늘 호출자가 없으므로(§12.1) P2. 승인 단계를 배선하면 부하 아래에서 경계가 새는 것과, 큐 계수기가 단조 증가해 `at capacity` 가 영구히 참이 되는 것이 함께 온다. 세 메서드를 비교 후 교체 루프로 바꾸고, 큐 경로에 대응하는 반납 메서드를 둔다. - -## 확인하지 못한 것 - -경합을 실행으로 재현하지 않았다. 읽기와 증가가 분리되어 있다는 것과 하한 가드가 없다는 것으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-grpc-testkit-f06.md b/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-grpc-testkit-f06.md deleted file mode 100644 index 5e85552..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-grpc-testkit-f06.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -kind: CASE -slug: grpc-testkit-f06 -title: 던져 버릴 비밀번호를 만들어 놓고 외부 프로세스의 명령줄에 싣는다 -topic: security-policy-enforcement -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-testkit-f06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-testkit-f06 - file: ../../../final/evidence/rendered/grpc-testkit-f06.svg -evidence: - - ../../../final/evidence/raw/grpc-testkit-f06.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-testkit#L262 이다. -module: grpc-testkit -priority: P3 ---- - -# 던져 버릴 비밀번호를 만들어 놓고 외부 프로세스의 명령줄에 싣는다 - -GrpcTlsTestMaterial 이 상수 비밀번호를 피하는 이유를 세 줄로 적는다. 그리고 같은 클래스가 그 값을 keytool 인자로 넘긴다. - -## 문제 - -GrpcTlsTestMaterial 이 상수 비밀번호를 피하는 이유를 세 줄로 적는다. - -그리고 같은 클래스가 그 값을 keytool 인자로 넘긴다. - -## 결론 - -프로세스 명령줄은 같은 호스트의 다른 사용자가 ps 나 /proc//cmdline 로 읽을 수 있다. - -소스 리터럴보다 관측 가능성이 오히려 높다. - -영향은 작다 — 값이 매번 새로 만들어지고, 키스토어는 임시 디렉터리에 있으며 close() 가 지운다. - -기록하는 이유는 이 클래스가 정확히 그 위험 계층을 스스로 논증했다는 점이다. - -완화와 노출이 같은 메서드 안에 있다. - -keytool 은 -storepass:file 과 -keypass:file 을 받는다. - -임시 파일 하나면 명령줄에서 값이 사라진다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : GrpcTlsTestMaterial 참조 16건 검색과 비밀번호가 실리는 인자 목록 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-testkit#L262 에 있다. - -## 본문 - - - -`GrpcTlsTestMaterial` 이 상수 비밀번호를 피하는 이유를 세 줄로 적는다. 그리고 같은 클래스가 그 값을 `keytool` 인자로 넘긴다. - -## GrpcTlsTestMaterial 참조 위치 - -:::evidence key="grpc-testkit-f06" alt="코드베이스에서 GrpcTlsTestMaterial 를 검색한 출력 16줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcTlsTestMaterial 코드베이스 검색 — 16줄 · exit 0" zoom="true" -::: - -## 명령줄은 같은 호스트의 다른 사용자가 읽을 수 있다 - -`ps` 나 `/proc//cmdline` 로 읽는다 — 소스 리터럴보다 관측 가능성이 오히려 높다. - -## 영향은 작다 - -값이 매번 새로 만들어지고, 키스토어는 임시 디렉터리에 있으며 `close()` 가 지운다. 기록하는 이유는 이 클래스가 정확히 그 위험 계층을 스스로 논증했다는 점이다 — 완화와 노출이 같은 메서드 안에 있다. - -## 수정 - -`keytool` 은 `-storepass:file` 과 `-keypass:file` 을 받는다. 임시 파일 하나면 명령줄에서 값이 사라진다. - -## 확인하지 못한 것 - -keytool 명령줄 노출을 실제로 ps 로 관측하지 않았다. ProcessBuilder 인자 목록에 비밀번호가 들어간다는 것으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-ipv4-only-mask-passes-every-other-form.md b/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-ipv4-only-mask-passes-every-other-form.md deleted file mode 100644 index 6d5d78f..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-ipv4-only-mask-passes-every-other-form.md +++ /dev/null @@ -1,103 +0,0 @@ ---- -kind: CASE -slug: ipv4-only-mask-passes-every-other-form -title: 마스킹이 IPv4 만 알아서 검사가 나머지 주소 형태를 전부 통과시킨다 -topic: security-policy-enforcement -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:ipv4-only-mask-passes-every-other-form -evidenceCapturedOn: 2026-09-01 -body: case-ipv4-only-mask-passes-every-other-form.body.md -assets: - - key: ipv4-only-mask-passes-every-other-form - file: ../../../final/evidence/rendered/ipv4-only-mask-passes-every-other-form.svg -evidence: - - ../../../final/evidence/raw/ipv4-only-mask-passes-every-other-form.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-advanced-diagnostics#L123 이다. ---- - -# 마스킹이 IPv4 만 알아서 검사가 나머지 주소 형태를 전부 통과시킨다 - -주소 마스킹이 정규식 하나만 갖고 맞지 않는 입력을 그대로 돌려준다. 스냅숏 생성자는 마스킹 결과가 입력과 같으면 이미 마스킹된 것으로 판정한다. 두 규칙이 겹쳐 IPv6 주소와 호스트 이름과 소켓 경로가 전부 통과한다. - -## 관계 - -- **위험한 조합은 정책이 아니라 생성자가 거부하게 만든다** - 같은 계열의 규칙이다. -- **붉은 test를 제품 결함으로 잘못 읽었다** - 같은 사이클이 철회한 P1 의 원인과 같은 계열의 가정이다. -- **문서끼리의 일치는 아무것도 증명하지 않는다** - 검사의 방향을 다루는 규칙이다. - -## 문제 - -진단 표면은 노출이 위험한 자리다. - -편집기 자바독이 그 이유를 적는다. 이 표면이 정말로 유용하기 때문에 위험하다는 것이다. 모든 소켓의 지역·원격 주소와 각 연결의 보안 세부와 호출별 상태를 담고 있으며, 종점이 존재하는 순간 그 전체가 인가 실수 하나 거리에 있다는 것이다. - -그래서 주소를 버리지 않고 가린다. 운영자가 두 서브채널을 구별할 수 있어야 하기 때문이다. - -그리고 스냅숏이 스스로 검사한다. 마스킹되지 않은 주소를 담은 스냅숏은 만들 수 없다. - -## 결론 - -그 검사의 범위가 좁다. - -마스킹 함수는 IPv4 와 선택적 포트를 인식하는 정규식 하나만 갖는다. 맞지 않는 입력은 치환이 일어나지 않아 입력 그대로 반환된다. - -그리고 스냅숏 생성자의 검사는 마스킹 결과가 입력과 같으면 이미 마스킹된 것으로 판정한다. - -두 규칙이 겹치면 IPv4 가 아닌 주소는 전부 검사를 통과한다. - -IPv6 주소가 통과한다. 파드 DNS 이름이 통과한다. 유닉스 소켓 경로도 통과한다. 마지막 것은 호스트 파일 시스템 경로다. - -이 플랫폼이 겨냥하는 배포 형태를 보면 도달 가능성이 낮지 않다. 같은 가족의 탐색 리프 전체가 쿠버네티스 라우팅을 주제로 하고, 헤드리스 레코드의 엔드포인트는 파드 DNS 이름이며 이중 스택 군집에서는 IPv6 주소다. - -편집기가 막으려 한 것이 정확히 그것이다. 피어 주소를 게시하는 진단 종점은 모든 소속의 연결을 게시한다는 문장이다. - -테스트가 이것을 볼 수 없다. 주소 리터럴이 전부 IPv4 다. - -같은 사이클이 철회한 최상위 판정의 원인도 같은 계열이었다. 이중 스택 호스트 이름이 픽스처에서 다른 주소로 풀린 것이다. 두 사례의 공통점은 주소 표현의 다양성이 아니라 판정의 방향이다. 둘 다 모르는 형태를 안전한 쪽이 아니라 통과 쪽으로 접었다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 정규식과 생성자 검사 대조, 테스트의 주소 리터럴 확인 -소스 수정 : x - -## 재현 조건 - -원문은 document-detail 의 final/document.md#a20-grpc-advanced-diagnostics 에 있다. - -1. 마스킹 함수의 정규식과 치환을 읽는다. -2. 맞지 않는 입력에서 무엇이 반환되는지 확인한다. -3. 스냅숏 생성자의 마스킹 검사 조건을 읽는다. -4. 두 규칙을 겹쳐 어떤 형태가 통과하는지 나열한다. -5. 테스트의 주소 리터럴을 전부 확인한다. - -## 본문 - - - -`maskAddress` 는 IPv4 패턴 하나만 갖고 맞지 않는 입력을 그대로 돌려준다. 그리고 스냅숏 생성자는 마스킹 결과가 입력과 같으면 이미 마스킹된 것으로 판정한다. - -## maskAddress 가 가진 패턴 - -:::evidence key="ipv4-only-mask-passes-every-other-form" alt="분석 문서 final/document.md#a20-grpc-advanced-diagnostics 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-advanced-diagnostics 발췌 — 15줄" zoom="true" -::: - -## 두 규칙이 겹치면 나머지가 전부 통과한다 - -IPv6 주소·파드 DNS 이름·유닉스 소켓 경로가 검사를 지난다. 이 플랫폼이 겨냥하는 배포가 쿠버네티스이고 헤드리스 레코드의 엔드포인트가 DNS 이름이므로 도달 가능한 형태다. - -## 테스트의 주소 리터럴은 전부 IPv4 다 - -같은 사이클이 철회한 P1 의 원인도 듀얼스택 호스트명이었다 — 판정이 모르는 형태를 통과 쪽으로 접는 같은 계열이다. - -## 확인하지 못한 것 - -IPv6 주소로 스냅숏을 만들어 통과를 재현하지 않았다. 이 리프는 고급 가족이라 배선 경로가 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-messaging-admin-api-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-messaging-admin-api-f02.md deleted file mode 100644 index 978e58e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-messaging-admin-api-f02.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -kind: CASE -slug: messaging-admin-api-f02 -title: 계획 다이제스트가 승인 정규 형식과 다른 인코딩을 쓴다 -topic: security-policy-enforcement -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-admin-api-f02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-admin-api-f02 - file: ../../../final/evidence/rendered/messaging-admin-api-f02.svg -evidence: - - ../../../final/evidence/raw/messaging-admin-api-f02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-admin-api#L944 이다. -module: messaging-admin-api -priority: P3 ---- - -# 계획 다이제스트가 승인 정규 형식과 다른 인코딩을 쓴다 - -ApprovalGrant.canonicalForm() 은 길이 접두를, ReplayPlan.digest()/RedrivePlan.digest() 는 String.join("|", …) 를 쓴다(§12.3(b), EVD-304). 현재는 충돌을 만들 수 없다 — 자유 형식 필드가 topologyVersion 하나뿐이기 때문이다. - -## 문제 - -ApprovalGrant.canonicalForm() 은 길이 접두를, ReplayPlan.digest()/RedrivePlan.digest() 는 String.join("|", …) 를 쓴다(§12.3(b), EVD-304). - -현재는 충돌을 만들 수 없다 — 자유 형식 필드가 topologyVersion 하나뿐이기 때문이다. - -## 결론 - -그러나 그 조건은 코드 어디에도 적혀 있지 않고, 필드가 하나 추가되면 조용히 깨진다. - -ApprovalGrant 의 appendField 를 PlanDigest 쪽으로 옮겨 재사용하는 편이 낫다 — 규칙과 그 근거가 이미 같은 리프에 있다. - -부수적으로 topologyVersion 에 형식 제약을 주는 것도 검토할 만하다. - -지금은 isBlank() 만 본다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : ApprovalGrant 참조 32건 검색과 두 인코딩 방식의 형태 대조, 자유 형식 필드 전수 조사 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-admin-api#L944 에 있다. - -## 본문 - - - -`ApprovalGrant.canonicalForm()` 은 길이 접두를, `ReplayPlan.digest()`/`RedrivePlan.digest()` 는 `String.join("|", …)` 를 쓴다(§12.3(b), `EVD-304`). - -## ApprovalGrant 참조 위치 - -:::evidence key="messaging-admin-api-f02" alt="코드베이스에서 ApprovalGrant 를 검색한 출력 32줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ApprovalGrant 코드베이스 검색 — 32줄 · exit 0" zoom="true" -::: - -## 현재는 충돌을 만들 수 없다 - -자유 형식 필드가 `topologyVersion` 하나뿐이기 때문이다. 그러나 그 조건은 코드 어디에도 적혀 있지 않고, 필드가 하나 추가되면 조용히 깨진다. - -## 규칙과 근거가 이미 같은 리프에 있다 - -`ApprovalGrant` 의 `appendField` 를 `PlanDigest` 쪽으로 옮겨 재사용하는 편이 낫다. 부수적으로 `topologyVersion` 에 형식 제약을 주는 것도 검토할 만하다 — 지금은 `isBlank()` 만 본다. - -## 확인하지 못한 것 - -topologyVersion 이 실제 배포에서 어떤 형식인지 확인하지 못했다. BrokerTopologyInspector 구현이 이 저장소에 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-messaging-admin-api-f05.md b/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-messaging-admin-api-f05.md deleted file mode 100644 index 065d8d0..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-messaging-admin-api-f05.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -kind: CASE -slug: messaging-admin-api-f05 -title: VerifiedApproval 의 위조 방지가 package-private 에만 의존한다 -topic: security-policy-enforcement -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-admin-api-f05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-admin-api-f05 - file: ../../../final/evidence/rendered/messaging-admin-api-f05.svg -evidence: - - ../../../final/evidence/raw/messaging-admin-api-f05.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-admin-api#L962 이다. -module: messaging-admin-api -priority: P3 ---- - -# VerifiedApproval 의 위조 방지가 package-private 에만 의존한다 - -이 저장소는 JPMS 를 쓰지 않는다(module-info.java 0개). 따라서 어떤 모듈이든 package dev.caskeleton.messaging.admin; 을 선언하면 VerifiedApproval.of(grant) 를 호출할 수 있다. - -## 문제 - -이 저장소는 JPMS 를 쓰지 않는다(module-info.java 0개). - -따라서 어떤 모듈이든 package dev.caskeleton.messaging.admin; 을 선언하면 VerifiedApproval.of(grant) 를 호출할 수 있다. - -## 결론 - -현재 그런 파일은 없지만, 이 타입의 존재 이유가 "아무도 만들 수 없다" 이므로 그 조건을 자동으로 지키는 검사가 있어야 한다. - -ApprovalForgeryTest.aVerifiedApprovalCannotBeConstructedOutsideTheVerifier 가 있으나, 그것은 같은 패키지 안에서 API 표면을 확인하는 테스트지 다른 모듈의 패키지 선언을 막지 못한다. - -ArchUnit 규칙 한 줄 — "dev.caskeleton.messaging.admin 패키지는 messaging-admin-api 소스 경로에만 존재한다" — 이면 된다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : VerifiedApproval 참조 40건 검색과 저장소 전체의 module-info.java 수 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-admin-api#L962 에 있다. - -## 본문 - - - -이 저장소는 JPMS 를 쓰지 않는다(`module-info.java` 0개). 따라서 어떤 모듈이든 `package dev.caskeleton.messaging.admin;` 을 선언하면 `VerifiedApproval.of(grant)` 를 호출할 수 있다. - -## VerifiedApproval 참조 위치 - -:::evidence key="messaging-admin-api-f05" alt="코드베이스에서 VerifiedApproval 를 검색한 출력 40줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="VerifiedApproval 코드베이스 검색 — 40줄 · exit 0" zoom="true" -::: - -## 이 타입의 존재 이유가 그 조건이다 - -"아무도 만들 수 없다" 이므로 그 조건을 자동으로 지키는 검사가 있어야 한다. 현재 그런 파일은 없다. - -## 기존 테스트는 다른 것을 본다 - -`ApprovalForgeryTest.aVerifiedApprovalCannotBeConstructedOutsideTheVerifier` 는 같은 패키지 안에서 API 표면을 확인하는 테스트지 다른 모듈의 패키지 선언을 막지 못한다. - -## 수정 - -ArchUnit 규칙 한 줄 — "`dev.caskeleton.messaging.admin` 패키지는 `messaging-admin-api` 소스 경로에만 존재한다" — 이면 된다. - -## 확인하지 못한 것 - -같은 패키지를 선언하는 모듈을 만들어 실제로 위조를 시도하지 않았다. JPMS 미사용과 접근 제한자로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-messaging-admin-api-f07.md b/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-messaging-admin-api-f07.md deleted file mode 100644 index fc0ec72..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-messaging-admin-api-f07.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -kind: CASE -slug: messaging-admin-api-f07 -title: 같은 인가 실패 코드가 세 파일에 문자열 리터럴로 흩어져 있다 -topic: security-policy-enforcement -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-admin-api-f07 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-admin-api-f07 - file: ../../../final/evidence/rendered/messaging-admin-api-f07.svg -evidence: - - ../../../final/evidence/raw/messaging-admin-api-f07.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-admin-api#L972 이다. -module: messaging-admin-api -priority: P3 ---- - -# 같은 인가 실패 코드가 세 파일에 문자열 리터럴로 흩어져 있다 - -APPROVAL_OPERATION_MISMATCH, APPROVAL_SOURCE_MISMATCH, APPROVAL_PLAN_MISMATCH, APPROVAL_EXPIRED 가 HmacApprovalVerifier, DestructiveOperationGuard, ApprovedReplayPlan, ApprovedRedrivePlan 에 각각 리터럴로 존재하며 메시지 문구가 서로 다르다. 검사의 3중화 자체는 의도된 심층 방어지만(§12.2 대조군 2), 코드 문자열은 상수 하나로 모으는 편이 집계와 검색에 낫다. - -## 문제 - -APPROVAL_OPERATION_MISMATCH, APPROVAL_SOURCE_MISMATCH, APPROVAL_PLAN_MISMATCH, APPROVAL_EXPIRED 가 HmacApprovalVerifier, DestructiveOperationGuard, ApprovedReplayPlan, ApprovedRedrivePlan 에 각각 리터럴로 존재하며 메시지 문구가 서로 다르다. - -검사의 3중화 자체는 의도된 심층 방어지만(§12.2 대조군 2), 코드 문자열은 상수 하나로 모으는 편이 집계와 검색에 낫다. - -## 결론 - -APPROVAL_OPERATION_MISMATCH, APPROVAL_SOURCE_MISMATCH, APPROVAL_PLAN_MISMATCH, APPROVAL_EXPIRED 가 HmacApprovalVerifier, DestructiveOperationGuard, ApprovedReplayPlan, ApprovedRedrivePlan 에 각각 리터럴로 존재하며 메시지 문구가 서로 다르다. - -검사의 3중화 자체는 의도된 심층 방어지만(§12.2 대조군 2), 코드 문자열은 상수 하나로 모으는 편이 집계와 검색에 낫다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : HmacApprovalVerifier 참조 14건 검색과 네 코드가 리터럴로 선언된 파일 집계 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-admin-api#L972 에 있다. - -## 본문 - - - -`APPROVAL_OPERATION_MISMATCH`, `APPROVAL_SOURCE_MISMATCH`, `APPROVAL_PLAN_MISMATCH`, `APPROVAL_EXPIRED` 가 `HmacApprovalVerifier`, `DestructiveOperationGuard`, `ApprovedReplayPlan`, `ApprovedRedrivePlan` 에 각각 리터럴로 존재하며 메시지 문구가 서로 다르다. - -## HmacApprovalVerifier 참조 위치 - -:::evidence key="messaging-admin-api-f07" alt="코드베이스에서 HmacApprovalVerifier 를 검색한 출력 14줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="HmacApprovalVerifier 코드베이스 검색 — 14줄 · exit 0" zoom="true" -::: - -## 3중화 자체는 의도된 심층 방어다 - -§12.2 대조군 2. 다만 코드 문자열은 상수 하나로 모으는 편이 집계와 검색에 낫다. - -## 확인하지 못한 것 - -한 리터럴만 바꿔 검증이 조용히 어긋나는 것을 실행으로 재현하지 않았다. 리터럴의 분포로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-messaging-admin-runtime-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-messaging-admin-runtime-f01.md deleted file mode 100644 index 360dfee..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-messaging-admin-runtime-f01.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -kind: CASE -slug: messaging-admin-runtime-f01 -title: 파괴적 작업의 승인만 위조 가능한 형태로 남아 있다 -topic: security-policy-enforcement -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-admin-runtime-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-admin-runtime-f01 - file: ../../../final/evidence/rendered/messaging-admin-runtime-f01.svg - - key: messaging-admin-runtime-f01-diagram - file: ../../../final/assets/diagrams/messaging-admin-runtime-f01.svg -evidence: - - ../../../final/evidence/raw/messaging-admin-runtime-f01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-admin-runtime#L883 이다. -module: messaging-admin-runtime -priority: P2 ---- - -# 파괴적 작업의 승인만 위조 가능한 형태로 남아 있다 - -생성자는 null·음수만 본다. approval 이 이 operation 을 인가하는지, 이 destination 을 인가하는지, estimatedMessagesAffected 가 승인 상한 이하인지 — 아무것도 검사하지 않는다. - -## 문제 - -생성자는 null·음수만 본다. - -approval 이 이 operation 을 인가하는지, 이 destination 을 인가하는지, estimatedMessagesAffected 가 승인 상한 이하인지 — 아무것도 검사하지 않는다. - -## 결론 - -계획 다이제스트 필드 자체가 없다. - -이 형태가 정확히 messaging-admin-api 가 고쳤다고 기록한 것이다. - -수정은 REPLAY·REDRIVE(복구 가능한 작업)에 적용되었고, PURGE·DELETE_DESTINATION·OFFSET_RESET(복구 불가능한 작업)에는 적용되지 않았다. - -현재 구현체가 0건이라 실행되는 결함은 아니다(EVD-307). - -그러나 이 인터페이스는 운영자 도구가 구현하라고 존재하는 것이고, 그 도구가 생기는 순간의 모양이 이것이다. - -Approved 를 ApprovedReplayPlan 과 같은 형태로 — VerifiedApproval + 생성자 검사 — 바꾸는 것이 맞다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : Approved record 의 생성자 검사 항목과 검증된 승인 타입이 적용된 작업 범위의 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-admin-runtime#L883 에 있다. - -## 본문 - - - -생성자는 null·음수만 본다. `approval` 이 이 `operation` 을 인가하는지, 이 `destination` 을 인가하는지, `estimatedMessagesAffected` 가 승인 상한 이하인지 — 아무것도 검사하지 않는다. 계획 다이제스트 필드 자체가 없다. - -## 보호가 적용된 절반 - -:::evidence key="messaging-admin-runtime-f01-diagram" alt="REPLAY 와 REDRIVE 가 검증된 승인이 덮는 범위 안에 놓이고 PURGE 와 OFFSET_RESET 과 DELETE_DESTINATION 이 바깥에 빗금으로 놓인다" caption="보호가 적용된 절반" zoom="false" -::: - -## 생성자가 보는 것 - -:::evidence key="messaging-admin-runtime-f01" alt="분석 문서 final/document.md#a19-messaging-admin-runtime 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a19-messaging-admin-runtime 발췌 — 15줄" zoom="true" -::: - -## 정확히 admin-api 가 고쳤다고 기록한 형태다 - -수정은 `REPLAY`·`REDRIVE`(복구 가능한 작업)에 적용되었고, `PURGE`·`DELETE_DESTINATION`·`OFFSET_RESET`(복구 불가능한 작업)에는 적용되지 않았다. - -## 지금 실행되는 결함은 아니다 - -현재 구현체가 0건이다(`EVD-307`). 그러나 이 인터페이스는 운영자 도구가 구현하라고 존재하는 것이고, 그 도구가 생기는 순간의 모양이 이것이다. `Approved` 를 `ApprovedReplayPlan` 과 같은 형태로 — `VerifiedApproval` + 생성자 검사 — 바꾸는 것이 맞다. - -## 확인하지 못한 것 - -위조 승인을 넣어 실행해 보지 않았다. 구현체가 0 건이라 실행 경로 자체가 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-messaging-admin-runtime-f05.md b/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-messaging-admin-runtime-f05.md deleted file mode 100644 index 1b596af..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-messaging-admin-runtime-f05.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -kind: CASE -slug: messaging-admin-runtime-f05 -title: 감사 싱크가 중복 선언되어 있고 레닥션 계약이 유실된다 -topic: security-policy-enforcement -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-admin-runtime-f05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-admin-runtime-f05 - file: ../../../final/evidence/rendered/messaging-admin-runtime-f05.svg -evidence: - - ../../../final/evidence/raw/messaging-admin-runtime-f05.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-admin-runtime#L930 이다. -module: messaging-admin-runtime -priority: P3 ---- - -# 감사 싱크가 중복 선언되어 있고 레닥션 계약이 유실된다 - -RedriveService.AuditSink 는 MessagingAuditSink 와 시그니처가 같다. admin-runtime 은 이미 messaging-observability 를 의존한다. - -## 문제 - -RedriveService.AuditSink 는 MessagingAuditSink 와 시그니처가 같다. - -admin-runtime 은 이미 messaging-observability 를 의존한다. - -## 결론 - -표준 싱크를 쓰면 세 가지가 함께 해결된다: ReplayService 가 형제의 중첩 타입에 의존하는 것, InMemory 구현 재작성, 그리고 무엇보다 "모든 기록이 MessagingRedactor 를 통과했다" 는 계약. - -현재 RedriveService:125-136 은 목적지 이름과 details 를 그대로 넣는다. - -목적지 이름은 DestinationName 이라 형식이 제한되어 있어 지금은 문제가 아니지만, 계약이 없는 자리에 값이 늘어나는 것을 막을 것이 없다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : RedriveService 참조 15건 검색과 중첩 인터페이스·플랫폼 싱크의 시그니처 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-admin-runtime#L930 에 있다. - -## 본문 - - - -`RedriveService.AuditSink` 는 `MessagingAuditSink` 와 시그니처가 같다. admin-runtime 은 이미 `messaging-observability` 를 의존한다. - -## RedriveService 참조 위치 - -:::evidence key="messaging-admin-runtime-f05" alt="코드베이스에서 RedriveService 를 검색한 출력 15줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RedriveService 코드베이스 검색 — 15줄 · exit 0" zoom="true" -::: - -## 표준 싱크를 쓰면 셋이 함께 해결된다 - -`ReplayService` 가 형제의 중첩 타입에 의존하는 것, `InMemory` 구현 재작성, 그리고 무엇보다 **"모든 기록이 `MessagingRedactor` 를 통과했다" 는 계약**. - -## 지금은 계약이 없는 자리다 - -`RedriveService:125-136` 은 목적지 이름과 details 를 그대로 넣는다. 목적지 이름은 `DestinationName` 이라 형식이 제한되어 있어 지금은 문제가 아니지만, 계약이 없는 자리에 값이 늘어나는 것을 막을 것이 없다. - -## 확인하지 못한 것 - -레닥션이 빠진 감사 이벤트를 실제로 기록해 관측하지 않았다. 두 시그니처의 대조와 의존 선언으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-messaging-admin-runtime-f08.md b/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-messaging-admin-runtime-f08.md deleted file mode 100644 index 34a9ce3..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-messaging-admin-runtime-f08.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -kind: CASE -slug: messaging-admin-runtime-f08 -title: 격리 리플레이의 guard 우회가 dryRun 파라미터로 표현된다 -topic: security-policy-enforcement -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-admin-runtime-f08 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-admin-runtime-f08 - file: ../../../final/evidence/rendered/messaging-admin-runtime-f08.svg -evidence: - - ../../../final/evidence/raw/messaging-admin-runtime-f08.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-admin-runtime#L948 이다. -module: messaging-admin-runtime -priority: P3 ---- - -# 격리 리플레이의 guard 우회가 dryRun 파라미터로 표현된다 - -판단 자체는 근거가 있다. 다만 "승인이 필요 없다" 와 "실제로는 아무것도 하지 않는다" 가 guard 입장에서 구별되지 않는다. - -## 문제 - -판단 자체는 근거가 있다. - -다만 "승인이 필요 없다" 와 "실제로는 아무것도 하지 않는다" 가 guard 입장에서 구별되지 않는다. - -## 결론 - -DestructiveOperationGuard 에 skipAuthorization 성격의 별도 경로를 두거나, 격리 리플레이는 애초에 guard 를 거치지 않는 편이 의도를 드러낸다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : DestructiveOperationGuard 참조 19건 검색과 guard 에 넘어가는 인자의 의미 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-admin-runtime#L948 에 있다. - -## 본문 - - - -guard 호출이 두 뜻을 한 인자로 합친다. - -```java -// ReplayService.java:58-64 -guard.authorize(REPLAY, request.destination(), approval, request.dryRun() || !needsApproval, now); -``` - -## DestructiveOperationGuard 참조 위치 - -:::evidence key="messaging-admin-runtime-f08" alt="코드베이스에서 DestructiveOperationGuard 를 검색한 출력 19줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DestructiveOperationGuard 코드베이스 검색 — 19줄 · exit 0" zoom="true" -::: - -## 판단 자체는 근거가 있다 - -다만 "승인이 필요 없다" 와 "실제로는 아무것도 하지 않는다" 가 guard 입장에서 구별되지 않는다. `DestructiveOperationGuard` 에 `skipAuthorization` 성격의 별도 경로를 두거나, 격리 리플레이는 애초에 guard 를 거치지 않는 편이 의도를 드러낸다. - -## 확인하지 못한 것 - -guard 를 실제로 호출해 두 경우가 구별되지 않는 것을 관측하지 않았다. 인자 하나가 두 뜻을 겸한다는 호출 형태로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-messaging-observability-f05.md b/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-messaging-observability-f05.md deleted file mode 100644 index 437c8f4..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-messaging-observability-f05.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -kind: CASE -slug: messaging-observability-f05 -title: 자격증명 판정이 core-api보다 약하다 -topic: security-policy-enforcement -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-observability-f05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-observability-f05 - file: ../../../final/evidence/rendered/messaging-observability-f05.svg -evidence: - - ../../../final/evidence/raw/messaging-observability-f05.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-observability#L708 이다. -module: messaging-observability -priority: P3 ---- - -# 자격증명 판정이 core-api보다 약하다 - -MessagingRedactor.isDenied는 27키 정확 일치다. messaging-core-api의 MessageHeaders.carriesACredential은 세그먼트 매칭 + 인접 결합으로 x-api-key·auth-token·db_password를 잡는다. - -## 관계 - -- **타입이 문서화한 불변식은 타입이 강제한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 문제 - -MessagingRedactor.isDenied는 27키 정확 일치다. - -messaging-core-api의 MessageHeaders.carriesACredential은 세그먼트 매칭 + 인접 결합으로 x-api-key·auth-token·db_password를 잡는다. - -## 결론 - -redactor의 denylist에 api_key·apikey는 있으나 x-api-key는 없다. - -두 표면이 다르지만 더 자유로운 입력을 받는 쪽이 더 약하다. - -이 leaf 자신이 진단 맵을 "the one place where a caller can pass arbitrary keys"라고 부른다. - -그리고 recordDiagnostics가 redaction을 첫 단계로 두는 이유가 바로 그것이다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : MessagingRedactor 참조 13건 검색과 core-api 쪽 판정 함수의 매칭 방식 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-observability#L708 에 있다. - -## 본문 - - - -`MessagingRedactor.isDenied`는 27키 **정확 일치**다. `messaging-core-api`의 `MessageHeaders.carriesACredential`은 세그먼트 매칭 + 인접 결합으로 `x-api-key`·`auth-token`·`db_password`를 잡는다. - -## MessagingRedactor 참조 위치 - -:::evidence key="messaging-observability-f05" alt="코드베이스에서 MessagingRedactor 를 검색한 출력 13줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingRedactor 코드베이스 검색 — 13줄 · exit 0" zoom="true" -::: - -## 더 자유로운 입력을 받는 쪽이 더 약하다 - -redactor의 denylist에 `api_key`·`apikey`는 있으나 `x-api-key`는 없다. 이 leaf 자신이 진단 맵을 "the one place where a caller can pass arbitrary keys"라고 부르고, `recordDiagnostics`가 redaction을 첫 단계로 두는 이유가 바로 그것이다. - -## 확인하지 못한 것 - -두 판정이 실제 헤더 집합에서 얼마나 벌어지는지 측정하지 않았다. 매칭 방식(정확 일치 대 세그먼트 매칭)의 코드 대조로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-messaging-security-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-messaging-security-f01.md deleted file mode 100644 index 2281a7c..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-messaging-security-f01.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -kind: CASE -slug: messaging-security-f01 -title: 같은 TLS posture를 두 클래스가 다른 엄격도로 검사한다 -topic: security-policy-enforcement -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-security-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-security-f01 - file: ../../../final/evidence/rendered/messaging-security-f01.svg - - key: messaging-security-f01-diagram - file: ../../../final/assets/diagrams/messaging-security-f01.svg -evidence: - - ../../../final/evidence/raw/messaging-security-f01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-security#L634 이다. -module: messaging-security -priority: P2 ---- - -# 같은 TLS posture를 두 클래스가 다른 엄격도로 검사한다 - -MessageSecurityValidator는 hostname 검증을 production && !hostnameVerification일 때만 요구하고, BrokerTlsPolicy는 tlsEnabled && !hostnameVerification일 때 요구한다. 전자는 코드 없는 IllegalArgumentException, 후자는 안정 코드가 붙은 MessagingConfigurationException을 던진다. - -## 관계 - -- **배선된 게이트는 자기 leaf 레인에서 검증한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 술어가 두 타입에 있으면 하나가 다른 하나를 부른다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **가변 필드로 상태 전이를 표현하면 가시성을 함께 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **맵 갱신 함수 안에서 I/O를 하면 그 지연이 락 범위가 된다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 문제 - -MessageSecurityValidator는 hostname 검증을 production && !hostnameVerification일 때만 요구하고, BrokerTlsPolicy는 tlsEnabled && !hostnameVerification일 때 요구한다. - -전자는 코드 없는 IllegalArgumentException, 후자는 안정 코드가 붙은 MessagingConfigurationException을 던진다. - -## 결론 - -둘 다 같은 BrokerSecurityProfile을 받고, 후자만 어댑터에서 실제로 호출된다. - -비운영에서 TLS를 켜고 hostname 검증을 끈 구성을 두 검사가 다르게 판정한다. - -그리고 이 leaf 자신의 javadoc이 그 구성을 "looks encrypted in every dashboard while accepting any certificate a man in the middle presents"라고 부른다 — 즉 더 느슨한 쪽이 그 위험을 통과시킨다. - -실패 형태도 달라서 운영자가 두 어휘를 알아야 한다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : MessageSecurityValidator 참조 7건 검색과 두 클래스의 검사 조건·예외 타입 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-security#L634 에 있다. - -## 본문 - - - -`MessageSecurityValidator`는 hostname 검증을 `production && !hostnameVerification`일 때만 요구하고, `BrokerTlsPolicy`는 `tlsEnabled && !hostnameVerification`일 때 요구한다. - -## 두 검사의 차이 - -:::evidence key="messaging-security-f01-diagram" alt="검증기 쪽에 production 일 때만과 코드 없는 예외가 빗금으로 놓이고 정책 쪽에 tls 켜지면 언제나와 안정 코드 예외가 놓인다" caption="두 검사의 차이" zoom="false" -::: - -전자는 코드 없는 `IllegalArgumentException`, 후자는 안정 코드가 붙은 `MessagingConfigurationException`을 던진다. - -## MessageSecurityValidator 참조 위치 - -:::evidence key="messaging-security-f01" alt="코드베이스에서 MessageSecurityValidator 를 검색한 출력 7줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessageSecurityValidator 코드베이스 검색 — 7줄 · exit 0" zoom="true" -::: - -## 느슨한 쪽이 위험을 통과시킨다 - -둘 다 같은 `BrokerSecurityProfile`을 받고, 후자만 어댑터에서 실제로 호출된다. 비운영에서 TLS를 켜고 hostname 검증을 끈 구성을 두 검사가 다르게 판정한다. 이 leaf 자신의 javadoc이 그 구성을 "looks encrypted in every dashboard while accepting any certificate a man in the middle presents"라고 부른다. 실패 형태도 달라서 운영자가 두 어휘를 알아야 한다. - -## 확인하지 못한 것 - -validate 가 실제로 호출되는지 확인하지 못했다. starter 가 bean 을 만들고 같은 파일에서 직접 부를 가능성이 있으며, 그 leaf 가 답한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-messaging-security-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-messaging-security-f02.md deleted file mode 100644 index 982a1ab..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-messaging-security-f02.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -kind: CASE -slug: messaging-security-f02 -title: 권한 거부가 AUTHORIZATION이 아니라 CONFIGURATION으로 기록된다 -topic: security-policy-enforcement -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-security-f02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-security-f02 - file: ../../../final/evidence/rendered/messaging-security-f02.svg - - key: messaging-security-f02-diagram - file: ../../../final/assets/diagrams/messaging-security-f02.svg -evidence: - - ../../../final/evidence/raw/messaging-security-f02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-security#L643 이다. -module: messaging-security -priority: P2 ---- - -# 권한 거부가 AUTHORIZATION이 아니라 CONFIGURATION으로 기록된다 - -DestinationAccessValidator.requirePublish는 MessageAuthorizationException("DESTINATION_PUBLISH_DENIED")을 던지고 그 카테고리는 AUTHORIZATION이다. 소비자가 0이다. - -## 관계 - -- **배선된 게이트는 자기 leaf 레인에서 검증한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 술어가 두 타입에 있으면 하나가 다른 하나를 부른다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **가변 필드로 상태 전이를 표현하면 가시성을 함께 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **맵 갱신 함수 안에서 I/O를 하면 그 지연이 락 범위가 된다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 문제 - -DestinationAccessValidator.requirePublish는 MessageAuthorizationException("DESTINATION_PUBLISH_DENIED")을 던지고 그 카테고리는 AUTHORIZATION이다. - -소비자가 0이다. - -## 결론 - -실제 발행 경로는 access.mayPublish를 직접 묻고 rejected("PUBLISH_FORBIDDEN", ...)을 반환하는데, rejected(...)는 FailureCategory.CONFIGURATION을 붙인다. - -FailureCategory는 "stable classification a retry engine, DLQ router, and dashboard all agree on"이다(messaging-core-api §4.12). - -권한 거부가 구성 오류로 분류되면 보안 대시보드가 그것을 보지 못하고, 구성 오류 알림이 권한 거부로 오염된다. - -그리고 AUTHORIZATION 카테고리를 쓰는 유일한 코드가 미사용 클래스에 있다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : DestinationAccessValidator 참조 2건 검색과 실제 발행 경로가 붙이는 범주 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-security#L643 에 있다. - -## 본문 - - - -`DestinationAccessValidator.requirePublish`는 `MessageAuthorizationException("DESTINATION_PUBLISH_DENIED")`을 던지고 그 카테고리는 `AUTHORIZATION`이다. 소비자가 0이다. - -## 기록되는 범주 - -:::evidence key="messaging-security-f02-diagram" alt="CONFIGURATION 만 실행되는 분류 안에 놓이고 AUTHORIZATION 이 바깥에 빗금으로 놓인다" caption="기록되는 범주" zoom="false" -::: - -실제 발행 경로는 `access.mayPublish`를 직접 묻고 `rejected("PUBLISH_FORBIDDEN", ...)`을 반환하는데, `rejected(...)`는 `FailureCategory.CONFIGURATION`을 붙인다. - -## DestinationAccessValidator 참조 위치 - -:::evidence key="messaging-security-f02" alt="코드베이스에서 DestinationAccessValidator 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DestinationAccessValidator 코드베이스 검색 — 2줄 · exit 0" zoom="true" -::: - -## 세 소비자의 합의가 어긋난다 - -`FailureCategory`는 "stable classification a retry engine, DLQ router, and dashboard all agree on"이다(`messaging-core-api` §4.12). 권한 거부가 구성 오류로 분류되면 보안 대시보드가 그것을 보지 못하고, 구성 오류 알림이 권한 거부로 오염된다. 그리고 `AUTHORIZATION` 카테고리를 쓰는 유일한 코드가 미사용 클래스에 있다. - -## 확인하지 못한 것 - -권한 거부를 실제로 발생시켜 대시보드에 어떤 범주로 집계되는지 관측하지 않았다. 두 경로가 붙이는 범주의 코드 대조로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/question/openquestion-messaging-security-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/question/openquestion-messaging-security-f03.md deleted file mode 100644 index d8c48e3..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/question/openquestion-messaging-security-f03.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -kind: QUESTION -slug: messaging-security-f03 -title: ACL 매니페스트 전체가 쓰이지 않는다 -topic: security-policy-enforcement -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: open-question:messaging-security-f03 -questionStatus: OPEN -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-security#L652 ---- - -# ACL 매니페스트 전체가 쓰이지 않는다 - -브로커가 producer 자격증명에 파괴적 권한을 준 경우를 아무도 보지 않는다. 그것을 보라고 만든 매니페스트가 소비자 0 이다. - -## 관계 - -- **배선된 게이트는 자기 leaf 레인에서 검증한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 술어가 두 타입에 있으면 하나가 다른 하나를 부른다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **가변 필드로 상태 전이를 표현하면 가시성을 함께 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **맵 갱신 함수 안에서 I/O를 하면 그 지연이 락 범위가 된다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 사실 - -BrokerAclManifest 의 세 메서드(requireApplicationRuntime · undeclared · missing)와 두 enum 이 소비자 0 이다. git grep -l 'BrokerAclManifest' -- src ':!src/messaging/messaging-security' 가 아무것도 돌려주지 않는다. - -javadoc 은 "The manifest is what the platform checks itself against at startup" 이라고 적는다. - -"애플리케이션 런타임은 파괴적 권한을 갖지 않는다" 가 이 leaf 의 핵심 원칙 중 하나인데, MessageSecurityValidator 는 admin 자격증명의 부재만 검사한다. - -## 미지수 - -브로커 ACL 을 읽는 경로가 있는가. 그 답은 messaging-admin-api 가 갖는다. 읽을 수 없다면 이 매니페스트는 자기 점검이 아니라 선언에 그친다. - -## 선택지 - -startup 검사에 배선한다 - 선언된 권한과 실제 권한의 차이가 부팅 시점에 드러난다. - -읽기 경로가 없음을 javadoc 에 적는다 - "platform checks itself" 라는 문장이 현재 상태와 맞아진다. - -## 다음 검증 - -messaging-admin-api 에서 브로커 ACL 조회 경로의 존재를 확인한다. 그 leaf 가 답을 소유한다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/reference/reference-messaging-security-f06.md b/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/reference/reference-messaging-security-f06.md deleted file mode 100644 index 68dd14b..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/reference/reference-messaging-security-f06.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-security-f06 -title: 맵 갱신 함수 안에서 I/O를 하면 그 지연이 락 범위가 된다 -topic: security-policy-enforcement -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-security-f06 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-security#L679 ---- - -# 맵 갱신 함수 안에서 I/O를 하면 그 지연이 락 범위가 된다 - -## 관계 - -- **종료 시 자격증명 소거가 호출되지 않는다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **같은 TLS posture를 두 클래스가 다른 엄격도로 검사한다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **권한 거부가 `AUTHORIZATION`이 아니라 `CONFIGURATION`으로 기록된다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. - -## 목적 - -single-flight 를 얻은 대가로 같은 credential id 를 요청하는 스레드가 저장소 왕복 동안 막힌다. 그것은 의도이고 옳다. 의도가 아닌 것은 ConcurrentHashMap 의 bin 을 공유하는 다른 credential id 까지 함께 막히는 것, 그리고 저장소 지연이 타임아웃 없이 발행 경로로 전파되는 것이다. - -## 규칙 - -1. 갱신 함수 안에서 외부 호출이 일어나는지 본다 - resolve 가 resolved.compute(credentialId, (key, existing) -> { ... provider.resolve(key) ... }) 형태다. CredentialProvider.resolve 는 외부 비밀 저장소를 호출할 수 있는 port 다. - -2. 락 범위가 키 단위가 아니라 bin 단위임을 계산에 넣는다 - 해시가 충돌하는 다른 키의 요청도 같은 대기에 들어간다. - -3. 구조를 유지한다면 시간 계약을 port 에 적는다 - CredentialProvider javadoc 에 "구현은 유한 시간 안에 반환해야 한다" 를 명시한다. - -## 적용 조건 - -compute · computeIfAbsent · merge 의 함수 인자 안에서 port 를 호출하는 모든 자리. - -## 예외 - -호출 대상이 같은 프로세스 안의 순수 계산이면 이 규칙의 대상이 아니다. 여기서는 port 뒤에 외부 저장소가 올 수 있다. - -## 예시 - -CredentialRuntimeRegistry.java:71-86 의 람다 본문. 확인 방법은 provider.resolve 호출 위치가 그 람다 안임을 보는 것이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/self-disclosure-grading/case/case-a-soak-threshold-written-for-a-grade-that-does-not-exist.md b/docs/clean-architecture-backend-template/tech-log-studio/self-disclosure-grading/case/case-a-soak-threshold-written-for-a-grade-that-does-not-exist.md deleted file mode 100644 index 1cf3d3f..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/self-disclosure-grading/case/case-a-soak-threshold-written-for-a-grade-that-does-not-exist.md +++ /dev/null @@ -1,120 +0,0 @@ ---- -kind: CASE -slug: a-soak-threshold-written-for-a-grade-that-does-not-exist -title: 담금 임계값이 열거형에 없는 등급을 위해 쓰여 중간 등급으로 올라가는 것이 더 어려워졌다 -topic: self-disclosure-grading -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a-soak-threshold-written-for-a-grade-that-does-not-exist -evidenceCapturedOn: 2026-09-02 -assets: - - key: a-soak-threshold-written-for-a-grade-that-does-not-exist - file: ../../../final/evidence/rendered/a-soak-threshold-written-for-a-grade-that-does-not-exist.svg -evidence: - - ../../../final/evidence/raw/a-soak-threshold-written-for-a-grade-that-does-not-exist.txt -source: - - 분석 문서는 grpc-advanced-bootstrap 편 §17.4 다. ---- - -# 담금 임계값이 열거형에 없는 등급을 위해 쓰여 중간 등급으로 올라가는 것이 더 어려워졌다 - -승격 게이트가 담금 기간 임계값 둘을 갖는다. 긴 쪽은 열거형에 없는 등급을 위해 쓰였고, 선택이 그 등급이 아닌 나머지 전부를 긴 쪽으로 보낸다. 그래서 담금 기록이 가장 적을 등급이 밟아야 하는 첫 걸음이 상위 등급으로 가는 것보다 엄격해졌다. - -## 관계 - -- **등급은 네 단계로 나누고 관측보다 높게 적지 않는다** - 같은 주제의 규칙이고 이 사례는 그 반대편이다. 여기서는 낮은 등급으로 올라가는 것이 더 어렵다. -- **지원 등급은 추론이 아니라 선언이고 증거 없이는 올라가지 않는다** - 등급 체계를 코드로 두는 결정이다. -- **이름이 검사한다고 말하는 것을 본문이 검사하지 않는 테스트 넷** - 이 결함을 덮은 테스트가 그 목록의 넷째다. - -## 문제 - -게이트 javadoc 의 모형은 등급 둘이다. 하나는 능력이 동작하고 문서화되었다는 것이고, 다른 하나는 모든 배포가 그것을 받게 된다는 것이다. 둘째가 의존성을 모든 클래스패스에 올리고 실패 양식을 모든 당직에 올리므로 첫째에 더해 더 긴 담금을 요구한다는 설명이 붙어 있다. - -상수도 둘이다. 짧은 쪽이 이레, 긴 쪽이 서른 날이다. - -## 결론 - -둘째 등급이 열거형에 없다. 값은 넷이고 그중 어느 것도 그 이름이 아니다. - -담금 선택은 목표 등급이 첫째 등급이면 짧은 쪽, 아니면 긴 쪽이다. 그러므로 첫째가 아닌 나머지 세 등급 전부가 긴 쪽으로 떨어진다. - -증거 항목 일곱 개는 목표 등급과 무관하게 요구된다. 그래서 결과가 뒤집힌다. 중간 등급에서 첫째 등급으로 올라가려면 일곱 항목과 이레가 필요하고, 추적 등급에서 중간 등급으로 올라가려면 일곱 항목과 서른 날이 필요하다. - -같은 메서드의 셋째 차단 사유가 추적 등급의 능력은 다른 무엇보다 먼저 중간 등급을 밟아야 한다고 적는다. 그 강제된 첫 걸음이 상위 등급으로 가는 것보다 엄격하다. 추적 등급의 뜻이 추적할 뿐 구현되지 않았다는 것이므로, 정의상 담금 기록이 가장 적을 등급에 가장 긴 담금이 붙는다. - -테스트가 그 뒤틀림을 그대로 보여 준다. 긴 갈래를 실행하려고 고른 전이가 철회 방향인데 표시 이름은 둘째 등급이 되는 것이라고 말한다. 그리고 중간 등급으로 올라가는 테스트는 담금을 예순 날로 준다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Gradle : 9.0.0 -확인 방식 : 평가 메서드 본문과 등급 열거형 대조, 두 테스트가 고른 숫자 확인, 해당 테스트 실행 -소스 수정 : x - -## 재현 조건 - -1. 승격 게이트의 두 담금 상수와 그 javadoc 의 등급 모형을 읽는다. -2. 등급 열거형의 값을 전부 나열하고 javadoc 의 둘째 등급이 그 안에 있는지 센다. -3. 증거 검사와 담금 선택을 잇는 구간을 읽고, 어느 쪽에 목표 등급 조건이 있는지 확인한다. -4. 추적 등급 차단 사유의 조건과 문구를 확인한다. -5. 긴 갈래를 실행하는 테스트가 고른 전이와 표시 이름을 대조하고, 중간 등급 테스트가 담금에 넣은 값을 확인한다. -6. 그 테스트들을 실행한다. - -## 본문 - - - -승격 게이트는 능력 하나의 등급 이동을 판정한다. 그 판정에 담금 기간 임계값이 둘 들어간다. - -## javadoc 이 말하는 둘째 등급이 열거형에 없다 - -:::evidence key="a-soak-threshold-written-for-a-grade-that-does-not-exist" alt="코드베이스에서 승격 게이트 javadoc 의 두 등급 모형과 담금 상수 둘, 등급 열거형의 값 넷과 둘째 등급에 해당하는 값의 수, 증거 검사와 담금 선택을 잇는 여섯 줄과 증거 항목 일곱, 추적 등급이 중간 등급을 먼저 밟게 하는 차단 사유, 긴 갈래를 실행하는 테스트가 고른 전이와 표시 이름, 중간 등급으로 올라가는 테스트가 준 담금 값, 그리고 그 테스트들을 실제로 돌린 결과를 뽑은 출력 66줄. 증거 검사에는 목표 등급 조건이 없고 담금 선택에만 있다는 것이 그 여섯 줄에 그대로 보인다." caption="두 등급 모형과 상수 둘 · 열거형 값 넷 · 증거 검사에는 목표 조건 없음 · 테스트가 고른 전이와 담금 · 실행 결과 8건 전부 통과 — 66줄 · exit 0" zoom="true" -::: - -javadoc 의 모형은 이렇다. 첫째 등급에 도달한다는 것은 능력이 동작하고 문서화되었다는 뜻이고, 둘째 등급이 된다는 것은 모든 배포가 그것을 받아 의존성이 모든 클래스패스에 오르고 실패 양식이 모든 당직에 오른다는 뜻이다. 그래서 둘째는 첫째에 더해 더 긴 담금을 요구한다. - -상수 이름도 그 모형을 따른다. 하나는 첫째 등급용 이레, 다른 하나는 둘째 등급용 서른 날이다. - -등급 열거형의 값은 넷이다. 증거가 갖춰진 것(첫째 등급), 실패 양식이 규명되지 않은 것(중간 등급), 추적만 하는 것(추적 등급), 철회되었거나 거부된 것이다. 둘째 등급에 해당하는 값은 0 이다. - -## 그래서 조건이 나머지 세 등급을 전부 받는다 - -담금을 고르는 식은 삼항 하나다. 목표 등급이 첫째 등급이면 이레, 아니면 서른 날. - -없는 등급을 위해 만든 갈래가 있는 세 등급을 전부 받는다. 중간 등급으로 올라가는 것도, 추적 등급으로 내려가는 것도, 철회되는 것도 서른 날이다. - -## 증거 요구는 목표를 보지 않는다 - -증거 검사와 담금 선택이 여섯 줄 안에 나란히 있다. 앞의 셋은 빠진 증거를 그대로 차단 사유로 옮기고, 목표 등급을 보지 않는다. 뒤의 둘만 목표 등급을 본다. - -요구되는 항목은 일곱이다. 호환성 증거, 보안 검토, 결함 증거, 성능 증거, 결정 기록, 런북, 실환경 시험이다. - -그래서 두 승격의 차이가 담금 기간뿐이 되고, 그 기간이 뒤집혀 있다. 중간 등급에서 위 등급으로 올라가는 데 이레, 추적 등급에서 중간 등급으로 올라오는 데 서른 날이다. - -## 담금 기록이 가장 적을 등급에 가장 긴 담금이 걸린다 - -같은 메서드의 셋째 차단 사유가 추적 등급을 다룬다. 추적 등급의 능력은 다른 무엇보다 먼저 중간 등급이 되어야 한다는 것이다. - -그 강제된 첫 걸음의 목표가 첫째 등급이 아니므로 서른 날이 걸린다. - -추적 등급의 뜻은 추적할 뿐 구현되지 않았다는 것이다. 정의상 담금 기록이 가장 적을 등급에 가장 긴 담금이 붙는다. - -## 테스트가 그 뒤틀림을 그대로 보여 준다 - -긴 임계값을 실행하는 테스트가 있다. 표시 이름은 둘째 등급이 되는 것이 첫째 등급이 되는 것보다 긴 담금을 요구한다고 말한다. - -실제로 고른 전이는 첫째 등급에서 철회로 가는 것이다. 그 전이가 긴 갈래에 떨어지는 이유는 둘째 등급이어서가 아니라 첫째 등급이 아니어서다. 이름이 가리키는 전이와 검사하는 전이가 다르다. - -추적 등급 테스트도 마찬가지다. 중간 등급으로 올라가는 쪽이 통과해야 하는데, 담금에 예순 날을 넣는다. 요구치의 두 배다. - -두 테스트를 돌리면 여덟 건이 전부 통과한다. 무엇을 통과시키는지는 단언 대상이 정하고, 여기서는 그 대상이 이름과 다르다. - -## 확인하지 못한 것 - -담금 값을 바꿔 가며 게이트를 직접 불러 보지는 않았다. 판정은 갈래 조건과 두 테스트가 고른 숫자에 근거하고, 위 실행은 저장소가 가진 테스트를 그대로 돌린 것이다. 이레를 넣었을 때 무엇이 실패하는지는 갈래 조건으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/case/case-a05-f020-inspect-claim.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/case/case-a05-f020-inspect-claim.md deleted file mode 100644 index 582d4a8..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/case/case-a05-f020-inspect-claim.md +++ /dev/null @@ -1,174 +0,0 @@ ---- -kind: CASE -slug: a05-f020-inspect-claim -title: 만료를 보는 청구와 시각을 읽지 않는 조회가 같은 행을 다르게 답한다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a05-f020-inspect-claim -evidenceCapturedOn: 2026-09-02 -assets: - - key: a05-f020-inspect-claim - file: ../../../final/evidence/rendered/a05-f020-inspect-claim.svg - - key: a05-f020-inspect-claim-postgres - file: ../../../final/evidence/rendered/a05-f020-inspect-claim-postgres.svg -evidence: - - ../../../final/evidence/raw/a05-f020-inspect-claim.txt - - ../../../final/evidence/raw/a05-f020-inspect-claim-postgres.txt -source: - - 원본 분석 절은 `final/document.md#a05` §59.1 이다. 두 경로가 만료된 완료 행을 다르게 해석한다는 판정과 그 실행 탐침 값이 그 절에 있다. 같은 §59 의 나머지 한 군데는 §59.2 이고 별도 사례가 담당한다. - - 소비자 세 자리의 분기별 동작과 시험 범위, 레디스 구현과의 대조는 이 기록에서 확인했다. ---- - -# 만료를 보는 청구와 시각을 읽지 않는 조회가 같은 행을 다르게 답한다 - -멱등성 저장소의 청구 경로는 행을 잠근 뒤 데이터베이스 시각을 읽어 재생 유효 기간이 지난 완료 행을 인계로 보낸다. 조회 경로는 그 시각을 한 번도 읽지 않고 완료 상태에 응답이 있으면 재생으로 답한다. 실제 PostgreSQL 에서 만료 뒤 같은 행에 두 답이 나온다. - -## 관계 - -- **만료된 claim과 만료된 실행은 다르게 다뤄야 한다** - 같은 상태 기계의 만료 처리 규칙이다. -- **시간은 DB에서, 그리고 행을 잠근 다음에 읽는다** - 두 경로가 같은 시각 기준을 써야 하는 이유다. -- **전이 다섯 중 넷은 공용 판정을 쓰고 완료만 손으로 비교한다** - 같은 분석 절이 짚은 나머지 한 군데 재생 경계다. - -## 문제 - -멱등성 저장소에는 두 진입 경로가 있다. 조회는 이 연산이 이미 처리됐는지 묻고, 청구는 지금 처리해도 되는지 묻는다. - -같은 행에 대한 두 답이 갈리면 어느 쪽을 믿을지 정하는 규칙이 코드에 없다. - -## 결론 - -청구가 데이터베이스 시각을 읽는 것은 행을 잠근 다음이고 한 번뿐이다. 그 시각으로 완료 상태이면서 재생 유효 기간이 지난 행을 먼저 걸러 인계로 보내고, 그 검사가 재생 응답 분기보다 앞에 있다. - -조회 쪽에는 그 호출이 없다. replayUntil 은 응답에 실려 나갈 뿐 어디에서도 읽히지 않는다. 만료를 비교하는 헬퍼가 하나 있고, 그것을 부르는 것은 청구뿐이다. 조회가 쓰는 조회 SQL 에도 시간 술어가 없다. - -실제 PostgreSQL 16 에 이 리프의 마이그레이션을 적용하고 재생 유효 기간 2초로 완료했다. 만료 시각 전 조회는 재생으로 답하고, 만료 뒤 조회도 같은 답과 같은 옛 응답을 준다. 같은 행을 다시 청구하면 인계가 돌아온다. - -멱등성 실행기는 조회의 재생 결과를 세 곳에서 받고, 셋 다 청구나 시작이나 완료가 불확정으로 끝난 뒤의 복구 경로다. 그중 둘은 저장된 응답을 그대로 돌려주고 행동을 실행하지 않는다. 그 둘에서 만료가 소비자에게 번진다. - -나머지 하나는 저장된 응답을 호출자가 방금 만든 결과와 비교하고 다르면 던진다. 그 경로는 호출자가 이미 실행한 뒤에만 닿고 돌려주는 값도 자기 결과이므로 이 문제가 번지지 않는다. - -재생 창 만료를 짚는 시험은 없다. 통합 시험에 이름이 만료인 시험이 둘 있지만 둘 다 처리 임차를 25밀리초로 몰아 만든 것이고, 그 파일의 요청 헬퍼는 재생 유효 기간을 언제나 24시간으로 고정한다. - -같은 계약의 레디스 구현에는 이 비교가 없다. 완료가 키에 재생 유효 기간을 그대로 만료로 걸어서, 창이 끝나면 해시가 사라지고 조회는 부재로 답한다. - -## 검증 환경 - -OpenJDK : 21.0.12 -데이터베이스 : PostgreSQL 16.15, 실제 실행 -확인 방식 : 두 경로의 시각 참조 계수, 실제 PostgreSQL 에 마이그레이션 적용 후 만료 전후 호출, 소비자 세 자리 추적 -소스 수정 : x - -## 재현 조건 - -1. 청구 경로에서 데이터베이스 시각을 읽는 줄과 만료 판정 헬퍼를 찾고, 그것이 재생 응답 분기보다 앞인지 본다. -2. 조회 경로 전문에서 데이터베이스 시각 호출을 세고, replayUntil 이 어디에 쓰이는지 본다. -3. 조회가 쓰는 조회 SQL 에 시간 술어가 있는지 본다. -4. 실제 PostgreSQL 에 이 리프의 마이그레이션을 적용하고, 짧은 재생 유효 기간으로 완료한 뒤 만료 전후로 조회와 청구를 부른다. -5. 조회의 재생 결과를 받는 세 자리가 각각 무엇을 하는지 읽는다. -6. 통합 시험의 만료 시험이 무엇을 만료시키는지, 요청 헬퍼의 재생 유효 기간이 무엇인지 본다. - -## 본문 - - - -청구는 행을 잠근 뒤 데이터베이스 시각을 한 번 읽는다. 조회는 그 호출이 0 이다. - -## 청구는 만료를 먼저 본다 - -:::evidence key="a05-f020-inspect-claim" alt="청구 경로가 데이터베이스 시각을 읽고 만료된 완료 행을 먼저 걸러 내는 구간과 그 판정 헬퍼, 청구 본문의 시각 참조 수, 조회 경로 전문과 그 본문의 시각 참조 수와 replayUntil 이 쓰이는 자리, 만료 비교 헬퍼를 부르는 곳, 조회가 쓰는 조회 SQL, 조회 결과를 받는 세 자리, 그리고 출하 통합 시험의 만료 시험이 무엇을 만료시키는지와 요청 헬퍼의 재생 유효 기간을 출력한 터미널 기록." caption="청구는 databaseNow 1회와 만료 선분기 · 조회는 시각 0회, replayUntil 은 생성자 인자로만 · 만료 비교 헬퍼는 청구만 호출 · 조회 SQL 에 시간 술어 없음 · 만료 시험 둘은 처리 임차, 재생 창은 늘 24시간 — 95줄 · exit 0" zoom="true" -::: - -```java -Instant dbNow = rows.databaseNow(); -... -if (isExpiredCompleted(row, dbNow)) { - return resetClaim(request, row); -} -... -if (row.state() == IdempotencyState.COMPLETED && row.replayUntil() != null) { - return new IdempotencyClaimOutcome.CompletedReplay( - new StoredResponse(row.responsePayload()), row.replayUntil()); -} -``` - -만료 판정이 재생 응답 분기보다 앞에 있다. - -## 조회는 만료된 완료 행도 재생으로 답한다 - -```java -if (row.state() == IdempotencyState.COMPLETED && row.responsePayload() != null) { - return new IdempotencyInspection( - IdempotencyInspectionOutcome.COMPLETED_REPLAY, - ... - Optional.ofNullable(row.replayUntil())); -} -``` - -`replayUntil` 은 응답 생성자에 한 번 실려 나갈 뿐 비교되지 않는다. 만료 비교를 하는 헬퍼는 이 파일에 하나뿐이고 청구만 부른다. 조회가 쓰는 조회 SQL 도 범위 해시와 레코드 버전만 술어로 쓴다. - -## 실제 PostgreSQL 에서 두 답이 갈린다 - -:::evidence key="a05-f020-inspect-claim-postgres" alt="실제 PostgreSQL 컨테이너를 세우고 이 리프의 마이그레이션 세 스트림을 적용해 만들어진 테이블 목록, 그리고 재생 유효 기간 2초로 완료한 뒤 저장된 만료 시각과 만료 전 조회 결과, 만료 뒤 조회 결과와 그때 돌아온 응답, 같은 행에 대한 청구 결과를 출력한 터미널 기록." caption="PostgreSQL 16.15 에 마이그레이션 적용 · 재생 창 2초로 완료 · 만료 전 조회는 재생 · 만료 뒤 조회도 재생과 옛 응답 · 같은 행의 청구는 인계 — 12줄 · exit 0" zoom="true" -::: - -```text -replay_until : 2026-09-02 04:35:51.753222+00 -지금 : 2026-09-02 04:35:49.768587+00 -만료 전 조회 : COMPLETED_REPLAY -지금 : 2026-09-02 04:35:53.285753+00 -만료 후 조회 : COMPLETED_REPLAY 응답={"v":"OLD-RESPONSE"} -만료 후 청구 : TakenOverClaimed -``` - -만료 시각을 지난 뒤에도 조회는 같은 답과 같은 옛 응답을 준다. 같은 행에 대한 청구는 인계를 돌려준다. - -## 조회 결과를 그대로 돌려주는 두 자리 - -멱등성 실행기는 조회의 재생 결과를 세 곳에서 받는다. 모두 청구나 시작이나 완료가 불확정으로 끝난 뒤의 복구 경로다. 그중 둘은 저장된 응답을 그대로 돌려준다. - -```java -case COMPLETED_REPLAY -> codec.deserialize(requireResponse(inspection).payload()); -``` - -두 경로 모두 행동을 실행하지 않는다. 그래서 재생 유효 기간이 지나 청구라면 인계했을 행에서도, 청구나 시작이 불확정으로 끝난 호출자는 만료된 이전 응답을 자기 답으로 받는다. - -세 번째는 다르다. - -```java -StoredResponse stored = requireResponse(inspection); -if (stored.payload().equals(codec.serialize(result))) { - yield result; -} -throw recovery("the completed response conflicts with the one this caller produced"); -``` - -이 경로는 호출자가 이미 행동을 실행한 뒤에만 닿고, 돌려주는 값도 저장된 응답이 아니라 호출자 자신의 결과다. 만료가 번지는 자리는 앞의 둘이다. - -## 재생 창 만료를 짚는 시험은 없다 - -통합 시험에 이름이 만료인 시험이 둘 있다. 만료된 청구를 인계하되 낡은 소유자는 시작하지 못한다는 것과, 만료된 실행 중 행은 조정을 요구한다는 것이다. - -둘 다 처리 임차를 25밀리초로 몰아 만든 것이고, 그 파일의 요청 헬퍼는 재생 유효 기간을 언제나 24시간으로 고정한다. 만료 판정 헬퍼가 참이 되는 분기는 이 파일의 어느 시험도 밟지 않는다. - -조회는 그 파일 전체에서 한 번 불리고, 포기 상태를 확인한다. - -## 같은 계약의 레디스 구현에는 이 비교가 없다 - -완료가 키에 재생 유효 기간을 그대로 만료로 건다. JPA 쪽이 완료 SQL 에서 `replay_until` 로 적는 값과 같은 값이다. 창이 끝나면 해시가 사라지고 조회는 부재로 답한다. - -시작과 갱신은 만료를 건드리지 않고, 실패 표시는 보존 기간을 건다. - -## 고칠 방향 - -조회도 청구와 같은 데이터베이스 시각 기준을 써야 한다. 그리고 재생 유효 기간이 지난 뒤 조회가 무엇을 답하는지 고정하는 경계 시험이 있어야 한다. - -## 확인하지 못한 것 - -두 답이 공존하는 창이 얼마나 지속되는지는 재지 않았다. 탐침에서 인계가 끝나면 조회는 다른 답으로 바뀌므로, 갈림은 먼저 도는 쪽이 상태를 바꿀 때까지다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/case/case-a05-f021-complete-replayttl.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/case/case-a05-f021-complete-replayttl.md deleted file mode 100644 index 4388148..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/case/case-a05-f021-complete-replayttl.md +++ /dev/null @@ -1,197 +0,0 @@ ---- -kind: CASE -slug: a05-f021-complete-replayttl -title: 전이 다섯 중 넷은 공용 판정을 쓰고 완료만 손으로 비교한다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a05-f021-complete-replayttl -evidenceCapturedOn: 2026-09-02 -assets: - - key: a05-f021-complete-replayttl-postgres - file: ../../../final/evidence/rendered/a05-f021-complete-replayttl-postgres.svg - - key: a05-f021-complete-replayttl - file: ../../../final/evidence/rendered/a05-f021-complete-replayttl.svg -evidence: - - ../../../final/evidence/raw/a05-f021-complete-replayttl-postgres.txt - - ../../../final/evidence/raw/a05-f021-complete-replayttl.txt -source: - - 원본 분석 절은 `final/document.md#a05` §59.2 다. 등급은 P2 이고, 만료 해석 불일치인 §59.1 은 형제 기록이 다룬다. - - 디지스트 정책 단위 시험이 이 값을 이미 고정한다는 것은 그 절이 짚는다. 레디스 구현의 같은 자리와 공용 헬퍼를 쓰는 전이 넷은 이 기록에서 확인했다. ---- - -# 전이 다섯 중 넷은 공용 판정을 쓰고 완료만 손으로 비교한다 - -이미 완료된 같은 연산의 재생 분기는 응답 다이제스트만 비교한다. 완료가 다이제스트에 넣어 열에 저장한 재생 창은 읽히지 않는다. 나머지 네 전이는 그 열을 비교하는 공용 헬퍼를 쓰고 다른 인자를 들고 온 재시도를 충돌로 돌려보낸다. - -## 관계 - -- **transition digest가 "누가·언제"만 덮고 "무엇"을 덮지 않아 다른 전이를 같다고 보고했다** - 같은 다이제스트가 무엇을 덮어야 하는지 다룬 사례다. -- **digest는 길이 프레이밍하고 버전을 붙인다** - 이 다이제스트의 형식 규칙이다. -- **만료를 보는 청구와 시각을 읽지 않는 조회가 같은 행을 다르게 답한다** - 같은 분석 절이 짚은 나머지 한 군데 재생 경계다. - -## 문제 - -완료가 정하는 것은 둘이다. 응답으로 무엇을 남길지, 그리고 그것을 언제까지 재생할지다. - -첫 완료는 둘 다 다이제스트에 넣는다. 두 번째 완료는 앞의 것만 본다. - -## 결론 - -실제 PostgreSQL 16 에서 같은 연산과 같은 응답에 1시간과 9시간을 차례로 넣었다. 두 번째는 이미 완료된 같은 결과로 답하고, 행에는 첫 1시간이 남고, 전이 다이제스트도 그대로다. - -값이 다르게 계산된다는 것은 이미 단위 시험이 고정하고 있다. 60000 과 90000 을 넣은 완료 다이제스트가 다르다는 시험이 같은 모듈에 있다. 값은 계산되고, 열에 저장되고, 시험으로 지켜진다. 그것을 읽지 않는 쪽이 재생 판정이다. - -나머지 네 전이는 다르게 한다. 시작과 갱신과 실패 표시와 해제가 공용 헬퍼에 자기 다이제스트를 넘기고, 그 헬퍼가 행의 전이 다이제스트와 비교한다. 완료는 그 헬퍼를 부르지 않는다. - -갱신 경로의 주석이 왜 그래야 하는지 적는다. 임대 유효 기간이 갱신이 결정한 것의 일부이므로 다이제스트에 들어가고, 그것이 없으면 다른 임대를 요청한 재시도가 이미 적용된 갱신으로 확인된다는 것이다. - -레디스 구현도 같은 자리에서 멈춘다. 완료 재생에서 응답 페이로드만 비교하고, 전이 스크립트는 이미 목표 상태이면 재생 창을 다시 걸기 전에 반환한다. 레디스에는 인자 비교라는 개념 자체가 없다. 한 구현의 누락이 아니라 포트가 정하지 않은 자리다. - -갈리는 조건은 좁다. 멱등성 실행기 쪽은 주입 시점의 재생 창을 끝까지 들고 간다. 창이 갈리는 조건은 둘이다. 설정 변경 뒤의 재시도이거나, 이 포트를 직접 부르는 별도 호출자다. 원본 분석이 이것을 만료 해석 불일치와 달리 P2 로 둔 자리도 거기다. - -고치려면 주의가 필요하다. 헬퍼는 어긋났다는 답만 주고, 응답 때문인지 창 때문인지는 말하지 않는다. 완료가 지금 돌려주는 응답 충돌을 그대로 두려면, 헬퍼의 어긋남 판정 뒤에 다이제스트 비교를 한 번 더 넣어 두 경우를 갈라야 한다. - -## 검증 환경 - -OpenJDK : 21.0.12 -데이터베이스 : PostgreSQL 16.15, 실제 실행 -확인 방식 : 다섯 전이의 재생 판정 경로 대조, 실제 PostgreSQL 에서 같은 응답에 두 재생 창으로 완료 호출, 형제 구현 대조 -소스 수정 : x - -## 재현 조건 - -1. 완료의 재생 분기와 첫 완료의 다이제스트 인자를 나란히 읽는다. -2. 같은 파일의 공용 재생 판정 헬퍼와 그것이 읽는 행 열, 그리고 그 열의 마이그레이션을 확인한다. -3. PostgreSQL 을 띄우고 청구와 시작을 거쳐 1시간으로 완료한 뒤, 같은 연산·같은 응답에 9시간으로 다시 완료한다. -4. 두 번째 결과와 행의 재생 창, 그리고 전이 다이제스트를 본다. - -## 본문 - - - -완료는 두 가지를 정한다. 무엇을 응답으로 남길지, 그리고 그 응답을 언제까지 재생할지다. - -## 같은 응답에 다른 창을 넣으면 - -:::evidence key="a05-f021-complete-replayttl-postgres" alt="실제 PostgreSQL 컨테이너를 세우고 이 리프의 마이그레이션 세 스트림을 적용한 뒤, 같은 연산과 같은 응답에 재생 창만 1시간과 9시간으로 바꿔 완료를 두 번 부르고 각 호출의 결과와 행에 저장된 재생 창, 그리고 전이 다이제스트가 그대로인지를 출력한 터미널 기록." caption="PostgreSQL 16.15 에 마이그레이션 적용 · 1시간으로 완료 뒤 저장 3600초 · 같은 응답에 9시간을 넣은 둘째 완료는 이미 완료된 같은 결과 · 저장은 3600초 그대로, 전이 다이제스트도 그대로 — 8줄 · exit 0" zoom="true" -::: - -```text -첫 완료 (재생 창 1시간) : COMPLETED -저장된 재생 창(초) : 3600 -둘째 완료 (재생 창 9시간) : ALREADY_COMPLETED_SAME_RESULT -저장된 재생 창(초) : 3600 -전이 다이제스트 그대로인가 : true -``` - -두 번째 호출은 다른 인자를 들고 왔는데 같은 결과로 확인됐다. - -## 두 번째 완료가 비교하는 것 - -:::evidence key="a05-f021-complete-replayttl" alt="이미 완료된 같은 연산의 재생 분기가 비교하는 값, 첫 완료가 전이 다이제스트에 넣는 인자와 그 위 주석, 같은 파일의 공용 재생 판정 헬퍼와 그것이 읽는 행 열과 그 열의 마이그레이션, 그 헬퍼를 쓰는 네 전이와 완료 본문에서의 호출 수, 재생 창이 다르면 완료 다이제스트가 다르다는 단위 시험, 갱신 경로의 주석, 출하 통합 시험이 덮는 인자 재생과 완료 호출 수, 그리고 레디스 구현의 완료 재생 분기와 전이 스크립트를 출력한 터미널 기록." caption="재생 분기는 응답 다이제스트만 비교 · 첫 완료는 재생 창을 다이제스트에 넣음 · 헬퍼는 행의 전이 다이제스트를 비교하고 그 열은 마이그레이션에 있음 · 그 헬퍼를 쓰는 전이 넷, 완료 0 · 창이 다르면 다이제스트가 다르다는 단위 시험 · 레디스도 응답만 비교 — 81줄 · exit 0" zoom="true" -::: - -```java -if (row.state() == IdempotencyState.COMPLETED - && "COMPLETE".equals(row.lastTransitionKind()) - && operationId.value().equals(row.lastTransitionOperationId())) { - return responseDigest.equals(row.responseDigest()) - ? IdempotencyCompleteOutcome.ALREADY_COMPLETED_SAME_RESULT - : IdempotencyCompleteOutcome.RESPONSE_CONFLICT; -} -``` - -## 첫 완료가 다이제스트에 넣는 것 - -```java -// The transition digest, not the response digest. Reusing the response digest here made -// two completions of different operations with identical payloads indistinguishable, -// and lost the replay window the completion also decided. -transitionDigest( - "COMPLETE", - operationId, - owner, - responseDigest, - Long.toString(replayTtl.toMillis())), -``` - -주석은 응답 다이제스트를 쓰던 때에 무엇을 잃었는지 과거형으로 적는다. 재생 창도 그 목록에 있다. - -## 값이 다르다는 것은 이미 시험이 고정한다 - -같은 모듈의 단위 시험에 완료 다이제스트가 재생 창에 따라 달라진다는 것을 고정하는 시험이 있다. - -```java -assertThat(IdempotencyDigestPolicy.of("COMPLETE", "op-1", "owner-1", 1, 3, "digest-a", "60000")) - .isNotEqualTo( - IdempotencyDigestPolicy.of("COMPLETE", "op-1", "owner-1", 1, 3, "digest-a", "90000")); -``` - -값은 계산되고, `last_transition_result_digest` 열에 저장되고, 시험으로 지켜진다. 재생 판정만 그것을 읽지 않는다. - -## 공용 헬퍼와 그것을 쓰는 네 전이 - -```java -if (!transitionKind.equals(row.lastTransitionKind()) - || !operationId.value().equals(row.lastTransitionOperationId())) { - return ReplayVerdict.NOT_A_REPLAY; -} -return expectedDigest.equals(row.lastTransitionResultDigest()) - ? ReplayVerdict.SAME_ARGUMENTS - : ReplayVerdict.DIFFERENT_ARGUMENTS; -``` - -```text -182: switch (replayVerdict(row, "START", operationId, startDigest)) { -221: switch (replayVerdict(row, "RENEW", operationId, renewDigest)) { -319: switch (replayVerdict(row, transitionKind, operationId, failDigest)) { -366: switch (replayVerdict(row, "RELEASE", operationId, releaseDigest)) { -``` - -완료 본문에서 그것을 부르는 줄은 0 이다. - -## 갱신 경로의 주석 - -```text -216: // The lease TTL is part of what a renewal decided, so it is part of the digest. Without it, a -217: // retry asking for a different lease was confirmed as the renewal already applied, and the -218: // caller went on believing it held the record for longer than the row says it does. -``` - -재생 창도 호출자가 나중에 읽는 지속 상태다. - -## 출하 통합 시험이 덮는 인자 재생 - -같은 연산 식별자에 다른 보존 기간이 오면 충돌이라는 시험, 다른 임대가 오면 충돌이라는 시험, 같은 인자면 확인이라는 시험이 있다. 완료를 두 번 부르는 시험은 그 파일에 없다. - -## 레디스 구현도 같은 자리에서 멈춘다 - -```java -case "ALREADY" -> - reply.payload().equals(response.payload()) - ? IdempotencyCompleteOutcome.ALREADY_COMPLETED_SAME_RESULT - : IdempotencyCompleteOutcome.RESPONSE_CONFLICT; -``` - -전이 스크립트는 이미 목표 상태이면 재생 창을 다시 걸기 전에 반환한다. 레디스에는 인자 비교라는 개념 자체가 없으므로, 이것은 한 구현의 누락이 아니라 포트가 정하지 않은 자리다. - -## 언제 갈리는가 - -애플리케이션의 멱등성 실행기는 주입받은 재생 창 하나를 계속 쓴다. 창이 달라지려면 설정이 바뀐 뒤 재시도가 넘어오거나, 이 포트를 직접 부르는 다른 호출자가 있어야 한다. - -## 고칠 방향 - -완료의 재생 분기도 완료 전이 다이제스트를 먼저 계산해 공용 헬퍼에 넘기면 창이 다른 호출을 걸러낼 수 있다. - -다만 그 헬퍼의 판정은 응답이 달라서 어긋난 경우와 창이 달라서 어긋난 경우를 구분하지 않는다. 지금 완료가 돌려주는 응답 충돌을 유지하려면, 헬퍼가 어긋났다고 답한 뒤 응답 다이제스트를 한 번 더 비교해 두 답을 나눠야 한다. - -## 확인하지 못한 것 - -첫 창이 남은 뒤 실제 재생 요청이 어떻게 처리되는지는 관측하지 않았다. 확인한 것은 두 번째 완료의 답과 행에 남은 값까지다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/case/case-a05-f025-filequotaservice-commit.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/case/case-a05-f025-filequotaservice-commit.md deleted file mode 100644 index edba1fb..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/case/case-a05-f025-filequotaservice-commit.md +++ /dev/null @@ -1,181 +0,0 @@ ---- -kind: CASE -slug: a05-f025-filequotaservice-commit -title: 만료 조건이 연장에는 있고 확정에는 없다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a05-f025-filequotaservice-commit -evidenceCapturedOn: 2026-09-02 -assets: - - key: a05-f025-filequotaservice-commit - file: ../../../final/evidence/rendered/a05-f025-filequotaservice-commit.svg - - key: a05-f025-filequotaservice-commit-postgres - file: ../../../final/evidence/rendered/a05-f025-filequotaservice-commit-postgres.svg -evidence: - - ../../../final/evidence/raw/a05-f025-filequotaservice-commit.txt - - ../../../final/evidence/raw/a05-f025-filequotaservice-commit-postgres.txt -source: - - 원본 분석 절은 `final/document.md#a05` §81 이다. 등급은 P2 이고 판정 문구는 프로덕션 API 계약 결함이다. 확정 질의에 만료 조건이 없다는 판정과 그 실행 탐침, 그리고 게이트웨이의 정산 경로가 수정 경계라는 지적이 그 절에 있다. - - 포트의 네 메서드 중 확정을 부르는 프로덕션 호출자가 0 이라는 것, 저장소 인터페이스 javadoc 의 두 절이 어긋난다는 것, 정산 행의 만료 시각이 생성 시각과 같다는 것은 이 기록에서 확인했다. ---- - -# 만료 조건이 연장에는 있고 확정에는 없다 - -저장소 인터페이스의 javadoc 은 연장과 확정과 해제가 모두 예약이 아직 살아 있기를 요구하므로 만료로 회수된 예약은 되살릴 수 없다고 적는다. 확정 질의에는 만료 조건이 없고, 실제 PostgreSQL 에서 만료된 예약을 확정하면 1행이 바뀐다. 다만 그 확정 메서드를 부르는 프로덕션 호출자는 없다. - -## 관계 - -- **조건부 update로 행을 claim하고 읽은 값으로 판단하지 않는다** - 읽은 값으로 판단하지 말고 조건부 갱신의 결과로 판단하라는 규칙이다. -- **만료된 claim과 만료된 실행은 다르게 다뤄야 한다** - 만료 처리를 갈라야 하는 이유다. -- **리스가 만료 시각만 기록하고 소유자를 기록하지 않아 최종 상태를 되돌릴 수 있었다** - 같은 리프 계열의 만료 처리 사례다. - -## 문제 - -JpaFileQuotaService 는 클래스 javadoc 에서 네 연산이 조건부 문장이므로 만료되거나 해제된 예약은 연장도 확정도 될 수 없다고 선언한다. - -저장소 인터페이스 javadoc 쪽은 여기서 더 나아간다. 연장과 확정과 해제가 예약이 기대한 버전에서 아직 예약됨 상태이기를 요구하므로 만료로 회수된 예약은 되살릴 수 없다는 것이다. - -## 결론 - -연장 질의에는 expiresAt > :now 가 있다. 확정 질의의 조건은 예약 식별자와 상태뿐이다. 만료와 해제 두 사유에 연장과 확정 두 연산을 곱한 네 조합 중 0행을 돌려주지 않는 것은 만료된 예약의 확정 하나다. - -저장소 javadoc 의 다른 절반도 어긋난다. 세 질의 어느 시그니처에도 버전 파라미터가 없다. - -만료된 사실이 어디에도 기록되지 않는다. 예약 상태 enum 이 만료됨을 선언해 두었는데 main 에서 그 값을 쓰지 않고, 낡은 예약을 정리 대상으로 삼는 코드도 없다. 만료된 예약은 계속 예약됨으로 남으므로 상태만 보는 질의는 둘을 구분할 방법이 없다. - -실제 PostgreSQL 16 에서 만료 시각이 한 시간 전인 예약을 만들고 두 질의를 돌렸다. 연장은 0행, 확정은 1행이다. 그 행은 확정됨이 되고 바이트가 기록된다. - -그 확정을 부르는 프로덕션 호출자는 없다. 포트의 네 메서드 중 main 코드가 부르는 것은 예약과 해제 둘뿐이다. 업로드 확정이 실제로 지나는 것은 별도 게이트웨이이고, 그 게이트웨이는 살아 있는 예약만 이 질의에 넘긴다. 그래서 결함은 포트 계약과 그 구현 쪽에 있다. - -수정에는 경계가 있다. 살아 있는 예약이 없으면 게이트웨이는 사용량을 새 행으로 만들고 바로 확정한다. 그 행의 만료 시각이 생성 시각이므로, 확정 질의에 expiresAt > :now 를 무조건 붙이면 등호 하나 차이로 이 행만 걸린다. 조회와 확정이 같은 시각을 쓰기 때문에 살아 있는 예약 쪽은 걸리지 않는다. - -걸렸을 때 나타나는 결과가 조용하다. 확정의 반환값을 게이트웨이가 받지 않아서, 행은 만들어지고 확정만 0행으로 끝난다. 남은 행은 만료된 예약됨이라 예약 합계에도 확정 합계에도 잡히지 않는다. - -## 검증 환경 - -OpenJDK : 21.0.12 -데이터베이스 : PostgreSQL 16.15, 실제 실행 -확인 방식 : 두 질의의 조건 대조, 포트 호출자 계수, 실제 PostgreSQL 에 만료된 예약과 정산 행을 만들어 질의 실행 -소스 수정 : x - -## 재현 조건 - -1. 서비스와 저장소 인터페이스의 javadoc 을 나란히 읽는다. -2. 연장 질의와 확정 질의의 조건, 그리고 세 질의의 시그니처를 확인한다. -3. 예약 상태 enum 의 만료됨을 쓰는 코드를 센다. -4. 포트의 네 메서드를 부르는 main 소스 호출자를 각각 센다. -5. 실제 PostgreSQL 에 파일서버 마이그레이션을 적용하고 만료 시각이 과거인 예약을 만든다. -6. 두 질의를 그 행에 돌려 바뀐 행 수와 최종 상태를 본다. -7. 게이트웨이가 살아 있는 예약이 없을 때 만드는 행에 만료 조건을 붙인 확정을 돌려 본다. - -## 본문 - - - -`JpaFileQuotaService` 는 클래스 javadoc 첫 문단에서 네 연산의 조건을 선언한다. - -```text -Reservation, extension, commit, and release are conditional statements, so a reservation -that already expired or was released can never be extended or committed. -``` - -## 연장에는 있고 확정에는 없는 조건 - -:::evidence key="a05-f025-filequotaservice-commit" alt="서비스와 저장소 인터페이스의 javadoc, 세 질의 시그니처의 버전 파라미터 수, 연장 질의와 확정 질의 전문, 예약 상태 enum 의 만료됨을 쓰는 코드와 낡은 예약을 정리 대상으로 넣는 코드 수, 포트의 네 메서드를 부르는 프로덕션 호출자 수와 실제 호출 두 줄, 프로덕션 확정이 지나는 게이트웨이와 그 조회 조건, 살아 있는 예약이 없을 때 만드는 행, 그리고 그 행을 만드는 생성자의 인자 순서를 출력한 터미널 기록." caption="두 javadoc 의 선언 · 세 질의에 버전 파라미터 0 · 연장에는 만료 조건, 확정에는 없음 · EXPIRED 를 쓰는 코드 0 · 포트 호출자는 예약과 해제뿐, 확정 0 · 게이트웨이는 살아 있는 예약만 넘김 — 93줄 · exit 0" zoom="true" -::: - -같은 리프의 저장소 인터페이스 javadoc 은 한 걸음 더 나간다. - -```text - *

Extend, commit, and release all require the reservation to still be {@code RESERVED} at the - * expected version, so a reservation reclaimed by expiry cannot be resurrected. -``` - -두 절이 다 어긋난다. 세 질의 어느 시그니처에도 버전 파라미터가 없고, 만료로 회수됐어야 할 예약을 되살리는 것이 바로 확정 질의다. - -연장 질의에는 만료 조건이 있다. - -```sql -where q.reservationId = :reservationId - and q.status = 'RESERVED' - and q.expiresAt > :now -``` - -확정 질의의 조건은 둘뿐이다. - -```sql -where q.reservationId = :reservationId - and q.status = 'RESERVED' -``` - -만료는 상태로 남지 않는다. 예약 상태 enum 에 `EXPIRED` 가 선언되어 있지만 그 값을 쓰는 main 코드가 없고, 낡은 예약을 정리 대상으로 넣는 코드도 없다. 만료된 예약은 계속 `RESERVED` 다. - -## 만료된 예약에서 0행과 1행이 갈린다 - -:::evidence key="a05-f025-filequotaservice-commit-postgres" alt="실제 PostgreSQL 컨테이너에 파일서버 마이그레이션을 적용해 쿼터 예약 테이블의 상태와 만료 열을 확인하고, 만료 시각이 한 시간 전인 예약에 저장소의 연장 질의와 확정 질의를 각각 돌려 바뀐 행 수와 최종 상태를 본 결과, 그리고 게이트웨이가 만드는 정산 행과 같은 모양의 행에 만료 조건을 붙인 확정을 돌린 결과를 출력한 터미널 기록." caption="PostgreSQL 16.15 에 파일서버 마이그레이션 적용 · 만료된 예약에 연장 0행, 확정 1행 · 결과는 COMMITTED 600 · 만료 시각이 생성 시각인 행에 조건을 붙이면 0행 — 11줄 · exit 0" zoom="true" -::: - -```text -만료된 예약을 하나 만든다 (expires_at = 한 시간 전) -연장 질의가 바꾼 행 : 0 -확정 질의가 바꾼 행 : 1 -결과 행 : status=COMMITTED committed_bytes=600 -``` - -저장소 질의만 놓고 보면 두 javadoc 이 금지한 전이가 그대로 일어난다. - -## 다만 그 질의에 만료된 행을 넘기는 호출자가 없다 - -포트의 네 메서드 중 main 소스가 부르는 것은 둘이다. - -```text -DefaultUploadApplicationService.java:128 quotaService.reserve(scope, reservationBytes, uploadPolicy.reservationTtl()); -DefaultUploadApplicationService.java:157 quotaService.release(created.reservation()); -``` - -확정과 연장은 0곳이다. 업로드 확정이 실제로 지나는 것은 `JpaQuotaCommitGateway` 이고, 그 게이트웨이는 살아 있는 예약을 먼저 조회한다. 그 조회에 만료 조건이 이미 들어 있다. - -```sql -and q.status = 'RESERVED' -and q.expiresAt > :now -``` - -그래서 이 결함은 포트 계약과 그 구현에 있고, 오늘의 업로드 경로에서 관측되는 사건은 아니다. - -## 정산 행의 만료 시각은 생성 시각이다 - -게이트웨이는 살아 있는 예약이 없으면 사용량을 새 행으로 만들어 곧바로 확정한다. 업로드가 유효 기간보다 오래 걸렸더라도 실제로 저장된 바이트를 적게 세지 않기 위한 경로다. - -```java -QuotaReservationEntity settled = - new QuotaReservationEntity( - UUID.randomUUID(), scope.type(), scope.value(), actualBytes, now, "RESERVED", now); -reservations.save(settled); -reservations.commit(settled.getReservationId(), actualBytes, now); -``` - -생성자의 다섯째 인자가 만료 시각이고 거기 들어간 값이 `now` 다. 저장과 확정이 같은 `now` 를 쓰므로 이 행은 `expiresAt > :now` 를 등호 하나 차이로 통과하지 못한다. - -같은 이유로 살아 있는 예약을 확정하는 쪽은 엄격한 조건을 붙여도 통과한다. 조회가 이미 같은 `now` 로 걸러 냈기 때문이다. 걸리는 것은 정산 행 하나다. - -```text -게이트웨이의 정산 행은 expires_at = now 로 만들어진다 -만료 조건을 붙인 확정이 그 행을 바꾼 수 : 0 -``` - -걸렸을 때 결과는 조용하다. 게이트웨이는 확정의 반환값을 받지 않으므로 행은 만들어지고 확정만 0행이 된다. 남은 행은 만료된 `RESERVED` 라서 예약 합계는 만료 조건에 걸려 세지 않고, 확정 합계는 상태가 달라 세지 않는다. 저장된 바이트가 장부 어디에도 잡히지 않는다. - -## 고칠 방향 - -`commit` 하나가 두 의미를 겸하고 있다. 저장소에 만료 조건을 건 확정 문과 걸지 않은 정산 문을 따로 두고, 포트도 확정과 정산으로 나눈다. 지금은 정산이 확정과 같은 문을 쓰기 때문에 조건 하나를 고치면 다른 쪽이 깨진다. - -## 확인하지 못한 것 - -정산 행이 예약됨으로 남았을 때 회수되는지는 확인하지 않았다. 낡은 예약을 정리 대상으로 넣는 코드가 없다는 것까지만 봤다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/case/case-a05-f027-maximum-attempts.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/case/case-a05-f027-maximum-attempts.md deleted file mode 100644 index ccfd339..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/case/case-a05-f027-maximum-attempts.md +++ /dev/null @@ -1,171 +0,0 @@ ---- -kind: CASE -slug: a05-f027-maximum-attempts -title: reclaimExpiredClaim이 MAXIMUM_ATTEMPTS를 보지 않는다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a05-f027-maximum-attempts -evidenceCapturedOn: 2026-09-02 -assets: - - key: a05-f027-maximum-attempts - file: ../../../final/evidence/rendered/a05-f027-maximum-attempts.svg - - key: a05-f027-maximum-attempts-reclaim - file: ../../../final/evidence/rendered/a05-f027-maximum-attempts-reclaim.svg -evidence: - - ../../../final/evidence/raw/a05-f027-maximum-attempts.txt - - ../../../final/evidence/raw/a05-f027-maximum-attempts-reclaim.txt -source: - - 원본 분석 절은 `final/document.md#a05` §82.1 이다. 등급은 P2 이고, 회수 문장에 종료 조건이 없다는 판정과 리스 만료를 최대값보다 많이 반복한 탐침, 그리고 회수가 배치 시작에 먼저 불린다는 관찰이 그 절에 있다. - - 회수 질의 javadoc 의 원문, 저장소가 `attempt` 를 한 번도 비교하지 않는다는 것, 회수 래퍼가 시계를 다시 읽어 같은 배치의 재청구를 막는다는 것은 이 기록에서 덧붙였다. ---- - -# reclaimExpiredClaim이 MAXIMUM_ATTEMPTS를 보지 않는다 - -회수 질의의 javadoc 은 매번 죽는 작업자의 항목도 정상 실패와 같은 재시도 예산에 묶이며 영원히 회수되지는 않는다고 적는다. 다섯 줄 아래 질의는 시도를 올리기만 하고 그 예산을 걸지 않는다. 실제 PostgreSQL 에서 아홉 번 반복하면 시도가 아홉이 되고 상태는 여전히 청구 가능한 실패다. - -## 관계 - -- **만료된 claim과 만료된 실행은 다르게 다뤄야 한다** - 만료 처리를 갈라야 하는 이유다. -- **fenced lease — 만료 시각만으로는 부족한 이유** - 만료 시각만으로는 회수한 항목의 소유자를 가릴 수 없다고 적은 문서다. -- **재시도 가능성은 멱등성과 실패 범주를 함께 봐야 정해진다** - 재시도 예산이 어디서 강제되는지의 문제다. - -## 문제 - -큐의 계약은 클래스 javadoc 에 있다. 계속 실패하는 항목은 영원히 재시도되는 대신 결국 포기된다. - -그 계약이 크래시 경로까지 덮는다는 것은 회수 질의 javadoc 이 명시한다. 항목은 대기 중이 아니라 실패로 돌아오고 시도 계수가 오르므로, 매번 죽는 작업자의 항목도 곧바로 실패하는 항목과 같은 예산에 묶이며 영원히 회수되지는 않는다는 것이다. - -## 결론 - -그 문장 다섯 줄 아래 질의에는 예산이 없다. 저장소가 attempt 에 하는 일은 두 자리에서 하나씩 올리는 것뿐이고, 한 번도 비교하지 않는다. - -예산을 끊는 코드가 있는 곳은 한 군데다. 정상 실패 경로가 다음 시도를 여덟과 비교해 포기 상태로 넘긴다. - -회수 대상 선정 질의에도 시도 한계 조건이 빠져 있다. 그 조회가 고르는 것은 리스가 만료된 진행 중 행이다. - -실제 PostgreSQL 16 에서 청구 문장과 회수 문장을 아홉 번 반복했다. 회차마다 시도가 하나씩 올라 아홉이 되고, 상태는 매번 실패다. - -되풀이의 속도는 느리다. 리스가 10분이라 청구된 항목은 그동안 처리 대상 조회에 보이지 않고, 회수된 뒤에도 곧바로 돌아오지 않는다. 배치는 시각을 한 번 잡아 회수와 청구에 같이 쓰는데, 회수 래퍼는 그 시각 대신 시계를 다시 읽어 다음 시도 시각에 넣는다. 처리 대상 조회가 그 시각을 넘지 않은 행만 고르므로 회수된 항목은 다음 배치로 넘어간다. - -굶주림이 아니라 종료가 없다는 것이 문제다. 시도가 아홉이 되고 열이 되어도 종료 상태로 가지 않는다. - -되풀이가 유지되려면 작업자가 매번 예외를 던지지 않고 죽어야 한다. 예외를 던지면 정리 서비스가 그것을 잡아 정상 실패 경로로 보내고, 시도가 이미 여덟을 넘었으므로 그 한 번에 포기로 끝난다. - -## 검증 환경 - -OpenJDK : 21.0.12 -데이터베이스 : PostgreSQL 16.15, 실제 실행 -확인 방식 : 저장소가 attempt 에 하는 일 전수 확인, 예산 전환 지점 계수, 실제 PostgreSQL 에서 청구와 회수 반복 -소스 수정 : x - -파일서버 플랫폼 스위치와 정리 스위치가 모두 참인 배포에서 프로덕션 경로다. 고정 지연 스케줄러가 배치를 돌리고, 정리 서비스의 세 지점이 정상 실패 경로를 실제로 탄다. 두 스위치의 출하 기본값은 거짓이다. - -## 재현 조건 - -1. 회수 질의의 javadoc 과 그 아래 질의를 나란히 읽는다. -2. 저장소 전체에서 attempt 가 나오는 줄을 전부 뽑는다. 올리는 두 자리와 javadoc 뿐이다. -3. 정리 항목 행에 포기 상태를 쓰는 코드를 코드베이스에서 센다. -4. 회수 대상을 고르는 조회와 다시 청구되는 조회의 조건을 확인한다. -5. 회수 래퍼가 다음 시도 시각에 넣는 값이 배치가 잡아 둔 시각인지 확인한다. -6. 실제 PostgreSQL 에 마이그레이션을 적용하고 청구와 회수를 아홉 번 반복해 시도와 상태를 본다. - -## 본문 - - - -정리 큐가 스스로 적어 둔 계약은 계속 실패하는 항목이 영원히 재시도되는 대신 결국 포기된다는 것이다. - -회수 질의의 javadoc 은 그 계약이 크래시 경로에도 적용된다고 못박는다. - -```text - *

The item comes back as FAILED rather than PENDING, and its attempt counter advances. An item - * whose worker dies every time is then bounded by the same retry budget as one that fails - * outright, instead of being reclaimed forever. -``` - -## 그 아래 다섯 줄에 예산이 없다 - -:::evidence key="a05-f027-maximum-attempts" alt="회수 질의의 javadoc 과 질의 전문, 저장소 전체에서 attempt 가 나오는 줄, 정리 항목에 포기 상태를 쓰는 코드와 정상 실패 경로의 비교, 회수 대상을 고르는 조회와 다시 청구되는 조회의 조건, 회수 래퍼가 다음 시도 시각에 넣는 값과 배치가 시각을 한 번 잡는 구간과 리스 길이, 정상 실패 경로가 불리는 지점, 그리고 스케줄러 배선과 두 스위치의 출하 기본값을 출력한 터미널 기록." caption="회수 javadoc 은 같은 예산에 묶인다고 적음 · 저장소는 attempt 를 두 자리에서 올리기만 함 · ABANDONED 전환은 markFailed 한 곳 · 회수 대상 조회에도 한계 없음 · 회수 래퍼는 시계를 다시 읽음 · 리스 10분 · 두 스위치 기본값 false — 113줄 · exit 0" zoom="true" -::: - -저장소가 `attempt` 에 하는 일은 두 자리에서 하나씩 올리는 것뿐이다. - -```text -68: c.attempt = c.attempt + 1, ← 정상 실패 정산 -121: c.attempt = c.attempt + 1, ← 크래시 회수 -``` - -한 번도 비교하지 않는다. 예산을 끊는 코드는 코드베이스에 한 군데다. - -```java -boolean exhausted = item.attempt() + 1 >= MAXIMUM_ATTEMPTS; -``` - -회수 대상을 고르는 조회에도 시도 한계가 없다. 그 조회는 리스가 만료된 진행 중 행만 고른다. - -## 실제 PostgreSQL 에서 아홉 회차 - -:::evidence key="a05-f027-maximum-attempts-reclaim" alt="실제 PostgreSQL 컨테이너에 마이그레이션을 적용한 뒤 저장소의 청구 문장과 회수 갱신 문장을 아홉 번 반복하며 회차마다 시도 횟수와 상태와 마지막 오류 코드를 출력한 터미널 기록. 회수 대상을 고르는 조회는 실행하지 않았고 시계 출처만 데이터베이스로 바꿨다는 단서가 함께 적혀 있다." caption="PostgreSQL 16.15 · 최대 시도 상수 8 · 청구와 회수 갱신을 9회 반복 · 시도는 1부터 9까지 오르고 상태는 매번 FAILED · 회수 대상 조회는 태우지 않음 — 15줄 · exit 0" zoom="true" -::: - -```text -최대 시도 횟수 상수 : 8 -... -9 회차 후: 시도 9 상태 FAILED 마지막 오류 CLAIM_LEASE_EXPIRED -``` - -## 되풀이는 느리다. 다만 끝나지 않는다 - -되돌아간 실패는 청구가 다시 받는 상태다. 다만 관문이 하나 더 있다. - -```sql -where c.status in ('PENDING', 'FAILED') - and c.nextAttemptAt <= :now -order by c.nextAttemptAt asc -``` - -배치는 시각을 한 번 잡아 회수와 청구에 같이 쓴다. 그런데 회수 래퍼는 그 시각 대신 시계를 다시 읽어 넘긴다. - -```java -reclaimed += - items.reclaimExpiredClaim( - abandoned.getCleanupId(), abandoned.getClaimToken(), clock.instant()); -``` - -그래서 회수된 항목의 다음 시도 시각은 배치가 잡아 둔 시각보다 뒤이고, 그 배치의 청구 조회에서 탈락한다. 서비스 주석은 회수를 먼저 도는 이유로 회수된 항목이 같은 배치에서 곧바로 대상이 된다는 것을 들지만, 실제로는 다음 배치에 가서야 대상이 된다. - -리스도 10분이다. 청구된 항목은 그동안 처리 대상 조회에 보이지 않는다. 그리고 회수 경로에는 백오프가 없다. 다음 시도 시각을 회수 시각으로 그냥 되돌린다. 주기를 정하는 것은 백오프가 아니라 리스다. - -javadoc 이 일어나지 않게 하겠다고 적은 상황은 성공하지 못하는 항목이 매 배치의 자리를 차지하는 것이다. 여기서 일어나는 것은 그보다 느리다. 문제는 굶주림이 아니라 끝나지 않는 것이다. - -## 되풀이가 유지되는 조건 - -작업자가 매번 예외를 던지지 않고 죽어야 한다. 예외를 던지면 정리 서비스가 그것을 잡아 정상 실패 경로로 보낸다. - -```java -} catch (RuntimeException failure) { - markFailed(item, "CLEANUP_ATTEMPT_FAILED", now); -``` - -시도가 이미 여덟을 넘었으므로 그 한 번에 포기로 끝난다. 예산 우회가 이어지려면 작업자가 조용히 사라져야 한다. - -## 이 경로는 배선되어 있다 - -스케줄러가 고정 지연으로 배치를 부르고, 정상 실패 경로도 정리 서비스의 세 지점에서 실제로 불린다. 파일서버 플랫폼 스위치와 정리 스위치의 출하 기본값은 둘 다 거짓이므로, 둘 다 켠 배포에서 프로덕션 경로다. - -## 고칠 방향 - -두 경로가 같은 예산을 봐야 한다. 회수 문장이 증가 후 값을 검사해 한계에서 포기로 넘기는 것이 가장 작은 변경이고, 저장소가 다음 상태를 호출자에게서 받는 쪽이 더 곧다. 그 경우 비교 교환이 토큰과 시도를 함께 봐야 회수와 정산이 같은 행을 두고 엇갈리지 않는다. - -회귀는 두 경로를 섞어도 총합이 예산을 넘으면 반드시 포기로 끝나는지를 고정해야 한다. - -## 확인하지 못한 것 - -실제 작업자 크래시로 재현하지 않았다. 탐침은 저장소의 세 질의 중 청구와 회수 갱신 둘만 네이티브 SQL 로 옮겨 반복했고, 회수 대상을 고르는 조회는 실행하지 않았다. 그 조회에도 시도 한계가 없다는 것은 질의를 읽어 확인했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-inbound-graphql-c12.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-inbound-graphql-c12.md deleted file mode 100644 index 99d6c62..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-inbound-graphql-c12.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-graphql-c12 -title: 기계가 읽는 매니페스트와 사람이 읽는 등급표가 다르게 답한다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-graphql-c12 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-graphql-c12 - file: ../../../final/evidence/rendered/adapter-inbound-graphql-c12.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-graphql-c12.txt -source: - - 원본 분석 절은 final/document.md#a16#L867 이다. -module: adapter-inbound-graphql ---- - -# 기계가 읽는 매니페스트와 사람이 읽는 등급표가 다르게 답한다 - -`GraphQlStableCapabilityManifest.STABLE`에 `SIGNED_CURSOR_CONNECTION`이 들어 있는데 `CLAUDE.md` 등급표는 cursor 서명을 `modelled`(요청 경로에 없음)로 매긴다. - -## 본문 - - - -`GraphQlStableCapabilityManifest.STABLE`에 `SIGNED_CURSOR_CONNECTION`이 들어 있다. `CLAUDE.md` 등급표는 cursor 서명을 `modelled`(요청 경로에 없음)로 매긴다. - -## GraphQlStableCapabilityManifest 참조 위치 - -:::evidence key="adapter-inbound-graphql-c12" alt="코드베이스에서 GraphQlStableCapabilityManifest 를 검색한 출력 11줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GraphQlStableCapabilityManifest 코드베이스 검색 — 11줄 · exit 0" zoom="true" -::: - -## 용도가 달라도 답이 갈리는 것은 남는다 - -두 목록의 용도가 다르다 — 매니페스트는 `requireStable(capability)`로 **릴리스 게이트가 소비하는 기계 판정**이고, 등급표는 사람이 읽는 공시다. 그러나 같은 능력에 대해 하나는 "Stable에서 지원"이라 하고 하나는 "요청 경로에 없음"이라 한다. §29.2. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-inbound-graphql-c13.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-inbound-graphql-c13.md deleted file mode 100644 index f961829..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-inbound-graphql-c13.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-graphql-c13 -title: 릴리스 게이트의 미배선은 정상이고 작성기의 호출자 부재는 다르다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-graphql-c13 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-graphql-c13 - file: ../../../final/evidence/rendered/adapter-inbound-graphql-c13.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-graphql-c13.txt -source: - - 원본 분석 절은 final/document.md#a16#L873 이다. -module: adapter-inbound-graphql ---- - -# 릴리스 게이트의 미배선은 정상이고 작성기의 호출자 부재는 다르다 - -`release` 9개 파일 전부 autoconf=0이다. 릴리스 게이트는 빌드·릴리스 시점 도구이므로 런타임 미배선이 정상이다. - -## 본문 - - - -`release` 9개 파일 전부 autoconf=0이다. 릴리스 게이트는 런타임 컴포넌트가 아니라 빌드·릴리스 시점 도구이므로 정상이다. - -## GraphQlReleaseReportWriter 참조 위치 - -:::evidence key="adapter-inbound-graphql-c13" alt="코드베이스에서 GraphQlReleaseReportWriter 를 검색한 출력 4줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GraphQlReleaseReportWriter 코드베이스 검색 — 4줄 · exit 0" zoom="true" -::: - -## 작성기에 호출자가 없다는 것은 별개 사실이다 - -`GraphQlReleaseReportWriter`(57)는 main_other=0 · test=1로, 게이트 결과를 기록할 작성기에 호출자가 없다. CLAUDE.md가 그 상태를 명시한다 — "`graphqlPerformanceTest` 레인이 자리를 예약, **증거 없으면 릴리스 게이트가 거부**". 즉 게이트는 CI 레인에서 호출되도록 설계됐다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-inbound-web-c09.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-inbound-web-c09.md deleted file mode 100644 index 854d891..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-inbound-web-c09.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-web-c09 -title: 캐시 헤더를 소유하는 것은 24줄 상수 두 개다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-web-c09 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-web-c09 - file: ../../../final/evidence/rendered/adapter-inbound-web-c09.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-web-c09.txt -source: - - 원본 분석 절은 final/document.md#a14#L864 이다. -module: adapter-inbound-web ---- - -# 캐시 헤더를 소유하는 것은 24줄 상수 두 개다 - -배선된 `CacheControlFilter`는 `@Component`라 컴포넌트 스캔이 잡고 모든 응답에 `no-store`를 붙인다. 프로파일·지시자·`Vary` 규칙을 갖춘 `cache` 패키지 4파일 310 LOC은 배선되지 않았다. - -## 본문 - - - -배선된 쪽은 `CacheControlFilter`다. `@Component`이므로 컴포넌트 스캔이 잡고 Spring이 `Filter` 빈을 체인에 넣는다. **모든 응답에 `no-store`를 붙인다.** 배선되지 않은 것은 `cache` 패키지 4개 파일 310 LOC이고, `web.cache.` 패키지를 참조하는 파일이 자기 패키지 밖에 **0개**다. - -## 분석 원문의 두 벌 비교 - -:::evidence key="adapter-inbound-web-c09" alt="분석 문서 final/document.md#a14 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a14 발췌 — 15줄" zoom="true" -::: - -## 소유자를 지목하는 주석과 실제 소유자 - -`SecurityConfig:80`이 Spring Security의 기본 캐시 헤더 작성기를 끄면서 그 이유를 적는다 — "`CacheControlFilter` **owns the cache header policy**". 소유자는 24줄짜리 상수 두 개이고, 프로파일·지시자·`Vary` 규칙을 갖춘 310줄은 소유하지 않는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-inbound-web-c16.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-inbound-web-c16.md deleted file mode 100644 index ddf8152..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-inbound-web-c16.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-web-c16 -title: 선언된 능력 열하나 중 켤 수 있는 것은 둘이다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-web-c16 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-web-c16 - file: ../../../final/evidence/rendered/adapter-inbound-web-c16.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-web-c16.txt -source: - - 원본 분석 절은 final/document.md#a14#L1217 이다. -module: adapter-inbound-web ---- - -# 선언된 능력 열하나 중 켤 수 있는 것은 둘이다 - -`advanced/**` 전체에서 프로덕션 `@Configuration`은 셋이고 실제 `@ConditionalOnProperty` 접두사는 둘이다. 나머지 아홉에는 프로퍼티도, `@Configuration`도, 빈도 없다. - -## 본문 - - - -`advanced/**` 전체에서 프로덕션 `@Configuration`은 셋이고(`MvcStreamingExecutorConfiguration` · `VirtualThreadMvcConfiguration` · `VirtualThreadSettings`) 실제 `@ConditionalOnProperty` 접두사는 둘이다. - -## WebAdvancedFeature 참조 위치 - -:::evidence key="adapter-inbound-web-c16" alt="코드베이스에서 WebAdvancedFeature 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="WebAdvancedFeature 코드베이스 검색 — 5줄 · exit 0" zoom="true" -::: - -## 프로퍼티도 Configuration도 빈도 없는 아홉 - -`WEBFLUX_BLOCKING_BRIDGE` · `JSON_MERGE_PATCH` · `JSON_PATCH` · `SSE` · `JSON_SEQUENCE` · `FUNCTIONAL_WEBFLUX` · `CBOR` · `XML` · `RATELIMIT_DRAFT_HEADERS`. §36.1. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-inbound-websocket-c05.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-inbound-websocket-c05.md deleted file mode 100644 index 41345ab..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-inbound-websocket-c05.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-websocket-c05 -title: 릴리스 시점 도구라서 런타임 미배선이 정상이다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-websocket-c05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-websocket-c05 - file: ../../../final/evidence/rendered/adapter-inbound-websocket-c05.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-websocket-c05.txt -source: - - 원본 분석 절은 final/document.md#a17#L420 이다. -module: adapter-inbound-websocket ---- - -# 릴리스 시점 도구라서 런타임 미배선이 정상이다 - -`advanced/release/AdvancedPromotionGate`(120)와 `release/WebSocketStableReleaseGate`(106)가 각각 Advanced 승격과 Stable 릴리스를 판정한다. 둘 다 main 참조 0이고 테스트만 있다. - -## 본문 - - - -`advanced/release/AdvancedPromotionGate`(120)와 `release/WebSocketStableReleaseGate`(106, §11)가 각각 Advanced 승격과 Stable 릴리스를 판정한다. 둘 다 main 참조 0이고 테스트만 있다 — 릴리스 시점 도구이므로 런타임 미배선이 정상이다. - -## 분석 원문의 판정 - -:::evidence key="adapter-inbound-websocket-c05" alt="분석 문서 final/document.md#a17 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a17 발췌 — 15줄" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-cache-redis-c08.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-cache-redis-c08.md deleted file mode 100644 index 846d3e8..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-cache-redis-c08.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-cache-redis-c08 -title: 한쪽에만 적용되는 변경이 구조적으로 불가능하다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-cache-redis-c08 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-cache-redis-c08 - file: ../../../final/evidence/rendered/adapter-outbound-cache-redis-c08.svg - - key: adapter-outbound-cache-redis-c08-diagram - file: ../../../final/assets/diagrams/adapter-outbound-cache-redis-c08.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-cache-redis-c08.txt -source: - - 원본 분석 절은 final/document.md#a10#L456 이다. -module: adapter-outbound-cache-redis ---- - -# 한쪽에만 적용되는 변경이 구조적으로 불가능하다 - -`ValueOperationRequests`의 javadoc이 불변식을 적고, 두 구현이 같은 builder를 생성자에서 만들면서 그것을 구조로 보장한다. - -## 본문 - - - -`ValueOperationRequests`의 javadoc이 불변식을 적는다 — "Both the blocking and the reactive string operations call exactly these methods, so **a change to a permit, a budget, an encoding, or a command choice cannot apply to one API and not the other.**" - -## 두 모델이 공유하는 빌더 - -:::evidence key="adapter-outbound-cache-redis-c08-diagram" alt="공유 request builder 에서 동기 표면과 반응형 표면으로 각각 화살표가 나가고 화살표에 같은 요청 객체가 붙은 구조" caption="두 모델이 공유하는 빌더" zoom="false" -::: - -구조가 그것을 보장한다. `LettuceRedisValueOperations`와 `LettuceReactiveRedisValueOperations`는 둘 다 생성자에서 `new ValueOperationRequests(gateway, context, counters)`를 만들고, 차이는 `SyncRedisCommandExecutor` vs `ReactiveRedisCommandExecutor` 하나뿐이다. - -## ValueOperationRequests 참조 위치 - -:::evidence key="adapter-outbound-cache-redis-c08" alt="코드베이스에서 ValueOperationRequests 를 검색한 출력 10줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ValueOperationRequests 코드베이스 검색 — 10줄 · exit 0" zoom="true" -::: - -## 열한 계열 전부에서 확인했다 - -각 메서드는 `executor.execute(requests.xxx(...))` 한 줄이고, reactive 쪽은 그 위에 `flatMap`/`then` 같은 형태 변환만 얹는다. Value·Hash·List·Set·SortedSet·Key·Geo·Bitmap·Stream·HyperLogLog·PubSub 모두 sync와 reactive 양쪽이 같은 `*OperationRequests`를 생성한다(`162-...` §8.2). - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-cache-redis-c10.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-cache-redis-c10.md deleted file mode 100644 index 6a18844..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-cache-redis-c10.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-cache-redis-c10 -title: 뒤 단계일수록 비싸도록 검증 순서를 고정했다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-cache-redis-c10 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-cache-redis-c10 - file: ../../../final/evidence/rendered/adapter-outbound-cache-redis-c10.svg - - key: adapter-outbound-cache-redis-c10-diagram - file: ../../../final/assets/diagrams/adapter-outbound-cache-redis-c10.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-cache-redis-c10.txt -source: - - 원본 분석 절은 final/document.md#a10#L560 이다. -module: adapter-outbound-cache-redis ---- - -# 뒤 단계일수록 비싸도록 검증 순서를 고정했다 - -`CommandPolicyGuard`의 javadoc이 순서와 이유를 적는다 — 각 단계가 다음 단계보다 싸므로 명백히 부적격한 명령은 인코딩도 전송도 하기 전에 거절된다. - -## 본문 - - - -javadoc이 순서와 그 이유를 적는다 — "Validation order is fixed and **each step is cheaper than the one after it**, so an obviously inadmissible command is refused before anything is encoded or sent." - -```text -capability → risk/permit provenance → namespace → slot → request budget - → connection lane → timeout/retry → invocation → reply budget → translation → telemetry -``` - -## 고정된 검증 순서 - -:::evidence key="adapter-outbound-cache-redis-c10-diagram" alt="capability 와 위험 등급과 namespace 와 slot 과 요청 예산이 왼쪽에서 오른쪽으로 이어지는 구조" caption="고정된 검증 순서" zoom="false" -::: - -각 단계가 구체적이다. `requireReachable`은 `BLOCKED`거나 `access == NONE`이면 거부하고 R3/R4를 애플리케이션 경로에서 배제한다. `requireCapability`는 명령의 최소 버전을 프로브된 서버 버전과 대조한다. `requireNamespace`는 모든 키의 네임스페이스를 확인하고 렌더까지 수행한다. `requireSameSlot`은 Cluster에서 두 개 이상 슬롯이면 `RedisCrossSlotException`을 **서버를 부르기 전에** 던진다. `effectiveTimeout`은 블로킹 명령이 유한한 server block을 선언하지 않으면 거부하고, 설정 상한을 넘으면 거부하며, 통과하면 `BLOCKING_MARGIN`(2초)을 더한다. - -## CommandPolicyGuard 참조 위치 - -:::evidence key="adapter-outbound-cache-redis-c10" alt="코드베이스에서 CommandPolicyGuard 를 검색한 출력 26줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="CommandPolicyGuard 코드베이스 검색 — 26줄 · exit 0" zoom="true" -::: - -## 죽은 중복을 지운 기록 - -`validateReply(...)`가 있었고 아무도 부르지 않았다. - -> "Two mechanisms for one rule, with the more visible one dead, is worse than one: a reader finds the guard's method, assumes replies are bounded during admission, and writes an operation that never bounds its own. **Admission cannot do this job anyway.** The guard runs before the command is sent, so the only reply size available to it is the estimate the request declared. The authority has to sit where the bytes actually arrive." - -## 발화하지 못하던 조건을 떼어낸 기록 - -다중 키 permit 검사가 advanced permit 검사와 한 조건으로 접혀 있었고, "둘 다 없음"이 위에서 이미 던지므로 다중 키 절은 도달 불가였다 — "set algebra over any number of keys was admitted on an advanced permit alone." 지금은 `request.keys().size() > 1 && request.multiKeyPermit().isEmpty()`가 독립 조건이다. test `rejectsAMultiKeyCommandCarryingOnlyAnAdvancedPermit`과 `aSingleKeyAdvancedCommandStillNeedsNoMultiKeyPermit`가 양쪽을 고정한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-fileserver-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-fileserver-c03.md deleted file mode 100644 index 84932f8..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-fileserver-c03.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-fileserver-c03 -title: stateRevision은 정확히 하나씩만 증가한다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-fileserver-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-fileserver-c03 - file: ../../../final/evidence/rendered/adapter-outbound-fileserver-c03.svg - - key: adapter-outbound-fileserver-c03-diagram - file: ../../../final/assets/diagrams/adapter-outbound-fileserver-c03.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-fileserver-c03.txt -source: - - 원본 분석 절은 final/document.md#a08#L184 이다. -module: adapter-outbound-fileserver ---- - -# stateRevision은 정확히 하나씩만 증가한다 - -`validateOperationTransition`이 여섯 가지를 순서대로 강제하고, 두 종착 상태에는 후속 전이가 없다. - -## 본문 - - - -`validateOperationTransition`이 여섯 가지를 순서대로 강제한다: requestFingerprint 불변 → 불변 identity 10개 필드 불변 → `stateRevision` 감소 금지 → 동일 revision 다른 내용 금지 → **정확히 +1** 증가 → 인접 전이 행렬. - -## 두 종착 상태 - -:::evidence key="adapter-outbound-fileserver-c03-diagram" alt="비terminal 상태에서 PUBLISHED 로 가는 실선 화살표와 QUARANTINED 로 가는 점선 화살표가 있고 두 종착 상태에서 나가는 화살표는 없는 구조" caption="두 종착 상태" zoom="false" -::: - -행렬은 README가 적은 사슬과 일치하고, 모든 비terminal 상태에서 `QUARANTINED`로만 이탈할 수 있으며 `PUBLISHED`·`QUARANTINED`는 후속 전이가 없다(`case PUBLISHED, QUARANTINED -> false`). - -## 분석 원문의 전이 검사 - -:::evidence key="adapter-outbound-fileserver-c03" alt="분석 문서 final/document.md#a08 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a08 발췌 — 15줄" zoom="true" -::: - -## 봉인 이후 얼어붙는 사실 - -`requireSealedFactsUnchanged`가 byteSize·rowCount·columnCount·sha256·formulaMitigatedCount·sealedAt을 고정하고, `MANIFEST_PUBLISHED` 이후에는 `manifestDigest`, `REFERENCE_PUBLISHED` 이후에는 `referenceDigest`도 고정된다. - -## 같은 레코드 재기록을 전이가 아니라 복구로 본다 - -`current.equals(candidate)`는 전이가 아니라 **복구(repair)**로 취급된다 — 부모 디렉터리를 다시 force하고 정확 read-back만 수행한다. 크래시 후 같은 레코드를 다시 쓰는 재시도가 conflict가 되지 않게 하는 처리이고, `parentForcedCallbackFailureIsRepairedByIdenticalOperationRetry`가 이를 고정한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-fileserver-c04.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-fileserver-c04.md deleted file mode 100644 index 4713db9..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-fileserver-c04.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-fileserver-c04 -title: 배타성이 필요한 쪽에만 OS 락을 둔다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-fileserver-c04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-fileserver-c04 - file: ../../../final/evidence/rendered/adapter-outbound-fileserver-c04.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-fileserver-c04.txt -source: - - 원본 분석 절은 final/document.md#a08#L192 이다. -module: adapter-outbound-fileserver ---- - -# 배타성이 필요한 쪽에만 OS 락을 둔다 - -한 클래스 안에 락이 두 종류다. 얼핏 비대칭으로 보이지만 각자의 커밋 방식이 다르다. - -## 본문 - - - -한 클래스 안에 락이 두 종류다. 얼핏 비대칭으로 보이지만 각자의 커밋 방식이 다르다 — 배타성이 필요한 쪽(replace)에는 OS 락을 두고, 원시연산 자체가 배타적인 쪽(create-link)에는 JVM 스트라이프만 둔 것이다. - -## SecureRandom 참조 위치 - -:::evidence key="adapter-outbound-fileserver-c04" alt="코드베이스에서 SecureRandom 를 검색한 출력 39줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="SecureRandom 코드베이스 검색 — 39줄 · exit 0" zoom="true" -::: - -## root 미포함이 배타 누락으로 이어지지 않는 이유 - -후자의 root 미포함은 **과잉 직렬화** 방향이라(다른 root의 같은 fileId가 같은 스트라이프를 공유) 배타 누락으로는 이어지지 않고, `fileId`는 `SecureRandom` 16바이트라 실질 충돌도 없다. 결함이 아니라 설계로 기록한다. - -## 충돌 경로가 닫히는 방법 - -`createLink`가 충돌하면 임시 파일을 정확히 지우고, 기존 레코드를 읽어 identity와 내용 동등성을 확인한 뒤 같으면 repair, 다르면 `CONFLICT`다. `concurrentCrossInstanceImmutableCollisionIsNeverClassifiedAsStorage`가 그 분류를 고정한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-fileserver-c08.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-fileserver-c08.md deleted file mode 100644 index 5074798..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-fileserver-c08.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-fileserver-c08 -title: 잔여 경로 연산을 선언하고 identity 검사로 감싼다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-fileserver-c08 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-fileserver-c08 - file: ../../../final/evidence/rendered/adapter-outbound-fileserver-c08.svg - - key: adapter-outbound-fileserver-c08-diagram - file: ../../../final/assets/diagrams/adapter-outbound-fileserver-c08.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-fileserver-c08.txt -source: - - 원본 분석 절은 final/document.md#a08#L558 이다. -module: adapter-outbound-fileserver ---- - -# 잔여 경로 연산을 선언하고 identity 검사로 감싼다 - -`LocalPersistentPayloadOperations`의 클래스 javadoc이 무엇이 서술자 상대이고 무엇이 아닌지를 앞에서 밝히고, 전수 검사가 그 서술과 일치한다. - -## 본문 - - - -`LocalPersistentPayloadOperations`의 클래스 javadoc이 무엇이 서술자 상대이고 무엇이 아닌지를 앞에서 밝힌다. 전수 검사가 그 서술과 일치한다(`147-...` §8.2). - -## 선언된 것과 선언되지 않은 것 - -:::evidence key="adapter-outbound-fileserver-c08-diagram" alt="구현 경계 안에 fileKey 와 소유자 권한과 FileStore 재확인이 들어 있고 서술자 상대 대응물 없음이 경계 밖 빗금 상자로 놓인 구조" caption="선언된 것과 선언되지 않은 것" zoom="false" -::: - -이 파일의 `Files.*` 호출은 정확히 그 셋 — `Files.createLink`(\:941), `Files.createDirectory`(\:802), 그리고 force/stat/FileStore 조회 — 뿐이고, 각각 앞뒤로 `fileKey`·소유자·권한·FileStore 재확인이 붙는다. JDK가 `linkat`/`mkdirat`를 노출하지 않으므로 서술자 상대 대응물이 없고, 그 사실을 숨기는 대신 적었다. - -## LocalPersistentPayloadOperations 참조 위치 - -:::evidence key="adapter-outbound-fileserver-c08" alt="코드베이스에서 LocalPersistentPayloadOperations 를 검색한 출력 8줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="LocalPersistentPayloadOperations 코드베이스 검색 — 8줄 · exit 0" zoom="true" -::: - -## 같은 문제를 한 번은 정직하게 다룬 대비 - -**이것이 §32와의 차이다.** 여기서는 잔여 경로 연산이 (a) 문서에 선언되고 (b) identity 검사로 감싸인다. `AtomicMoveContentPublisher`의 발행 rename은 (a) 어디에도 선언되지 않고 (b) 같은 모듈이 "a precheck could only ever approximate"라고 적은 사전검사 하나로만 보호된다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-httpclient-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-httpclient-c02.md deleted file mode 100644 index 188e6e5..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-httpclient-c02.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-httpclient-c02 -title: 회전이 틈으로 관측되지 않게 만든 순서와 두 누수 이력 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-httpclient-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-httpclient-c02 - file: ../../../final/evidence/rendered/adapter-outbound-httpclient-c02.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-httpclient-c02.txt -source: - - 원본 분석 절은 final/document.md#a11#L112 이다. -module: adapter-outbound-httpclient ---- - -# 회전이 틈으로 관측되지 않게 만든 순서와 두 누수 이력 - -`ClientRuntimeRegistry`는 교체본을 먼저 발행하고 이전 세대를 나중에 드레인한다. 과거 누수 두 건이 코드와 주석에 남아 있다. - -## 본문 - - - -"A swap publishes the replacement first and drains the predecessor afterwards, so a rotation is never observable as a gap." `acquire`는 관측한 세대가 예약 직전에 draining으로 넘어가면 **새로 발행된 세대에 대해 재시도**한다. - -## ClientRuntimeRegistry 참조 위치 - -:::evidence key="adapter-outbound-httpclient-c02" alt="코드베이스에서 ClientRuntimeRegistry 를 검색한 출력 24줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ClientRuntimeRegistry 코드베이스 검색 — 24줄 · exit 0" zoom="true" -::: - -## 아무도 열거하지 않아서 보이지 않던 누수 - -교체된 세대가 `runtimes`에서 빠지고 스케줄된 drain 작업만 소유하게 되어, 그 작업이 발화하기 전에 레지스트리가 닫히면 "leaked the whole generation — and the resource-bound suite could not see it, because nothing enumerated it." 지금은 `retired` 집합이 추적한다. - -## 실패가 클수록 더 많이 새던 종료 - -`close()`가 `forEach`로 닫다가 첫 예외에서 멈춰 "a single misbehaving pool left every remaining connection, thread and socket open — **shutdown leaked more the worse the failure was.**" 지금은 전부 닫고 실패를 suppressed로 모은다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-httpclient-c04.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-httpclient-c04.md deleted file mode 100644 index 58fa47f..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-httpclient-c04.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-httpclient-c04 -title: 뒤 규칙이 앞 규칙의 금지를 되돌리지 못한다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-httpclient-c04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-httpclient-c04 - file: ../../../final/evidence/rendered/adapter-outbound-httpclient-c04.svg - - key: adapter-outbound-httpclient-c04-diagram - file: ../../../final/assets/diagrams/adapter-outbound-httpclient-c04.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-httpclient-c04.txt -source: - - 원본 분석 절은 final/document.md#a11#L305 이다. -module: adapter-outbound-httpclient ---- - -# 뒤 규칙이 앞 규칙의 금지를 되돌리지 못한다 - -`DefaultRetryEligibilityEngine.decide`의 javadoc이 규칙이다 — 싼 절대 차단이 먼저 오고, 그다음 모호성, 그다음 상태·실패별 규칙이 온다. - -## 본문 - - - -`DefaultRetryEligibilityEngine.decide`의 javadoc이 규칙이다 — "The order is the point. Cheap absolute blockers come first (attempts, budget, replayability, first byte, deadline, draining), then ambiguity, then status- and failure-specific rules. **A later rule can never re-enable something an earlier rule forbade.**" - -## 재시도 결정의 순서 - -:::evidence key="adapter-outbound-httpclient-c04-diagram" alt="절대 차단 여섯과 영구 실패 범주와 증거와 상태별 규칙이 왼쪽에서 오른쪽으로 이어지는 구조" caption="재시도 결정의 순서" zoom="false" -::: - -절대 차단 여섯이 먼저다 — 시도 수 소진 · 예산 소진 · 본문 재생 불가 · **첫 바이트 전달됨** · 런타임 draining · 남은 deadline이 최소 시도 예산 이하. 그다음 영구 실패 범주, 그다음 증거, 그다음 상태/실패별 규칙이다. - -## DefaultRetryEligibilityEngine 참조 위치 - -:::evidence key="adapter-outbound-httpclient-c04" alt="코드베이스에서 DefaultRetryEligibilityEngine 를 검색한 출력 14줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DefaultRetryEligibilityEngine 코드베이스 검색 — 14줄 · exit 0" zoom="true" -::: - -## HTTP 메서드가 결정에 들어 있지 않다 - -`RetryContext.safelyIdempotent()`가 이 모듈의 D-09를 구현한다 — HTTP 메서드는 `RetryContext`에 **아예 없다**("so a POST with a registered idempotency key and a GET against a non-idempotent RPC endpoint are both handled correctly instead of by method-name folklore"). 키 기반 멱등성은 **키가 실제로 전송됐는지**까지 요구한다. test `aKeyThatWasNeverSentDoesNotMakeARepeatSafe`가 그것을 고정한다. - -## 존중한 Retry-After를 자르지 않는 이유 - -`Retry-After`는 남은 deadline 안에 들어갈 때만 존중된다(`allowWithin`), 그리고 존중된 `Retry-After`는 `maxBackoff`로 잘리지 **않는다** — 잘라 버리면 업스트림이 요청한 대기보다 일찍 다시 두드리게 되기 때문이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-messaging-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-messaging-c03.md deleted file mode 100644 index 7c67370..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-messaging-c03.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-messaging-c03 -title: 열린 타입은 페이로드로 받지 않는다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-messaging-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-messaging-c03 - file: ../../../final/evidence/rendered/adapter-outbound-messaging-c03.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-messaging-c03.txt -source: - - 원본 분석 절은 final/document.md#a12#L237 이다. -module: adapter-outbound-messaging ---- - -# 열린 타입은 페이로드로 받지 않는다 - -`ContractCatalogCompiler`가 정확한 record 타입 토큰으로부터 불변 카탈로그를 만들고, test 이름이 무엇을 거부하는지 전부 적는다. - -## 본문 - - - -`ContractCatalogCompiler`가 **정확한 record 타입 토큰**으로부터 불변 카탈로그를 만들고, test 이름이 무엇을 거부하는지 전부 적는다 — 중복 stable/schema/payload 신원, 음수 버전, 잘못된 payload kind, null·공백·중복·반사 불일치 성분 순서, 서술자 누락, **payload 버전 사이의 logical destination 드리프트**. - -## ContractCatalogCompiler 참조 위치 - -:::evidence key="adapter-outbound-messaging-c03" alt="코드베이스에서 ContractCatalogCompiler 를 검색한 출력 18줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ContractCatalogCompiler 코드베이스 검색 — 18줄 · exit 0" zoom="true" -::: - -## 그래프가 닫혀 있어야 스냅샷할 수 있다 - -`recursivelyFreezesOnlyTheClosedDeclaredGenericPayloadGraph` / `rejectsOpenRawWildcardMapJsonTreeInterfaceAndGenericRecordGraphs` — 열린 타입(raw·wildcard·`Map`·JSON 트리·인터페이스·제네릭 record 그래프)을 페이로드로 받지 않는다. 봉투 작성기가 shape을 따라 스냅샷할 수 있으려면 그래프가 닫혀 있어야 한다(§11). - -## 접근자를 정확히 한 번만 부르는 이유 - -`snapshotsEveryContributionAccessorExactlyOnceIncludingAStatefulSchemaHash` / `statefulDescriptorCannotBypassCrossVersionLogicalDestinationDrift` — 기여 접근자를 **정확히 한 번만** 호출한다. 가변 서술자가 검사와 저장 사이에 값을 바꿔 규칙을 우회하는 경로를 닫는다. - -## 반사가 밖으로 새지 않는다 - -`compiledContractUsesOnlyAStaticPublicCompositionBridgeWithoutReflectionLeak` — 컴파일된 계약이 반사를 밖으로 새게 하지 않는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-objectstorage-c05.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-objectstorage-c05.md deleted file mode 100644 index bf85485..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-objectstorage-c05.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-objectstorage-c05 -title: revision은 건너뛰지도 되돌아가지도 못한다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-objectstorage-c05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-objectstorage-c05 - file: ../../../final/evidence/rendered/adapter-outbound-objectstorage-c05.svg - - key: adapter-outbound-objectstorage-c05-diagram - file: ../../../final/assets/diagrams/adapter-outbound-objectstorage-c05.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-objectstorage-c05.txt -source: - - 원본 분석 절은 final/document.md#a09#L244 이다. -module: adapter-outbound-objectstorage ---- - -# revision은 건너뛰지도 되돌아가지도 못한다 - -`ObjectOperationStateMachine`이 publication·scan·reference·direct-grant·multipart 다섯 계열의 전이를 각각 switch로 적고, terminal 처리가 계열마다 명시적이다. - -## 본문 - - - -`ObjectOperationStateMachine`이 publication·scan·reference·direct-grant·multipart 다섯 계열의 전이를 각각 switch로 적는다. terminal 처리가 계열마다 명시적이다. - -## 어디서든 들어가고 나올 수 없는 분기 - -:::evidence key="adapter-outbound-objectstorage-c05-diagram" alt="정상 사슬의 어느 단계에서 EXPIRED 와 ABORTED 와 FAILED 와 CORRUPT 네 상자로 화살표가 나가고 네 상자가 모두 빗금으로 표시된 구조" caption="어디서든 들어가고 나올 수 없는 분기" zoom="false" -::: - -뒤 둘은 **branch 전이**(EXPIRED/ABORTED/FAILED/CORRUPT)를 별도로 허용해, 정상 사슬 어디서든 실패로 빠질 수 있되 terminal에서는 나올 수 없게 한다. - -## ObjectOperationStateMachine 참조 위치 - -:::evidence key="adapter-outbound-objectstorage-c05" alt="코드베이스에서 ObjectOperationStateMachine 를 검색한 출력 16줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ObjectOperationStateMachine 코드베이스 검색 — 16줄 · exit 0" zoom="true" -::: - -## revision을 건너뛰거나 되돌릴 수 없다 - -`requireNextRevision(current, next)`가 `next == current + 1`을 강제한다. `ObjectOperationStateMachineTest`가 셋을 이름으로 고정한다: `publicationFollowsScanFreeAndScanRequiredPaths`, `terminalOutOfOrderAndStaleRevisionTransitionsFailClosed`, `independentStateFamiliesDoNotImplyEachOther`. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-objectstorage-c07.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-objectstorage-c07.md deleted file mode 100644 index 2aa47ac..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-objectstorage-c07.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-objectstorage-c07 -title: durable record가 비밀을 담지 않는다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-objectstorage-c07 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-objectstorage-c07 - file: ../../../final/evidence/rendered/adapter-outbound-objectstorage-c07.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-objectstorage-c07.txt -source: - - 원본 분석 절은 final/document.md#a09#L420 이다. -module: adapter-outbound-objectstorage ---- - -# durable record가 비밀을 담지 않는다 - -`DirectTransferSessionRecord`의 javadoc 한 줄이 이 sub-scope의 출발점이다 — "Durable non-secret direct-transfer session state; bearer material is deliberately absent." - -## 본문 - - - -durable record가 **비밀을 담지 않는다**는 것이 출발점이다. `DirectTransferSessionRecord`의 한 줄 javadoc이 그것이다 — "Durable non-secret direct-transfer session state; bearer material is deliberately absent." 저장되는 것은 generation·제약 다이제스트·서명 시각·만료·credential revision·reference revision뿐이고, presigned URI와 서명 헤더는 **process-local 캐시**에만 남는다. - -프로세스가 재시작하면 이미 발급된 grant는 재현되지 않고 `"issued direct grant bearer material is unavailable after process restart"`로 명시적으로 실패한다 — 조용히 새로 서명해서 두 번째 bearer를 만드는 대신이다. - -## DirectTransferSessionRecord 참조 위치 - -:::evidence key="adapter-outbound-objectstorage-c07" alt="코드베이스에서 DirectTransferSessionRecord 를 검색한 출력 10줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DirectTransferSessionRecord 코드베이스 검색 — 10줄 · exit 0" zoom="true" -::: - -## CAS가 고정하는 순서 - -`SESSION_RESERVED → GRANT_PREPARED → GRANT_ISSUED → DATA_UPLOADED`이고, test 이름이 그 순서를 그대로 못박는다 — `preparedCasPrecedesSigningAndIssuedCasPrecedesReturningTheBearerGrant`. 서명 **전에** prepared가 durable해야 하고, bearer를 **반환하기 전에** issued가 durable해야 한다. - -## 완료를 지평이 지날 때까지 거부한다 - -multipart 쪽에서 가장 흥미로운 것은 완료 시점의 **admission drain**이다. `DirectMultipartCompletionVerifier.requireAdmissionDrained`는 provider가 "controlled ingress가 비었다"고 권위 있게 말해 주지 않으면, `마지막 grant 만료 + 검증된 시계 오차 + 최대 in-flight 지평` 이 지나기 전에는 완료를 거부한다. 이미 발급된 part PUT이 아직 날아가고 있을 수 있기 때문이다. test 이름이 `completionHorizonRejectsWhileIssuedPartRequestsMayStillArrive`와 `lateGrantAdmissionRejectsAfterCompletionFence`로 양방향 모두 고정한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c01.md deleted file mode 100644 index 531e8f8..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c01.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c01 -title: 내부 소비자 부재와 public 계약 필요성을 따로 묻는다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c01 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c01.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c01.txt -source: - - 원본 분석 절은 final/document.md#a05#L70 이다. -module: adapter-outbound-persistence-jpa ---- - -# 내부 소비자 부재와 public 계약 필요성을 따로 묻는다 - -`api/**`는 public top-level production type이 정확히 49개이고 committed baseline과 일치한다. 이 package에는 Spring/JPA annotation이 하나도 없다. - -## 본문 - - - -public top-level production type도 정확히 49개다. `docs/architecture/jpa-api-surface.txt`의 committed API baseline 역시 `api` namespace에서 49개를 기록하고 있어 현재 이름 목록 drift는 없다. Gradle `verifyJpaApiSurface`가 이 surface의 추가/삭제를 fail-closed로 검증한다. 이 package에는 Spring/JPA/Repository/Entity/Configuration annotation이 하나도 없다. - -## JpaEntityNotFoundException 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c01" alt="코드베이스에서 JpaEntityNotFoundException 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="JpaEntityNotFoundException 코드베이스 검색 — 3줄 · exit 0" zoom="true" -::: - -## annotation이 없다는 것의 의미 - -JPA adapter 안에 위치하지만 **API vocabulary 자체는 Spring bean discovery나 JPA mapping으로 활성화되지 않는다.** 실제 composition은 `app-bootstrap` 및 implementation package가 소유한다. `api/**`는 implementation package와 달리 의도적으로 외부 adopter surface다. - -## 참조 0을 dead로 읽지 않는 이유 - -구분해야 하는 질문이 둘이다 — repository 내부 production consumer가 있는가, 그리고 public library contract로 존재할 이유가 있는가. `JpaEntityNotFoundException`은 현재 repository production에서 자신을 제외한 참조 파일이 0개다. 하지만 이 한 사실만으로 dead type이라고 판정하지 않았다. external API surface는 repository 내부에서 직접 생성되지 않더라도 adopter가 catch/translate하는 계약일 수 있기 때문이다. - -## 반대 방향도 성립하지 않는다 - -public API라는 이유로 내부 invariant 결함까지 "미사용이라 안전"으로 넘기지는 않는다. `SignedJsonCursorCodec`처럼 codec 자체가 public contract이고 자기 encode/decode algebra가 불일치하면 repository 내부 consumer 유무와 무관하게 API defect다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c03.md deleted file mode 100644 index ce6ccb1..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c03.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c03 -title: 위험한 shape 자체를 표현하기 어렵게 만든다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c03 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c03.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c03.txt -source: - - 원본 분석 절은 final/document.md#a05#L208 이다. -module: adapter-outbound-persistence-jpa ---- - -# 위험한 shape 자체를 표현하기 어렵게 만든다 - -error hierarchy의 핵심은 예외 class를 많이 만든 것이 아니라 provider-specific signal을 bounded semantic category로 변환하는 것이다. - -## 본문 - - - -error hierarchy의 핵심은 "예외 class를 많이 만든 것"이 아니라 provider-specific signal을 bounded semantic category로 변환하는 것이다. 대표 category는 serialization failure, deadlock, optimistic conflict, lock not available, connection unavailable, timeout 계열, unique/FK/not-null/check constraint, entity not found, schema mismatch, data corruption, completion unknown이다. 이 category는 뒤의 retry policy/metric이 SQLSTATE/provider message를 직접 해석하지 않게 하는 중간 vocabulary다. - -## JpaFailureContext 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c03" alt="코드베이스에서 JpaFailureContext 를 검색한 출력 31줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="JpaFailureContext 코드베이스 검색 — 31줄 · exit 0" zoom="true" -::: - -## 표현 자체를 막는 네 불변식 - -`JpaFailureContext`가 가지는 정보는 operation, SQLSTATE/constraint, attempt, retryability, completion-unknown, elapsed, trace 등으로 제한된다. - -- arbitrary identifier는 그대로 담지 않고 bounded/redacted form으로 축약 -- malformed SQLSTATE는 `redacted`, absent SQLSTATE는 sentinel로 표현 -- completion unknown과 retryable=true를 동시에 표현할 수 없음 -- completion-unknown factory는 항상 automatic retry를 차단하는 형태를 만든다 - -즉 "exception이 발생한 뒤 로그에서 실수하지 말자"보다 앞선 위치에서 **failure context가 위험한 shape 자체를 표현하기 어렵게** 만든다. - -## 메시지가 provider cause를 복사하지 않는다 - -base exception message는 provider cause message를 그대로 복사하지 않고 category + bounded context로 만든다. dedicated test도 provider cause에 email marker를 넣었을 때 top-level exception message에 노출되지 않는 것을 검증한다. 동시에 raw `Throwable cause`는 보존한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c06.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c06.md deleted file mode 100644 index 5c8efca..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c06.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c06 -title: 설정으로 completion unknown을 다시 살릴 수 없다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c06 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c06.svg - - key: adapter-outbound-persistence-jpa-c06-diagram - file: ../../../final/assets/diagrams/adapter-outbound-persistence-jpa-c06.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c06.txt -source: - - 원본 분석 절은 final/document.md#a05#L462 이다. -module: adapter-outbound-persistence-jpa ---- - -# 설정으로 completion unknown을 다시 살릴 수 없다 - -`RetryProfile`의 retryable category allowlist가 타입에서 좁혀져 있고, `COMPLETION_UNKNOWN`을 넣으면 constructor가 즉시 거부한다. - -## 본문 - - - -`TransactionProfile`은 name·propagation·isolation·timeout·readOnly·retryProfile을 결합한다. write profile은 positive timeout이 필수다. read-only는 zero timeout을 "connection default" 의미로 허용한다. 지원 propagation을 REQUIRED / MANDATORY / REQUIRES_NEW로 좁혀 SUPPORTS/NESTED/NOT_SUPPORTED/NEVER처럼 "실제로 transaction 안에 있는가"를 흐리는 mode를 surface에서 제거했다. isolation 역시 PostgreSQL에서 의미가 겹치는 READ_UNCOMMITTED를 expose하지 않는다. - -## allowlist에 들어갈 수 있는 범주 - -:::evidence key="adapter-outbound-persistence-jpa-c06-diagram" alt="allowlist 경계 안에 직렬화 실패와 낙관적 충돌과 락 획득 실패가 들어 있고 COMPLETION_UNKNOWN 이 경계 밖 빗금 상자로 놓인 구조" caption="allowlist 에 들어갈 수 있는 범주" zoom="false" -::: - -retryable category allowlist는 serialization failure, optimistic conflict, lock not available, connection unavailable 계열로 제한된다. `COMPLETION_UNKNOWN`을 넣으면 constructor가 즉시 거부한다. Unique constraint 같은 ineligible category도 거부한다. 즉 failure translator가 retryability를 판단하고, profile이 category allowlist를 가진다고 해서 "어떤 failure도 설정으로 retry 가능하게" 만들 수 없다. - -## RetryDecision 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c06" alt="코드베이스에서 RetryDecision 를 검색한 출력 40줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RetryDecision 코드베이스 검색 — 40줄 · exit 0" zoom="true" -::: - -## 모르겠음과 즉시 한 번 더를 구분한다 - -RETRY_FULL_TRANSACTION을 포함한 세 가지 중 retry만 non-zero delay를 가질 수 있다. 이 분리 덕분에 completion unknown이 `delay=0 retry`처럼 표현되지 않는다. - -## reason에 길이 상한이 없다 - -`RetryDecision.reason`은 javadoc상 bounded diagnostic/low-cardinality-safe string으로 설명된다. 그러나 constructor는 non-null/nonblank만 확인하고 길이/형식 상한은 없다. runtime constructor probe에서는 100,000-character reason도 accepted됐다. 다만 실제 `JpaRetryObservation`은 decision.reason을 metric tag로 사용하지 않는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c07.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c07.md deleted file mode 100644 index 0bb09ad..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c07.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c07 -title: 이름 목록만 고정하고 의미는 고정하지 않는다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c07 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c07 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c07.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c07.txt -source: - - 원본 분석 절은 final/document.md#a05#L639 이다. -module: adapter-outbound-persistence-jpa ---- - -# 이름 목록만 고정하고 의미는 고정하지 않는다 - -`verifyJpaApiSurface --rerun-tasks`가 통과했다. 이 task가 증명하는 것은 public type names가 committed baseline과 동일하다는 것뿐이다. - -## 본문 - - - -`verifyJpaApiSurface --rerun-tasks`가 통과했다. 이 task가 증명하는 것은 **public type names가 committed baseline과 동일하다**는 것이다. - -## 분석 원문의 실행 기록 - -:::evidence key="adapter-outbound-persistence-jpa-c07" alt="분석 문서 final/document.md#a05 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a05 발췌 — 15줄" zoom="true" -::: - -## 이 게이트가 증명하지 않는 것 - -method semantics나 constructor invariant까지 ABI/API compatibility를 검증하는 것은 아니다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c08.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c08.md deleted file mode 100644 index 68edfc2..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c08.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c08 -title: 구현 클래스를 샘플링하지 않고 51개를 전부 읽었다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c08 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c08 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c08.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c08.txt -source: - - 원본 분석 절은 final/document.md#a05#L721 이다. -module: adapter-outbound-persistence-jpa ---- - -# 구현 클래스를 샘플링하지 않고 51개를 전부 읽었다 - -transaction 29 + failure 3 production과 19 test, 합계 51개 source/test를 FULL_READ했다. - -## 본문 - - - -모든 51개 source/test를 FULL_READ했다. 이 scope에서는 implementation class를 샘플링하지 않고 transaction state machine, retry budget, Spring mapping, failure translation, root wiring, consumer reachability까지 연결했다. - -## 분석 원문의 범위 선언 - -:::evidence key="adapter-outbound-persistence-jpa-c08" alt="분석 문서 final/document.md#a05 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a05 발췌 — 15줄" zoom="true" -::: - -## 이 sub-scope가 던진 질문 - -transaction을 여는 코드가 아니라 **commit 결과를 언제 확정하는가, 어떤 failure만 replay하는가, completion-unknown을 어떤 evidence로 남기는가**다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c09.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c09.md deleted file mode 100644 index 44e8be8..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c09.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c09 -title: 같은 leaf 안에 트랜잭션 모델이 둘 있다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c09 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c09 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c09.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c09.txt -source: - - 원본 분석 절은 final/document.md#a05#L737 이다. -module: adapter-outbound-persistence-jpa ---- - -# 같은 leaf 안에 트랜잭션 모델이 둘 있다 - -application-core가 소유한 canonical boundary와 이 leaf의 `api/**`가 소유한 boundary가 동시에 존재한다. - -## 본문 - - - -현재 persistence-jpa에는 transaction을 표현하는 두 계열이 동시에 존재한다. - -```text -PolicyTransactionPort / TransactionPort - -> SpringTransactionPort (@Component) - -> SpringPolicyTransactionPort - -> PlatformTransactionManager -``` - -A의 어휘는 `TransactionRequest`, `TransactionPolicyId`, `CallBudget`, `TransactionResult`, `TransactionOutcome`, `OperationId`, `TransactionPhase`, `ReconciliationReference`다. 이 모델은 application-core가 소유한다 — use case가 outbound adapter type을 import하지 않아도 transaction policy와 uncertain outcome을 표현할 수 있다. - -## TransactionRequest 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c09" alt="코드베이스에서 TransactionRequest 를 검색한 출력 33줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="TransactionRequest 코드베이스 검색 — 33줄 · exit 0" zoom="true" -::: - -## B는 bean graph에 있고 호출자는 없다 - -B의 어휘는 `PersistenceOperationName`, `TransactionProfile`, `RetryProfile`, `JpaPersistenceException`, `TransactionCompletionEvidence`다. `JpaPlatformRuntimeAutoConfiguration`은 `PlatformTransactionManager`가 있으면 `SpringJpaTransactionExecutor` bean을 만들고, 그 executor가 있으면 `FullTransactionRetryCoordinator` bean도 만든다. 따라서 source tree 수준에서는 B가 단순 historical class가 아니라 **현재 runtime bean graph에도 포함되는 구현**이다. 그러나 repository production call search에서는 `FullTransactionRetryCoordinator.execute(...)`를 실제 business/application code가 호출하는 경로가 확인되지 않았다. - -## 공존 자체는 결함이 아니다 - -반대로 application-core transaction port는 sample/use-case/composition에서 canonical contract로 사용된다. `api/**`는 intended external surface이므로 fork/application이 B를 programmatically 사용할 수 있다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c12.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c12.md deleted file mode 100644 index 2bd40ef..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c12.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c12 -title: commit exception을 rollback으로 가정하지 않는다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c12 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c12 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c12.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c12.txt -source: - - 원본 분석 절은 final/document.md#a05#L832 이다. -module: adapter-outbound-persistence-jpa ---- - -# commit exception을 rollback으로 가정하지 않는다 - -`PolicyTransactionPort.inTransaction(...)`은 단순 예외 기반 wrapper가 아니다. commit 호출 전에 phase를 `COMMIT_REQUESTED`로 올리고 Spring `TransactionSynchronization` sentinel로 실제 callback을 관찰한다. - -## 본문 - - - -`PolicyTransactionPort.inTransaction(...)`은 단순 예외 기반 wrapper가 아니다. 결과는 최소 `Committed`, `CommittedWithPostCommitFailure`, `Participating`, `DeterminateRollback`, `Indeterminate`를 구분한다. 핵심은 **commit exception = rollback**으로 가정하지 않는 것이다. - -## SpringPolicyTransactionPort 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c12" alt="코드베이스에서 SpringPolicyTransactionPort 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="SpringPolicyTransactionPort 코드베이스 검색 — 5줄 · exit 0" zoom="true" -::: - -## commit failure가 갈리는 세 갈래 - -commit 호출 전에 phase를 `COMMIT_REQUESTED`로 올리고, Spring `TransactionSynchronization` sentinel로 실제 callback을 관찰한다. commit에서 exception이 발생해도 이렇게 갈린다. - -1. `afterCommit()`이 이미 확인됐으면 `CommittedWithPostCommitFailure` -1. rollback callback/`UnexpectedRollbackException`/replay-candidate가 확인되면 `DeterminateRollback` -1. 그 외에는 `Indeterminate` - -즉 연결 끊김 같은 애매한 exception을 "rollback이겠지"라고 간주하지 않는다. - -## replay가 일어나는 조건 전부 - -`Indeterminate`는 retry 대상이 아니다. replay 조건은 모두 만족해야 한다 — policy가 `COMMAND_SERIALIZABLE_REPLAY_SAFE`, 현재 attempt가 physical transaction owner, attempt < configured max, 현재 스레드가 인터럽트되지 않음, 결과가 `DeterminateRollback`, failure가 40001 serialization 또는 40P01 deadlock replay candidate. - -따라서 commit ack를 못 받은 상태는 replay되지 않는다. 이 점은 뒤에서 다룰 JPA public API completion-evidence wiring gap의 중요한 mitigation이다 — **현재 canonical application path는 completion evidence infrastructure가 없어도 불확정 commit을 자동 재실행하지 않는다.** - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c13.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c13.md deleted file mode 100644 index 03cf519..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c13.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c13 -title: 이미 소비한 시간을 트랜잭션 계층이 다시 주지 않는다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c13 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c13 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c13.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c13.txt -source: - - 원본 분석 절은 final/document.md#a05#L879 이다. -module: adapter-outbound-persistence-jpa ---- - -# 이미 소비한 시간을 트랜잭션 계층이 다시 주지 않는다 - -application policy path는 timeout을 `TransactionDefinition.setTimeout()` 하나로 끝내지 않는다. CallBudget admission이 connection pool을 빌리기 전부터 시작한다. - -## 본문 - - - -application policy path는 timeout을 단순히 `TransactionDefinition.setTimeout()` 하나로 끝내지 않는다. `ca-skeleton.jpa.transaction` settings는 transaction/resource-budget defaults를 가진다. - -- duration positive -- duration <= 1 day -- retry max attempts 1..5 -- statement timeout <= transaction timeout -- lock timeout < statement timeout -- completion/acquisition/action margin hierarchy - -즉 runtime에서 무한 retry나 무한 transaction timeout을 property 하나로 열 수 없게 hard cap을 둔다. - -## RetryProfile 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c13" alt="코드베이스에서 RetryProfile 를 검색한 출력 14줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RetryProfile 코드베이스 검색 — 14줄 · exit 0" zoom="true" -::: - -이 점은 API `RetryProfile.maxAttempts`가 upper bound를 갖지 않는 것과 대비된다. canonical application path는 실제 deployment settings에서 최대 5회를 강제한다. - -## 풀에서 기다린 시간이 다시 주어지지 않는다 - -CallBudget admission은 connection pool을 빌리기 **전**부터 시작한다. transaction을 열 가치가 있으려면 남은 budget이 최소한을 감당해야 하고, begin 후에는 실제 남은 budget으로 Spring whole-transaction timeout, statement timeout, lock timeout, idle-in-transaction timeout을 정한다. 따라서 pool에서 오래 기다린 요청이 "원래 5초 timeout이었으니 DB에서 다시 5초"를 받지 않는다. - -## backoff도 budget을 본다 - -canonical path의 retry backoff도 CallBudget-aware다. 다음 attempt를 시작하기 전에 jitter delay, 다음 acquisition reserve, 다음 최소 transaction/action margin을 모두 감당할 수 있는지 확인한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c17.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c17.md deleted file mode 100644 index 013fe9b..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c17.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c17 -title: SQLSTATE만으로는 구분할 수 없어 phase가 필요하다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c17 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c17 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c17.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c17.txt -source: - - 원본 분석 절은 final/document.md#a05#L1074 이다. -module: adapter-outbound-persistence-jpa ---- - -# SQLSTATE만으로는 구분할 수 없어 phase가 필요하다 - -`CommitFailureClassifier`를 generic SQLSTATE translator 대신 commit call 내부에서만 적용한다. - -## 본문 - - - -completion unknown 후보는 SQLSTATE 40003, connection class 08*, admin shutdown / crash / cannot-connect-now 계열, transport break cause다. - -## CommitFailureClassifier 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c17" alt="코드베이스에서 CommitFailureClassifier 를 검색한 출력 18줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="CommitFailureClassifier 코드베이스 검색 — 18줄 · exit 0" zoom="true" -::: - -## 같은 SQLSTATE가 phase에 따라 다른 뜻이 된다 - -중요한 건 이 classifier를 generic SQLSTATE translator 대신 **commit call 내부에서만** 적용한다는 것이다. connection reset이 query 실행 중 발생했다면 connection unavailable일 수 있지만, provider에게 COMMIT을 보낸 후 reset됐다면 "commit됐는지 모름"이다. SQLSTATE만으로 이 둘을 구분할 수 없고 transaction phase가 필요하다. PostgreSQL classifier source도 이 이유를 직접 설명한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c19.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c19.md deleted file mode 100644 index 9b1b39a..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c19.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c19 -title: runbook이 지목하는 지표를 만드는 호출이 없다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c19 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c19 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c19.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c19.txt -source: - - 원본 분석 절은 final/document.md#a05#L1210 이다. -module: adapter-outbound-persistence-jpa ---- - -# runbook이 지목하는 지표를 만드는 호출이 없다 - -`JpaTransactionObservation.recordCompletionUnknown(...)` 구현은 존재하지만 production에서 그것을 부르는 곳이 없다. - -## 본문 - - - -`JpaTransactionObservation.recordCompletionUnknown(...)` 구현은 존재한다. 하지만 production에서 참조 수를 세면 이렇다 — `JpaObservabilityAutoConfiguration` construction = 0, `JpaTransactionObservation.recordCompletionUnknown(...)` call = 0, `recordCommitted`/`recordRolledBack`/`recordTimedOut` call도 0. - -## JpaObservabilityAutoConfiguration 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c19" alt="코드베이스에서 JpaObservabilityAutoConfiguration 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="JpaObservabilityAutoConfiguration 코드베이스 검색 — 2줄 · exit 0" zoom="true" -::: - -## 기본 listener도 비어 있다 - -`JpaPlatformRuntimeAutoConfiguration`이 만드는 default `RetryEventListener`도 empty implementation이며, `JpaObservabilityAutoConfiguration`을 통해 metric listener로 합성하지 않는다. - -## runbook 신호의 생성 근거를 찾지 못했다 - -runbook의 `jpa.transaction.completion.unknown` signal은 현재 source wiring으로는 생성 근거를 찾지 못했다. 이 observability factory 전체의 reachability 문제는 later observation/baseline capability sub-scope에서 다시 exhaustive하게 확인한다. 여기서는 completion-unknown path의 cross-scope evidence로만 기록한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c30.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c30.md deleted file mode 100644 index 84bdb74..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c30.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c30 -title: Querydsl은 production runtimeClasspath에 들어오지 않는다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c30 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c30 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c30.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c30.txt -source: - - 원본 분석 절은 final/document.md#a05#L2087 이다. -module: adapter-outbound-persistence-jpa ---- - -# Querydsl은 production runtimeClasspath에 들어오지 않는다 - -`QuerydslJpaSupport`는 Advanced opt-in으로 설계돼 있고, lockfile에서 Querydsl은 compileClasspath와 test/integration/performance classpath에만 나타난다. - -## 본문 - - - -`QuerydslJpaSupport`는 Advanced opt-in으로 설계돼 있다. lockfile에서 Querydsl은 compileClasspath와 test/integration/performance classpath에는 나타나지만 production `runtimeClasspath` configuration에는 포함되지 않는다. - -## QuerydslJpaSupport 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c30" alt="코드베이스에서 QuerydslJpaSupport 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="QuerydslJpaSupport 코드베이스 검색 — 3줄 · exit 0" zoom="true" -::: - -## helper가 두는 제한 - -따라서 JPA leaf를 사용하는 것만으로 Querydsl runtime dependency가 Stable deployment에 따라오는 구조는 아니다. `QuerydslJpaSupport`도 bounded page size <= 500, null predicate는 explicit unbounded opt-in 없으면 거부, 등록된 `QueryName`을 Hibernate comment hint로 적용이라는 제한을 둔다. - -## 미채택 상태로 기록한다 - -현재 production consumer는 확인되지 않았다. 이는 Advanced opt-in helper의 미채택 상태로 기록하며 dead-code defect로 단정하지 않는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c34.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c34.md deleted file mode 100644 index 9f6d493..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c34.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c34 -title: 게이트 통과가 오히려 검증기의 한계를 보여 준다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c34 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c34 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c34.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c34.txt -source: - - 원본 분석 절은 final/document.md#a05#L2523 이다. -module: adapter-outbound-persistence-jpa ---- - -# 게이트 통과가 오히려 검증기의 한계를 보여 준다 - -fresh `verifyJpaReleaseGateTasks`가 성공했다. 이 성공은 오히려 validator limitation의 evidence다. - -## 본문 - - - -fresh `verifyJpaReleaseGateTasks`도 성공했다. 이 success는 오히려 validator limitation의 evidence다 — task semantic coverage/tag를 검사하지 않기 때문이다. - -## 분석 원문의 판정 - -:::evidence key="adapter-outbound-persistence-jpa-c34" alt="분석 문서 final/document.md#a05 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a05 발췌 — 15줄" zoom="true" -::: - -## 무엇을 검사하지 않는가 - -task semantic coverage/tag를 검사하지 않는다. 그래서 registry가 지목한 producer task가 실제로 그 대상 테스트를 실행하지 않아도 게이트가 통과한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c35.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c35.md deleted file mode 100644 index 60bf87d..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c35.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c35 -title: 40003이 UNKNOWN으로 강등되면서 조정 경로도 함께 사라진다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c35 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c35 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c35.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c35.txt -source: - - 원본 분석 절은 final/document.md#a05#L2632 이다. -module: adapter-outbound-persistence-jpa ---- - -# 40003이 UNKNOWN으로 강등되면서 조정 경로도 함께 사라진다 - -`PostgreSqlFailureClassifier`의 SQLSTATE 분류는 맞지만 `PostgreSqlExceptionTranslator.translate()`가 `COMPLETION_UNKNOWN`을 일반 `UNKNOWN`으로 강등한다. - -## 본문 - - - -`PostgreSqlFailureClassifier`는 PostgreSQL SQLSTATE를 bounded `FailureCategory`로 분류한다. serialization failure, deadlock, lock-not-available, constraint family, timeout, connection failure, schema/data 문제를 문자열 메시지가 아니라 SQLSTATE/structured server field 기준으로 다루는 방향은 적절하다. constraint 이름도 server error field에서 꺼내 catalog로 번역하므로 localized message parsing에 의존하지 않는다. - -## PostgreSqlFailureClassifier 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c35" alt="코드베이스에서 PostgreSqlFailureClassifier 를 검색한 출력 11줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="PostgreSqlFailureClassifier 코드베이스 검색 — 11줄 · exit 0" zoom="true" -::: - -## 번역기가 completionUnknown을 항상 false로 만든다 - -현재 `PostgreSqlExceptionTranslator.translate()`는 classifier 결과가 `COMPLETION_UNKNOWN`이어도 `JpaFailureContext`의 `completionUnknown`을 항상 `false`로 만들고, switch에서 `COMPLETION_UNKNOWN`을 `UNKNOWN`과 함께 일반 `JpaPersistenceException(FailureCategory.UNKNOWN, ...)`으로 강등한다. 직접 probe에서 SQLSTATE `40003`은 다음처럼 변환됐다. - -```text -type=JpaPersistenceException -category=UNKNOWN -sqlState=40003 -completionUnknown=false -retryable=false -``` - -## 조정 경로까지 함께 사라진다 - -여기서 단순 진단 정보만 사라지는 것이 아니다. 현재 `DefaultJpaRetryPolicy`는 `TransactionCompletionUnknownException` 또는 `FailureCategory.COMPLETION_UNKNOWN`을 가장 먼저 검사해 `RECONCILE`로 보낸다. 그런데 실제 translator를 통과시키면 그 분기에 닿지 못한다. - -즉 **재실행은 막지만, commit 결과를 확인해야 하는 reconciliation 경로도 잃는다.** fail-closed라는 이유로 안전하다고 볼 수 없는 이유다. commit이 실제로 적용됐는지 알 수 없는 상태를 terminal failure로 바꾸면 caller는 설계된 recovery protocol을 실행할 근거를 잃는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c36.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c36.md deleted file mode 100644 index a78cf2a..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c36.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c36 -title: 만료된 COMPLETED row를 inspect와 claim이 다르게 읽는다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c36 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c36 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c36.svg - - key: adapter-outbound-persistence-jpa-c36-diagram - file: ../../../final/assets/diagrams/adapter-outbound-persistence-jpa-c36.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c36.txt -source: - - 원본 분석 절은 final/document.md#a05#L2669 이다. -module: adapter-outbound-persistence-jpa ---- - -# 만료된 COMPLETED row를 inspect와 claim이 다르게 읽는다 - -`PostgreSqlOwnerSafeIdempotencyStore`의 owner/CAS 구조 자체는 강하지만, `inspect()`가 `replayUntil` 만료를 보지 않는다. - -## 본문 - - - -`PostgreSqlOwnerSafeIdempotencyStore`는 row lock, owner token, attempt, state revision, operation id와 transition digest를 결합해 claim/renew/fail/complete를 보호한다. `renew`와 `markFailed`는 동일 operation id replay에서도 semantic argument를 digest에 넣어 `SAME_ARGUMENTS`와 `DIFFERENT_ARGUMENTS`를 분리한다. 이 구조 자체는 강하다. 현재 revision에서는 `PostgreSqlIdempotencyProviderConfig`가 이 store를 production provider로 실제 생성하므로 아래는 dormant helper 문제가 아니다. - -## 같은 row를 읽는 두 경로 - -:::evidence key="adapter-outbound-persistence-jpa-c36-diagram" alt="만료된 COMPLETED row 에서 inspect 와 claim 두 상자로 화살표가 나가고 화살표에 서로 다른 결과가 붙은 구조" caption="같은 row 를 읽는 두 경로" zoom="false" -::: - -`inspect()`는 row가 `COMPLETED`이고 response payload가 있으면 `replayUntil`이 이미 지난 값인지 확인하지 않고 무조건 `COMPLETED_REPLAY`를 반환한다. 반면 claim path는 DB time과 expiry를 보고 만료된 row를 takeover 가능 상태로 처리한다. 실제 PostgreSQL 16에서 replay TTL 25ms로 완료한 뒤 50ms를 기다린 probe 결과는 이렇다. - -```text -expiredInspect.outcome=COMPLETED_REPLAY -expiredInspect.replayUntil= -expiredInspect.claimAfterExpiry=TakenOverClaimed -``` - -## PostgreSqlOwnerSafeIdempotencyStore 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c36" alt="코드베이스에서 PostgreSqlOwnerSafeIdempotencyStore 를 검색한 출력 14줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="PostgreSqlOwnerSafeIdempotencyStore 코드베이스 검색 — 14줄 · exit 0" zoom="true" -::: - -## introspection 문제가 아니라 lifecycle 문제인 이유 - -`IdempotencyExecutorV2`는 reconciliation에서 `COMPLETED_REPLAY`를 실제 저장 응답 반환 신호로 사용한다. 따라서 이 불일치는 **만료 후 새 실행이 허용된 시점에도 이전 응답을 reconciliation 결과로 반환할 수 있는 lifecycle correctness 문제**다. - -## 같은 계약의 다른 구현은 만료로 수명을 끝낸다 - -JPA 설계 문서가 동일 Idempotency V2 contract를 구현한다고 참조하는 Redis state machine도 `COMPLETED -> [*] : replay TTL expires`로 수명을 끝낸다. JPA `inspect()`만 이 만료를 무시한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c38.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c38.md deleted file mode 100644 index f7b0fcd..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c38.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c38 -title: SQL 식별자를 호출자가 조립하지 않는다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c38 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c38 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c38.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c38.txt -source: - - 원본 분석 절은 final/document.md#a05#L2780 이다. -module: adapter-outbound-persistence-jpa ---- - -# SQL 식별자를 호출자가 조립하지 않는다 - -native write와 COPY는 caller가 임의 SQL identifier를 조립하도록 두지 않고 registered statement/name boundary를 사용한다. 값은 JDBC parameter 또는 COPY stream으로 전달된다. - -## 본문 - - - -native write와 COPY는 caller가 임의 SQL identifier를 조립하도록 두지 않고 registered statement/name boundary를 사용한다. 값은 JDBC parameter 또는 COPY stream으로 전달된다. - -## 분석 원문의 경계 확인 - -:::evidence key="adapter-outbound-persistence-jpa-c38" alt="분석 문서 final/document.md#a05 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a05 발췌 — 15줄" zoom="true" -::: - -## 각 경로가 두는 경계 - -COPY에는 format/size bound와 transaction requirement가 있고, work claiming은 등록된 queue definition과 PostgreSQL `FOR UPDATE ... SKIP LOCKED` 경계를 사용한다. JSON path/query support와 range query support도 registry/typed value boundary를 두고 실제 값은 bind한다. constraint translation 역시 structured SQLSTATE/server fields를 사용한다. - -## 이번 sub-scope의 결론 - -이 영역의 새로운 SQL-injection/runtime-wiring defect는 확인되지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c39.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c39.md deleted file mode 100644 index 2d43ebb..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c39.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c39 -title: vendor migration 아홉을 실제 PostgreSQL에서 적용했다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c39 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c39 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c39.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c39.txt -source: - - 원본 분석 절은 final/document.md#a05#L2805 이다. -module: adapter-outbound-persistence-jpa ---- - -# vendor migration 아홉을 실제 PostgreSQL에서 적용했다 - -9개 migration을 모두 읽고 실제 PostgreSQL probe에서 Flyway가 전부 validate/apply했다. - -## 본문 - - - -다음 9개 migration을 모두 읽었다. 확인한 경계는 이렇다. - -- idempotency owner/state/replay/transition metadata의 persisted shape -- outbox claim/delivery/index shape -- integer advisory/row-lock support table과 expiry extension -- capability schema registry adoption/widening -- request hash `char`/`varchar` drift 보정 -- durable operation / live-event log schema - -## 분석 원문의 확인 목록 - -:::evidence key="adapter-outbound-persistence-jpa-c39" alt="분석 문서 final/document.md#a05 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a05 발췌 — 15줄" zoom="true" -::: - -## 실제 적용 결과와 남긴 범위 - -real PostgreSQL probe에서 Flyway는 vendor 9 migrations를 모두 validate/apply했다. 이번 sub-scope에서 migration 순서, 현재 schema 제약, index 선언 자체로 승격할 신규 defect는 확인하지 못했다. capability-specific migration의 완전한 cross-stream adoption은 각 owning capability scope에서 다시 본다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c40.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c40.md deleted file mode 100644 index 9414bef..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c40.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c40 -title: 재현에 쓴 레인과 복원까지 남긴 기록 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c40 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c40 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c40.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c40.txt -source: - - 원본 분석 절은 final/document.md#a05#L2883 이다. -module: adapter-outbound-persistence-jpa ---- - -# 재현에 쓴 레인과 복원까지 남긴 기록 - -`postgresqlIdempotencyIntegrationTest` 레인에서 두 경계를 실제로 재현했고, 임시 테스트는 실행 후 source에서 복원했다. - -## 본문 - - - -`postgresqlIdempotencyIntegrationTest` 레인에서 두 경계를 실제로 재현했다. - -- `complete()`가 replay TTL을 바꿨을 때의 false-same replay 재현 -- 만료된 COMPLETED row의 `inspect()`/`claim()` lifecycle 불일치 재현 - -원본은 `evidence/raw/066-postgresql-idempotency-replay-boundary-probe.txt`에 있고, 실행 결과는 BUILD SUCCESSFUL이다. 임시 테스트는 실행 후 source에서 복원했다. - -## 분석 원문의 실행 기록 - -:::evidence key="adapter-outbound-persistence-jpa-c40" alt="분석 문서 final/document.md#a05 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a05 발췌 — 15줄" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c41.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c41.md deleted file mode 100644 index 2884798..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c41.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c41 -title: 보류한 항목과 보류한 이유 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c41 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c41 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c41.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c41.txt -source: - - 원본 분석 절은 final/document.md#a05#L2913 이다. -module: adapter-outbound-persistence-jpa ---- - -# 보류한 항목과 보류한 이유 - -이번 scope에서 확인했으나 finding으로 승격하지 않은 항목들이다. - -## 본문 - - - -이번 scope에서 확인했으나 finding으로 승격하지 않은 항목이다. - -- registered native write/COPY의 SQL/value boundary -- work-claim `SKIP LOCKED` 기본 구조 -- structured SQLSTATE/constraint-name 추출 -- array/json helper의 bounded value handling -- vendor migration 9개의 현재 적용 순서/문법 - -## 분석 원문의 보류 목록 - -:::evidence key="adapter-outbound-persistence-jpa-c41" alt="분석 문서 final/document.md#a05 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a05 발췌 — 15줄" zoom="true" -::: - -## 보류 이유가 따로 있는 항목 - -polling outbox cutover sentinel의 transition별 반복 검사 차이는, claim 자체가 immutable sentinel을 요구하고 current evidence만으로 stale claim이 cutover를 우회한다고 입증되지 않아 보류했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c43.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c43.md deleted file mode 100644 index 860604d..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c43.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c43 -title: H2 idempotency와 V2 owner 필드 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c43 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c43 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c43.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c43.txt -source: - - 원본 분석 절은 final/document.md#a05#L3119 이다. -module: adapter-outbound-persistence-jpa ---- - -# H2 idempotency와 V2 owner 필드 - -`H2IdempotencyClaimRepository`의 MERGE/takeover는 V2 owner/transition field를 초기화하지 않는다. baseline `IdempotencyRecordEntity`가 V1 field만 매핑하고 owner-safe V2는 PostgreSQL capability stream으로 분리되어 있으므로, 서로 다른 schema generation의 필드를 H2 V1이 reset하지 않는 것은 현재 계약 위반이 아니다. - -## 본문 - - - -## H2 baseline이 소유하는 필드 - -`H2IdempotencyClaimRepository`의 MERGE/takeover 경로에는 V2 owner/transition field 초기화가 없다. 그러나 baseline `IdempotencyRecordEntity` 자체가 V1 field만 매핑한다. - -## H2IdempotencyClaimRepository 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c43" alt="코드베이스에서 H2IdempotencyClaimRepository 를 검색한 출력 4줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="H2IdempotencyClaimRepository 코드베이스 검색 — 4줄 · exit 0" zoom="true" -::: - -## PostgreSQL V2와 분리된 activation contract - -owner-safe V2는 PostgreSQL capability stream으로 분리되어 별도 activation contract를 가진다. H2 baseline 경로와 PostgreSQL V2 경로가 서로 다른 schema generation을 소유하므로, H2 V1이 V2 field를 reset하지 않는 동작은 현재 계약과 충돌하지 않는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c46.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c46.md deleted file mode 100644 index 3546e02..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c46.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c46 -title: FIFO 정산은 기록된 적응이고 전제가 빠진 것이 문제다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c46 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c46 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c46.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c46.txt -source: - - 원본 분석 절은 final/document.md#a05#L3350 이다. -module: adapter-outbound-persistence-jpa ---- - -# FIFO 정산은 기록된 적응이고 전제가 빠진 것이 문제다 - -reservation row가 upload id와 연결되지 않아 `JpaQuotaCommitGateway`가 scope의 가장 오래된 live reservation부터 정산하는 것은 `docs/fileserver/design-deviations.md`에 명시적으로 기록된 adaptation이다. - -## 본문 - - - -reservation row가 upload id와 연결되지 않아 `JpaQuotaCommitGateway`가 scope의 가장 오래된 live reservation부터 정산하는 것은 `docs/fileserver/design-deviations.md`에 명시적으로 기록된 adaptation이다. row identity와 실제 upload identity가 1\:1이 아닌 것 자체는 현재 설계 계약이다. - -## JpaQuotaCommitGateway 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c46" alt="코드베이스에서 JpaQuotaCommitGateway 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="JpaQuotaCommitGateway 코드베이스 검색 — 5줄 · exit 0" zoom="true" -::: - -## 전제가 실제로 없다는 것은 따로 올렸다 - -그 문서가 전제로 둔 aggregate byte enforcement가 실제로 없다는 점은 §78의 별도 P1 finding으로 올렸다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c47.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c47.md deleted file mode 100644 index b5d42ea..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c47.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c47 -title: 만료 직후 takeover 전 창은 보이지만 계약이 그것을 금지하지 않는다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c47 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c47 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c47.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c47.txt -source: - - 원본 분석 절은 final/document.md#a05#L3354 이다. -module: adapter-outbound-persistence-jpa ---- - -# 만료 직후 takeover 전 창은 보이지만 계약이 그것을 금지하지 않는다 - -cleanup settlement query는 claim token을 fence하고 reaper takeover가 token을 교체한다. lease expiry 직후 아직 takeover 전인 worker가 settle할 수 있는 window는 보인다. - -## 본문 - - - -cleanup settlement query는 claim token을 fence하고 reaper takeover가 token을 교체한다. lease expiry 직후 아직 takeover 전인 worker가 settle할 수 있는 window는 보이지만, 새 owner가 생긴 뒤 stale worker가 상태를 덮어쓰는 race는 token CAS가 막는다. - -## 분석 원문의 판정 - -:::evidence key="adapter-outbound-persistence-jpa-c47" alt="분석 문서 final/document.md#a05 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a05 발췌 — 15줄" zoom="true" -::: - -## finding으로 올리지 않은 이유 - -durable-operation과 달리 현재 계약만으로 "expiry 순간부터 절대 settle 금지"라고 확정할 충분한 근거가 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c48.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c48.md deleted file mode 100644 index 05863ff..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c48.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c48 -title: 레지스트리는 V4에 멈춰 있고 매핑은 V10까지 의존한다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c48 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c48 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c48.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c48.txt -source: - - 원본 분석 절은 final/document.md#a05#L3401 이다. -module: adapter-outbound-persistence-jpa ---- - -# 레지스트리는 V4에 멈춰 있고 매핑은 V10까지 의존한다 - -Notification JPA capability는 production opt-in path로 실제 composition된다. 그런데 schema 레지스트리 revision이 V4에서 멈춰 있고 현재 매핑은 V5~V10에서 추가된 column/constraint에 의존한다. - -## 본문 - - - -Notification JPA capability는 production opt-in path로 실제 composition된다. `PersistenceJpaRootAutoConfiguration`이 `NotificationJpaPersistenceFacade`를 import한다. - -## PersistenceJpaRootAutoConfiguration 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c48" alt="코드베이스에서 PersistenceJpaRootAutoConfiguration 를 검색한 출력 10줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="PersistenceJpaRootAutoConfiguration 코드베이스 검색 — 10줄 · exit 0" zoom="true" -::: - -## 조립되는 것 - -facade가 `NotificationJpaPersistenceConfig`를 import하고 entity/repository/store bean을 조립한다. application-side worker/config가 recipient lease, reconciliation, provider-event ledger, admin operation store를 실제 소비한다. - -## 레지스트리 revision이 V4에서 멈춰 있다 - -`NotificationSchemaActivation`은 capability registry를 읽어 startup activation을 검사한다. schema stream은 V1\~V10까지 진화했지만 registry는 V4에서 `jpa-notification-platform-v4`, `feature_revision=4`, `INSTALLED_INACTIVE`를 기록한 뒤 더 이상 revision을 올리지 않는다. 반면 current Java mapping과 SQL은 V5\~V10에서 추가된 column/constraint에 실제 의존한다. 이 drift가 §88의 startup false-positive를 만든다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c50.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c50.md deleted file mode 100644 index 0d9ef3e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c50.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c50 -title: 이 scope에서는 cross-tenant bypass를 확정하지 못했다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c50 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c50 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c50.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c50.txt -source: - - 원본 분석 절은 final/document.md#a05#L3565 이다. -module: adapter-outbound-persistence-jpa ---- - -# 이 scope에서는 cross-tenant bypass를 확정하지 못했다 - -`TenantBoundRepositoryGuard`와 tenant-qualified repository method가 존재하고, 이번 68-file owning scope에서 즉시 재현 가능한 cross-tenant bypass는 확정하지 못했다. - -## 본문 - - - -`TenantBoundRepositoryGuard`와 tenant-qualified repository method가 존재하고, 이번 68-file owning scope에서 즉시 재현 가능한 cross-tenant bypass는 확정하지 못했다. - -## TenantBoundRepositoryGuard 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c50" alt="코드베이스에서 TenantBoundRepositoryGuard 를 검색한 출력 1줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="TenantBoundRepositoryGuard 코드베이스 검색 — 1줄 · exit 0" zoom="true" -::: - -## 이 기록이 주장하지 않는 범위 - -tenant-sensitive lookup이 전부 완전하다고 corpus 전체 결론을 내리지는 않았다. 별도 inbound/application authorization 조합은 cross-scope 단계가 소유한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c52.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c52.md deleted file mode 100644 index 803384a..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c52.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c52 -title: schema 값을 statement text에 붙이지 않고 set_config로 바인딩한다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c52 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c52 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c52.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c52.txt -source: - - 원본 분석 절은 final/document.md#a05#L3780 이다. -module: adapter-outbound-persistence-jpa ---- - -# schema 값을 statement text에 붙이지 않고 set_config로 바인딩한다 - -`SchemaTenantRegistry`가 unquoted PostgreSQL identifier shape를 제한하고, connection provider는 schema 값을 bound `set_config`로 적용하며 release 시 neutral `pg_catalog`로 reset한다. - -## 본문 - - - -`SchemaTenantRegistry`는 unquoted PostgreSQL identifier shape를 제한하고, connection provider는 schema 값을 statement text에 직접 붙이지 않고 bound `set_config`로 적용하며 release 시 neutral `pg_catalog`로 reset한다. - -## SchemaTenantRegistry 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c52" alt="코드베이스에서 SchemaTenantRegistry 를 검색한 출력 14줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="SchemaTenantRegistry 코드베이스 검색 — 14줄 · exit 0" zoom="true" -::: - -## 보장하지 않는 범위 - -별도 failure-in-reset / pool-implementation semantics까지 corpus 전체 보장은 하지 않는다. 다만 현재 happy-path isolation contract를 뒤집을 evidence는 없었다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c54.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c54.md deleted file mode 100644 index 9038389..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c54.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c54 -title: 이름이 말하는 것과 반환하는 것이 다른 helper -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c54 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c54 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c54.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c54.txt -source: - - 원본 분석 절은 final/document.md#a05#L4017 이다. -module: adapter-outbound-persistence-jpa ---- - -# 이름이 말하는 것과 반환하는 것이 다른 helper - -두 public helper는 defining file 밖 exact FQN reference가 0이다. `PostgreSqlContractExtension.serverVersion()`은 이름과 달리 database의 `SHOW server_version`이 아니라 Docker image + container id를 반환한다. - -## 본문 - - - -두 public helper는 defining file 밖 exact FQN reference가 0이다. 특히 `PostgreSqlContractExtension.serverVersion()`은 이름과 달리 database의 `SHOW server_version`이 아니라 Docker image + container id를 반환한다. - -## CommitAmbiguityProxy 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c54" alt="코드베이스에서 CommitAmbiguityProxy 를 검색한 출력 4줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="CommitAmbiguityProxy 코드베이스 검색 — 4줄 · exit 0" zoom="true" -::: - -## 실제로 server version을 읽는 쪽은 따로 있다 - -현재 integration support는 별도 `JpaPlatformContractSupport.serverVersion()`로 실제 server version을 읽는다. 따라서 잘못된 current evidence로 분류하지 않고 dead/unadopted helper로 기록한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c55.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c55.md deleted file mode 100644 index ea66458..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c55.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c55 -title: 정규식 파서를 통과해도 Gradle이 다시 파싱한다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c55 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c55 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c55.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c55.txt -source: - - 원본 분석 절은 final/document.md#a05#L4021 이다. -module: adapter-outbound-persistence-jpa ---- - -# 정규식 파서를 통과해도 Gradle이 다시 파싱한다 - -Java testkit parser 자체는 정규식 기반이라 일반 JSON parser가 아니다. 그러나 실제 registry 파일은 root Gradle `verifyJpaReleaseGateTasks`에서 `JsonSlurper`로 다시 parse되고 real task graph까지 resolve한다. - -## 본문 - - - -Java testkit parser 자체는 정규식 기반이라 일반-purpose JSON parser가 아니다. 그러나 실제 registry 파일은 root Gradle `verifyJpaReleaseGateTasks`에서 `JsonSlurper`로 다시 parse되고 real task graph까지 resolve한다. - -## JpaReleaseManifest 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c55" alt="코드베이스에서 JpaReleaseManifest 를 검색한 출력 19줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="JpaReleaseManifest 코드베이스 검색 — 19줄 · exit 0" zoom="true" -::: - -## 별도 finding으로 중복 승격하지 않은 이유 - -malformed JSON을 Java regex parser 하나가 받아들일 가능성만으로 release fail-open을 별도 finding으로 중복 승격하지 않는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c57.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c57.md deleted file mode 100644 index 88b1b98..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c57.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c57 -title: registry에서 task graph 한 방향만 검사한다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c57 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c57 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c57.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c57.txt -source: - - 원본 분석 절은 final/document.md#a05#L4306 이다. -module: adapter-outbound-persistence-jpa ---- - -# registry에서 task graph 한 방향만 검사한다 - -`config/jpa/release-registry.json`의 gate는 6개이고 모두 blocking이다. pool lane은 registry에도 support-matrix에도 `JpaReleaseGate.required()`에도 없는데 `jpaPlatformReleaseGate`가 그것을 의존한다. - -## 본문 - - - -`config/jpa/release-registry.json`의 gate는 6개이고 모두 blocking이다. pool lane은 registry에도, `docs/jpa/support-matrix.md` §Release gates 6행에도, testkit `JpaReleaseGate.required()`에도 없다(세 곳 모두 grep exit=1). 그런데 `jpaPlatformReleaseGate`는 `dependsOn jpaPlatformPoolContractTest`를 갖고, root `jpaReleaseGate`가 그것을 다시 의존한다. - -## 분석 원문의 세 곳 확인 - -:::evidence key="adapter-outbound-persistence-jpa-c57" alt="분석 문서 final/document.md#a05 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a05 발췌 — 15줄" zoom="true" -::: - -## 검사되지 않는 반대 방향 - -`verifyJpaReleaseGateTasks`는 registry → task graph 한 방향만 검사한다 — registry의 각 gate가 실제 `Test` task로 resolve되는가. 반대 방향, 즉 release gate에 들어 있는 lane이 registry에 있는가는 어디서도 검사되지 않는다. - -## 결함으로 올리지 않은 이유 - -`jpaPlatformReleaseGate`의 주석이 밝힌 집계 기준은 "documented gate가 검증되지 않은 채 통과하게 만드는 lane"이고, pool lane은 문서화된 gate를 뒷받침하지 않으므로 기준상 registry에 없는 것이 일관적이다. 다만 그 결과로 `jpaPlatformReleaseGate`에서 pool lane 의존을 지워도 어떤 verifier도 반응하지 않고, 남는 실행 경로는 nightly workflow 한 줄뿐이다. 이 lane의 release gate 소속만은 아무 계약도 보호하지 않는다. P3. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c58.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c58.md deleted file mode 100644 index 0b7c562..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c58.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c58 -title: JVM 공유를 설계 근거로 내세웠지만 소비자 31곳 어디에도 없다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c58 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c58 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c58.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c58.txt -source: - - 원본 분석 절은 final/document.md#a05#L4541 이다. -module: adapter-outbound-persistence-jpa ---- - -# JVM 공유를 설계 근거로 내세웠지만 소비자 31곳 어디에도 없다 - -`JpaPlatformContractSupport`의 클래스 javadoc이 말하는 컨테이너 수명과 실제 사용이 다르다. 실제 사용은 per class이고 JVM 수준 공유 인스턴스나 static holder는 없다. - -## 본문 - - - -클래스 javadoc은 컨테이너를 JVM 수준에서 공유한다고 말한다. 실제 사용은 정확히 그 "per class"다. `JpaPlatformContractSupport.start()` 호출 지점은 31곳이고 대부분 `@BeforeAll`에서 시작해 `@AfterAll`에서 `close()`한다. JVM 수준 공유 인스턴스나 static holder는 없다. - -## 분석 원문의 실측 - -:::evidence key="adapter-outbound-persistence-jpa-c58" alt="분석 문서 final/document.md#a05 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a05 발췌 — 15줄" zoom="true" -::: - -## 87번 기동하고 3분 10초에 끝난다 - -`StablePostgreSqlMatrixContractTest`는 test마다, `JpaPlatformContractSupportOwnershipTest`는 test마다(5개) 컨테이너를 새로 띄운다. 한 번의 전체 tag lane 통과에 PostgreSQL 컨테이너가 87번 기동한다(`115-integration-lane-original-verification.txt`, XML의 Testcontainers 로그 집계). 그럼에도 5개 lane 전체가 3분 10초에 끝났으므로 비용 주장이 무너지는 수준은 아니다. - -## 기록하는 이유는 서술과 구현의 불일치다 - -클래스가 자기 설계 근거로 내세운 "JVM 공유"가 소비자 31곳 어디에서도 성립하지 않는다. P3. - -## 같은 클래스의 다른 서술은 사실이다 - -multi-version 선택을 fail-closed로 거부하는 것(`start()`가 `selected.size() != 1`이면 예외), 그리고 "the CI matrix fans out"은 `jpa-release.yml`(16/17/18), `jpa-pr.yml`(16/18), `jpa-nightly.yml`이 `-Pjpa.matrix.versions`로 실제 fan-out하는 것으로 확인된다. `JpaPlatformContractSupportOwnershipTest`가 지키는 pool 소유권(호출당 새 pool을 만들어 참조를 잃던 과거 결함)도 실제 assertion으로 고정돼 있다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-mongo-c04.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-mongo-c04.md deleted file mode 100644 index b38b931..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-mongo-c04.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-mongo-c04 -title: 취소된 stream은 성공도 실패도 기록하지 않는다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-mongo-c04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-mongo-c04 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-mongo-c04.svg - - key: adapter-outbound-persistence-mongo-c04-diagram - file: ../../../final/assets/diagrams/adapter-outbound-persistence-mongo-c04.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-mongo-c04.txt -source: - - 원본 분석 절은 final/document.md#a06#L648 이다. -module: adapter-outbound-persistence-mongo ---- - -# 취소된 stream은 성공도 실패도 기록하지 않는다 - -`DefaultReactiveMongoExecutor`의 javadoc이 blocking 경로가 공짜로 얻는 것과 여기서 직접 배치해야 하는 것을 대비한다. 그 배치의 결과로 취소된 stream이 `result=unknown` 버킷에 남는다. - -## 본문 - - - -`DefaultReactiveMongoExecutor`의 javadoc이 blocking 경로가 공짜로 얻는 것과 여기서 직접 배치해야 하는 것을 대비한다. - -## 반응형 경로가 직접 배치한 것 - -:::evidence key="adapter-outbound-persistence-mongo-c04-diagram" alt="실행기 경계 안에 관측 범위와 조립 후 timeout 과 Reactor Context 이동 세 상자가 나란히 들어 있는 구조" caption="반응형 경로가 직접 배치한 것" zoom="false" -::: - -observation scope를 Reactor 자원(`Mono.using`/`Flux.using`)으로 두어 완료·오류·**취소** 모두에서 닫는다. HTTP 클라이언트 연결 해제가 취소를 일으키므로 취소가 흔한 경우다. timeout은 조립된 publisher에 적용한다 — 구독 전에 적용하면 "람다를 만드는 데 걸린 시간"을 재게 된다. context는 Reactor Context로 옮긴다(`ReactiveMongoContextKeys`) — 체인은 operator 경계마다 스레드를 바꾸므로 구독 시점의 `ThreadLocal`은 driver 응답 시점에 이미 없다. - -## DefaultReactiveMongoExecutor 참조 위치 - -:::evidence key="adapter-outbound-persistence-mongo-c04" alt="코드베이스에서 DefaultReactiveMongoExecutor 를 검색한 출력 8줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DefaultReactiveMongoExecutor 코드베이스 검색 — 8줄 · exit 0" zoom="true" -::: - -## result=unknown 버킷이 두 가지를 함께 담는다 - -`executeMany(...)`는 성공을 `doOnComplete`로 기록하므로 **취소된 stream은 success도 failure도 기록하지 않는다.** observation은 `close()`되고 초기 tag(`result=unknown`, `failureCategory=none`)로 한 번 계수된다. 취소가 흔한 경로라는 점을 감안하면 이는 의도된 분류로 보이지만, `result=unknown` bucket이 "취소"와 "관측 시작 직후 예외"를 함께 담는다는 사실은 계약에 없다. P3/기록. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-mongo-c06.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-mongo-c06.md deleted file mode 100644 index 615af38..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-persistence-mongo-c06.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-mongo-c06 -title: 더 자주 갱신해도 고쳐지지 않는 문제라서 fencing을 얹는다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-mongo-c06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-mongo-c06 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-mongo-c06.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-mongo-c06.txt -source: - - 원본 분석 절은 final/document.md#a06#L868 이다. -module: adapter-outbound-persistence-mongo ---- - -# 더 자주 갱신해도 고쳐지지 않는 문제라서 fencing을 얹는다 - -`MongoMigrationLock.fence()`의 javadoc이 lease 만료와 보유자 정지가 다르다는 것을 적는다 — 첫 runner는 갱신했어야 할 그 순간에 돌고 있지 않다. - -## 본문 - - - -`MongoMigrationLock.fence()`의 javadoc이 이 sub-scope에서 가장 정확한 문장을 담고 있다. - -> A lease expiring is not the same as its holder stopping. A runner paused inside a long `execute` — a stop-the-world pause, a stalled network write — loses the lease on the server while its thread is still alive and still writing… **Refreshing more often does not fix that: the first runner is not running at the moment it would refresh.** - -그래서 lease 위에 monotonic fencing token을 얹고, `MongoCollectionMigrationLock.tryAcquire`가 그 token을 **lease를 부여하는 같은 조건부 update 안에서 서버가 증가**시킨다("A token handed out anywhere else could be handed out twice"). `held()`는 owner 이름이 같아도 fence가 다르면 false를 반환한다 — 프로세스가 재시작했거나 운영자가 owner 문자열을 재사용한 경우다. - -## MongoCollectionMigrationLock 참조 위치 - -:::evidence key="adapter-outbound-persistence-mongo-c06" alt="코드베이스에서 MongoCollectionMigrationLock 를 검색한 출력 15줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MongoCollectionMigrationLock 코드베이스 검색 — 15줄 · exit 0" zoom="true" -::: - -## heartbeat이 migration에게 넘겨진 이유 - -`matchedCount`를 쓰는 이유(같은 값을 다시 쓰면 `modifiedCount`가 0이라 소유권 판정이 뒤집힌다)도 두 곳에 적혀 있다. `MongoMigrationHeartbeat`은 이미 고쳐진 결함의 산물이다 — runner가 `execute`가 **반환된 뒤에** 한 번만 refresh했으므로, 40분짜리 `execute`는 35분 동안 만료된 lease를 들고 있었고 그 사이 두 번째 runner가 정당하게 획득해 같은 migration을 동시에 돌렸다. 이제 heartbeat이 migration에게 넘겨진다 — batch 경계를 아는 것은 migration뿐이기 때문이다. - -## 첫 checkpoint가 전부 거부되던 두 결함 - -`MongoCollectionMigrationLedger.saveCheckpoint`에는 **두 개의** 결함 이력이 주석으로 남아 있다. upsert 하나로는 "매치할 게 없었다"와 "fence filter가 배제했다"를 구분할 수 없어 *모든 migration의 첫 checkpoint*가 "a newer migration runner owns the lease"로 거부됐고, 동시에 진짜 배제 경로는 unique index의 duplicate-key로 죽어 그 문장을 만드는 분기가 **도달 불가**였다. 지금은 replace-then-insert로 두 경우를 분리한다. - -## rollback이 없는 것도 명시적 결정이다 - -"A rollback method implies the reverse operation is always safe and always possible, and for a backfill that dropped a column's old values it is neither." 실패한 production 변경은 forward-fix migration으로 고친다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-support-c04.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-support-c04.md deleted file mode 100644 index 1bfff09..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-adapter-outbound-support-c04.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-support-c04 -title: 직접 참조 0이지만 broad component scan으로 도달한다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-support-c04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-support-c04 - file: ../../../final/evidence/rendered/adapter-outbound-support-c04.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-support-c04.txt -source: - - 원본 분석 절은 final/document.md#a04#L318 이다. -module: adapter-outbound-support ---- - -# 직접 참조 0이지만 broad component scan으로 도달한다 - -`OutboundSupportConfig`는 `@Configuration`이며 `FailOpenDependencyLogger` bean 하나만 제공한다. support 밖 production Java에서 명시적으로 참조하는 파일은 0개지만 현재 active scanned path다. - -## 본문 - - - -`OutboundSupportConfig`는 `@Configuration`이며 `FailOpenDependencyLogger` bean 하나만 제공한다. 별도 master property condition은 없다 — support 자체를 optional capability로 취급하지 않고, 실제 provider/client capability의 on/off를 sibling adapter가 소유하게 하려는 구조다. - -## OutboundSupportConfig 참조 위치 - -:::evidence key="adapter-outbound-support-c04" alt="코드베이스에서 OutboundSupportConfig 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="OutboundSupportConfig 코드베이스 검색 — 3줄 · exit 0" zoom="true" -::: - -## 참조 0인데도 unwired가 아닌 이유 - -`OutboundSupportConfig`를 support 밖 production Java에서 명시적으로 참조하는 파일은 0개다. 그러나 실제 composition root `CaSkeletonApplication`은 `dev.caskeleton.adapter`를 broad component scan한다. `AUTO_CONFIGURED_PACKAGES` exclusion에는 messaging/notification/persistence 등은 들어가지만 support package는 포함되지 않으므로 support config는 broad component scan으로 도달한다. registry도 support runtime membership을 `app-bootstrap`으로 선언하고 `app-bootstrap/build.gradle`이 support project를 직접 `implementation`한다. 따라서 이 configuration은 현재 **active scanned path**다. - -## master switch 누락으로 판정하지 않은 이유 - -support config 자체에는 `@ConditionalOnProperty`가 없고 `@ConditionalOnMissingBean`만 있다. support는 provider/client를 생성하지 않고 logger bean 하나만 default로 제공한다. 실제 messaging/notification/httpclient 등은 자기 capability root에서 activation을 소유한다. support README와 config javadoc 모두 이 비대칭을 의도적으로 설명한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-application-core-c04.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-application-core-c04.md deleted file mode 100644 index e0474bf..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-application-core-c04.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -kind: CONCEPT -slug: application-core-c04 -title: 효과가 없었다고 증명할 수 없으면 자동 재시도 권한을 주지 않는다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:application-core-c04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: application-core-c04 - file: ../../../final/evidence/rendered/application-core-c04.svg - - key: application-core-c04-diagram - file: ../../../final/assets/diagrams/application-core-c04.svg -evidence: - - ../../../final/evidence/raw/application-core-c04.txt -source: - - 원본 분석 절은 final/document.md#a03#L118 이다. -module: application-core ---- - -# 효과가 없었다고 증명할 수 없으면 자동 재시도 권한을 주지 않는다 - -idempotency V2와 inbox와 outbox 셋이 각각 응답을 잃은 구간을 상태로 보존한다. - -## 관계 - -- **legacy storage/notification compatibility surface의 제거 조건 추적** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -초기 contract는 scope + request fingerprint로 claim/replay를 제공하고, same key/different fingerprint를 conflict로 분리한다. completed result는 replay하고 in-flight는 bounded poll한다. 이 버전은 "DB operation의 효과가 이미 발생했지만 응답만 잃은 상태"를 충분히 표현하지 못한다. - -## 응답을 잃은 구간을 맡는 세 장치 - -:::evidence key="application-core-c04-diagram" alt="응답을 잃은 구간에서 멱등 기록과 인박스와 아웃박스 세 상자로 화살표가 나가고 화살표마다 담당 방식이 붙은 구조" caption="응답을 잃은 구간을 맡는 세 장치" zoom="false" -::: - -V2는 owner-safe CAS handle에 scope/token/attempt/revision/claimOperationId를 넣고 stale owner mutation을 거부한다. processing-start를 durable하게 확인하기 전에는 body를 실행하지 않으며 claim/start/completion의 unknown result는 inspect/reconcile 대상으로 남긴다. - -## 증명할 수 없는 것을 재시도 근거로 쓰지 않는다 - -processing start 이후 ordinary RuntimeException은 효과가 없다고 증명할 수 없으므로 `EFFECT_UNKNOWN_ABANDONED` 쪽으로 분류되고 자동 replay 권한을 주지 않는다. 명시적인 `RetryableNoEffect`만 안전 재시도 근거로 취급한다. - -## RetryableNoEffect 참조 위치 - -:::evidence key="application-core-c04" alt="코드베이스에서 RetryableNoEffect 를 검색한 출력 4줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RetryableNoEffect 코드베이스 검색 — 4줄 · exit 0" zoom="true" -::: - -## bounded 하게 만든 것들 - -scope digest는 versioned keyed digest + operation code로 정규화되고 raw identity는 외부 surface에서 제거된다. lease/replay TTL과 owner token grammar도 bounded다. - -**Historical evidence.** V2 contract가 인접한 package에 중복 복제돼 구현체들이 서로 다른 nominal type을 참조한 문제가 있었고, singular contract를 유지하는 regression test가 존재한다. - -## inbox 상태 모델 - -Inbox contract는 same-store 처리와 owner-safe receive/process state를 모델링한다. `RECEIVED -> PROCESSING -> COMPLETED/RETRYABLE/DEAD` 상태를 가지고 ACK는 handler transaction commit 이후에만 가능하다. expired owner가 늦게 결과를 기록하는 것을 owner token/attempt/revision/operation identity로 막는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-application-core-c05.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-application-core-c05.md deleted file mode 100644 index 2eac6c2..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-application-core-c05.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -kind: CONCEPT -slug: application-core-c05 -title: efficiency lock이 correctness authority를 대체하지 않는다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:application-core-c05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: application-core-c05 - file: ../../../final/evidence/rendered/application-core-c05.svg -evidence: - - ../../../final/evidence/raw/application-core-c05.txt -source: - - 원본 분석 절은 final/document.md#a03#L152 이다. -module: application-core ---- - -# efficiency lock이 correctness authority를 대체하지 않는다 - -`CacheAsideExecutor`는 결과 종류를 명시적으로 구분하고, lease와 lock은 `EFFICIENCY_ONLY`로 자기 보증 등급을 스스로 적는다. - -## 관계 - -- **legacy storage/notification compatibility surface의 제거 조건 추적** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -`CacheAsideExecutor`는 fresh/negative hit, hard miss, stale, incompatible schema, provider unavailable을 명시적으로 구분한다. source load에는 key-local single-flight와 global source bulkhead를 함께 적용한다. in-flight key 수, waiter 수, source concurrency, admission wait, load deadline이 모두 bounded다. - -## CacheAsideExecutor 참조 위치 - -:::evidence key="application-core-c05" alt="코드베이스에서 CacheAsideExecutor 를 검색한 출력 22줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="CacheAsideExecutor 코드베이스 검색 — 22줄 · exit 0" zoom="true" -::: - -## stale을 돌려주는 조건과 돌려주지 않는 조건 - -stale value는 hard expiry 이전이며 **classified transient failure**일 때만 fallback될 수 있다. permanent failure에는 stale을 반환하지 않는다. - -## invalidate 직후의 resurrect race를 막는 방법 - -source load 중 invalidation이 발생하면 lookup 때 캡처한 `CacheWriteCondition`이 더 이상 일치하지 않아 이전 source result의 refill을 거부한다. 이는 invalidate 직후 늦게 끝난 source load가 stale value를 resurrect하는 race를 막는다. - -## refresh 조정은 correctness lock이 아니다 - -optional distributed refresh coordination은 soft lease로 한 pod만 refresh하도록 하지만 correctness lock은 아니다. owner는 lease 획득 후 cache를 재확인해 다른 pod가 이미 fill했다면 source를 호출하지 않는다. claim 결과가 indeterminate이면 **동일 attempt token으로 한 번만 재시도**한다. contender는 stale이 아직 valid하면 즉시 stale을 반환할 수 있다. - -## single-flight가 leader를 영원히 두지 않는 방법 - -`CacheSingleFlight`는 waiter timeout/interruption을 보존하고 완료된 flight를 제거한다. leader가 영원히 남아 key bound를 점유하지 않도록 monotonic deadline 이후 abandoned flight를 opportunistic reap한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-application-core-c09.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-application-core-c09.md deleted file mode 100644 index b237822..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-application-core-c09.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -kind: CONCEPT -slug: application-core-c09 -title: 메타데이터와 물리 내용이 원자적이지 않다는 사실을 숨기지 않는다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:application-core-c09 -evidenceCapturedOn: 2026-09-01 -assets: - - key: application-core-c09 - file: ../../../final/evidence/rendered/application-core-c09.svg -evidence: - - ../../../final/evidence/raw/application-core-c09.txt -source: - - 원본 분석 절은 final/document.md#a03#L200 이다. -module: application-core ---- - -# 메타데이터와 물리 내용이 원자적이지 않다는 사실을 숨기지 않는다 - -fileserver의 핵심 contract는 metadata transaction과 filesystem/object I/O가 원자적이지 않다는 사실을 숨기지 않고 recovery model을 두는 것이다. - -## 관계 - -- **legacy storage/notification compatibility surface의 제거 조건 추적** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -fileserver는 application-core 안의 가장 큰 독립 orchestration 중 하나다. 핵심 contract는 "metadata transaction과 filesystem/object I/O가 원자적이지 않다"는 사실을 숨기지 않고 recovery model을 두는 것이다. - -## 거절이 부작용을 남기지 않게 하는 순서 - -upload admission은 authorization을 quota/storage admission보다 먼저 수행해 denial이 side-effect-free이도록 한다. reservation metadata/session은 DB transaction에서 만들지만 staging physical object는 외부 작업이므로 실패 시 compensation/reconciliation 대상이 된다. - -## AmbiguousCompletionException 참조 위치 - -:::evidence key="application-core-c09" alt="코드베이스에서 AmbiguousCompletionException 를 검색한 출력 26줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="AmbiguousCompletionException 코드베이스 검색 — 26줄 · exit 0" zoom="true" -::: - -## writer가 fencing token을 쓰는 이유 - -writer는 one-writer lease + fencing token을 사용한다. stale token은 append/finalize를 진행할 수 없고 takeover는 새 token을 만든다. append 시 metadata offset과 physical length가 다르면 자동 repair하지 않고 conflict로 중단한다. - -## finalize의 검사 순서와 READY의 지위 - -finalize는 declared length, server-computed digest, optional client digest를 순서대로 검사한다. client digest는 server digest를 대체하지 않는다. 이후 VERIFYING으로 이동하고 verifier가 publish 승인해야 READY가 된다. READY가 유일한 public/downloadable state다. - -## publish 뒤 메타데이터가 실패하면 재시도가 아니다 - -publish physical success 뒤 READY metadata transaction이 실패하면 결과는 단순 retryable failure가 아니라 `AmbiguousCompletionException`과 recovery queue로 간다. physical publish가 이미 발생했을 수 있기 때문이다. cancel/cleanup race를 막기 위해 cleanup claim은 writer/cleaner barrier를 형성하며 stale cleanup claim을 reclaim하는 경로가 실제로 호출된다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-domain-core-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-domain-core-c01.md deleted file mode 100644 index f9ec45c..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-domain-core-c01.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -kind: CONCEPT -slug: domain-core-c01 -title: 도메인 내용이 아니라 도메인 모델링 계약을 소유한다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:domain-core-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: domain-core-c01 - file: ../../../final/evidence/rendered/domain-core-c01.svg - - key: domain-core-c01-diagram - file: ../../../final/assets/diagrams/domain-core-c01.svg -evidence: - - ../../../final/evidence/raw/domain-core-c01.txt -source: - - 원본 분석 절은 final/document.md#a01#L68 이다. -module: domain-core ---- - -# 도메인 내용이 아니라 도메인 모델링 계약을 소유한다 - -현재 `domain-core`에는 WorkLog 같은 실제 aggregate가 없다. 실제 샘플 aggregate/value object/event는 `sample-portfolio`에 있다. - -## 본문 - - - -현재 `domain-core`에는 WorkLog 같은 실제 aggregate가 없다. 실제 샘플 aggregate/value object/event는 `sample-portfolio`에 있다. - -## 이 모듈이 실제로 소유한 것 - -:::evidence key="domain-core-c01-diagram" alt="모듈 경계 안에 식별자 추상화와 모델링 표식 두 상자가 들어 있고 sample-portfolio 의 aggregate 가 경계 밖 점선 상자로 놓인 구조" caption="이 모듈이 실제로 소유한 것" zoom="false" -::: - -이 모듈에 남은 production surface는 두 종류다 — **식별자 추상화**(`ResourceId`, `IdFactory`)와 **모델링 표식**(`@ValueObject`, `@AggregateRoot`, `@DomainEvent`). - -## ResourceId 참조 위치 - -:::evidence key="domain-core-c01" alt="코드베이스에서 ResourceId 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ResourceId 코드베이스 검색 — 2줄 · exit 0" zoom="true" -::: - -## 정책이 허용하는 범위와 현재 내용의 차이 - -"business concepts, entities, value objects…"를 둘 수 있는 계층이라는 정책과 달리, 현재 snapshot의 실제 contents는 skeleton 전반에서 사용할 **domain-layer contract/marker**에 가깝다. 이는 현재 source에 대한 관찰이며, 향후 실제 production domain type이 이 module에 추가되지 않는다는 뜻은 아니다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-grpc-advanced-resilience-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-grpc-advanced-resilience-c01.md deleted file mode 100644 index d900197..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-grpc-advanced-resilience-c01.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -kind: CONCEPT -slug: grpc-advanced-resilience-c01 -title: 헤지는 성공했을지도 모르는 호출에 대해 일어난다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:grpc-advanced-resilience-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-advanced-resilience-c01 - file: ../../../final/evidence/rendered/grpc-advanced-resilience-c01.svg - - key: grpc-advanced-resilience-c01-diagram - file: ../../../final/assets/diagrams/grpc-advanced-resilience-c01.svg -evidence: - - ../../../final/evidence/raw/grpc-advanced-resilience-c01.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-advanced-resilience#L58 이다. -module: grpc-advanced-resilience ---- - -# 헤지는 성공했을지도 모르는 호출에 대해 일어난다 - -헤징 예산은 토큰 버킷이다. 헤지 하나가 `round(1/ratio)` 토큰을 쓰고, 완료된 호출 하나가 토큰 하나를 돌려준다. - -## 본문 - - - -토큰 버킷이다. 헤지 하나가 `round(1/ratio)` 토큰을 쓰고, 완료된 호출 하나가 토큰 하나를 돌려준다. 상한이 조용한 구간 뒤의 폭주를 제한한다. - -## 헤지가 허용되는 조건 - -:::evidence key="grpc-advanced-resilience-c01-diagram" alt="첫 시도와 예산 확인과 토큰 소비와 헤지 시도가 왼쪽에서 오른쪽으로 이어지는 구조" caption="헤지가 허용되는 조건" zoom="false" -::: - -비율 상한이 0.5 이고 그 근거가 적혀 있다. - -> "a hedging ratio above 0.5 means more than half of all calls are duplicated, which is a load decision rather than a latency one" - -## 재시도 예산보다 급한 이유 - -> "A retry happens after a failure; a hedge happens on a call that might have succeeded, so a fleet that hedges without a budget doubles its backend load in the steady state and doubles it again the moment latency rises." - -## 이 기록이 다루는 파일 범위 - -:::evidence key="grpc-advanced-resilience-c01" alt="코드베이스에서 파일 목록을 만든 출력 16줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 16줄 · exit 0" zoom="true" -::: - -## 소비가 비교 후 교체 루프다 - -이 가족에서 원자성을 제대로 다룬 몇 안 되는 곳이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-grpc-codegen-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-grpc-codegen-c01.md deleted file mode 100644 index 6431724..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-grpc-codegen-c01.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -kind: CONCEPT -slug: grpc-codegen-c01 -title: 손으로 적은 요구 목록은 갱신이 멈추는 쪽이다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:grpc-codegen-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-codegen-c01 - file: ../../../final/evidence/rendered/grpc-codegen-c01.svg - - key: grpc-codegen-c01-diagram - file: ../../../final/assets/diagrams/grpc-codegen-c01.svg -evidence: - - ../../../final/evidence/raw/grpc-codegen-c01.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-codegen#L104 이다. -module: grpc-codegen ---- - -# 손으로 적은 요구 목록은 갱신이 멈추는 쪽이다 - -`GrpcConsumerFixture.fromJavaSource`가 릴리스된 소비자의 자바 소스에서 요구 사항 셋을 기계적으로 유도한다. - -## 본문 - - - -`GrpcConsumerFixture.fromJavaSource` 가 릴리스된 소비자의 자바 소스에서 요구 사항 셋을 기계적으로 유도한다. - -> "a hand-written requirement list is a second copy of what the client already says and the copy is the one that stops being updated." - -## 소비자 소스에서 유도하는 셋 - -:::evidence key="grpc-codegen-c01-diagram" alt="소비자 자바 소스에서 생성 패키지와 서비스 경로와 메서드 경로 세 상자로 화살표가 나가고 화살표마다 유도 규칙이 붙은 구조" caption="소비자 소스에서 유도하는 셋" zoom="false" -::: - -```text -생성 자바 패키지 = fixture 클래스가 import 하는 패키지 중 접미가 맞는 것 -서비스 = Grpc import → . -메서드 = stub.( 호출 → / -``` - -## GrpcConsumerFixture 참조 위치 - -:::evidence key="grpc-codegen-c01" alt="코드베이스에서 GrpcConsumerFixture 를 검색한 출력 26줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcConsumerFixture 코드베이스 검색 — 26줄 · exit 0" zoom="true" -::: - -## 근사라는 것과 보고를 합치지 않는다는 것 - -그것이 컴파일의 근사라는 것과, 근사인 이유(ADR-GRPC-002)를 함께 적는다. `breaksAgainst` 는 세 종류를 따로 보고한다 — 서비스 경로, 메서드 경로, 자바 패키지. 하나의 개수로 합치지 않는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-grpc-discovery-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-grpc-discovery-c01.md deleted file mode 100644 index da25fc8..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-grpc-discovery-c01.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -kind: CONCEPT -slug: grpc-discovery-c01 -title: 위험한 조합은 정책이 아니라 생성자가 거부한다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:grpc-discovery-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-discovery-c01 - file: ../../../final/evidence/rendered/grpc-discovery-c01.svg - - key: grpc-discovery-c01-diagram - file: ../../../final/assets/diagrams/grpc-discovery-c01.svg -evidence: - - ../../../final/evidence/raw/grpc-discovery-c01.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-discovery#L85 이다. -module: grpc-discovery ---- - -# 위험한 조합은 정책이 아니라 생성자가 거부한다 - -`GrpcResolverProfile`과 `GrpcKubernetesProfile`의 정규 생성자가 일곱 조합을 아예 만들 수 없게 하고, 나머지는 검증기가 잡는다. - -## 본문 - - - -`GrpcResolverProfile` 정규 생성자가 네 조합을 아예 만들 수 없게 한다 — 주소 0 이하, 단일 엔드포인트 리졸버에 복수 주소, 음수 갱신 주기, DNS 인데 갱신 주기 0. `GrpcKubernetesProfile` 정규 생성자는 셋을 막는다 — 메시 라우팅에 in-process 재시도 소유자, 긴 스트림인데 재접속 예산 0, 긴 스트림인데 배수 유예 0. - -## 생성자가 막는 것과 검증기가 잡는 것 - -:::evidence key="grpc-discovery-c01-diagram" alt="정규 생성자 경계 안에 세 종류의 조합이 들어 있고 검증기가 잡는 조합이 경계 밖 점선 상자로 놓인 구조" caption="생성자가 막는 것과 검증기가 잡는 것" zoom="false" -::: - -두 겹의 역할 분담이 이 저장소의 다른 곳에 적힌 규칙과 같다 — 위험한 조합은 정책이 아니라 생성자가 거부하게 만든다. - -## GrpcResolverProfile 참조 위치 - -:::evidence key="grpc-discovery-c01" alt="코드베이스에서 GrpcResolverProfile 를 검색한 출력 16줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcResolverProfile 코드베이스 검색 — 16줄 · exit 0" zoom="true" -::: - -## 검증기 규칙이 일부 조합에서만 발화하는 이유 - -`MESH` + `GRPC_PLATFORM` 은 생성자가 먼저 던지므로(둘 다 in-process 재시도) 검증기까지 오지 않고, `MESH` + `NONE` 이나 `K8S_VIP` + `SERVICE_MESH` 는 생성자를 통과해 검증기가 잡는다. 도달 불가 분기가 아니라 역할 분담이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-grpc-operation-ledger-jpa-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-grpc-operation-ledger-jpa-c01.md deleted file mode 100644 index 939140a..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-grpc-operation-ledger-jpa-c01.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: grpc-operation-ledger-jpa-c01 -title: record를 지나지 않는 쓰기가 있어서 제약을 DB에도 둔다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:grpc-operation-ledger-jpa-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-operation-ledger-jpa-c01 - file: ../../../final/evidence/rendered/grpc-operation-ledger-jpa-c01.svg -evidence: - - ../../../final/evidence/raw/grpc-operation-ledger-jpa-c01.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-operation-ledger-jpa#L53 이다. -module: grpc-operation-ledger-jpa ---- - -# record를 지나지 않는 쓰기가 있어서 제약을 DB에도 둔다 - -마이그레이션 헤더가 왜 애플리케이션 검사가 아니라 제약인지 적는다. 마이그레이션·백필·지원 스크립트가 쓴 행은 record를 지나지 않기 때문이다. - -## 본문 - - - -마이그레이션 헤더가 왜 애플리케이션 검사가 아니라 제약인지 적는다. 커밋 행이 결과를 반드시 갖는다는 검사를 자바 record 와 DB 양쪽에 둔 이유도 적혀 있다 — 마이그레이션·백필·지원 스크립트가 쓴 행은 record 를 지나지 않는다. - -## 이 기록이 다루는 파일 범위 - -:::evidence key="grpc-operation-ledger-jpa-c01" alt="코드베이스에서 파일 목록을 만든 출력 3줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 3줄 · exit 0" zoom="true" -::: - -## 전용 Flyway 위치를 쓰는 이유 - -`db/migration/grpc`를 쓴다. gRPC 플랫폼을 채택하지 않은 배포가 이 테이블을 만들도록 강요받지 않기 위해서다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-grpc-operation-ledger-jpa-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-grpc-operation-ledger-jpa-c02.md deleted file mode 100644 index 2d7e700..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-grpc-operation-ledger-jpa-c02.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -kind: CONCEPT -slug: grpc-operation-ledger-jpa-c02 -title: 같은 신원의 두 번째 청구가 같은 행을 겨냥한다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:grpc-operation-ledger-jpa-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-operation-ledger-jpa-c02 - file: ../../../final/evidence/rendered/grpc-operation-ledger-jpa-c02.svg -evidence: - - ../../../final/evidence/raw/grpc-operation-ledger-jpa-c02.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-operation-ledger-jpa#L76 이다. -module: grpc-operation-ledger-jpa ---- - -# 같은 신원의 두 번째 청구가 같은 행을 겨냥한다 - -기본 키는 유니크 제약의 세 컬럼을 이어 붙인 파생값이다. 복합 쪽이 원자성을 주고 파생 키가 조회에 단일 컬럼 기본 키를 준다. - -## 본문 - - - -기본 키는 유니크 제약의 세 컬럼을 이어 붙인 파생값이다. - -```java -public String storageKey() { - return callerFingerprint + "|" + method.canonical() + "|" + idempotencyKeyHash; -} -``` - -## GrpcOperationIdentity 참조 위치 - -:::evidence key="grpc-operation-ledger-jpa-c02" alt="코드베이스에서 GrpcOperationIdentity 를 검색한 출력 19줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcOperationIdentity 코드베이스 검색 — 19줄 · exit 0" zoom="true" -::: - -## 이중 저장의 근거와 그 결과 - -엔티티 javadoc 이 그 이중 저장을 설명한다 — 복합 쪽이 원자성을 주고, 파생 키가 조회에 단일 컬럼 기본 키를 준다. 그래서 같은 신원의 두 번째 청구는 **같은 기본 키 행**을 겨냥한다. §17.1 이 그 사실에서 나온다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-grpc-operation-ledger-jpa-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-grpc-operation-ledger-jpa-c03.md deleted file mode 100644 index 8b29b3e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-grpc-operation-ledger-jpa-c03.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -kind: CONCEPT -slug: grpc-operation-ledger-jpa-c03 -title: 서수 컬럼은 값이 끼어들면 모든 행을 조용히 바꾼다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:grpc-operation-ledger-jpa-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-operation-ledger-jpa-c03 - file: ../../../final/evidence/rendered/grpc-operation-ledger-jpa-c03.svg - - key: grpc-operation-ledger-jpa-c03-diagram - file: ../../../final/assets/diagrams/grpc-operation-ledger-jpa-c03.svg -evidence: - - ../../../final/evidence/raw/grpc-operation-ledger-jpa-c03.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-operation-ledger-jpa#L108 이다. -module: grpc-operation-ledger-jpa ---- - -# 서수 컬럼은 값이 끼어들면 모든 행을 조용히 바꾼다 - -`IN_PROGRESS`에서만 전이할 수 있고 커밋은 결과 참조가 비면 거부한다. 상태 컬럼은 `EnumType.STRING`이다. - -## 본문 - - - -`IN_PROGRESS` 에서만 전이할 수 있다(`requireInProgress`). 커밋은 결과 참조가 비면 거부한다. - -## 원장의 전이 - -:::evidence key="grpc-operation-ledger-jpa-c03-diagram" alt="IN_PROGRESS 에서 COMMITTED 로 가는 실선 화살표와 FAILED 로 가는 점선 화살표가 있고 두 종착 상태에서 나가는 화살표는 없는 구조" caption="원장의 전이" zoom="false" -::: - -## EnumType 참조 위치 - -:::evidence key="grpc-operation-ledger-jpa-c03" alt="코드베이스에서 EnumType 를 검색한 출력 25줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="EnumType 코드베이스 검색 — 25줄 · exit 0" zoom="true" -::: - -## 상태 컬럼을 문자열로 두는 이유 - -`EnumType.STRING` 을 쓰는 이유가 javadoc 에 있다 — 서수 컬럼은 열거형에 값이 끼어들면 저장된 모든 행을 조용히 다른 값으로 만든다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-grpc-spring-boot-starter-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-grpc-spring-boot-starter-c02.md deleted file mode 100644 index 95bdf83..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-grpc-spring-boot-starter-c02.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -kind: CONCEPT -slug: grpc-spring-boot-starter-c02 -title: 한 번에 전부 모아 실패한다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:grpc-spring-boot-starter-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-spring-boot-starter-c02 - file: ../../../final/evidence/rendered/grpc-spring-boot-starter-c02.svg -evidence: - - ../../../final/evidence/raw/grpc-spring-boot-starter-c02.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-spring-boot-starter#L87 이다. -module: grpc-spring-boot-starter ---- - -# 한 번에 전부 모아 실패한다 - -시작 검증기가 다섯 갈래 규칙을 담고, 어긴 것을 하나씩 던지지 않고 한 번에 모아 실패한다 — "so a deployment learns the whole list in one restart." - -## 본문 - - - -javadoc 이 선정 기준을 적고, 규칙이 다섯 갈래다. - -- **전송·보안** — production 전송이 아니면 거부, 배포 환경에서 TLS 미사용·trust-all·반사 전체 공개 거부 -- **실행기** — 큐 용량 1 미만(무제한) 거부, 풀 크기 양수 요구 -- **메서드** — 단항인데 사용 가능한 마감이 0, 명시적 재시도가 멱등 프로파일과 모순, 멱등 키 필수인데 원장 비활성, Stable 범위 밖 RPC 종류 -- **채널** — Stable 스킴 요구, 두 재시도 소유자가 동시에 in-process 재시도 -- **고급 격리** — Stable 스타터가 advanced 의존을 끌면 위반 - -## 이 기록이 다루는 파일 범위 - -:::evidence key="grpc-spring-boot-starter-c02" alt="코드베이스에서 파일 목록을 만든 출력 4줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 4줄 · exit 0" zoom="true" -::: - -## 하나씩 던지지 않는 이유 - -그리고 한 번에 전부 모아 실패한다 — "so a deployment learns the whole list in one restart." - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-grpc-testkit-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-grpc-testkit-c01.md deleted file mode 100644 index 7e32c19..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-grpc-testkit-c01.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -kind: CONCEPT -slug: grpc-testkit-c01 -title: 런북을 나중에 쓰면 처음 만나는 사람이 새벽에 알아내야 한다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:grpc-testkit-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-testkit-c01 - file: ../../../final/evidence/rendered/grpc-testkit-c01.svg -evidence: - - ../../../final/evidence/raw/grpc-testkit-c01.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-testkit#L102 이다. -module: grpc-testkit ---- - -# 런북을 나중에 쓰면 처음 만나는 사람이 새벽에 알아내야 한다 - -릴리스 게이트가 문서 부재를 차단 사유로 삼는다. 차단 사유가 다섯 갈래다. - -## 본문 - - - -릴리스 게이트가 문서 부재를 차단 사유로 삼는 근거가 적혀 있다. - -> "A platform whose failure modes are `COMPLETION_UNKNOWN` and a stream that needs a full resync is a platform whose on-call has to be told what to do about them; shipping the behaviour and writing the runbook afterwards means the first person to meet it is the one who has to work it out at three in the morning." - -## 차단 사유 다섯 갈래 - -호환성 표의 누락 결과, 생산되지 않은 증거 등급, 스키마 발행 거부, 런북 부재, 결정 기록 부재, 지원 표 부재. - -## 이 기록이 다루는 파일 범위 - -:::evidence key="grpc-testkit-c01" alt="코드베이스에서 파일 목록을 만든 출력 25줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 25줄 · exit 0" zoom="true" -::: - -## 거절 메시지가 담는 것 - -`requireCertified` 는 능력이 이번 릴리스가 낸 증거로 인증되지 않으면 던지고, 메시지에 실제로 돈 등급을 나열한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-admin-api-c06.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-admin-api-c06.md deleted file mode 100644 index 51ea18b..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-admin-api-c06.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-admin-api-c06 -title: 실행 코드가 없어서 동시성 계약이 전부 문서다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-admin-api-c06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-admin-api-c06 - file: ../../../final/evidence/rendered/messaging-admin-api-c06.svg - - key: messaging-admin-api-c06-diagram - file: ../../../final/assets/diagrams/messaging-admin-api-c06.svg -evidence: - - ../../../final/evidence/raw/messaging-admin-api-c06.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-admin-api#L588 이다. -module: messaging-admin-api ---- - -# 실행 코드가 없어서 동시성 계약이 전부 문서다 - -이 리프에 실행 코드가 없으므로 동시성 계약은 전부 인터페이스 문서로 표현되어 있고, 강제는 구현 리프의 몫이다. - -## 본문 - - - -이 리프에 실행 코드가 없으므로 동시성 계약은 전부 **인터페이스 문서로 표현**되어 있고, 강제는 구현 리프의 몫이다. - -## 타입이 지는 것과 구현이 지는 것 - -:::evidence key="messaging-admin-api-c06-diagram" alt="리프 경계 안에 불변 타입과 펜싱 토큰 하한이 들어 있고 오래된 토큰 거절이 경계 밖 점선 상자로 놓인 구조" caption="타입이 지는 것과 구현이 지는 것" zoom="false" -::: - -펜싱 토큰의 하한이 타입으로 강제된다. `begin`/`checkpoint` 의 javadoc 이 각각 던져야 할 조건을 명시한다 — `begin` 은 "already completed, or another runtime holds a live lease", `checkpoint` 는 "the lease has been taken over by a newer token". 즉 **오래된 토큰의 쓰기를 거절하는 것이 구현 의무**로 문서화되어 있다. - -## DestructiveOperationGuard 참조 위치 - -:::evidence key="messaging-admin-api-c06" alt="코드베이스에서 DestructiveOperationGuard 를 검색한 출력 19줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DestructiveOperationGuard 코드베이스 검색 — 19줄 · exit 0" zoom="true" -::: - -## 공유해도 안전한 이유와 수명주기 훅 - -이 리프의 모든 타입은 record 이거나 불변 final 클래스다. `DestructiveOperationGuard` 는 `final boolean` 하나만 갖고, `HmacApprovalVerifier` 는 키를 clone 해 보관한다. 수명주기 훅은 없다 — `MessagingAdminDurabilityValidator`(스타터, `InitializingBean`)가 유일한 기동 시점 훅이며 이 리프 밖이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-admin-api-c07.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-admin-api-c07.md deleted file mode 100644 index 86c9546..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-admin-api-c07.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-admin-api-c07 -title: 승인이 처음에는 데이터였고 지금은 타입이다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-admin-api-c07 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-admin-api-c07 - file: ../../../final/evidence/rendered/messaging-admin-api-c07.svg -evidence: - - ../../../final/evidence/raw/messaging-admin-api-c07.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-admin-api#L802 이다. -module: messaging-admin-api ---- - -# 승인이 처음에는 데이터였고 지금은 타입이다 - -javadoc이 커밋 로그를 대신한다. 여섯 개의 "이전에는 이랬다" 기록이 전부 같은 결함 계열을 가리킨다 — 자기 자신을 근거로 삼는 주장. - -## 본문 - - - -`messaging-testkit` 과 마찬가지로 이 리프도 **javadoc 이 커밋 로그를 대신한다**. 여섯 개의 "이전에는 이랬다" 기록이 있고, 전부 같은 결함 계열을 가리킨다: **자기 자신을 근거로 삼는 주장.** - -| 위치 | 기록된 과거 결함 | -|---|---| -| `VerifiedApproval.java:9-13` | "…a plain `AdminApproval` record with a public constructor, so 'this plan was approved' was a claim the caller made about itself." | -| `ApprovalGrant.java:11-14` | "`AdminApproval` carried a ticket, an approver, and a window. Nothing in it said which operation… the audit trail recorded a ticket that proved nothing about what was executed." | -| `PlanDigest.java:12-16` | "The operator who got a redrive of one dead-letter destination approved could execute a redrive of a different one with the same ticket, and every audit record would look correct." | -| `AdminOperationJournal.java:10-14` | "…a `ConcurrentHashMap` registered by the starter as the default… both are worse than having no store at all because the map made the platform look protected." | -| `AdminOperationState.java:5-9` | "…recorded one fact — 'this approval was claimed' — and recorded it before any work happened." | - -## 이 기록이 다루는 파일 범위 - -:::evidence key="messaging-admin-api-c07" alt="코드베이스에서 파일 목록을 만든 출력 25줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 25줄 · exit 0" zoom="true" -::: - -## 일곱 기록이 하나의 이야기다 - -**승인이 처음에는 데이터였고, 지금은 타입이다.** 커밋 로그 자체는 정보가 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-admin-runtime-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-admin-runtime-c03.md deleted file mode 100644 index 0b6092a..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-admin-runtime-c03.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-admin-runtime-c03 -title: 리드라이브는 재개하고 리플레이는 처음부터 다시 읽는다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-admin-runtime-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-admin-runtime-c03 - file: ../../../final/evidence/rendered/messaging-admin-runtime-c03.svg - - key: messaging-admin-runtime-c03-diagram - file: ../../../final/assets/diagrams/messaging-admin-runtime-c03.svg -evidence: - - ../../../final/evidence/raw/messaging-admin-runtime-c03.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-admin-runtime#L163 이다. -module: messaging-admin-runtime ---- - -# 리드라이브는 재개하고 리플레이는 처음부터 다시 읽는다 - -리드라이브는 `lease.resumeFrom()`과 체크포인트 콜백을 실행 측에 넘기지만 리플레이는 넘기지 않는다. `ReplayService.replay(...)` 시그니처에 `resumeFrom`이 없다. - -## 본문 - - - -검사 순서가 요점이다. 승인이 아직 창 안인지, 계획 승인 이후 토폴로지가 바뀌지 않았는지, 승인이 이미 실행되지 않았는지를 순서대로 보고 그다음에야 무엇이 움직인다. 저널 항목은 작업 **전에** 쓴다 — 나중에 쓰면 첫 실행이 도는 중에 두 번째 실행이 시작되는 창이 생기고, 그것이 저널이 막으려는 이중 리드라이브다. - -## 재개가 붙는 쪽과 붙지 않는 쪽 - -:::evidence key="messaging-admin-runtime-c03-diagram" alt="실행 경계 안에 리드라이브가 들어 있고 리플레이가 경계 밖 빗금 상자로 놓인 구조" caption="재개가 붙는 쪽과 붙지 않는 쪽" zoom="false" -::: - -**리플레이와 리드라이브의 비대칭이 하나 있다.** 리드라이브는 `lease.resumeFrom()` 과 체크포인트 콜백을 실행 측에 넘기지만, 리플레이는 넘기지 않는다. `ReplayService.replay(...)` 시그니처에 `resumeFrom` 이 없다(`ReplayService.java:53-54`). 즉 리플레이는 리스를 받지만 재개하지 않는다 — 죽으면 처음부터 다시 읽는다. 클래스 javadoc 의 "a retry continues the same operation instead of either redoing it" 은 리드라이브에만 해당한다. - -## DestructiveOperationGuard 참조 위치 - -:::evidence key="messaging-admin-runtime-c03" alt="코드베이스에서 DestructiveOperationGuard 를 검색한 출력 19줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DestructiveOperationGuard 코드베이스 검색 — 19줄 · exit 0" zoom="true" -::: - -## 격리 리플레이가 dry run인 척으로 통과한다 - -`DestructiveOperationGuard.authorize` 는 `dryRun` 이 참이면 즉시 반환한다(`DestructiveOperationGuard.java:54-56`). 즉 격리 리플레이는 "승인 불필요" 가 아니라 "dry run 인 척" 으로 통과한다. 감사 이벤트는 그 구분을 남긴다 — `approval.map(VerifiedApproval::ticket).orElse("isolated")`(`:77`) — 그러나 guard 쪽에는 남지 않는다. §17 P3. - -## 세 수정 중 세 번째의 인덱스 계산 - -세 가지 실패를 고쳤다고 javadoc 이 적고, 세 수정이 코드에 있다. 발행 → 확인 → 정산 순서가 이 리프의 핵심 불변식이다. **세 번째 수정 — 재개 — 는 인덱스 계산이 틀렸다.** §12.1(a)에서 상술한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-admin-runtime-c06.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-admin-runtime-c06.md deleted file mode 100644 index 42d1c72..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-admin-runtime-c06.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-admin-runtime-c06 -title: 펜싱 토큰이 세 지점에서 작동한다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-admin-runtime-c06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-admin-runtime-c06 - file: ../../../final/evidence/rendered/messaging-admin-runtime-c06.svg - - key: messaging-admin-runtime-c06-diagram - file: ../../../final/assets/diagrams/messaging-admin-runtime-c06.svg -evidence: - - ../../../final/evidence/raw/messaging-admin-runtime-c06.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-admin-runtime#L493 이다. -module: messaging-admin-runtime ---- - -# 펜싱 토큰이 세 지점에서 작동한다 - -트랜잭션 경계 없이 `ConcurrentHashMap.compute(...)`로 키 단위 원자성을 얻고, 펜싱 토큰이 인수·쓰기·clamp 세 지점에서 작동한다. - -## 본문 - - - -트랜잭션 경계 없음 — `InMemoryAdminOperationJournal` 은 `ConcurrentHashMap.compute(...)` 로 키 단위 원자성을 얻는다(`:44, :198`). `begin` 의 검사-후-갱신 전체가 `compute` 람다 안에 있어 두 복제본이 동시에 `begin` 해도 하나만 성공한다. `AdminOperationJournalTest.twoReplicasRacingProduceExactlyOneLease` 가 그것을 검증한다. - -## 펜싱 토큰이 동작하는 세 지점 - -:::evidence key="messaging-admin-runtime-c06-diagram" alt="저널 경계 안에 인수 시 토큰 증가와 쓰기 시 토큰 대조와 clamp 로 단조성 유지 세 상자가 나란히 들어 있는 구조" caption="펜싱 토큰이 동작하는 세 지점" zoom="false" -::: - -인수 시 `existing.leaseToken() + 1`(`:110`), 쓰기 시 토큰 대조(`:206`), 그리고 clamp 로 인한 단조성(`:128, :147, :167`). `aRuntimeThatLostItsLeaseCannotWriteOverTheSuccessor` 가 세 가지를 한 번에 확인한다 — 낡은 리스의 `complete(30)` 이 거절되고 기록은 45·STARTED 로 남는다. - -## InMemoryAdminOperationJournal 참조 위치 - -:::evidence key="messaging-admin-runtime-c06" alt="코드베이스에서 InMemoryAdminOperationJournal 를 검색한 출력 6줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="InMemoryAdminOperationJournal 코드베이스 검색 — 6줄 · exit 0" zoom="true" -::: - -## 불변 필드와 외부화된 시간 - -`RedriveService`·`ReplayService`·`DefaultMessagingAdminService` 는 모두 불변 필드만 갖는다. `clock` 을 `Supplier` 로 주입받아 시간도 외부화되어 있다. - -## 수명주기 훅이 없다 - -이 리프의 어떤 클래스도 `InitializingBean`·`SmartLifecycle` 을 구현하지 않는다 — 이것이 §17 첫 항목의 직접 원인이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-admin-runtime-c07.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-admin-runtime-c07.md deleted file mode 100644 index 37c217e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-admin-runtime-c07.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-admin-runtime-c07 -title: 크래시와 복제본을 고려하지 않은 admin 평면 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-admin-runtime-c07 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-admin-runtime-c07 - file: ../../../final/evidence/rendered/messaging-admin-runtime-c07.svg -evidence: - - ../../../final/evidence/raw/messaging-admin-runtime-c07.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-admin-runtime#L772 이다. -module: messaging-admin-runtime ---- - -# 크래시와 복제본을 고려하지 않은 admin 평면 - -javadoc이 이력을 대신한다. 다섯 개의 "이전에는 이랬다"가 전부 분산 실행의 실패를 가리킨다. - -## 본문 - - - -이 리프도 javadoc 이 이력을 대신한다. 다섯 개의 "이전에는 이랬다" 가 있고 전부 **분산 실행의 실패**를 가리킨다. 다섯이 하나의 이야기다 — **크래시와 복제본을 고려하지 않은 admin 평면**. 고친 결과가 리스·펜싱·체크포인트·per-item 경계다. - -## 이 기록이 다루는 파일 범위 - -:::evidence key="messaging-admin-runtime-c07" alt="코드베이스에서 파일 목록을 만든 출력 12줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 12줄 · exit 0" zoom="true" -::: - -## 마지막 두 항목이 이어지는 곳 - -"재시도가 처음부터 다시 시작하는" 문제를 고치려고 `resumeFrom` 을 도입했고, 도입한 지점의 인덱스 계산이 실패분을 고려하지 않았다 — §12.1(a). 커밋 로그는 정보가 없다(4개, messaging 전체 공통). - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-claim-check-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-claim-check-c01.md deleted file mode 100644 index 764a3a4..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-claim-check-c01.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-claim-check-c01 -title: 위협은 실패가 아니라 잘못된 성공이다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-claim-check-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-claim-check-c01 - file: ../../../final/evidence/rendered/messaging-claim-check-c01.svg - - key: messaging-claim-check-c01-diagram - file: ../../../final/assets/diagrams/messaging-claim-check-c01.svg -evidence: - - ../../../final/evidence/raw/messaging-claim-check-c01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-claim-check#L57 이다. -module: messaging-claim-check ---- - -# 위협은 실패가 아니라 잘못된 성공이다 - -브로커 한계를 넘는 payload를 객체 저장소에 두고 메시지는 참조만 나른다. 위협 모델은 "decode perfectly into the wrong object"다. - -## 관계 - -- **leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 튜닝 값이 두 계층에 있으면 어느 쪽이 이기는지 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -**Claim Check 패턴** — 브로커 한계를 넘는 payload를 객체 저장소에 두고 메시지는 참조만 나른다. `messaging-reliability-api`의 `ClaimCheckReference`(storageKey·sizeBytes·sha256·expiresAt)를 값 타입으로 쓰고, 이 leaf가 그것을 만들고 검증하는 동작을 소유한다. - -## payload가 지나는 경로 - -:::evidence key="messaging-claim-check-c01-diagram" alt="payload 와 객체 저장소와 참조와 메시지가 왼쪽에서 오른쪽으로 이어지고 화살표마다 무엇이 넘어가는지 붙은 구조" caption="payload 가 지나는 경로" zoom="false" -::: - -## ClaimCheckReference 참조 위치 - -:::evidence key="messaging-claim-check-c01" alt="코드베이스에서 ClaimCheckReference 를 검색한 출력 33줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ClaimCheckReference 코드베이스 검색 — 33줄 · exit 0" zoom="true" -::: - -## 이 리프의 위협 모델 - -경계 진술이 두 클래스에 있다. **"decode perfectly into the wrong object"**가 이 leaf의 위협 모델이다 — 실패가 아니라 잘못된 성공. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-claim-check-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-claim-check-c02.md deleted file mode 100644 index 87c5e56..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-claim-check-c02.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-claim-check-c02 -title: 싣고 쓰지 않는다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-claim-check-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-claim-check-c02 - file: ../../../final/evidence/rendered/messaging-claim-check-c02.svg -evidence: - - ../../../final/evidence/raw/messaging-claim-check-c02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-claim-check#L85 이다. -module: messaging-claim-check ---- - -# 싣고 쓰지 않는다 - -`ClaimCheckStore`의 production 구현이 0이고 세 타입 생성이 leaf 밖에서 0건인데, `runtime_memberships`는 `["app-bootstrap"]`이다. - -## 관계 - -- **leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 튜닝 값이 두 계층에 있으면 어느 쪽이 이기는지 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -들어오는 것: `messaging-core-api`(api), `messaging-reliability-api`(api). 나가는 것: `messaging-spring-boot-starter`의 `allowed_dependencies`에 포함된다. - -## ClaimCheckStore 참조 위치 - -:::evidence key="messaging-claim-check-c02" alt="코드베이스에서 ClaimCheckStore 를 검색한 출력 6줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ClaimCheckStore 코드베이스 검색 — 6줄 · exit 0" zoom="true" -::: - -## 배선은 없는데 아티팩트에는 실린다 - -**배선: 없다.** `ClaimCheckStore`의 production 구현이 0이고(유일한 구현은 테스트의 `FakeStore`), `ClaimCheckPublisher`·`ClaimCheckResolver`·`ClaimCheckPolicy` 생성이 leaf 밖에서 0건이다. 그런데 **`runtime_memberships`가 `["app-bootstrap"]`이다.** starter closure를 통해 배포 아티팩트에 실린다. `messaging-cloudevents`와 같은 조합이다 — **싣고 쓰지 않는다**(`final/document.md#a19-messaging-cloudevents` §12.1). - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-claim-check-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-claim-check-c03.md deleted file mode 100644 index ba7ae07..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-claim-check-c03.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-claim-check-c03 -title: 보존이 생성자 불변식이고 삭제를 부르는 쪽이 없다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-claim-check-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-claim-check-c03 - file: ../../../final/evidence/rendered/messaging-claim-check-c03.svg - - key: messaging-claim-check-c03-diagram - file: ../../../final/assets/diagrams/messaging-claim-check-c03.svg -evidence: - - ../../../final/evidence/raw/messaging-claim-check-c03.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-claim-check#L124 이다. -module: messaging-claim-check ---- - -# 보존이 생성자 불변식이고 삭제를 부르는 쪽이 없다 - -`ClaimCheckPolicy`는 보존이 브로커 보존 + 전체 재시도·DLQ 창을 넘지 않으면 생성자가 거부한다. 그런데 `ClaimCheckStore.delete`를 부르는 쪽이 이 리프에 없다. - -## 관계 - -- **leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 튜닝 값이 두 계층에 있으면 어느 쪽이 이기는지 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -보존이 생성자 불변식이다. `retention`이 `brokerRetention.plus(maxRedeliveryWindow)`보다 짧으면 `CLAIM_CHECK_RETENTION_TOO_SHORT`로 던진다. javadoc이 이유를 적는다. - -> "A claim check object deleted while its message is still deliverable turns a large message into an undeliverable one — the consumer fetches, gets nothing, and the message dead-letters for a reason that has nothing to do with the message." - -**이것이 `messaging-reliability-api`의 `InboxRepository.purgeProcessedBefore` javadoc이 요구하고 강제하지 않는 것과 같은 형태의 규칙인데, 이쪽은 생성자가 강제한다.** - -## 이 리프가 하는 것과 하지 않는 것 - -:::evidence key="messaging-claim-check-c03-diagram" alt="리프 경계 안에 오프로드와 참조 전달과 무결성 검증이 들어 있고 보존 sweep 이 경계 밖 빗금 상자로 놓인 구조" caption="이 리프가 하는 것과 하지 않는 것" zoom="false" -::: - -**`ClaimCheckStore.delete`가 이 leaf에서 호출되지 않는다.** 인터페이스에 선언돼 있고 publisher가 의도적으로 안 부른다("Nothing here deletes on failure") — `REJECTED`와 `AMBIGUOUS`를 구분할 수 없는 시점에 삭제하면 모호한 발행의 payload를 지운다. 보존 sweep이 부를 것을 전제하는데 그 sweep이 이 leaf에 없다. - -## InboxRepository 참조 위치 - -:::evidence key="messaging-claim-check-c03" alt="코드베이스에서 InboxRepository 를 검색한 출력 20줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="InboxRepository 코드베이스 검색 — 20줄 · exit 0" zoom="true" -::: - -## 기본값이 주는 여유 - -`defaults()`가 브로커 1일 보존 + 1일 재시도 경로에 대해 3일 보존을 준다 — 요구치(2일)보다 1일 여유. `DEFAULT_THRESHOLD_BYTES = 262,144` = 1 MiB의 1/4이고 javadoc이 그렇게 부른다. - -## 복사와 검사 순서 - -오프로드된 메시지는 payload를 **아예 갖지 않는다**. `Offloaded` record가 양방향 방어 복사를 한다(생성자 `payload.clone()`, 접근자 `payload.clone()`) — `EncodedMessage`(schema-api)·`OutboxRecord`(reliability-api)와 같은 패턴이다. 크기 검사가 digest보다 먼저인 것이 합리적이다 — 크기 불일치는 SHA-256 계산 없이 즉시 판정된다. `sha256(byte[])`가 `HexFormat.of().formatHex(...)`로 **소문자** hex를 만들고 `ClaimCheckReference`의 정규식이 `[a-f0-9]{64}`이므로 두 쪽이 맞는다. `verify`가 검증된 payload의 **복사본**을 반환한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-claim-check-c04.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-claim-check-c04.md deleted file mode 100644 index 7a079ef..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-claim-check-c04.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-claim-check-c04 -title: 두 경로 모두 production에서 호출되지 않는다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-claim-check-c04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-claim-check-c04 - file: ../../../final/evidence/rendered/messaging-claim-check-c04.svg -evidence: - - ../../../final/evidence/raw/messaging-claim-check-c04.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-claim-check#L260 이다. -module: messaging-claim-check ---- - -# 두 경로 모두 production에서 호출되지 않는다 - -발행과 소비 두 경로가 구현돼 있지만 production 호출자가 없다(§12.1). - -## 관계 - -- **leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 튜닝 값이 두 계층에 있으면 어느 쪽이 이기는지 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -**발행** — `publisher.offload(encodedPayload)` → 문턱 이하면 인라인 → 초과면 `store.put` → `Offloaded(빈 바이트, reference)`. - -**소비** — `resolver.resolve(inline, reference, now)` → reference 없으면 인라인 → 만료 확인 → `store.get` → null이면 NOT_FOUND → `guard.verify`(만료·크기·digest) → `_MISMATCH`면 `ClaimCheckIntegrityException`. - -## ClaimCheckIntegrityException 참조 위치 - -:::evidence key="messaging-claim-check-c04" alt="코드베이스에서 ClaimCheckIntegrityException 를 검색한 출력 8줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ClaimCheckIntegrityException 코드베이스 검색 — 8줄 · exit 0" zoom="true" -::: - -## 두 경로 다 production 호출자가 없다 - -§12.1. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-claim-check-c05.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-claim-check-c05.md deleted file mode 100644 index e295ea2..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-claim-check-c05.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-claim-check-c05 -title: 만료와 불일치를 다른 카테고리로 가른다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-claim-check-c05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-claim-check-c05 - file: ../../../final/evidence/rendered/messaging-claim-check-c05.svg -evidence: - - ../../../final/evidence/raw/messaging-claim-check-c05.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-claim-check#L270 이다. -module: messaging-claim-check ---- - -# 만료와 불일치를 다른 카테고리로 가른다 - -분류가 두 단계로 정확하다 — 만료·부재는 운영 문제(`PERMANENT_BUSINESS`), 크기·digest 불일치는 오염(`POISON_MESSAGE`). - -## 관계 - -- **leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 튜닝 값이 두 계층에 있으면 어느 쪽이 이기는지 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -**분류가 두 단계로 정확하다.** 만료·부재는 운영 문제(`PERMANENT_BUSINESS`), 크기·digest 불일치는 오염(`POISON_MESSAGE`). 두 예외 클래스와 두 카테고리가 그 구분을 담는다. - -## ClaimCheckIntegrityGuard 참조 위치 - -:::evidence key="messaging-claim-check-c05" alt="코드베이스에서 ClaimCheckIntegrityGuard 를 검색한 출력 11줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ClaimCheckIntegrityGuard 코드베이스 검색 — 11줄 · exit 0" zoom="true" -::: - -## 환경 문제를 메시지 실패로 만들지 않는다 - -`ClaimCheckIntegrityGuard.sha256`이 `NoSuchAlgorithmException`을 `IllegalStateException("Java runtime does not provide SHA-256")`으로 감싼다 — 복구 불가능한 환경 문제이므로 메시지 실패가 아니다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-claim-check-c06.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-claim-check-c06.md deleted file mode 100644 index c387f4b..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-claim-check-c06.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-claim-check-c06 -title: MessageDigest를 호출마다 새로 만드는 것이 옳다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-claim-check-c06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-claim-check-c06 - file: ../../../final/evidence/rendered/messaging-claim-check-c06.svg -evidence: - - ../../../final/evidence/raw/messaging-claim-check-c06.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-claim-check#L286 이다. -module: messaging-claim-check ---- - -# MessageDigest를 호출마다 새로 만드는 것이 옳다 - -`ClaimCheckPublisher`·`ClaimCheckResolver`는 final 필드만 갖는 불변 객체이고, `MessageDigest.getInstance("SHA-256")`은 호출마다 새 인스턴스를 만든다. - -## 관계 - -- **leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 튜닝 값이 두 계층에 있으면 어느 쪽이 이기는지 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -`ClaimCheckPublisher`·`ClaimCheckResolver`는 final 필드만 갖는 불변 객체다. `ClaimCheckIntegrityGuard`는 상태가 없고 `ClaimCheckResolver`가 인스턴스를 필드로 하나 만든다. - -## ClaimCheckPublisher 참조 위치 - -:::evidence key="messaging-claim-check-c06" alt="코드베이스에서 ClaimCheckPublisher 를 검색한 출력 12줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ClaimCheckPublisher 코드베이스 검색 — 12줄 · exit 0" zoom="true" -::: - -## 호출마다 새로 만드는 것이 옳은 이유 - -`MessageDigest.getInstance("SHA-256")`이 **호출마다** 새 인스턴스를 만든다 — `MessageDigest`는 스레드 안전하지 않으므로 이것이 옳다. 재사용했다면 동시 호출이 서로의 상태를 오염시킨다. - -## 계약에 적히지 않은 요구 - -`ClaimCheckStore` 구현의 스레드 안전성 요구는 인터페이스 javadoc에 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-claim-check-c07.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-claim-check-c07.md deleted file mode 100644 index 98f8a95..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-claim-check-c07.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-claim-check-c07 -title: 테스트에서는 드물고 부하에서는 일상인 것 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-claim-check-c07 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-claim-check-c07 - file: ../../../final/evidence/rendered/messaging-claim-check-c07.svg -evidence: - - ../../../final/evidence/raw/messaging-claim-check-c07.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-claim-check#L436 이다. -module: messaging-claim-check ---- - -# 테스트에서는 드물고 부하에서는 일상인 것 - -이 leaf의 javadoc은 이전 결함을 서술하지 않고 막으려는 사고를 서술한다. - -## 관계 - -- **leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 튜닝 값이 두 계층에 있으면 어느 쪽이 이기는지 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -이 leaf의 javadoc은 이전 결함을 서술하지 않는다 — 대신 **막으려는 사고**를 서술한다. - -| 위치 | 막으려는 것 | -|---|---| -| `ClaimCheckPublisher` | 발행 후 저장 순서 → 존재하지 않는 객체의 참조를 소비자가 받음. "rare in a test and routine under load" | -| `ClaimCheckPublisher` | 실패 시 즉시 삭제 → 모호한 발행의 payload를 지움 | -| `ClaimCheckResolver` | 검증 없는 fetch → 잘못된 키가 완벽히 디코딩되는 다른 객체를 반환 | -| `ClaimCheckResolver` | fetch 후 만료 확인 → sweep이 늦은 저장소에서 오설정이 숨음 | -| `ClaimCheckPolicy` | 짧은 보존 → 메시지와 무관한 이유로 dead-letter | -| `ClaimCheckIntegrityException` | 만료와 불일치를 한 진단으로 합침 | - -## 이 기록이 다루는 파일 범위 - -:::evidence key="messaging-claim-check-c07" alt="코드베이스에서 파일 목록을 만든 출력 6줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 6줄 · exit 0" zoom="true" -::: - -## 같은 형태가 반복되는 곳 - -**"rare in a test and routine under load"**가 이 저장소 전반의 주제다 — `messaging-observability`의 카디널리티, `messaging-security`의 회전 경합, `messaging-transport-spi`의 자원 누수가 같은 형태다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-cloudevents-c07.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-cloudevents-c07.md deleted file mode 100644 index 28d8f80..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-cloudevents-c07.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-cloudevents-c07 -title: 상태가 없다는 사실이 javadoc에는 적혀 있지 않다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-cloudevents-c07 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-cloudevents-c07 - file: ../../../final/evidence/rendered/messaging-cloudevents-c07.svg -evidence: - - ../../../final/evidence/raw/messaging-cloudevents-c07.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-cloudevents#L321 이다. -module: messaging-cloudevents ---- - -# 상태가 없다는 사실이 javadoc에는 적혀 있지 않다 - -`DefaultCloudEventMapper`는 필드가 상수 하나뿐이고 모든 메서드가 인자만 쓴다. 스레드 안전하지만 그 사실이 문서에 없다. - -## 관계 - -- **왕복이라 부르는 테스트는 무엇을 보존하지 않는지도 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **문서가 범위를 제한하면 그 제한을 누가 강제하는지 같이 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -`DefaultCloudEventMapper`는 **상태가 없다** — 필드가 `SPEC_CONTENT_TYPE_FALLBACK` 상수 하나뿐이고 모든 메서드가 인자만 쓴다. 스레드 안전하다. - -## DefaultCloudEventMapper 참조 위치 - -:::evidence key="messaging-cloudevents-c07" alt="코드베이스에서 DefaultCloudEventMapper 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DefaultCloudEventMapper 코드베이스 검색 — 2줄 · exit 0" zoom="true" -::: - -## 그 사실이 문서에 없다 - -스레드 안전하다는 사실이 javadoc에 적혀 있지 않다. `CloudEventExtensions`는 상수 홀더이고 private 생성자를 갖는다. `CloudEventBuilder`는 호출마다 새로 만들어진다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-core-api-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-core-api-c01.md deleted file mode 100644 index f5771ca..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-core-api-c01.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-core-api-c01 -title: 무엇이 아닌가가 무엇인가만큼 중요하다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-core-api-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-core-api-c01 - file: ../../../final/evidence/rendered/messaging-core-api-c01.svg - - key: messaging-core-api-c01-diagram - file: ../../../final/assets/diagrams/messaging-core-api-c01.svg -evidence: - - ../../../final/evidence/raw/messaging-core-api-c01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-core-api#L70 이다. -module: messaging-core-api ---- - -# 무엇이 아닌가가 무엇인가만큼 중요하다 - -85개 타입 중 실행 가능한 로직은 넷뿐이고, `build.gradle` 의존 블록은 비어 있으며 `java.*`와 자기 패키지 밖 import가 0개다. - -## 본문 - - - -이 leaf는 **브로커 중립 공개 계약**을 소유한다. 여기에는 구현이 거의 없다 — 85개 타입 중 인터페이스 11개, enum 12개, record 46개, 유틸리티 final class 5개, 예외 26개이고, 실행 가능한 로직은 `UuidV7.next()`, `WireSafeText.require`, `MessageHeaders.validateAndCopy`, 그리고 record 생성자의 검증뿐이다. - -## 경계가 그어진 방향 - -:::evidence key="messaging-core-api-c01-diagram" alt="리프 경계 안에 논리 목적지 이름과 완료 단계 계약이 들어 있고 물리 주소와 프레임워크 어휘가 경계 밖 빗금 상자로 놓인 구조" caption="경계가 그어진 방향" zoom="false" -::: - -`build.gradle`의 의존 블록은 비어 있고, `src/main/java` 전체에서 `java.*`와 자기 패키지 밖 import는 **0개**다(`evidence/raw/269` §F). Spring도, Kafka·AMQP 클라이언트도, Reactor도 없다. 이것은 우연이 아니라 원래 계획이 명시한 제약이고, 현재 소스에서 재측정해도 참이다. - -## WireSafeText 참조 위치 - -:::evidence key="messaging-core-api-c01" alt="코드베이스에서 WireSafeText 를 검색한 출력 11줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="WireSafeText 코드베이스 검색 — 11줄 · exit 0" zoom="true" -::: - -## 브로커 쪽으로 그은 선 - -`MessageDestination`은 논리 이름·카탈로그 타입·payload 클래스만 갖고 topic/exchange/queue/subject를 갖지 않는다(`destination/MessageDestination.java:9-11`). `DestinationName`의 패턴 `[a-z0-9][a-z0-9.-]{0,159}`은 `:`과 `/`와 공백을 배제해서 `topic://orders` 같은 물리 주소를 논리 이름으로 밀어 넣는 것을 생성자에서 막는다(`destination/DestinationName.java:16`). 주석이 이유를 적는다 — "otherwise the physical mapping owned by the destination profile could be bypassed from application code." - -## 프로그래밍 모델 쪽으로 그은 선 - -핵심 계약은 `CompletionStage`다. blocking facade(`BlockingMessagePublisher`)는 인터페이스만 여기 두고 구현을 다른 모듈로 밀어냈으며, Reactor facade는 아예 없다(`publish/MessagePublisher.java:10-11`). - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-core-api-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-core-api-c03.md deleted file mode 100644 index c71a8e5..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-core-api-c03.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-core-api-c03 -title: 예약 네임스페이스를 봉투 필드와 헤더 전용으로 쪼갠다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-core-api-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-core-api-c03 - file: ../../../final/evidence/rendered/messaging-core-api-c03.svg -evidence: - - ../../../final/evidence/raw/messaging-core-api-c03.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-core-api#L123 이다. -module: messaging-core-api ---- - -# 예약 네임스페이스를 봉투 필드와 헤더 전용으로 쪼갠다 - -`MessageEnvelope`가 중심이고 나머지 11개가 그 필드 타입이다. `ReservedHeaders`의 23개 이름을 `CanonicalEnvelopeHeaders`가 둘로 쪼갠다. - -## 본문 - - - -`MessageEnvelope`가 중심이고 나머지 11개가 그 필드 타입이다. 봉투는 불변이고 네 가지 파생 메서드가 있다 — `withPayload`, `withContentType`, `withTenant`, `withHeaders`. 넷 다 `messageId`를 복사한다. - -> "Encoding, decoding, Claim Check offloading, and DLQ forwarding all need this, and every one of them must keep `messageId()` intact — which is exactly what this method guarantees by construction" (`MessageEnvelope.java:80-82`) - -## UuidV7 참조 위치 - -:::evidence key="messaging-core-api-c03" alt="코드베이스에서 UuidV7 를 검색한 출력 20줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="UuidV7 코드베이스 검색 — 20줄 · exit 0" zoom="true" -::: - -## 예약 네임스페이스가 둘로 쪼개진다 - -`ReservedHeaders`는 23개 이름 상수와 `msg.` **prefix 전체**를 소유한다. `CanonicalEnvelopeHeaders`는 그 예약 네임스페이스를 둘로 쪼갠다 — 봉투 필드가 이미 갖고 있는 15개(`ENVELOPE_FIELDS`)와, 봉투에 대응 필드가 없어서 헤더로만 이동할 수 있는 나머지 8개(`REDRIVE_ID`, `REDRIVE_COUNT`, `RETRY_ATTEMPT`, `FIRST_FAILURE_AT`, `LAST_FAILURE_AT`, `FAILURE_CATEGORY`, `FAILURE_CODE`, `ORIGIN_DESTINATION`). - -## 목적지 쪽 어휘 - -`MessageDestination`, `DestinationName`, `DestinationKind`(7), `MessagingCapabilities`(boolean 12), `DestinationCapabilities`, `ConfirmationRequirement`(3), `CapabilityRegistry`. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-core-api-c07.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-core-api-c07.md deleted file mode 100644 index 55d61d3..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-core-api-c07.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-core-api-c07 -title: 동시성 지점이 하나뿐이다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-core-api-c07 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-core-api-c07 - file: ../../../final/evidence/rendered/messaging-core-api-c07.svg -evidence: - - ../../../final/evidence/raw/messaging-core-api-c07.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-core-api#L463 이다. -module: messaging-core-api ---- - -# 동시성 지점이 하나뿐이다 - -트랜잭션 개념은 선언으로만 등장하고 DB 트랜잭션은 이 리프가 만지지 않는다. 동시성 지점은 `UuidV7.STATE` 하나다. - -## 본문 - - - -트랜잭션 개념이 이 leaf에는 두 가지 형태로만 등장하고 둘 다 **선언**이다 — `MessagingCapabilities.brokerTransaction`, `ProcessingGuarantee.BROKER_TRANSACTIONAL`("Atomicity holds only inside the transaction scope the broker itself defines"), `ExternalSideEffectGuarantee.INBOX_TRANSACTIONAL`("An Inbox row and the side effect commit inside the same database transaction"). DB 트랜잭션은 이 leaf가 만지지 않는다. - -## MessagingCapabilities 참조 위치 - -:::evidence key="messaging-core-api-c07" alt="코드베이스에서 MessagingCapabilities 를 검색한 출력 39줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingCapabilities 코드베이스 검색 — 39줄 · exit 0" zoom="true" -::: - -## 유일한 동시성 지점 - -`UuidV7.STATE`(`AtomicLong`) 하나다. `updateAndGet`이 CAS 루프이므로 다중 스레드에서도 각 호출이 서로 다른 packed state를 얻는다. `RANDOM`(`SecureRandom`)은 thread-safe다. `MessageHeaders`는 생성 시 `LinkedHashMap`에 복사하고 `Collections.unmodifiableMap`으로 감싸 반환하므로 공유 안전하다. - -## 최대 예순넷이라 실용상 문제가 아닌 선형 탐색 - -`find(String)`이 `values.entrySet().stream()` 선형 탐색이다 — 최대 64개이므로 실용상 문제는 아니지만 hot path에서 반복 호출되면 O(n)이다. - -## 도달 불가능한 수명주기 필드 - -수명주기 개념은 `DeliveryContext.shutdownRequested`뿐이고, javadoc이 목적을 적는다 — "during a graceful drain the platform stops creating new retry attempts, and a long-running handler that can wind down early shortens the drain instead of being cancelled at the deadline." **이 필드는 production에서 도달 불가능하다**(§12.1). - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-core-api-c08.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-core-api-c08.md deleted file mode 100644 index d9f9cd2..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-core-api-c08.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-core-api-c08 -title: 검사가 없었던 게 아니라 잘못된 단위로 되어 있었다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-core-api-c08 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-core-api-c08 - file: ../../../final/evidence/rendered/messaging-core-api-c08.svg -evidence: - - ../../../final/evidence/raw/messaging-core-api-c08.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-core-api#L734 이다. -module: messaging-core-api ---- - -# 검사가 없었던 게 아니라 잘못된 단위로 되어 있었다 - -코드 주석이 보존한 실패 이력이 이 leaf의 가장 밀도 높은 사료다. 13개 이상의 wire 경계 결함을 한 번에 정리한 흔적이다. - -## 본문 - - - -이 leaf를 건드린 커밋은 4개다. 최초 커밋 메시지는 **24개 leaf**라고 적었고 현재 registry의 messaging leaf는 **25개**다. 이후 커밋에서 하나가 늘었다는 뜻이며, 커밋 메시지는 그 시점의 사실이므로 drift로 분류하지 않는다. - -## 이 기록이 다루는 파일 범위 - -:::evidence key="messaging-core-api-c08" alt="코드베이스에서 파일 목록을 만든 출력 25줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 25줄 · exit 0" zoom="true" -::: - -## 코드 주석이 보존한 실패 이력 - -**코드 주석이 보존한 실패 이력**이 이 leaf의 가장 밀도 높은 사료다. 전부 "예전에는 이랬고 그래서 무엇이 깨졌다"를 현재 코드가 직접 적어 둔 것이다. 이 목록 자체가 이 leaf의 성격을 말한다 — **13개 이상의 wire 경계 결함을 한 번에 정리한 흔적**이고, 대부분이 "검사가 없었다"가 아니라 "검사가 잘못된 단위(문자 vs 바이트, 정확일치 vs 세그먼트, 이름목록 vs prefix)로 되어 있었다"이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-inbox-jdbc-postgresql-c04.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-inbox-jdbc-postgresql-c04.md deleted file mode 100644 index b6cffcf..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-inbox-jdbc-postgresql-c04.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-inbox-jdbc-postgresql-c04 -title: action 예외가 예약까지 함께 롤백시킨다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-inbox-jdbc-postgresql-c04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-inbox-jdbc-postgresql-c04 - file: ../../../final/evidence/rendered/messaging-inbox-jdbc-postgresql-c04.svg -evidence: - - ../../../final/evidence/raw/messaging-inbox-jdbc-postgresql-c04.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-inbox-jdbc-postgresql#L319 이다. -module: messaging-inbox-jdbc-postgresql ---- - -# action 예외가 예약까지 함께 롤백시킨다 - -수신 처리와 실패와 보존 세 경로가 있고, 실패 경로에서 롤백이 예약도 함께 되돌린다. - -## 관계 - -- **컬럼 폭은 애플리케이션 검증과 짝을 이룬다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 안전 규칙은 한 공식과 한 강제 시점을 갖는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -**수신 처리** — `handleOnce(name, delivery, action)` → `consumer.runOnce(messageId, name, now, () -> { action.apply(delivery); return APPLIED; })` → runner가 트랜잭션 열기 → `repository.reserve(...)` → 세 검사 → `INSERT … ON CONFLICT DO NOTHING` → 1행이면 부작용 실행, 0행이면 `duplicate()` → 커밋 → `HandleResult.success()`. - -**실패** — action 예외 → `ActionFailedException` → runner가 롤백(예약도 함께) → `HandleResult.Retry("INBOX_ACTION_FAILED")`. - -**보존** — `cleanupJob.runOnce(now)` → `policy.cutoff(now)` → 무제한 DELETE 1회 → 두 번째 호출 0 → 종료. - -## ActionFailedException 참조 위치 - -:::evidence key="messaging-inbox-jdbc-postgresql-c04" alt="코드베이스에서 ActionFailedException 를 검색한 출력 4줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ActionFailedException 코드베이스 검색 — 4줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-inbox-jdbc-postgresql-c05.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-inbox-jdbc-postgresql-c05.md deleted file mode 100644 index 2d2d3a4..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-inbox-jdbc-postgresql-c05.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-inbox-jdbc-postgresql-c05 -title: 일시적 인프라 문제가 재시도 불가로 분류된다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-inbox-jdbc-postgresql-c05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-inbox-jdbc-postgresql-c05 - file: ../../../final/evidence/rendered/messaging-inbox-jdbc-postgresql-c05.svg -evidence: - - ../../../final/evidence/raw/messaging-inbox-jdbc-postgresql-c05.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-inbox-jdbc-postgresql#L329 이다. -module: messaging-inbox-jdbc-postgresql ---- - -# 일시적 인프라 문제가 재시도 불가로 분류된다 - -SQL 실패 셋이 전부 `MessagingConfigurationException`이고 그 카테고리는 `CONFIGURATION`, `retryable = false`다. 그런데 `SQLException`의 원인은 대부분 일시적 인프라 문제다. - -## 관계 - -- **컬럼 폭은 애플리케이션 검증과 짝을 이룬다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 안전 규칙은 한 공식과 한 강제 시점을 갖는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -**SQL 실패 셋이 전부 `MessagingConfigurationException`이다.** 그 예외의 카테고리는 `CONFIGURATION`이고 `retryable = false`다. 그런데 `SQLException`의 원인은 대부분 **일시적 인프라 문제**(연결 끊김, 데드락, 타임아웃)다. 즉 재시도 가능한 실패가 재시도 불가로 분류된다. §17. - -## MessagingConfigurationException 참조 위치 - -:::evidence key="messaging-inbox-jdbc-postgresql-c05" alt="코드베이스에서 MessagingConfigurationException 를 검색한 출력 19줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingConfigurationException 코드베이스 검색 — 19줄 · exit 0" zoom="true" -::: - -## 분류가 정확한 하나 - -`INBOX_ACTION_FAILED`만 `TRANSIENT_INFRASTRUCTURE`/`retryable = true`이고 예외가 아니라 `HandleResult`로 흐른다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-inbox-jdbc-postgresql-c06.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-inbox-jdbc-postgresql-c06.md deleted file mode 100644 index ded6034..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-inbox-jdbc-postgresql-c06.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-inbox-jdbc-postgresql-c06 -title: 커넥션 조회가 새 커넥션을 열기 전에 검사가 돌아야 한다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-inbox-jdbc-postgresql-c06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-inbox-jdbc-postgresql-c06 - file: ../../../final/evidence/rendered/messaging-inbox-jdbc-postgresql-c06.svg - - key: messaging-inbox-jdbc-postgresql-c06-diagram - file: ../../../final/assets/diagrams/messaging-inbox-jdbc-postgresql-c06.svg -evidence: - - ../../../final/evidence/raw/messaging-inbox-jdbc-postgresql-c06.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-inbox-jdbc-postgresql#L346 이다. -module: messaging-inbox-jdbc-postgresql ---- - -# 커넥션 조회가 새 커넥션을 열기 전에 검사가 돌아야 한다 - -`DataSourceUtils.getConnection`은 활성 트랜잭션에 묶인 커넥션이 있으면 그것을 주고 없으면 새로 연다. 그래서 `requireActiveTransaction`이 먼저 도는 것이 필수다. - -## 관계 - -- **컬럼 폭은 애플리케이션 검증과 짝을 이룬다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 안전 규칙은 한 공식과 한 강제 시점을 갖는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -**이 leaf의 주제 자체가 트랜잭션이다.** `DataSourceUtils.getConnection`은 활성 트랜잭션에 묶인 커넥션이 있으면 그것을 주고, 없으면 새로 연다. - -## 커넥션 조회보다 앞선 검사 - -:::evidence key="messaging-inbox-jdbc-postgresql-c06-diagram" alt="트랜잭션 검사와 커넥션 조회와 INSERT 가 왼쪽에서 오른쪽으로 이어지는 구조" caption="커넥션 조회보다 앞선 검사" zoom="false" -::: - -그래서 `requireActiveTransaction`이 **먼저** 도는 것이 필수다 — 없으면 새 커넥션이 열리고 자동 커밋된다. 그것이 §4.1의 이전 결함이다. - -## DataSourceUtils 참조 위치 - -:::evidence key="messaging-inbox-jdbc-postgresql-c06" alt="코드베이스에서 DataSourceUtils 를 검색한 출력 15줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DataSourceUtils 코드베이스 검색 — 15줄 · exit 0" zoom="true" -::: - -## 트랜잭션에 참여하지 않는 세 메서드 - -`isProcessed`와 두 `purge*`는 `dataSource.getConnection()`을 직접 쓴다 — 트랜잭션에 참여하지 않는다. javadoc이 그것을 명시한다("The no-argument overload is provided only for retention sweeps and read-only queries"). - -## 동시성 원시 요소가 DB에 있다 - -Java 쪽에 락이나 원자 변수가 없다. 수명주기 참여 없음 — `InboxCleanupJob`을 스케줄링하는 것은 starter다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-kafka-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-kafka-c02.md deleted file mode 100644 index 0e28056..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-kafka-c02.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-kafka-c02 -title: 느린 메시지 하나가 그 파티션의 워터마크를 붙든다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-kafka-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-kafka-c02 - file: ../../../final/evidence/rendered/messaging-kafka-c02.svg - - key: messaging-kafka-c02-diagram - file: ../../../final/assets/diagrams/messaging-kafka-c02.svg -evidence: - - ../../../final/evidence/raw/messaging-kafka-c02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-kafka#L87 이다. -module: messaging-kafka ---- - -# 느린 메시지 하나가 그 파티션의 워터마크를 붙든다 - -오프셋 커밋은 집합이 아니라 워터마크다. 동시 처리에서 오프셋이 순서 없이 끝나므로 연속 구간만 커밋한다. - -## 본문 - - - -오프셋 커밋은 집합이 아니라 워터마크다. - -> "A Kafka offset commit is a watermark, not a set: committing offset 13 declares that everything below it is done. With concurrent handlers, offsets finish out of order — 10 and 12 may complete while 11 is still running — and committing 13 at that moment would silently discard 11." - -## 완료 표시가 커밋으로 가는 갈림 - -:::evidence key="messaging-kafka-c02-diagram" alt="완료 표시 맵에서 연속 구간과 빈 자리 뒤 구간으로 화살표가 나가고 빈 자리 뒤 구간만 빗금으로 표시된 구조" caption="완료 표시가 커밋으로 가는 갈림" zoom="false" -::: - -그 대가도 적혀 있다 — 느린 메시지 하나가 그 파티션의 워터마크를 붙든다. 그것이 옳은 교환이라는 근거는 대안이 메시지를 잃는다는 것이고, 지연은 소비자 랙으로 보인다는 것이다. - -## 이 기록이 다루는 파일 범위 - -:::evidence key="messaging-kafka-c02" alt="코드베이스에서 파일 목록을 만든 출력 25줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 25줄 · exit 0" zoom="true" -::: - -## 워커 없는 오프셋을 되돌리는 경로 - -> "A delivered offset with no worker behind it holds the contiguous watermark back forever: nothing will ever complete it, so the partition stops committing while continuing to consume." - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-kafka-share-experimental-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-kafka-share-experimental-c03.md deleted file mode 100644 index bdbb6cc..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-kafka-share-experimental-c03.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-kafka-share-experimental-c03 -title: register가 아무것도 등록하지 않고 활성을 돌려준다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-kafka-share-experimental-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-kafka-share-experimental-c03 - file: ../../../final/evidence/rendered/messaging-kafka-share-experimental-c03.svg - - key: messaging-kafka-share-experimental-c03-diagram - file: ../../../final/assets/diagrams/messaging-kafka-share-experimental-c03.svg -evidence: - - ../../../final/evidence/raw/messaging-kafka-share-experimental-c03.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-kafka-share-experimental#L131 이다. -module: messaging-kafka-share-experimental ---- - -# register가 아무것도 등록하지 않고 활성을 돌려준다 - -`register(...)`는 프로파일 검증만 하고 `isActive() == true`인 객체를 반환한다. sink를 저장하지 않으므로 어떤 메시지도 전달되지 않는다. - -## 관계 - -- **허용 의존 목록은 상한이므로 미사용을 잡지 않는다 — 그것을 잡으려면 별도 검사가 필요하다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **에러 메시지가 지시하는 설정은 그 설정을 읽는 코드와 함께 존재해야 한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **구성 오류는 한 예외 타입과 안정 코드로 보고한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -`KafkaShareProfile`은 다섯 필드이고 생성자가 `shareGroup` 공백과 `maxDeliveryCount < 1`을 거절한다. `maxDeliveryCount`가 javadoc에서 "how many times a record may be re-acquired before it is released"라고 정의된다 — Share Group의 재획득 한계다. **이 필드를 읽는 코드가 이 leaf에 없다.** - -## register가 실제로 하는 것 - -:::evidence key="messaging-kafka-share-experimental-c03-diagram" alt="등록기 경계 안에 프로파일 검증이 들어 있고 소비자 생성과 sink 저장이 경계 밖 빗금 상자로 놓인 구조" caption="register 가 실제로 하는 것" zoom="false" -::: - -`TransportConsumerSpec`은 `(DestinationProfile profile, Function> sink)`이고, `sink`가 플랫폼이 전달마다 부르는 콜백이다. 그 sink가 저장되지 않으므로 **어떤 메시지도 전달되지 않는다.** Kafka 소비자도 만들어지지 않는다 — `kafka-clients`를 import하는 코드가 없다. 즉 `register(...)`는 **아무것도 등록하지 않고** `isActive() == true`인 객체를 반환한다. §17. - -## MessagingCapabilityUnavailableException 참조 위치 - -:::evidence key="messaging-kafka-share-experimental-c03" alt="코드베이스에서 MessagingCapabilityUnavailableException 를 검색한 출력 37줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingCapabilityUnavailableException 코드베이스 검색 — 37줄 · exit 0" zoom="true" -::: - -## 두 거절의 예외 타입이 다르다 - -첫째는 `MessagingCapabilityUnavailableException`(카테고리 `CONFIGURATION`, 안정 코드 있음), 둘째는 `IllegalArgumentException`(코드 없음). 둘 다 설정 오류인데 하나만 플랫폼 실패 어휘를 쓴다. §17. - -## 에러 메시지가 지목하는 키를 읽는 코드가 없다 - -에러 메시지가 프로퍼티 키를 직접 적는다 — `backend.messaging.experimental.kafka-share=true`. 그 키를 읽는 코드가 이 저장소에 없다(§12.4). - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-kafka-share-experimental-c06.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-kafka-share-experimental-c06.md deleted file mode 100644 index 3183051..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-kafka-share-experimental-c06.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-kafka-share-experimental-c06 -title: 닫을 자원이 없어서 두 번 닫아도 무해하다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-kafka-share-experimental-c06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-kafka-share-experimental-c06 - file: ../../../final/evidence/rendered/messaging-kafka-share-experimental-c06.svg -evidence: - - ../../../final/evidence/raw/messaging-kafka-share-experimental-c06.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-kafka-share-experimental#L258 이다. -module: messaging-kafka-share-experimental ---- - -# 닫을 자원이 없어서 두 번 닫아도 무해하다 - -`ShareRegistration.active`가 `AtomicBoolean`이고 `close()`가 `set(false)`이므로 멱등이다. 수명주기 참여가 없다. - -## 관계 - -- **허용 의존 목록은 상한이므로 미사용을 잡지 않는다 — 그것을 잡으려면 별도 검사가 필요하다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **에러 메시지가 지시하는 설정은 그 설정을 읽는 코드와 함께 존재해야 한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **구성 오류는 한 예외 타입과 안정 코드로 보고한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -`ShareRegistration.active`가 `AtomicBoolean`이다. `close()`가 `set(false)`이고 CAS가 아니므로 두 번 닫아도 무해하다(멱등). - -## ShareRegistration 참조 위치 - -:::evidence key="messaging-kafka-share-experimental-c06" alt="코드베이스에서 ShareRegistration 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ShareRegistration 코드베이스 검색 — 3줄 · exit 0" zoom="true" -::: - -## 상태를 갖는 것과 갖지 않는 것 - -`KafkaShareProfileValidator`·`KafkaShareWorkQueueCapability`는 상태가 없다. `KafkaShareGroupRegistrar`는 validator 참조 하나만 갖는다. - -## 수명주기 참여가 없는 이유 - -`TransportConsumerRegistration`이 `AutoCloseable`이지만 이 구현은 닫을 자원을 갖지 않는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-observability-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-observability-c01.md deleted file mode 100644 index 8d735ec..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-observability-c01.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-observability-c01 -title: seam은 중립이고 구현만 벤더에 묶인다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-observability-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-observability-c01 - file: ../../../final/evidence/rendered/messaging-observability-c01.svg - - key: messaging-observability-c01-diagram - file: ../../../final/assets/diagrams/messaging-observability-c01.svg -evidence: - - ../../../final/evidence/raw/messaging-observability-c01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-observability#L52 이다. -module: messaging-observability ---- - -# seam은 중립이고 구현만 벤더에 묶인다 - -메트릭·추적·감사 셋이 같은 제약 아래 있다 — 경계가 알려진 값만 나간다. `MessagingObservation` 인터페이스 자체는 Micrometer를 모른다. - -## 관계 - -- **타입이 문서화한 불변식은 타입이 강제한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -이 leaf는 **"메시징이 무엇을 밖으로 내보내도 되는가"**를 소유한다. 메트릭·추적·감사 셋이 여기 있고, 셋 다 같은 제약 아래 있다 — **경계가 알려진 값만 나간다.** - -## seam과 구현의 결합 범위 - -:::evidence key="messaging-observability-c01-diagram" alt="관측 seam 경계 안에 중립 인터페이스가 들어 있고 벤더에 결합된 구현이 경계 밖 점선 상자로 놓인 구조" caption="seam 과 구현의 결합 범위" zoom="false" -::: - -Micrometer를 `api`로 선언한 이유가 build.gradle에 있고 `src/messaging/CLAUDE.md:40-43`의 vendor `api` 게이트를 통과한다. 다만 `MessagingObservation` 인터페이스 자체는 Micrometer를 모른다 — 벤더는 `MessagingMetrics` 한 클래스에만 나타난다. 즉 **seam은 중립이고 구현만 벤더에 묶인다.** - -## MessagingObservation 참조 위치 - -:::evidence key="messaging-observability-c01" alt="코드베이스에서 MessagingObservation 를 검색한 출력 9줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingObservation 코드베이스 검색 — 9줄 · exit 0" zoom="true" -::: - -## 의존이 하나뿐인 것도 의도적이다 - -`messaging-core-api` 하나다. `MessagingTracer`가 `TraceContext`·`MessageHeaders`를 쓰고 `DefaultMessagingObservationConvention`이 `PublishCompletion`·`FailureCategory`를 쓴다. policy나 transport는 필요 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-observability-c06.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-observability-c06.md deleted file mode 100644 index 9825980..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-observability-c06.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-observability-c06 -title: Set 인스턴스를 락으로 쓰지만 외부 경합이 없다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-observability-c06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-observability-c06 - file: ../../../final/evidence/rendered/messaging-observability-c06.svg -evidence: - - ../../../final/evidence/raw/messaging-observability-c06.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-observability#L363 이다. -module: messaging-observability ---- - -# Set 인스턴스를 락으로 쓰지만 외부 경합이 없다 - -트랜잭션이 없고, 이 leaf는 messaging family에서 `messaging-transport-spi` 다음으로 동시성이 조밀하다. - -## 관계 - -- **타입이 문서화한 불변식은 타입이 강제한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -트랜잭션 없음. 이 leaf는 messaging family에서 `messaging-transport-spi` 다음으로 동시성이 조밀하다. `MessagingRedactor`·`MessagingTracer`·`DefaultMessagingObservationConvention`은 상태가 없고 `MessagingTags`는 불변 record다. - -## MessagingRedactor 참조 위치 - -:::evidence key="messaging-observability-c06" alt="코드베이스에서 MessagingRedactor 를 검색한 출력 13줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingRedactor 코드베이스 검색 — 13줄 · exit 0" zoom="true" -::: - -## Set 인스턴스를 락으로 쓰는 것이 안전한 이유 - -**`synchronized(values)`가 `Set` 인스턴스를 락으로 쓴다.** 그 `Set`은 `ConcurrentHashMap.newKeySet()`이고 외부에 노출되지 않으므로(`observed` 맵이 private) 외부 락 경합은 없다. 차원별로 락이 분리되는 효과도 있다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-observability-c07.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-observability-c07.md deleted file mode 100644 index 2e290c5..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-observability-c07.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-observability-c07 -title: 경계는 예산을 정확히 소비할 때만 경계다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-observability-c07 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-observability-c07 - file: ../../../final/evidence/rendered/messaging-observability-c07.svg -evidence: - - ../../../final/evidence/raw/messaging-observability-c07.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-observability#L597 이다. -module: messaging-observability ---- - -# 경계는 예산을 정확히 소비할 때만 경계다 - -세 이력 중 세 번째가 가장 무겁다 — 경계가 있었는데 기본 차원만 보호했고 진단 값은 그 밖이었다. - -## 관계 - -- **타입이 문서화한 불변식은 타입이 강제한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -세 번째가 가장 무겁다 — **경계가 있었는데 기본 차원만 보호했고 진단 값은 그 밖이었다.** 현재는 값이 태그가 되지 않고 키만 별도 guard 차원(`"diagnostic"`)을 통과한다. - -## 이 기록이 다루는 파일 범위 - -:::evidence key="messaging-observability-c07" alt="코드베이스에서 파일 목록을 만든 출력 9줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 9줄 · exit 0" zoom="true" -::: - -## 앞 두 이력이 말하는 같은 주제 - -첫 두 개는 같은 주제의 두 형태다 — **경계는 예산을 정확히 소비할 때만 경계다.** `messaging-policy`의 슬롯 누수 방지, `messaging-transport-spi`의 `endWork` clamp와 같은 계열이고 각 leaf §13이 소유한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-outbox-jdbc-postgresql-c05.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-outbox-jdbc-postgresql-c05.md deleted file mode 100644 index 48f3de1..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-outbox-jdbc-postgresql-c05.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-outbox-jdbc-postgresql-c05 -title: 동시성 제어가 전부 데이터베이스에 있다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-outbox-jdbc-postgresql-c05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-outbox-jdbc-postgresql-c05 - file: ../../../final/evidence/rendered/messaging-outbox-jdbc-postgresql-c05.svg - - key: messaging-outbox-jdbc-postgresql-c05-diagram - file: ../../../final/assets/diagrams/messaging-outbox-jdbc-postgresql-c05.svg -evidence: - - ../../../final/evidence/raw/messaging-outbox-jdbc-postgresql-c05.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-outbox-jdbc-postgresql#L486 이다. -module: messaging-outbox-jdbc-postgresql ---- - -# 동시성 제어가 전부 데이터베이스에 있다 - -두 가지 커넥션 획득 방식이 공존하고, Java 쪽에는 락이 없다. - -## 본문 - - - -**두 가지 커넥션 획득 방식이 공존한다.** - -## 커넥션을 얻는 두 방식 - -:::evidence key="messaging-outbox-jdbc-postgresql-c05-diagram" alt="커넥션 획득에서 릴레이와 저널 두 상자로 화살표가 나가고 화살표에 독립 커넥션과 호출자 트랜잭션이 붙은 구조" caption="커넥션을 얻는 두 방식" zoom="false" -::: - -릴레이 연산이 비즈니스 트랜잭션에 합류하면 안 되므로 `withConnection` 의 선택은 타당하다. 다만 그 판단이 주석으로 남아 있지 않고, 같은 리프의 저널은 반대 방식을 쓴다. §17 P3. - -## OutboxRelayWorker 참조 위치 - -:::evidence key="messaging-outbox-jdbc-postgresql-c05" alt="코드베이스에서 OutboxRelayWorker 를 검색한 출력 17줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="OutboxRelayWorker 코드베이스 검색 — 17줄 · exit 0" zoom="true" -::: - -## 동시성 원시 요소가 전부 DB에 있다 - -`FOR UPDATE SKIP LOCKED`(청구), 서버측 토큰 증가, 펜싱 술어, `ON CONFLICT DO NOTHING`, 복합 기본키. Java 쪽에 락이 없다. - -## 수명주기 - -`OutboxRelayWorker` 는 데몬 스레드 1개, `setExecuteExistingDelayedTasksAfterShutdownPolicy(false)`, `start()` 멱등, `stop(deadline)` 드레인 후 실패 시 `shutdownNow()`. 셋 다 근거 주석이 있다(`:79-88`, `:92`, `:121-122`). - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-policy-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-policy-c01.md deleted file mode 100644 index 1f9e2f8..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-policy-c01.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-policy-c01 -title: 모순은 부팅 실패여야 한다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-policy-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-policy-c01 - file: ../../../final/evidence/rendered/messaging-policy-c01.svg - - key: messaging-policy-c01-diagram - file: ../../../final/assets/diagrams/messaging-policy-c01.svg -evidence: - - ../../../final/evidence/raw/messaging-policy-c01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-policy#L56 이다. -module: messaging-policy ---- - -# 모순은 부팅 실패여야 한다 - -이 leaf는 "이 목적지는 무엇을 약속하는가"를 소유한다. 브로커를 만지지 않고 벤더 의존성이 0이며, 브로커 어댑터가 따라야 할 판단을 미리 계산한다. - -## 관계 - -- **구성 오류는 한 예외 타입과 안정 코드로 보고한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **저장소 밖 문서를 절 번호로 인용하지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **부팅 경로의 알고리즘 복잡도는 문서화한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -이 leaf는 **"이 목적지는 무엇을 약속하는가"**를 소유한다. 브로커를 만지지 않고 벤더 의존성이 0이며, 대신 브로커 어댑터가 따라야 할 판단을 미리 계산한다. - -## 이 리프가 독점하는 것 - -:::evidence key="messaging-policy-c01-diagram" alt="리프 경계 안에 물리 주소와 프로파일 판단이 들어 있고 브로커 클라이언트가 경계 밖 빗금 상자로 놓인 구조" caption="이 리프가 독점하는 것" zoom="false" -::: - -경계 규칙 하나가 leaf 전체를 관통한다 — **모순은 부팅 실패여야 한다.** - -## 이 기록이 다루는 파일 범위 - -:::evidence key="messaging-policy-c01" alt="코드베이스에서 파일 목록을 만든 출력 25줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 25줄 · exit 0" zoom="true" -::: - -## 두 리프가 같은 경계를 양쪽에서 지킨다 - -두 번째 경계는 **물리 주소의 격리**다. `messaging-core-api`의 `DestinationName`이 `:`과 `/`를 정규식으로 막고(그쪽 §4), 이 leaf가 물리 주소를 독점한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-policy-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-policy-c03.md deleted file mode 100644 index 4d6c646..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-policy-c03.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-policy-c03 -title: 거절이 전송 전에 끝나는 것이 설계의 핵심이다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-policy-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-policy-c03 - file: ../../../final/evidence/rendered/messaging-policy-c03.svg - - key: messaging-policy-c03-diagram - file: ../../../final/assets/diagrams/messaging-policy-c03.svg -evidence: - - ../../../final/evidence/raw/messaging-policy-c03.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-policy#L135 이다. -module: messaging-policy ---- - -# 거절이 전송 전에 끝나는 것이 설계의 핵심이다 - -`DestinationProfileValidator.validate`가 15가지 모순을 순서대로 거절하고, `admit`은 네 단계를 전송 전에 통과시킨다. - -## 관계 - -- **구성 오류는 한 예외 타입과 안정 코드로 보고한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **저장소 밖 문서를 절 번호로 인용하지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **부팅 경로의 알고리즘 복잡도는 문서화한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -`DestinationProfileValidator.validate`가 프로파일 하나에 대해 15가지 모순을 순서대로 거절한다. 11번과 12번이 짝이다 — 전자는 목적지 수준 동시성(`orderingScope == DESTINATION && consumer.concurrency > 1`), 후자는 순서 단위 안 동시성(`isOrdered() && maxInFlightPerOrderingUnit > 1`). 둘 다 있어야 "순서 보장"이 실제로 성립한다. - -## 사이클 탐색이 전역 집합이 아니라 경로를 쓰는 이유 - -`Edge` enum이 `RETRY`와 `DEAD_LETTER` 둘을 갖고, `walk`가 두 간선을 동시에 따라간다. **`onPath`가 전역 방문 집합이 아니라 현재 경로다.** 각 분기마다 `new LinkedHashSet<>(onPath)`로 복사하므로 형제 분기가 서로의 방문 기록을 오염시키지 않는다. 다이아몬드(A→C, B→C)는 사이클이 아니고, 그것을 사이클로 판정하면 정상 구성이 부팅에 실패한다. 테스트가 두 경우를 각각 붙든다 — `aMixedEdgeCycleIsRejected`(retry/DLQ 교대 사이클 거절)와 `aSharedDeadLetterIsNotACycle`(다이아몬드 허용). 미등록 목적지도 여기서 잡힌다 — `anUnregisteredRetryDestinationIsRejected`. - -## 어디에도 기록되지 않은 비용 - -매 분기마다 `onPath`와 `path`를 복사하므로 시간·공간이 경로 수에 지수적이다. 목적지 수가 수십 개인 정상 구성에서는 문제가 없지만, 이 성질이 어디에도 기록되지 않았다 — §17의 P3. - -## admit의 검사 순서 - -:::evidence key="messaging-policy-c03-diagram" alt="payload 검사와 종료 여부와 목적지 슬롯과 전역 semaphore 가 왼쪽에서 오른쪽으로 이어지는 구조" caption="admit 의 검사 순서" zoom="false" -::: - -1. `payloadGuard.checkPayload` → 초과면 `MessageTooLargeException` -1. `acceptingNewWork` 확인 → 종료 중이면 `MessageBackpressureException("SHUTTING_DOWN")` -1. `reserve(destination)` — 목적지별 CAS 루프 → 초과면 `DESTINATION_IN_FLIGHT_LIMIT_EXCEEDED` -1. `limiter.tryAcquire()` — 프로세스 전역 semaphore, 유한 대기 → 실패면 목적지 슬롯 **반납 후** `IN_FLIGHT_LIMIT_EXCEEDED` - -## MessageTooLargeException 참조 위치 - -:::evidence key="messaging-policy-c03" alt="코드베이스에서 MessageTooLargeException 를 검색한 출력 12줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessageTooLargeException 코드베이스 검색 — 12줄 · exit 0" zoom="true" -::: - -**거절이 모호하지 않은 것이 설계의 핵심**이다 — 두 거절 모두 전송 전에 일어난다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-policy-c06.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-policy-c06.md deleted file mode 100644 index 1f5677b..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-policy-c06.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-policy-c06 -title: 제거 실패는 안전한 방향의 경합이다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-policy-c06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-policy-c06 - file: ../../../final/evidence/rendered/messaging-policy-c06.svg -evidence: - - ../../../final/evidence/raw/messaging-policy-c06.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-policy#L451 이다. -module: messaging-policy ---- - -# 제거 실패는 안전한 방향의 경합이다 - -동시성 지점은 `MessagingAdmissionController`와 `InFlightLimiter` 둘이고, `release`에 미세한 경합이 있지만 안전한 방향이다. - -## 관계 - -- **구성 오류는 한 예외 타입과 안정 코드로 보고한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **저장소 밖 문서를 절 번호로 인용하지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **부팅 경로의 알고리즘 복잡도는 문서화한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -동시성 지점은 `MessagingAdmissionController`와 `InFlightLimiter` 둘이다. `reserve`의 CAS 루프는 `AtomicInteger.updateAndGet`으로 쓸 수 있었지만 조건부 실패(`return false`)가 필요해서 직접 루프를 돈다. - -## MessagingAdmissionController 참조 위치 - -:::evidence key="messaging-policy-c06" alt="코드베이스에서 MessagingAdmissionController 를 검색한 출력 22줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingAdmissionController 코드베이스 검색 — 22줄 · exit 0" zoom="true" -::: - -## release의 경합이 안전한 방향인 이유 - -`getAndUpdate`로 감소한 뒤 `computeIfPresent`로 0인 항목을 제거하는데, 그 사이에 다른 스레드가 `computeIfAbsent`로 같은 키를 만들고 증가시킬 수 있다. 그러면 `computeIfPresent`의 람다가 `value.get() == 0`을 보지 못해 제거하지 않는다 — 안전한 방향의 경합이다(누수가 아니라 제거 실패). 반대 순서였다면 살아 있는 카운터를 지울 수 있었다. - -## 상태가 없거나 불변인 것들 - -`DefaultRetryDecisionEngine`·`BackoffCalculator`·`DeadLetterOrchestrator`·`DeadLetterEnvelopeFactory`·`DestinationProfileValidator`는 전부 상태가 없거나 불변이다. `BackoffCalculator`의 기본 생성자가 `ThreadLocalRandom`을 쓰므로 스레드 안전하다. - -## 수명주기 참여는 하나뿐이다 - -`stopAcceptingNewWork()` 하나이고, `MessagingShutdownLifecycle`(starter)이 종료 1단계에서 부른다(`messaging-transport-spi` §12.1 참조). - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-policy-c09.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-policy-c09.md deleted file mode 100644 index f6e2473..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-policy-c09.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-policy-c09 -title: 반납이 획득보다 많으면 제한이 사라진다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-policy-c09 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-policy-c09 - file: ../../../final/evidence/rendered/messaging-policy-c09.svg -evidence: - - ../../../final/evidence/raw/messaging-policy-c09.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-policy#L700 이다. -module: messaging-policy ---- - -# 반납이 획득보다 많으면 제한이 사라진다 - -이 leaf의 주석은 이전 결함보다 왜 이 형태여야 하는가를 더 많이 적는다. 이전 상태를 직접 서술하는 것은 셋이다. - -## 관계 - -- **구성 오류는 한 예외 타입과 안정 코드로 보고한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **저장소 밖 문서를 절 번호로 인용하지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **부팅 경로의 알고리즘 복잡도는 문서화한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -이 leaf의 주석은 이전 결함보다 **왜 이 형태여야 하는가**를 더 많이 적는다. 그중 이전 상태를 직접 서술하는 것은 셋이다. - -## GracefulShutdownCoordinator 참조 위치 - -:::evidence key="messaging-policy-c09" alt="코드베이스에서 GracefulShutdownCoordinator 를 검색한 출력 14줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GracefulShutdownCoordinator 코드베이스 검색 — 14줄 · exit 0" zoom="true" -::: - -## 세 번째와 다섯 번째가 같은 형태다 - -**반납이 획득보다 많으면 제한이 사라진다.** `messaging-transport-spi`의 `GracefulShutdownCoordinator.endWork` clamp와 `DefaultMessagingRuntimeRegistry`의 "정확히 한 번 close"도 같은 계열이고, 그 leaf §13이 소유한다. 저장소 전체에서 반복되는 주제다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-reliability-api-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-reliability-api-c01.md deleted file mode 100644 index ad7b971..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-reliability-api-c01.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-reliability-api-c01 -title: Outbox 하나로는 부족하다는 것을 타입이 직접 말한다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-reliability-api-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-reliability-api-c01 - file: ../../../final/evidence/rendered/messaging-reliability-api-c01.svg - - key: messaging-reliability-api-c01-diagram - file: ../../../final/assets/diagrams/messaging-reliability-api-c01.svg -evidence: - - ../../../final/evidence/raw/messaging-reliability-api-c01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-reliability-api#L51 이다. -module: messaging-reliability-api ---- - -# Outbox 하나로는 부족하다는 것을 타입이 직접 말한다 - -이 leaf는 effectively-once 처리의 계약을 소유한다. 구현이 없고 13개 중 인터페이스 5개, record 5개, enum 3개다. - -## 관계 - -- **계약만 담는 leaf도 계약의 거절 조건은 자기 레인에서 검증한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **record의 `equals`를 좁히면 이유를 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **호출 컨텍스트가 계약이면 그 컨텍스트를 검증할 수단을 함께 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -이 leaf는 **effectively-once 처리의 계약**을 소유한다. 구현이 없다 — 13개 중 인터페이스 5개, record 5개, enum 3개이고 실행 가능한 로직은 record 생성자 검증과 `isExpired`/`expiredAt` 술어 정도다. 벤더 의존성 0, 저장소 기술 중립이다. - -## 이 리프가 담는 세 메커니즘 - -:::evidence key="messaging-reliability-api-c01-diagram" alt="리프 경계 안에 Outbox 와 Inbox 와 Claim Check 세 상자가 나란히 들어 있는 구조" caption="이 리프가 담는 세 메커니즘" zoom="false" -::: - -**Outbox** — dual-write 문제의 답. **Inbox** — 소비 측 중복 제거. **Claim Check** — 브로커 밖 payload 참조. - -## OutboxRecord 참조 위치 - -:::evidence key="messaging-reliability-api-c01" alt="코드베이스에서 OutboxRecord 를 검색한 출력 10줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="OutboxRecord 코드베이스 검색 — 10줄 · exit 0" zoom="true" -::: - -## 타입이 자기 한계를 직접 말한다 - -셋의 관계를 `OutboxRecord`가 명시한다. **Outbox 하나로는 부족하다는 것을 타입의 javadoc이 직접 말한다.** 이 저장소에서 반복되는 "보장을 과대 진술하지 않는다"의 예다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-reliability-api-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-reliability-api-c02.md deleted file mode 100644 index c0ea318..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-reliability-api-c02.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-reliability-api-c02 -title: Outbox에 행을 쓰는 진입점이 없다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-reliability-api-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-reliability-api-c02 - file: ../../../final/evidence/rendered/messaging-reliability-api-c02.svg -evidence: - - ../../../final/evidence/raw/messaging-reliability-api-c02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-reliability-api#L92 이다. -module: messaging-reliability-api ---- - -# Outbox에 행을 쓰는 진입점이 없다 - -구현 leaf가 셋 있고 전부 배선되는데, `ReliableMessagePublisher`는 구현도 소비자도 0이다. - -## 관계 - -- **계약만 담는 leaf도 계약의 거절 조건은 자기 레인에서 검증한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **record의 `equals`를 좁히면 이유를 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **호출 컨텍스트가 계약이면 그 컨텍스트를 검증할 수단을 함께 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -들어오는 것: `messaging-core-api`(api) 하나. 나가는 것: `messaging-outbox-jdbc-postgresql`, `messaging-inbox-jdbc-postgresql`, `messaging-claim-check`, `messaging-spring-boot-starter`. - -## ReliableMessagePublisher 참조 위치 - -:::evidence key="messaging-reliability-api-c02" alt="코드베이스에서 ReliableMessagePublisher 를 검색한 출력 1줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ReliableMessagePublisher 코드베이스 검색 — 1줄 · exit 0" zoom="true" -::: - -## 구현 leaf는 셋 다 배선되는데 진입점이 없다 - -**구현 leaf가 셋 있고 전부 배선된다.** `ReliableMessagePublisher`는 구현도 소비자도 0이다(§12.1). Outbox에 행을 쓰는 애플리케이션 측 진입점인데, 그 진입점이 없다. 이 leaf 자체는 Spring 주석을 갖지 않는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-reliability-api-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-reliability-api-c03.md deleted file mode 100644 index bf78bf4..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-reliability-api-c03.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-reliability-api-c03 -title: 이 enum의 의미가 저장소 규칙 하나의 존재 이유다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-reliability-api-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-reliability-api-c03 - file: ../../../final/evidence/rendered/messaging-reliability-api-c03.svg - - key: messaging-reliability-api-c03-diagram - file: ../../../final/assets/diagrams/messaging-reliability-api-c03.svg -evidence: - - ../../../final/evidence/raw/messaging-reliability-api-c03.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-reliability-api#L143 이다. -module: messaging-reliability-api ---- - -# 이 enum의 의미가 저장소 규칙 하나의 존재 이유다 - -`OutboxLease`의 fencing token이 이 leaf에서 가장 중요한 안전 장치이고, `OutboxStatus.FAILED`의 의미가 애플리케이션 쪽 동명 enum과 반대다. - -## 관계 - -- **계약만 담는 leaf도 계약의 거절 조건은 자기 레인에서 검증한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **record의 `equals`를 좁히면 이유를 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **호출 컨텍스트가 계약이면 그 컨텍스트를 검증할 수단을 함께 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -`OutboxLease`가 이 leaf에서 가장 중요한 안전 장치이고, 이전 결함이 javadoc에 통째로 있다. - -> "The port used to take a `MessageId` for every terminal transition, so a write said which row to change and nothing about which claim it belonged to. A relay that stalled past its lease could still record `AMBIGUOUS` over the `PUBLISHED` another relay had already written, and the row became claimable again — one message, published twice, by a system whose whole purpose is to publish it once. -> The token is the part that makes staleness detectable. It increases on every claim, so a superseded relay holds a number the row no longer has and its update matches zero rows." - -`token < 1`을 거절하는 이유도 적혀 있다 — "a claim's token starts at 1; 0 is the value of a row nobody has claimed". `expiredAt(now)`가 `!now.isBefore(expiresAt)`다. - -## 청구된 행이 갈리는 네 종착 - -:::evidence key="messaging-reliability-api-c03-diagram" alt="IN_FLIGHT 에서 PUBLISHED 와 AMBIGUOUS 와 FAILED 와 EXHAUSTED 네 상자로 화살표가 나가는 구조" caption="청구된 행이 갈리는 네 종착" zoom="false" -::: - -`PENDING` → `IN_FLIGHT` → `PUBLISHED` / `AMBIGUOUS` / `FAILED` / `EXHAUSTED`. **두 쌍의 구분이 각각 이유를 갖는다.** `OutboxRepository.markExhausted`의 javadoc이 같은 말을 반복한다 — "The first needs a fix, the second a redrive." - -## OutboxRepository 참조 위치 - -:::evidence key="messaging-reliability-api-c03" alt="코드베이스에서 OutboxRepository 를 검색한 출력 35줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="OutboxRepository 코드베이스 검색 — 35줄 · exit 0" zoom="true" -::: - -## 이름이 같고 의미가 반대인 enum - -**`FAILED`의 의미가 애플리케이션 쪽 동명 enum과 반대다.** `CleanArchitectureTest.APPLICATION_DOES_NOT_DEPEND_ON_THE_MESSAGING_PLATFORM`의 `.because(...)`가 그것을 ArchUnit 규칙의 근거로 든다 — "its `OutboxStatus.FAILED` means the opposite of the legacy `OutboxEventStatus.FAILED`, so the two models cannot be mixed by name without inverting retryable and terminal." 즉 **이 enum의 의미가 저장소 규칙 하나의 존재 이유다.** - -## 정산하면 안 되는 경우를 상수가 들고 있다 - -`safeToSettle` 플래그가 상수에 붙어 있다. 세 번째의 javadoc이 결론을 적는다 — "Do *not* settle. The other transaction may still roll back, and this delivery is the only remaining copy." - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-reliability-api-c04.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-reliability-api-c04.md deleted file mode 100644 index bf55fd6..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-reliability-api-c04.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-reliability-api-c04 -title: Outbox에 행을 쓰는 진입점만 구현이 없다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-reliability-api-c04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-reliability-api-c04 - file: ../../../final/evidence/rendered/messaging-reliability-api-c04.svg -evidence: - - ../../../final/evidence/raw/messaging-reliability-api-c04.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-reliability-api#L338 이다. -module: messaging-reliability-api ---- - -# Outbox에 행을 쓰는 진입점만 구현이 없다 - -세 실행 경로가 있고 그중 Outbox 쓰기의 진입점만 구현이 없다. - -## 관계 - -- **계약만 담는 leaf도 계약의 거절 조건은 자기 레인에서 검증한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **record의 `equals`를 좁히면 이유를 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **호출 컨텍스트가 계약이면 그 컨텍스트를 검증할 수단을 함께 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -**Outbox 쓰기** — 애플리케이션 트랜잭션 안에서 `ReliableMessagePublisher.addToOutbox(...)` → `OutboxRepository.append(record)`. 이 진입점의 구현이 없다(§12.1). - -**Outbox 릴레이** — `claimBatch(owner, size, lease, now, maxAttempts)` → `List` → 각 lease에 대해 발행 → 결과에 따라 `markPublished`/`markAmbiguous`/`markExhausted`/`markFailed`(lease 기반) → `APPLIED`면 정상, `STALE_LEASE`면 다른 릴레이가 가져감. - -**Inbox** — `handleOnce(consumerName, delivery, action)` → 한 트랜잭션 안에서 `reserve(messageId, consumerId, now)` → true면 `action.apply(delivery)` → 커밋. - -## 이 기록이 다루는 파일 범위 - -:::evidence key="messaging-reliability-api-c04" alt="코드베이스에서 파일 목록을 만든 출력 13줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 13줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-reliability-api-c05.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-reliability-api-c05.md deleted file mode 100644 index 79e8024..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-reliability-api-c05.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-reliability-api-c05 -title: 실패를 예외가 아니라 상태와 반환값으로 표현한다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-reliability-api-c05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-reliability-api-c05 - file: ../../../final/evidence/rendered/messaging-reliability-api-c05.svg -evidence: - - ../../../final/evidence/raw/messaging-reliability-api-c05.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-reliability-api#L348 이다. -module: messaging-reliability-api ---- - -# 실패를 예외가 아니라 상태와 반환값으로 표현한다 - -이 leaf는 `MessagingException`을 하나도 던지지 않는다. `IllegalArgumentException`을 던지는 곳은 record 생성자 여섯뿐이다. - -## 관계 - -- **계약만 담는 leaf도 계약의 거절 조건은 자기 레인에서 검증한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **record의 `equals`를 좁히면 이유를 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **호출 컨텍스트가 계약이면 그 컨텍스트를 검증할 수단을 함께 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -**이 leaf는 `MessagingException`을 하나도 던지지 않는다.** 실패를 상태와 반환값으로 표현한다. `IllegalArgumentException`을 던지는 곳은 record 생성자 여섯이다 — 전부 호출자의 프로그래밍 오류다. - -## MessagingException 참조 위치 - -:::evidence key="messaging-reliability-api-c05" alt="코드베이스에서 MessagingException 를 검색한 출력 36줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingException 코드베이스 검색 — 36줄 · exit 0" zoom="true" -::: - -## 예외가 롤백 신호인 자리 - -`TransactionalMessageAction.apply`가 `throws Exception`이다 — javadoc: "rolling back both it and the inbox reservation". 즉 예외가 롤백 신호이고, 그 처리는 구현 leaf가 소유한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-reliability-api-c06.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-reliability-api-c06.md deleted file mode 100644 index f8e7100..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-reliability-api-c06.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-reliability-api-c06 -title: 전부 트랜잭션 계약인데 코드에는 트랜잭션이 없다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-reliability-api-c06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-reliability-api-c06 - file: ../../../final/evidence/rendered/messaging-reliability-api-c06.svg - - key: messaging-reliability-api-c06-diagram - file: ../../../final/assets/diagrams/messaging-reliability-api-c06.svg -evidence: - - ../../../final/evidence/raw/messaging-reliability-api-c06.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-reliability-api#L366 이다. -module: messaging-reliability-api ---- - -# 전부 트랜잭션 계약인데 코드에는 트랜잭션이 없다 - -이 leaf 전체가 트랜잭션 계약이지만 코드에는 트랜잭션이 없다 — 전부 javadoc이 요구하는 규약이고, 마지막 하나만 타입이 강제한다. - -## 관계 - -- **계약만 담는 leaf도 계약의 거절 조건은 자기 레인에서 검증한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **record의 `equals`를 좁히면 이유를 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **호출 컨텍스트가 계약이면 그 컨텍스트를 검증할 수단을 함께 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -**이 leaf 전체가 트랜잭션 계약이다.** 그런데 코드에는 트랜잭션이 없다 — 전부 javadoc이 요구하는 규약이다. 마지막 하나만 타입이 강제한다. - -## 타입이 강제하는 것과 규약으로 남는 것 - -:::evidence key="messaging-reliability-api-c06-diagram" alt="리프 경계 안에 fencing token 이 들어 있고 트랜잭션 경계가 경계 밖 점선 상자로 놓인 구조" caption="타입이 강제하는 것과 규약으로 남는 것" zoom="false" -::: - -동시성 원시 요소는 하나 — **fencing token**. 그것이 `OutboxLease.token`이고 검사는 구현의 SQL `WHERE`에 있다(§12.1). - -## OutboxLease 참조 위치 - -:::evidence key="messaging-reliability-api-c06" alt="코드베이스에서 OutboxLease 를 검색한 출력 10줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="OutboxLease 코드베이스 검색 — 10줄 · exit 0" zoom="true" -::: - -## 상태를 가진 클래스가 없다 - -모든 record가 불변이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-reliability-api-c07.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-reliability-api-c07.md deleted file mode 100644 index 39efdd3..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-reliability-api-c07.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-reliability-api-c07 -title: 전달 경로가 메시지 내용을 바꾸면 그것은 전달이 아니다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-reliability-api-c07 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-reliability-api-c07 - file: ../../../final/evidence/rendered/messaging-reliability-api-c07.svg -evidence: - - ../../../final/evidence/raw/messaging-reliability-api-c07.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-reliability-api#L610 이다. -module: messaging-reliability-api ---- - -# 전달 경로가 메시지 내용을 바꾸면 그것은 전달이 아니다 - -javadoc이 세 개의 서로 다른 결함을 보존한다. 첫 둘이 같은 사건의 두 측면이다. - -## 관계 - -- **계약만 담는 leaf도 계약의 거절 조건은 자기 레인에서 검증한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **record의 `equals`를 좁히면 이유를 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **호출 컨텍스트가 계약이면 그 컨텍스트를 검증할 수단을 함께 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -이 leaf의 javadoc은 **세 개의 서로 다른 결함**을 보존한다. 첫 둘이 같은 사건의 두 측면이다 — fencing token(감지 수단)과 반환값(감지 결과의 전달 수단). 둘 다 있어야 stale lease가 관측된다. - -## 이 기록이 다루는 파일 범위 - -:::evidence key="messaging-reliability-api-c07" alt="코드베이스에서 파일 목록을 만든 출력 13줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 13줄 · exit 0" zoom="true" -::: - -## 세 번째의 마지막 문장 - -이 저장소에서 가장 날카로운 진술 중 하나다 — **"which makes the publish path — direct, polling or CDC — part of the message's meaning."** 전달 경로가 메시지 내용을 바꾸면 그것은 더 이상 전달이 아니다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-runtime-core-c04.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-runtime-core-c04.md deleted file mode 100644 index 1ff405d..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-runtime-core-c04.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-runtime-core-c04 -title: 전송이 없으면 조용히 반환하는 설치 경로 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-runtime-core-c04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-runtime-core-c04 - file: ../../../final/evidence/rendered/messaging-runtime-core-c04.svg -evidence: - - ../../../final/evidence/raw/messaging-runtime-core-c04.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-runtime-core#L387 이다. -module: messaging-runtime-core ---- - -# 전송이 없으면 조용히 반환하는 설치 경로 - -발행 경로는 조립돼 있고 소비 경로는 조립되지 않았다. 세대 설치는 전송이 없으면 조용히 반환한다. - -## 관계 - -- **만들어 두고 흘리지 않는 진단값은 진단이 아니다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **증가한다고 문서화한 값이 리터럴이면 그 사실을 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **안정 코드는 운영자의 행동이 갈리는 지점마다 나눈다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **`CompletionStage`를 반환하는 메서드는 동기적으로 던지지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -**발행(조립됨)** — §4.1의 8단계. - -**소비(미조립)** — `TransportDelivery` → `handler.apply(envelope)` → `HandleResult` 4분기 → `OneShotSettlement`로 정확히 한 번 정산. - -## TransportDelivery 참조 위치 - -:::evidence key="messaging-runtime-core-c04" alt="코드베이스에서 TransportDelivery 를 검색한 출력 6줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="TransportDelivery 코드베이스 검색 — 6줄 · exit 0" zoom="true" -::: - -## 세대 설치가 조용히 반환하는 지점 - -**세대 설치(조립됨)** — `InitializingBean` → `transport.getIfAvailable()` → null이면 조용히 반환(이유가 주석에 있음) → `new TransportMessagingRuntime(brokerName, 1L, transport)` → `runtimes.install(...)`. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-runtime-core-c06.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-runtime-core-c06.md deleted file mode 100644 index c15977b..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-runtime-core-c06.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-runtime-core-c06 -title: 지역 변수 하나만 람다가 복사해 캡처한다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-runtime-core-c06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-runtime-core-c06 - file: ../../../final/evidence/rendered/messaging-runtime-core-c06.svg -evidence: - - ../../../final/evidence/raw/messaging-runtime-core-c06.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-runtime-core#L429 이다. -module: messaging-runtime-core ---- - -# 지역 변수 하나만 람다가 복사해 캡처한다 - -`DefaultMessagePublisher` 자체는 불변이고 필드 여덟이 전부 final 협력자다. `lease`만 메서드 지역 변수다. - -## 관계 - -- **만들어 두고 흘리지 않는 진단값은 진단이 아니다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **증가한다고 문서화한 값이 리터럴이면 그 사실을 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **안정 코드는 운영자의 행동이 갈리는 지점마다 나눈다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **`CompletionStage`를 반환하는 메서드는 동기적으로 던지지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -`DefaultMessagePublisher` 자체는 불변이고 상태를 갖지 않는다 — 필드 여덟이 전부 final 협력자다. `lease`만 메서드 지역 변수이고 `handle` 람다가 `held`라는 effectively-final 복사본으로 캡처한다. - -## DefaultMessagePublisher 참조 위치 - -:::evidence key="messaging-runtime-core-c06" alt="코드베이스에서 DefaultMessagePublisher 를 검색한 출력 22줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DefaultMessagePublisher 코드베이스 검색 — 22줄 · exit 0" zoom="true" -::: - -## 수명주기 참여는 하나뿐이다 - -`TransportMessagingRuntime.close()`뿐이고, 그것을 부르는 것은 registry(회전 시)와 컨텍스트 종료 두 경로다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-schema-api-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-schema-api-c01.md deleted file mode 100644 index f2b5fed..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-schema-api-c01.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-schema-api-c01 -title: codec은 닫힌 registry에 대해서만 동작한다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-schema-api-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-schema-api-c01 - file: ../../../final/evidence/rendered/messaging-schema-api-c01.svg - - key: messaging-schema-api-c01-diagram - file: ../../../final/assets/diagrams/messaging-schema-api-c01.svg -evidence: - - ../../../final/evidence/raw/messaging-schema-api-c01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-schema-api#L62 이다. -module: messaging-schema-api ---- - -# codec은 닫힌 registry에 대해서만 동작한다 - -이 leaf는 "바이트를 어떻게 만들고 읽는가"의 계약을 소유하고 실제 포맷 구현은 갖지 않는다. - -## 관계 - -- **port 계약은 동시성 요구를 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **도달성 판정은 단어가 아니라 import로 확인한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -이 leaf는 **"바이트를 어떻게 만들고 읽는가"의 계약**을 소유한다. 실제 포맷 구현은 갖지 않는다 — 단 하나의 예외가 `RawBytesMessageCodec`이고, 그것은 포맷이 아니라 포맷의 부재를 구현한다. - -## 계약과 포맷 구현의 소재 - -:::evidence key="messaging-schema-api-c01-diagram" alt="리프 경계 안에 codec 계약과 닫힌 registry 가 들어 있고 포맷별 벤더가 경계 밖 점선 상자로 놓인 구조" caption="계약과 포맷 구현의 소재" zoom="false" -::: - -경계 규칙 하나가 모든 곳에 반복된다 — **codec은 닫힌 registry에 대해서만 동작한다.** - -## RawBytesMessageCodec 참조 위치 - -:::evidence key="messaging-schema-api-c01" alt="코드베이스에서 RawBytesMessageCodec 를 검색한 출력 6줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RawBytesMessageCodec 코드베이스 검색 — 6줄 · exit 0" zoom="true" -::: - -## 벤더 의존이 없다 - -`build.gradle`는 `api project(':messaging:messaging-core-api')` 하나뿐이고 vendor 의존성이 없다. 포맷별 vendor(`jackson`, `avro`, `protobuf`)는 각자 leaf가 갖는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-schema-api-c05.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-schema-api-c05.md deleted file mode 100644 index a7fc709..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-schema-api-c05.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-schema-api-c05 -title: 프로그래밍 오류를 MessagingException 밖에 둔다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-schema-api-c05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-schema-api-c05 - file: ../../../final/evidence/rendered/messaging-schema-api-c05.svg -evidence: - - ../../../final/evidence/raw/messaging-schema-api-c05.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-schema-api#L267 이다. -module: messaging-schema-api ---- - -# 프로그래밍 오류를 MessagingException 밖에 둔다 - -이 leaf가 던지는 예외는 셋이고 전부 `messaging-core-api` 소유다. 그 밖에 `IllegalArgumentException`을 던지는 자리가 셋 있다. - -## 관계 - -- **port 계약은 동시성 요구를 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **도달성 판정은 단어가 아니라 import로 확인한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -이 leaf가 던지는 예외는 셋이고 전부 `messaging-core-api` 소유다. `IllegalArgumentException`도 던진다 — `BoundedByteSink` 생성자의 `maxBytes < 1`, `requireFits`의 음수, `SchemaReference`의 빈 subject. - -## IllegalArgumentException 참조 위치 - -:::evidence key="messaging-schema-api-c05" alt="코드베이스에서 IllegalArgumentException 를 검색한 출력 4줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="IllegalArgumentException 코드베이스 검색 — 4줄 · exit 0" zoom="true" -::: - -## 계층 밖인 것이 일관적인 이유 - -이들은 **호출자의 프로그래밍 오류**이고 메시지 실패가 아니므로 `MessagingException` 계층 밖인 것이 일관적이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-schema-api-c06.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-schema-api-c06.md deleted file mode 100644 index 3662ec5..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-schema-api-c06.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-schema-api-c06 -title: 의도적으로 스레드 안전하지 않은 타입이 하나 있다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-schema-api-c06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-schema-api-c06 - file: ../../../final/evidence/rendered/messaging-schema-api-c06.svg -evidence: - - ../../../final/evidence/raw/messaging-schema-api-c06.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-schema-api#L282 이다. -module: messaging-schema-api ---- - -# 의도적으로 스레드 안전하지 않은 타입이 하나 있다 - -`BoundedByteSink`는 의도적으로 thread-safe가 아니다 — javadoc이 명시하고 codec들이 매 `encode` 호출마다 새로 만든다. - -## 관계 - -- **port 계약은 동시성 요구를 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **도달성 판정은 단어가 아니라 import로 확인한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -`BoundedByteSink`가 **의도적으로 thread-safe가 아니다.** javadoc이 명시한다 — "Not thread-safe, and not meant to be: an instance belongs to a single encode call." 실제로 codec들이 매 `encode` 호출마다 새로 만든다. - -## BoundedByteSink 참조 위치 - -:::evidence key="messaging-schema-api-c06" alt="코드베이스에서 BoundedByteSink 를 검색한 출력 15줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="BoundedByteSink 코드베이스 검색 — 15줄 · exit 0" zoom="true" -::: - -## 불변인 것과 상태가 없는 것 - -`EncodedMessage`, `MessageContractKey`, `SchemaReference`는 불변이다. `SchemaCompatibilityValidator`는 registry 참조만 갖고 상태가 없다. - -## 이 리프가 규정하지 않는 것 - -`MessageCodecRegistry`/`SchemaRegistry` 구현의 스레드 안전성은 이 leaf가 규정하지 않는다 — port javadoc에 그에 대한 요구가 없다. 이것은 §17의 P3 항목이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-schema-avro-c06.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-schema-avro-c06.md deleted file mode 100644 index 2146104..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-schema-avro-c06.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-schema-avro-c06 -title: 재사용 자리에 항상 null을 넘겨 공유 상태를 없앤다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-schema-avro-c06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-schema-avro-c06 - file: ../../../final/evidence/rendered/messaging-schema-avro-c06.svg -evidence: - - ../../../final/evidence/raw/messaging-schema-avro-c06.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-schema-avro#L298 이다. -module: messaging-schema-avro ---- - -# 재사용 자리에 항상 null을 넘겨 공유 상태를 없앤다 - -`AvroMessageCodec`은 불변이고, 인코더·디코더·sink가 전부 호출마다 새로 만들어진다. - -## 관계 - -- **모드 enum을 분기 조건으로 쓰면 각 분기에 테스트를 둔다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **컬렉션 순서가 계약이면 양쪽에서 같은 방향으로 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **안정 코드는 판단 단위로 정하고 구현 단위로 정하지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -`AvroMessageCodec`은 불변이다 — `schemas`가 `Map.copyOf`된 평탄 맵, `maxBytes`는 int. `BoundedByteSink`·`BinaryEncoder`·`DatumReader`·`BinaryDecoder`는 전부 호출마다 새로 만들어진다. - -## AvroMessageCodec 참조 위치 - -:::evidence key="messaging-schema-avro-c06" alt="코드베이스에서 AvroMessageCodec 를 검색한 출력 21줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="AvroMessageCodec 코드베이스 검색 — 21줄 · exit 0" zoom="true" -::: - -## 재사용 자리에 항상 null을 넘긴다 - -`EncoderFactory.get()`/`DecoderFactory.get()`은 Avro의 싱글턴 팩토리이고 스레드 안전하다. 다만 `binaryDecoder(encoded, null)`의 두 번째 인자가 재사용 decoder 자리인데 항상 `null`을 넘긴다 — 재사용하지 않으므로 공유 상태가 없다. 성능을 버리고 안전을 택한 형태다. `AvroCompatibilityGate`는 상태가 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-schema-json-c06.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-schema-json-c06.md deleted file mode 100644 index b68f445..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-schema-json-c06.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-schema-json-c06 -title: 메시지당 유지 메모리를 성능 테스트가 간접 확인한다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-schema-json-c06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-schema-json-c06 - file: ../../../final/evidence/rendered/messaging-schema-json-c06.svg -evidence: - - ../../../final/evidence/raw/messaging-schema-json-c06.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-schema-json#L263 이다. -module: messaging-schema-json ---- - -# 메시지당 유지 메모리를 성능 테스트가 간접 확인한다 - -`JacksonMessageCodec`은 불변이고 `BoundedByteSink`는 매 `encode`마다 새로 만들어져 공유되지 않는다. - -## 관계 - -- **실패 코드는 운영자의 다음 행동이 갈리는 지점마다 나눈다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -`JacksonMessageCodec`은 불변이다 — `registry`는 `Map.copyOf`, `maxBytes`는 int, `mapper`는 빌드 후 재구성되지 않는 Jackson `ObjectMapper`(스레드 안전). `BoundedByteSink`는 매 `encode`마다 새로 만들어지므로 공유되지 않는다. - -## JacksonMessageCodec 참조 위치 - -:::evidence key="messaging-schema-json-c06" alt="코드베이스에서 JacksonMessageCodec 를 검색한 출력 39줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="JacksonMessageCodec 코드베이스 검색 — 39줄 · exit 0" zoom="true" -::: - -## 성능 테스트가 간접 확인하는 것 - -`PlatformOverheadPerformanceTest.aRoundTripDoesNotAllocateAGrowingRetainedSet`이 codec이 메시지별 상태를 보유하지 않음을 간접 확인한다(메시지당 유지 메모리 64바이트 미만). - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-schema-json-c07.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-schema-json-c07.md deleted file mode 100644 index 9976c48..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-schema-json-c07.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-schema-json-c07 -title: 이 리프에서 발견된 문제가 상위 리프의 타입을 만들어냈다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-schema-json-c07 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-schema-json-c07 - file: ../../../final/evidence/rendered/messaging-schema-json-c07.svg -evidence: - - ../../../final/evidence/raw/messaging-schema-json-c07.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-schema-json#L396 이다. -module: messaging-schema-json ---- - -# 이 리프에서 발견된 문제가 상위 리프의 타입을 만들어냈다 - -`JsonContractRegistryTest` 클래스 javadoc이 이 codec에서 만난 두 결함을 남겼고, 둘 다 `messaging-schema-api`가 소유하는 타입으로 고쳐졌다. - -## 관계 - -- **실패 코드는 운영자의 다음 행동이 갈리는 지점마다 나눈다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -`JsonContractRegistryTest` 클래스 javadoc이 이 codec에서 만난 두 결함을 남겼다. - -> "Two defects met in this codec. The registry was keyed on message type alone, so a message labelled v999 was decoded with the v1 class and kept its v999 label — the compatibility gate and the audit record then both described a contract that was never registered. And the size limit was applied to the finished byte array, which reports an oversized payload rather than preventing one." - -## JsonContractRegistryTest 참조 위치 - -:::evidence key="messaging-schema-json-c07" alt="코드베이스에서 JsonContractRegistryTest 를 검색한 출력 1줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="JsonContractRegistryTest 코드베이스 검색 — 1줄 · exit 0" zoom="true" -::: - -## 아래에서 발견된 문제가 위의 타입이 되었다 - -두 결함 다 `messaging-schema-api`가 소유하는 타입(`MessageContractKey`, `BoundedByteSink`)으로 고쳐졌다. 즉 **이 leaf에서 발견된 문제가 상위 leaf의 타입을 만들어냈다.** - -## 같은 configuration이 담은 다른 이력 - -`MessagingCoreAutoConfiguration:420-427`의 주석은 이 codec이 아니라 publisher 조립 결함(MSG-INT-003)을 기록하는데, 같은 configuration 안에 있으므로 조립 이력의 맥락으로 참조할 가치가 있다 — "no configuration produced one … the starter did not depend on that leaf." - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-schema-protobuf-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-schema-protobuf-c02.md deleted file mode 100644 index 0acd72e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-schema-protobuf-c02.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-schema-protobuf-c02 -title: 나가는 의존이 하나도 없다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-schema-protobuf-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-schema-protobuf-c02 - file: ../../../final/evidence/rendered/messaging-schema-protobuf-c02.svg -evidence: - - ../../../final/evidence/raw/messaging-schema-protobuf-c02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-schema-protobuf#L76 이다. -module: messaging-schema-protobuf ---- - -# 나가는 의존이 하나도 없다 - -들어오는 것은 셋이고 나가는 것은 없다. 어떤 leaf의 `allowed_dependencies`에도 없고 starter 목록에도 없다. - -## 관계 - -- **검증되지 않는 스키마 파일은 문서임을 파일 안에 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **신뢰할 수 없는 입력 쪽 경계를 먼저 테스트한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -들어오는 것: `messaging-core-api`(api), `messaging-schema-api`(api), `protobuf-java:4.29.3`(api). 나가는 것: **없다.** 어떤 leaf의 `allowed_dependencies`에도 없고 starter 목록에도 없다. 런타임 배선도 없고 bean도 없다(Spring 주석 0개). - -## 이 기록이 다루는 파일 범위 - -:::evidence key="messaging-schema-protobuf-c02" alt="코드베이스에서 파일 목록을 만든 출력 2줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 2줄 · exit 0" zoom="true" -::: - -## lockfile이 확인하는 실제 해석 - -컴파일/런타임은 4.29.3, annotation processor 경로만 4.33.2다. §12.4에서 저장소 전체의 protobuf 버전 지형을 다룬다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-schema-protobuf-c06.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-schema-protobuf-c06.md deleted file mode 100644 index 5c3e594..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-schema-protobuf-c06.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-schema-protobuf-c06 -title: 평탄한 registry라 얕은 복사 문제가 없다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-schema-protobuf-c06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-schema-protobuf-c06 - file: ../../../final/evidence/rendered/messaging-schema-protobuf-c06.svg -evidence: - - ../../../final/evidence/raw/messaging-schema-protobuf-c06.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-schema-protobuf#L258 이다. -module: messaging-schema-protobuf ---- - -# 평탄한 registry라 얕은 복사 문제가 없다 - -`ProtobufMessageCodec`은 불변이고, `Map.copyOf`가 여기서는 얕은 복사 문제를 만들지 않는다. - -## 관계 - -- **검증되지 않는 스키마 파일은 문서임을 파일 안에 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **신뢰할 수 없는 입력 쪽 경계를 먼저 테스트한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -`ProtobufMessageCodec`은 불변이다 — `contracts`는 `Map.copyOf`, `maxBytes`는 int. `ProtobufMessageContract`는 record이고 `Class`/`Parser` 둘 다 protobuf-java에서 스레드 안전하다. `BoundedByteSink`는 매 encode마다 새로 만들어진다. - -## ProtobufMessageCodec 참조 위치 - -:::evidence key="messaging-schema-protobuf-c06" alt="코드베이스에서 ProtobufMessageCodec 를 검색한 출력 14줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ProtobufMessageCodec 코드베이스 검색 — 14줄 · exit 0" zoom="true" -::: - -## 평탄한 쪽이 결함을 만들지 않았다 - -`Map.copyOf`가 여기서는 **얕은 복사 문제가 없다** — `Map`가 이미 평탄한 한 레벨이다. `AvroMessageCodec`이 중첩 맵을 받아 `flatten`이 필요했던 것과 대비된다. 두 codec이 같은 registry 개념을 다른 형태로 받았고, 평탄한 쪽이 결함을 만들지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-security-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-security-c01.md deleted file mode 100644 index 76d5613..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-security-c01.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-security-c01 -title: 비밀은 참조로만 다루고 역할은 필드로 나눈다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-security-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-security-c01 - file: ../../../final/evidence/rendered/messaging-security-c01.svg - - key: messaging-security-c01-diagram - file: ../../../final/assets/diagrams/messaging-security-c01.svg -evidence: - - ../../../final/evidence/raw/messaging-security-c01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-security#L52 이다. -module: messaging-security ---- - -# 비밀은 참조로만 다루고 역할은 필드로 나눈다 - -이 leaf는 "브로커에 연결하기 전에 무엇이 참이어야 하는가"를 소유한다. 벤더 의존성이 0이고 브로커를 만지지 않는다. - -## 관계 - -- **배선된 게이트는 자기 leaf 레인에서 검증한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 술어가 두 타입에 있으면 하나가 다른 하나를 부른다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **가변 필드로 상태 전이를 표현하면 가시성을 함께 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **맵 갱신 함수 안에서 I/O를 하면 그 지연이 락 범위가 된다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -이 leaf는 **"브로커에 연결하기 전에 무엇이 참이어야 하는가"**를 소유한다. 벤더 의존성이 0이고 브로커를 만지지 않는다 — 어댑터의 security configurer가 이 leaf의 타입을 받아 실제 클라이언트 설정을 만든다. - -## 이 리프가 다루는 것과 넘기는 것 - -:::evidence key="messaging-security-c01-diagram" alt="리프 경계 안에 자격증명 참조와 역할별 분리가 들어 있고 브로커 클라이언트 설정이 경계 밖 점선 상자로 놓인 구조" caption="이 리프가 다루는 것과 넘기는 것" zoom="false" -::: - -## 비밀은 참조로만 다룬다 - -`BrokerCredentialProfile`의 다섯 변형 전부가 `credentialId` 하나만 갖는다 — `SaslScram`, `OAuth2`, `MutualTls`, `UsernamePassword`, `Nkey`. sealed interface이므로 여섯 번째를 만들려면 이 파일을 고쳐야 한다. - -## BrokerCredentialProfile 참조 위치 - -:::evidence key="messaging-security-c01" alt="코드베이스에서 BrokerCredentialProfile 를 검색한 출력 21줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="BrokerCredentialProfile 코드베이스 검색 — 21줄 · exit 0" zoom="true" -::: - -## 타입이 통제의 일부다 - -`CredentialProvider.resolve`가 `char[]`을 반환하고 `CredentialRuntime`이 그것을 참조로 보관하며 `clear()`가 `Arrays.fill(material, '\0')` 후 빈 배열로 교체한다. - -## 역할 분리가 강제된다 - -`BrokerSecurityProfile`이 producer·consumer·admin 세 자격증명을 **별도 필드**로 갖는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-security-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-security-c03.md deleted file mode 100644 index 2203717..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-security-c03.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-security-c03 -title: 한 경합에서 서로 다른 두 결함이 나왔다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-security-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-security-c03 - file: ../../../final/evidence/rendered/messaging-security-c03.svg - - key: messaging-security-c03-diagram - file: ../../../final/assets/diagrams/messaging-security-c03.svg -evidence: - - ../../../final/evidence/raw/messaging-security-c03.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-security#L141 이다. -module: messaging-security ---- - -# 한 경합에서 서로 다른 두 결함이 나왔다 - -`CredentialRuntimeRegistry.resolve`의 key별 single-flight가 이 leaf에서 가장 조밀한 동시성 코드이고, 이전 결함이 주석에 통째로 남아 있다. - -## 관계 - -- **배선된 게이트는 자기 leaf 레인에서 검증한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 술어가 두 타입에 있으면 하나가 다른 하나를 부른다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **가변 필드로 상태 전이를 표현하면 가시성을 함께 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **맵 갱신 함수 안에서 I/O를 하면 그 지연이 락 범위가 된다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -이 leaf에서 가장 조밀한 동시성 코드이고, 이전 결함이 주석에 통째로 남아 있다. - -> "get → fetch → put → clear had no synchronization at all. Two callers rotating the same credential both read the same old runtime and both fetched a replacement: one replacement was lost from the map without ever being cleared — a secret left in memory that nothing owns — and the caller that lost the race could clear material the winner was still using." - -**두 개의 서로 다른 결함이 한 경합에서 나왔다** — 소유자 없는 비밀이 힙에 남는 것과, 사용 중인 자격증명이 지워지는 것. - -## 교체와 소거의 순서 - -:::evidence key="messaging-security-c03-diagram" alt="compute 진입과 provider resolve 와 replacement 설치와 이전 세대 소거가 왼쪽에서 오른쪽으로 이어지는 구조" caption="교체와 소거의 순서" zoom="false" -::: - -`ConcurrentHashMap.compute`가 해당 bin의 락을 잡으므로 fetch가 정확히 한 번 일어난다. 그리고 **`clear()`가 `replacement` 생성 후에 온다** — 주석이 그 순서의 이유를 적는다: "no reader sees a window with no usable credential — and only by the thread that replaced it, so the material a concurrent reader holds is never wiped underneath it." - -## ConcurrentHashMap 참조 위치 - -:::evidence key="messaging-security-c03" alt="코드베이스에서 ConcurrentHashMap 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ConcurrentHashMap 코드베이스 검색 — 2줄 · exit 0" zoom="true" -::: - -## 외부 I/O가 bin 락을 잡은 채로 일어난다 - -`compute`의 람다 안에서 `provider.resolve(...)`가 호출된다. 같은 credential id를 요청하는 다른 스레드는 그 동안 막히고, `ConcurrentHashMap` 문서는 compute 람다 안에서 같은 맵을 갱신하지 말라고 요구한다(여기서는 지켜진다). 다른 키는 다른 bin이면 막히지 않지만 해시 충돌 시 같은 bin이면 막힌다. §17. - -## 별도 플래그 없이 같은 필드로 상태를 표현한다 - -소거 판정이 `material.length == 0`이다. 생성자가 빈 배열을 거절하므로(`"credential material must not be empty"`) 길이 0은 소거된 상태를 뜻한다. - -## volatile이 아닌 필드가 남긴 위험 - -**`material` 필드가 `volatile`이 아니다.** `clear()`가 다른 스레드에서 호출되면 `material()`이 옛 참조를 볼 수 있다. 실제 경로에서는 `compute` 안에서만 `clear()`가 불리고 그 전에 `replacement`가 맵에 들어가므로 위험이 낮지만, `clearAll()`은 락 없이 순회한다. §17. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-security-c04.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-security-c04.md deleted file mode 100644 index 52a356e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-security-c04.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-security-c04 -title: 시작 검증 호출 지점이 두 곳으로 갈린다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-security-c04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-security-c04 - file: ../../../final/evidence/rendered/messaging-security-c04.svg -evidence: - - ../../../final/evidence/raw/messaging-security-c04.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-security#L327 이다. -module: messaging-security ---- - -# 시작 검증 호출 지점이 두 곳으로 갈린다 - -자격증명 해석과 두 갈래 시작 검증, 그리고 발행 권한 확인이 각각 다른 소유자를 갖는다. - -## 관계 - -- **배선된 게이트는 자기 leaf 레인에서 검증한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 술어가 두 타입에 있으면 하나가 다른 하나를 부른다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **가변 필드로 상태 전이를 표현하면 가시성을 함께 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **맵 갱신 함수 안에서 I/O를 하면 그 지연이 락 범위가 된다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -**자격증명 해석** — 어댑터의 security configurer → `registry.resolve(credentialId, now)` → 캐시 유효하면 반환 → 아니면 `compute` 안에서 `provider.resolve` + `provider.expiresAt` → 새 `CredentialRuntime` 설치 → 옛 것 `clear()`. - -**시작 검증(1)** — starter가 `MessageSecurityValidator` bean 생성 → `validate(profile)` 호출 지점은 starter가 소유. - -**시작 검증(2)** — 어댑터 configurer가 `BrokerTlsPolicy.validate(profile, enabledProtocols)` 호출. - -**발행 권한** — `DefaultMessagePublisher` → `access.mayPublish(name)` → false면 `PublishResult(REJECTED, PUBLISH_FORBIDDEN)`. - -## CredentialRuntime 참조 위치 - -:::evidence key="messaging-security-c04" alt="코드베이스에서 CredentialRuntime 를 검색한 출력 12줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="CredentialRuntime 코드베이스 검색 — 12줄 · exit 0" zoom="true" -::: - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-security-c06.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-security-c06.md deleted file mode 100644 index cc94048..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-security-c06.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-security-c06 -title: 종료 시 자격증명을 소거하는 코드가 없다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-security-c06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-security-c06 - file: ../../../final/evidence/rendered/messaging-security-c06.svg -evidence: - - ../../../final/evidence/raw/messaging-security-c06.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-security#L359 이다. -module: messaging-security ---- - -# 종료 시 자격증명을 소거하는 코드가 없다 - -수명주기 참여는 `clearAll()`뿐이고 javadoc이 "for shutdown"이라고 적는데, 그것을 부르는 코드가 저장소에 없다. - -## 관계 - -- **배선된 게이트는 자기 leaf 레인에서 검증한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 술어가 두 타입에 있으면 하나가 다른 하나를 부른다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **가변 필드로 상태 전이를 표현하면 가시성을 함께 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **맵 갱신 함수 안에서 I/O를 하면 그 지연이 락 범위가 된다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -레코드 여섯(`BrokerSecurityProfile`, `BrokerCredentialProfile` 5변형, `DestinationAccessPolicy`, `BrokerAclManifest`, `CredentialRotationPlan`)은 전부 불변이다. `BrokerTlsPolicy`·`MessageSecurityValidator`·`DestinationAccessValidator`는 상태가 없거나 불변 참조만 갖는다. - -## BrokerSecurityProfile 참조 위치 - -:::evidence key="messaging-security-c06" alt="코드베이스에서 BrokerSecurityProfile 를 검색한 출력 26줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="BrokerSecurityProfile 코드베이스 검색 — 26줄 · exit 0" zoom="true" -::: - -## 부르는 코드가 없는 수명주기 훅 - -수명주기 참여는 `clearAll()`뿐이고 "for shutdown"이라고 javadoc이 적는다. **그것을 부르는 코드가 저장소에 없다** — 종료 시 자격증명이 소거되지 않는다. §17. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-security-c07.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-security-c07.md deleted file mode 100644 index 455335b..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-security-c07.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-security-c07 -title: 거부목록이 실제로 뚫린 기록이 남아 있다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-security-c07 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-security-c07 - file: ../../../final/evidence/rendered/messaging-security-c07.svg -evidence: - - ../../../final/evidence/raw/messaging-security-c07.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-security#L562 이다. -module: messaging-security ---- - -# 거부목록이 실제로 뚫린 기록이 남아 있다 - -두 번째 이력이 `messaging-schema-api` §12.3의 허용목록/거부목록 축과 같은 주제이고, 여기서는 거부목록이 실제로 뚫린 기록이 남아 있다. - -## 관계 - -- **배선된 게이트는 자기 leaf 레인에서 검증한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 술어가 두 타입에 있으면 하나가 다른 하나를 부른다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **가변 필드로 상태 전이를 표현하면 가시성을 함께 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **맵 갱신 함수 안에서 I/O를 하면 그 지연이 락 범위가 된다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -두 번째가 `messaging-schema-api` §12.3의 허용목록/거부목록 축과 같은 주제이고, 여기서는 **거부목록이 실제로 뚫린 기록**이 남아 있다. - -## 이 기록이 다루는 파일 범위 - -:::evidence key="messaging-security-c07" alt="코드베이스에서 파일 목록을 만든 출력 12줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 12줄 · exit 0" zoom="true" -::: - -## 첫 번째가 속한 계열 - -이 저장소가 반복하는 "정확히 한 번" 주제의 보안 판본이다 — `messaging-transport-spi`의 세대 close, `messaging-policy`의 permit 반납과 같은 계열이며, 여기서는 실패의 결과가 **비밀 잔류**다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-spring-cloud-stream-bridge-c06.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-spring-cloud-stream-bridge-c06.md deleted file mode 100644 index b57690e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-spring-cloud-stream-bridge-c06.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-spring-cloud-stream-bridge-c06 -title: 등록된 핸들러를 해제하는 방법이 없다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-spring-cloud-stream-bridge-c06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-spring-cloud-stream-bridge-c06 - file: ../../../final/evidence/rendered/messaging-spring-cloud-stream-bridge-c06.svg -evidence: - - ../../../final/evidence/raw/messaging-spring-cloud-stream-bridge-c06.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-spring-cloud-stream-bridge#L341 이다. -module: messaging-spring-cloud-stream-bridge ---- - -# 등록된 핸들러를 해제하는 방법이 없다 - -각 맵은 스레드 안전하지만 두 맵의 갱신이 원자적이지 않고, 수명주기 참여가 없어 등록을 되돌릴 수 없다. - -## 관계 - -- **허용 의존 목록은 상한이므로 미사용을 잡지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **등록을 받는 컴포넌트는 해제도 제공한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **함께 읽히는 두 맵은 한 값으로 묶는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **에러 메시지가 지시하는 설정은 그 설정을 읽는 코드와 함께 존재해야 한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **한 개념의 등록 상태를 두 객체가 나눠 갖지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -각 맵은 스레드 안전하지만 **두 맵의 갱신이 원자적이지 않다**(§4.5). 정산이나 자원 해제가 없으므로 다른 동시성 지점은 없다. - -## StreamBridgePolicyGuard 참조 위치 - -:::evidence key="messaging-spring-cloud-stream-bridge-c06" alt="코드베이스에서 StreamBridgePolicyGuard 를 검색한 출력 4줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="StreamBridgePolicyGuard 코드베이스 검색 — 4줄 · exit 0" zoom="true" -::: - -## 수명주기 참여가 없어서 되돌릴 수 없다 - -`StreamBridgePolicyGuard`·`BindingProfileValidator`는 상태가 없다. 수명주기 참여 없음 — `close()`나 `stop()`이 없다. 등록된 핸들러를 해제하는 방법이 없다. §17. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-testkit-c07.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-testkit-c07.md deleted file mode 100644 index fd3528f..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-testkit-c07.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-testkit-c07 -title: 가변인데 공유되지 않아서 안전한 것들 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-testkit-c07 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-testkit-c07 - file: ../../../final/evidence/rendered/messaging-testkit-c07.svg -evidence: - - ../../../final/evidence/raw/messaging-testkit-c07.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-testkit#L539 이다. -module: messaging-testkit ---- - -# 가변인데 공유되지 않아서 안전한 것들 - -트랜잭션이 없고 동시성 관련해서 세 가지를 확인했다. 가변 타입이 둘 있지만 실제 사용 패턴에서 공유되지 않는다. - -## 본문 - - - -트랜잭션 없음. 동시성 관련해서 세 가지를 확인했다. - -**`CertifiedEvidence.RECORDED` 는 `static final` 이며 클래스 초기화 시 1회 로드된다**(`:34`). JVM 클래스 초기화 락이 스레드 안전을 보장하고, 반환되는 `List` 는 `Stream.toList()` 결과라 불변이다. 테스트가 병렬로 돌아도 안전하다. - -## CertifiedEvidence 참조 위치 - -:::evidence key="messaging-testkit-c07" alt="코드베이스에서 CertifiedEvidence 를 검색한 출력 21줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="CertifiedEvidence 코드베이스 검색 — 21줄 · exit 0" zoom="true" -::: - -## 가변인데 공유되지 않는다 - -**`BrokerFailureMatrix` 는 가변이고 동기화가 없다**(`LinkedHashMap`, `:23`). 그러나 `from(...)` 이 매번 새 인스턴스를 만들고 그 안에서만 `record(...)` 를 호출한 뒤 반환하므로, 실제 사용 패턴에서 공유되는 인스턴스가 없다. `CrossBrokerContractSuite` 는 필드 하나(`:34`)를 갖지만 JUnit5 기본 생명주기가 메서드당 인스턴스라 매 테스트가 자기 행렬을 만든다. `public BrokerFailureMatrix record(...)` 가 노출되어 있어 원리상 외부에서 공유·변형할 수 있으나, 실제 그런 호출부는 0건이다. - -## 결정론이 목적인 비동기화 - -**`InMemoryMessagingHarness` 는 전부 비동기화 컬렉션**(`ArrayDeque`, `ArrayList`, `LinkedHashSet`)이고 `CompletableFuture.completedFuture(...)` 로 즉시 완료한다. 결정론이 목적이므로 옳다 — 실제 스레드 전환이 하나도 없다. - -## 플래그만 바꾸는 구현을 통과시키지 않는다 - -수명주기는 `MessagingAdapterHarness` 의 두 메서드에 압축되어 있다. `stopsAcceptingNewWorkDuringShutdown` 이 `beginShutdown()` 후 `isAcceptingWork()==false` 와 발행 로컬 거절 둘 다를 요구한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-testkit-c08.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-testkit-c08.md deleted file mode 100644 index 5c10389..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-testkit-c08.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-testkit-c08 -title: 자기 자신을 검증하는 상수가 여섯 얼굴로 나타났다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-testkit-c08 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-testkit-c08 - file: ../../../final/evidence/rendered/messaging-testkit-c08.svg -evidence: - - ../../../final/evidence/raw/messaging-testkit-c08.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-testkit#L848 이다. -module: messaging-testkit ---- - -# 자기 자신을 검증하는 상수가 여섯 얼굴로 나타났다 - -커밋 메시지는 정보가 거의 없지만 코드 주석이 커밋 로그를 대신한다. 세 javadoc이 남긴 것이 하나의 결함이다. - -## 본문 - - - -커밋 메시지는 정보가 거의 없다. 그러나 이 리프는 **코드 주석이 커밋 로그를 대신하는 드문 사례**다. 세 개의 javadoc 이 각각 "무엇이 틀렸었고 왜 지금 형태인가" 를 남겼다. - -## CompatibilityMatrixTest 참조 위치 - -:::evidence key="messaging-testkit-c08" alt="코드베이스에서 CompatibilityMatrixTest 를 검색한 출력 1줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="CompatibilityMatrixTest 코드베이스 검색 — 1줄 · exit 0" zoom="true" -::: - -## 여섯 곳이 같은 결함의 여섯 얼굴이다 - -**자기 자신을 검증하는 상수.** 그리고 여섯 곳 모두 지금은 매니페스트를 가리킨다. - -## 강등의 근거가 적혀 있다 - -`messaging-rabbit` 이 Stable 에서 Experimental 로 **강등된 흔적**도 남아 있다 — `CompatibilityMatrixTest.theStableSetIsExactlyWhatALaneHasCertified` 의 `.as("RabbitMQ passes the shared contract, but no fault scenario has been run against it")`. 강등의 근거가 "계약은 통과하지만 결함 증거가 없다" 로 정확히 적혀 있다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-transport-spi-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-transport-spi-c01.md deleted file mode 100644 index 8254f47..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-transport-spi-c01.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-transport-spi-c01 -title: 어댑터가 코덱도 한도도 스스로 고르지 않는다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-transport-spi-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-transport-spi-c01 - file: ../../../final/evidence/rendered/messaging-transport-spi-c01.svg - - key: messaging-transport-spi-c01-diagram - file: ../../../final/assets/diagrams/messaging-transport-spi-c01.svg -evidence: - - ../../../final/evidence/raw/messaging-transport-spi-c01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-transport-spi#L62 이다. -module: messaging-transport-spi ---- - -# 어댑터가 코덱도 한도도 스스로 고르지 않는다 - -브로커 어댑터가 구현할 SPI와 그 어댑터들의 수명주기·세대 관리를 소유한다. 벤더 의존성이 0이다. - -## 관계 - -- **멱등 종료를 보장하는 컴포넌트는 종료 이후의 등록도 정의한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 개념의 sentinel은 계층을 넘어 하나로 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -브로커 어댑터가 구현할 **SPI**와, 그 어댑터들의 **수명주기·세대 관리**를 소유한다. 벤더 의존성이 0이다. - -## 시그니처에 나오는 것과 나오지 않는 것 - -:::evidence key="messaging-transport-spi-c01-diagram" alt="리프 경계 안에 SPI 타입과 세대 수명주기가 들어 있고 브로커 네이티브 타입과 코덱 선택이 경계 밖 빗금 상자로 놓인 구조" caption="시그니처에 나오는 것과 나오지 않는 것" zoom="false" -::: - -가장 중요한 경계 규칙이 `MessagingTransport`의 javadoc에 있다 — 13개 타입 중 어느 것도 브로커 네이티브 타입을 시그니처에 노출하지 않는다. `BrokerPosition`(core-api)이 `Map diagnosticAttributes()`로 좌표를 문자열로만 내보내는 것과 같은 규율이다. - -## MessagingTransport 참조 위치 - -:::evidence key="messaging-transport-spi-c01" alt="코드베이스에서 MessagingTransport 를 검색한 출력 6줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingTransport 코드베이스 검색 — 6줄 · exit 0" zoom="true" -::: - -## 두 번째 경계는 인코딩 위치다 - -`TransportPublishRequest`도 대칭이다 — "The payload arrives already encoded and the profile arrives already validated, so an adapter never chooses a codec or a limit for itself. That is what keeps two adapters from disagreeing about what 'the same message' means." - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-transport-spi-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-transport-spi-c03.md deleted file mode 100644 index 1e42bfb..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-transport-spi-c03.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-transport-spi-c03 -title: 회전은 변경이 아니라 교체다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-transport-spi-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-transport-spi-c03 - file: ../../../final/evidence/rendered/messaging-transport-spi-c03.svg - - key: messaging-transport-spi-c03-diagram - file: ../../../final/assets/diagrams/messaging-transport-spi-c03.svg -evidence: - - ../../../final/evidence/raw/messaging-transport-spi-c03.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-transport-spi#L124 이다. -module: messaging-transport-spi ---- - -# 회전은 변경이 아니라 교체다 - -자격증명 회전과 토폴로지 재적재는 살아 있는 세대를 바꾸지 않고 통째로 교체한다. 진행 중인 발행은 시작한 세대를 유지한다. - -## 관계 - -- **멱등 종료를 보장하는 컴포넌트는 종료 이후의 등록도 정의한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 개념의 sentinel은 계층을 넘어 하나로 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -세대 모델의 근거가 javadoc에 있다. - -> "Credential rotation and topology reload replace a whole generation rather than mutating a live one. In-flight publishes keep the generation they started on, which is what makes a rotation invisible to callers instead of a burst of authentication failures." - -## 회전이 교체인 순서 - -:::evidence key="messaging-transport-spi-c03-diagram" alt="새 세대 설치와 옛 세대 은퇴와 lease 반환 대기와 정확히 한 번 close 가 왼쪽에서 오른쪽으로 이어지는 구조" caption="회전이 교체인 순서" zoom="false" -::: - -`ConcurrentHashMap.put`이 원자적이므로 호출자는 옛 세대 또는 새 세대만 본다 — javadoc: "never a half-rebuilt connection pool". `computeIfPresent`의 리맵 함수가 **버킷 잠금 안에서** 실행되므로, 조회와 증가가 원자적이다. `get` 후 증가였다면 그 사이에 `install`이 세대를 교체해 이미 은퇴한 세대의 계수를 올릴 수 있다. - -## ConcurrentHashMap 참조 위치 - -:::evidence key="messaging-transport-spi-c03" alt="코드베이스에서 ConcurrentHashMap 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ConcurrentHashMap 코드베이스 검색 — 2줄 · exit 0" zoom="true" -::: - -## 두 층의 멱등성 - -`closed`가 CAS로 보호되므로 **정확히 한 번만** `runtime.close()`가 불린다. 테스트가 그것을 직접 단언한다(`aRetiredGenerationIsClosedExactlyOnce`, `as("a second close on a real connection pool throws from a shutdown hook")`). `Lease.close()`도 자체 `AtomicBoolean released`로 멱등이다. - -## 고쳐진 세 결함 - -**세대별 은퇴 시각** — 하나의 타임스탬프를 전체 목록에 적용하면 회전이 겹칠 때 판정이 호출자가 우연히 넘긴 값에 좌우된다. **닫힌 세대의 목록 제거** — `drainingCount()`가 관측 지표이므로, 이미 닫힌 세대가 목록에 남으면 지표가 영원히 0으로 안 떨어진다. **`close()`가 현재 세대까지 닫는다** — 회전 없이 종료하는 프로세스(=대부분의 프로세스)가 연결을 정리하지 않았다. - -## 남은 미세 결함과 이중 검사 - -`close()`가 `draining`은 `synchronized`로 비우지만 `current`는 `List.copyOf(current.keySet())` 후 하나씩 `remove`한다. 그 사이에 `install`이 새 세대를 넣으면 그 세대는 닫히지 않는다. 종료 중 설치는 정상 시나리오가 아니므로 실질 위험은 낮다 — §17의 P3. `tryBeginWork`는 **이중 검사**다 — 증가 후 다시 확인해서 증가와 `beginDrain` 사이의 경합에서 계수를 되돌린다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-transport-spi-c04.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-transport-spi-c04.md deleted file mode 100644 index 1ab7a6a..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-transport-spi-c04.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-transport-spi-c04 -title: 선언된 종료 순서와 실제 종료 경로 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-transport-spi-c04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-transport-spi-c04 - file: ../../../final/evidence/rendered/messaging-transport-spi-c04.svg -evidence: - - ../../../final/evidence/raw/messaging-transport-spi-c04.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-transport-spi#L326 이다. -module: messaging-transport-spi ---- - -# 선언된 종료 순서와 실제 종료 경로 - -발행·수신·회전·종료 네 경로가 있고, 종료는 실제 경로가 따로 있다. - -## 관계 - -- **멱등 종료를 보장하는 컴포넌트는 종료 이후의 등록도 정의한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 개념의 sentinel은 계층을 넘어 하나로 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -**발행** — 상위(`DefaultMessagePublisher`)가 `TransportPublishRequest`를 만들어 `MessagingTransport.publish` → 어댑터가 `TransportPublishResult(PublishResult)` 반환. - -**수신** — 상위가 `TransportConsumerSpec(profile, sink)`로 `register` → 어댑터가 메시지마다 `sink.apply(TransportDelivery)` → 상위가 `TransportSettlement`으로 정산. - -## DefaultMessagePublisher 참조 위치 - -:::evidence key="messaging-transport-spi-c04" alt="코드베이스에서 DefaultMessagePublisher 를 검색한 출력 22줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DefaultMessagePublisher 코드베이스 검색 — 22줄 · exit 0" zoom="true" -::: - -## 회전과 실제 종료 경로 - -**회전** — 새 `MessagingRuntime` 생성 → `registry.install(runtime, now)` → 옛 세대 `retire` → lease가 0이면 즉시 close, 아니면 `draining`에 적재 → 스케줄러가 `closeExpiredDraining(now)` 호출. - -**종료(실제 경로)** — `MessagingShutdownLifecycle.stop()` → `admission.stopAcceptingNewWork()` → `drain.beginDrain(now)` → 50 ms 폴링으로 `isDrained` 대기 → 마감 도달 시 중단. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-transport-spi-c05.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-transport-spi-c05.md deleted file mode 100644 index bc4989b..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-transport-spi-c05.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-transport-spi-c05 -title: 포기가 조용하지 않다는 것이 설계다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-transport-spi-c05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-transport-spi-c05 - file: ../../../final/evidence/rendered/messaging-transport-spi-c05.svg -evidence: - - ../../../final/evidence/raw/messaging-transport-spi-c05.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-transport-spi#L338 이다. -module: messaging-transport-spi ---- - -# 포기가 조용하지 않다는 것이 설계다 - -이 leaf가 직접 던지는 예외는 하나다. 나머지는 IllegalArgumentException(생성자 인자 검증)과 NullPointerException(Objects.requireNonNull)이다. - -## 관계 - -- **멱등 종료를 보장하는 컴포넌트는 종료 이후의 등록도 정의한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 개념의 sentinel은 계층을 넘어 하나로 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -이 leaf가 직접 던지는 예외는 **하나**다. 나머지는 `IllegalArgumentException`(생성자 인자 검증)과 `NullPointerException`(`Objects.requireNonNull`)이다. - -## IllegalArgumentException 참조 위치 - -:::evidence key="messaging-transport-spi-c05" alt="코드베이스에서 IllegalArgumentException 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="IllegalArgumentException 코드베이스 검색 — 2줄 · exit 0" zoom="true" -::: - -## 실패의 대부분이 예외가 아니라 상태다 - -드레인 마감 초과는 `abandonedWorkAtDeadline(now)`가 true를 반환하는 것이고, 세대 강제 종료는 `closeExpiredDraining`의 반환 계수다. - -## 포기가 조용하지 않다 - -마감에 도달한 작업은 정산되지 않은 채 버려지고, 브로커가 재전달한다. `GracefulShutdownCoordinator` javadoc — "rather than the platform pretending it completed." - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-transport-spi-c06.md b/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-transport-spi-c06.md deleted file mode 100644 index 659a810..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-machines-and-ownership/concept/concept-messaging-transport-spi-c06.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-transport-spi-c06 -title: 두 자료구조가 다른 방식으로 보호되고 주석이 없다 -topic: state-machines-and-ownership -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-transport-spi-c06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-transport-spi-c06 - file: ../../../final/evidence/rendered/messaging-transport-spi-c06.svg -evidence: - - ../../../final/evidence/raw/messaging-transport-spi-c06.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-transport-spi#L352 이다. -module: messaging-transport-spi ---- - -# 두 자료구조가 다른 방식으로 보호되고 주석이 없다 - -이 leaf는 messaging family에서 동시성 밀도가 가장 높다. `current`는 lock-free로, `draining`은 `synchronized`로 다룬다. - -## 관계 - -- **멱등 종료를 보장하는 컴포넌트는 종료 이후의 등록도 정의한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 개념의 sentinel은 계층을 넘어 하나로 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -이 leaf는 messaging family에서 **동시성 밀도가 가장 높다.** - -## DefaultMessagingRuntimeRegistry 참조 위치 - -:::evidence key="messaging-transport-spi-c06" alt="코드베이스에서 DefaultMessagingRuntimeRegistry 를 검색한 출력 25줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DefaultMessagingRuntimeRegistry 코드베이스 검색 — 25줄 · exit 0" zoom="true" -::: - -## 두 자료구조가 다른 방식으로 보호된다 - -**주목할 비대칭:** `DefaultMessagingRuntimeRegistry`가 `current`는 lock-free(`ConcurrentHashMap`)로, `draining`은 `synchronized ArrayList`로 다룬다. `draining`은 회전 때만 접근하므로 경합이 없다 — 합리적 선택이지만 주석이 없다. - -## 선언된 것과 실현된 것 - -수명주기는 §4.4의 8단계가 **선언**이고 §12.1이 실현 상태를 다룬다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-a05-f029-for-update-skip-locked.md b/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-a05-f029-for-update-skip-locked.md deleted file mode 100644 index 38479b9..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-a05-f029-for-update-skip-locked.md +++ /dev/null @@ -1,184 +0,0 @@ ---- -kind: CASE -slug: a05-f029-for-update-skip-locked -title: SKIP LOCKED 로 잡은 조정 작업을 두 번째 연결이 그대로 받는다 -topic: state-ownership-and-concurrency -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a05-f029-for-update-skip-locked -evidenceCapturedOn: 2026-09-02 -assets: - - key: a05-f029-for-update-skip-locked - file: ../../../final/evidence/rendered/a05-f029-for-update-skip-locked.svg - - key: a05-f029-for-update-skip-locked-probe - file: ../../../final/evidence/rendered/a05-f029-for-update-skip-locked-probe.svg -evidence: - - ../../../final/evidence/raw/a05-f029-for-update-skip-locked.txt - - ../../../final/evidence/raw/a05-f029-for-update-skip-locked-probe.txt -source: - - 원본 분석 절은 `final/document.md#a05` §90 이다. 등급은 P2 이고, 잠금 수명과 작업 수명이 다르다는 판정과 두 작업자 시나리오 탐침이 그 절에 있다. 그 절도 수정으로 짧은 청구 쪽을 택한다. - - 표에 소유자와 리스 열이 아예 없다는 것, 같은 패키지의 전달 큐 청구문과의 대조, 그리고 두 도달 조건은 이 기록에서 확인했다. ---- - -# SKIP LOCKED 로 잡은 조정 작업을 두 번째 연결이 그대로 받는다 - -조정 작업 청구는 `FOR UPDATE SKIP LOCKED` 로 끝나는 SELECT 하나다. 트랜잭션도 열지 않고 표에 소유자나 리스를 쓰지도 않으므로, 그 조회가 끝나면서 잠금도 사라진다. 같은 패키지의 전달 큐는 같은 잠금 구문 뒤에 `UPDATE` 를 붙여 청구를 행에 남긴다. - -## 관계 - -- **조건부 update로 행을 claim하고 읽은 값으로 판단하지 않는다** - 이 사례가 위반하는 규칙이다. -- **fenced lease — 만료 시각만으로는 부족한 이유** - 수정 방향이 기대는 구조다. -- **CAS 튜플을 where 절에 전부 반복하고 update count를 답으로 쓴다** - 조건을 where 절에 전부 넣고 갱신 결과로 판단하라는 규칙이다. - -## 문제 - -청구 저장소의 javadoc 은 잠금 구문을 쓰는 이유를 전달 큐와 같다고 적는다. 같은 표를 폴링하는 두 작업자가 같은 작업을 둘 다 가져가면 안 된다는 것이다. - -## 결론 - -청구 메서드는 질의 하나를 실행하고 끝난다. 그 클래스에서 트랜잭션을 여는 애너테이션도 포트 호출도 찾을 수 없다. 자동 커밋이면 SELECT 종료와 함께 암묵 커밋이 걸리고 행 잠금도 그 자리에서 풀린다. - -표 쪽에도 남길 자리가 없다. 조정 작업 표의 열은 다음 확인 시각과 시도 횟수와 마지막 결과뿐이고, 이 표를 건드리는 마이그레이션은 만든 것과 색인 하나와 완결성 검사용 이름 리터럴이 전부다. - -작업자도 전체 구간을 감싸지 않는다. 만기 조회로 목록을 받은 뒤, 제공자를 다녀오고 나서야 상태를 쓴다. - -실제 PostgreSQL 16 에서 자동 커밋 연결 둘이 그 문장을 차례로 실행했다. 사이에 정산은 없다. 둘 다 같은 작업을 받았고 행 상태는 그대로다. 겹쳐 실행하지 않아도 재현된다는 것이 잠금이 이미 사라졌다는 뜻이다. - -같은 패키지의 전달 큐는 다르게 한다. 이쪽은 잠금 구문을 CTE 로 감싸고 한 문장 안에서 소유자·리스 만료·펜스를 갱신하며 상태를 배송 중으로 바꾼다. 청구가 행에 남는 시점이 잠금이 풀리기 전이다. 조정 쪽에서만 그 쓰기가 생략됐다. - -여기서 일어나는 일이 전송이 아니므로 판정을 한 단계 낮췄다. 깨지는 것은 javadoc 이 적어 둔 계약 쪽이다. - -## 검증 환경 - -OpenJDK : 21.0.12 -데이터베이스 : PostgreSQL 16.15, 실제 실행 -확인 방식 : 청구 메서드와 작업자의 경계 선언 확인, 표를 건드리는 마이그레이션 전수, 실제 PostgreSQL 에서 두 연결로 같은 문장 실행 -소스 수정 : x - -ca-skeleton.notification.platform.enabled 가 참이고 모드가 SERVING 이며 인스턴스가 둘 이상인 배포다. 출하 기본값이 거짓이고, 프로세스가 하나면 스케줄러가 자기 겹침을 막는다. - -## 재현 조건 - -1. 청구 저장소의 javadoc 과 청구 문장, 그리고 그것을 실행하는 메서드를 읽는다. -2. 그 클래스와 작업자 클래스의 트랜잭션 경계 선언을 센다. -3. 조정 작업 표의 열과, 그 표를 건드리는 마이그레이션을 전부 나열한다. -4. 같은 패키지의 전달 큐 청구문을 읽는다. -5. PostgreSQL 에 알림 플랫폼 마이그레이션을 적용하고 만기 작업을 하나 넣는다. -6. 자동 커밋 연결 둘로 그 문장을 차례로 실행하고 받은 작업과 행 상태를 비교한다. - -## 본문 - - - -조정 작업 저장소는 청구 질의를 `FOR UPDATE SKIP LOCKED` 로 끝낸다. javadoc 이 이유를 적어 두었다. - -```text - *

Claiming is {@code FOR UPDATE SKIP LOCKED} inside the select, for the same reason the delivery - * queue is: two workers polling the same table must not both take the same job. -``` - -## 청구 메서드는 트랜잭션을 열지 않는다 - -:::evidence key="a05-f029-for-update-skip-locked" alt="청구 저장소의 javadoc 과 청구 문장과 그것을 실행하는 메서드, 그 클래스의 트랜잭션 경계 선언 수, 같은 패키지 전달 큐의 청구문 전문, 작업자의 처리 순서와 그 클래스의 경계 선언 수, 조정 작업 표의 열 정의와 그 표를 건드리는 마이그레이션 전부, 그리고 알림 플랫폼 마스터 스위치의 출하 기본값과 배경 작업자 스케줄러의 스레드 수를 출력한 터미널 기록." caption="javadoc 은 전달 큐와 같은 이유라고 적음 · 청구는 질의 하나, 경계 선언 0 · 전달 큐는 같은 잠금 구문에 UPDATE 를 붙여 소유자·리스·펜스를 씀 · 표에는 그 열이 없음 · 마스터 스위치 기본값 false, 스케줄러 스레드 1 — 97줄 · exit 0" zoom="true" -::: - -```java -public List claimDue(int limit, Instant now) { - Objects.requireNonNull(now, "now"); - if (limit < 1) { - throw new IllegalArgumentException("limit"); - } - return jdbc.query(CLAIM_DUE, MAPPER, Timestamp.from(now), limit); -} -``` - -그 클래스에는 `@Transactional` 도 `TransactionPort` 호출도 없다. 자동 커밋에서는 이 SELECT 가 끝나면서 암묵 커밋이 일어나고, 행 잠금은 거기서 사라진다. - -표에도 남길 곳이 없다. 조정 작업 표에는 소유자도 리스도 상태도 없다. 다음 확인 시각과 시도 횟수와 마지막 결과가 전부다. 이 표를 건드리는 마이그레이션은 만든 것과 색인 하나, 그리고 완결성 검사용 이름 리터럴뿐이다. - -## 작업자는 조회와 정산 사이에 제공자를 부른다 - -```java -public int reconcileOnce() { - List due = jobs.claimDue(batchSize, clock.instant()); - int settled = 0; - for (ReconciliationJob job : due) { - // One job's failure is not the pass's: a provider that is refusing connections would - // otherwise stop every other provider's jobs behind it. - try { - if (handle(job)) { - settled++; - } - } catch (RuntimeException failure) { - jobs.reschedule( - job, failure.getClass().getSimpleName(), clock.instant().plus(retryBackoff)); - } - } - return settled; -} -``` - -목록을 먼저 받고, 반복문 안에서 제공자 조정을 수행한 뒤에야 완료나 재예약을 부른다. 작업자 클래스도 마찬가지로 경계 선언이 없다. - -## 두 연결이 같은 작업을 받는다 - -:::evidence key="a05-f029-for-update-skip-locked-probe" alt="실제 PostgreSQL 컨테이너에 알림 플랫폼 마이그레이션을 적용해 상위 표들이 세워진 것을 확인하고, 만기 작업 하나를 넣은 뒤 자동 커밋 연결 두 개가 저장소의 청구 문장을 차례로 실행해 각각 받은 작업 식별자와 그 둘이 같은지, 그리고 행 상태를 출력한 터미널 기록. 상위 행 사슬을 만들지 않으려고 외래 키 하나만 뗐다는 단서가 함께 적혀 있다." caption="PostgreSQL 16.15 · 상위 표는 마이그레이션이 세움 · 두 연결 모두 autoCommit true · A 와 B 가 같은 작업을 받음 · 행은 attempts 0, last_result null, 다음 확인 시각 그대로 — 11줄 · exit 0" zoom="true" -::: - -```text -두 연결의 autoCommit : A=true B=true -작업자 A 가 받은 작업 : 441a6b30-6574-4af2-a637-1caeef230678 -작업자 B 가 받은 작업 : 441a6b30-6574-4af2-a637-1caeef230678 -같은 작업인가 : true -행 상태 : attempts=0 last_result=null next_check_at 그대로 -``` - -겹쳐 실행하지 않아도 재현된다. 두 번째 호출 시점에는 첫 호출의 잠금이 이미 사라져 있다. - -## 전달 청구는 같은 문장 안에서 소유자를 쓴다 - -javadoc 이 이유로 든 전달 큐의 청구문이 같은 패키지에 있다. - -```sql -WITH claimable AS ( - SELECT id FROM notification_recipient_delivery - WHERE next_dispatch_at <= :now - ... - FOR UPDATE SKIP LOCKED - LIMIT :batchSize -) -UPDATE notification_recipient_delivery AS d - SET lease_owner = :owner, - lease_until = :leaseUntil, - lease_fence = d.lease_fence + 1, - delivery_state = 'DISPATCHING', - ... -``` - -같은 잠금 구문을 CTE 안에 넣고, 그 문장의 `UPDATE` 로 소유자와 리스 만료와 펜스를 쓰고 상태를 배송 중으로 바꾼다. 잠금이 풀리기 전에 청구가 행에 남는다. - -조정 작업 청구에는 그 `UPDATE` 가 없다. 차이는 절 하나다. - -## 이 결함이 나타나는 조건 - -알림 플랫폼 마스터 스위치가 켜져 있어야 한다. `application.yml` 이 그것을 거짓으로 내보내므로 기본 배포는 작업자 빈 자체를 만들지 않는다. - -인스턴스도 둘 이상이어야 한다. 한 프로세스 안에서 이 패스는 스레드 하나짜리 스케줄러에 고정 지연으로 걸려 자기 자신과 겹치지 않는다. 두 작업자란 두 프로세스를 뜻한다. - -## 영향과 수정 - -조정은 전송이 아니라 제공자 상태 조회와 상태 투영이므로 영향은 전달 경로보다 낮다. 다만 javadoc 이 적은 계약은 깨진다. - -수정은 전달 청구처럼 잠금과 같은 문장 안에서 소유자와 펜스와 리스를 쓰는 청구문을 두는 것이다. 외부 제공자 호출을 긴 데이터베이스 트랜잭션 안에 넣는 것보다 이 방식이 낫다. - -## 확인하지 못한 것 - -두 작업자가 실제로 제공자 조정을 중복 수행했을 때의 결과를 관측하지 않았다. 확인한 것은 두 호출이 같은 작업을 받는다는 사실이다. - -탐침은 상위 행 사슬을 만들지 않으려고 외래 키 하나를 뗐다. 표는 마이그레이션이 모두 세운다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-a06-f013-recordapplied.md b/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-a06-f013-recordapplied.md deleted file mode 100644 index 1ca1ab9..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-a06-f013-recordapplied.md +++ /dev/null @@ -1,228 +0,0 @@ ---- -kind: CASE -slug: a06-f013-recordapplied -title: recordApplied가 펜스를 비교하지 않아 밀려난 실행자가 원장을 차지한다 -topic: state-ownership-and-concurrency -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a06-f013-recordapplied -evidenceCapturedOn: 2026-09-02 -body: case-a06-f013-recordapplied.body.md -assets: - - key: a06-f013-recordapplied - file: ../../../final/evidence/rendered/a06-f013-recordapplied.svg - - key: a06-f013-recordapplied-probe - file: ../../../final/evidence/rendered/a06-f013-recordapplied-probe.svg - - key: a06-f013-recordapplied-around - file: ../../../final/evidence/rendered/a06-f013-recordapplied-around.svg -evidence: - - ../../../final/evidence/raw/a06-f013-recordapplied.txt - - ../../../final/evidence/raw/a06-f013-recordapplied-probe.txt - - ../../../final/evidence/raw/a06-f013-recordapplied-around.txt -source: - - 원본 분석 절은 final/document.md#a06#L884 이다. 등급은 P2 다. 계약과 구현의 어긋남, 복제 세트 탐침의 네 줄, 체크포인트 저장에서 이미 한 번 고친 형태라는 지적, 도달성 셋, 그리고 시험이 이 경계를 보지 않는다는 사실이 그 절에 있다. - - 갱신과 삽입 사이에 들어가는 것이 사후조건 검사 하나라는 것, 갱신이 던지는 예외의 타입, 색인 없이 같은 경합이 남기는 항목 수, 그리고 실서버 시험의 단언 문장은 이 기록에서 확인했다. ---- - -# recordApplied가 펜스를 비교하지 않아 밀려난 실행자가 원장을 차지한다 - -원장 기록 메서드의 계약은 펜스가 여전히 현재 것일 때만 기록한다고 적는다. 구현이 하는 검사는 펜스가 미지정 값인지 하나뿐이다. 복제 세트에서 밀려난 실행자의 항목이 원장에 남고, 실제로 작업한 실행자는 드라이버의 중복 키 오류를 받았다. - -## 관계 - -- **fenced lease — 만료 시각만으로는 부족한 이유** - 펜스와 소유자를 함께 요구하는 이유를 적은 문서다. -- **CAS 튜플을 where 절에 전부 반복하고 update count를 답으로 쓴다** - 같은 원장의 다른 메서드가 지키는 규칙이다. -- **리스가 만료 시각만 기록하고 소유자를 기록하지 않아 최종 상태를 되돌릴 수 있었다** - 다른 리프에서도 소유자를 기록하지 않아 최종 상태가 되돌려졌다. - -## 문제 - -원장은 포크가 구현하도록 공개된 인터페이스다. 그 인터페이스의 javadoc 이 기록 메서드의 계약을 세 문장으로 적는다. 펜스가 현재 것일 때만 기록한다는 것, 밀려난 실행자의 항목은 대체 실행자가 덮어쓴 작업을 완료됐다고 말한다는 것, 더 새로운 획득이 있으면 전용 예외를 던진다는 것이다. - -## 결론 - -구현은 널 검사 넷과 미지정 값 검사 하나를 하고 삽입한다. 저장된 펜스와의 비교도, 서버측 조건도 없다. 검사 메서드 이름은 requireCurrentFence 인데 몸통의 조건은 fence == UNFENCED 하나다. 그 메서드의 javadoc 은 자기가 거절하는 것이 미지정 리스뿐이라고 정확히 적는다. 어긋난 것은 이름과 그것이 만족시켜야 할 계약이다. - -같은 파일의 체크포인트 저장은 그 계약을 지킨다. 저장된 펜스를 필터에 걸고, 중복 키 오류를 잡아 플랫폼 예외로 번역한다. 그 자리의 주석은 왜 그렇게 됐는지도 적는다. 밀려난 실행자가 자기 몫으로 쓰인 문장 대신 드라이버 오류를 받았기 때문이다. 같은 형태를 한쪽에서 고치고 다른 쪽에는 적용하지 않았다. - -복제 세트에서 확인했다. 펜스 5 의 체크포인트가 있는 상태에서 펜스 1 을 든 실행자가 두 메서드를 부르면 체크포인트는 거절되고 원장 기록은 수용된다. 그 뒤 펜스 5 로 기록하면 고유 색인이 중복 키로 거절한다. 주석이 서술한 과거와 받는 쪽이 바뀌어 있다. 그때는 밀려난 실행자가 드라이버 오류를 받았고 지금은 실제로 작업한 실행자가 받는다. - -중복 키가 나오는 것은 고유 색인이 있을 때뿐이다. 그 색인을 만드는 메서드를 부르는 곳은 시험뿐이고, 만들지 않고 운영하면 같은 경합이 오류 없이 원장에 두 줄을 남긴다. 색인이 없으면 오류가 나지 않으므로 중복이 더 늦게 발견된다. - -실행기와 원장과 잠금을 조립하는 프로덕션 코드가 이 리프에 없다. 포크가 이 저장소의 실행기와 잠금을 함께 쓰면 인접한 장치가 막는다. 실행기가 원장을 쓰기 전에 리스를 갱신하고, 그 갱신은 소유자와 펜스를 조건으로 건다. 다만 셋이 남는다. 갱신과 삽입 사이에 사후조건 검사가 들어가는데 그것은 마이그레이션이 넘긴 코드이고, 그 갱신이 던지는 것은 플랫폼의 거절 문장이 아니라 맨 IllegalStateException 이며, 인터페이스가 약속한 보호는 어느 원장 구현에도 없다. 다른 구현은 펜스 인자를 받기만 한다. - -시험 중에 밀려난 펜스로 원장 기록을 부르는 것은 없다. 원장 쓰기가 펜스를 실어 나른다는 이름을 단 시험은 미완료를 돌려주는 마이그레이션을 써서 원장 기록 분기까지 가지 않는다. 실서버 레인의 시험 하나가 같은 펜스로 두 번 부르는데, 그 단언에 붙은 문장이 막는 것은 읽고-쓰기 검사가 아니라 고유 색인이라고 적는다. - -수정은 체크포인트 저장이 쓰는 방식을 그대로 쓰면 된다. 원장 기록도 저장된 펜스를 조건으로 삼고, 중복 키를 잡아 플랫폼 예외로 번역하는 것이다. - -## 검증 환경 - -OpenJDK : 21.0.12 -MongoDB : 8.0.16 단일 노드 복제 세트 -확인 방식 : 계약과 구현 대조, 실제 복제 세트에 두 실행자의 쓰기 실행 -소스 수정 : x - -## 재현 조건 - -1. 인터페이스 javadoc 의 계약과 구현의 몸통, 그리고 검사 메서드와 그 javadoc 을 나란히 읽는다. -2. 같은 파일의 체크포인트 저장이 거는 필터와 중복 키 처리, 그리고 그 자리 주석을 읽는다. -3. 단일 노드 복제 세트를 띄우고 고유 색인을 만든다. -4. 펜스 5 로 체크포인트를 쓴다. -5. 펜스 1 로 체크포인트 저장과 원장 기록을 각각 시도하고 원장 항목을 읽는다. -6. 펜스 5 로 원장 기록을 시도한다. -7. 두 메서드에 미지정 값을 넣어 결과를 비교한다. -8. 고유 색인 없이 같은 경합을 반복하고 원장 항목 수를 센다. -9. 실행기에서 갱신과 원장 기록 사이에 무엇이 실행되는지, 완료를 만드는 팩토리가 체크포인트를 남기는지 읽는다. -10. 원장 기록을 부르는 시험과 고유 색인을 만드는 곳을 전수로 센다. - -## 본문 - - - -원장 인터페이스는 포크가 구현하도록 공개돼 있고, 기록 메서드의 계약을 javadoc 이 적는다. - -## 계약과 몸통 - -:::evidence key="a06-f013-recordapplied" alt="원장 인터페이스의 javadoc 계약, 그 계약을 구현한 메서드의 삽입 부분, 그것이 부르는 펜스 검사와 그 검사의 javadoc, 같은 파일의 체크포인트 저장이 거는 저장된 펜스 필터, 그리고 그 자리가 중복 키를 플랫폼 예외로 번역하게 된 이유를 적은 주석과 번역 코드를 출력한 터미널 기록." caption="계약은 펜스가 현재 것일 때만 기록 · 구현은 검사 하나 뒤 삽입 · 검사의 조건은 fence == UNFENCED 하나이고 javadoc 도 미지정 리스만 거절한다고 적음 · 체크포인트 저장은 저장된 펜스를 필터에 걸고 중복 키를 번역 — 64줄 · exit 0" zoom="true" -::: - -```java -/** - * Records a completed migration, only if the fence is still the current one. - * - *

Conditioned on the fence because the runner that wrote the batches may no longer be the - * runner that owns the lease. A ledger entry from a superseded runner says a migration completed - * when the work it describes was overwritten by the runner that replaced it. - * - * @param fence the acquisition token from {@link MongoMigrationLock#fence()} - * @throws ... MongoOperationRejectedException when a newer acquisition exists - */ -``` - -구현은 검사 하나를 부르고 삽입한다. - -```java -requireCurrentFence(fence, "ledger entry for " + migrationId.value()); -ledger.insertOne( - new Document(MIGRATION_ID, migrationId.value()) - .append("checksum", checksum.value()) - ... - .append("fence", fence)); -``` - -그 검사의 javadoc 과 몸통은 서로 맞는다. - -```java -/** - * Refuses a write from an unfenced lease. - ... -private static void requireCurrentFence(long fence, String what) { - if (fence == MongoMigrationLock.UNFENCED) { -``` - -비교 대상은 저장된 값이 아니라 상수 하나다. 서버로 나가는 조건에 펜스는 없고, 펜스는 삽입되는 문서의 필드로만 남는다. 문서가 틀린 것이 아니라, 이 검사로는 인터페이스가 적은 계약을 만족시킬 수 없다. - -## 같은 파일이 다른 메서드에서는 지킨다 - -체크포인트 저장은 저장된 펜스를 필터에 건다. - -```java -Filters.and( - Filters.eq(MIGRATION_ID, checkpoint.migrationId().value()), - Filters.or(Filters.exists("fence", false), Filters.lte("fence", fence))), -``` - -그리고 중복 키를 잡아 번역한다. 그 자리 주석이 이유를 적는다. - -```java -// Nor was the refusal itself reachable. With `upsert(true)`, a superseded write matches nothing -// and MongoDB attempts an insert, which the unique index on migrationId rejects — so a -// superseded runner got a driver-level duplicate-key error instead of the sentence written for -// it, and the branch meant to produce that sentence was dead. -``` - -## 복제 세트에서 - -:::evidence key="a06-f013-recordapplied-probe" alt="단일 노드 복제 세트에서 살아 있는 실행자가 펜스 5 로 쓴 체크포인트, 펜스 1 을 든 밀려난 실행자가 체크포인트 저장과 원장 기록을 각각 시도한 결과와 그 뒤의 원장 항목, 살아 있는 실행자가 다시 원장 기록을 시도한 결과, 두 메서드에 미지정 펜스를 넣었을 때의 결과, 그리고 고유 색인 없이 같은 경합을 반복했을 때 남은 원장 항목 수와 그 두 항목을 출력한 터미널 기록." caption="밀려난 펜스로 체크포인트는 거절되고 원장 기록은 수용 · 살아 있는 펜스의 기록은 migrationId_1 중복 키로 거절 · 미지정 펜스는 두 메서드 다 거절 · 색인이 없으면 둘 다 수용되어 원장에 두 줄 — 22줄 · exit 0" zoom="true" -::: - -펜스 5 의 체크포인트가 있는 상태에서 펜스 1 을 든 실행자가 두 메서드를 부른다. - -```text -[밀려난 실행자, 펜스 1] 같은 계약을 두 메서드에 건다 - saveCheckpoint -> 거절, MongoOperationRejectedException: a newer migration runner owns the lease; this runner's checkpoint write was refused - recordApplied -> 수용 - 원장 항목 : {"migrationId": "20260829-001", "checksum": "superseded", "operator": "stale-runner", ..., "fence": 1} -``` - -그 뒤 살아 있는 실행자가 기록하면 이렇게 된다. - -```text - recordApplied -> 거절, MongoWriteException code=11000 index=migrationId_1 -``` - -같은 드라이버 오류가 지금 원장 기록에서 나온다. 다만 받는 쪽이 바뀌었다. 주석이 적은 과거에는 밀려난 실행자가 그 오류를 받았고, 지금은 실제로 작업한 실행자가 받는다. - -미지정 펜스는 두 메서드 다 거절한다. 밀려난 펜스는 원장 기록만 통과한다. 두 결과 사이에 이 구현이 거절할 수 있는 값의 집합이 있다. - -그 색인이 없으면 어떻게 되는지도 같이 돌렸다. - -```text -[대조 2] ensureIndexes 를 부르지 않은 원장에서 같은 경합 - recordApplied -> 수용 - recordApplied -> 수용 - 원장 항목 수 : 2 -``` - -## 포크가 조립할 때 인접한 장치가 막는다 - -:::evidence key="a06-f013-recordapplied-around" alt="실행기에서 리스 갱신과 원장 기록 사이에 실행되는 것, 완료와 미완료를 만드는 두 팩토리, 그 갱신이 던지는 예외와 같은 인터페이스의 다른 잠금 구현이 같은 상황에 던지는 예외, 펜스 인자를 쓰지 않는 다른 원장 구현, 원장 기록을 부르는 시험 전수와 그중 실서버 시험의 단언, 그리고 고유 색인을 만드는 메서드를 부르는 곳 전수를 출력한 터미널 기록." caption="갱신과 삽입 사이에 사후조건 검사 · 완료 팩토리는 체크포인트를 널로 넣음 · 갱신은 IllegalStateException, 다른 잠금 구현은 플랫폼 예외 · 다른 원장 구현은 펜스 미사용 · 실서버 시험의 단언 문장이 막는 것은 고유 색인이라고 적음 · 색인 생성 호출자는 시험 하나 — 69줄 · exit 0" zoom="true" -::: - -이 리프에는 실행기와 원장과 잠금을 조립하는 프로덕션 코드가 없다. 포크가 이 저장소의 실행기와 잠금을 함께 쓰면, 실행기가 원장을 쓰기 전에 리스를 갱신한다. - -```java -lock.refresh(template.maxTime()); -if (!migration.postcondition().isSatisfied(context, result)) { -... -result.resumePoint().ifPresent(checkpoint -> ledger.saveCheckpoint(checkpoint, lock.fence())); -if (result.status() == MongoMigrationResult.Status.COMPLETED && !context.dryRun()) { - ledger.recordApplied( -``` - -갱신과 삽입 사이에 있는 것은 사후조건 검사 하나다. 바로 위 줄의 체크포인트 저장은 이 경로에 들어오지 않는다. 완료를 만드는 팩토리가 체크포인트를 널로 넣으므로 두 줄은 배타적이다. - -```java -public static MongoMigrationResult completed(long processedCount) { - return new MongoMigrationResult(Status.COMPLETED, processedCount, null, ""); -} -``` - -그리고 그 갱신이 실패했을 때 나오는 것은 플랫폼의 거절 문장이 아니다. - -```java -throw new IllegalStateException( - "the migration lease was lost before it could be refreshed; another runner may have " -``` - -같은 인터페이스의 다른 잠금 구현은 같은 상황에 플랫폼 예외를 던진다. 이 기록이 다루는 형태가 보호 장치 쪽에서 한 번 더 나온다. - -## 시험은 이 자리를 밟지 않는다 - -원장 기록을 부르는 시험은 다섯 줄이고, 밀려난 펜스를 넣는 것은 없다. 이름이 원장 쓰기가 펜스를 실어 나른다고 말하는 시험은 미완료를 돌려주는 마이그레이션을 쓰므로 원장 기록 분기에 들어가지 않는다. 실서버 레인의 시험 하나가 같은 펜스로 두 번 부르는데, 그 단언에 붙은 문장이 이 기록의 결론을 그대로 적는다. - -```java -.as("the unique index, not the read-then-write check, is what makes this impossible") -.isInstanceOf(RuntimeException.class); -``` - -그 고유 색인을 만드는 메서드를 부르는 파일은 시험 하나와 선언 파일 자신뿐이다. - -## 확인하지 못한 것 - -원장이 밀려난 값으로 남은 뒤 후속 마이그레이션 판정이 어떻게 되는지 추적하지 않았다. 갱신과 삽입 사이의 창을 실제로 벌려 보지도 않았다. 확인한 것은 그 창을 지나 원장 기록에 도달했을 때 무엇이 일어나는지다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-assigned-id-turns-claim-into-upsert.md b/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-assigned-id-turns-claim-into-upsert.md deleted file mode 100644 index 809dae0..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-assigned-id-turns-claim-into-upsert.md +++ /dev/null @@ -1,110 +0,0 @@ ---- -kind: CASE -slug: assigned-id-turns-claim-into-upsert -title: 배정 식별자 때문에 insert-first 청구가 UPSERT 가 되어 커밋된 결과를 덮었다 -topic: state-ownership-and-concurrency -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:assigned-id-turns-claim-into-upsert -evidenceCapturedOn: 2026-09-01 -body: case-assigned-id-turns-claim-into-upsert.body.md -assets: - - key: assigned-id-turns-claim-into-upsert - file: ../../../final/evidence/rendered/assigned-id-turns-claim-into-upsert.svg -evidence: - - ../../../final/evidence/raw/assigned-id-turns-claim-into-upsert.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-operation-ledger-jpa#L133 이다. ---- - -# 배정 식별자 때문에 insert-first 청구가 UPSERT 가 되어 커밋된 결과를 덮었다 - -어댑터가 먼저 넣고 충돌에서 읽는 순서를 자기 존재 이유로 적는다. 엔티티의 식별자가 배정값이라 저장 메서드가 병합으로 가고, 파생 기본 키가 유니크 제약과 같은 행을 가리켜 두 번째 청구가 위반 없이 기존 행을 덮는다. - -## 관계 - -- **Atomic 타입의 존재는 원자성의 증거가 아니다** - 같은 계열의 규칙이다. -- **중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다** - 테스트 이중과 실제 저장소가 갈리는 지점을 찾는 방법이다. -- **커밋 증거 프레임을 두 주인이 pop해서 바깥 트랜잭션의 실패가 익명이 됐다** - 같은 리프 계열의 소유권 사례다. - -## 문제 - -어댑터의 자바독이 순서를 계약으로 적는다. - -먼저 넣고 충돌에서 읽는다는 것이다. 읽고 나서 넣는 구현은 읽기와 넣기 사이에 창이 있고, 그 창의 너비가 정확히 그것이 닫으려는 경합의 너비이며, 두 시도를 동시에 돌리지 않는 모든 테스트를 통과한다는 것이다. - -전제는 저장 호출이 삽입이고, 같은 신원의 두 번째 청구가 유니크 제약을 건드린다는 것이다. - -## 결론 - -두 전제가 모두 성립하지 않는다. - -엔티티의 식별자는 호출자가 배정한다. 청구 팩토리가 신원에서 파생한 저장 키를 필드에 채우므로 식별자가 널이 아니다. - -Spring Data 의 저장 구현은 식별자가 널이 아닌 엔티티를 새 것으로 보지 않는다. 병합으로 보낸다. - -그리고 저장 키는 유니크 제약의 세 컬럼을 이어 붙인 파생값이다. 호출자 지문과 정규 메서드 이름과 멱등 키 해시다. - -그러므로 같은 신원의 두 번째 청구는 같은 기본 키 행을 겨냥한다. - -실제로 일어나는 일은 이렇다. - -병합이 그 행을 찾는다. 유니크 제약은 발화하지 않는다. 새 행을 넣는 것이 아니라 같은 행을 갱신하기 때문이다. - -분리 상태의 새 엔티티가 기존 행 위에 복사된다. 상태가 진행 중으로, 결과 참조가 널로, 완료 시각이 널로, 청구 시각이 지금으로 바뀐다. - -예외가 없으므로 청구는 빈 값을 돌려준다. 호출자는 자기가 청구를 소유했다고 읽는다. - -즉 이미 커밋된 연산의 결과 참조가 지워지고, 재시도가 그 변경을 다시 실행한다. 이 모듈이 존재하는 이유로 든 결과 그대로다. - -데이터베이스의 검사 제약도 막지 못한다. 갱신 후 상태는 진행 중이고 완료 시각이 널이라 셋 다 합법이다. - -테스트가 이것을 볼 수 없는 이유는 이중에 있다. 인메모리 저장소의 저장 메서드는 키가 이미 있으면 예외를 던진다. 삽입과 유니크 제약을 모사한다. - -즉 이중은 삽입을 하고 실제 저장소는 갱신을 한다. 빌드 파일 주석이 실제 데이터스토어를 쓰지 않는 근거로 벤더 의미론이 시험 대상일 때만 정당하다고 적었는데, 여기서 갈린 것이 정확히 벤더 의미론이다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Spring Data JPA : 4.0.7 -확인 방식 : 엔티티 식별자 형태와 저장 계약 대조, 파생 키 구성 확인 -소스 수정 : x - -## 재현 조건 - -원문은 document-detail 의 final/document.md#a20-grpc-operation-ledger-jpa 에 있다. - -1. 어댑터의 청구 메서드와 그 자바독을 읽는다. -2. 엔티티의 식별자가 어디서 채워지는지 확인한다. -3. 저장 구현이 식별자 널 여부로 무엇을 고르는지 확인한다. -4. 저장 키의 파생식과 유니크 제약의 컬럼을 대조한다. -5. 테스트 이중의 저장 메서드가 무엇을 하는지 읽는다. - -## 본문 - - - -`claim` 은 insert-first, read-on-conflict 를 주장한다. 그러나 엔티티의 `@Id` 가 배정값(`caller|method|keyHash`)이라 `SimpleJpaRepository.save` 가 `persist` 가 아니라 `merge` 로 간다. - -## claim 이 주장하는 순서 - -:::evidence key="assigned-id-turns-claim-into-upsert" alt="분석 문서 final/document.md#a20-grpc-operation-ledger-jpa 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-operation-ledger-jpa 발췌 — 15줄" zoom="true" -::: - -## 위반 대신 갱신이 일어난다 - -그 파생 키가 유니크 제약의 세 컬럼과 같은 행을 가리키므로 두 번째 청구는 위반을 일으키지 않고 기존 행을 갱신한다 — 상태가 `IN_PROGRESS` 로, `outcome_reference` 가 널로 되돌아가고 `claim` 은 빈 값을 돌려줘 호출자가 소유를 얻었다고 읽는다. - -## 테스트 이중이 그 차이를 가린다 - -인메모리 테스트 이중의 `save` 는 키가 있으면 던지므로 INSERT 를 흉내 낸다. - -## 확인하지 못한 것 - -실제 데이터베이스로 같은 신원을 두 번 청구해 재현하지 않았다. 이 리프는 어떤 배포에도 조립되지 않아 실행 경로가 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-complete-drain-rolls-back-a-rotation.md b/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-complete-drain-rolls-back-a-rotation.md deleted file mode 100644 index 23b989a..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-complete-drain-rolls-back-a-rotation.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -kind: CASE -slug: complete-drain-rolls-back-a-rotation -title: 배수 완료가 자기가 읽은 값으로 상태를 다시 써서 진행 중인 자격증명 회전을 되돌린다 -topic: state-ownership-and-concurrency -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:complete-drain-rolls-back-a-rotation -evidenceCapturedOn: 2026-09-01 -body: case-complete-drain-rolls-back-a-rotation.body.md -assets: - - key: complete-drain-rolls-back-a-rotation - file: ../../../final/evidence/rendered/complete-drain-rolls-back-a-rotation.svg -evidence: - - ../../../final/evidence/raw/complete-drain-rolls-back-a-rotation.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-policy#L158 이다. ---- - -# 배수 완료가 자기가 읽은 값으로 상태를 다시 써서 진행 중인 자격증명 회전을 되돌린다 - -회전 관리자가 원자 참조를 들고 있으면서 두 메서드 모두 읽고 나서 조건 없이 쓴다. 배수 완료가 자기가 읽은 현재 세대로 새 상태를 만들기 때문에, 그 사이에 일어난 회전이 지워지고 이전 세대가 다시 현재가 된다. - -## 관계 - -- **Atomic 타입의 존재는 원자성의 증거가 아니다** - 이 사례가 그 규칙의 형태다. -- **배정 식별자 때문에 insert-first 청구가 UPSERT가 되어 커밋된 결과를 덮었다** - 같은 통독에서 나온 같은 계열의 사례다. -- **재시도 안전은 증거로 결정된다** - 같은 가족이 원자성을 제대로 다룬 정본이 그 옆에 있다. - -## 문제 - -자격증명 회전 관리자의 자바독이 존재 이유를 적는다. - -자재를 제자리에서 바꾸는 것이 이 클래스가 피하려는 실패를 만든다는 것이다. 교체 시점에 진행 중이던 모든 호출이 인증 오류로 실패하고, 그 오류는 클라이언트에서 보면 애초에 유효하지 않았던 자격증명과 똑같아 보인다는 것이다. - -그래서 준비하고 교체하고 배수하는 순서를 지킨다. - -상태는 원자 참조 하나에 담긴다. 현재 세대와 배수 중 세대와 배수 마감을 묶은 값이다. - -## 결론 - -두 메서드 모두 원자적이지 않다. - -회전은 현재 상태를 읽고, 승계 여부를 판정하고, 새 상태를 조건 없이 쓴다. - -배수 완료는 현재 상태를 읽고, 자기가 읽은 현재 세대로 새 상태를 만들어 조건 없이 쓴다. - -두 결과가 다르다. - -회전 경합에서는 두 회전이 같은 값을 읽고 둘 다 승계 검사를 통과할 수 있다. 나중 쓰기가 앞의 것을 덮고, 덮인 회전이 배수 대상으로 기록해 둔 세대가 상태에서 사라진다. 그 세대 위의 호출은 아무도 배수하지 않는다. - -자바독이 이 상황을 이미 안다. 승계 검사의 존재 이유로 회전이 뒤로 가는 흔한 원인이 두 회전자의 경합이라고 적는다. 검사는 있고 원자성이 없다. - -배수 완료의 되돌림이 더 무겁다. - -읽기와 쓰기 사이에 회전이 일어나면, 그 회전이 활성화한 세대가 지워지고 이전 세대가 다시 현재가 된다. - -방금 교체된 자격 자재가 되살아난다. 이 클래스의 존재 이유가 그 교체다. - -같은 가족 안에 정본이 있다. 재시도 예산과 헤징 예산이 정확한 비교 후 교체 루프를 쓰고, 수요 제어기는 같은 형태를 동기화로 닫는다. - -그리고 같은 형태가 옆 리프에도 있다. 채널 런타임 레지스트리의 회전이 같은 파일의 설치가 비교 후 교체를 쓰는데도 조건 없이 덮는다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 두 메서드의 읽기와 쓰기 사이 원자성 분석, 같은 가족의 정본 대조 -소스 수정 : x - -## 재현 조건 - -원문은 document-detail 의 final/document.md#a20-grpc-policy 와 final/document.md#a20-grpc-client 에 있다. - -1. 회전 관리자의 상태 필드 타입을 확인한다. -2. 회전 메서드에서 읽기와 쓰기 사이에 조건이 있는지 본다. -3. 배수 완료 메서드가 새 상태를 무엇으로 만드는지 읽는다. -4. 자바독의 경합 서술을 읽는다. -5. 같은 가족의 예산 클래스들과 비교한다. - -## 본문 - - - -`AtomicReference` 를 들고 있으면서 `rotate` 와 `completeDrain` 이 모두 `get()` 후 조건 없는 `set()` 을 한다. - -## 두 메서드가 같은 참조를 다루는 방식 - -:::evidence key="complete-drain-rolls-back-a-rotation" alt="분석 문서 final/document.md#a20-grpc-policy 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-policy 발췌 — 15줄" zoom="true" -::: - -## 두 경합의 결과가 다르다 - -회전 경합에서는 덮인 세대가 배수 목록에 오르지 못한다. 배수 완료에서는 자기가 읽은 `observed.current()` 로 새 상태를 만들기 때문에 그 사이에 일어난 회전이 지워지고 이전 세대가 다시 현재가 된다. - -## 클래스의 목적이 뒤집힌다 - -자격 자재를 떨어뜨리지 않고 교체하려고 만든 클래스가 교체 자체를 되돌린다. 같은 파일의 형제(`install`)와 같은 가족의 `GrpcRetryBudget` 이 비교 후 교체를 정확히 쓴다. - -## 확인하지 못한 것 - -동시 회전과 동시 배수 완료를 실행으로 재현하지 않았다. 두 리프 모두 배선 경로가 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-admin-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-admin-f03.md deleted file mode 100644 index ffe1f93..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-admin-f03.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -kind: CASE -slug: grpc-admin-f03 -title: 배수 조정자가 가변이고 동기화가 없다 -topic: state-ownership-and-concurrency -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-admin-f03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-admin-f03 - file: ../../../final/evidence/rendered/grpc-admin-f03.svg -evidence: - - ../../../final/evidence/raw/grpc-admin-f03.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-admin#L181 이다. -module: grpc-admin -priority: P3 ---- - -# 배수 조정자가 가변이고 동기화가 없다 - -phasesRun(ArrayList), startedAt, completedUnaryCalls, signalledStreams 가 평범한 필드다. synchronized·volatile·동시 자료구조가 없다. - -## 문제 - -phasesRun(ArrayList), startedAt, completedUnaryCalls, signalledStreams 가 평범한 필드다. - -synchronized·volatile·동시 자료구조가 없다. - -## 결론 - -같은 리프의 건강 레지스트리는 정반대다 — ConcurrentHashMap 둘과 volatile boolean draining. - -즉 이 리프는 동시성을 인지하고 있고 한 클래스에만 적용했다. - -조정자의 javadoc 이 대기를 호출자에게 맡긴다고 적으므로 단일 호출자 전제로 읽을 수 있다. - -다만 그 전제가 자바독에 적혀 있지 않고, unaryDrainComplete 는 반복 호출을 전제한 형태라 종료 훅과 상태 조회가 다른 스레드에서 닿기 쉽다. - -수정은 단일 스레드 전제를 자바독에 적거나, 형제 클래스와 같은 수준으로 맞추는 것이다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : 조정자 필드 선언과 동시성 장치(synchronized·volatile·동시 자료구조) 유무 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-admin#L181 에 있다. - -## 본문 - - - -`phasesRun`(`ArrayList`), `startedAt`, `completedUnaryCalls`, `signalledStreams` 가 평범한 필드다. `synchronized`·`volatile`·동시 자료구조가 없다. - -## 조정자의 필드 선언 - -:::evidence key="grpc-admin-f03" alt="분석 문서 final/document.md#a20-grpc-admin 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-admin 발췌 — 15줄" zoom="true" -::: - -## 같은 리프의 건강 레지스트리는 정반대다 - -`ConcurrentHashMap` 둘과 `volatile boolean draining` 을 쓴다. 즉 이 리프는 동시성을 인지하고 있고 한 클래스에만 적용했다. - -## 단일 호출자 전제로 읽을 수는 있다 - -조정자의 javadoc 이 대기를 호출자에게 맡긴다고 적는다. 다만 그 전제가 자바독에 적혀 있지 않고, `unaryDrainComplete` 는 반복 호출을 전제한 형태라 종료 훅과 상태 조회가 다른 스레드에서 닿기 쉽다. 수정은 단일 스레드 전제를 자바독에 적거나, 형제 클래스와 같은 수준으로 맞추는 것이다. - -## 확인하지 못한 것 - -여러 스레드에서 조정자를 동시에 호출해 경합을 재현하지 않았다. 배선 경로가 없어 실제 서버로 배수를 돌릴 수 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-advanced-bootstrap-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-advanced-bootstrap-f03.md deleted file mode 100644 index cd032bf..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-advanced-bootstrap-f03.md +++ /dev/null @@ -1,84 +0,0 @@ ---- -kind: CASE -slug: grpc-advanced-bootstrap-f03 -title: 깃발 홀더가 가변이고 동기화가 없다 -topic: state-ownership-and-concurrency -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-advanced-bootstrap-f03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-advanced-bootstrap-f03 - file: ../../../final/evidence/rendered/grpc-advanced-bootstrap-f03.svg -evidence: - - ../../../final/evidence/raw/grpc-advanced-bootstrap-f03.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-advanced-bootstrap#L209 이다. -module: grpc-advanced-bootstrap -priority: P3 ---- - -# 깃발 홀더가 가변이고 동기화가 없다 - -GrpcAdvancedFeatureFlags 는 두 EnumMap 을 enable·withGrade 로 갱신하고, available·active 가 같은 맵을 읽는다. synchronized·volatile·동시 자료구조가 없다. - -## 문제 - -GrpcAdvancedFeatureFlags 는 두 EnumMap 을 enable·withGrade 로 갱신하고, available·active 가 같은 맵을 읽는다. - -synchronized·volatile·동시 자료구조가 없다. - -## 결론 - -시작 시 전부 설정하고 그 뒤로 읽기만 한다면 안전 공개 문제만 남는다. - -다만 두 메서드가 this 를 돌려주는 유창한 형태라 런타임 중 갱신을 권하는 모양이고, active() 는 순회 중 갱신에 노출된다. - -같은 저장소가 이 형태를 다른 리프에서 결함으로 기록했다(GrpcCompletionReconciler 의 동기화 없는 ArrayList). - -여기서는 등급과 깃발이 요청 경로에서 읽히므로 같은 노출이 생길 수 있다. - -수정은 홀더를 불변으로 만들고 enable·withGrade 가 새 인스턴스를 돌려주게 하는 것이다. - -이 저장소가 다른 곳에서 쓰는 형태다(GrpcProtoStyleManifest.allowingWellKnownTypes 등). - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : GrpcAdvancedFeatureFlags 참조 7건 검색과 두 EnumMap 의 갱신·읽기 지점 동기화 마커 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-advanced-bootstrap#L209 에 있다. - -## 본문 - - - -`GrpcAdvancedFeatureFlags` 는 두 `EnumMap` 을 `enable`·`withGrade` 로 갱신하고, `available`·`active` 가 같은 맵을 읽는다. `synchronized`·`volatile`·동시 자료구조가 없다. - -## GrpcAdvancedFeatureFlags 참조 위치 - -:::evidence key="grpc-advanced-bootstrap-f03" alt="코드베이스에서 GrpcAdvancedFeatureFlags 를 검색한 출력 7줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcAdvancedFeatureFlags 코드베이스 검색 — 7줄 · exit 0" zoom="true" -::: - -## 시작 시 전부 설정한다면 안전 공개 문제만 남는다 - -다만 두 메서드가 `this` 를 돌려주는 유창한 형태라 런타임 중 갱신을 권하는 모양이고, `active()` 는 순회 중 갱신에 노출된다. - -## 같은 저장소가 이 형태를 다른 리프에서 결함으로 기록했다 - -`GrpcCompletionReconciler` 의 동기화 없는 `ArrayList` 다. 여기서는 등급과 깃발이 요청 경로에서 읽히므로 같은 노출이 생길 수 있다. - -## 수정 - -홀더를 불변으로 만들고 `enable`·`withGrade` 가 새 인스턴스를 돌려주게 하는 것이다. 이 저장소가 다른 곳에서 쓰는 형태다(`GrpcProtoStyleManifest.allowingWellKnownTypes` 등). - -## 확인하지 못한 것 - -동시 갱신과 읽기를 실행으로 재현하지 않았다. 동기화 장치가 없다는 코드 형태로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-advanced-resilience-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-advanced-resilience-f03.md deleted file mode 100644 index 08636e2..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-advanced-resilience-f03.md +++ /dev/null @@ -1,101 +0,0 @@ ---- -kind: CASE -slug: grpc-advanced-resilience-f03 -title: 리졸버의 개정 가드가 비교 후 교체가 아니다 -topic: state-ownership-and-concurrency -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-advanced-resilience-f03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-advanced-resilience-f03 - file: ../../../final/evidence/rendered/grpc-advanced-resilience-f03.svg - - key: grpc-advanced-resilience-f03-diagram - file: ../../../final/assets/diagrams/grpc-advanced-resilience-f03.svg -evidence: - - ../../../final/evidence/raw/grpc-advanced-resilience-f03.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-advanced-resilience#L176 이다. -module: grpc-advanced-resilience -priority: P2 ---- - -# 리졸버의 개정 가드가 비교 후 교체가 아니다 - -GrpcCustomResolver 의 javadoc 이 지키겠다고 하는 것은 명확하다. 빈 집합은 GrpcEndpointSnapshot 의 생성자가 지키므로 성립한다. - -## 문제 - -GrpcCustomResolver 의 javadoc 이 지키겠다고 하는 것은 명확하다. - -빈 집합은 GrpcEndpointSnapshot 의 생성자가 지키므로 성립한다. - -## 결론 - -개정 가드는 그렇지 않다. - -AtomicReference 를 쓰면서 읽기와 쓰기 사이에 원자성이 없다. - -개정 5 와 6 을 든 두 스레드가 같은 applied(개정 4)를 읽으면 둘 다 supersedes 를 통과하고, 나중에 set 하는 쪽이 이긴다. - -6 이 먼저 쓰이고 5 가 덮으면 채널이 옛 엔드포인트로 되돌아간다 — 개정 번호가 존재하는 이유가 정확히 그것을 막는 것이다. - -listener.accept(update) 도 set 밖에 있으므로, applied 의 최종 값이 옳더라도 리스너(=채널)가 받는 순서는 뒤집힐 수 있다. - -채널은 마지막으로 받은 것을 믿는다. - -같은 형태가 이 가족에 셋이다. - -정본이 같은 리프 안에 있다는 점이 §12.2 의 대조와 같다 — 이 리프는 예산에서는 CAS 를 쓰고 리졸버에서는 쓰지 않는다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : GrpcCustomResolver 참조 5건 검색과 개정 가드의 읽기·쓰기 순서 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-advanced-resilience#L176 에 있다. - -## 본문 - - - -`GrpcCustomResolver` 의 javadoc 이 지키겠다고 하는 것은 명확하다. 빈 집합은 `GrpcEndpointSnapshot` 의 생성자가 지키므로 성립한다. 개정 가드는 그렇지 않다. - -## 가드가 지키지 못하는 구간 - -:::evidence key="grpc-advanced-resilience-f03-diagram" alt="두 스레드의 개정이 같은 applied 읽기와 둘 다 통과를 지나 옛 엔드포인트 회귀로 이어진다" caption="가드가 지키지 못하는 구간" zoom="false" -::: - -`AtomicReference` 를 쓰면서 읽기와 쓰기 사이에 원자성이 없다. 개정 5 와 6 을 든 두 스레드가 같은 `applied`(개정 4)를 읽으면 둘 다 `supersedes` 를 통과하고, 나중에 `set` 하는 쪽이 이긴다. 6 이 먼저 쓰이고 5 가 덮으면 **채널이 옛 엔드포인트로 되돌아간다** — 개정 번호가 존재하는 이유가 정확히 그것을 막는 것이다. - -## GrpcCustomResolver 참조 위치 - -:::evidence key="grpc-advanced-resilience-f03" alt="코드베이스에서 GrpcCustomResolver 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcCustomResolver 코드베이스 검색 — 5줄 · exit 0" zoom="true" -::: - -## 통지 순서도 뒤집힐 수 있다 - -`listener.accept(update)` 도 `set` 밖에 있으므로, `applied` 의 최종 값이 옳더라도 리스너(=채널)가 받는 순서는 뒤집힐 수 있다. 채널은 마지막으로 받은 것을 믿는다. - -## 정본이 같은 리프 안에 있다 - -이 리프는 예산에서는 CAS 를 쓰고 리졸버에서는 쓰지 않는다. 같은 형태가 이 가족에 셋이다. - -## 테스트는 순차 경로만 본다 - -`a stale revision is dropped rather than applied` 는 단일 스레드에서 개정 2 를 적용한 뒤 개정 1 을 제시한다. 순차적으로는 가드가 정확히 작동한다. - -## 수정 - -`applied.updateAndGet` 안에서 판정과 교체를 함께 하거나, `compareAndSet(observed, snapshot)` 이 실패하면 다시 읽어 판정한다. 리스너 통지는 성공한 CAS 뒤에 그 CAS 가 이긴 순서로 해야 한다 — 예산 쪽의 `tryConsume` 루프가 같은 리프 안의 본보기다. 이 리프가 배선되지 않으므로 P2. - -## 확인하지 못한 것 - -두 스레드로 개정을 겹쳐 옛 엔드포인트 회귀를 재현하지 않았다. 읽기와 쓰기가 원자적이지 않다는 코드 형태로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-advanced-streaming-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-advanced-streaming-f03.md deleted file mode 100644 index 5e68550..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-advanced-streaming-f03.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -kind: CASE -slug: grpc-advanced-streaming-f03 -title: 체크포인트 전진이 ConcurrentMap 위의 확인 후 쓰기다 -topic: state-ownership-and-concurrency -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-advanced-streaming-f03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-advanced-streaming-f03 - file: ../../../final/evidence/rendered/grpc-advanced-streaming-f03.svg -evidence: - - ../../../final/evidence/raw/grpc-advanced-streaming-f03.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-advanced-streaming#L180 이다. -module: grpc-advanced-streaming -priority: P3 ---- - -# 체크포인트 전진이 ConcurrentMap 위의 확인 후 쓰기다 - -GrpcClientStreamCheckpoint.advancedTo 가 뒤로 가는 것을 거부하고, 그 메시지가 원인을 정확히 짚는다 — "two writers are checkpointing one session". 그 가드가 보는 것은 호출한 스레드가 읽은 값 이다. - -## 문제 - -GrpcClientStreamCheckpoint.advancedTo 가 뒤로 가는 것을 거부하고, 그 메시지가 원인을 정확히 짚는다 — "two writers are checkpointing one session". - -그 가드가 보는 것은 호출한 스레드가 읽은 값 이다. - -## 결론 - -두 스레드가 순번 5 와 6 을 적용하며 같은 체크포인트(4)를 읽으면 둘 다 advancedTo 를 통과한다. - -5 를 든 쪽이 나중에 put 하면 체크포인트는 6 에서 5 로 뒤로 간다 — advancedTo 가 막겠다고 한 바로 그 상태이고, 이번에는 예외 없이 조용히 일어난다. - -그러면 순번 6 의 메시지가 다시 APPLY 로 판정되어 두 번 적용된다. - -이 클래스가 존재하는 이유가 정확히 그것을 막는 것이다. - -ConcurrentHashMap 에는 이 형태를 위한 연산이 있다. - -compute 안에서는 읽기와 쓰기가 원자적이므로, 뒤처진 쪽이 advancedTo 의 예외를 실제로 받는다 — 가드가 설계대로 발화한다. - -같은 리프의 GrpcDemandController 는 모든 공개 메서드가 synchronized 이고, GrpcBidiSequenceTracker 도 그렇다(§12.2 가 그것을 이 가족의 모범으로 든다). - -중복 제거기만 ConcurrentMap 의 원자 연산을 쓰지 않는다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : ConcurrentMap 참조 23건 검색과 체크포인트 전진의 확인·쓰기 분리 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-advanced-streaming#L180 에 있다. - -## 본문 - - - -`GrpcClientStreamCheckpoint.advancedTo` 가 뒤로 가는 것을 거부하고, 그 메시지가 원인을 정확히 짚는다 — "two writers are checkpointing one session". 그 가드가 보는 것은 **호출한 스레드가 읽은 값** 이다. - -## ConcurrentMap 참조 위치 - -:::evidence key="grpc-advanced-streaming-f03" alt="코드베이스에서 ConcurrentMap 를 검색한 출력 23줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ConcurrentMap 코드베이스 검색 — 23줄 · exit 0" zoom="true" -::: - -## 체크포인트가 조용히 뒤로 간다 - -두 스레드가 순번 5 와 6 을 적용하며 같은 체크포인트(4)를 읽으면 둘 다 `advancedTo` 를 통과한다. 5 를 든 쪽이 나중에 `put` 하면 체크포인트는 6 에서 5 로 뒤로 간다 — `advancedTo` 가 막겠다고 한 바로 그 상태이고, 이번에는 예외 없이 조용히 일어난다. 그러면 순번 6 의 메시지가 다시 `APPLY` 로 판정되어 두 번 적용된다. - -## ConcurrentHashMap 에는 이 형태를 위한 연산이 있다 - -`compute` 안에서는 읽기와 쓰기가 원자적이므로, 뒤처진 쪽이 `advancedTo` 의 예외를 실제로 받는다 — 가드가 설계대로 발화한다. - -## 같은 리프의 다른 클래스들은 닫혀 있다 - -`GrpcDemandController` 는 모든 공개 메서드가 `synchronized` 이고, `GrpcBidiSequenceTracker` 도 그렇다(§12.2 가 그것을 이 가족의 모범으로 든다). 중복 제거기만 `ConcurrentMap` 의 원자 연산을 쓰지 않는다. - -## 시험 아홉 개가 전부 단일 스레드다 - -순차적으로는 `advancedTo` 가 정확히 작동하고, 전용 시험(`aCheckpointRecordsWhatWasApplied`)이 확인하는 것은 record 의 메서드이지 맵에 쓰는 경로가 아니다. 미배선이므로 P3. 다만 이 클래스의 javadoc 이 "The application effect and this checkpoint belong in one transaction" 이라고 적어 둔 것과 함께 보면, 이 자리는 배선되는 날 트랜잭션 경계와 함께 다시 설계될 곳이다. - -## 확인하지 못한 것 - -두 writer 가 한 세션을 체크포인트하는 경합을 재현하지 않았다. 가드가 보는 값이 호출 스레드가 읽은 값이라는 코드 형태로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-client-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-client-f01.md deleted file mode 100644 index f3624dc..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-client-f01.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -kind: CASE -slug: grpc-client-f01 -title: rotate 가 비교 후 교체가 아니라 덮어쓰기다 -topic: state-ownership-and-concurrency -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-client-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-client-f01 - file: ../../../final/evidence/rendered/grpc-client-f01.svg - - key: grpc-client-f01-diagram - file: ../../../final/assets/diagrams/grpc-client-f01.svg -evidence: - - ../../../final/evidence/raw/grpc-client-f01.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-client#L120 이다. -module: grpc-client -priority: P2 ---- - -# rotate 가 비교 후 교체가 아니라 덮어쓰기다 - -install 은 정확하다. rotate 는 그렇지 않다. - -## 문제 - -install 은 정확하다. - -rotate 는 그렇지 않다. - -## 결론 - -두 회전이 동시에 들어오면 둘 다 같은 previous 를 읽고, 둘 다 대체본을 만들고, 나중 set 이 앞의 대체본을 덮는다. - -덮인 대체본은 어디에도 등록되지 않는다 — draining 목록에 들어가는 것은 previous 뿐이다. - -그러므로 그 세대는 배수도 회수도 되지 않고, 그 위에서 시작된 호출은 아무도 세지 않는다. - -클래스가 이 문제를 인지하고 있다는 증거가 같은 파일에 있다 — install 의 비교 후 교체와 AtomicReference 선택이다. - -회전 쪽만 그 규율에서 벗어나 있다. - -수정은 holder.compareAndSet(previous, replacement) 로 바꾸고 실패 시 다시 읽어 판정하거나 던지는 것이다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : install 과 rotate 두 경로의 읽기·쓰기 원자성 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-client#L120 에 있다. - -## 본문 - - - -`install` 은 정확하다. `rotate` 는 그렇지 않다. - -## 경합이 끼는 자리 - -:::evidence key="grpc-client-f01-diagram" alt="동시 회전 둘이 같은 previous 읽기와 나중 set 이 덮음을 지나 덮인 세대 미등록으로 이어진다" caption="경합이 끼는 자리" zoom="false" -::: - -두 회전이 동시에 들어오면 둘 다 같은 `previous` 를 읽고, 둘 다 대체본을 만들고, 나중 `set` 이 앞의 대체본을 덮는다. - -## install 과 rotate 의 차이 - -:::evidence key="grpc-client-f01" alt="분석 문서 final/document.md#a20-grpc-client 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-client 발췌 — 15줄" zoom="true" -::: - -## 덮인 대체본은 어디에도 등록되지 않는다 - -`draining` 목록에 들어가는 것은 `previous` 뿐이다. 그러므로 그 세대는 배수도 회수도 되지 않고, 그 위에서 시작된 호출은 아무도 세지 않는다. - -## 클래스가 이 문제를 인지한다는 증거 - -같은 파일의 `install` 이 비교 후 교체를 쓰고 `AtomicReference` 를 골랐다. 회전 쪽만 그 규율에서 벗어나 있다. 수정은 `holder.compareAndSet(previous, replacement)` 로 바꾸고 실패 시 다시 읽어 판정하거나 던지는 것이다. - -## 확인하지 못한 것 - -실제 채널을 만들어 회전시키지 않았다. ManagedChannel 을 만드는 코드가 이 리프에 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-client-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-client-f02.md deleted file mode 100644 index b7d3605..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-client-f02.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -kind: CASE -slug: grpc-client-f02 -title: 비원자적 감소가 세대를 영구히 회수 불가로 만든다 -topic: state-ownership-and-concurrency -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-client-f02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-client-f02 - file: ../../../final/evidence/rendered/grpc-client-f02.svg - - key: grpc-client-f02-diagram - file: ../../../final/assets/diagrams/grpc-client-f02.svg -evidence: - - ../../../final/evidence/raw/grpc-client-f02.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-client#L149 이다. -module: grpc-client -priority: P2 ---- - -# 비원자적 감소가 세대를 영구히 회수 불가로 만든다 - -카운터가 1 일 때 두 스레드가 동시에 끝나면 둘 다 조건을 통과해 둘 다 감소시켜 −1 이 된다. 그 결과가 이 리프에서는 구체적이다. - -## 문제 - -카운터가 1 일 때 두 스레드가 동시에 끝나면 둘 다 조건을 통과해 둘 다 감소시켜 −1 이 된다. - -그 결과가 이 리프에서는 구체적이다. - -## 결론 - -정확히 0 을 요구한다. - -음수가 되면 조용해짐 판정이 영원히 거짓이고, retireQuiescent 가 그 세대를 결코 제거하지 않는다. - -회전이 반복될수록 draining 목록이 자란다. - -같은 형태가 이 가족의 다른 두 곳에도 있다(GrpcAdmissionController.release, GrpcStreamAdmission.release). - -그쪽은 경계가 느슨해지는 결과였고, 이쪽은 자원이 회수되지 않는 결과다. - -수정은 updateAndGet(v -> Math.max(0, v - 1)) 이나 decrementAndGet() 후 하한 보정이다. - -같은 가족의 GrpcRetryBudget 이 정확한 비교 후 교체 루프를 이미 쓴다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : GrpcAdmissionController 참조 16건 검색과 감소 연산의 원자성 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-client#L149 에 있다. - -## 본문 - - - -카운터가 1 일 때 두 스레드가 동시에 끝나면 둘 다 조건을 통과해 둘 다 감소시켜 −1 이 된다. - -## 회수가 막히는 자리 - -:::evidence key="grpc-client-f02-diagram" alt="비원자적 감소에서 계수기 음수로 둘 다 통과가 건너가고 계수기 음수에서 회수 불가로 조용함 거짓이 건너간다" caption="회수가 막히는 자리" zoom="false" -::: - -조용해짐 판정이 정확히 0 을 요구하므로, 음수가 되면 그 판정이 영원히 거짓이고 `retireQuiescent` 가 그 세대를 결코 제거하지 않는다. 회전이 반복될수록 `draining` 목록이 자란다. - -## GrpcAdmissionController 참조 위치 - -:::evidence key="grpc-client-f02" alt="코드베이스에서 GrpcAdmissionController 를 검색한 출력 16줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcAdmissionController 코드베이스 검색 — 16줄 · exit 0" zoom="true" -::: - -## 같은 형태가 이 가족의 다른 두 곳에도 있다 - -`GrpcAdmissionController.release`, `GrpcStreamAdmission.release`. 그쪽은 경계가 느슨해지는 결과였고, 이쪽은 자원이 회수되지 않는 결과다. 수정은 `updateAndGet(v -> Math.max(0, v - 1))` 이나 `decrementAndGet()` 후 하한 보정이다 — 같은 가족의 `GrpcRetryBudget` 이 정확한 비교 후 교체 루프를 이미 쓴다. - -## 확인하지 못한 것 - -동시 해제를 실행으로 재현해 계수기가 음수가 되는 것을 관측하지 않았다. 원자성 분석으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-client-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-client-f03.md deleted file mode 100644 index 3ffdf03..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-client-f03.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -kind: CASE -slug: grpc-client-f03 -title: 배수 목록의 순회가 동기화 밖에서 일어난다 -topic: state-ownership-and-concurrency -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-client-f03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-client-f03 - file: ../../../final/evidence/rendered/grpc-client-f03.svg -evidence: - - ../../../final/evidence/raw/grpc-client-f03.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-client#L176 이다. -module: grpc-client -priority: P3 ---- - -# 배수 목록의 순회가 동기화 밖에서 일어난다 - -Collections.synchronizedList 는 개별 연산만 동기화한다. 순회는 호출자가 그 목록을 잠그고 해야 한다는 것이 그 API 의 계약이다. - -## 문제 - -Collections.synchronizedList 는 개별 연산만 동기화한다. - -순회는 호출자가 그 목록을 잠그고 해야 한다는 것이 그 API 의 계약이다. - -## 결론 - -List.copyOf(...) 와 stream() 둘 다 순회다. - -회전이 동시에 add 하면 동시 변경 예외가 가능하다. - -그리고 읽고 지우는 두 단계가 원자적이지 않으므로, 그 사이에 조용해진 세대가 추가되면 이번 회수에서 빠진다. - -후자는 다음 호출에서 회수되므로 무해하다. - -수정은 CopyOnWriteArrayList 로 바꾸는 것이다. - -배수 목록은 쓰기가 드물고 읽기가 잦아 그 자료구조의 전형적 용례다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : synchronizedList 의 순회 계약과 실제 순회 지점의 잠금 유무 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-client#L176 에 있다. - -## 본문 - - - -`Collections.synchronizedList` 는 개별 연산만 동기화한다. 순회는 호출자가 그 목록을 잠그고 해야 한다는 것이 그 API 의 계약이다. `List.copyOf(...)` 와 `stream()` 둘 다 순회다. - -## synchronizedList 의 계약 - -:::evidence key="grpc-client-f03" alt="분석 문서 final/document.md#a20-grpc-client 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-client 발췌 — 15줄" zoom="true" -::: - -## 두 결과가 다르다 - -회전이 동시에 `add` 하면 동시 변경 예외가 가능하다. 그리고 읽고 지우는 두 단계가 원자적이지 않으므로, 그 사이에 조용해진 세대가 추가되면 이번 회수에서 빠진다 — 후자는 다음 호출에서 회수되므로 무해하다. - -## 수정 - -`CopyOnWriteArrayList` 로 바꾸는 것이다. 배수 목록은 쓰기가 드물고 읽기가 잦아 그 자료구조의 전형적 용례다. - -## 확인하지 못한 것 - -순회 중 동시 변경으로 예외를 재현하지 않았다. 순회가 호출자 잠금을 요구한다는 API 계약과 코드 형태로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-codegen-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-codegen-f02.md deleted file mode 100644 index f14cedb..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-codegen-f02.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -kind: CASE -slug: grpc-codegen-f02 -title: 릴리스 버전 불변성이 프로세스 안에서만 성립한다 -topic: state-ownership-and-concurrency -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-codegen-f02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-codegen-f02 - file: ../../../final/evidence/rendered/grpc-codegen-f02.svg -evidence: - - ../../../final/evidence/raw/grpc-codegen-f02.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-codegen#L220 이다. -module: grpc-codegen -priority: P3 ---- - -# 릴리스 버전 불변성이 프로세스 안에서만 성립한다 - -발행 이력이 발행자 인스턴스의 필드다. 새 프로세스는 아무것도 기억하지 못하므로 같은 버전을 다른 해시로 다시 발행하려는 시도가 통과한다. - -## 문제 - -발행 이력이 발행자 인스턴스의 필드다. - -새 프로세스는 아무것도 기억하지 못하므로 같은 버전을 다른 해시로 다시 발행하려는 시도가 통과한다. - -## 결론 - -이 클래스가 존재하는 이유가 그 규칙이다 — "refuses to let a released version change underneath its consumers." 그 규칙이 지켜지는 범위가 한 발행자 인스턴스의 수명이다. - -빌드마다 새 프로세스가 도는 것이 정상 형태이므로, 실제로 이 검사가 무언가를 막으려면 이력이 산출물 저장소나 파일에서 와야 한다. - -GrpcSchemaBaseline 이 이미 릴리스된 해시를 들고 있으므로 그 방향의 재료는 있다. - -덧붙여 이 맵은 동기화되지 않는다. - -발행자를 공유해 병렬로 평가하면 경합한다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : GrpcSchemaBaseline 참조 11건 검색과 발행 이력의 보관 위치 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-codegen#L220 에 있다. - -## 본문 - - - -발행 이력이 발행자 인스턴스의 필드다. 새 프로세스는 아무것도 기억하지 못하므로 같은 버전을 다른 해시로 다시 발행하려는 시도가 통과한다. - -## GrpcSchemaBaseline 참조 위치 - -:::evidence key="grpc-codegen-f02" alt="코드베이스에서 GrpcSchemaBaseline 를 검색한 출력 11줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcSchemaBaseline 코드베이스 검색 — 11줄 · exit 0" zoom="true" -::: - -## 이 클래스가 존재하는 이유가 그 규칙이다 - -"refuses to let a released version change underneath its consumers." 그 규칙이 지켜지는 범위가 한 발행자 인스턴스의 수명이다. - -## 빌드마다 새 프로세스가 도는 것이 정상이다 - -실제로 이 검사가 무언가를 막으려면 이력이 산출물 저장소나 파일에서 와야 한다. `GrpcSchemaBaseline` 이 이미 릴리스된 해시를 들고 있으므로 그 방향의 재료는 있다. - -## 덧붙여 이 맵은 동기화되지 않는다 - -발행자를 공유해 병렬로 평가하면 경합한다. - -## 확인하지 못한 것 - -새 프로세스에서 같은 버전을 다른 해시로 발행해 통과를 관측하지 않았다. 이력이 인스턴스 필드라는 것으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-discovery-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-discovery-f03.md deleted file mode 100644 index 4b12532..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-discovery-f03.md +++ /dev/null @@ -1,97 +0,0 @@ ---- -kind: CASE -slug: grpc-discovery-f03 -title: 목록으로 보고하는 검증기가 주소 수 0 에서 던진다 -topic: state-ownership-and-concurrency -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-discovery-f03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-discovery-f03 - file: ../../../final/evidence/rendered/grpc-discovery-f03.svg -evidence: - - ../../../final/evidence/raw/grpc-discovery-f03.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-discovery#L187 이다. -module: grpc-discovery -priority: P3 ---- - -# 목록으로 보고하는 검증기가 주소 수 0 에서 던진다 - -profile.resolverProfile(n) 이 new GrpcResolverProfile(DNS, …, n) 을 만들고, 그 정규 생성자가 거부한다. 그래서 violations(profile, 0) 은 빈 목록도 위반 목록도 아닌 IllegalArgumentException 이다. - -## 문제 - -profile.resolverProfile(n) 이 new GrpcResolverProfile(DNS, …, n) 을 만들고, 그 정규 생성자가 거부한다. - -그래서 violations(profile, 0) 은 빈 목록도 위반 목록도 아닌 IllegalArgumentException 이다. - -## 결론 - -같은 메서드가 profile == null 에는 명시적으로 던지고 나머지는 목록으로 답하므로, 호출자는 이 API 를 "던지지 않고 보고한다" 로 읽는다. - -expectedAddressCount 는 이 리프가 계산하지 않고 입력으로 받는 값이고(§16), 그 출처는 헤드리스 레코드의 DNS 조회 결과다. - -롤아웃 중 파드가 모두 교체되는 순간이나 셀렉터가 어긋난 서비스에서 그 답은 0 이다. - -그것은 이 리프가 다루는 문제 영역 안의 상태이지 프로그래밍 오류가 아니다 — 그리고 운영자가 가장 보고받고 싶어 할 상태다. - -GrpcResolverProfile 쪽 거부 자체는 옳다. - -값 객체가 "주소 0 개인 목표"를 표현하지 않는 것은 §4 의 두 겹 분담과 일치한다. - -어긋난 것은 그 위에 얹힌 검증기가 그 예외를 그대로 통과시킨다는 점이다. - -violations 가 expectedAddressCount < 1 을 먼저 보고 위반 문자열로 보고한 뒤 나머지 검사를 건너뛴다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : resolverProfile(n) 이 부르는 정규 생성자의 거부 조건 경로 추적 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-discovery#L187 에 있다. - -## 본문 - - - -검증기는 목록으로 보고하는 형태다. - -```java -public static List violations(GrpcKubernetesProfile profile, int expectedAddressCount) { - … - List violations = new ArrayList<>( - GrpcDiscoveryPolicyValidator.violations(profile.resolverProfile(expectedAddressCount))); -``` - -`profile.resolverProfile(n)` 이 `new GrpcResolverProfile(DNS, …, n)` 을 만들고, 그 정규 생성자가 `expectedAddressCount < 1` 을 거부한다. 그래서 `violations(profile, 0)` 은 빈 목록도 위반 목록도 아닌 `IllegalArgumentException` 이다. - -## 목록으로 보고하는 형태 - -:::evidence key="grpc-discovery-f03" alt="분석 문서 final/document.md#a20-grpc-discovery 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-discovery 발췌 — 15줄" zoom="true" -::: - -## 호출자는 이 API 를 던지지 않는 것으로 읽는다 - -같은 메서드가 `profile == null` 에는 명시적으로 던지고 나머지는 목록으로 답한다. - -## 왜 0 이 실제 값인가 - -`expectedAddressCount` 는 이 리프가 계산하지 않고 입력으로 받는 값이고(§16), 그 출처는 헤드리스 레코드의 DNS 조회 결과다. 롤아웃 중 파드가 모두 교체되는 순간이나 셀렉터가 어긋난 서비스에서 그 답은 0 이다 — 이 리프가 다루는 문제 영역 안의 상태이지 프로그래밍 오류가 아니고, 운영자가 가장 보고받고 싶어 할 상태다. - -## 값 객체 쪽 거부 자체는 옳다 - -값 객체가 "주소 0 개인 목표"를 표현하지 않는 것은 §4 의 두 겹 분담과 일치한다. 어긋난 것은 그 위에 얹힌 검증기가 그 예외를 그대로 통과시킨다는 점이다. `violations` 가 `expectedAddressCount < 1` 을 먼저 보고 위반 문자열로 보고한 뒤 나머지 검사를 건너뛰면 된다. - -## 확인하지 못한 것 - -violations(profile, 0) 을 실행으로 재현하지 않았다. resolverProfile → GrpcResolverProfile 정규 생성자 경로로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-operation-ledger-jpa-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-operation-ledger-jpa-f02.md deleted file mode 100644 index 25ca7fe..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-operation-ledger-jpa-f02.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -kind: CASE -slug: grpc-operation-ledger-jpa-f02 -title: markCommitted 는 던지고 markFailed 는 조용히 넘어간다 -topic: state-ownership-and-concurrency -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-operation-ledger-jpa-f02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-operation-ledger-jpa-f02 - file: ../../../final/evidence/rendered/grpc-operation-ledger-jpa-f02.svg -evidence: - - ../../../final/evidence/raw/grpc-operation-ledger-jpa-f02.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-operation-ledger-jpa#L192 이다. -module: grpc-operation-ledger-jpa -priority: P3 ---- - -# markCommitted 는 던지고 markFailed 는 조용히 넘어간다 - -커밋 쪽의 근거는 자바독에 있다 — 청구 없이 커밋하면 변경은 내구적이고 보호받지 못한다. 실패 쪽에는 근거가 없다. - -## 문제 - -커밋 쪽의 근거는 자바독에 있다 — 청구 없이 커밋하면 변경은 내구적이고 보호받지 못한다. - -실패 쪽에는 근거가 없다. - -## 결론 - -청구가 사라진 뒤 도착한 종결 실패가 아무 흔적도 남기지 않는다. - -회수가 청구를 지운 뒤 원래 소유자가 실패를 기록하려는 경우가 그 형태다. - -의도라면 그 이유를 자바독에 적어야 하고, 아니라면 커밋 쪽과 같게 다뤄야 한다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : 두 종결 경로의 없는 청구 처리 분기와 각 자바독의 근거 유무 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-operation-ledger-jpa#L192 에 있다. - -## 본문 - - - -두 종결 경로가 없는 청구를 다르게 다룬다. - -```java -markCommitted → findById(...).orElseThrow(IllegalStateException…) // 청구 없으면 실패 -markFailed → findById(...).ifPresent(entity -> …) // 청구 없으면 무동작 -``` - -## 두 종결 경로의 차이 - -:::evidence key="grpc-operation-ledger-jpa-f02" alt="분석 문서 final/document.md#a20-grpc-operation-ledger-jpa 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-operation-ledger-jpa 발췌 — 15줄" zoom="true" -::: - -## 커밋 쪽에는 근거가 있고 실패 쪽에는 없다 - -커밋 쪽의 근거는 자바독에 있다 — 청구 없이 커밋하면 변경은 내구적이고 보호받지 못한다. - -## 흔적 없이 사라지는 경우 - -청구가 사라진 뒤 도착한 종결 실패가 아무 흔적도 남기지 않는다. 회수가 청구를 지운 뒤 원래 소유자가 실패를 기록하려는 경우가 그 형태다. 의도라면 그 이유를 자바독에 적어야 하고, 아니라면 커밋 쪽과 같게 다뤄야 한다. - -## 확인하지 못한 것 - -청구 없는 markFailed 가 흔적 없이 사라지는 것을 실제 데이터베이스로 재현하지 않았다. 두 메서드의 분기 대조로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-policy-f04.md b/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-policy-f04.md deleted file mode 100644 index 8378ff7..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-policy-f04.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -kind: CASE -slug: grpc-policy-f04 -title: 완료 조정자가 요청 경로에서 동기화 없는 가변 리스트를 변경한다 -topic: state-ownership-and-concurrency -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-policy-f04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-policy-f04 - file: ../../../final/evidence/rendered/grpc-policy-f04.svg - - key: grpc-policy-f04-diagram - file: ../../../final/assets/diagrams/grpc-policy-f04.svg -evidence: - - ../../../final/evidence/raw/grpc-policy-f04.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-policy#L271 이다. -module: grpc-policy -priority: P2 ---- - -# 완료 조정자가 요청 경로에서 동기화 없는 가변 리스트를 변경한다 - -synchronized·Concurrent*·volatile·Lock 전부 0 이고 단일 스레드 전용 표기도 없다. 같은 리프의 GrpcSerializedStreamWriter 는 아홉 마커로 제대로 닫혀 있어, 이 리프가 동시성을 인지하고 있음을 보여 준다. - -## 문제 - -synchronized·Concurrent*·volatile·Lock 전부 0 이고 단일 스레드 전용 표기도 없다. - -같은 리프의 GrpcSerializedStreamWriter 는 아홉 마커로 제대로 닫혀 있어, 이 리프가 동시성을 인지하고 있음을 보여 준다. - -## 결론 - -reconcile 은 완료 결과가 불확실한 호출마다 불린다 — 장애 상황에서 동시에 몰리는 경로다. - -그리고 pending 이 담는 것은 사람이 조정해야 하는 연산 목록이므로, 유실은 조정되지 않은 채 잊히는 연산이 된다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : GrpcSerializedStreamWriter 참조 11건 검색과 조정자 쪽 동기화 마커 유무 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-policy#L271 에 있다. - -## 본문 - - - -`synchronized`·`Concurrent*`·`volatile`·`Lock` 전부 0 이고 단일 스레드 전용 표기도 없다. - -## 동기화가 걸린 곳과 아닌 곳 - -:::evidence key="grpc-policy-f04-diagram" alt="GrpcSerializedStreamWriter 만 동기화 마커 안에 놓이고 완료 조정자의 pending 이 바깥에 빗금으로 놓인다" caption="동기화가 걸린 곳과 아닌 곳" zoom="false" -::: - -같은 리프의 `GrpcSerializedStreamWriter` 는 아홉 마커로 제대로 닫혀 있어, 이 리프가 동시성을 인지하고 있음을 보여 준다. - -## GrpcSerializedStreamWriter 참조 위치 - -:::evidence key="grpc-policy-f04" alt="코드베이스에서 GrpcSerializedStreamWriter 를 검색한 출력 11줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcSerializedStreamWriter 코드베이스 검색 — 11줄 · exit 0" zoom="true" -::: - -## 하필 장애 상황에 몰리는 경로다 - -`reconcile` 은 완료 결과가 불확실한 호출마다 불린다. 그리고 `pending` 이 담는 것은 사람이 조정해야 하는 연산 목록이므로, 유실은 조정되지 않은 채 잊히는 연산이 된다. - -## 확인하지 못한 것 - -요청 경로에서 동시 변경을 재현하지 않았다. 동기화 마커가 0 이고 단일 스레드 전용 표기도 없다는 것으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-policy-f05.md b/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-policy-f05.md deleted file mode 100644 index 9cab41c..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-policy-f05.md +++ /dev/null @@ -1,91 +0,0 @@ ---- -kind: CASE -slug: grpc-policy-f05 -title: 스트림 수명 조정자의 배수 신호가 스레드를 건너면서 volatile 이 아니다 -topic: state-ownership-and-concurrency -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-policy-f05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-policy-f05 - file: ../../../final/evidence/rendered/grpc-policy-f05.svg - - key: grpc-policy-f05-diagram - file: ../../../final/assets/diagrams/grpc-policy-f05.svg -evidence: - - ../../../final/evidence/raw/grpc-policy-f05.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-policy#L285 이다. -module: grpc-policy -priority: P2 ---- - -# 스트림 수명 조정자의 배수 신호가 스레드를 건너면서 volatile 이 아니다 - -두 메서드의 호출자가 다른 스레드다. signalDrain() 은 서버가 내려갈 때 종료 훅이 부르고, terminationDue(...) 는 스트림 자신의 틱에서 불린다 — 클래스 javadoc 이 검사 순서를 "then drain, because a server that has been told to stop should stop before its own timers fire" 로 규정한 그 틱이다. - -## 문제 - -두 메서드의 호출자가 다른 스레드다. - -signalDrain() 은 서버가 내려갈 때 종료 훅이 부르고, terminationDue(...) 는 스트림 자신의 틱에서 불린다 — 클래스 javadoc 이 검사 순서를 "then drain, because a server that has been told to stop should stop before its own timers fire" 로 규정한 그 틱이다. - -## 결론 - -평범한 boolean 이고 volatile·synchronized·AtomicBoolean 어느 것도 없다. - -자바 메모리 모델 아래서 틱 스레드가 이 쓰기를 관측할 보장이 없다. - -관측하지 못하면 스트림은 배수 명령을 받고도 계속 돌고, 최대 수명(기본 1시간)이 차야 끝난다. - -같은 저장소가 같은 뜻의 플래그를 두 번은 volatile 로 적었다(§12.3). - -세 번째만 빠졌다. - -수정은 volatile boolean 한 단어다. - -heartbeat 의 lastActivity 는 같은 문제가 아니다 — 스트림 틱 스레드만 만진다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : 두 메서드를 부르는 스레드의 소속 추적과 배수 신호 필드의 volatile 유무 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-policy#L285 에 있다. - -## 본문 - - - -두 메서드의 호출자가 다른 스레드다. `signalDrain()` 은 서버가 내려갈 때 종료 훅이 부르고, `terminationDue(...)` 는 스트림 자신의 틱에서 불린다 — 클래스 javadoc 이 검사 순서를 "then drain, because a server that has been told to stop should stop before its own timers fire" 로 규정한 그 틱이다. - -## 신호가 건너는 경계 - -:::evidence key="grpc-policy-f05-diagram" alt="종료 훅 스레드에서 배수 신호 boolean 으로 signalDrain 쓰기가 건너가고 배수 신호 boolean 에서 스트림 틱 스레드로 terminationDue 읽기가 건너간다" caption="신호가 건너는 경계" zoom="false" -::: - -평범한 `boolean` 이고 `volatile`·`synchronized`·`AtomicBoolean` 어느 것도 없다. - -## 두 메서드를 부르는 스레드 - -:::evidence key="grpc-policy-f05" alt="분석 문서 final/document.md#a20-grpc-policy 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-policy 발췌 — 15줄" zoom="true" -::: - -## 관측하지 못하면 최대 수명까지 돈다 - -자바 메모리 모델 아래서 틱 스레드가 이 쓰기를 관측할 보장이 없다. 관측하지 못하면 스트림은 배수 명령을 받고도 계속 돌고, 최대 수명(기본 1시간)이 차야 끝난다. - -## 같은 뜻의 플래그를 두 번은 volatile 로 적었다 - -§12.3. 세 번째만 빠졌다. 수정은 `volatile boolean` 한 단어다. `heartbeat` 의 `lastActivity` 는 같은 문제가 아니다 — 스트림 틱 스레드만 만진다. - -## 확인하지 못한 것 - -가시성 실패를 관측하지 않았다. 관측하려면 배수 스레드와 스트림 틱 스레드를 분리한 반복 시험이 필요하고, 이런 실패는 재현되지 않는 것이 정상이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-testkit-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-testkit-f02.md deleted file mode 100644 index f3df294..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-grpc-testkit-f02.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -kind: CASE -slug: grpc-testkit-f02 -title: 릴리스 게이트의 입력이 전부 호출자가 손으로 만드는 값이다 -topic: state-ownership-and-concurrency -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-testkit-f02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-testkit-f02 - file: ../../../final/evidence/rendered/grpc-testkit-f02.svg -evidence: - - ../../../final/evidence/raw/grpc-testkit-f02.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-testkit#L172 이다. -module: grpc-testkit -priority: P3 ---- - -# 릴리스 게이트의 입력이 전부 호출자가 손으로 만드는 값이다 - -세 입력 중 어느 것도 실제 레인 결과나 실제 산출물에서 오지 않는다. 게이트를 부르는 곳은 자기 테스트 하나뿐이고, 그 테스트가 세 값을 리터럴로 만든다. - -## 문제 - -세 입력 중 어느 것도 실제 레인 결과나 실제 산출물에서 오지 않는다. - -게이트를 부르는 곳은 자기 테스트 하나뿐이고, 그 테스트가 세 값을 리터럴로 만든다. - -## 결론 - -이 형태 자체는 이 저장소의 다른 게이트와 다르다. - -mongo 가족의 증거 검증기는 테스트 결과 XML 을 읽고 파일의 수정 시각까지 본다. - -이쪽 게이트는 그런 산출물 판독기를 갖지 않는다. - -지금은 무해하다 — 릴리스 절차가 이 게이트를 부르지 않기 때문이다. - -기록하는 이유는 §17.1 을 고쳐 레인을 자동으로 돌리게 되면, 그 결과를 이 게이트에 넣어 주는 코드가 함께 필요하다는 점이다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : 게이트의 세 입력이 오는 지점 추적과 유일한 호출처인 테스트의 값 생성 방식 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-testkit#L172 에 있다. - -## 본문 - - - -세 입력 중 어느 것도 실제 레인 결과나 실제 산출물에서 오지 않는다. 게이트를 부르는 곳은 자기 테스트 하나뿐이고, 그 테스트가 세 값을 리터럴로 만든다. - -## 게이트의 세 입력이 오는 곳 - -:::evidence key="grpc-testkit-f02" alt="분석 문서 final/document.md#a20-grpc-testkit 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-testkit 발췌 — 15줄" zoom="true" -::: - -## 이 저장소의 다른 게이트와 형태가 다르다 - -mongo 가족의 증거 검증기는 테스트 결과 XML 을 읽고 파일의 수정 시각까지 본다. 이쪽 게이트는 그런 산출물 판독기를 갖지 않는다. - -## 지금은 무해하다 - -릴리스 절차가 이 게이트를 부르지 않는다. 기록하는 이유는 §17.1 을 고쳐 레인을 자동으로 돌리게 되면, 그 결과를 이 게이트에 넣어 주는 코드가 함께 필요하다는 점이다. - -## 확인하지 못한 것 - -게이트를 실제 레인 결과로 실행해 보지 않았다. 세 값이 테스트 안에서 리터럴로 만들어진다는 것으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-messaging-admin-runtime-f06.md b/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-messaging-admin-runtime-f06.md deleted file mode 100644 index f0331e7..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-messaging-admin-runtime-f06.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -kind: CASE -slug: messaging-admin-runtime-f06 -title: 저널의 itemsCompleted 단조성이 인터페이스 계약에 없다 -topic: state-ownership-and-concurrency -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-admin-runtime-f06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-admin-runtime-f06 - file: ../../../final/evidence/rendered/messaging-admin-runtime-f06.svg -evidence: - - ../../../final/evidence/raw/messaging-admin-runtime-f06.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-admin-runtime#L936 이다. -module: messaging-admin-runtime -priority: P3 ---- - -# 저널의 itemsCompleted 단조성이 인터페이스 계약에 없다 - -AdminOperationJournal javadoc 은 구현 의무 셋을 명시하면서 이것을 빠뜨렸고, fail 의 @param 은 오히려 문자 그대로 저장하라고 읽힌다. 유일한 호출자는 낡은 값을 넘긴다. - -## 문제 - -AdminOperationJournal javadoc 은 구현 의무 셋을 명시하면서 이것을 빠뜨렸고, fail 의 @param 은 오히려 문자 그대로 저장하라고 읽힌다. - -유일한 호출자는 낡은 값을 넘긴다. - -## 결론 - -두 구현이 각각 clamp 해서 무사한 상태다(EVD-308). - -두 가지 중 하나가 필요하다. - -인터페이스 javadoc 에 "itemsCompleted 는 단조 증가해야 하며 구현은 기존 값보다 작은 값을 무시한다" 를 명시하거나, 호출자가 실제 체크포인트 값을 넘기도록 고친다. - -후자가 더 정직하다 — 지금 journal.fail(lease, lease.resumeFrom(), …) 은 "이번 시도가 아무것도 못 했다" 고 주장하는 것이고, 그것은 대개 사실이 아니다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : AdminOperationJournal 참조 30건 검색과 javadoc 의 구현 의무 목록·유일한 호출자가 넘기는 값 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-admin-runtime#L936 에 있다. - -## 본문 - - - -`AdminOperationJournal` javadoc 은 구현 의무 셋을 명시하면서 `itemsCompleted` 단조성을 빠뜨렸고, `fail` 의 `@param` 은 오히려 문자 그대로 저장하라고 읽힌다. - -## AdminOperationJournal 참조 위치 - -:::evidence key="messaging-admin-runtime-f06" alt="코드베이스에서 AdminOperationJournal 를 검색한 출력 30줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="AdminOperationJournal 코드베이스 검색 — 30줄 · exit 0" zoom="true" -::: - -## 유일한 호출자가 낡은 값을 넘긴다 - -두 구현이 각각 clamp 해서 무사한 상태다(`EVD-308`). - -## 두 가지 중 하나가 필요하다 - -인터페이스 javadoc 에 "`itemsCompleted` 는 단조 증가해야 하며 구현은 기존 값보다 작은 값을 무시한다" 를 명시하거나, 호출자가 실제 체크포인트 값을 넘기도록 고친다. 후자가 더 정직하다 — 지금 `journal.fail(lease, lease.resumeFrom(), …)` 은 "이번 시도가 아무것도 못 했다" 고 주장하는 것이고, 그것은 대개 사실이 아니다. - -## 확인하지 못한 것 - -JdbcAdminOperationJournal 의 실제 동작을 확인하지 않았다. Postgres 컨테이너가 필요하고 미실행이다. SQL 문자열은 읽어 GREATEST 를 확인했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-messaging-admin-runtime-f07.md b/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-messaging-admin-runtime-f07.md deleted file mode 100644 index 66b92f4..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-messaging-admin-runtime-f07.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -kind: CASE -slug: messaging-admin-runtime-f07 -title: 리플레이가 리스를 받지만 재개하지 않는다 -topic: state-ownership-and-concurrency -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-admin-runtime-f07 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-admin-runtime-f07 - file: ../../../final/evidence/rendered/messaging-admin-runtime-f07.svg -evidence: - - ../../../final/evidence/raw/messaging-admin-runtime-f07.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-admin-runtime#L942 이다. -module: messaging-admin-runtime -priority: P3 ---- - -# 리플레이가 리스를 받지만 재개하지 않는다 - -executeReplay 는 journal.begin(...) 으로 리스를 받고 lease.resumeFrom() 을 쓰지 않는다. ReplayService.replay(...) 시그니처에 재개 지점이 없고 체크포인트 콜백도 없다. - -## 문제 - -executeReplay 는 journal.begin(...) 으로 리스를 받고 lease.resumeFrom() 을 쓰지 않는다. - -ReplayService.replay(...) 시그니처에 재개 지점이 없고 체크포인트 콜백도 없다. - -## 결론 - -클래스 javadoc 의 "a retry continues the same operation" 은 리드라이브에만 해당한다. - -리플레이가 재개 불필요하다면(같은 구간을 다시 읽는 것이 멱등이므로) 그 근거를 적고, 저널 사용을 "중복 실행 방지" 로만 한정하는 것이 낫다. - -재개가 필요하다면 리드라이브와 같은 형태로 맞춘다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : executeReplay 가 리스에서 읽는 값과 replay 시그니처의 재개 지점 유무 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-admin-runtime#L942 에 있다. - -## 본문 - - - -`executeReplay` 는 `journal.begin(...)` 으로 리스를 받고 `lease.resumeFrom()` 을 쓰지 않는다. `ReplayService.replay(...)` 시그니처에 재개 지점이 없고 체크포인트 콜백도 없다. - -## executeReplay 가 리스를 다루는 방식 - -:::evidence key="messaging-admin-runtime-f07" alt="분석 문서 final/document.md#a19-messaging-admin-runtime 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a19-messaging-admin-runtime 발췌 — 15줄" zoom="true" -::: - -## 클래스 javadoc 의 문장이 절반에만 해당한다 - -"a retry continues the same operation" 은 리드라이브에만 해당한다. - -## 두 방향 - -리플레이가 재개 불필요하다면(같은 구간을 다시 읽는 것이 멱등이므로) 그 근거를 적고 저널 사용을 "중복 실행 방지" 로만 한정하는 것이 낫다. 재개가 필요하다면 리드라이브와 같은 형태로 맞춘다. - -## 확인하지 못한 것 - -리플레이를 중간에 끊고 재개해 관측하지 않았다. 시그니처에 재개 지점과 체크포인트 콜백이 없다는 것으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-messaging-rabbit-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-messaging-rabbit-f02.md deleted file mode 100644 index 8d58494..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/case/case-messaging-rabbit-f02.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -kind: CASE -slug: messaging-rabbit-f02 -title: 반환을 순번에 맞추는 조각이 production 에 없고, 시험이 그 자리를 스스로 메운다 -topic: state-ownership-and-concurrency -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-rabbit-f02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-rabbit-f02 - file: ../../../final/evidence/rendered/messaging-rabbit-f02.svg - - key: messaging-rabbit-f02-diagram - file: ../../../final/assets/diagrams/messaging-rabbit-f02.svg -evidence: - - ../../../final/evidence/raw/messaging-rabbit-f02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-rabbit#L238 이다. -module: messaging-rabbit -priority: P2 ---- - -# 반환을 순번에 맞추는 조각이 production 에 없고, 시험이 그 자리를 스스로 메운다 - -이 어댑터의 핵심 보장(§1)은 반환과 확인을 같은 발행 에 묶는 데 달려 있다. 묶는 열쇠는 순번이다. - -## 문제 - -이 어댑터의 핵심 보장(§1)은 반환과 확인을 같은 발행 에 묶는 데 달려 있다. - -묶는 열쇠는 순번이다. - -## 결론 - -그런데 AMQP 의 basic.return 콜백은 순번을 주지 않는다. - -교환기·라우팅 키·속성·본문만 온다. - -그래서 발행자가 순번을 메시지에 실어 보내고 반환에서 되읽어야 한다. - -RabbitHeaderMapper.toProperties 전문에 그런 헤더가 없다. - -쓰는 것은 msg.* 예약 헤더들과 AMQP 의 messageId·correlationId·timestamp·deliveryMode 뿐이다. - -그 조각이 존재하는 곳은 시험 하나다. - -그 메서드의 javadoc 이 문제를 정확히 서술한다. - -"the adapter has to" 인데 어댑터는 하지 않는다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : RabbitHeaderMapper 참조 13건 검색과 toProperties 전문에서 순번 헤더 유무 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-rabbit#L238 에 있다. - -## 본문 - - - -이 어댑터의 핵심 보장(§1)은 반환과 확인을 **같은 발행** 에 묶는 데 달려 있고, 묶는 열쇠는 순번이다. 그런데 AMQP 의 `basic.return` 콜백은 순번을 주지 않는다 — 교환기·라우팅 키·속성·본문만 온다. - -## 프로덕션에 없는 조각 - -:::evidence key="messaging-rabbit-f02-diagram" alt="msg 예약 헤더와 AMQP 표준 속성만 헤더 매퍼가 쓰는 것 안에 놓이고 순번 헤더가 바깥에 빗금으로 놓인다" caption="프로덕션에 없는 조각" zoom="false" -::: - -`RabbitHeaderMapper.toProperties` 전문에 그런 헤더가 없다. 쓰는 것은 `msg.*` 예약 헤더들과 AMQP 의 `messageId`·`correlationId`·`timestamp`·`deliveryMode` 뿐이다. - -## RabbitHeaderMapper 참조 위치 - -:::evidence key="messaging-rabbit-f02" alt="코드베이스에서 RabbitHeaderMapper 를 검색한 출력 13줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RabbitHeaderMapper 코드베이스 검색 — 13줄 · exit 0" zoom="true" -::: - -## 그 조각이 존재하는 곳은 시험 하나다 - -그 메서드의 javadoc 이 문제를 정확히 서술한다 — "the adapter has to" 인데 어댑터는 하지 않는다. `RabbitChannelPublisher` 의 javadoc 은 등록 경합(확인이 `basicPublish` 반환보다 먼저 올 수 있다)만 설명하고 이 상관 문제는 언급하지 않는다. - -## 구현하는 사람이 규약을 다시 발명해야 한다 - -발명하지 않으면 `onReturn` 이 호출되지 않아 unroutable 발행이 **`CONFIRMED` 로 보고된다** — 이 어댑터가 존재하는 이유로 든 바로 그 실패다. 수정은 순번 헤더를 `RabbitHeaderMapper` 나 `RabbitPublishMapper` 로 올려 production 계약으로 만들고, 그 이름을 `RabbitChannelPublisher` javadoc 에 적는 것이다. - -## 확인하지 못한 것 - -실행으로 재현하지 않았다. 매퍼 전문에 순번 헤더가 없다는 것과, 통합 시험이 자기 발행 람다에서 그 헤더를 붙인다는 것으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/reference/reference-messaging-claim-check-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/reference/reference-messaging-claim-check-f02.md deleted file mode 100644 index 179fad5..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/reference/reference-messaging-claim-check-f02.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-claim-check-f02 -title: 같은 튜닝 값이 두 계층에 있으면 어느 쪽이 이기는지 정한다 -topic: state-ownership-and-concurrency -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-claim-check-f02 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-claim-check#L516 ---- - -# 같은 튜닝 값이 두 계층에 있으면 어느 쪽이 이기는지 정한다 - -## 관계 - -- **배포 아티팩트가 싣지만 아무도 부르지 않고, 다른 곳의 에러 메시지가 이 경로를 권한다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **예외 승격이 에러 코드 문자열 접미사에 의존한다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. - -## 목적 - -두 값이 나란히 선언돼 있고 어느 쪽도 다른 쪽을 읽지 않으면, 실행되는 순간 한쪽만 살아남고 다른 쪽은 선언으로만 남는다. 목적지별로 다르게 두려던 설계가 전역 값 하나에 덮이는 것이 그 형태다. - -## 규칙 - -1. 같은 의미의 튜닝 값이 두 계층에 있는지 먼저 센다 - messaging-policy 의 PayloadPolicy.claimCheckThresholdBytes 는 목적지별이고 DestinationProfileValidator:49 가 검사한다. messaging-claim-check 의 ClaimCheckPolicy.thresholdBytes 는 전역이다. - -2. 두 값을 대조하는 코드가 있는지 확인한다 - 대조가 없으면 둘은 같은 이름을 가진 서로 다른 설정이다. - -3. 우선순위를 코드로 표현한다 - 좁은 쪽이 넓은 쪽을 읽거나, 넓은 쪽에서 그 필드를 없앤다. 문서로만 정한 우선순위는 강제되지 않는다. - -## 적용 조건 - -같은 값이 정책 계층과 구현 계층에 각각 선언되는 자리. 문턱·상한·타임아웃처럼 목적지별로 달라질 수 있는 값이 특히 그렇다. - -## 예외 - -SSOT 가 이 규칙의 반례를 적지 않았다. 두 값을 의도적으로 다르게 두는 설계가 있다면 그 이유가 어느 한쪽 javadoc 에 있어야 하는데, 지금은 없다. - -## 예시 - -두 필드의 선언 위치와 DestinationProfileValidator:49 의 검사 대상, 그리고 두 값을 잇는 코드가 없다는 것. 원문 근거는 evidence/raw/290 §B 이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/reference/reference-messaging-security-f08.md b/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/reference/reference-messaging-security-f08.md deleted file mode 100644 index ea5d395..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/reference/reference-messaging-security-f08.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-security-f08 -title: 가변 필드로 상태 전이를 표현하면 가시성을 함께 정한다 -topic: state-ownership-and-concurrency -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-security-f08 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-security#L697 ---- - -# 가변 필드로 상태 전이를 표현하면 가시성을 함께 정한다 - -## 관계 - -- **종료 시 자격증명 소거가 호출되지 않는다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **같은 TLS posture를 두 클래스가 다른 엄격도로 검사한다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **권한 거부가 `AUTHORIZATION`이 아니라 `CONFIGURATION`으로 기록된다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. - -## 목적 - -정상 경로에서는 ConcurrentHashMap 의 compute 가 happens-before 를 준다. clearAll() 경로에는 그 보장이 없어서, 다른 스레드가 소거된 배열의 옛 참조를 읽을 수 있다. 방향은 안전한 쪽이다 — 비밀 유출이 아니라 0 으로 채워진 값을 읽는다. - -## 규칙 - -1. 상태 전이를 표현하는 필드의 선언을 확인한다 - private char[] material 이 volatile 이 아니고 clear() 가 그것을 교체한다. - -2. 그 필드를 읽는 경로가 전부 같은 동기화 안에 있는지 본다 - clearAll() 은 락 없이 순회한다. 그 경로만 보장 밖이다. - -3. 가시성을 필드나 접근 경로 중 한쪽에서 정한다 - material 을 volatile 로 하거나 clearAll() 을 compute 기반으로 바꾼다. - -## 적용 조건 - -소거·회전·상태 전이를 필드 교체로 표현하고, 그 필드를 여러 스레드가 읽는 모든 자리. - -## 예외 - -모든 읽기와 쓰기가 같은 compute 안에서 일어나면 별도 가시성 선언이 필요 없다. 이 클래스는 그 조건을 한 경로에서만 만족한다. - -## 예시 - -CredentialRuntime.java:29,129-132 와 CredentialRuntimeRegistry.java:129-132. 확인 방법은 필드 선언을 보는 것이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/reference/reference-messaging-spring-cloud-stream-bridge-f04.md b/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/reference/reference-messaging-spring-cloud-stream-bridge-f04.md deleted file mode 100644 index c730a9d..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/reference/reference-messaging-spring-cloud-stream-bridge-f04.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-spring-cloud-stream-bridge-f04 -title: 함께 읽히는 두 맵은 한 값으로 묶는다 -topic: state-ownership-and-concurrency -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-spring-cloud-stream-bridge-f04 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-spring-cloud-stream-bridge#L579 ---- - -# 함께 읽히는 두 맵은 한 값으로 묶는다 - -## 목적 - -두 put 사이에 dispatch 가 들어오면 destination == null 이 되어 NO_BRIDGED_HANDLER 가 난다. 방향은 안전하다 — 잘못된 목적지로 전달하지는 않는다. 틀리는 것은 에러 코드다. 핸들러가 없다고 말하는데 실제로는 핸들러가 있고 목적지가 아직 없다. - -## 규칙 - -1. 한 논리 등록이 몇 번의 put 으로 나뉘는지 센다 - SpringCloudStreamConsumerBridge.register 가 handlers.put(...) 후 destinations.put(...) 을 한다. 같은 형태가 publisher 의 두 맵에도 있고 그쪽은 키가 각각 독립이다. - -2. 그 사이에 읽는 경로가 있는지 본다 - dispatch 가 그 창에 들어온다. - -3. 두 값을 한 record 로 묶어 한 번에 넣는다 - 창 자체가 사라진다. - -## 적용 조건 - -한 등록·한 전이가 두 개 이상의 맵 갱신으로 표현되고, 그 맵들을 함께 읽는 경로가 있는 자리. - -## 예외 - -두 맵의 키가 독립이고 읽는 쪽이 둘을 함께 보지 않으면 대상이 아니다. publisher 쪽이 그 경우에 가깝다. - -## 예시 - -SpringCloudStreamConsumerBridge.java:38-39 의 두 put. 확인 방법은 그 사이의 창을 보는 것이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/reference/reference-messaging-spring-cloud-stream-bridge-f06.md b/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/reference/reference-messaging-spring-cloud-stream-bridge-f06.md deleted file mode 100644 index c8fe3d1..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/state-ownership-and-concurrency/reference/reference-messaging-spring-cloud-stream-bridge-f06.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-spring-cloud-stream-bridge-f06 -title: 에러 메시지가 지시하는 설정은 그 설정을 읽는 코드와 함께 존재해야 한다 -topic: state-ownership-and-concurrency -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-spring-cloud-stream-bridge-f06 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-spring-cloud-stream-bridge#L597 ---- - -# 에러 메시지가 지시하는 설정은 그 설정을 읽는 코드와 함께 존재해야 한다 - -## 목적 - -STREAM_BRIDGE_DISABLED 메시지가 지시하는 프로퍼티를 읽는 코드가 없다. 이 저장소에서 같은 형태가 세 번째다 — messaging-kafka-share-experimental 과 messaging-claim-check 가 앞선 둘이다. - -## 규칙 - -1. 메시지에 등장하는 키를 검색한다 - git grep -n 'spring-cloud-stream=true' -- src 가 이 leaf 의 문자열 하나만 돌려준다. - -2. 같은 형태가 가족 안에 몇 번 있는지 센다 - 세 leaf 가 같은 방식으로 실행 불가능한 지시를 남겼다. 개별 실수가 아니라 형태다. - -3. 바인딩을 만들거나 메시지에서 키를 뺀다 - 지시는 실행 가능할 때만 지시다. - -## 적용 조건 - -실패 메시지가 복구 방법을 프로퍼티 키로 제시하는 모든 자리. - -## 예외 - -SSOT 가 이 규칙의 반례를 적지 않았다. 배선 계획이 확정된 키를 미리 안내하는 경우라면 그 사실이 메시지 안에 있어야 한다. - -## 예시 - -backend.messaging.bridge.spring-cloud-stream=true 가 STREAM_BRIDGE_DISABLED 메시지에만 존재한다는 것. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/tech-log-tree.json b/docs/clean-architecture-backend-template/tech-log-studio/tech-log-tree.json index 034dd8a..3d436c7 100644 --- a/docs/clean-architecture-backend-template/tech-log-studio/tech-log-tree.json +++ b/docs/clean-architecture-backend-template/tech-log-studio/tech-log-tree.json @@ -27,9 +27,10 @@ "제2부 — 모듈 분석 전문", "제3부 — 분석 재료" ], - "note": "제1부(§0~§14 + 부록)가 통합 분석이고 그 가운데 §3~§11 이 후보를 찾는 범위다. 제2부의 모듈 분석 65편과 제3부의 분석 재료는 근거이지 후보 자리가 아니다 — 지금 트리의 글감 대부분이 거기서 나왔고, 그것이 재판정이 필요한 이유다" + "note": "제1부(§0~§14 + 부록)가 통합 분석이고 그 가운데 §3~§11 이 후보를 찾는 범위다. 제2부의 모듈 분석 65편과 제3부의 분석 재료는 근거이지 후보 자리가 아니다. 글감의 source 는 제1부 앵커를 적어도 하나 갖고, 제2부 앵커는 그 주장을 상세히 확인하는 자리로만 붙인다", + "excludedAnchorPattern": "#`?(?:a\\d\\d|a\\d\\d-|부록)" }, - "note": "이 프로젝트의 글감 전부다. 분해 계약이자 색인이고, 이 파일이 정본이다. 노드의 칸(readiness·source·classification·relations…)은 사람이 적고, file·publication·status 는 기록 파일에서 읽어 채운다 — python3 scripts/build-tech-log-tree.py <프로젝트>", + "note": "이 프로젝트의 글감 전부다. 분해 계약이자 색인이고, 이 파일이 정본이다. 노드의 칸은 사람이 적고 file·publication·status 는 기록 파일에서 읽어 채운다 — python3 scripts/build-tech-log-tree.py clean-architecture-backend-template", "contract": { "decomposition": [ "**분해 기준.** Topic은 공학 문제 공간이고 디렉터리가 아니다. 한 Topic 안의 Case들은 서로 다른", @@ -64,12 +65,12 @@ } }, "counts": { - "topics": 44, - "nodes": 1001, - "written": 837, - "unwritten": 164, - "unlisted": 103, - "candidates": 965 + "topics": 16, + "nodes": 123, + "written": 112, + "unwritten": 11, + "unlisted": 0, + "candidates": 1088 }, "topics": { "commit-ambiguity-as-a-result": { @@ -109,6 +110,7 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [ "../../../final/evidence/raw/050-jpa-commit-ambiguity-probe.txt", "../../../final/evidence/raw/115-integration-lane-original-verification.txt" @@ -138,7 +140,33 @@ "assets": [ "commit-evidence-phase-machine" ], + "assetFiles": [ + "commit-evidence-phase-machine" + ], "evidenceFiles": [] + }, + { + "title": "발행 증거와 완료 판정이 따로 있는 이유", + "slug": "publish-evidence-and-completion", + "readiness": "READY", + "source": [ + "final/document.md#3-3", + "final/document.md#a19 §3.2" + ], + "code": [ + "PublishEvidence", + "PublishCompletion", + "TransmissionEvidence" + ], + "basis-version": "clean-architecture-backend-template @ 21234e38 · messaging 플랫폼의 3축 실패 어휘", + "classification": "아무것도 프로세스를 떠나지 않은 실패와 wire 위에 있던 실패가 다른 결론을 받아야 하는 이유를, compact constructor 가 표현 불가능한 조합을 거부하는 구조로 설명한다", + "relations": [ + "decision:record-the-evidence-first-choose-the-conclusion-later", + "reference:unknown-is-a-third-result", + "concept:transaction-result-algebra" + ], + "kind": "concept", + "publication": "미작성" } ], "reference": [ @@ -163,6 +191,7 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] } ], @@ -189,6 +218,7 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [ "../../../final/evidence/raw/115-integration-lane-original-verification.txt" ] @@ -220,7 +250,29 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] + }, + { + "title": "증거를 먼저 기록하고 결론은 나중에 고른다", + "slug": "record-the-evidence-first-choose-the-conclusion-later", + "readiness": "READY", + "source": [ + "final/document.md#10-2", + "final/document.md#3-3", + "final/document.md#a19 §3.2" + ], + "decision-status": "ADOPTED", + "decision-evidence": "`PublishEvidence` 가 brokerAccepted·confirmationLevel·transmission 을 따로 담고 compact constructor 가 표현 불가능한 조합을 거부한다 (final/document.md#3-3)", + "grounds": "저장된 결과만으로 운영자가 「브로커가 이 메시지를 들고 있을 수 있나」에 답할 수 있어야 한다. 결론만 남기면 그 질문에 답할 수 없고, ambiguous 로 뭉뚱그리면 어떤 브로커도 보지 못한 메시지에 대해 caller 를 reconciliation 으로 보낸다. 감수한 비용은 저장할 것이 늘어난다는 것이다", + "classification": "프로젝트가 증거와 결론을 분리하는 쪽을 골랐고 타입이 그 분리를 강제한다", + "relations": [ + "concept:publish-evidence-and-completion", + "decision:completion-unknown-is-never-retried", + "reference:unknown-is-a-third-result" + ], + "kind": "decision", + "publication": "미작성" } ] } @@ -228,7 +280,7 @@ "assembly-ownership": { "topic": "assembly-ownership", "title": "조립 소유권 — 통제와 그 의존을 같은 곳이 소유하기", - "readerQuestion": "", + "readerQuestion": "선언된 통제가 실제 출하 경로에 설치되었는지를 무엇으로 판정하는가?", "kinds": { "case": [ { @@ -265,6 +317,9 @@ "assets": [ "scan-exclusion-without-an-owner" ], + "assetFiles": [ + "scan-exclusion-without-an-owner" + ], "evidenceFiles": [ "../../../final/evidence/raw/scan-exclusion-without-an-owner.txt", "../../../final/evidence/raw/tl-web-six-unowned-components.txt" @@ -302,6 +357,9 @@ "assets": [ "outbox-chain-behind-an-unsatisfiable-condition" ], + "assetFiles": [ + "outbox-chain-behind-an-unsatisfiable-condition" + ], "evidenceFiles": [ "../../../final/evidence/raw/outbox-chain-behind-an-unsatisfiable-condition.txt", "../../../final/evidence/raw/tl-outbox-unsatisfiable-condition.txt" @@ -339,107 +397,14 @@ "assets": [ "observation-downgraded-by-the-composition" ], + "assetFiles": [ + "observation-downgraded-by-the-composition" + ], "evidenceFiles": [ "../../../final/evidence/raw/observation-downgraded-by-the-composition.txt", "../../../final/evidence/raw/tl-messaging-observation-noop.txt" ] }, - { - "title": "이름만 AutoConfiguration이던 세 클래스가 capability 리포트에 Stable로 올라 있었다", - "kind": "case", - "slug": "autoconfiguration-in-name-only", - "readiness": "READY", - "source": [ - "`final/document.md#a05` §14.4" - ], - "code": [ - "`.../app-bootstrap/.../autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java`의 javadoc" - ], - "evidence": [ - "없음 — javadoc의 사후 기록" - ], - "classification": "세 클래스가 `...AutoConfiguration`으로 이름 붙었고 plain factory였다 — `@AutoConfiguration`도, `@Bean`도, `.imports` 엔트리도 없었고 합성 루트는 그 패키지를 스캔에서 제외한다. 그래서 capability 리포트는 transaction retry·completion evidence·observability를 Stable로 나열했고 **돌고 있는 컨텍스트에는 그중 아무것도 없었다.** 개발자가 재시도되지 않는 재시도에 의존하는 코드를 배포할 수 있었다.", - "missing-verification": "없음 — 수정 후 형태를 코드로 확인했다", - "relations": [ - "`reference:a-bean-is-not-composition-evidence`", - "`case:a-retry-implementation-nobody-calls`", - "`decision:capability-grade-is-declared-not-inferred`" - ], - "publication": "초안", - "file": "assembly-ownership/case/case-autoconfiguration-in-name-only.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "autoconfiguration-in-name-only" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/autoconfiguration-in-name-only.txt" - ] - }, - { - "title": "`@ConditionalOnBean(DataSource.class)`가 클래스 파싱 시점에 평가되어 여덟 빈이 사라졌다", - "kind": "case", - "slug": "conditionalonbean-evaluated-at-parse-time", - "readiness": "READY", - "source": [ - "`final/document.md#a05` §14.4" - ], - "code": [ - "`.../autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java`", - "`.../PersistenceJpaRootAutoConfiguration.java`" - ], - "evidence": [ - "없음 — javadoc의 사후 기록" - ], - "classification": "이 클래스는 루트가 **import**하지 auto-configure하지 않으므로, 그 조건이 클래스 파싱 중 — datasource 빈 정의가 존재하기 전에 — 평가됐고 따라서 **모든 실제 배포에서 false**였다. 아래 여덟 빈이 조용히 사라졌고 아무것도 그중 어느 것에도 의존하지 않아 아무것도 보고하지 않았다. datasource validator가 caller에 배선되고 Compose 레인이 \"No qualifying bean\"이라고 답했을 때에야 드러났다. 같은 함정을 피하려고 루트의 검사가 validator를 주입받지 않고 직접 생성한다.", - "missing-verification": "현재 리비전에서 재발하지 않는지 `ConditionEvaluationReport`로 확인하지 않았다", - "relations": [ - "`concept:when-conditions-are-evaluated`", - "`open-question:conditional-evaluation-order-unverified`" - ], - "publication": "초안", - "file": "assembly-ownership/case/case-conditionalonbean-evaluated-at-parse-time.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "conditionalonbean-evaluated-at-parse-time" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/conditionalonbean-evaluated-at-parse-time.txt" - ] - }, - { - "title": "넓은 스캔을 좁히자 여덟 컴포넌트에 아무것도 도달하지 않았다", - "kind": "case", - "slug": "narrowing-the-scan-orphaned-eight-components", - "readiness": "READY", - "source": [ - "`final/document.md#a05` §14.2" - ], - "code": [ - "`.../persistence-jpa/.../config/JpaAdapterComponentsConfig.java`의 javadoc" - ], - "evidence": [ - "없음 — javadoc의 사후 기록" - ], - "classification": "합성 루트의 스캔이 persistence 트리를 정규식으로 제외했고 **그 제외는 옳다** — 그것이 optional capability를 optional하게 만든다. 빠진 것은 나머지 절반이다. 이 leaf의 여덟 클래스가 scanned component로 쓰여 있는데(`SpringTransactionPort`·`PersistenceExceptionTranslator`·`StandardSqlStateErrorMapping`·`DomainContextAuditContextPort`·idempotency store와 reaper·outbox store와 reaper) 넓은 스캔이 멈추자 **아무것도 도달하지 않았다.** 특히 `TransactionPort`는 구현이 전혀 없어서 트랜잭션을 여는 모든 유스케이스가 열 포트를 갖지 못했고, 단위 테스트는 각 클래스를 직접 생성하므로 볼 수 있는 것이 없었다.", - "missing-verification": "없음 — 수정된 `@ComponentScan` 대상 6개를 코드로 확인했다", - "relations": [ - "`concept:three-assembly-paths`", - "`reference:read-the-assembling-side-first`", - "`reference:off-must-be-structural`" - ], - "publication": "초안", - "file": "assembly-ownership/case/case-narrowing-the-scan-orphaned-eight-components.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "narrowing-the-scan-orphaned-eight-components" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/narrowing-the-scan-orphaned-eight-components.txt" - ] - }, { "title": "시작 검증기 13개 규칙이 유일한 조립 지점에서 호출되지 않는다", "kind": "case", @@ -470,12 +435,15 @@ "assets": [ "thirteen-startup-rules-never-run" ], + "assetFiles": [ + "thirteen-startup-rules-never-run" + ], "evidenceFiles": [ "../../../final/evidence/raw/thirteen-startup-rules-never-run.txt" ] }, { - "title": "subsystem 전체가 미배선인데 그것을 켜는 flag는 startup 검사를 수행한다", + "title": "하위 시스템 전체가 미배선인데 그것을 켜는 플래그는 시작 검사를 수행한다", "kind": "case", "slug": "a-flag-that-validates-an-unwired-subsystem", "readiness": "READY", @@ -503,6 +471,9 @@ "assets": [ "a-flag-that-validates-an-unwired-subsystem" ], + "assetFiles": [ + "a-flag-that-validates-an-unwired-subsystem" + ], "evidenceFiles": [ "../../../final/evidence/raw/a-flag-that-validates-an-unwired-subsystem.txt" ] @@ -540,10 +511,38 @@ "a-validator-that-demands-tls-and-an-assembly-that-omits-it", "a-validator-that-demands-tls-and-an-assembly-that-omits-it-run" ], + "assetFiles": [ + "a-validator-that-demands-tls-and-an-assembly-that-omits-it", + "a-validator-that-demands-tls-and-an-assembly-that-omits-it-run" + ], "evidenceFiles": [ "../../../final/evidence/raw/a-validator-that-demands-tls-and-an-assembly-that-omits-it.txt", "../../../final/evidence/raw/a-validator-that-demands-tls-and-an-assembly-that-omits-it-run.txt" ] + }, + { + "title": "가드와 journal 과 durability 검증기는 켜지고, 부를 서비스가 없었다", + "slug": "the-guard-is-on-and-the-service-is-not", + "readiness": "READY", + "source": [ + "final/document.md#5-4", + "final/document.md#a19 §8.1", + "final/document.md#a19 §8.2" + ], + "code": [ + "DestructiveOperationGuard", + "MessagingAdminService", + "AdminOperationJournal" + ], + "classification": "이 저장소에서 가장 잘 조립된 게이트가 동시에 형태 A 의 사례이기도 하다는 것을 부재 네 건으로 확정했고, 그중 하나만 문서화돼 있다는 데서 닫힌다", + "missing-verification": "실제로 admin 을 켜고 부팅해 호출 대상이 없다는 것을 확인하지는 않았다. 참조 0 과 조건 판정으로만 확정했다", + "relations": [ + "decision:destructive-admin-operations-are-not-autoconfigured", + "reference:a-bean-is-not-composition-evidence", + "case:scan-exclusion-without-an-owner" + ], + "kind": "case", + "publication": "미작성" } ], "concept": [ @@ -572,6 +571,7 @@ "`reference:read-the-assembling-side-first`", "`reference:a-bean-is-not-composition-evidence`" ], + "basis-version": "clean-architecture-backend-template @ 21234e38 · Spring Boot 자동설정(.imports) · 컴포넌트 스캔 · @Bean 손 배선 셋을 쓰는 app-bootstrap 과 sample-portfolio 두 합성 루트", "publication": "초안", "file": "assembly-ownership/concept/concept-three-assembly-paths.md", "status": "게시 전", @@ -580,45 +580,19 @@ "three-assembly-paths", "three-assembly-paths-diagram" ], + "assetFiles": [ + "three-assembly-paths", + "three-assembly-paths" + ], "evidenceFiles": [ "../../../final/evidence/raw/three-assembly-paths.txt", "../../../final/evidence/raw/tl-web-six-unowned-components.txt" ] - }, - { - "title": "조건부 빈의 평가 시점 — 파싱 시점과 등록 시점", - "kind": "concept", - "slug": "when-conditions-are-evaluated", - "readiness": "READY", - "source": [ - "`final/document.md#a05` §14.4" - ], - "code": [ - "`.../app-bootstrap/.../autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java`의 근거 javadoc", - "`.../PersistenceJpaRootAutoConfiguration.java`의 `jpaResolvedDataSourceCheck`" - ], - "classification": "`@ConditionalOnBean`은 그 클래스가 **언제 평가되는가**에 따라 답이 달라진다. `@AutoConfiguration`으로 등록되면 다른 자동설정 이후에 평가되지만, plain `@Configuration`이 `@Import`로 들어오면 **클래스가 파싱되는 동안 — 대상 빈 정의가 존재하기 전에** 평가된다. 이 저장소가 그 함정을 실제로 밟았고(여덟 빈이 조용히 사라짐), 회피 방법 두 가지를 남겼다 — 검증기를 주입받지 않고 직접 생성하기, 그리고 조건을 루트로 올리기.", - "missing-verification": "현재 리비전의 각 조건부 빈이 어느 시점에 평가되는지는 `ConditionEvaluationReport`로 확인하지 않았다", - "relations": [ - "`case:conditionalonbean-evaluated-at-parse-time`", - "`reference:conditionalonbean-must-be-satisfiable`", - "`open-question:conditional-evaluation-order-unverified`" - ], - "publication": "초안", - "file": "assembly-ownership/concept/concept-when-conditions-are-evaluated.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "when-conditions-are-evaluated" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/when-conditions-are-evaluated.txt" - ] } ], "reference": [ { - "title": "`@Bean`이 있다는 것은 조립 증거가 아니다", + "title": "@Bean이 있다는 것은 조립 증거가 아니다", "kind": "reference", "slug": "a-bean-is-not-composition-evidence", "readiness": "READY", @@ -644,10 +618,11 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] }, { - "title": "`@ConditionalOnBean`은 조건이 만족될 수 있는지까지 확인해야 한다", + "title": "@ConditionalOnBean은 조건이 만족될 수 있는지까지 확인해야 한다", "kind": "reference", "slug": "conditionalonbean-must-be-satisfiable", "readiness": "READY", @@ -672,6 +647,7 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] }, { @@ -700,6 +676,7 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] }, { @@ -728,6 +705,7 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] }, { @@ -757,33 +735,7 @@ "status": "게시 전", "studioId": "", "assets": [], - "evidenceFiles": [] - }, - { - "title": "프레임워크가 기여하는 자동설정까지 세지 않으면 스위치가 아니다", - "kind": "reference", - "slug": "count-the-frameworks-own-autoconfigurations", - "readiness": "READY", - "source": [ - "`final/document.md#a05` §14.1" - ], - "classification": "프로젝트 자신의 설정만 조건화하는 것으로는 부족하다 — starter가 프레임워크의 import metadata를 통해 자기 것을 기여하므로, 평범한 `@EnableAutoConfiguration` 애플리케이션은 프로젝트 조건이 무엇이라 하든 풀을 열고 마이그레이션을 돌린다. 기준은 \"이 능력이 꺼졌을 때 프레임워크가 여전히 무엇을 만드는가\"다.", - "scope": [ - "starter를 클래스패스에 두는 모든 optional capability. 이 저장소의 형태는 `AutoConfigurationImportFilter`가 10종을 막는 것이다." - ], - "exceptions": [ - "필터 목록의 오타는 **조용히 fail-open**한다(매치하지 않을 뿐). 그래서 테스트가 필터의 반환값이 아니라 **빈 부재**로 assert해야 한다." - ], - "relations": [ - "`case:narrowing-the-scan-orphaned-eight-components`", - "`reference:off-must-be-structural`", - "`decision:pool-need-is-a-capability-question`" - ], - "publication": "초안", - "file": "assembly-ownership/reference/reference-count-the-frameworks-own-autoconfigurations.md", - "status": "게시 전", - "studioId": "", - "assets": [], + "assetFiles": [], "evidenceFiles": [] } ], @@ -818,10 +770,11 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] }, { - "title": "`@ConditionalOnBean` 사슬의 실제 평가 순서를 확인하지 않았다", + "title": "@ConditionalOnBean 사슬의 실제 평가 순서를 확인하지 않았다", "kind": "question", "slug": "conditional-evaluation-order-unverified", "readiness": "OPEN", @@ -852,6 +805,7 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] } ], @@ -887,6 +841,7 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] }, { @@ -916,7 +871,29 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] + }, + { + "title": "파괴적 admin 작업은 자동설정하지 않는다", + "slug": "destructive-admin-operations-are-not-autoconfigured", + "readiness": "READY", + "source": [ + "final/document.md#10-3", + "final/document.md#5-4", + "final/document.md#a19 §8.1" + ], + "decision-status": "ADOPTED", + "decision-evidence": "`DestructiveOperationGuard(false)` 가 기본이고 `DestructiveMessagingAdmin` 이 의도적으로 bean 이 아니며, 그 부재를 javadoc 이 명시한다 (final/document.md#5-4)", + "grounds": "애플리케이션 런타임은 admin 자격증명을 들고 있지 않으므로, 그 자격증명이 필요한 작업을 가드가 거부한다. purge 나 delete 가 필요한 운영 도구는 자기 자격증명으로 스스로 등록한다. 감수한 비용은 사고 대응 중에 그 도구가 없다는 것을 발견할 수 있다는 것이고, 그래서 부재를 문서화하는 것이 짝이다", + "classification": "프로젝트가 이 방향을 실제로 골랐고 기본값·가드·javadoc 셋으로 표현했다", + "relations": [ + "case:the-guard-is-on-and-the-service-is-not", + "decision:one-root-owns-the-master-switch", + "reference:off-must-be-structural" + ], + "kind": "decision", + "publication": "미작성" } ] } @@ -924,7 +901,7 @@ "what-a-gate-does-not-prove": { "topic": "what-a-gate-does-not-prove", "title": "게이트가 증명하지 않는 것", - "readerQuestion": "", + "readerQuestion": "게이트가 초록불인데 아무것도 증명하지 않을 수 있다면, 무엇을 대신 확인해야 하는가?", "kinds": { "case": [ { @@ -956,6 +933,9 @@ "assets": [ "a-certifying-lane-that-compared-nothing" ], + "assetFiles": [ + "a-certifying-lane-that-compared-nothing" + ], "evidenceFiles": [ "../../../final/evidence/raw/a-certifying-lane-that-compared-nothing.txt" ] @@ -991,6 +971,9 @@ "assets": [ "doc-contract-test-boundary-predicted-the-drift" ], + "assetFiles": [ + "doc-contract-test-boundary-predicted-the-drift" + ], "evidenceFiles": [ "../../../final/evidence/raw/doc-contract-test-boundary-predicted-the-drift.txt" ] @@ -1028,76 +1011,14 @@ "assets": [ "a-release-gate-with-no-evidence-producer" ], + "assetFiles": [ + "a-release-gate-with-no-evidence-producer" + ], "evidenceFiles": [ "../../../final/evidence/raw/a-release-gate-with-no-evidence-producer.txt", "../../../final/evidence/raw/tl-grpc-release-gate-no-producer.txt" ] }, - { - "title": "패키지 카탈로그가 트리보다 아홉 개 적어서 사이클이 통과했다", - "kind": "case", - "slug": "a-catalog-nine-entries-short", - "readiness": "READY", - "source": [ - "`final/document.md#a05` §1" - ], - "code": [ - "`.../persistence-jpa/src/test/.../JpaModuleBoundaryTest.java`" - ], - "evidence": [ - "없음 — 테스트 javadoc의 사후 기록" - ], - "classification": "경계 테스트가 production root의 직계 자식 패키지 닫힌 카탈로그를 들고 실제 트리와 비교하는데, 카탈로그에 13개가 적혀 있고 트리에는 22개가 있었다. 그래서 아홉 패키지가 **아무 규칙의 지배도 받지 않았고** `transaction → postgresql` / `postgresql → transaction` 사이클이 통과했다. 수정은 \"카탈로그와 트리의 **정확한 동등성**\" 검사를 추가하는 것이었다.", - "missing-verification": "없음", - "relations": [ - "`reference:omission-that-passes-is-not-a-gate`", - "`concept:strict-test-lane`" - ], - "publication": "초안", - "file": "what-a-gate-does-not-prove/case/case-a-catalog-nine-entries-short.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a-catalog-nine-entries-short" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a-catalog-nine-entries-short.txt" - ] - }, - { - "title": "릴리스 레인이 매트릭스 세 버전 중 첫 번째만 돌리고 세 개를 커버로 기록했다", - "kind": "case", - "slug": "three-versions-declared-one-executed", - "readiness": "READY", - "source": [ - "`final/document.md#a05` §15.4" - ], - "code": [ - "`config/jpa/release-registry.json`", - "`.../JpaPlatformContractSupport.start()`", - "`.github/workflows/jpa-release.yml`" - ], - "evidence": [ - "없음 — 레지스트리 `_comment`와 javadoc의 사후 기록" - ], - "classification": "릴리스 레인이 `-Pjpa.matrix.versions=16,17,18`을 `selectedVersions().get(0)`을 쓰는 지원 클래스에 넘겼고 **통합 suite 전체가 PostgreSQL 16에 대해 돌았으며**, 지원 표는 3개 assertion짜리 smoke test의 힘으로 17과 18을 완전 커버로 기록했다. 수정은 `start()`가 다중 선택을 아예 거부하고, workflow가 major당 job으로 fan-out하며, promotion job이 세 major의 증거가 **같은 commit SHA**를 담기를 요구하는 것이다.", - "missing-verification": "현재 릴리스 워크플로를 실행하지 않았다", - "relations": [ - "`reference:agreement-between-documents-proves-nothing`", - "`concept:evidence-grades-and-provenance`", - "`decision:capability-grade-is-declared-not-inferred`" - ], - "publication": "초안", - "file": "what-a-gate-does-not-prove/case/case-three-versions-declared-one-executed.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "three-versions-declared-one-executed" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/three-versions-declared-one-executed.txt" - ] - }, { "title": "다중 타깃 검증을 확인한다는 테스트가 다른 가드에 걸려 통과했다", "kind": "case", @@ -1126,84 +1047,12 @@ "assets": [ "a-test-that-passed-on-the-wrong-guard" ], + "assetFiles": [ + "a-test-that-passed-on-the-wrong-guard" + ], "evidenceFiles": [ "../../../final/evidence/raw/a-test-that-passed-on-the-wrong-guard.txt" ] - }, - { - "title": "두 파일이 같은 검증기를 \"빌드를 실패시키는 것\"이라 적고, 어떤 빌드도 그것을 부르지 않는다", - "kind": "case", - "slug": "two-files-name-a-build-gate-that-no-build-runs", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-proto-contract` §17.4", - "`final/document.md#a20-grpc-codegen` §17.1" - ], - "code": [ - "`.../grpc-proto-contract/.../GrpcProtoContractValidator.java`", - "`.../grpc-proto-contract/src/main/resources/proto/buf.yaml:3-5`", - "`.../grpc-codegen/.../GrpcBufPolicy.java:8-10`" - ], - "evidence": [ - "없음 — `*.gradle`·`*.kts`·`*.yml` 과 자바 타입 이름 전수 grep 으로 판정했다" - ], - "classification": "두 파일이 같은 논증을 편다 — Buf CLI 가 이 툴체인에 없으므로 자바로 구현한 규칙 엔진이 그 자리를 대신하고, \"그것이 실제로 이 저장소의 빌드를 실패시킨다\". `buf.yaml` 주석과 `GrpcBufPolicy` javadoc 이 각각 그 문장을 갖는다. 그런데 `GrpcProtoContractValidator` 를 부르는 Gradle 태스크도, 검증 훅도, 다른 모듈의 호출도 없다. 실제로 아홉 규칙을 실행하는 것은 그 리프의 단위 테스트 하나이고, 그 테스트가 판정하는 대상은 **하드코딩된 두 파일**이다. 거꾸로 `GrpcBufPolicy` 가 계약이라고 든 네 태스크 이름(`bufFormatCheck`·`bufLint`·`bufBuild`·`bufBreaking`)도 어떤 빌드 파일에도 없고, 그 javadoc 은 \"a missing stage is a test failure rather than a stage nobody noticed was gone\" 라고 적는데 테스트는 그 목록을 리터럴 및 자기 자신과 비교한다. 두 쪽이 서로를 게이트라고 가리키고 어느 쪽도 실행되지 않는다.", - "missing-verification": "리플렉션이나 서비스 로더로 부르는 형태는 배제하지 못했다 — 이름 기반 grep 으로만 확인했다", - "relations": [ - "`reference:a-gate-declared-in-prose-is-not-in-the-build`", - "`reference:a-gate-nobody-runs-reports-the-last-run`", - "`case:a-release-gate-with-no-evidence-producer`", - "`case:a-build-gate-that-is-not-in-the-build`" - ], - "listedInTree": false, - "publication": "초안", - "file": "what-a-gate-does-not-prove/case/case-two-files-name-a-build-gate-that-no-build-runs.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "two-files-name-a-build-gate-that-no-build-runs" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/two-files-name-a-build-gate-that-no-build-runs.txt" - ] - }, - { - "title": "이름이 검사한다고 말하는 것을 본문이 검사하지 않는 테스트 다섯", - "kind": "case", - "slug": "test-names-that-assert-what-their-bodies-do-not", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-pulsar-experimental` §17.3", - "`final/document.md#a19-messaging-nats-experimental` §17.4", - "`final/document.md#a20-grpc-advanced-bootstrap` §17.4" - ], - "code": [ - "`.../PulsarSubscriptionGuardTest.theValidatorAcceptsAKeyedProfileOnKeyShared`", - "`.../PulsarAdapterContractTest.aTransportWithoutAConsumerFactoryRefusesToRegister…`", - "`.../NatsAdapterContractTest.theReportedElapsedTimeIsMeasuredRatherThanZero`", - "`.../GrpcAdvancedPromotionGateTest.theStableDefaultThresholdIsHigher`" - ], - "evidence": [ - "없음 — 테스트 본문과 이름·`as()` 메시지 대조로 판정했다" - ], - "classification": "네 형태가 같은 결과를 낳는다. (1) 이름이 `theValidatorAccepts…` 인데 본문에 검증기가 없다 — `PulsarProfile` 생성자만 부른다. 그 리프에서 검증기를 언급하는 유일한 테스트 이름이 이것이라, 이름만 읽으면 커버리지가 있다고 읽힌다. (2) 이름이 \"소비자 팩토리 없이 만든 전송이 등록을 거절한다\" 인데 본문은 `register(null)` 을 불러 첫 줄의 널 검사에 걸린다 — 겨냥한 `PULSAR_CONSUMER_NOT_CONFIGURED` 는 한 번도 실행되지 않는다. (3) `as()` 가 \"모든 결과가 `Duration.ZERO` 였다\" 는 회귀를 막는다고 적는데 단언이 `isGreaterThanOrEqualTo(Duration.ZERO)` 라 `Duration.ZERO` 도 통과한다 — 구현이 무엇을 하든 참이다. (4) 이름이 \"Stable default 가 되는 데 더 긴 담금이 필요하다\" 인데 실제로 평가하는 전이는 `ADVANCED_STABLE → DISABLED`(철회)다. 어느 것도 잘못된 동작을 통과시키지는 않는다 — 틀리는 것은 커버리지 지도이고, 그래서 그 아래의 진짜 공백이 오래 눈에 띄지 않았다.", - "missing-verification": "테스트를 실행하지 않았다 — 단언 의미론과 호출 경로로 판정했다", - "relations": [ - "`reference:omission-that-passes-is-not-a-gate`", - "`case:a-test-that-passed-on-the-wrong-guard`", - "`reference:a-gate-declared-in-prose-is-not-in-the-build`" - ], - "listedInTree": false, - "publication": "초안", - "file": "what-a-gate-does-not-prove/case/case-test-names-that-assert-what-their-bodies-do-not.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "test-names-that-assert-what-their-bodies-do-not" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/test-names-that-assert-what-their-bodies-do-not.txt" - ] } ], "concept": [ @@ -1229,6 +1078,7 @@ "`reference:a-gate-nobody-runs-reports-the-last-run`", "`decision:only-the-certification-lane-carries-no-docker-guard`" ], + "basis-version": "clean-architecture-backend-template @ 21234e38 · Gradle Test 태스크의 failOnNoDiscoveredTests = true · outputs.upToDateWhen { false } 규약", "publication": "초안", "file": "what-a-gate-does-not-prove/concept/concept-strict-test-lane.md", "status": "게시 전", @@ -1236,6 +1086,9 @@ "assets": [ "strict-test-lane" ], + "assetFiles": [ + "strict-test-lane" + ], "evidenceFiles": [ "../../../final/evidence/raw/strict-test-lane.txt" ] @@ -1260,6 +1113,7 @@ "`reference:agreement-between-documents-proves-nothing`", "`open-question:container-lanes-not-executed`" ], + "basis-version": "clean-architecture-backend-template @ 21234e38 · gradle/jpa-evidence.gradle 917줄 · active card 11개 · R1/R2 분리", "publication": "초안", "file": "what-a-gate-does-not-prove/concept/concept-evidence-grades-and-provenance.md", "status": "게시 전", @@ -1268,6 +1122,10 @@ "evidence-grades-and-provenance", "evidence-grades-and-provenance-diagram" ], + "assetFiles": [ + "evidence-grades-and-provenance", + "evidence-grades-and-provenance" + ], "evidenceFiles": [ "../../../final/evidence/raw/evidence-grades-and-provenance.txt" ] @@ -1303,6 +1161,7 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] }, { @@ -1331,6 +1190,7 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] }, { @@ -1361,6 +1221,7 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] }, { @@ -1389,6 +1250,7 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] }, { @@ -1416,6 +1278,7 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] }, { @@ -1448,6 +1311,7 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] } ], @@ -1499,6 +1363,7 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [ "../../../final/evidence/raw/tl-platform-suite-results.txt" ] @@ -1533,6 +1398,7 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] }, { @@ -1564,6 +1430,7 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] }, { @@ -1594,6 +1461,7 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] } ] @@ -1602,7 +1470,7 @@ "bounding-by-type": { "topic": "bounding-by-type", "title": "타입으로 카디널리티와 개인정보를 막기", - "readerQuestion": "", + "readerQuestion": "값이 담을 수 있는 것을 타입이 미리 줄이면 무엇을 막을 수 있는가?", "kinds": { "case": [ { @@ -1636,72 +1504,12 @@ "assets": [ "pii-through-an-exception-message" ], + "assetFiles": [ + "pii-through-an-exception-message" + ], "evidenceFiles": [ "../../../final/evidence/raw/pii-through-an-exception-message.txt" ] - }, - { - "title": "진단 리포트가 살아 있는 리소스를 담지 않도록 값 타입을 좁혔다", - "kind": "case", - "slug": "a-report-that-cannot-carry-a-datasource", - "readiness": "READY", - "source": [ - "`final/document.md#a05` §2.5, §12.1" - ], - "code": [ - "`.../api/capability/CapabilitySupport.java`", - "`.../security/DatabasePrivilegeReport.java`" - ], - "evidence": [ - "없음 — 값 타입 정의와 그 javadoc" - ], - "classification": "`CapabilitySupport`가 provider 객체(`DataSource`·`EntityManagerFactory`·`SessionFactory`)를 절대 담지 않고, `DatabasePrivilegeReport`가 JDBC URL·패스워드·호스트를 담지 않는다. 이유가 같다 — 이 값들은 리포트로 직렬화되고 actuator로 publish될 수 있어야 하는데, 살아 있는 리소스를 값 타입에 끌고 들어가면 리포트가 자격증명을 흘린다. \"publish 전에 마스킹해야 할 것은 애초에 들어가지 않는다\"가 설계 문장이다.", - "missing-verification": "없음", - "relations": [ - "`concept:cardinality-bounds-as-types`", - "`reference:telemetry-can-be-more-dangerous-than-its-subject`" - ], - "publication": "초안", - "file": "bounding-by-type/case/case-a-report-that-cannot-carry-a-datasource.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a-report-that-cannot-carry-a-datasource" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a-report-that-cannot-carry-a-datasource.txt" - ] - }, - { - "title": "커서 서명 검증이 길이·상수시간·순서를 전부 지켜야 했던 이유", - "kind": "case", - "slug": "cursor-verification-order", - "readiness": "READY", - "source": [ - "`final/document.md#a05` §2.4" - ], - "code": [ - "`.../api/query/SignedJsonCursorCodec.java`" - ], - "evidence": [ - "없음 — 구현과 그 javadoc" - ], - "classification": "다섯 방어가 각각 다른 공격을 막고 순서가 계약이다. `MAX_ENCODED_LENGTH(4096)` 검사가 첫 줄에 없으면 decode가 caller가 보낸 크기만큼 할당하고, MAC 길이 확인이 없으면 `MessageDigest.isEqual`의 상수 시간 보장이 깨지며, 서명 검증 전에 파싱하면 서명 없는 토큰이 애플리케이션 JSON 파서에 도달한다. 그리고 MAC이 **버전과 payload를 함께** 덮어 prefix 재작성으로 옛 포맷으로 다운그레이드하는 것을 막는다.", - "missing-verification": "없음", - "relations": [ - "`concept:signed-cursor-structure`", - "`decision:cursors-are-signed-for-integrity`" - ], - "publication": "초안", - "file": "bounding-by-type/case/case-cursor-verification-order.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "cursor-verification-order" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/cursor-verification-order.txt" - ] } ], "concept": [ @@ -1727,6 +1535,7 @@ "`decision:cursors-are-signed-for-integrity`", "`reference:names-are-registry-keys-not-values`" ], + "basis-version": "clean-architecture-backend-template @ 21234e38 · persistence-jpa 의 keyset 커서 계약 · messaging 의 같은 계열 커서", "publication": "초안", "file": "bounding-by-type/concept/concept-signed-cursor-structure.md", "status": "게시 전", @@ -1735,6 +1544,10 @@ "signed-cursor-structure", "signed-cursor-structure-diagram" ], + "assetFiles": [ + "signed-cursor-structure", + "signed-cursor-structure" + ], "evidenceFiles": [ "../../../final/evidence/raw/signed-cursor-structure.txt" ] @@ -1762,6 +1575,7 @@ "`reference:reject-rather-than-sanitize`", "`decision:tenant-id-is-never-a-metric-tag`" ], + "basis-version": "clean-architecture-backend-template @ 21234e38 · PersistenceOperationName 등 이름 타입 여덟 · JpaMetricTags 다섯 태그 · shared-contract 의 ForbiddenMetricTags", "publication": "초안", "file": "bounding-by-type/concept/concept-cardinality-bounds-as-types.md", "status": "게시 전", @@ -1770,6 +1584,10 @@ "cardinality-bounds-as-types", "cardinality-bounds-as-types-diagram" ], + "assetFiles": [ + "cardinality-bounds-as-types", + "cardinality-bounds-as-types" + ], "evidenceFiles": [ "../../../final/evidence/raw/cardinality-bounds-as-types.txt" ] @@ -1802,6 +1620,7 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] }, { @@ -1834,6 +1653,7 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] }, { @@ -1861,33 +1681,7 @@ "status": "게시 전", "studioId": "", "assets": [], - "evidenceFiles": [] - }, - { - "title": "sanitize가 아니라 reject가 기본이다", - "kind": "reference", - "slug": "reject-rather-than-sanitize", - "readiness": "READY", - "source": [ - "`final/document.md#a05` §12.2" - ], - "classification": "경계를 넘는 값을 조용히 자르거나 치환하면 caller가 계속 그 값을 넘기고 절대 눈치채지 못한다. 거부하면 도입된 자리에서 실패한다. 기준은 \"이 값의 생산자를 우리가 고칠 수 있는가\"이고, 답이 예면 reject다.", - "scope": [ - "metric tag·이름 값 타입·설정 키·헤더 이름. `LowCardinality.REGISTERED`가 정규식을 통과하지 못하면 던지는 것이 그 형태다." - ], - "exceptions": [ - "생산자를 고칠 수 없는 경우(외부 드라이버의 예외 메시지, 서드파티 응답 본문)는 redaction이 맞고, 그때는 그것이 보증이 아님을 문서에 적어야 한다." - ], - "relations": [ - "`case:pii-through-an-exception-message`", - "`reference:telemetry-can-be-more-dangerous-than-its-subject`", - "`concept:cardinality-bounds-as-types`" - ], - "publication": "초안", - "file": "bounding-by-type/reference/reference-reject-rather-than-sanitize.md", - "status": "게시 전", - "studioId": "", - "assets": [], + "assetFiles": [], "evidenceFiles": [] } ], @@ -1923,6 +1717,7 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] }, { @@ -1952,7 +1747,48 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] + }, + { + "title": "keyset 페이지에 offset 필드를 두지 않는다", + "slug": "a-keyset-page-has-no-offset-field", + "readiness": "READY", + "source": [ + "final/document.md#10-4", + "final/document.md#a05 §2.4" + ], + "decision-status": "ADOPTED", + "decision-evidence": "페이지 계약에 offset·total count·page number 칸이 없고, 그 부재를 javadoc 이 이유와 함께 적는다 (final/document.md#10-4)", + "grounds": "필드의 부재가 나중에 하나 추가되는 것을 막는다. total count 는 같은 predicate 에 두 번째 집계 쿼리를 요구하고, 움직이는 데이터셋에서 그 숫자는 클라이언트에 닿기 전에 이미 낡았다. 감수한 비용은 「몇 쪽 중 몇 쪽」을 그릴 수 없다는 것이다", + "classification": "프로젝트가 칸을 두지 않는 쪽을 골랐고 그 부재 자체가 가드다", + "relations": [ + "decision:cursors-are-signed-for-integrity", + "concept:signed-cursor-structure", + "reference:register-paths-bind-values" + ], + "kind": "decision", + "publication": "미작성" + }, + { + "title": "JSONB 문서 안에 타입 메타데이터를 넣지 않는다", + "slug": "no-type-metadata-inside-a-jsonb-document", + "readiness": "READY", + "source": [ + "final/document.md#10-4", + "final/document.md#a05 §7.5" + ], + "decision-status": "ADOPTED", + "decision-evidence": "JSONB 컬럼에 타입 메타데이터를 쓰지 않기로 하고 그 이유를 javadoc 이 적는다 (final/document.md#10-4)", + "grounds": "문서 안의 타입 메타데이터는 JSONB 컬럼을 역직렬화 가젯으로 만든다. 감수한 비용은 다형 문서를 저장할 때 타입을 컬럼으로 따로 들어야 한다는 것이다", + "classification": "프로젝트가 이 방향을 골랐고 같은 판단이 mongo 쪽 `_class` 정책과 대비된다", + "relations": [ + "decision:a-keyset-page-has-no-offset-field", + "reference:register-paths-bind-values", + "case:mongo-default-throws-on-first-write" + ], + "kind": "decision", + "publication": "미작성" } ] } @@ -1960,11 +1796,11 @@ "duplicate-mechanisms": { "topic": "duplicate-mechanisms", "title": "중복 장치 — 조립된 쪽이 약한 쪽일 때", - "readerQuestion": "", + "readerQuestion": "같은 일을 하는 장치가 둘일 때 실제 요청이 지나는 것은 어느 쪽인가?", "kinds": { "case": [ { - "title": "클라이언트가 준 엔드포인트가 SSRF 가드가 아니라 약한 private 사본을 지났다", + "title": "강한 가드가 웹푸시를 지목하는데 값 타입은 약한 검사를 다시 썼다", "kind": "case", "slug": "a-weaker-private-copy-on-the-wired-path", "readiness": "READY", @@ -1992,6 +1828,10 @@ "a-weaker-private-copy-on-the-wired-path", "a-weaker-private-copy-on-the-wired-path-probe" ], + "assetFiles": [ + "a-weaker-private-copy-on-the-wired-path", + "a-weaker-private-copy-on-the-wired-path-probe" + ], "evidenceFiles": [ "../../../final/evidence/raw/a-weaker-private-copy-on-the-wired-path.txt", "../../../final/evidence/raw/a-weaker-private-copy-on-the-wired-path-probe.txt" @@ -2025,6 +1865,9 @@ "assets": [ "a-policy-reversed-by-a-later-filter" ], + "assetFiles": [ + "a-policy-reversed-by-a-later-filter" + ], "evidenceFiles": [ "../../../final/evidence/raw/a-policy-reversed-by-a-later-filter.txt" ] @@ -2064,6 +1907,9 @@ "assets": [ "jpa-platform-capabilities-have-no-consumer" ], + "assetFiles": [ + "jpa-platform-capabilities-have-no-consumer" + ], "evidenceFiles": [ "../../../final/evidence/raw/jpa-platform-capabilities-have-no-consumer.txt" ] @@ -2097,12 +1943,15 @@ "assets": [ "trust-policy-lives-in-nginx-not-in-the-code" ], + "assetFiles": [ + "trust-policy-lives-in-nginx-not-in-the-code" + ], "evidenceFiles": [ "../../../final/evidence/raw/trust-policy-lives-in-nginx-not-in-the-code.txt" ] }, { - "title": "재시도 구현이 둘이고 정교한 쪽을 아무도 호출하지 않는다", + "title": "정식 경계라고 적은 클래스가 그것을 구현하지 않는다", "kind": "case", "slug": "a-retry-implementation-nobody-calls", "readiness": "READY", @@ -2134,44 +1983,36 @@ "assets": [ "a-retry-implementation-nobody-calls" ], + "assetFiles": [ + "a-retry-implementation-nobody-calls" + ], "evidenceFiles": [ "../../../final/evidence/raw/a-retry-implementation-nobody-calls.txt" ] }, { - "title": "같은 저장소가 \"결정을 그 결정이 판정한 대상에 묶는 것\"을 한 번은 맞게, 한 번은 틀리게 썼다", - "kind": "case", - "slug": "the-same-repository-bound-a-decision-once-and-not-the-other-time", + "title": "부하 아래에서 지키라고 만든 경계가 부하 아래에서만 샌다", + "slug": "a-boundary-that-leaks-only-under-load", "readiness": "READY", "source": [ - "`final/document.md#a20-grpc-codegen` §17.4", - "`final/document.md#a20-grpc-advanced-bootstrap`(확인된 설계)" + "final/document.md#5-5", + "final/document.md#8-2 항목 6", + "final/document.md#a20 §7" ], "code": [ - "`.../grpc-codegen/.../GrpcSchemaArtifactPublisher.java`(`evaluate`/`publish`)", - "`.../grpc-advanced-bootstrap/.../release/GrpcAdvancedSupportMatrix.java`(`apply`)" + "GrpcAdmissionController", + "inFlight", + "AtomicInteger" ], - "evidence": [ - "없음 — 두 메서드 본문 대조로 판정했다" - ], - "classification": "두 리프가 같은 문제를 푼다 — 판정과 기록이 두 호출로 나뉠 때 그 사이를 무엇이 묶는가. `GrpcSchemaArtifactPublisher.publish(candidate, decision)` 는 `decision.allowed()` 만 보고 기록한다. `PublishDecision` 은 `(boolean, List)` 뿐이라 자기가 무엇을 판정했는지 들고 있지 않으므로, A 를 평가한 결정으로 B 를 발행할 수 있고 그러면 소비자 컴파일 게이트와 릴리스 버전 불변성을 둘 다 우회한다. 이 클래스가 존재하는 이유인 두 규칙이 인자 짝 하나로 무력해진다. 반대편에서 `GrpcAdvancedSupportMatrix.apply(decision)` 는 결정의 `from` 이 현재 등급과 다르면 던지고, 그 이유를 \"두 승격이 경합했거나 하나가 재생된 경우\" 라고 적는다. 결정이 자기가 밟고 선 상태를 들고 있고 적용 시점에 대조하는 형태다. 두 테스트의 차이도 같다 — 전자의 테스트는 `publish(artifact, evaluate(artifact, …))` 로 한 줄에서 짝을 맞춰 규율을 지키지만 코드가 그것을 강제하지 않고, 후자는 어긋난 짝을 넣는 테스트가 따로 있다.", - "missing-verification": "어긋난 짝을 실제로 실행해 보지 않았다 — `publish` 본문에 대조 코드가 없다는 것으로 판정했다", + "classification": "조립되는 9개 bean 중 하나가 검사와 증가 사이를 열어 두었고, 같은 가족 안에 CAS 루프로 정확히 쓴 참조 구현이 함께 있다는 데서 원인이 지식의 부재가 아니라 적용의 불균일임이 닫힌다", + "missing-verification": "경계를 실제로 넘기는 부하를 걸어 재현하지 않았다. 코드 읽기와 같은 가족의 올바른 구현 대조로만 확정했다", "relations": [ - "`reference:check-which-duplicate-is-wired`", - "`case:the-same-rotation-defect-closed-once-and-reproduced`", - "`reference:a-validator-is-enforced-by-injection`" + "reference:atomic-type-is-not-atomicity", + "case:the-same-rotation-defect-closed-once-and-reproduced", + "reference:check-which-duplicate-is-wired" ], - "listedInTree": false, - "publication": "초안", - "file": "duplicate-mechanisms/case/case-the-same-repository-bound-a-decision-once-and-not-the-other-time.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "the-same-repository-bound-a-decision-once-and-not-the-other-time" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/the-same-repository-bound-a-decision-once-and-not-the-other-time.txt" - ] + "kind": "case", + "publication": "미작성" } ], "concept": [], @@ -2202,10 +2043,11 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] }, { - "title": "`Atomic*` 타입의 존재는 원자성의 증거가 아니다", + "title": "Atomic* 타입의 존재는 원자성의 증거가 아니다", "kind": "reference", "slug": "atomic-type-is-not-atomicity", "readiness": "READY", @@ -2229,74 +2071,18 @@ "status": "게시 전", "studioId": "", "assets": [], - "evidenceFiles": [] - }, - { - "title": "같은 개념의 두 어휘가 공존하면 하나를 죽은 것으로 표시한다", - "kind": "reference", - "slug": "two-vocabularies-for-one-concept", - "readiness": "READY", - "source": [ - "`final/document.md#a19` §5.4", - "`final/document.md#a06` §58, §69", - "`final/document.md#a05` §12.5" - ], - "classification": "같은 개념을 두 타입이 표현하고 하나만 조립돼 있으면, 남은 쪽은 다음 사람이 어느 것을 써야 할지 알 수 없게 만든다. 기준은 \"이 둘이 같은 질문에 답하는가\"이고, 그렇다면 조립된 쪽을 정본으로 표시하고 나머지를 제거 대상으로 명시한다.", - "scope": [ - "감사 메타데이터·자격 증명 회전·TTL 선언·recovery 어휘. 표시 방법은 support matrix의 상태 컬럼(\"Candidate, not composed\")과 ArchUnit 규칙(엔티티가 둘 다 쓰는 것을 금지)이다." - ], - "exceptions": [ - "두 어휘가 다른 계층에 속하고 각각 소비자가 있으면 중복이 아니다 — `api.error`의 안정 예외 계층과 `failure`의 웹 표면 매핑이 그 경우다." - ], - "relations": [ - "`reference:check-which-duplicate-is-wired`", - "`decision:one-audit-mechanism-per-entity`" - ], - "publication": "초안", - "file": "duplicate-mechanisms/reference/reference-two-vocabularies-for-one-concept.md", - "status": "게시 전", - "studioId": "", - "assets": [], + "assetFiles": [], "evidenceFiles": [] } ], "question": [], - "decision": [ - { - "title": "감사 메커니즘은 엔티티당 정확히 하나여야 한다", - "kind": "decision", - "slug": "one-audit-mechanism-per-entity", - "readiness": "READY", - "decision-status": "`ADOPTED`", - "source": [ - "`final/document.md#a05` §12.5" - ], - "decision-evidence": [ - "`.../testkit/arch/JpaArchitectureRules.java`의 `entitiesUseExactlyOneAuditMechanism`", - "`docs/jpa/support-matrix.md`의 두 메커니즘 상태 표기" - ], - "grounds": [ - "`reference:two-vocabularies-for-one-concept`", - "`reference:check-which-duplicate-is-wired`" - ], - "classification": "`audit/AuditableEntity`(canonical, `@MappedSuperclass`, `created_at/by`·`updated_at/by`, actor 256)와 `auditing/AuditMetadata`(candidate, `@Embeddable`, `modified_*`, actor 64)가 공존하고 ArchUnit이 한 엔티티가 둘 다 쓰는 것을 막는다. 근거: \"둘 다 고른 엔티티는 하나의 의미에 두 writer, 하나의 사실에 두 컬럼 계열, 그리고 어느 엔티티가 뭘 골랐는지 알아야 하는 마이그레이션을 얻는다.\" 어느 메커니즘도 bulk/native update에 도달하지 않으므로 `bulkUpdatesOfAuditedEntitiesStampAudit`가 별도로 그것을 강제한다.", - "relations": [ - "`reference:two-vocabularies-for-one-concept`" - ], - "publication": "초안", - "file": "duplicate-mechanisms/decision/decision-one-audit-mechanism-per-entity.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - } - ] + "decision": [] } }, "drift-direction": { "topic": "drift-direction", "title": "문서 드리프트의 방향", - "readerQuestion": "", + "readerQuestion": "문서가 코드와 어긋났을 때 어느 방향의 어긋남을 먼저 고쳐야 하는가?", "kinds": { "case": [ { @@ -2332,6 +2118,9 @@ "assets": [ "support-matrix-said-the-opposite-of-the-code" ], + "assetFiles": [ + "support-matrix-said-the-opposite-of-the-code" + ], "evidenceFiles": [ "../../../final/evidence/raw/support-matrix-said-the-opposite-of-the-code.txt", "../../../final/evidence/raw/tl-kafka-dedup-drift.txt" @@ -2368,6 +2157,9 @@ "assets": [ "the-readme-recipe-does-not-start" ], + "assetFiles": [ + "the-readme-recipe-does-not-start" + ], "evidenceFiles": [ "../../../final/evidence/raw/the-readme-recipe-does-not-start.txt" ] @@ -2405,6 +2197,9 @@ "assets": [ "mongo-default-throws-on-first-write" ], + "assetFiles": [ + "mongo-default-throws-on-first-write" + ], "evidenceFiles": [ "../../../final/evidence/raw/mongo-default-throws-on-first-write.txt" ] @@ -2439,12 +2234,15 @@ "assets": [ "documented-uuidv7-generates-v4" ], + "assetFiles": [ + "documented-uuidv7-generates-v4" + ], "evidenceFiles": [ "../../../final/evidence/raw/documented-uuidv7-generates-v4.txt" ] }, { - "title": "선택할 수 없는 브로커가 지원 매트릭스에 기능 목록과 함께 실려 있다", + "title": "실 브로커로 증명된 어댑터가 선택하면 기동이 실패하는 이름으로 등록돼 있다", "kind": "case", "slug": "an-unselectable-broker-listed-with-features", "readiness": "READY", @@ -2472,6 +2270,9 @@ "assets": [ "an-unselectable-broker-listed-with-features" ], + "assetFiles": [ + "an-unselectable-broker-listed-with-features" + ], "evidenceFiles": [ "../../../final/evidence/raw/an-unselectable-broker-listed-with-features.txt" ] @@ -2509,6 +2310,9 @@ "assets": [ "five-documents-say-nineteen-leaves" ], + "assetFiles": [ + "five-documents-say-nineteen-leaves" + ], "evidenceFiles": [ "../../../final/evidence/raw/five-documents-say-nineteen-leaves.txt" ] @@ -2542,33 +2346,7 @@ "status": "게시 전", "studioId": "", "assets": [], - "evidenceFiles": [] - }, - { - "title": "문서의 수치는 세지 말고 파생하거나 게이트로 붙든다", - "kind": "reference", - "slug": "numbers-in-docs-should-be-derived", - "readiness": "READY", - "source": [ - "`final/document.md#a19` §6.4", - "`final/document.md#a05` §17 P3" - ], - "classification": "산문에 적은 수치는 다음 변경에서 드리프트한다. 기준은 \"이 수치의 정본이 어디인가\"이고, 정본이 있으면 문서는 세지 말고 가리켜야 한다. messaging 가족 문서가 자기 오류를 고치며 남긴 문장이 그 규칙이다 — \"정확한 목록은 registry가 소유하므로 여기서 세지 않는다 — 세는 순간 다시 drift한다.\"", - "scope": [ - "leaf 수·버전 목록·커버리지 수치·capability 개수. 대안은 (1) 세지 않고 SSOT를 가리키기, (2) 세야 한다면 그 수치를 검사하는 게이트를 두고 그 게이트의 walk 범위를 문서까지 넓히기." - ], - "exceptions": [ - "스냅샷임을 명시한 문서는 갱신하지 않는 것이 오히려 정확하다 — `00-project-overview.md`가 초기 sizing을 그대로 두고 헤더에 그 사실을 적는 형태다." - ], - "relations": [ - "`case:five-documents-say-nineteen-leaves`", - "`reference:omission-that-passes-is-not-a-gate`" - ], - "publication": "초안", - "file": "drift-direction/reference/reference-numbers-in-docs-should-be-derived.md", - "status": "게시 전", - "studioId": "", - "assets": [], + "assetFiles": [], "evidenceFiles": [] } ], @@ -2601,17 +2379,11 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] } ], "decision": [ - { - "title": "문서 계약 테스트의 단언 범위를 capability 표까지 넓힐 것인가", - "kind": "decision", - "slug": "", - "readiness": "", - "publication": "미작성" - }, { "title": "지원 등급은 추론이 아니라 선언이고 증거 없이는 올라가지 않는다", "kind": "decision", @@ -2644,6 +2416,7 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] } ] @@ -2652,7 +2425,7 @@ "learning-transfer-between-families": { "topic": "learning-transfer-between-families", "title": "플랫폼 가족 사이의 학습 전이", - "readerQuestion": "", + "readerQuestion": "한 플랫폼에서 배운 것이 다음 플랫폼으로 옮겨질 때 무엇이 따라가고 무엇이 남는가?", "kinds": { "case": [ { @@ -2691,6 +2464,9 @@ "assets": [ "the-second-platform-carried-the-design-not-the-wiring" ], + "assetFiles": [ + "the-second-platform-carried-the-design-not-the-wiring" + ], "evidenceFiles": [ "../../../final/evidence/raw/the-second-platform-carried-the-design-not-the-wiring.txt", "../../../final/evidence/raw/tl-platform-suite-results.txt", @@ -2730,6 +2506,9 @@ "assets": [ "the-same-rotation-defect-closed-once-and-reproduced" ], + "assetFiles": [ + "the-same-rotation-defect-closed-once-and-reproduced" + ], "evidenceFiles": [ "../../../final/evidence/raw/the-same-rotation-defect-closed-once-and-reproduced.txt", "../../../final/evidence/raw/tl-rotation-defect-reproduced.txt" @@ -2763,6 +2542,7 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] } ], @@ -2799,6 +2579,7 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] } ] @@ -2807,11 +2588,11 @@ "owner-safe-state-machines": { "topic": "owner-safe-state-machines", "title": "owner-safe 상태 기계 — 소유권을 SQL에 적기", - "readerQuestion": "", + "readerQuestion": "동시에 도는 두 일꾼이 같은 행과 같은 진행 위치를 두고 다투지 않게 하려면 무엇을 어디에 적어야 하는가?", "kinds": { "case": [ { - "title": "lease가 만료 시각만 기록하고 소유자를 기록하지 않아 terminal state가 되돌려졌다", + "title": "리스가 만료 시각만 기록하고 소유자를 기록하지 않아 최종 상태를 되돌릴 수 있었다", "kind": "case", "slug": "a-lease-without-an-owner", "readiness": "READY", @@ -2841,43 +2622,15 @@ "assets": [ "a-lease-without-an-owner" ], + "assetFiles": [ + "a-lease-without-an-owner" + ], "evidenceFiles": [ "../../../final/evidence/raw/a-lease-without-an-owner.txt" ] }, { - "title": "transition digest가 \"누가·언제\"만 덮고 \"무엇\"을 덮지 않아 다른 전이를 같다고 보고했다", - "kind": "case", - "slug": "a-digest-that-covered-who-but-not-what", - "readiness": "READY", - "source": [ - "`final/document.md#a05` §10.1" - ], - "code": [ - "`.../postgresql/idempotency/IdempotencyDigestPolicy.java`" - ], - "evidence": [ - "없음 — javadoc의 사후 기록" - ], - "classification": "digest가 `transition|operationId|ownerToken|attempt|stateRevision`이었고 **무엇을 했는지**를 전혀 덮지 않았다. retryable로 기록된 `FAIL`과 abandoned로 기록된 `FAIL`이 같은 digest를 냈고, 서로 다른 응답이나 서로 다른 retention을 가진 두 completion도 그랬다. digest를 비교하는 replay는 \"차이 전체가 중요한 부분인 두 전이\"를 같다고 결론지었다. 수정은 `semanticArguments`(disposition·retention·response digest·codec identity)를 포함하고 **길이 프레이밍**으로 구성하는 것 — 모든 구성요소가 가변 폭 텍스트이고 최소 하나(owner token)는 플랫폼이 제약할 것이 아니므로 delimiter join은 서로 다른 목록을 같은 문자열로 렌더링할 수 있다. `VERSION`을 붙여 구성이 바뀌면 저장된 digest가 가로질러 비교되지 않게 한다.", - "missing-verification": "없음", - "relations": [ - "`reference:digest-must-be-length-framed-and-versioned`", - "`concept:cas-tuple-and-update-count`" - ], - "publication": "초안", - "file": "owner-safe-state-machines/case/case-a-digest-that-covered-who-but-not-what.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a-digest-that-covered-who-but-not-what" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a-digest-that-covered-who-but-not-what.txt" - ] - }, - { - "title": "활성 트랜잭션 검사가 data source를 묻지 않아 다른 커넥션에서 커밋됐다", + "title": "활성 트랜잭션 검사가 data source를 묻지 않아 남의 트랜잭션이 통과했다", "kind": "case", "slug": "an-active-transaction-check-that-asked-the-wrong-question", "readiness": "READY", @@ -2904,45 +2657,15 @@ "assets": [ "an-active-transaction-check-that-asked-the-wrong-question" ], + "assetFiles": [ + "an-active-transaction-check-that-asked-the-wrong-question" + ], "evidenceFiles": [ "../../../final/evidence/raw/an-active-transaction-check-that-asked-the-wrong-question.txt" ] }, { - "title": "만료된 CLAIMED는 takeover하고 만료된 EXECUTING은 조정을 요구하도록 갈랐다", - "kind": "case", - "slug": "expired-claim-versus-expired-execution", - "readiness": "READY", - "source": [ - "`final/document.md#a05` §10.1, §10.4" - ], - "code": [ - "`.../postgresql/idempotency/PostgreSqlOwnerSafeIdempotencyStore.java`의 claim 결정 트리", - "`.../inbox/PostgreSqlSameStoreInboxAdapter.java`" - ], - "evidence": [ - "없음 — 결정 트리와 그 javadoc" - ], - "classification": "만료된 lease를 일률적으로 takeover하면 이미 실행이 시작된 작업을 blind retry하게 된다. 이 저장소는 상태로 나눈다 — 만료된 `CLAIMED`는 `resetClaim`으로 takeover하고, 만료된 `EXECUTING`은 `abandonExpiredExecution`으로 `ABANDONED`에 넣고 `RecoveryRequired`를 반환한다. inbox도 같은 축을 쓰되 `RECEIVED`(takeover 가능)와 `PROCESSING`(→ DEAD, recovery-required)로 나눈다. 즉 \"claim만 했다\"와 \"실행에 들어갔다\"가 만료 시 다른 결론을 낳는다.", - "missing-verification": "컨테이너 레인 미실행", - "relations": [ - "`reference:expired-claim-and-expired-execution-differ`", - "`concept:fenced-lease`", - "`open-question:v2-state-machine-lanes-not-executed`" - ], - "publication": "초안", - "file": "owner-safe-state-machines/case/case-expired-claim-versus-expired-execution.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "expired-claim-versus-expired-execution" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/expired-claim-versus-expired-execution.txt" - ] - }, - { - "title": "native claim이 `@Version`을 올리지 않아 충돌을 보고하지 않는 낙관적 잠금이 됐다", + "title": "native claim이 @Version을 올리지 않아 충돌을 보고하지 않는 낙관적 잠금이 됐다", "kind": "case", "slug": "native-claim-did-not-bump-the-version", "readiness": "READY", @@ -2970,9 +2693,36 @@ "assets": [ "native-claim-did-not-bump-the-version" ], + "assetFiles": [ + "native-claim-did-not-bump-the-version" + ], "evidenceFiles": [ "../../../final/evidence/raw/native-claim-did-not-bump-the-version.txt" ] + }, + { + "title": "「본 적 있는 위치」를 「투영이 끝난 위치」로 쓴 mark 가 재전달된 이벤트를 삼켰다", + "slug": "a-mark-that-meant-seen-not-projected", + "readiness": "READY", + "source": [ + "final/document.md#4-2", + "final/document.md#8-1 항목 9", + "final/document.md#a06 §67" + ], + "code": [ + "MongoChangeStreamPipeline", + "highWaterMark", + "NoOpCheckpoints" + ], + "classification": "failover 한 번으로 변경이 영구히 사라지는 경로를 probe 로 재현했고, mark 가 전진하는 시점이 원인임을 확정했다. 세 테스트가 각각 절반씩만 보아 「본 적 있지만 완료되지 않은 위치」라는 제3의 상태가 어디에도 없었다는 것까지 닫힌다", + "missing-verification": "probe C 는 worker 하나에 평범한 failover 한 번이다. worker 가 여럿이거나 resume 이 반복될 때의 손실 폭은 재지 않았다", + "relations": [ + "concept:fenced-lease", + "case:a-lease-without-an-owner", + "reference:read-the-clock-after-the-lock" + ], + "kind": "case", + "publication": "미작성" } ], "concept": [ @@ -2997,6 +2747,7 @@ "`reference:cas-tuple-in-the-where-clause`", "`reference:expired-claim-and-expired-execution-differ`" ], + "basis-version": "clean-architecture-backend-template @ 21234e38 · owner-safe 상태 기계 넷(idempotency · outbox-storage · outbox-polling · inbox) · V2__messaging_outbox_lease_fencing.sql", "publication": "초안", "file": "owner-safe-state-machines/concept/concept-fenced-lease.md", "status": "게시 전", @@ -3004,6 +2755,9 @@ "assets": [ "fenced-lease" ], + "assetFiles": [ + "fenced-lease" + ], "evidenceFiles": [ "../../../final/evidence/raw/fenced-lease.txt" ] @@ -3028,6 +2782,7 @@ "`case:native-claim-did-not-bump-the-version`", "`open-question:v2-state-machine-lanes-not-executed`" ], + "basis-version": "clean-architecture-backend-template @ 21234e38 · owner-safe 상태 기계 넷이 공유하는 여섯 단계 패턴 · PostgreSQL 16/17/18", "publication": "초안", "file": "owner-safe-state-machines/concept/concept-cas-tuple-and-update-count.md", "status": "게시 전", @@ -3036,6 +2791,10 @@ "cas-tuple-and-update-count", "cas-tuple-and-update-count-diagram" ], + "assetFiles": [ + "cas-tuple-and-update-count", + "cas-tuple-and-update-count" + ], "evidenceFiles": [ "../../../final/evidence/raw/cas-tuple-and-update-count.txt" ] @@ -3061,6 +2820,7 @@ "`concept:independent-flyway-streams`", "`case:registry-column-too-short-for-its-own-path`" ], + "basis-version": "clean-architecture-backend-template @ 21234e38 · capability_schema_registry · INSTALLED_INACTIVE 와 ACTIVE 두 상태 · notification 과 fileserver 두 capability 스트림", "publication": "초안", "file": "owner-safe-state-machines/concept/concept-capability-schema-registry.md", "status": "게시 전", @@ -3069,6 +2829,10 @@ "capability-schema-registry", "capability-schema-registry-diagram" ], + "assetFiles": [ + "capability-schema-registry", + "capability-schema-registry" + ], "evidenceFiles": [ "../../../final/evidence/raw/capability-schema-registry.txt" ] @@ -3101,6 +2865,7 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] }, { @@ -3128,59 +2893,7 @@ "status": "게시 전", "studioId": "", "assets": [], - "evidenceFiles": [] - }, - { - "title": "만료된 claim과 만료된 실행은 다르게 다뤄야 한다", - "kind": "reference", - "slug": "expired-claim-and-expired-execution-differ", - "readiness": "READY", - "source": [ - "`final/document.md#a05` §10.1, §10.4" - ], - "classification": "lease 만료는 \"누가 들고 있었는가\"만 말하고 \"무엇까지 했는가\"를 말하지 않는다. claim만 한 상태의 만료는 안전하게 takeover할 수 있지만, 실행에 들어간 상태의 만료는 외부 부수효과가 이미 발생했을 수 있으므로 blind retry가 아니라 조정으로 보내야 한다.", - "scope": [ - "실행 전 예약과 실행 자체를 구별하는 모든 작업 큐·멱등성 저장소. 구현은 상태를 둘로 나누는 것이다(`CLAIMED`/`EXECUTING`, `RECEIVED`/`PROCESSING`)." - ], - "exceptions": [ - "외부 부수효과가 없는 순수 계산 작업은 구별이 필요 없다. 다만 \"부수효과 없음\"이 유지되는지는 시간이 지나며 바뀌므로 그 전제를 적어야 한다." - ], - "relations": [ - "`case:expired-claim-versus-expired-execution`", - "`concept:fenced-lease`", - "`reference:unknown-is-a-third-result`" - ], - "publication": "초안", - "file": "owner-safe-state-machines/reference/reference-expired-claim-and-expired-execution-differ.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "digest는 길이 프레이밍하고 버전을 붙인다", - "kind": "reference", - "slug": "digest-must-be-length-framed-and-versioned", - "readiness": "READY", - "source": [ - "`final/document.md#a05` §10.1" - ], - "classification": "가변 폭 문자열을 delimiter로 이어 digest를 만들면 서로 다른 구성요소 목록이 같은 문자열로 렌더링될 수 있다. 길이 프레이밍(`len:value`)이 그것을 막고, 버전 번호가 구성 변경 전후의 digest를 가로질러 비교하지 못하게 한다. 그리고 digest는 \"누가·언제\"뿐 아니라 **\"무엇을\"**까지 덮어야 한다.", - "scope": [ - "replay 판정·중복 탐지·전이 동일성 비교에 쓰는 모든 digest. 구성요소 중 하나라도 플랫폼이 제약하지 않는 값(외부 토큰)이면 프레이밍이 필수다." - ], - "exceptions": [ - "모든 구성요소가 고정 길이이거나 플랫폼이 문법을 강제하는 값이면 delimiter로 충분하다. 다만 그 강제가 어디 있는지 적어야 한다." - ], - "relations": [ - "`case:a-digest-that-covered-who-but-not-what`", - "`reference:names-are-registry-keys-not-values`" - ], - "publication": "초안", - "file": "owner-safe-state-machines/reference/reference-digest-must-be-length-framed-and-versioned.md", - "status": "게시 전", - "studioId": "", - "assets": [], + "assetFiles": [], "evidenceFiles": [] } ], @@ -3217,6 +2930,7 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] } ], @@ -3250,34 +2964,7 @@ "status": "게시 전", "studioId": "", "assets": [], - "evidenceFiles": [] - }, - { - "title": "상태 기계 구현은 Spring stereotype을 갖지 않는다", - "kind": "decision", - "slug": "state-machines-carry-no-stereotype", - "readiness": "READY", - "decision-status": "`ADOPTED`", - "source": [ - "`final/document.md#a05` §10" - ], - "decision-evidence": [ - "네 V2 구현의 클래스 선언과 그 javadoc(\"두 composition root가 `dev.caskeleton.adapter`를 component-scan하므로 `@Repository`를 붙이면 선택하지 않은 배포에서도 빈이 된다\")" - ], - "grounds": [ - "`reference:off-must-be-structural`", - "`reference:a-bean-is-not-composition-evidence`" - ], - "classification": "스테레오타입을 붙이면 두 합성 루트의 스캔이 그것을 잡아, capability를 선택하지 않은 배포에서도 빈이 된다. 그래서 네 구현이 전부 plain class이고 협력자를 주입이 아니라 생성자에서 조립한다 — \"이건 이 store의 부품이지 애플리케이션이 조립하거나 교체하는 서비스가 아니고, 주입하면 public bean 표면이 1개에서 6개로 넓어진다.\"", - "relations": [ - "`reference:off-must-be-structural`", - "`decision:one-root-owns-the-master-switch`" - ], - "publication": "초안", - "file": "owner-safe-state-machines/decision/decision-state-machines-carry-no-stereotype.md", - "status": "게시 전", - "studioId": "", - "assets": [], + "assetFiles": [], "evidenceFiles": [] } ] @@ -3286,11 +2973,11 @@ "transaction-deadline-and-pool": { "topic": "transaction-deadline-and-pool", "title": "트랜잭션 데드라인과 커넥션 예산", - "readerQuestion": "", + "readerQuestion": "호출자에게 남은 시간이 DB 의 타임아웃까지 어떻게 내려가고, 그 사이에 커넥션은 몇 개가 필요한가?", "kinds": { "case": [ { - "title": "`connection-timeout: 5s`가 모든 prod 배포를 시작 실패시켰고 local만 통과했다", + "title": "connection-timeout: 5s가 모든 prod 배포를 시작 실패시켰고 local만 통과했다", "kind": "case", "slug": "a-five-second-string-that-broke-every-prod-deploy", "readiness": "READY", @@ -3320,46 +3007,17 @@ "a-five-second-string-that-broke-every-prod-deploy-bind", "a-five-second-string-that-broke-every-prod-deploy" ], + "assetFiles": [ + "a-five-second-string-that-broke-every-prod-deploy-bind", + "a-five-second-string-that-broke-every-prod-deploy" + ], "evidenceFiles": [ "../../../final/evidence/raw/a-five-second-string-that-broke-every-prod-deploy-bind.txt", "../../../final/evidence/raw/a-five-second-string-that-broke-every-prod-deploy.txt" ] }, { - "title": "validator가 요청을 서비스하지 않는 datasource를 검증하고 있었다", - "kind": "case", - "slug": "a-validator-checking-the-wrong-datasource", - "readiness": "READY", - "source": [ - "`final/document.md#a05` §14.5" - ], - "code": [ - "`.../app-bootstrap/.../persistencejpa/JpaDataSourceProfileValidator.java`의 javadoc" - ], - "evidence": [ - "없음 — javadoc의 사후 기록" - ], - "classification": "validator가 `app.jpa-platform.datasource.*`에 바인딩된 settings를 읽었는데 **요청을 서비스하는 풀은 `spring.datasource.hikari.*`에서** 만들어진다 — 하나의 풀에 두 개의 기술, 그리고 validator는 사용되지 않는 기술에 대해 통과할 수 있다. 더 나쁜 건 그 평행 네임스페이스가 어떤 shipped YAML에도 env-key 레지스트리의 어떤 행에도 없어서 두 필드가 항상 null이었고 `requirePoolBounds`가 모든 배포에서 던졌을 것이라는 점이다 — 아무것도 그것을 호출하지 않아서 아무것도 실패하지 않았다. **서로를 상쇄한 두 결함이고, 애플리케이션이 시작한 이유는 두 번째가 첫 번째를 숨겼기 때문이다.**", - "missing-verification": "없음", - "relations": [ - "`reference:a-bean-is-not-composition-evidence`", - "`case:a-five-second-string-that-broke-every-prod-deploy`" - ], - "publication": "초안", - "file": "transaction-deadline-and-pool/case/case-a-validator-checking-the-wrong-datasource.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a-validator-checking-the-wrong-datasource", - "a-validator-checking-the-wrong-datasource-run" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a-validator-checking-the-wrong-datasource.txt", - "../../../final/evidence/raw/a-validator-checking-the-wrong-datasource-run.txt" - ] - }, - { - "title": "`REQUIRES_NEW`가 바깥 커넥션을 핀한 채 새 커넥션을 딴다", + "title": "REQUIRES_NEW가 바깥 커넥션을 핀한 채 새 커넥션을 딴다", "kind": "case", "slug": "requires-new-pins-the-outer-connection", "readiness": "READY", @@ -3387,6 +3045,9 @@ "assets": [ "requires-new-pins-the-outer-connection" ], + "assetFiles": [ + "requires-new-pins-the-outer-connection" + ], "evidenceFiles": [ "../../../final/evidence/raw/requires-new-pins-the-outer-connection.txt" ] @@ -3414,6 +3075,7 @@ "`reference:session-scoped-settings-outlive-the-transaction`", "`case:a-five-second-string-that-broke-every-prod-deploy`" ], + "basis-version": "clean-architecture-backend-template @ 21234e38 · SpringTransactionPort · PostgreSQL 의 statement_timeout · lock_timeout · idle_in_transaction_session_timeout 을 set_config 로 transaction-local 지정", "publication": "초안", "file": "transaction-deadline-and-pool/concept/concept-deadline-propagation.md", "status": "게시 전", @@ -3422,12 +3084,16 @@ "deadline-propagation", "deadline-propagation-diagram" ], + "assetFiles": [ + "deadline-propagation", + "deadline-propagation" + ], "evidenceFiles": [ "../../../final/evidence/raw/deadline-propagation.txt" ] }, { - "title": "`REQUIRES_NEW`의 커넥션 비용과 풀 사이징 제약", + "title": "REQUIRES_NEW의 커넥션 비용과 풀 사이징 제약", "kind": "concept", "slug": "requires-new-connection-cost", "readiness": "READY", @@ -3446,6 +3112,7 @@ "`case:tenant-pools-summed-past-the-server-ceiling`", "`open-question:pool-contract-lane-not-executed`" ], + "basis-version": "clean-architecture-backend-template @ 21234e38 · Spring 의 REQUIRES_NEW 전파 · HikariCP 풀 제약 검증기", "publication": "초안", "file": "transaction-deadline-and-pool/concept/concept-requires-new-connection-cost.md", "status": "게시 전", @@ -3453,92 +3120,15 @@ "assets": [ "requires-new-connection-cost" ], + "assetFiles": [ + "requires-new-connection-cost" + ], "evidenceFiles": [ "../../../final/evidence/raw/requires-new-connection-cost.txt" ] } ], - "reference": [ - { - "title": "데드라인은 호출 예산에서 시작해 세 단계로 좁힌다", - "kind": "reference", - "slug": "deadline-narrows-in-three-stages", - "readiness": "READY", - "source": [ - "`final/document.md#a05` §3.3" - ], - "classification": "상류 호출자의 남은 시간이 트랜잭션·statement·lock 순으로 좁혀져야 하고, 각 단계에서 남은 시간이 다음 단계의 최소 요구를 못 채우면 **시작하지 않는 것**이 옳다. 시작해서 중간에 잘리면 completion-unknown을 만들지만 시작하지 않으면 확정 거부다.", - "scope": [ - "호출 예산을 전파하는 모든 계층. 각 단계에 여유(margin)를 두어 마지막에 결과를 기록할 시간을 남긴다." - ], - "exceptions": [ - "예산을 모르는 진입점(스케줄러·부팅 작업)은 자기 상한을 갖되 그것이 무한이 아니어야 한다." - ], - "relations": [ - "`concept:deadline-propagation`", - "`reference:write-transactions-need-a-finite-timeout`" - ], - "publication": "초안", - "file": "transaction-deadline-and-pool/reference/reference-deadline-narrows-in-three-stages.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "쓰기 트랜잭션에는 유한 타임아웃이 필수다", - "kind": "reference", - "slug": "write-transactions-need-a-finite-timeout", - "readiness": "READY", - "source": [ - "`final/document.md#a05` §2.3" - ], - "classification": "무제한 write 트랜잭션은 statement 하나가 막히면 커넥션·락·row version을 무한정 잡는다. 기준은 \"이 프로파일이 read-only가 아닌데 타임아웃이 없는가\"이고, 있으면 생성자가 거부해야 한다.", - "scope": [ - "트랜잭션 프로파일·정책 값 타입. `TransactionProfile`의 생성자가 `!readOnly && timeout이 null/0/음수`를 거부하는 형태다." - ], - "exceptions": [ - "읽기 전용 트랜잭션은 상한이 있으면 좋지만 필수는 아니다 — 락을 잡지 않기 때문이다. 다만 락을 잡는 read(`FOR UPDATE`)는 쓰기와 같이 취급한다." - ], - "relations": [ - "`reference:deadline-narrows-in-three-stages`", - "`reference:make-the-unsafe-state-unrepresentable`" - ], - "publication": "초안", - "file": "transaction-deadline-and-pool/reference/reference-write-transactions-need-a-finite-timeout.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "세션 스코프 설정은 풀로 돌아간 커넥션에 남는다", - "kind": "reference", - "slug": "session-scoped-settings-outlive-the-transaction", - "readiness": "READY", - "source": [ - "`final/document.md#a05` §3.3, §13.2" - ], - "classification": "`SET`으로 건 설정은 커넥션이 풀로 돌아가도 살아남아 다음 borrower가 상속한다. 다음 borrower가 다른 tenant이거나 tenant 없는 백그라운드 job이면 그 설정이 격리 경계를 무너뜨린다. 기준은 \"이 설정이 트랜잭션과 함께 되돌아가는가\"이고, PostgreSQL에서는 `set_config(..., true)`가 그것을 보장한다.", - "scope": [ - "`statement_timeout`·`lock_timeout`·`search_path`·`app.tenant_id` 등 세션 상태 전부. RLS의 tenant 바인딩이 transaction-local이어야 하는 이유가 같다." - ], - "exceptions": [ - "대응물이 없는 벤더(H2의 idle 가드)에서는 세션 스코프를 인정하되 **그 사실을 적고** caller-side deadline에 맡긴다 — \"적용했다\"고 거짓 보고하지 않는다." - ], - "relations": [ - "`concept:deadline-propagation`", - "`concept:rls-three-preconditions`", - "`case:search-path-survived-the-return-to-the-pool`" - ], - "publication": "초안", - "file": "transaction-deadline-and-pool/reference/reference-session-scoped-settings-outlive-the-transaction.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - } - ], + "reference": [], "question": [ { "title": "풀 계약 레인이 실행되지 않아 포화 동작이 확인되지 않았다", @@ -3569,6 +3159,7 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] } ], @@ -3600,10 +3191,11 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] }, { - "title": "`inRootWrite`는 suspend하지 않고 fail-fast한다", + "title": "inRootWrite는 suspend하지 않고 fail-fast한다", "kind": "decision", "slug": "in-root-write-fails-fast", "readiness": "READY", @@ -3629,6 +3221,7 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] } ] @@ -3637,7 +3230,7 @@ "schema-ownership-and-capability-streams": { "topic": "schema-ownership-and-capability-streams", "title": "스키마 소유권과 capability 스트림", - "readerQuestion": "", + "readerQuestion": "스키마를 누가 소유하고, 마이그레이션이 여러 갈래로 갈릴 때 무엇이 충돌하는가?", "kinds": { "case": [ { @@ -3668,12 +3261,15 @@ "assets": [ "two-trees-both-numbered-from-v1" ], + "assetFiles": [ + "two-trees-both-numbered-from-v1" + ], "evidenceFiles": [ "../../../final/evidence/raw/two-trees-both-numbered-from-v1.txt" ] }, { - "title": "`char(64)`와 `varchar(64)` 불일치를 H2가 가리고 있었다", + "title": "char(64)와 varchar(64) 불일치를 H2가 가리고 있었다", "kind": "case", "slug": "h2-hid-a-column-type-mismatch", "readiness": "READY", @@ -3702,75 +3298,15 @@ "assets": [ "h2-hid-a-column-type-mismatch" ], + "assetFiles": [ + "h2-hid-a-column-type-mismatch" + ], "evidenceFiles": [ "../../../final/evidence/raw/h2-hid-a-column-type-mismatch.txt" ] }, { - "title": "레지스트리 컬럼이 38자 경로에서 짧아 \"더 짧은 경로를 적는\" 우회를 유혹했다", - "kind": "case", - "slug": "registry-column-too-short-for-its-own-path", - "readiness": "READY", - "source": [ - "`final/document.md#a05` §8.4" - ], - "code": [ - "`db/migration/postgresql/V9__widen_capability_schema_stream.sql`", - "`db/migration/jpa/idempotency/V2`" - ], - "evidence": [ - "없음 — 마이그레이션 헤더의 사후 기록" - ], - "classification": "`schema_stream`이 `varchar(32)`였고 작성 당시 모든 스트림에 맞았으며 `'db/migration/jpa/notification-platform'`(38자)에서 안 맞기 시작했다. 실패 모드가 나쁜 종류다 — 모든 면에서 올바른 등록이 `value too long`으로 마이그레이션 타임에 실패하고, **뻔한 우회책은 스트림의 실제 경로가 아닌 더 짧은 경로를 기록하는 것**이며, 스키마가 어디서 왔는지에 대해 거짓말하는 레지스트리는 없는 것보다 나쁘다. 128로 넓힌 이유도 적혀 있다 — `capability_id`가 이미 `varchar(128)`이고 **하나의 bound가 두 개보다 추론하기 쉽다.**", - "missing-verification": "없음", - "relations": [ - "`concept:capability-schema-registry`", - "`reference:an-applied-checksum-is-a-promise`" - ], - "publication": "초안", - "file": "schema-ownership-and-capability-streams/case/case-registry-column-too-short-for-its-own-path.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "registry-column-too-short-for-its-own-path" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/registry-column-too-short-for-its-own-path.txt" - ] - }, - { - "title": "Flyway location customizer가 운영자가 바인딩한 값을 덮어썼다", - "kind": "case", - "slug": "a-customizer-that-discarded-the-bound-property", - "readiness": "READY", - "source": [ - "`final/document.md#a05` §7.7" - ], - "code": [ - "`.../postgresql/PostgreSqlPersistenceConfig.java:94-115`의 javadoc" - ], - "evidence": [ - "없음 — javadoc의 사후 기록" - ], - "classification": "customizer가 무조건 `locations(...)`를 호출했는데 그것은 Spring이 `spring.flyway.locations`에서 바인딩한 것을 **대체**한다. 그래서 운영자가 `SPRING_FLYWAY_LOCATIONS`로 capability 스트림을 추가하고 Flyway가 성공적 마이그레이션을 보고하는 것을 보고도 **벤더 스트림만** 얻을 수 있었다 — 프로퍼티는 읽히고 바인딩되고 그 뒤에 도는 customizer가 버렸다. `local-notification-ingest` 레인은 7개 location을 세팅하고 1개를 적용했다. 지금은 \"아무도 고르지 않았을 때만 기여하고, 누군가 골랐으면 비켜선다\".", - "missing-verification": "없음", - "relations": [ - "`concept:independent-flyway-streams`", - "`reference:a-bean-is-not-composition-evidence`" - ], - "publication": "초안", - "file": "schema-ownership-and-capability-streams/case/case-a-customizer-that-discarded-the-bound-property.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a-customizer-that-discarded-the-bound-property" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a-customizer-that-discarded-the-bound-property.txt" - ] - }, - { - "title": "messaging 마이그레이션 두 leaf가 같은 디렉터리에서 `V2`를 둘 만들었다", + "title": "messaging 마이그레이션 두 leaf가 같은 디렉터리에서 V2를 둘 만들었다", "kind": "case", "slug": "messaging-migrations-collide-at-v2", "readiness": "READY", @@ -3799,6 +3335,9 @@ "assets": [ "messaging-migrations-collide-at-v2" ], + "assetFiles": [ + "messaging-migrations-collide-at-v2" + ], "evidenceFiles": [ "../../../final/evidence/raw/messaging-migrations-collide-at-v2.txt" ] @@ -3826,6 +3365,7 @@ "`concept:capability-schema-registry`", "`decision:flyway-owns-the-schema`" ], + "basis-version": "clean-architecture-backend-template @ 21234e38 · Flyway 마이그레이션 스트림 여덟 갈래와 각자의 history 테이블 · baseline version 0", "publication": "초안", "file": "schema-ownership-and-capability-streams/concept/concept-independent-flyway-streams.md", "status": "게시 전", @@ -3833,39 +3373,15 @@ "assets": [ "independent-flyway-streams" ], + "assetFiles": [ + "independent-flyway-streams" + ], "evidenceFiles": [ "../../../final/evidence/raw/independent-flyway-streams.txt" ] } ], "reference": [ - { - "title": "마이그레이션 스트림은 자기 history 테이블을 갖는다", - "kind": "reference", - "slug": "each-stream-owns-its-history-table", - "readiness": "READY", - "source": [ - "`final/document.md#a05` §8.3" - ], - "classification": "두 마이그레이션 트리가 하나의 history를 공유하면 버전 공간을 공유하게 되고, 각 트리가 독립적으로 번호를 매기는 한 충돌은 시간 문제다. 기준은 \"이 트리의 버전 번호를 누가 정하는가\"이고, 답이 둘 이상이면 스트림을 나누고 history를 분리한다.", - "scope": [ - "capability별·모듈별로 나뉜 모든 마이그레이션. 분리 시 `baselineVersion(\"0\")` + `baselineOnMigrate(true)`가 필요하고, 0이어야 그 스트림의 마이그레이션이 전부 돈다." - ], - "exceptions": [ - "하나의 팀이 하나의 트리를 소유하고 그 안에서 번호를 조정할 수 있으면 분리가 불필요하다. sample composition이 두 location을 하나의 목록으로 합쳐 버전 공간을 공유하는 경우가 그 예이고, 그때는 번호 충돌을 사람이 피해야 한다." - ], - "relations": [ - "`concept:independent-flyway-streams`", - "`case:two-trees-both-numbered-from-v1`", - "`case:messaging-migrations-collide-at-v2`" - ], - "publication": "초안", - "file": "schema-ownership-and-capability-streams/reference/reference-each-stream-owns-its-history-table.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, { "title": "로컬이 다른 DB면 로컬 테스트는 다른 시스템에 대한 진술이다", "kind": "reference", @@ -3891,33 +3407,7 @@ "status": "게시 전", "studioId": "", "assets": [], - "evidenceFiles": [] - }, - { - "title": "적용된 마이그레이션의 checksum은 그것을 돌린 모든 배포에 대한 약속이다", - "kind": "reference", - "slug": "an-applied-checksum-is-a-promise", - "readiness": "READY", - "source": [ - "`final/document.md#a05` §8.1, §8.5" - ], - "classification": "이미 적용된 마이그레이션을 in-place로 고치면 그것을 돌린 배포들의 history와 어긋난다. 기준은 \"이 파일이 어딘가에 적용된 적이 있는가\"이고, 있으면 새 버전을 추가하고 가드된 변환(`DO $$ ... IF EXISTS ... THEN ALTER`)을 쓴다. 같은 이유로 Flyway `repair`는 모드가 아니다 — history를 지금 디스크에 맞게 다시 써서 **증거를 지워 증상을 해결**한다.", - "scope": [ - "모든 forward-only 마이그레이션. 스트림이 둘 이상이고 상대 순서가 고정되지 않았으면 같은 가드된 변환을 양쪽에 둔다." - ], - "exceptions": [ - "어디에도 적용된 적 없는 마이그레이션(방금 작성한 것)은 고쳐도 된다. 판정 근거는 개발자의 기억이 아니라 history 테이블이다." - ], - "relations": [ - "`case:h2-hid-a-column-type-mismatch`", - "`case:registry-column-too-short-for-its-own-path`", - "`decision:repair-is-not-a-mode`" - ], - "publication": "초안", - "file": "schema-ownership-and-capability-streams/reference/reference-an-applied-checksum-is-a-promise.md", - "status": "게시 전", - "studioId": "", - "assets": [], + "assetFiles": [], "evidenceFiles": [] } ], @@ -3952,35 +3442,7 @@ "status": "게시 전", "studioId": "", "assets": [], - "evidenceFiles": [] - }, - { - "title": "Repair는 모드가 아니라 운영자가 호출하는 작업이다", - "kind": "decision", - "slug": "repair-is-not-a-mode", - "readiness": "READY", - "decision-status": "`ADOPTED`", - "source": [ - "`final/document.md#a05` §8.1" - ], - "decision-evidence": [ - "`.../migration/FlywaySchemaPolicy.java`(모드 enum에 repair 없음)", - "`.../FlywayValidationGate.java`의 javadoc" - ], - "grounds": [ - "`reference:an-applied-checksum-is-a-promise`", - "`decision:flyway-owns-the-schema`" - ], - "classification": "Flyway의 `repair`는 schema history를 지금 디스크에 있는 스크립트에 맞게 다시 쓴다 — **증거를 지워서 증상을 해결**한다. checksum mismatch는 배포된 스크립트가 적용된 것과 다르다는 뜻이고 흥미로운 질문은 \"어떤 변경이 이 DB에 빠졌는가\"인데, repair는 그 질문을 물을 수 없게 만들어 답한다. 그래서 startup 동작이 아니라 운영자가 의도적으로 호출하는 operation descriptor로만 존재한다.", - "relations": [ - "`decision:flyway-owns-the-schema`", - "`reference:an-applied-checksum-is-a-promise`" - ], - "publication": "초안", - "file": "schema-ownership-and-capability-streams/decision/decision-repair-is-not-a-mode.md", - "status": "게시 전", - "studioId": "", - "assets": [], + "assetFiles": [], "evidenceFiles": [] } ] @@ -3989,7 +3451,7 @@ "redis-command-admission": { "topic": "redis-command-admission", "title": "Redis 명령 admission과 카탈로그", - "readerQuestion": "", + "readerQuestion": "명령 하나가 Redis 에 닿기까지 무엇을 지나야 하고, 그 관문을 건너뛰는 경로는 어디에 있는가?", "kinds": { "case": [ { @@ -4022,6 +3484,9 @@ "assets": [ "a-build-gate-that-is-not-in-the-build" ], + "assetFiles": [ + "a-build-gate-that-is-not-in-the-build" + ], "evidenceFiles": [ "../../../final/evidence/raw/a-build-gate-that-is-not-in-the-build.txt" ] @@ -4056,6 +3521,9 @@ "assets": [ "five-adapters-bypass-the-single-admission-point" ], + "assetFiles": [ + "five-adapters-bypass-the-single-admission-point" + ], "evidenceFiles": [ "../../../final/evidence/raw/five-adapters-bypass-the-single-admission-point.txt" ] @@ -4088,6 +3556,9 @@ "assets": [ "five-copies-of-noscript-recovery" ], + "assetFiles": [ + "five-copies-of-noscript-recovery" + ], "evidenceFiles": [ "../../../final/evidence/raw/five-copies-of-noscript-recovery.txt" ] @@ -4120,6 +3591,9 @@ "assets": [ "a-startup-probe-that-never-runs" ], + "assetFiles": [ + "a-startup-probe-that-never-runs" + ], "evidenceFiles": [ "../../../final/evidence/raw/a-startup-probe-that-never-runs.txt" ] @@ -4147,6 +3621,7 @@ "`reference:a-single-admission-point-must-count-its-bypasses`", "`decision:unclassified-commands-are-refused`" ], + "basis-version": "clean-architecture-backend-template @ 21234e38 · adapter-outbound-cache-redis 390 파일 · 명령 카탈로그와 CommandPolicyGuard", "publication": "초안", "file": "redis-command-admission/concept/concept-redis-admission-stages.md", "status": "게시 전", @@ -4155,70 +3630,16 @@ "redis-admission-stages", "redis-admission-stages-diagram" ], + "assetFiles": [ + "redis-admission-stages", + "redis-admission-stages" + ], "evidenceFiles": [ "../../../final/evidence/raw/redis-admission-stages.txt" ] } ], - "reference": [ - { - "title": "서버 메타데이터가 명령의 정의이고 정책 파일은 허용 범위다", - "kind": "reference", - "slug": "server-metadata-defines-the-command", - "readiness": "READY", - "source": [ - "`final/document.md#a10` §47" - ], - "classification": "명령이 **무엇인가**는 서버의 `COMMAND DOCS`/`COMMAND INFO`/`COMMAND GETKEYSANDFLAGS`가 정하고, 이 SDK가 그것으로 **무엇을 할 용의가 있는가**는 정책 파일이 정한다. 둘을 대조하는 게이트가 없으면 서버가 명령을 늘리거나 key spec을 옮겨도 알 수 없다.", - "scope": [ - "벤더 프로토콜을 감싸는 모든 SDK. 대조해야 할 다섯 버킷 — 미분류 신규 명령", - "사라진 명령", - "key spec 이동", - "ACL 카테고리 변경", - "deprecation." - ], - "exceptions": [ - "미분류 명령은 fail-closed 카탈로그가 이미 막으므로 그 버킷만은 게이트 없이도 안전하다. 나머지 넷은 카탈로그가 잡지 못한다." - ], - "relations": [ - "`case:a-build-gate-that-is-not-in-the-build`", - "`decision:unclassified-commands-are-refused`" - ], - "publication": "초안", - "file": "redis-command-admission/reference/reference-server-metadata-defines-the-command.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "단일 admission point는 우회 경로를 세어야 성립한다", - "kind": "reference", - "slug": "a-single-admission-point-must-count-its-bypasses", - "readiness": "READY", - "source": [ - "`final/document.md#a10` §64" - ], - "classification": "\"모든 것이 여기를 지난다\"는 주장은 그 지점의 코드가 아니라 **그것을 지나지 않는 경로의 수**로 검증된다. 기준은 하류 실행기를 직접 부르는 호출자를 세는 것이고, 0이 아니면 그 주장은 성립하지 않는다.", - "scope": [ - "guard·interceptor·gateway처럼 \"유일한 통로\"를 주장하는 모든 컴포넌트. 판정 방법은 하류 타입(gateway·executor)의 참조를 전수로 세고 guard를 지나는 것과 아닌 것을 나누는 것이다." - ], - "exceptions": [ - "우회 경로가 있어도 그 경로가 **같은 보장을 다른 방식으로** 유지하면 부분적으로 정당하다 — 이 저장소에서 네임스페이스가 그런 경우다. 다만 그 사실을 주장 옆에 적어야 하고, 나머지 여덟 단계는 그렇지 않다." - ], - "relations": [ - "`case:five-adapters-bypass-the-single-admission-point`", - "`concept:redis-admission-stages`", - "`reference:check-which-duplicate-is-wired`" - ], - "publication": "초안", - "file": "redis-command-admission/reference/reference-a-single-admission-point-must-count-its-bypasses.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - } - ], + "reference": [], "question": [ { "title": "Redis 토폴로지 레인이 실행되지 않아 key spec 드리프트가 확인되지 않았다", @@ -4249,49 +3670,21 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] } ], - "decision": [ - { - "title": "분류되지 않은 명령은 fail-closed로 거부한다", - "kind": "decision", - "slug": "unclassified-commands-are-refused", - "readiness": "READY", - "decision-status": "`ADOPTED`", - "source": [ - "`final/document.md#a10` §47" - ], - "decision-evidence": [ - "`.../RedisCommandCatalog.java`의 `require` 구현과 테스트 `theCatalogFailsClosedForAnUnclassifiedCommand`" - ], - "grounds": [ - "`reference:server-metadata-defines-the-command`", - "`concept:redis-admission-stages`" - ], - "classification": "카탈로그에 분류가 없는 명령은 허용이 아니라 거부다. 그래서 서버가 새 명령을 추가해도 이 SDK를 통해 조용히 나가지 못한다. 이것이 catalog drift 게이트가 없는 상태에서도 한 버킷(미분류 신규 명령)만은 안전한 이유이고, 동시에 나머지 네 버킷은 이 fail-closed가 잡지 못한다는 사실의 근거이기도 하다.", - "relations": [ - "`reference:server-metadata-defines-the-command`", - "`case:a-build-gate-that-is-not-in-the-build`" - ], - "publication": "초안", - "file": "redis-command-admission/decision/decision-unclassified-commands-are-refused.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - } - ] + "decision": [] } }, "http-failure-classification": { "topic": "http-failure-classification", "title": "HTTP 실패 분류와 재시도 안전성", - "readerQuestion": "", + "readerQuestion": "전송 실패를 어떤 어휘로 적고, 그 실패를 다시 시도해도 되는지는 무엇이 답하는가?", "kinds": { "case": [ { - "title": "붉은 테스트를 제품 결함으로 읽은 오진 — 듀얼스택 `localhost`가 TLS 실패를 가린다", + "title": "붉은 테스트를 제품 결함으로 읽은 오진 — 듀얼스택 localhost가 TLS 실패를 버린다", "kind": "case", "slug": "a-red-test-misread-as-a-product-defect", "readiness": "READY", @@ -4326,46 +3719,19 @@ "a-red-test-misread-as-a-product-defect", "a-red-test-misread-as-a-product-defect-host-control" ], + "assetFiles": [ + "a-red-test-misread-as-a-product-defect", + "a-red-test-misread-as-a-product-defect-host-control" + ], "evidenceFiles": [ "../../../final/evidence/raw/a-red-test-misread-as-a-product-defect.txt", "../../../final/evidence/raw/a-red-test-misread-as-a-product-defect-host-control.txt" ] - }, - { - "title": "로컬 거부 경로에서 회로 브레이커 permission이 반환되지 않는다", - "kind": "case", - "slug": "a-circuit-breaker-permit-that-leaks-on-local-rejection", - "readiness": "READY", - "source": [ - "`final/document.md#a11` §22" - ], - "code": [ - "`.../httpclient/...`의 회로 브레이커 permit 획득/반환 경로" - ], - "evidence": [ - "없음 — 경로 확인" - ], - "classification": "회로 브레이커 permit을 얻은 뒤 로컬 검증에서 요청이 거부되면 그 permit이 반환되지 않는다. 실패 한 번에 하나씩 줄어드는 형태이고, 같은 저장소의 messaging publish 경로가 명시적으로 막은 것(\"실패 경로에서 새는 permit은 실패 한 번에 하나씩 줄어들다 아무것도 받지 않게 되는 limiter다\")과 같은 결함이다.", - "missing-verification": "반복 호출로 permit 고갈을 재현하지 않았다", - "relations": [ - "`concept:transport-failure-stage-and-category`", - "`reference:check-which-duplicate-is-wired`" - ], - "publication": "초안", - "file": "http-failure-classification/case/case-a-circuit-breaker-permit-that-leaks-on-local-rejection.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a-circuit-breaker-permit-that-leaks-on-local-rejection" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a-circuit-breaker-permit-that-leaks-on-local-rejection.txt" - ] } ], "concept": [ { - "title": "전송 실패의 단계와 범주 — `AttemptStage`와 `FailureCategory`", + "title": "전송 실패의 단계와 범주 — AttemptStage와 FailureCategory", "kind": "concept", "slug": "transport-failure-stage-and-category", "readiness": "READY", @@ -4385,6 +3751,7 @@ "`reference:walk-the-cause-chain-most-specific-wins`", "`decision:retry-safety-is-decided-by-evidence`" ], + "basis-version": "clean-architecture-backend-template @ 21234e38 · adapter-outbound-httpclient 의 AttemptStage 와 FailureCategory", "publication": "초안", "file": "http-failure-classification/concept/concept-transport-failure-stage-and-category.md", "status": "게시 전", @@ -4393,120 +3760,59 @@ "transport-failure-stage-and-category", "transport-failure-stage-and-category-diagram" ], + "assetFiles": [ + "transport-failure-stage-and-category", + "transport-failure-stage-and-category" + ], "evidenceFiles": [ "../../../final/evidence/raw/transport-failure-stage-and-category.txt" ] + }, + { + "title": "실패 어휘 세 층과 그 사이를 잇는 SQLState 매트릭스", + "slug": "three-failure-vocabularies", + "readiness": "READY", + "source": [ + "final/document.md#5-1", + "final/document.md#a05 §7.1", + "final/document.md#a02" + ], + "code": [ + "Category", + "FailureCategory", + "PublishCompletion", + "TransmissionEvidence" + ], + "basis-version": "clean-architecture-backend-template @ 21234e38 · shared-contract Category 10값 · persistence-jpa FailureCategory 16값", + "classification": "세 어휘가 각각 다른 질문에 답하고 그 사이를 매트릭스 하나가 잇는다는 것을 알아야, 어떤 실패가 어느 층에서 이름을 얻는지 읽을 수 있다", + "relations": [ + "reference:one-sqlstate-with-two-contributors-fails-startup", + "decision:an-unrecognised-sqlstate-is-not-guessed", + "concept:transport-failure-stage-and-category" + ], + "kind": "concept", + "publication": "미작성" } ], "reference": [ { - "title": "원인 사슬은 가장 구체적인 분류가 이기도록 순회한다", - "kind": "reference", - "slug": "walk-the-cause-chain-most-specific-wins", + "title": "같은 SQLState 를 둘이 등록하면 값이 같아도 시작을 실패시킨다", + "slug": "one-sqlstate-with-two-contributors-fails-startup", "readiness": "READY", "source": [ - "일반 규칙 — 이 규칙을 처음 끌어낸 사례(`final/document.md#a11` §51)는 사이클 2에서 철회되었다(`EVD-332`). 규칙 자체는 유효하지만, **이 저장소의 `ApacheFailureClassifier`는 그 실패의 사례가 아니다.**" - ], - "classification": "예외 사슬을 바깥에서 안쪽으로 훑으며 처음 인식되는 것을 돌려주면, 라이브러리가 구체적 원인을 일반적 예외로 감쌌을 때 잘못된 분류가 나온다. 기준은 \"이 사슬에서 가장 구체적인 분류가 이기는가\"이고, 구현은 (a) 구체적 분기를 앞으로 옮기거나 (b) 사슬 전체를 훑어 최선의 매치를 고르는 것이다.", - "scope": [ - "드라이버·클라이언트 예외를 자기 범주로 번역하는 모든 분류기. 특히 전송 계층은 감싸기가 흔하다. `IdentityHashMap` + 최대 깊이로 사이클 안전을 확보하고, `SQLException.getNextException()` 같은 벤더별 곁가지도 따라간다." - ], - "exceptions": [ - "바깥 예외가 실제로 더 구체적인 경우가 있다 — 그때는 순서가 아니라 우선순위 표가 필요하고, 그 표를 테스트로 고정해야 한다." + "final/document.md#5-1", + "final/document.md#a05 §7.1" ], + "classification": "last-writer-wins merge 가 매핑 소유권 드리프트를 숨긴다는 판단이 매트릭스 병합 규칙 하나로 굳었고, 여러 기여자가 같은 표를 채우는 어떤 레지스트리에도 적용된다", + "scope": "여러 모듈이 같은 표에 항목을 등록하고 그 표가 런타임 판정을 결정하는 자리", + "exceptions": "기여자가 하나뿐이거나 중복이 의미상 같다는 것을 검사가 확인할 수 있으면 실패시키지 않아도 된다", "relations": [ - "`case:a-red-test-misread-as-a-product-defect`", - "`reference:translation-chain-order-is-a-contract`" + "concept:three-failure-vocabularies", + "reference:omission-that-passes-is-not-a-gate", + "decision:an-unrecognised-sqlstate-is-not-guessed" ], - "publication": "초안", - "file": "http-failure-classification/reference/reference-walk-the-cause-chain-most-specific-wins.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "재시도 가능성은 멱등성과 실패 범주를 함께 봐야 정해진다", "kind": "reference", - "slug": "retryability-needs-both-idempotency-and-category", - "readiness": "READY", - "source": [ - "`final/document.md#a11` §51", - "`final/document.md#a05` §3.6" - ], - "classification": "실패 범주만으로 재시도를 정하면 비멱등 요청을 재시도하고, 멱등성만으로 정하면 영구 실패를 반복한다. 기준은 두 입력의 곱이다 — 범주가 재시도 가능하고 **동시에** 요청이 재시도 안전할 때만 재시도한다. 그리고 그 판정에 \"아무것도 전송되지 않았다\"는 증거가 있으면 멱등성 요구가 완화된다.", - "scope": [ - "HTTP·gRPC·메시징 클라이언트의 재시도 결정. 이 저장소는 `notSent` 증거를 별도 축으로 두어 그 완화를 표현한다." - ], - "exceptions": [ - "분류기가 terminal로 표시한 실패는 정책의 화이트리스트로 되살릴 수 없다 — 화이트리스트는 어떤 **범주**가 재시도될 수 있는지를 넓히지 **이 실패**에 대한 판정을 뒤집지 않는다." - ], - "relations": [ - "`concept:transport-failure-stage-and-category`", - "`reference:translation-chain-order-is-a-contract`", - "`decision:retry-safety-is-decided-by-evidence`" - ], - "publication": "초안", - "file": "http-failure-classification/reference/reference-retryability-needs-both-idempotency-and-category.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "분류기는 엔진이 남긴 것만 볼 수 있다", - "kind": "reference", - "slug": "a-classifier-sees-only-what-the-engine-kept", - "readiness": "READY", - "source": [ - "`final/document.md#a11` §51.3-51.4", - "`EVD-332`" - ], - "classification": "예외를 자기 범주로 번역하는 계층은 그 아래 엔진이 **버리지 않고 남긴 것**만 볼 수 있다. Apache HttpClient 5의 다중 주소 연결 루프는 마지막이 아닌 주소의 실패를 삼키므로, 호스트명이 여러 주소로 풀리면 호출자에게 도달하는 것은 마지막 주소의 오류뿐이다. 분기 순서를 아무리 잘 짜도 사슬에 없는 원인은 분류할 수 없다.", - "scope": [ - "이름 하나가 여러 엔드포인트로 풀리는 모든 클라이언트 — DNS A/AAAA, 서비스 디스커버리, 다중 브로커 부트스트랩. 실패 분류가 재시도 안전성이나 보안 판정으로 이어지는 곳에서 특히 중요하다." - ], - "exceptions": [ - "모든 주소가 같은 이유로 실패하면 마지막 오류가 대표성을 가지므로 문제가 되지 않는다. 강등은 **패밀리별·엔드포인트별 실패 양상이 다를 때만** 일어난다." - ], - "relations": [ - "`case:a-red-test-misread-as-a-product-defect`", - "`reference:walk-the-cause-chain-most-specific-wins`" - ], - "publication": "초안", - "file": "http-failure-classification/reference/reference-a-classifier-sees-only-what-the-engine-kept.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "런타임의 모양에 대한 주장은 런타임에서 확인한다", - "kind": "reference", - "slug": "verify-runtime-shape-at-runtime", - "readiness": "READY", - "source": [ - "`final/document.md#a99` §5", - "`EVD-332`", - "`EVD-326`" - ], - "classification": "\"이 라이브러리는 예외를 이렇게 감쌀 것이다\", \"이 게이트가 이 값을 읽을 것이다\", \"이 경로가 프로덕션 기본값이다\" — 이런 주장은 코드를 읽어서 얻은 **추론**이고, 런타임에서 확인하기 전까지는 가설이다. 사이클 2가 만든 판정 번복 한 건과 자기 교정 세 건은 모두 이 형태였고, 넷 다 측정 하나로 갈렸다. 사슬을 출력하고, 리플렉션으로 private 메서드를 부르고, 조건만 바꿔 대조하는 데 드는 비용은 분 단위다.", - "scope": [ - "프레임워크·드라이버·클라이언트 라이브러리의 런타임 동작에 의존하는 모든 판정. 특히 **실패하는 테스트를 결함의 증거로 읽을 때** — 붉은 테스트는 조사의 시작점이지 결론이 아니다." - ], - "exceptions": [ - "소스가 저장소 안에 있고 그 경로가 테스트로 고정돼 있으면 읽기로 충분하다. 벤더 코드의 동작에는 해당하지 않는다." - ], - "relations": [ - "`case:a-red-test-misread-as-a-product-defect`", - "`reference:a-classifier-sees-only-what-the-engine-kept`" - ], - "publication": "초안", - "file": "http-failure-classification/reference/reference-verify-runtime-shape-at-runtime.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] + "publication": "미작성" } ], "question": [], @@ -4542,7 +3848,29 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] + }, + { + "title": "인식하지 못한 SQLSTATE 는 추측하지 않는다", + "slug": "an-unrecognised-sqlstate-is-not-guessed", + "readiness": "READY", + "source": [ + "final/document.md#10-2", + "final/document.md#5-1", + "final/document.md#a05 §7.1" + ], + "decision-status": "ADOPTED", + "decision-evidence": "미지의 SQLState 는 `Optional.empty()` 이고 translator 가 `DB_*` 코드를 만들지 않는다. 원본 예외가 web catch-all 까지 전파돼 detail 없는 generic INTERNAL 로 답한다 (final/document.md#5-1)", + "grounds": "미지의 상태를 직렬화 실패로 분류하면 재시도 코디네이터가 이미 성공한 쓰기를 기꺼이 다시 돌린다. 감수한 비용은 새 SQLSTATE 마다 매핑을 손으로 더해야 한다는 것이다", + "classification": "프로젝트가 추측하지 않는 쪽을 골랐고 그 대가를 매핑 등록으로 지불한다", + "relations": [ + "concept:three-failure-vocabularies", + "decision:retry-safety-is-decided-by-evidence", + "reference:unknown-is-a-third-result" + ], + "kind": "decision", + "publication": "미작성" } ] } @@ -4550,7 +3878,7 @@ "fileserver-state-and-fencing": { "topic": "fileserver-state-and-fencing", "title": "파일 상태 기계와 물리 정리의 seam", - "readerQuestion": "", + "readerQuestion": "파일이 공개해도 되는 상태라는 것을 무엇이 보장하고, 물리 정리는 그 상태와 어떻게 맞물리는가?", "kinds": { "case": [ { @@ -4581,73 +3909,13 @@ "assets": [ "scriptable-detection-bypassed-by-a-bom" ], + "assetFiles": [ + "scriptable-detection-bypassed-by-a-bom" + ], "evidenceFiles": [ "../../../final/evidence/raw/scriptable-detection-bypassed-by-a-bom.txt" ] }, - { - "title": "cleanup이 읽은 lease와 삭제 사이에 writer가 그 lease를 얻을 수 있었다", - "kind": "case", - "slug": "a-read-then-delete-race-on-the-upload-lease", - "readiness": "READY", - "source": [ - "`final/document.md#a08` §V4" - ], - "code": [ - "`db/migration/jpa/fileserver/V4__fileserver_upload_terminal_state.sql` 헤더" - ], - "evidence": [ - "없음 — 마이그레이션 헤더의 사후 기록" - ], - "classification": "cleanup이 writer lease를 읽고, 없는 것을 확인하고, staging 바이트를 삭제했다. 그 read와 delete 사이에 writer가 바로 그 lease를 획득할 수 있다 — **업로드가 끝났다고 말하는 게 DB에 아무것도 없었으니까** — 그리고 cleanup이 제거한 객체는 업로드가 활발히 append 중이던 것이었다. 해법은 cancel/failed finalize가 cleanup을 큐잉하는 **같은 트랜잭션 안에서** 세션을 `TERMINAL`로 옮기고, acquire/renew/offset commit이 `ACTIVE`를 요구하며, cleanup이 읽은 값으로 판단하는 대신 **조건부 update로 행을 claim**하는 것이다.", - "missing-verification": "컨테이너 레인 미실행", - "relations": [ - "`reference:claim-with-a-conditional-update-not-a-read`", - "`concept:fenced-lease`" - ], - "publication": "초안", - "file": "fileserver-state-and-fencing/case/case-a-read-then-delete-race-on-the-upload-lease.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a-read-then-delete-race-on-the-upload-lease" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a-read-then-delete-race-on-the-upload-lease.txt" - ] - }, - { - "title": "claim이 소유자·토큰·만료를 기록하지 않아 죽은 worker의 항목이 영영 남았다", - "kind": "case", - "slug": "a-cleanup-claim-without-fencing", - "readiness": "READY", - "source": [ - "`final/document.md#a08` §V3" - ], - "code": [ - "`db/migration/jpa/fileserver/V3__fileserver_fenced_cleanup_lease.sql` 헤더" - ], - "evidence": [ - "없음 — 마이그레이션 헤더의 사후 기록" - ], - "classification": "claim이 항목을 `IN_PROGRESS`로 옮기고 **다른 건 아무것도 기록하지 않았다** — owner도, token도, lease expiry도. 두 결과가 나왔다. (1) 물리 delete를 수행하고 DB를 정리하기 전에 죽은 worker가 행을 영원히 `IN_PROGRESS`로 남겼고, 어떤 쿼리도 그것을 살아있는 worker가 활발히 삭제 중인 것과 구별할 수 없어 아무것도 회수하지 않았다. (2) 완료 update가 `cleanup_id`만으로 매칭해, 합리적 lease를 한참 지나 멈춰 있던 worker가 다른 worker가 반쯤 진행한 항목 위에 DONE을 쓸 수 있었다. `NULL` lease를 reaper가 자동 회수하지 않고 운영자에게 넘기는 결정도 함께 기록돼 있다 — \"자동 takeover는 아무도 상태를 기록하지 않은 작업에 대해 추측하는 것이다.\"", - "missing-verification": "컨테이너 레인 미실행", - "relations": [ - "`concept:fenced-lease`", - "`case:a-lease-without-an-owner`", - "`reference:claim-with-a-conditional-update-not-a-read`" - ], - "publication": "초안", - "file": "fileserver-state-and-fencing/case/case-a-cleanup-claim-without-fencing.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a-cleanup-claim-without-fencing" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a-cleanup-claim-without-fencing.txt" - ] - }, { "title": "README가 \"노출된 setting도 bean도 없다\"고 적은 능력에 production bean 여덟이 있다", "kind": "case", @@ -4677,6 +3945,9 @@ "assets": [ "readme-said-no-beans-there-are-eight" ], + "assetFiles": [ + "readme-said-no-beans-there-are-eight" + ], "evidenceFiles": [ "../../../final/evidence/raw/readme-said-no-beans-there-are-eight.txt" ] @@ -4702,6 +3973,7 @@ "`reference:a-publicly-readable-state-must-be-complete-by-constraint`", "`decision:no-physical-paths-in-metadata`" ], + "basis-version": "clean-architecture-backend-template @ 21234e38 · adapter-outbound-fileserver 6개 테이블 · ck_fs_file_ready_is_complete CHECK 제약 · V2/V3/V4 마이그레이션", "publication": "초안", "file": "fileserver-state-and-fencing/concept/concept-file-state-machine-and-ready.md", "status": "게시 전", @@ -4710,129 +3982,24 @@ "file-state-machine-and-ready", "file-state-machine-and-ready-diagram" ], + "assetFiles": [ + "file-state-machine-and-ready", + "file-state-machine-and-ready" + ], "evidenceFiles": [ "../../../final/evidence/raw/file-state-machine-and-ready.txt" ] } ], - "reference": [ - { - "title": "공개 읽기 가능한 상태는 완전한 identity를 DB 제약으로 요구한다", - "kind": "reference", - "slug": "a-publicly-readable-state-must-be-complete-by-constraint", - "readiness": "READY", - "source": [ - "`final/document.md#a08` §V1" - ], - "classification": "\"이 상태의 행은 반드시 이 필드들을 갖는다\"를 애플리케이션 코드로만 두면 어떤 경로가 그것을 어겼는지 알 수 없다. 부분 CHECK 제약(`state <> 'READY' OR (… IS NOT NULL …)`)으로 두면 그 상태에 도달하는 모든 경로가 강제된다. 기준은 \"이 상태가 외부에 무엇을 약속하는가\"이고, 약속이 있으면 제약으로 적는다.", - "scope": [ - "공개·발행·활성 같은 terminal 상태를 갖는 모든 상태 기계. 같은 형태가 messaging outbox delivery의 `ck_..._state_shape`/`ck_..._terminal_shape`에도 있다." - ], - "exceptions": [ - "제약이 마이그레이션 시점에 기존 행을 재검증하므로, 이미 데이터가 있는 테이블에는 `NOT VALID`로 추가해 앞으로의 쓰기에만 적용하는 선택지가 있다 — 그때는 기존 행의 정합성을 별도로 확인해야 한다." - ], - "relations": [ - "`concept:file-state-machine-and-ready`", - "`reference:cas-tuple-in-the-where-clause`" - ], - "publication": "초안", - "file": "fileserver-state-and-fencing/reference/reference-a-publicly-readable-state-must-be-complete-by-constraint.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "조건부 update로 행을 claim하고 읽은 값으로 판단하지 않는다", - "kind": "reference", - "slug": "claim-with-a-conditional-update-not-a-read", - "readiness": "READY", - "source": [ - "`final/document.md#a08` §V4", - "`final/document.md#a05` §10" - ], - "classification": "읽어서 확인하고 행동하면 그 사이에 상태가 바뀔 수 있다. 조건부 update가 행을 claim하면 update count가 곧 \"내가 얻었는가\"의 답이 된다. 기준은 \"이 판단과 그에 따른 행동 사이에 다른 참여자가 끼어들 수 있는가\"다.", - "scope": [ - "cleanup·relay·worker처럼 경쟁 참여자가 있는 모든 작업. 물리 자원(파일·객체)을 지우는 경로에서 특히 결정적이다 — 되돌릴 수 없기 때문이다." - ], - "exceptions": [ - "단일 참여자가 구조적으로 보장되면 불필요하다. 다만 그 보장이 배포 형태에서 오면 다중 인스턴스로 가는 날 깨진다." - ], - "relations": [ - "`case:a-read-then-delete-race-on-the-upload-lease`", - "`case:a-cleanup-claim-without-fencing`", - "`reference:cas-tuple-in-the-where-clause`" - ], - "publication": "초안", - "file": "fileserver-state-and-fencing/reference/reference-claim-with-a-conditional-update-not-a-read.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "접두사 시작 매칭은 시그니처에는 맞고 스니핑 패턴에는 맞지 않는다", - "kind": "reference", - "slug": "prefix-matching-fits-signatures-not-sniffing", - "readiness": "READY", - "source": [ - "`final/document.md#a08` §40" - ], - "classification": "매직바이트는 정의상 파일 선두에 있으므로 접두사 시작 비교가 정확하다. 브라우저가 스니핑하는 패턴은 선두 고정이 아니므로 같은 비교가 우회 가능하다. 기준은 \"이 패턴의 정의가 위치를 포함하는가\"이고, 아니면 정규화(BOM·NUL·주석 제거) 후 탐색이거나 범위 스캔이어야 한다.", - "scope": [ - "콘텐츠 기반 탐지 전반 — 스크립트 탐지·MIME 스니핑 방어·서명 확인. 정규화 대상은 최소한 BOM(U+FEFF)·NUL·선행 주석이다." - ], - "exceptions": [ - "시그니처 검증(매직바이트)은 선두 고정이 옳고 정규화하면 오히려 틀린다." - ], - "relations": [ - "`case:scriptable-detection-bypassed-by-a-bom`", - "`reference:reject-rather-than-sanitize`" - ], - "publication": "초안", - "file": "fileserver-state-and-fencing/reference/reference-prefix-matching-fits-signatures-not-sniffing.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - } - ], + "reference": [], "question": [], - "decision": [ - { - "title": "물리 경로와 원본 파일명을 저장하지 않는다", - "kind": "decision", - "slug": "no-physical-paths-in-metadata", - "readiness": "READY", - "decision-status": "`ADOPTED`", - "source": [ - "`final/document.md#a08` §V1" - ], - "decision-evidence": [ - "`db/migration/jpa/fileserver/V1__create_fileserver_metadata.sql` 헤더가 결정과 근거를 적는다" - ], - "grounds": [ - "`concept:file-state-machine-and-ready`", - "`reference:a-publicly-readable-state-must-be-complete-by-constraint`" - ], - "classification": "물리 경로·마운트·원본 물리 파일명을 저장하지 않고 `content_key`(서버 생성 opaque)와 `original_name`(신뢰할 수 없는 표시용 텍스트)만 둔다. 그래서 저장 위치를 바꿔도 메타데이터가 바뀌지 않고, 클라이언트가 준 이름이 경로로 해석될 여지가 없다.", - "relations": [ - "`concept:file-state-machine-and-ready`" - ], - "publication": "초안", - "file": "fileserver-state-and-fencing/decision/decision-no-physical-paths-in-metadata.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - } - ] + "decision": [] } }, "objectstorage-staged-lifecycle": { "topic": "objectstorage-staged-lifecycle", "title": "객체 스토리지의 단계적 수명주기와 권한 분리", - "readerQuestion": "", + "readerQuestion": "업로드를 단계로 나누면 각 단계에서 누가 무엇을 할 수 있어야 하는가?", "kinds": { "case": [ { @@ -4866,71 +4033,12 @@ "assets": [ "apply-is-a-setting-approval-is-not" ], + "assetFiles": [ + "apply-is-a-setting-approval-is-not" + ], "evidenceFiles": [ "../../../final/evidence/raw/apply-is-a-setting-approval-is-not.txt" ] - }, - { - "title": "직접 multipart의 마지막 part는 grant를 받을 수 없다", - "kind": "case", - "slug": "the-last-part-cannot-get-a-grant", - "readiness": "READY", - "source": [ - "`final/document.md#a09` §38" - ], - "code": [ - "`.../objectstorage/...`의 multipart grant 발급 경로" - ], - "evidence": [ - "없음 — 경로 확인" - ], - "classification": "직접 multipart 업로드에서 마지막 part가 grant를 받을 수 없는 경로 결함이다. grant가 업로드 단계별로 발급되는 구조인데 마지막 part의 조건이 그 발급 경로를 벗어난다.", - "missing-verification": "실제 multipart 업로드를 수행해 재현하지 않았다", - "relations": [ - "`concept:staged-lifecycle-and-signed-grants`", - "`case:endpoint-check-only-on-upload`" - ], - "publication": "초안", - "file": "objectstorage-staged-lifecycle/case/case-the-last-part-cannot-get-a-grant.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "the-last-part-cannot-get-a-grant" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/the-last-part-cannot-get-a-grant.txt" - ] - }, - { - "title": "서명된 grant의 endpoint 검증이 upload 경로에만 있다", - "kind": "case", - "slug": "endpoint-check-only-on-upload", - "readiness": "READY", - "source": [ - "`final/document.md#a09` §39" - ], - "code": [ - "`.../objectstorage/...`의 grant 검증 지점들" - ], - "evidence": [ - "없음 — 검증 지점 전수 확인" - ], - "classification": "grant가 endpoint를 바인딩하는데 그 검증이 upload 경로에만 있고 다른 경로에는 없다. grant를 발급받은 주체가 다른 endpoint로 그것을 쓸 수 있다는 뜻이고, 서명이 endpoint를 덮는 이유가 무력화된다.", - "missing-verification": "다른 endpoint로 grant를 제시해 재현하지 않았다", - "relations": [ - "`concept:staged-lifecycle-and-signed-grants`", - "`case:the-last-part-cannot-get-a-grant`" - ], - "publication": "초안", - "file": "objectstorage-staged-lifecycle/case/case-endpoint-check-only-on-upload.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "endpoint-check-only-on-upload" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/endpoint-check-only-on-upload.txt" - ] } ], "concept": [ @@ -4954,6 +4062,7 @@ "`case:the-last-part-cannot-get-a-grant`", "`reference:the-powerful-half-must-not-be-one-setting-away`" ], + "basis-version": "clean-architecture-backend-template @ 21234e38 · adapter-outbound-objectstorage 206 파일 · sample-portfolio 에만 출하", "publication": "초안", "file": "objectstorage-staged-lifecycle/concept/concept-staged-lifecycle-and-signed-grants.md", "status": "게시 전", @@ -4962,104 +4071,24 @@ "staged-lifecycle-and-signed-grants", "staged-lifecycle-and-signed-grants-diagram" ], + "assetFiles": [ + "staged-lifecycle-and-signed-grants", + "staged-lifecycle-and-signed-grants" + ], "evidenceFiles": [ "../../../final/evidence/raw/staged-lifecycle-and-signed-grants.txt" ] } ], - "reference": [ - { - "title": "권한이 센 절반이 설정 한 줄로 켜지면 안 된다", - "kind": "reference", - "slug": "the-powerful-half-must-not-be-one-setting-away", - "readiness": "READY", - "source": [ - "`final/document.md#a09` §49" - ], - "classification": "한 능력이 \"실행하는 절반\"과 \"승인하는 절반\"으로 나뉠 때, 실행 쪽이 설정으로 켜지고 승인 쪽이 손으로 배선해야 하면 비대칭이 위험 방향으로 기운다. 기준은 \"이 설정을 켠 배포에서 승인 검증이 없으면 무엇이 일어나는가\"이고, 답이 \"실행된다\"면 그 설정이 승인 bean의 부재를 startup 실패로 만들어야 한다.", - "scope": [ - "승인·서명·2인 통제를 요구하는 모든 관리 작업. 이 저장소의 admin plane이 반대 사례다 — 파괴적 작업의 bean을 **아예 자동설정하지 않고** 그 부재를 javadoc으로 문서화한다." - ], - "exceptions": [ - "승인 검증을 애플리케이션 계층이 소유하는 설계면 어댑터에 bean이 없는 것이 옳다. 다만 그때는 어댑터가 승인 없이 실행되는 경로를 갖지 않아야 하고, 이 저장소에서는 어댑터의 검사가 필드 동등성뿐이다." - ], - "relations": [ - "`case:apply-is-a-setting-approval-is-not`", - "`reference:a-bean-is-not-composition-evidence`" - ], - "publication": "초안", - "file": "objectstorage-staged-lifecycle/reference/reference-the-powerful-half-must-not-be-one-setting-away.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "이진 승인 코덱은 decode 후 재인코딩이 원본과 같아야 한다", - "kind": "reference", - "slug": "a-binary-approval-codec-must-round-trip", - "readiness": "READY", - "source": [ - "`final/document.md#a09` §49" - ], - "classification": "서명 대상이 이진 인코딩이면 같은 논리 값에 대해 여러 바이트 표현이 가능할 때 서명 우회가 생긴다. decode한 뒤 재인코딩해 원본 바이트와 동일한지 확인하면 비정규 인코딩이 거부된다. 기준은 \"이 인코딩이 canonical인가\"이고, 보장이 없으면 round-trip 검사가 그 자리다.", - "scope": [ - "서명·MAC의 대상이 되는 모든 이진·텍스트 인코딩. 길이 프레이밍이 함께 필요한 이유도 같은 축이다." - ], - "exceptions": [ - "인코딩이 정의상 canonical이면(고정 길이 필드만) 불필요하다. 다만 필드가 하나라도 가변이면 성립하지 않는다." - ], - "relations": [ - "`case:apply-is-a-setting-approval-is-not`", - "`reference:digest-must-be-length-framed-and-versioned`", - "`concept:signed-cursor-structure`" - ], - "publication": "초안", - "file": "objectstorage-staged-lifecycle/reference/reference-a-binary-approval-codec-must-round-trip.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - } - ], + "reference": [], "question": [], - "decision": [ - { - "title": "legacy 채택은 서로 다른 두 승인자의 서명을 요구한다", - "kind": "decision", - "slug": "legacy-adoption-requires-two-approvers", - "readiness": "READY", - "decision-status": "`ADOPTED`", - "source": [ - "`final/document.md#a09` §49" - ], - "decision-evidence": [ - "`.../Ed25519LegacyAdoptionApprovalVerifier.java`의 구현과 그 테스트(`verifiesCanonicalExactBindingWithTwoDistinctTrustedApprovers`, `rejectsDuplicateApproverAndAnyBindingTamper`)", - "`sample-portfolio`의 `AdoptLegacyPosterImageUseCase.authorizeApply`가 보여 주는 의도된 조립" - ], - "grounds": [ - "`concept:staged-lifecycle-and-signed-grants`", - "`reference:the-powerful-half-must-not-be-one-setting-away`" - ], - "classification": "원시 locator로 legacy 네임스페이스를 읽어 관리 네임스페이스에 발행하는 동작에 detached 2인 승인을 요구한다. 검증기가 서로 다른 두 승인자를 요구하고(중복 승인자 거부), destination·epoch·operationId·manifest·네임스페이스 다이제스트를 요청과 대조하며, 유효창을 최대 7일로 제한한다. 의도된 조립은 use case가 **호출자가 준 approval을 버리고** 검증 결과로 요청을 다시 만드는 것 — \"application에서 검증하고 adapter에서 적용한다\". 그 use case가 어디에도 배선돼 있지 않다는 점이 `case:apply-is-a-setting-approval-is-not`이다.", - "relations": [ - "`case:apply-is-a-setting-approval-is-not`", - "`reference:a-binary-approval-codec-must-round-trip`" - ], - "publication": "초안", - "file": "objectstorage-staged-lifecycle/decision/decision-legacy-adoption-requires-two-approvers.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - } - ] + "decision": [] } }, "self-disclosure-grading": { "topic": "self-disclosure-grading", "title": "자기 공시 — 등급을 스스로 낮춰 적기", - "readerQuestion": "", + "readerQuestion": "자기 미완성을 스스로 적는 것이 무엇을 바꾸고, 등급은 무엇에서 나와야 하는가?", "kinds": { "case": [ { @@ -5092,6 +4121,9 @@ "assets": [ "seven-rows-downgraded-by-the-family-itself" ], + "assetFiles": [ + "seven-rows-downgraded-by-the-family-itself" + ], "evidenceFiles": [ "../../../final/evidence/raw/seven-rows-downgraded-by-the-family-itself.txt" ] @@ -5127,6 +4159,9 @@ "assets": [ "a-startup-validator-that-requires-a-key-nothing-signs-with" ], + "assetFiles": [ + "a-startup-validator-that-requires-a-key-nothing-signs-with" + ], "evidenceFiles": [ "../../../final/evidence/raw/a-startup-validator-that-requires-a-key-nothing-signs-with.txt" ] @@ -5161,44 +4196,12 @@ "assets": [ "build-only-exempts-ninety-files-from-todays-incident" ], + "assetFiles": [ + "build-only-exempts-ninety-files-from-todays-incident" + ], "evidenceFiles": [ "../../../final/evidence/raw/build-only-exempts-ninety-files-from-todays-incident.txt" ] - }, - { - "title": "담금 임계값이 열거형에 없는 등급을 위해 쓰여 중간 등급이 상위 등급보다 어려워졌다", - "kind": "case", - "slug": "a-soak-threshold-written-for-a-grade-that-does-not-exist", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-advanced-bootstrap` §17.4" - ], - "code": [ - "`.../grpc-advanced-bootstrap/.../release/GrpcAdvancedPromotionGate.java`", - "`.../bootstrap/GrpcCapabilityGrade.java`", - "`.../release/GrpcAdvancedPromotionGateTest.java`" - ], - "evidence": [ - "없음 — `evaluate` 본문과 열거형 값 집합, 두 테스트가 고른 숫자로 판정했다" - ], - "classification": "게이트 javadoc 의 모형은 등급 둘이다 — Advanced Stable(7일)과 \"Stable default\"(30일). 상수도 둘이다. 그런데 `GrpcCapabilityGrade` 의 값은 `ADVANCED_STABLE`·`EXPERIMENTAL`·`WATCH`·`DISABLED` 넷이고 \"Stable default\" 는 없다. 선택이 `to == ADVANCED_STABLE ? 7일 : 30일` 이므로 **존재하지 않는 등급을 위해 만든 갈래가 존재하는 세 등급을 삼킨다.** 증거 일곱 항목도 목표 등급과 무관하게 요구되므로 결과가 뒤집힌다 — `EXPERIMENTAL → ADVANCED_STABLE` 은 일곱 + 7일, `WATCH → EXPERIMENTAL` 은 일곱 + 30일. `WATCH` 가 밟도록 강제된 유일한 첫 걸음(같은 메서드의 셋째 차단 사유가 그렇게 적는다)이 상위 등급보다 엄격하고, `WATCH` 의 뜻이 \"추적할 뿐 구현되지 않았다\" 이므로 담금 기록이 가장 적을 등급에 가장 긴 담금을 요구한다. 테스트가 그 뒤틀림을 그대로 보여 준다 — 30일 갈래를 실행하려고 고른 전이가 `ADVANCED_STABLE → DISABLED`(철회)인데 이름은 \"becoming a Stable default\" 이고, `WATCH → EXPERIMENTAL` 테스트는 담금을 60일로 줘서 30일 요구가 레인에 걸리지 않는다.", - "missing-verification": "테스트를 실행하지 않았다 — 두 테스트가 고른 숫자와 갈래 조건으로 판정했다", - "relations": [ - "`reference:grades-may-understate-never-overstate`", - "`decision:capability-grade-is-declared-not-inferred`", - "`case:test-names-that-assert-what-their-bodies-do-not`" - ], - "listedInTree": false, - "publication": "초안", - "file": "self-disclosure-grading/case/case-a-soak-threshold-written-for-a-grade-that-does-not-exist.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a-soak-threshold-written-for-a-grade-that-does-not-exist" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a-soak-threshold-written-for-a-grade-that-does-not-exist.txt" - ] } ], "concept": [ @@ -5221,6 +4224,7 @@ "`reference:grades-may-understate-never-overstate`", "`decision:capability-grade-is-declared-not-inferred`" ], + "basis-version": "clean-architecture-backend-template @ 21234e38 · adapter-inbound-graphql 의 CLAUDE.md 가 정의한 네 등급 · 13행 등급표", "publication": "초안", "file": "self-disclosure-grading/concept/concept-four-grade-disclosure.md", "status": "게시 전", @@ -5229,6 +4233,10 @@ "four-grade-disclosure", "four-grade-disclosure-diagram" ], + "assetFiles": [ + "four-grade-disclosure", + "four-grade-disclosure" + ], "evidenceFiles": [ "../../../final/evidence/raw/four-grade-disclosure.txt" ] @@ -5262,10 +4270,11 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] }, { - "title": "`runtime_memberships`를 먼저 읽고 심각도를 정한다", + "title": "runtime_memberships를 먼저 읽고 심각도를 정한다", "kind": "reference", "slug": "runtime-membership-decides-severity", "readiness": "READY", @@ -5292,6 +4301,7 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] } ], @@ -5327,6 +4337,7 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] } ] @@ -5335,7 +4346,7 @@ "multitenancy-isolation": { "topic": "multitenancy-isolation", "title": "멀티테넌시 격리가 조용히 무력화되는 방법", - "readerQuestion": "", + "readerQuestion": "테넌트 격리를 켰다고 적은 뒤에도 그것이 아무것도 하지 않을 수 있는 이유는 무엇인가?", "kinds": { "case": [ { @@ -5368,4978 +4379,12 @@ "assets": [ "three-ways-rls-does-nothing" ], + "assetFiles": [ + "three-ways-rls-does-nothing" + ], "evidenceFiles": [ "../../../final/evidence/raw/three-ways-rls-does-nothing.txt" ] - }, - { - "title": "`search_path`가 풀로 돌아간 커넥션에 남아 다음 tenant가 상속한다", - "kind": "case", - "slug": "search-path-survived-the-return-to-the-pool", - "readiness": "READY", - "source": [ - "`final/document.md#a05` §13.2" - ], - "code": [ - "`.../experimental/multitenancy/SchemaMultiTenantConnectionProvider.java`의 reset 구현과 javadoc" - ], - "evidence": [ - "없음 — 구현과 javadoc" - ], - "classification": "`search_path`는 세션 설정이라 풀로 돌아간 커넥션이 여전히 마지막 tenant의 스키마를 들고 있다. 다음 borrower — 아마 다른 tenant, 또는 tenant 없는 백그라운드 job — 가 **어떤 statement도 틀리지 않은 채로** 거기서 읽고 쓴다. 해법은 반환 시 `NEUTRAL_SCHEMA = \"pg_catalog\"`로 되돌리는 것이다. 같은 성질이 H2의 로컬 타임아웃 설정에도 있고(세션 스코프라 트랜잭션이 끝나도 남는다) 그쪽은 매 트랜잭션 전 재적용으로 실무상 가려진다.", - "missing-verification": "풀 재사용에서 잔존을 실제로 관측하지 않았다", - "relations": [ - "`reference:isolation-settings-must-be-transaction-local`", - "`reference:session-scoped-settings-outlive-the-transaction`", - "`concept:four-multitenancy-strategies`" - ], - "publication": "초안", - "file": "multitenancy-isolation/case/case-search-path-survived-the-return-to-the-pool.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "search-path-survived-the-return-to-the-pool" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/search-path-survived-the-return-to-the-pool.txt" - ] - }, - { - "title": "tenant별 풀이 개별적으로 합리적이고 그 합이 서버 상한을 넘긴다", - "kind": "case", - "slug": "tenant-pools-summed-past-the-server-ceiling", - "readiness": "READY", - "source": [ - "`final/document.md#a05` §13.2" - ], - "code": [ - "`.../experimental/multitenancy/TenantPoolBudget.java`의 두 ceiling과 javadoc" - ], - "evidence": [ - "없음 — 구현과 javadoc" - ], - "classification": "database-per-tenant는 특정한 방식으로 실패한다 — 각 tenant의 풀은 개별적으로 합리적이고 **그 합이 아니다.** 50 tenant × 10 커넥션 = `max_connections`가 100인 서버에 500 커넥션이고, 실패는 **idle이던 것 포함 모든 tenant에 동시에** connection refusal로 도착한다. `TenantPoolBudget`이 두 ceiling(풀 개수와 커넥션 총합)을 갖는 이유도 적혀 있다 — pool count만으로는 풀 크기 차이를 무시하고, connection total만으로는 각자 스레드와 모니터링을 가진 무한한 수의 작은 풀을 허용한다. 다만 `final/document.md#a05` §98이 기록하듯 예산이 **새 pool 크기를 계산하지 않아** ceiling을 넘길 수 있다.", - "missing-verification": "다중 tenant 풀을 실제로 세워 상한 초과를 재현하지 않았다", - "relations": [ - "`concept:requires-new-connection-cost`", - "`concept:four-multitenancy-strategies`", - "`reference:deadline-narrows-in-three-stages`" - ], - "publication": "초안", - "file": "multitenancy-isolation/case/case-tenant-pools-summed-past-the-server-ceiling.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "tenant-pools-summed-past-the-server-ceiling" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/tenant-pools-summed-past-the-server-ceiling.txt" - ] - }, - { - "title": "`IdFactory.newId()`의 “never-before-used” 문구 정밀화", - "kind": "case", - "slug": "a01-f002-idfactory-newid", - "readiness": "READY", - "source": [ - "`final/document.md#a01#L216`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "Fact: interface는 저장소 collision check를 요구하지 않고 sample test도 전역 uniqueness를 증명하지 않는다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "identity-and-value-contracts/case/case-a01-f002-idfactory-newid.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a01-f002-idfactory-newid" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a01-f002-idfactory-newid.txt" - ] - }, - { - "title": "notification admin atomic claim contract가 service에서 사용되지 않음", - "kind": "case", - "slug": "analysis-finding-a03-f001", - "readiness": "READY", - "source": [ - "`final/document.md#a03#L344`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "- Fact: `AdminOperationStorePort.claim()`과 `JpaAdminOperationStore.claim()`은 존재하지만 `NotificationAdminApplicationService`는 redrive/reconcile/suppress/provider-state에서 `findByOperationId -> side effect -> save`를 사용한다. - Why it matters: 동일 operation id의 concurrent 요청이 둘 다 side effect를 실행할 수 있으며, 이는 claim javadoc이 명시한 과거 race와 동일하다. - Verification: 동일 operation id/command를 barrier로 동시에 호출하고 destructive action invocation count가 1인지 검증하는 concurrency regression test. - Candidate direction: service가 command fingerprint를 계산해 atomic claim을 먼저 수행하고 claimed/replay/conflict/in-progress를 분기. - Tech-Log: CASE + OPEN QUESTION/DECISION 후보.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/case/case-analysis-finding-a03-f001.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a03-f001" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a03-f001.txt" - ] - }, - { - "title": "notification consumer는 diagnostic failure를 authoritative failure로 바꿀 수 있다", - "kind": "case", - "slug": "analysis-finding-a04-f002", - "readiness": "READY", - "source": [ - "`final/document.md#a04#L232`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "이 finding은 support logger의 consumer semantics를 추적하면서 발견했다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/case/case-analysis-finding-a04-f002.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a04-f002" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a04-f002.txt" - ] - }, - { - "title": "notification fail-open consumer가 logger failure를 격리하지 않는다", - "kind": "case", - "slug": "analysis-finding-a04-f004", - "readiness": "READY", - "source": [ - "`final/document.md#a04#L585`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "- Observed fact: `FailOpenNotificationProvider`는 send와 logSuccess를 동일 try에 두고 catch 안의 logFailure를 보호하지 않는다. - Runtime evidence: successful send 뒤 debug logger failure가 warn failure observation을 만들었고, provider failure 뒤 warn logger failure는 caller까지 전파됐다. - Comparison: messaging은 같은 shared logger를 `observeQuietly`로 이미 격리하고 regression test를 갖는다. - Why it matters: diagnostics가 business/provider outcome을 바꿔서는 안 된다는 non-authoritative observation 원칙이 consumer마다 달라진다. - Verification: `022a` focused probe; notification에 throwing-logger regression 추가. - Candidate: notification에서 observation isolation 또는 shared logger no-throw contract. - Tech-Log: CASE + DECISION 후보.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/case/case-analysis-finding-a04-f004.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a04-f004" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a04-f004.txt" - ] - }, - { - "title": "support README가 current architecture registry/history와 drift", - "kind": "case", - "slug": "analysis-finding-a04-f005", - "readiness": "READY", - "source": [ - "`final/document.md#a04#L595`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "- Observed fact: SSOT 위치, CLAUDE.md 존재 여부, HTTP logger 존재 여부가 current source와 불일치. - Why it matters: support module의 dependency policy와 비교 설계를 읽는 사람이 현재 architecture를 잘못 이해한다. - Verification: `019` raw search/history. - Candidate: README를 `modules.json`/current consumer topology에 맞춰 갱신. - Tech-Log: 보통 refactor/doc maintenance; 독립 CASE 우선순위는 낮음.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/case/case-analysis-finding-a04-f005.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a04-f005" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a04-f005.txt" - ] - }, - { - "title": "encode가 발급한 2046~2048-byte cursor를 decode가 거부한다", - "kind": "case", - "slug": "analysis-finding-a05-f001", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L382`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "문제는 decoded payload size를 decode 전에 추정하는 helper다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/case/case-analysis-finding-a05-f001.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a05-f001" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a05-f001.txt" - ] - }, - { - "title": "`RetryDecision.reason`의 “bounded” 설명과 constructor contract 불일치", - "kind": "case", - "slug": "a05-f004-retrydecision-reason", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L670`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "- Observed: 100,000-character reason accepted. - Observed: current retry metrics는 reason을 tag로 사용하지 않음. - Impact: 현재 cardinality defect로 확인되지 않음. - Candidate: length bound를 추가하거나 javadoc의 low-cardinality claim을 실제 사용 범위에 맞게 좁힘.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "bounding-by-type/case/case-a05-f004-retrydecision-reason.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a05-f004-retrydecision-reason", - "a05-f004-retrydecision-reason-probe" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a05-f004-retrydecision-reason.txt", - "../../../final/evidence/raw/a05-f004-retrydecision-reason-probe.txt" - ] - }, - { - "title": "application-supplied `JpaRetryPolicy`가 valid execution에서 무시된다", - "kind": "case", - "slug": "a05-f005-jparetrypolicy", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L991`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`JpaTransactionAutoConfiguration`은 명시적으로 다음 overload를 제공한다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "runtime-contract-correctness/case/case-a05-f005-jparetrypolicy.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a05-f005-jparetrypolicy", - "a05-f005-jparetrypolicy-probe" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a05-f005-jparetrypolicy.txt", - "../../../final/evidence/raw/a05-f005-jparetrypolicy-probe.txt" - ] - }, - { - "title": "Stable completion-evidence capability가 shipped composition에 설치되지 않는다", - "kind": "case", - "slug": "a05-f006-stable", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L1122`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "### 23.1 custom manager production construction = 0", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "capability-declaration-vs-proof/case/case-a05-f006-stable.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a05-f006-stable", - "a05-f006-stable-chain" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a05-f006-stable.txt", - "../../../final/evidence/raw/a05-f006-stable-chain.txt" - ] - }, - { - "title": "`TransactionProfileRegistry`는 declarative retry 제거 후 legacy residue 후보", - "kind": "case", - "slug": "a05-f007-transactionprofileregistry", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L1304`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "단 repository 밖 reflection/external direct construction은 source search로 알 수 없으므로 즉시 삭제 가능성까지 확정하지 않는다. module의 non-api package는 intended external이 아니라는 architecture policy와 함께 보면 cleanup 우선순위는 높아진다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/case/case-a05-f007-transactionprofileregistry.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a05-f007-transactionprofileregistry-adr", - "a05-f007-transactionprofileregistry" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a05-f007-transactionprofileregistry-adr.txt", - "../../../final/evidence/raw/a05-f007-transactionprofileregistry.txt" - ] - }, - { - "title": "canonical transaction boundary documentation과 실제 dual stack 불일치", - "kind": "case", - "slug": "analysis-finding-a05-f010", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L1542`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "- docs/source comment는 coordinator가 PolicyTransactionPort를 구현한다고 설명. - 실제 구현체는 SpringTransactionPort. - coordinator/runtime bean은 별도로 계속 존재. - 우선순위: P2 architecture consistency / Decision 필요.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/case/case-analysis-finding-a05-f010.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a05-f010" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a05-f010.txt" - ] - }, - { - "title": "property-access `IDENTITY` entity가 batch guard를 우회한다", - "kind": "case", - "slug": "a05-f012-identity", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L1731`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`HibernateBatchConfigurationGuard`는 batching-required profile에서 `GenerationType.IDENTITY`를 거부한다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "what-a-gate-does-not-prove/case/case-a05-f012-identity.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a05-f012-identity", - "a05-f012-identity-probe" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a05-f012-identity.txt", - "../../../final/evidence/raw/a05-f012-identity-probe.txt" - ] - }, - { - "title": "`SpecificationPolicy`는 `Specification.unrestricted()`를 bounded로 오인한다", - "kind": "case", - "slug": "a05-f013-specificationpolicy-specification-unrestricted", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L2021`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "> a specification with no predicate is a full table scan wearing a builder's clothing", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "what-a-gate-does-not-prove/case/case-a05-f013-specificationpolicy-specification-unrestricted.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a05-f013-specificationpolicy-specification-unrestricted", - "a05-f013-specificationpolicy-specification-unrestricted-probe" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a05-f013-specificationpolicy-specification-unrestricted.txt", - "../../../final/evidence/raw/a05-f013-specificationpolicy-specification-unrestricted-probe.txt" - ] - }, - { - "title": "`collection-fetch-pagination` blocking release gate가 실제 위험을 증명하지 않는다", - "kind": "case", - "slug": "a05-f014-collection-fetch-pagination", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L2257`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "support matrix는 다음 gate를 blocking release gate로 선언한다.", - "missing-verification": "### 52.2 release registry가 가리키는 producer task는 그 test를 실행하지도 않는다", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "what-a-gate-does-not-prove/case/case-a05-f014-collection-fetch-pagination.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a05-f014-collection-fetch-pagination", - "a05-f014-collection-fetch-pagination-probe", - "a05-f014-collection-fetch-pagination-lane" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a05-f014-collection-fetch-pagination.txt", - "../../../final/evidence/raw/a05-f014-collection-fetch-pagination-probe.txt", - "../../../final/evidence/raw/a05-f014-collection-fetch-pagination-lane.txt" - ] - }, - { - "title": "query SQL naming/observability composition 부재", - "kind": "case", - "slug": "a05-f018-sql", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L2556`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "분석 문서가 `query SQL naming/observability composition 부재`를 P2 finding으로 분류했다. 원문 source anchor를 record 작성 시 다시 열어 코드·테스트·실행 증거를 그대로 승계한다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/case/case-a05-f018-sql.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a05-f018-sql" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a05-f018-sql.txt" - ] - }, - { - "title": "export surface split SSOT", - "kind": "case", - "slug": "a05-f019-ssot", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L2562`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "- leaf export list와 app-bootstrap consumer list가 중복 정의되고 이미 다름 - current tests pass하지만 두 목록 간 drift를 막는 single-source rule 없음 - architecture governance hardening", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "declaration-and-document-drift/case/case-a05-f019-ssot.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a05-f019-ssot" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a05-f019-ssot.txt" - ] - }, - { - "title": "`inspect()`와 `claim()`이 만료된 COMPLETED row를 동시에 다른 상태로 해석한다", - "kind": "case", - "slug": "a05-f020-inspect-claim", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L2675`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "판정: P1 — production idempotency lifecycle/reconciliation inconsistency.", - "missing-verification": "`inspect()`는 row가 `COMPLETED`이고 response payload가 있으면 `replayUntil`이 이미 지난 값인지 확인하지 않고 무조건 `COMPLETED_REPLAY`를 반환한다. 반면 claim path는 DB time과 expiry를 보고 만료된 row를 takeover 가능 상태로 처리한다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/case/case-a05-f020-inspect-claim.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a05-f020-inspect-claim", - "a05-f020-inspect-claim-postgres" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a05-f020-inspect-claim.txt", - "../../../final/evidence/raw/a05-f020-inspect-claim-postgres.txt" - ] - }, - { - "title": "`complete()`의 replay 판정이 `replayTtl` 변경을 무시한다", - "kind": "case", - "slug": "a05-f021-complete-replayttl", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L2704`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "첫 호출은 1시간, 두 번째 호출은 동일 operation/response에 9시간을 전달했다. 두 번째 호출은 semantic argument가 다른데도 same-result로 판정됐고 DB에는 최초 1시간 window가 그대로 남았다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/case/case-a05-f021-complete-replayttl.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a05-f021-complete-replayttl-postgres", - "a05-f021-complete-replayttl" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a05-f021-complete-replayttl-postgres.txt", - "../../../final/evidence/raw/a05-f021-complete-replayttl.txt" - ] - }, - { - "title": "Stable runtime-role verification이 startup에서 실제 policy를 적용하지 않는다", - "kind": "case", - "slug": "a05-f022-stable", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L2991`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "판정: P1 — production security/runtime-composition contract violation.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "capability-declaration-vs-proof/case/case-a05-f022-stable.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a05-f022-stable" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a05-f022-stable.txt" - ] - }, - { - "title": "persistent byte quota가 실제 admission에서 집행되지 않는다", - "kind": "case", - "slug": "analysis-finding-a05-f023", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L3177`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "판정: P1 production cross-scope contract violation — persistent scope/tenant byte quota enforcement missing.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/case/case-analysis-finding-a05-f023.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a05-f023" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a05-f023.txt" - ] - }, - { - "title": "quota reclaim은 최대 64개 committed row만 처리하고 남은 byte를 조용히 버린다", - "kind": "case", - "slug": "analysis-finding-a05-f024", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L3246`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "판정: P2 production accounting correctness defect.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/case/case-analysis-finding-a05-f024.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a05-f024" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a05-f024.txt" - ] - }, - { - "title": "direct `FileQuotaService.commit()`은 만료 reservation을 commit한다", - "kind": "case", - "slug": "a05-f025-filequotaservice-commit", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L3266`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "판정: P2 production API-contract defect.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/case/case-a05-f025-filequotaservice-commit.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a05-f025-filequotaservice-commit", - "a05-f025-filequotaservice-commit-postgres" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a05-f025-filequotaservice-commit.txt", - "../../../final/evidence/raw/a05-f025-filequotaservice-commit-postgres.txt" - ] - }, - { - "title": "recovery queue의 `enqueue()`는 concurrent upsert가 아니다", - "kind": "case", - "slug": "a05-f026-enqueue", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L3287`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "판정: P2 production concurrency/idempotency defect.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "non-atomic-check-then-act/case/case-a05-f026-enqueue.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a05-f026-enqueue", - "a05-f026-enqueue-race" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a05-f026-enqueue.txt", - "../../../final/evidence/raw/a05-f026-enqueue-race.txt" - ] - }, - { - "title": "cleanup crash-reclaim은 `MAXIMUM_ATTEMPTS`를 우회해 poison item을 무한 재시도할 수 있다", - "kind": "case", - "slug": "a05-f027-maximum-attempts", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L3316`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "판정: P2 production liveness / bounded-retry defect.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/case/case-a05-f027-maximum-attempts.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a05-f027-maximum-attempts", - "a05-f027-maximum-attempts-reclaim" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a05-f027-maximum-attempts.txt", - "../../../final/evidence/raw/a05-f027-maximum-attempts-reclaim.txt" - ] - }, - { - "title": "provider 호출 뒤 recipient projection write가 lease fencing을 우회한다", - "kind": "case", - "slug": "analysis-finding-a05-f028", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L3460`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "판정: P1 production concurrency/correctness defect. provider side effect와 authoritative outcome write 사이의 lease handoff에서 stale writer가 살아남는다. 결과에 따라 중복 전송 위험 판단, retry/reconciliation state, attempt count가 새 holder의 흐름과 충돌할 수 있다.", - "missing-verification": "실제 fenced helper도 완전하지 않다. `saveProjectionHeldBy()` / `transitionHeldBy()`는 `id + lease_owner + lease_fence`만 조건으로 두고 `lease_until > now`는 확인하지 않는다. PostgreSQL에서 이미 만료되어 `stillHeld` 조건이 0건인 row에 동일 owner/fence write를 실행하면 `UPDATE 1`이었다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/case/case-analysis-finding-a05-f028.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a05-f028" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a05-f028.txt" - ] - }, - { - "title": "reconciliation `FOR UPDATE SKIP LOCKED`는 worker 처리 구간을 claim하지 않는다", - "kind": "case", - "slug": "a05-f029-for-update-skip-locked", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L3494`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "판정: P2 production multi-instance coordination defect. reconciliation은 send 자체가 아니라 provider 상태 조회/상태 projection이어서 recipient dispatch P1보다 영향도를 낮게 잡지만, 두 worker가 같은 job을 처리할 수 있다는 class contract는 깨진다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "state-ownership-and-concurrency/case/case-a05-f029-for-update-skip-locked.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a05-f029-for-update-skip-locked", - "a05-f029-for-update-skip-locked-probe" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a05-f029-for-update-skip-locked.txt", - "../../../final/evidence/raw/a05-f029-for-update-skip-locked-probe.txt" - ] - }, - { - "title": "V8 atomic admin claim은 production service에 연결되지 않았고 completion 모델도 미완성이다", - "kind": "case", - "slug": "analysis-finding-a05-f030", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L3525`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "판정: P2 production idempotency/wiring defect. V8에서 만든 fix가 dead path이며 completion state machine도 이어지지 않는다. DB transaction 안에서 수행되는 redrive/suppress 일부 경로는 마지막 unique conflict가 loser transaction을 rollback시켜 결과를 완화하지만, reconcile/provider runtime control처럼 action과 final audit insert가 하나의 동일 DB transaction으로 묶이지 않는 경로까지 전체적으로 exactly-once operation claim을 보장하지 못한다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/case/case-analysis-finding-a05-f030.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a05-f030" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a05-f030.txt" - ] - }, - { - "title": "vendor selector의 fail-fast 계약이 shipped composition에 설치돼 있지 않다", - "kind": "case", - "slug": "analysis-finding-a05-f031", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L4089`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "판정: P2 confirmed. 오타 난 vendor 값은 startup을 실패시키기는 하지만, 그 실패는 property를 지목하지 않는다 — `PersistenceVendorSettings`가 막겠다고 선언한 바로 그 증상이다. app-bootstrap의 `PersistenceVendorProdSafetyValidator`도 도움이 되지 않는다. 그 validator는 prod profile에서 값이 `h2`인지만 보고 알 수 없는 값은 통과시킨다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/case/case-analysis-finding-a05-f031.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a05-f031" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a05-f031.txt" - ] - }, - { - "title": "nightly workflow가 광고하는 세 가지 중 하나를 lane이 실제로 관측하지 않는다", - "kind": "case", - "slug": "analysis-finding-a05-f032", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L4273`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`.github/workflows/jpa-nightly.yml:122-129`는 이 lane이 검사하는 것을 세 가지로 적는다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/case/case-analysis-finding-a05-f032.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a05-f032" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a05-f032.txt" - ] - }, - { - "title": "selected base card `jpa-flyway-migration`의 producer가 현재 revision에서 실패한다", - "kind": "case", - "slug": "a05-f033-jpa-flyway-migration", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L4391`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "판정: P1 confirmed. 수정은 assertion을 stream의 현재 applied set으로 갱신하는 것이고, 재발 방지는 `PostgreSqlOptionalStreamLifecycle`이 이미 쓰는 방식(stream별 버전 목록을 한 곳에 고정)을 base stream에도 적용하는 것이다. 더 근본적으로는 이 lane이 tag lane과 완전히 분리돼 있다는 구조 자체가 재검토 대상이다 — 5개 tag lane이 green이라는 사실이 readiness lane의 상태에 대해 아무것도 말해주지 않는다.", - "missing-verification": "`postgresqlSecurityBaselineIntegrationTest`의 실패는 **분석 환경 제약**이지 결함이 아니다. `verifyFullAcceptsTrustedHostAndRejectsHostnameMismatchAndUntrustedCertificate`는 `PostgreSqlTlsMaterial`이 `CN=localhost` / `SAN=DNS:localhost`로 발급한 인증서를 `verify-full`로 검증하므로 컨테이너의 매핑 포트가 **테스트 JVM의 loopback**에서 열려 있어야 한다. 이번 분석은 Docker 소켓을 공유하는 형제 컨테이너 안에서 실행돼 매핑 포트가 Docker 브리지(172.17.0.1)에만 열렸고, 실패는 `java.net.ConnectException`이다. 이 lane은 skip이 아니라 실패하도록 설계돼 있으므로(no-skip) 동작 자체는 의도대로다. 다만 \"no-skip\"의 대가로 **Docker 호스트와 테스트 JVM이 loopback을 공유하는 환경**이 이 lane의 암묵적 전제가 된다는 사실은 기록해 둔다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "what-a-gate-does-not-prove/case/case-a05-f033-jpa-flyway-migration.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a05-f033-jpa-flyway-migration", - "a05-f033-jpa-flyway-migration-run", - "a05-f033-jpa-flyway-migration-gate" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a05-f033-jpa-flyway-migration.txt", - "../../../final/evidence/raw/a05-f033-jpa-flyway-migration-run.txt", - "../../../final/evidence/raw/a05-f033-jpa-flyway-migration-gate.txt" - ] - }, - { - "title": "selected base card 3개의 evidence tag가 production code 없는 fixture로 충족된다", - "kind": "case", - "slug": "analysis-finding-a05-f034", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L4462`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "판정: P2 confirmed. 대비되는 반례가 같은 card 집합 안에 있다는 점이 판단을 쉽게 해 준다 — `jpa-transaction-runtime`의 7개 scenario는 production `SpringTransactionPort` + `PostgreSqlLocalTimeoutConfigurer` + `PersistenceExceptionTranslator`를 실제 서버에서 돌리고, deadlock 40P01, serializable 재시도, lock/statement timeout 경계, pool admission 거부, `pg_terminate_backend`로 만든 commit 유실의 `INDETERMINATE` 판정까지 확인한다. 즉 이 결함은 체계적인 것이 아니라 세 card에 국한된다. 수정은 tag를 옮기는 문제다 — 이미 존재하는 강한 test를 scenario로 등재하거나, 약한 scenario의 `covers`에서 과대 tag를 떼는 것.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/case/case-analysis-finding-a05-f034.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a05-f034" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a05-f034.txt" - ] - }, - { - "title": "폐기된 namespace guard의 탐색 domain이 operator가 읽는 두 문서를 덮지 않는다", - "kind": "case", - "slug": "analysis-finding-a06-f002", - "readiness": "READY", - "source": [ - "`final/document.md#a06#L132`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "판정: P3 confirmed. guard가 막겠다고 명시한 형태(문서가 operator에게 폐기 키를 쓰라고 말하는 것)가 guard의 사각지대에서 그대로 살아 있고, 그중 하나는 복사해 쓰라고 제시된 예제다. 런타임은 영향받지 않는다 — Compose lane은 `SPRING_MONGODB_URI`를 공급하고, 폐기는 제거가 아니다. 수정은 두 문서의 키를 `spring.mongodb.*`로 바꾸고, guard의 domain에 leaf의 `*.md`를 추가하는 것이다(추가하면 위 세 곳이 즉시 red가 되므로 함께 고쳐야 한다).", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/case/case-analysis-finding-a06-f002.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a06-f002" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a06-f002.txt" - ] - }, - { - "title": "`change-streams=true`는 거부되지 않고 조용히 버려지며, 그 결과 startup validator의 한 분기가 production에서 도달 불가다", - "kind": "case", - "slug": "a06-f003-change-streams-true", - "readiness": "READY", - "source": [ - "`final/document.md#a06#L156`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "판정: P3 confirmed. 현재 잘못된 동작을 만들지는 않는다 — change stream 실행체는 애초에 shipped되지 않는다고 CLAUDE.md가 명시한다. 문제는 (a) 문서가 refuse라고 말하는 것이 discard이고, (b) 그 결과 capability 검사 한 갈래가 test에서만 살아 있다는 점이다. 수정은 두 방향 중 하나다 — 값을 정말로 거부하거나(`requiredSecondaries`와 같은 형태), 아니면 flag를 record component에서 제거해 존재하지 않는 스위치로 만드는 것.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/case/case-a06-f003-change-streams-true.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a06-f003-change-streams-true", - "a06-f003-change-streams-true-probe", - "a06-f003-change-streams-true-shipped" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a06-f003-change-streams-true.txt", - "../../../final/evidence/raw/a06-f003-change-streams-true-probe.txt", - "../../../final/evidence/raw/a06-f003-change-streams-true-shipped.txt" - ] - }, - { - "title": "schema version 실패는 두 경로 중 어느 쪽도 온전하지 않다", - "kind": "case", - "slug": "analysis-finding-a06-f004", - "readiness": "READY", - "source": [ - "`final/document.md#a06#L332`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`MongoFailureCategory`에는 이 실패를 위한 전용 값 `SCHEMA_VERSION_UNSUPPORTED`(\"The stored document's schema version is outside the supported range\")가 있고, 전용 예외 `MongoDataSchemaUnsupportedException`이 `documentVersion` / `minimumSupported` / `currentVersion` 세 정수를 공개 accessor로 노출한다. production 생성 지점은 정확히 둘이고, 각각 반쪽만 맞다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/case/case-analysis-finding-a06-f004.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a06-f004" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a06-f004.txt" - ] - }, - { - "title": "예외 계층의 \"cause를 붙이지 않는다\" 규칙에 문서화되지 않은 예외가 하나 있다", - "kind": "case", - "slug": "analysis-finding-a06-f005", - "readiness": "READY", - "source": [ - "`final/document.md#a06#L347`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`MongoPersistenceException`의 javadoc은 두 번째 규칙을 절대적으로 서술한다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/case/case-analysis-finding-a06-f005.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a06-f005" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a06-f005.txt" - ] - }, - { - "title": "D3 gateway가 문서화한 검사 순서에 존재하지 않는 단계가 있다", - "kind": "case", - "slug": "analysis-finding-a06-f007", - "readiness": "READY", - "source": [ - "`final/document.md#a06#L505`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "판정: P2. 수정은 문서를 실제 검사로 줄이거나(정직), 선언된 `timeout`/`maxResults`를 gateway가 실제로 적용하도록 만드는 것이다. 후자를 택하면 `hasBody()`가 처음으로 호출자를 갖게 된다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/case/case-analysis-finding-a06-f007.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a06-f007" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a06-f007.txt" - ] - }, - { - "title": "서버 측 deadline이 경로마다 다르게 적용되고, 문서가 지목한 메커니즘은 production 호출자가 0이다", - "kind": "case", - "slug": "analysis-finding-a06-f008", - "readiness": "READY", - "source": [ - "`final/document.md#a06#L600`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "판정: P2. 데이터 손상은 아니지만 platform이 스스로 선언한 자원 경계가 자신의 세 실행 경로에서 서버에 도달하지 않는다. 수정은 `MongoPlatformCollectionAccess`가 `rawOperations()` 대신 deadline이 붙은 접근자를 내보내거나, 세 executor가 query를 만들 때 `context.timeout()`을 붙이는 것이다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/case/case-analysis-finding-a06-f008.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a06-f008" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a06-f008.txt" - ] - }, - { - "title": "timeout 초과 경로가 한 observation에 success와 failure를 모두 기록한다", - "kind": "case", - "slug": "analysis-finding-a06-f009", - "readiness": "READY", - "source": [ - "`final/document.md#a06#L622`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`MongoOperationRejectedException`은 `MongoPersistenceException`의 하위 타입이고, 이 throw는 같은 `try` 블록 안에 있으므로 바로 다음 `catch (MongoPersistenceException alreadyTranslated)`가 잡아 `observation.failure(...)`를 호출한 뒤 다시 던진다. 결과적으로 하나의 observation에 `success`와 `failure`가 차례로 호출된다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/case/case-analysis-finding-a06-f009.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a06-f009" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a06-f009.txt" - ] - }, - { - "title": "collection 이름 불변식이 aggregation executor의 서명에서 깨진다", - "kind": "case", - "slug": "analysis-finding-a06-f010", - "readiness": "READY", - "source": [ - "`final/document.md#a06#L717`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "현재 노출은 없다(§41: 미배선). 그러나 fork가 이 executor를 배선하는 순간 두 가지가 동시에 생긴다 — registry가 보장한다고 적힌 불변식의 예외 하나, 그리고 관측·실패번역 없이 도는 실행 경로 하나. 판정: P2. 수정은 서명에서 `String collection`을 없애고 `context.collectionProfile()`을 registry로 해석하는 것, 그리고 실행을 `DefaultMongoImperativeExecutor.executeInternal(...)` 안으로 옮기는 것이다. 후자는 §32에서 본 deadline 문제도 함께 해결한다(현재 aggregation은 `maxTime`을 스스로 붙이므로 그 부분만은 이미 옳다).", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/case/case-analysis-finding-a06-f010.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a06-f010" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a06-f010.txt" - ] - }, - { - "title": "`MongoRegexPolicy.forbidden()`은 금지하지 않는다", - "kind": "case", - "slug": "a06-f011-mongoregexpolicy-forbidden", - "readiness": "READY", - "source": [ - "`final/document.md#a06#L740`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "\"금지\"가 별도 상태가 아니라 최대 길이 1로 표현돼 있다. `validate(pattern, flags)`의 네 검사를 길이 1짜리 패턴 `^`에 대해 따라가면 — 길이 1 ≤ 1 통과, flags 없음 통과, `requireAnchored && startsWith(\"^\")` 통과, `hasNestedQuantifier(\"^\")`는 그룹이 없으므로 false 통과 — 수용된다. 그리고 `^`는 모든 문자열에 매치된다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "security-policy-enforcement/case/case-a06-f011-mongoregexpolicy-forbidden.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a06-f011-mongoregexpolicy-forbidden-probe", - "a06-f011-mongoregexpolicy-forbidden" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a06-f011-mongoregexpolicy-forbidden-probe.txt", - "../../../final/evidence/raw/a06-f011-mongoregexpolicy-forbidden.txt" - ] - }, - { - "title": "`recordApplied`는 문서화된 fence 계약을 구현하지 않고, 보호를 역전시킨다", - "kind": "case", - "slug": "a06-f013-recordapplied", - "readiness": "READY", - "source": [ - "`final/document.md#a06#L884`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "판정: P2. 수정은 `saveCheckpoint`와 같은 모양이다 — `recordApplied`도 저장된 fence를 조건으로 삼고, duplicate-key를 잡아 platform 예외로 번역하는 것. 지금은 test도 이 경계를 보지 않는다: `MongoMigrationFencingTest.ledgerWritesCarryTheirFence`는 fence 값이 전달되는지만 보고, `MongoMigrationLaneTest`의 superseded 테스트는 checkpoint만 다룬다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "state-ownership-and-concurrency/case/case-a06-f013-recordapplied.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a06-f013-recordapplied", - "a06-f013-recordapplied-probe", - "a06-f013-recordapplied-around" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a06-f013-recordapplied.txt", - "../../../final/evidence/raw/a06-f013-recordapplied-probe.txt", - "../../../final/evidence/raw/a06-f013-recordapplied-around.txt" - ] - }, - { - "title": "index diff가 실제로 비교하는 것은 두 필드뿐이다", - "kind": "case", - "slug": "analysis-finding-a06-f014", - "readiness": "READY", - "source": [ - "`final/document.md#a06#L910`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`MongoIndexDescriptorView`의 javadoc이 \"reduced to the fields a diff can compare\"라고 스스로 한정하는 것은 사실이지만, 그 축소의 결과(무엇이 감지 불가가 되는지)는 어디에도 적혀 있지 않고, `MongoIndexDiff.render()`가 CI artifact로 쓰이도록 설계돼 있으므로 \"빈 보고서 = 일치\"로 읽힌다. 판정: P2. 최소 수정은 `MongoIndexDescriptorView`에 `expireAfter`와 `sparse`를 추가하고 `compare`에서 비교하는 것, 그리고 `actual.hidden() && !declared.hidden()`에 대한 `unhide` 항목을 두는 것이다. 그것이 과하다면 최소한 비교 대상 필드 집합을 diff 출력에 함께 적어 \"빈 보고서\"가 무엇을 뜻하는지 읽는 사람이 알 수 있게 해야 한다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/case/case-analysis-finding-a06-f014.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a06-f014" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a06-f014.txt" - ] - }, - { - "title": "TTL이 두 곳에 선언되고, 규칙을 가진 쪽은 아무도 쓰지 않는다", - "kind": "case", - "slug": "analysis-finding-a06-f015", - "readiness": "READY", - "source": [ - "`final/document.md#a06#L927`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "1. `MongoIndexManifest.expireAfter(Duration)` — 검증은 생성자의 `isNegative()` 하나. 2. `MongoTtlPolicy` / `MongoTtlIndexDescriptor` + `MongoTtlPolicyValidator` — 세 가지 실질 규칙: 최소 보존기간 1분(그 아래는 한 번의 sweep으로 전체 population을 지운다), expiry 필드의 BSON 타입이 `date`인지(아니면 MongoDB가 조용히 무시한다), 그리고 읽기가 `expiresAt > applicationNow`를 거는지(TTL monitor는 임의 간격으로 돌므로 만료된 문서는 그때까지 계속 읽힌다).", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/case/case-analysis-finding-a06-f015.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a06-f015" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a06-f015.txt" - ] - }, - { - "title": "Flamingock lease로는 어떤 migration도 실행할 수 없고, javadoc은 다르게 적는다", - "kind": "case", - "slug": "a06-f016-flamingock", - "readiness": "READY", - "source": [ - "`final/document.md#a06#L942`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`FlamingockLockAdapter.fence()`는 `UNFENCED`(-1)를 반환하고, 그 이유를 정직하게 적는다 — 로컬 카운터로 fencing을 흉내내면 \"look like fencing and protect nothing\". 여기까지는 옳다. 문제는 그 다음 문장이다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "declaration-and-document-drift/case/case-a06-f016-flamingock.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a06-f016-flamingock", - "a06-f016-flamingock-probe" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a06-f016-flamingock.txt", - "../../../final/evidence/raw/a06-f016-flamingock-probe.txt" - ] - }, - { - "title": "`changeStreams` flag는 `false`로 고정돼 있는데, 소비자 bean은 그것과 무관하게 조립된다", - "kind": "case", - "slug": "a06-f018-changestreams-false", - "readiness": "READY", - "source": [ - "`final/document.md#a06#L1069`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "판정: P2. 수정은 셋 중 하나다: `changeStreams`를 실제 flag로 되살려 소비자 조립의 조건으로 쓰거나, 소비자 bean이 조립될 때 CHANGE_STREAM capability를 startup에서 검증하거나, 최소한 `MongoPlatformSettings`의 주석을 현재 사실(\"source는 출하됐고 소비자도 조립된다\")로 고치는 것. 지금 주석은 운영자가 읽으면 틀린 결론에 도달한다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "assembly-ownership/case/case-a06-f018-changestreams-false.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a06-f018-changestreams-false", - "a06-f018-changestreams-false-wiring" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a06-f018-changestreams-false.txt", - "../../../final/evidence/raw/a06-f018-changestreams-false-wiring.txt" - ] - }, - { - "title": "recovery package에 쓰이는 어휘와 쓰이지 않는 어휘가 나란히 있다", - "kind": "case", - "slug": "analysis-finding-a06-f019", - "readiness": "READY", - "source": [ - "`final/document.md#a06#L1088`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`135-...` §8.3의 검색 결과를 정리하면, 소비자가 실제로 쓰는 것과 아닌 것이 갈린다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/case/case-analysis-finding-a06-f019.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a06-f019" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a06-f019.txt" - ] - }, - { - "title": "프로파일의 TLS·타임아웃·풀·Stable API가 driver에 도달하지 않는다", - "kind": "case", - "slug": "a06-f020-tls-stable", - "readiness": "READY", - "source": [ - "`final/document.md#a06#L1161`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "판정: P1. 수리는 이미 있는 형태를 따르면 된다 — `MongoClientSettingsBuilderCustomizer` bean 하나가 `MongoClientSettingsFactory`(또는 그 `build` 로직)를 Boot의 빌더에 적용하게 하는 것. 그때 `MongoCredentialResolver`도 비로소 경로에 들어온다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "security-and-trust-boundaries/case/case-a06-f020-tls-stable.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a06-f020-tls-stable", - "a06-f020-tls-stable-probe" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a06-f020-tls-stable.txt", - "../../../final/evidence/raw/a06-f020-tls-stable-probe.txt" - ] - }, - { - "title": "admin gateway의 두 audit 경로 중 하나만 fail-closed다", - "kind": "case", - "slug": "analysis-finding-a06-f021", - "readiness": "READY", - "source": [ - "`final/document.md#a06#L1189`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`MongoAdminGateway.execute`는 모든 audit 쓰기를 `audit(...)` 헬퍼로 보내고, 그 헬퍼는 sink 실패를 `MongoOperationRejectedException`으로 바꾼다 — \"an administrative operation that cannot be audited does not run\". `MongoAdminAuditStateMachineTest.anUnauditableCommandDoesNotRun`이 그것을 고정한다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/case/case-analysis-finding-a06-f021.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a06-f021" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a06-f021.txt" - ] - }, - { - "title": "태그 allowlist는 규약이지 강제가 아니다", - "kind": "case", - "slug": "analysis-finding-a06-f022", - "readiness": "READY", - "source": [ - "`final/document.md#a06#L1195`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`MongoObservationConvention`의 javadoc은 강제라고 말한다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/case/case-analysis-finding-a06-f022.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a06-f022" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a06-f022.txt" - ] - }, - { - "title": "sharding admin gateway의 네 작업 중 셋은 어떤 입력으로도 완료될 수 없다", - "kind": "case", - "slug": "analysis-finding-a06-f023", - "readiness": "READY", - "source": [ - "`final/document.md#a06#L1281`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "판정: P2. 데이터 위험은 없다 — 거부는 fail-closed이고, 오히려 안전한 방향으로 틀렸다. 위험은 능력이 문서상 존재하고 실제로는 없다는 것이며, 그 사실이 발견되는 시점은 운영자가 프로덕션 클러스터에서 reshard를 실행하려는 순간이다. 수정은 세 메서드가 `MongoAdminCommand.over(...)` + `MongoAdminApproval.of(command, approver, expiry)`를 만들어 2인자 `execute`에 넘기고, `ReshardApproval`의 증거를 그 승인의 전제로 쓰는 것이다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/case/case-analysis-finding-a06-f023.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a06-f023" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a06-f023.txt" - ] - }, - { - "title": "promotion 증거 어휘가 둘이고, gate는 하나만 검사한다", - "kind": "case", - "slug": "analysis-finding-a06-f024", - "readiness": "READY", - "source": [ - "`final/document.md#a06#L1305`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`MongoAdvancedPromotionEvidence.REQUIRED`는 여섯 범주다: `stable-platform`, `actual-topology`, `security`, `migration`, `failure`, `runbook`. `MongoAdvancedPromotionGate.verify(...)`가 그 여섯을 전부 검사한다 — 그리고 그 파일에는 고쳐진 결함이 주석으로 남아 있다: \"`migration` was in `MongoAdvancedPromotionEvidence.REQUIRED` and not here, so the gate demanded five of the six categories it declares… which is the shape MNG-008 names: a gate that certifies more than it ran.\"", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/case/case-analysis-finding-a06-f024.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a06-f024" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a06-f024.txt" - ] - }, - { - "title": "구현 없는 4개의 계약 중 셋은 그 사실을 적고, 하나는 적지 않는다", - "kind": "case", - "slug": "analysis-finding-a06-f025", - "readiness": "READY", - "source": [ - "`final/document.md#a06#L1324`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`MongoSearchOperations`·`MongoTimeSeriesOperations`·`MongoVectorSearchOperations`는 모두 동일한 문단을 담는다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/case/case-analysis-finding-a06-f025.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a06-f025" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a06-f025.txt" - ] - }, - { - "title": "커버리지 gate 둘이 나란히 있고, 하나는 발화할 수 없다", - "kind": "case", - "slug": "analysis-finding-a06-f026", - "readiness": "READY", - "source": [ - "`final/document.md#a06#L1383`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "판정: P2. 두 인증 lane(7.0/8.0)의 커버리지 주장이 무효다. 수정은 형제를 따르면 된다 — `run(...)`이 실행할 contract 집합을 인자로 받거나, runner가 실제로 호출된 것만 `executed`에 넣는 것.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/case/case-analysis-finding-a06-f026.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a06-f026" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a06-f026.txt" - ] - }, - { - "title": "release gate가 실제로 차단하는 것은 hermetic test 3개이고, mongo용 CI workflow는 없다", - "kind": "case", - "slug": "analysis-finding-a06-f027", - "readiness": "READY", - "source": [ - "`final/document.md#a06#L1410`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "판정: P2. 이것은 개별 코드 결함이 아니라 이 leaf의 검증 지형이다. 그리고 앞선 sub-scope들에서 찾은 것들 — §67의 change stream 소실, §75의 TLS 미적용, §85의 sharding 미완료, §56의 fence 계약 — 이 왜 살아남았는지를 설명한다: 그것들을 잡을 lane은 릴리스를 막지 않고 CI에서 돌지 않는다. 수정은 두 갈래다. (a) 컨테이너 lane 중 최소한 `mongoReplicaSetTest`·`mongoMigrationTest`·`mongoSecurityIntegrationTest`를 blocking contract로 승격하고, (b) JPA와 같은 형태의 workflow를 추가하는 것.", - "missing-verification": "차단 계약 **셋 전부가 `topology=none`**, 즉 컨테이너가 필요 없는 hermetic 클래스다. experimental 셋은 **어느 build 파일에도 등록되지 않은 task**를 가리킨다(`grep mongoShardedTest build.gradle` → 매치 0; 스크립트가 그 사실을 스스로 적는다: \"registered by no build file\"). 그리고 컨테이너가 필요한 여섯 lane — `mongoReplicaSetTest`·`mongoFailoverTest`·`mongoMigrationTest`·`mongoCompatibilityTest`·`mongoSecurityIntegrationTest`·`mongoPerformanceTest` — 은 **차단 목록에 하나도 없다**.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/case/case-analysis-finding-a06-f027.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a06-f027" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a06-f027.txt" - ] - }, - { - "title": "소비자가 없는 fixture 셋", - "kind": "case", - "slug": "analysis-finding-a06-f028", - "readiness": "READY", - "source": [ - "`final/document.md#a06#L1433`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`138-...` §8.1의 소비자 계수에서 test·testkit 양쪽 모두 0인 타입이 셋이다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/case/case-analysis-finding-a06-f028.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a06-f028" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a06-f028.txt" - ] - }, - { - "title": "모듈의 존재 논거인 `UuidCodec`에 production 소비자가 없다", - "kind": "case", - "slug": "a07-f001-uuidcodec", - "readiness": "READY", - "source": [ - "`final/document.md#a07#L73`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "판정: P2. 코드 자체에는 결함이 없다 — 30줄짜리 유틸이고 자기 스펙을 통과한다. 문제는 §1의 논거다. 모듈을 `adapter-outbound` 밖에 두는 근거로 \"UUID id/codec 능력\"을 들고 있는데, 그 능력은 아무도 쓰지 않고 같은 일이 저장소 곳곳에서 각자 수행된다. 나머지 두 타입(가명화·업로드 식별자)만으로도 non-IO 능력 모듈의 논거는 성립하므로, 수정은 둘 중 하나다: `UuidCodec`을 실제 단일 경로로 만들거나(그러면 §4가 먼저 고쳐져야 한다), 모듈의 논거에서 빼는 것.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "identity-and-identifier/case/case-a07-f001-uuidcodec.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a07-f001-uuidcodec", - "a07-f001-uuidcodec-elsewhere" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a07-f001-uuidcodec.txt", - "../../../final/evidence/raw/a07-f001-uuidcodec-elsewhere.txt" - ] - }, - { - "title": "`normalize`는 canonical이 아닌 입력을 받아 다른 UUID로 조용히 바꾼다", - "kind": "case", - "slug": "a07-f002-normalize", - "readiness": "READY", - "source": [ - "`final/document.md#a07#L89`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "도달성. 지금 이 메서드를 부르는 production 코드는 없다(§3). 그래서 현재 노출은 0이고, `UuidCodec`을 단일 경로로 승격하는 순간 결함이 된다. 판정: P2. 수정은 `input.length() != 36`이거나 대시 위치가 8-13-18-23이 아니면 먼저 거부하는 것 — 또는 계약 문구를 실제 동작(JDK 관대 파싱)에 맞추는 것이다. 전자가 문서가 말하는 바다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "identity-and-identifier/case/case-a07-f002-normalize.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a07-f002-normalize", - "a07-f002-normalize-probe" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a07-f002-normalize.txt", - "../../../final/evidence/raw/a07-f002-normalize-probe.txt" - ] - }, - { - "title": "CLAUDE.md의 의존성 서술이 세 항목 모두 틀렸다", - "kind": "case", - "slug": "a07-f004-claude", - "readiness": "READY", - "source": [ - "`final/document.md#a07#L131`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "> `:application-code`, `:domain-core`, `:shared-contract` (Gradle matrix). Currently only `:domain-core` + `com.github.f4b6a3:uuid-creator` are declared in build.gradle.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "identity-and-identifier/case/case-a07-f004-claude.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a07-f004-claude" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a07-f004-claude.txt" - ] - }, - { - "title": "README의 세 가지 사실 오류", - "kind": "case", - "slug": "analysis-finding-a07-f005", - "readiness": "READY", - "source": [ - "`final/document.md#a07#L150`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "셋 다 메커니즘 자체는 실재하고 동작한다 — 틀린 것은 이름과 버전이다. P3.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "identity-and-identifier/case/case-analysis-finding-a07-f005.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a07-f005" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a07-f005.txt" - ] - }, - { - "title": "CLAUDE.md가 대는 두 가드 중 하나는 저장소에 없다", - "kind": "case", - "slug": "a07-f006-claude", - "readiness": "READY", - "source": [ - "`final/document.md#a07#L160`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "1. ArchUnit `identifier_adapter_does_not_depend_on_other_adapters_or_bootstrap` — 존재한다(§2, 상수명은 대문자). confirmed. 2. `.claude/hooks/ca_import_gate.py` G4가 쓰기 시점에 차단 — `.claude/` 디렉터리에는 `settings.local.json` 하나뿐이고 `hooks/` 하위 디렉터리도 `ca_import_gate.py`도 tracked 되어 있지 않다(`140-...` §8.4e).", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "identity-and-identifier/case/case-a07-f006-claude.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a07-f006-claude" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a07-f006-claude.txt" - ] - }, - { - "title": "R1과 R2의 설정 취급이 비대칭이고, 검증된 쪽은 하나뿐이다", - "kind": "case", - "slug": "analysis-finding-a08-f002", - "readiness": "READY", - "source": [ - "`final/document.md#a08#L108`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "R2에서는 `strict-path-securty` 같은 오타가 컨텍스트를 실패시킨다. R1에서는 `app.file-export.maximum-rowz=10` 같은 오타가 조용히 무시되고, 설정했다고 믿는 상한이 적용되지 않은 채 기본값 1,000,000이 쓰인다. 두 selector가 같은 leaf의 같은 성격 설정인데 한쪽만 fail-closed다. P3 — R1은 문서상 \"compatibility only\"이므로 우선순위를 낮춘다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "file-transfer-and-storage/case/case-analysis-finding-a08-f002.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a08-f002" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a08-f002.txt" - ] - }, - { - "title": "문서가 지목한 기본값 위치와 test 목록이 실제와 다르다", - "kind": "case", - "slug": "analysis-finding-a08-f003", - "readiness": "READY", - "source": [ - "`final/document.md#a08#L122`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "- README는 R2 selector가 \"`app-bootstrap/application.yml`에서 `false`로 기본값을 갖는다\"고 적는다. 그 파일에 `app.fileserver.enabled`도 `app.file-export.enabled`도 없다(`142-...` §8.4e; `app.fileserver`로 걸리는 두 줄은 주석이다). 실효 기본값은 \"속성 부재 → `@ConditionalOnProperty` 미매치 → bean 없음\"이고 동작은 옳지만, 문서가 가리킨 자리에는 그 키가 없다. - README의 Tests 목록 첫 항목 `FilePublicationContractTest`는 이 leaf가 아니라 `application-core`에 있다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "file-transfer-and-storage/case/case-analysis-finding-a08-f003.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a08-f003" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a08-f003.txt" - ] - }, - { - "title": "발행 rename만 경로 기반이고, 그것을 지키는 것은 이 모듈이 \"근사에 불과하다\"고 적은 사전검사다", - "kind": "case", - "slug": "analysis-finding-a08-f004", - "readiness": "READY", - "source": [ - "`final/document.md#a08#L376`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "판정: P3. 실제 악용에는 스토리지 루트 안쪽 쓰기 권한이 필요하고, 이 leaf가 그 루트의 소유자·권한을 증명하는 것은 R2 경로(`LocalPersistentRootAttestor`)뿐이며 플랫폼 저장소 루트의 증명은 `app-bootstrap`의 startup validator 몫이다. 그래서 도달성은 배포 형상에 달려 있다. 심각도를 P3로 두는 이유는 그것이고, 그럼에도 기록하는 이유는 모듈 자신의 문서가 이 패턴을 명시적으로 불충분하다고 선언했다는 점이다. 수정은 발행 rename을 `SecureDirectoryWalk.inParentOf`로 옮겨 부모 서술자 상대 `move`를 쓰고, `sizeOf`를 `channels.readAttributes`로 바꾸는 것이다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "file-transfer-and-storage/case/case-analysis-finding-a08-f004.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a08-f004" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a08-f004.txt" - ] - }, - { - "title": "`TransferBufferPool.maxBorrowedBytes()`가 자기 회귀 test를 지목하는데 그 test가 읽지 않는다", - "kind": "case", - "slug": "a08-f005-transferbufferpool-maxborrowedbytes", - "readiness": "READY", - "source": [ - "`final/document.md#a08#L410`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`TransferBufferPool`은 대여 중 바이트의 최대치를 추적하고 javadoc에 이렇게 적는다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "file-transfer-and-storage/case/case-a08-f005-transferbufferpool-maxborrowedbytes.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a08-f005-transferbufferpool-maxborrowedbytes", - "a08-f005-transferbufferpool-maxborrowedbytes-peak" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a08-f005-transferbufferpool-maxborrowedbytes.txt", - "../../../final/evidence/raw/a08-f005-transferbufferpool-maxborrowedbytes-peak.txt" - ] - }, - { - "title": "production 판정이 두 개의 리터럴 프로파일 이름에 걸려 있다", - "kind": "case", - "slug": "analysis-finding-a09-f001", - "readiness": "READY", - "source": [ - "`final/document.md#a09#L106`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "이 저장소가 `application-prod.yml`을 싣고 있으므로 현재 형상에서는 맞는다. 그리고 `150-...` §8.2c에서 확인했듯 같은 방식으로 production을 판정하는 leaf는 이것 하나뿐이다 — 저장소 전체가 공유하는 production 판별 장치가 없다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "file-transfer-and-storage/case/case-analysis-finding-a09-f001.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a09-f001" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a09-f001.txt" - ] - }, - { - "title": "그러나 R0 경계가 문서에만 있고 compile 경로에서 닫히지 않는다", - "kind": "case", - "slug": "analysis-finding-a09-f004", - "readiness": "READY", - "source": [ - "`final/document.md#a09#L485`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "판정: P2. 데이터 위험은 없다 — 없는 port는 호출될 수 없다. 위험은 (a) 운영자가 켰다고 믿는 기능이 없다는 것과 (b) 아무도 쓰지 않는 서명 자격증명 핸들이 프로세스 수명 동안 살아 있다는 것이다. 수정은 셋 중 하나다: coordinator를 조건부로 조립하거나, R0인 동안 `DIRECT_UPLOAD`/`DIRECT_MULTIPART` 요구를 compile 단계에서 provider 종류와 무관하게 거부하거나, capability가 켜져도 presigner를 만들지 않도록 조립을 뒤로 미루거나.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "file-transfer-and-storage/case/case-analysis-finding-a09-f004.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a09-f004" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a09-f004.txt" - ] - }, - { - "title": "nonce replay 경계가 결과를 읽고 버린다", - "kind": "case", - "slug": "analysis-finding-a09-f006", - "readiness": "READY", - "source": [ - "`final/document.md#a09#L589`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`ClaimResult`는 `CLAIMED` / `EXACT_REPLAY` / `TERMINAL_REPLAY` 셋인데, 어느 값이든 발행은 그대로 진행된다. claim 결과가 바꾸는 것은 terminal 기록을 쓸지 여부뿐이다. 인터페이스 javadoc은 자신을 \"Durable compare-and-set nonce replay boundary\"라고 부르지만, 경계로서 무엇도 막지 않는다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "file-transfer-and-storage/case/case-analysis-finding-a09-f006.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a09-f006" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a09-f006.txt" - ] - }, - { - "title": "README readiness 표와 build.gradle 주석이 실제 소스와 어긋난다", - "kind": "case", - "slug": "a10-f001-readme", - "readiness": "READY", - "source": [ - "`final/document.md#a10#L121`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "판정: P2. 코드 결함이 아니라 문서 결함이지만 이 저장소 기준으로는 무겁다. 첫째, 이 leaf의 README는 \"readiness는 서로 다른 세 가지 질문이며 하나로 합치면 안 된다\"는 문장으로 시작하는, 정직한 readiness 보고를 자기 주제로 삼는 문서다. 둘째, 방향이 이례적이다 — 보통의 drift는 없는 것을 있다고 하는데 여기는 있는 것을 없다고 한다. fork가 이미 있는 4,900 LOC를 다시 구현하거나, 조립되지 않은 채 존재하는 코드의 존재 자체를 모르게 된다. 셋째, `RedisSdkAutoConfiguration`의 javadoc이 \"until this class existed the method had no production caller at all\"이라고 적는 것으로 보아 이 클래스가 README 문장보다 나중이다 — 조립이 진행됐는데 서술이 따라가지 않았다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "caching-and-redis/case/case-a10-f001-readme.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a10-f001-readme", - "a10-f001-readme-counts" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a10-f001-readme.txt", - "../../../final/evidence/raw/a10-f001-readme-counts.txt" - ] - }, - { - "title": "SDK가 선언한 두 진입점에 구현이 없다", - "kind": "case", - "slug": "analysis-finding-a10-f003", - "readiness": "READY", - "source": [ - "`final/document.md#a10#L251`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "판정: P2. 데이터 위험은 없다 — 없는 타입은 잘못된 답을 주지 않는다. 위험은 API 계약의 신뢰다: 이 leaf의 공개 표면 중 가장 먼저 읽히는 두 타입이 구현되지 않은 상태이고, 대칭 test가 그 사실을 가리지 못한다(인터페이스끼리만 비교하므로). sub-scope 01의 §5와 방향이 반대이면서 원인은 같다 — 조립이 절반이다. 수정은 이미 존재하는 26개 구현을 묶는 `LettuceRedisOperations` / `LettuceReactiveRedisOperations` 두 클래스를 추가하고, `ApiParityTest`에 \"두 facade는 구현을 가진다\"는 검사를 더하는 것이다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "caching-and-redis/case/case-analysis-finding-a10-f003.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a10-f003" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a10-f003.txt" - ] - }, - { - "title": "Pub/Sub 채널만 렌더 크기 검증을 받지 않는다", - "kind": "case", - "slug": "a10-f004-pub-sub", - "readiness": "READY", - "source": [ - "`final/document.md#a10#L263`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`RedisKeyRules.requireRenderedSize(...)`는 조립된 키 문자열이 설정된 최대 바이트를 넘지 않는지 본다. 세 형제 중 하나만 그것을 부른다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "caching-and-redis/case/case-a10-f004-pub-sub.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a10-f004-pub-sub", - "a10-f004-pub-sub-bound" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a10-f004-pub-sub.txt", - "../../../final/evidence/raw/a10-f004-pub-sub-bound.txt" - ] - }, - { - "title": "다중 키 fan-in 중 HyperLogLog `merge`만 budget이 없다", - "kind": "case", - "slug": "a10-f005-hyperloglog-merge", - "readiness": "READY", - "source": [ - "`final/document.md#a10#L277`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "같은 성격(여러 키를 읽어 하나에 쓰는, 비용이 입력 크기에 비례하는 연산)의 세 형제를 비교하면 요구가 다르다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "caching-and-redis/case/case-a10-f005-hyperloglog-merge.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a10-f005-hyperloglog-merge", - "a10-f005-hyperloglog-merge-scope", - "a10-f005-hyperloglog-merge-budget" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a10-f005-hyperloglog-merge.txt", - "../../../final/evidence/raw/a10-f005-hyperloglog-merge-scope.txt", - "../../../final/evidence/raw/a10-f005-hyperloglog-merge-budget.txt" - ] - }, - { - "title": "`requireIdentifier`의 다섯 검사 중 둘은 도달할 수 없다", - "kind": "case", - "slug": "a10-f006-requireidentifier", - "readiness": "READY", - "source": [ - "`final/document.md#a10#L375`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "판정: P3. 보안 효과는 그대로다 — 두 형태 모두 거부된다. 잃는 것은 진단 품질(운영자가 \"must not contain a mail address\" 대신 일반적인 문자 클래스 메시지를 본다)과, 두 분기가 실제로는 아무 일도 하지 않으면서 검증이 다섯 겹인 것처럼 보이게 만드는 점이다. 수정은 두 분기를 지우고 문자 클래스 메시지에 그 의도를 포함시키거나, 검사 순서를 뒤집어 구체적 형태를 먼저 판정하는 것이다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "caching-and-redis/case/case-a10-f006-requireidentifier.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a10-f006-requireidentifier", - "a10-f006-requireidentifier-branch" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a10-f006-requireidentifier.txt", - "../../../final/evidence/raw/a10-f006-requireidentifier-branch.txt" - ] - }, - { - "title": "패턴 구독의 R2 승인만 호출자가 아니라 배포에 대해 이루어진다", - "kind": "case", - "slug": "analysis-finding-a10-f007", - "readiness": "READY", - "source": [ - "`final/document.md#a10#L485`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "판정: P3. 배포 수준 게이트는 실재하고 네임스페이스 봉쇄도 있으므로 열린 구멍은 아니다. 기록하는 이유는 같은 위험 등급에 두 가지 다른 승인 모델이 적용되고 그 차이가 문서화돼 있지 않기 때문이다. 수정은 `patternSubscribe` 서명에 `AdvancedOperationPermit`을 추가하거나, javadoc에 \"배포 수준 승인\"임을 명시하는 것이다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "caching-and-redis/case/case-analysis-finding-a10-f007.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a10-f007" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a10-f007.txt" - ] - }, - { - "title": "permit 정책 이름이 세 곳에 문자열로 존재하고 교차 검사가 없다", - "kind": "case", - "slug": "analysis-finding-a10-f008", - "readiness": "READY", - "source": [ - "`final/document.md#a10#L502`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "판정: P3. 확정은 sub-scope 05로 이월한다 — `RedisCommandPolicyLoaderTest`가 정책 이름 집합을 검사하는지 그 sub-scope에서 확인한다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "caching-and-redis/case/case-analysis-finding-a10-f008.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a10-f008" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a10-f008.txt" - ] - }, - { - "title": "`close()`가 실패하면 drain 스케줄러 스레드가 남는다", - "kind": "case", - "slug": "a11-f001-close", - "readiness": "READY", - "source": [ - "`final/document.md#a11#L121`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "판정: P3. 스레드가 데몬이라 JVM 종료를 막지는 않고, 레지스트리당 하나이며, 닫기 실패라는 조건이 필요하다. 그러나 주석이 \"a thread per rotation cycle\"을 명시적 위험으로 적고 resource-bound suite가 그것을 잡으려 존재하는데, 정확히 그 누수가 실패 경로에 남아 있다. 수정은 스케줄러 종료를 `finally`로 옮기는 한 줄이다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "http-client-and-resilience/case/case-a11-f001-close.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a11-f001-close", - "a11-f001-close-leak" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a11-f001-close.txt", - "../../../final/evidence/raw/a11-f001-close-leak.txt" - ] - }, - { - "title": "`POOL_ROUTE_EXCEEDS_TOTAL` 위반 코드는 발화할 수 없다", - "kind": "case", - "slug": "a11-f002-pool-route-exceeds-total", - "readiness": "READY", - "source": [ - "`final/document.md#a11#L146`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "// ClientProfileValidator:192 if (profile.pool().maxConnectionsPerRoute() > profile.pool().maxTotalConnections()) { out.add(violation(\"POOL_ROUTE_EXCEEDS_TOTAL\", profile, \"pool.max-connections-per-route\")); } ```", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "http-client-and-resilience/case/case-a11-f002-pool-route-exceeds-total.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a11-f002-pool-route-exceeds-total", - "a11-f002-pool-route-exceeds-total-shape" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a11-f002-pool-route-exceeds-total.txt", - "../../../final/evidence/raw/a11-f002-pool-route-exceeds-total-shape.txt" - ] - }, - { - "title": "위반 코드 34종 중 22종이 어떤 test에서도 이름으로 확인되지 않는다", - "kind": "case", - "slug": "analysis-finding-a11-f003", - "readiness": "READY", - "source": [ - "`final/document.md#a11#L164`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`ClientProfileValidator`가 내는 코드는 34종이다. 저장소 전체의 `test`/`testkit` source set에서 그 문자열을 참조하는 파일 수를 세면(`168-...` §8.4b):", - "missing-verification": "test 두 개(`ClientProfileValidatorTest` 106줄)가 그룹으로 몇 개를 묶어 확인하지만(`rejectsSimpleFactoryAndUnacknowledgedHttp3AndJdkRoutePool`), 나머지 22종은 분기를 지워도 초록으로 남는다. 코드 자체는 현재 옳다 — 위험은 회귀다. **P3.** 수정은 `@ParameterizedTest`로 코드별 최소 케이스를 한 벌 놓는 것이고, 34종이 모두 결정적으로 정렬된 목록을 내므로 그 형태가 자연스럽다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "http-client-and-resilience/case/case-analysis-finding-a11-f003.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a11-f003" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a11-f003.txt" - ] - }, - { - "title": "`Number`가 허용 목록에 있어 가변 숫자 타입이 REPLAYABLE로 인증된다", - "kind": "case", - "slug": "a11-f004-number", - "readiness": "READY", - "source": [ - "`final/document.md#a11#L249`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "판정: P3. 도달성이 좁고(원자 카운터를 요청 DTO에 넣어야 한다), 검사 전체의 방향은 보수적이며, `aMutableValueIsOneShot` test가 일반적인 가변 객체는 잡는다. 기록하는 이유는 이 검사가 \"records, enums, strings, boxed primitives and immutable collection views replay; anything else is treated as one-shot\"라고 선언하는데 `Number` 한 줄이 그 선언보다 넓기 때문이다. 수정은 boxed primitive 여덟 종과 `BigInteger`/`BigDecimal`을 명시하거나, `java.util.concurrent.atomic` 패키지를 제외하는 것이다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "http-client-and-resilience/case/case-a11-f004-number.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a11-f004-number", - "a11-f004-number-bytes" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a11-f004-number.txt", - "../../../final/evidence/raw/a11-f004-number-bytes.txt" - ] - }, - { - "title": "`BoundedDataBufferFlux`의 두 연산자가 이름만 있고 아무것도 하지 않는다", - "kind": "case", - "slug": "a11-f006-boundeddatabufferflux", - "readiness": "READY", - "source": [ - "`final/document.md#a11#L424`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "판정: P3. 수정은 두 연산자를 지우고 javadoc이 `doOnDiscard`와 드라이버의 역할을 정확히 적게 하거나, 취소 경로에서 실제로 해제해야 할 것이 있다면 그것을 구현하는 것이다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "http-client-and-resilience/case/case-a11-f006-boundeddatabufferflux.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a11-f006-boundeddatabufferflux", - "a11-f006-boundeddatabufferflux-release" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a11-f006-boundeddatabufferflux.txt", - "../../../final/evidence/raw/a11-f006-boundeddatabufferflux-release.txt" - ] - }, - { - "title": "동적 대상 DNS 핀 능력 검사가 블로킹 오버로드에만 있다", - "kind": "case", - "slug": "a11-f007-dns", - "readiness": "READY", - "source": [ - "`final/document.md#a11#L583`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "판정: P3. 위험은 fork가 리액티브 전송을 추가하면서 `dynamicTargetStable=true, validatedDnsPinning=false`로 선언하는 경우 — 주석이 \"cannot serve a dynamic target no matter what its `dynamicTargetStable` flag says\"라고 못박은 정확히 그 조합이 리액티브 쪽에서는 통과한다. 수정은 같은 세 줄을 리액티브 오버로드에 복사하는 것이다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "http-client-and-resilience/case/case-a11-f007-dns.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a11-f007-dns", - "a11-f007-dns-overloads" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a11-f007-dns.txt", - "../../../final/evidence/raw/a11-f007-dns-overloads.txt" - ] - }, - { - "title": "`check`에 붙은 `verifyJsonSchemaRuntimeGraph`가 실행되면 실패한다", - "kind": "case", - "slug": "a12-f001-check-verifyjsonschemaruntimegraph", - "readiness": "READY", - "source": [ - "`final/document.md#a12#L82`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "판정: P2. 금지 조건 쪽(YAML 계열·Jackson 2 `core`/`databind` 부재)은 여전히 옳게 동작하지만, 필수 조건 쪽이 버전 드리프트로 고장 나 있어 게이트 전체가 통과할 수 없다. 결과는 이 저장소가 다른 곳에서 반복해 경계한 바로 그 상태다 — 붙어 있으나 초록일 수 없는 게이트는 사람들이 건너뛰는 법을 배우게 만든다. 수정은 필수 좌표에서 버전을 떼고 `group:name`만 확인하거나(닫힘 조건은 \"무엇이 없는가\"이지 \"어느 패치인가\"가 아니다), 잠금 파일에서 버전을 읽어 비교하는 것이다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "messaging-and-outbox/case/case-a12-f001-check-verifyjsonschemaruntimegraph.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a12-f001-check-verifyjsonschemaruntimegraph", - "a12-f001-check-verifyjsonschemaruntimegraph-run" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a12-f001-check-verifyjsonschemaruntimegraph.txt", - "../../../final/evidence/raw/a12-f001-check-verifyjsonschemaruntimegraph-run.txt" - ] - }, - { - "title": "README의 `jackson-databind` 부재 주장이 현재 상태와 어긋난다", - "kind": "case", - "slug": "a12-f002-jackson-databind", - "readiness": "READY", - "source": [ - "`final/document.md#a12#L118`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "판정: P3. 코드 결함은 아니다 — `OutboxEnvelopeJson`의 손수 짠 직렬화는 그 자체로 문제가 없다. 기록하는 이유는 그 선택의 근거로 적힌 사실이 더 이상 성립하지 않는다는 점이고, fork가 그 문장을 읽고 \"databind가 없다\"를 전제로 다른 결정을 내릴 수 있기 때문이다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "messaging-and-outbox/case/case-a12-f002-jackson-databind.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a12-f002-jackson-databind", - "a12-f002-jackson-databind-history" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a12-f002-jackson-databind.txt", - "../../../final/evidence/raw/a12-f002-jackson-databind-history.txt" - ] - }, - { - "title": "`AUTHENTICATION_FAILED`를 지우지 않는다는 `resumeHealthy`의 보장이, 관리자 평면에 노출된 2단계 시퀀스로 우회된다", - "kind": "case", - "slug": "a13-f002-authentication-failed-resumehealthy", - "readiness": "READY", - "source": [ - "`final/document.md#a13#L320`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`ProviderRuntime.resumeHealthy`는 자신이 지키는 성질을 javadoc에 명시한다:", - "missing-verification": "**테스트가 이것을 잡지 못하는 이유** — 해당 테스트는 세 전이를 **각각 새 런타임에서** 확인하고 합성을 확인하지 않는다:", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "notification-and-delivery/case/case-a13-f002-authentication-failed-resumehealthy.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a13-f002-authentication-failed-resumehealthy", - "a13-f002-authentication-failed-resumehealthy-sequence" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a13-f002-authentication-failed-resumehealthy.txt", - "../../../final/evidence/raw/a13-f002-authentication-failed-resumehealthy-sequence.txt" - ] - }, - { - "title": "\"모든 reveal은 감사된다\"고 선언한 `AccessContext`를 읽는 코드가 저장소에 하나도 없다", - "kind": "case", - "slug": "a13-f003-accesscontext", - "readiness": "READY", - "source": [ - "`final/document.md#a13#L430`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "// ContactPointProtector.java:11 / Decrypt a value for an audited purpose. */ ContactPointValue reveal(ProtectedContactPoint protectedValue, AccessContext context); ```", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "notification-and-delivery/case/case-a13-f003-accesscontext.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a13-f003-accesscontext", - "a13-f003-accesscontext-reveal" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a13-f003-accesscontext.txt", - "../../../final/evidence/raw/a13-f003-accesscontext-reveal.txt" - ] - }, - { - "title": "Thymeleaf 예외 메시지 삭제 가드가 프로덕션이 타지 않는 오버로드에만 있다", - "kind": "case", - "slug": "a13-f004-thymeleaf", - "readiness": "READY", - "source": [ - "`final/document.md#a13#L476`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`ThymeleafStringTemplateEngine`은 같은 클래스에 `render` 두 개를 갖는다. 모드 없는 쪽은 예외를 잡아 메시지를 버리고, 그 이유를 명시한다:", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "notification-and-delivery/case/case-a13-f004-thymeleaf.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a13-f004-thymeleaf", - "a13-f004-thymeleaf-messages" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a13-f004-thymeleaf.txt", - "../../../final/evidence/raw/a13-f004-thymeleaf-messages.txt" - ] - }, - { - "title": "음수 `Retry-After` 헤더가 throttle 결과 대신 `IllegalArgumentException`을 만든다", - "kind": "case", - "slug": "a13-f005-retry-after-illegalargumentexception", - "readiness": "READY", - "source": [ - "`final/document.md#a13#L636`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "파서는 `Long.parseLong`이 받아들이는 값을 그대로 `Duration`으로 만든다:", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "notification-and-delivery/case/case-a13-f005-retry-after-illegalargumentexception.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a13-f005-retry-after-illegalargumentexception", - "a13-f005-retry-after-illegalargumentexception-negative", - "a13-f005-retry-after-illegalargumentexception-unassembled", - "a13-f005-retry-after-illegalargumentexception-ambiguous" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a13-f005-retry-after-illegalargumentexception.txt", - "../../../final/evidence/raw/a13-f005-retry-after-illegalargumentexception-negative.txt", - "../../../final/evidence/raw/a13-f005-retry-after-illegalargumentexception-unassembled.txt", - "../../../final/evidence/raw/a13-f005-retry-after-illegalargumentexception-ambiguous.txt" - ] - }, - { - "title": "\"상한을 두고 읽는다\"는 본문 핸들러가 전부 읽은 뒤에 자른다", - "kind": "case", - "slug": "analysis-finding-a13-f007", - "readiness": "READY", - "source": [ - "`final/document.md#a13#L795`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`BodySubscribers.mapping(upstream, finisher)`의 finisher는 upstream이 완료된 뒤 그 결과에 적용된다. upstream은 `ofByteArray()`이고, 그것은 무제한으로 요청하여 본문 전체를 힙에 모은다. 잘라내기는 그 다음이다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "notification-and-delivery/case/case-analysis-finding-a13-f007.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a13-f007" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a13-f007.txt" - ] - }, - { - "title": "SigV4가 서명한 `host`에 포트가 없어, 기본 포트가 아닌 엔드포인트에서 서명이 어긋난다", - "kind": "case", - "slug": "a13-f008-host", - "readiness": "READY", - "source": [ - "`final/document.md#a13#L831`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "SigV4의 정규 요청은 실제로 전송되는 `Host` 헤더 값을 서명해야 하고, 기본이 아닌 포트는 그 값에 포함된다. 요청 자체는 `host` 헤더를 싣지 않으며(`JdkNotificationHttpGateway.RESTRICTED`가 거부하고 JDK가 URI에서 채운다), JDK는 `localhost:4566` 같은 값을 보낸다. 서명은 `localhost`에 대해 이루어졌다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "notification-and-delivery/case/case-a13-f008-host.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a13-f008-host", - "a13-f008-host-wire" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a13-f008-host.txt", - "../../../final/evidence/raw/a13-f008-host-wire.txt" - ] - }, - { - "title": "SigV4 서명 키 파생이 비밀을 지울 수 없는 `String`으로 승격시킨다", - "kind": "case", - "slug": "a13-f009-sigv4-string", - "readiness": "READY", - "source": [ - "`final/document.md#a13#L844`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`secretAccessKey`는 `NotificationSecretMaterialHandle`이 제공하는 지울 수 있는 가변 사본이다. 그 핸들의 존재 이유가 \"operation-scoped mutable secret copy that wipes itself on close\"이고, `close()`가 `Arrays.fill(bytes, (byte) 0)`을 한다. 이 한 줄이 그 바이트를 불변 `String`으로 복사하며, 그 `String`은 GC가 가져갈 때까지 힙에 남고 어떤 `close()`도 지울 수 없다. 힙 덤프 한 장이면 회수된다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "notification-and-delivery/case/case-a13-f009-sigv4-string.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a13-f009-sigv4-string", - "a13-f009-sigv4-string-heap" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a13-f009-sigv4-string.txt", - "../../../final/evidence/raw/a13-f009-sigv4-string-heap.txt" - ] - }, - { - "title": "FCM만 \"커밋 후 응답 손실 = ambiguous\" 규칙 밖에 있고, 그 FCM이 두 계약 집합 어디에도 없다", - "kind": "case", - "slug": "analysis-finding-a13-f010", - "readiness": "READY", - "source": [ - "`final/document.md#a13#L957`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`ProviderResults`의 존재 이유가 클래스 javadoc에 쓰여 있다:", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "notification-and-delivery/case/case-analysis-finding-a13-f010.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a13-f010" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a13-f010.txt" - ] - }, - { - "title": "공유 provider 계약이 8종 중 3종에서만 상속되고, 강제 장치가 없다", - "kind": "case", - "slug": "analysis-finding-a13-f011", - "readiness": "READY", - "source": [ - "`final/document.md#a13#L990`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "§28.1·§28.3. `ProviderAdapterContract`의 javadoc이 약속하는 성질(\"a new provider cannot be added without answering the same three questions\")을 지키는 장치가 없다. 이 저장소는 같은 실패 양식에 대해 `EndpointGuardCallSiteTest`와 `verifyNotificationApiSurface`라는 구조적 강제를 이미 두 번 만들었으므로, 형태는 이미 있다 — 적용되지 않았을 뿐이다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "notification-and-delivery/case/case-analysis-finding-a13-f011.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a13-f011" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a13-f011.txt" - ] - }, - { - "title": "`WebProblemSanitizer.alreadySafe`가 죽은 메서드이고 그 안의 조건도 죽어 있다", - "kind": "case", - "slug": "a14-f002-webproblemsanitizer-alreadysafe", - "readiness": "READY", - "source": [ - "`final/document.md#a14#L380`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "호출자 0(프로덕션·테스트 모두). 그리고 `!input.isBlank()`가 이미 통과했으므로 `input.trim()`은 비어 있을 수 없고, `toLowerCase`는 공백 여부를 바꾸지 않는다 — 삼항의 `REDACTED` 가지는 도달 불가다. javadoc이 약속하는 용도(\"for asserting a message is already safe\")를 수행하는 코드가 없다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "web-inbound-and-http-surface/case/case-a14-f002-webproblemsanitizer-alreadysafe.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a14-f002-webproblemsanitizer-alreadysafe", - "a14-f002-webproblemsanitizer-alreadysafe-branch" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a14-f002-webproblemsanitizer-alreadysafe.txt", - "../../../final/evidence/raw/a14-f002-webproblemsanitizer-alreadysafe-branch.txt" - ] - }, - { - "title": "플랫폼 요청 컨텍스트가 서블릿에는 생산자가 없고, 리액티브에는 익명 액터로 고정되어 있다", - "kind": "case", - "slug": "analysis-finding-a14-f003", - "readiness": "READY", - "source": [ - "`final/document.md#a14#L503`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "서블릿 절반. `WebMvcRequestContextHolder.store(...)`의 호출자가 저장소 전체에서 0이다. 그런데 그것을 읽는 쪽은 자동설정이 등록한다:", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "web-inbound-and-http-surface/case/case-analysis-finding-a14-f003.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a14-f003" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a14-f003.txt" - ] - }, - { - "title": "프레임워크 자유 신원 모델과 교차 테넌트 가드가 프로덕션에서 한 번도 참조되지 않는다", - "kind": "case", - "slug": "analysis-finding-a14-f004", - "readiness": "READY", - "source": [ - "`final/document.md#a14#L562`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "§11.1. `security` 패키지 11개 파일이 서로만 참조하고 바깥에서 들어오는 화살표가 없다. `AuthenticationView`를 만드는 프로덕션 코드가 없으므로 `WebSecurityContextBridge.resolve(...)`가 호출될 수 있는 상태 자체가 없다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "web-inbound-and-http-surface/case/case-analysis-finding-a14-f004.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a14-f004" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a14-f004.txt" - ] - }, - { - "title": "`publicPaths`가 `RestrictedPathRule`보다 먼저 등록되어, 넓은 공개 경로 하나가 관리 평면 규칙을 조용히 덮는다", - "kind": "case", - "slug": "a14-f005-publicpaths-restrictedpathrule", - "readiness": "READY", - "source": [ - "`final/document.md#a14#L582`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "§11.3. `RestrictedPathRule`의 javadoc은 이 규칙이 존재하는 이유를 \"an application-level policy consulted later cannot recover from a transport that already let the request through\"로 설명한다. 그런데 `permitAll(publicPaths)`이 그 규칙보다 먼저 등록되어 정확히 그 일을 한다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "web-inbound-and-http-surface/case/case-a14-f005-publicpaths-restrictedpathrule.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a14-f005-publicpaths-restrictedpathrule", - "a14-f005-publicpaths-restrictedpathrule-chain", - "a14-f005-publicpaths-restrictedpathrule-mgmtport" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a14-f005-publicpaths-restrictedpathrule.txt", - "../../../final/evidence/raw/a14-f005-publicpaths-restrictedpathrule-chain.txt", - "../../../final/evidence/raw/a14-f005-publicpaths-restrictedpathrule-mgmtport.txt" - ] - }, - { - "title": "용량 보호 계층 전체(41 main files)가 자기 테스트 픽스처 안에서만 실행된다", - "kind": "case", - "slug": "analysis-finding-a14-f006", - "readiness": "READY", - "source": [ - "`final/document.md#a14#L677`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "즉 이 플랫폼을 그대로 배포하면 요청 본문 크기 상한도, 응답 크기 상한도, 요청 데드라인도, 동시성 상한도, 큐 상한도 없다. `WebRequestBudget.standard()`가 정의하고 `WebBudgetCatalog`가 담고 있는 값들은 아무도 읽지 않는다(§15.3).", - "missing-verification": "**실패 시나리오** — 배포된 API에 무제한 청크 본문이 도착한다. `WebMvcBudgetFilter`가 필터 체인에 없으므로 `BoundedHttpServletRequest`가 스트림을 감싸지 않고, `WebBudgetMeter`가 바이트를 세지 않는다. 컨테이너 기본값(Tomcat `maxPostSize`는 `multipart/form-data`와 폼 인코딩에만 적용되고 임의 본문에는 적용되지 않는다) 외에 상한이 없다. 같은 요청에 대해 동시성 상한도 없으므로 `SemaphoreAdmissionController`가 내기로 되어 있던 503도 나오지 않는다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "web-inbound-and-http-surface/case/case-analysis-finding-a14-f006.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a14-f006" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a14-f006.txt" - ] - }, - { - "title": "리액티브 전송에는 속도 제한 경로가 하나도 없다", - "kind": "case", - "slug": "analysis-finding-a14-f007", - "readiness": "READY", - "source": [ - "`final/document.md#a14#L701`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "§15.2. 배선된 유일한 속도 제한기 `RateLimitInterceptor`는 `WebMvcConfigurer.addInterceptors`로 붙는 MVC 전용 장치다. `WebFluxThrottleFilter`가 리액티브 대응물이지만 등록되지 않는다(§16.1). `webflux/autoconfigure/WebFluxPlatformAutoConfiguration`의 11개 빈에도 없다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "web-inbound-and-http-surface/case/case-analysis-finding-a14-f007.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a14-f007" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a14-f007.txt" - ] - }, - { - "title": "멱등 실행 계층과 durable-operation 표면이 픽스처에서만 조립된다", - "kind": "case", - "slug": "analysis-finding-a14-f008", - "readiness": "READY", - "source": [ - "`final/document.md#a14#L793`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "§19.1·§19.2. 이 sub-scope에서 프로덕션 컨텍스트에 들어가는 것은 `WebExecutionEvidenceTracker`(두 요청 필터가 만든다)와 빈 `InMemoryWebOperationCatalog` 둘뿐이다. 나머지 38개 main 파일 — 게이트, 두 invoker, 응답 writer, 지문 공장, 헤더 정책, 명령 인코더, 승인 판정, 응답 계획, 코덱, 그리고 `operationasync` 9종 전부 — 는 테스트와 testkit에서만 생성된다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "web-inbound-and-http-surface/case/case-analysis-finding-a14-f008.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a14-f008" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a14-f008.txt" - ] - }, - { - "title": "의미 지문이 길이 프레이밍 없이 구분자로 만들어진다", - "kind": "case", - "slug": "analysis-finding-a14-f009", - "readiness": "READY", - "source": [ - "`final/document.md#a14#L807`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "§19.4. 경로 변수 값과 헤더 값이 이스케이프 없이 구분자로 이어붙는다. 값 자체에 그 구분자가 들어가면(퍼센트 인코딩 `%1F`를 Spring이 디코딩해 `@PathVariable`로 전달한다) 서로 다른 두 요청이 같은 정규 문자열을 만들 수 있다 — 경로 변수가 둘 이상인 연산에서 하나를 통제하면 구성 가능하다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "web-inbound-and-http-surface/case/case-analysis-finding-a14-f009.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a14-f009" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a14-f009.txt" - ] - }, - { - "title": "배선된 캐시 필터의 `no-store`가 배선된 조건부 읽기 경로를 무력화하고, 둘을 조정하려고 만든 패키지는 참조 0이다", - "kind": "case", - "slug": "a14-f010-no-store", - "readiness": "READY", - "source": [ - "`final/document.md#a14#L899`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`CacheControlFilter`가 모든 응답에 `Cache-Control: no-store`를 붙인다. RFC 9111에서 `no-store`는 \"어떤 캐시에도 저장하지 말라\"는 지시다. 규격을 지키는 클라이언트는 응답을 보관하지 않으므로, 나중에 그 리소스에 대해 `If-None-Match`를 보낼 근거(저장된 표현과 그 ETag)를 갖지 못한다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "web-inbound-and-http-surface/case/case-a14-f010-no-store.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a14-f010-no-store", - "a14-f010-no-store-conditional" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a14-f010-no-store.txt", - "../../../final/evidence/raw/a14-f010-no-store-conditional.txt" - ] - }, - { - "title": "`maxArrayElements`가 선언만 되고 강제되지 않으며, 바이트 예산 백스톱도 없다", - "kind": "case", - "slug": "a14-f011-maxarrayelements", - "readiness": "READY", - "source": [ - "`final/document.md#a14#L1006`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "읽는 코드가 저장소 전체에 0개다(§27.1). Jackson 3의 `StreamReadConstraints`에는 배열 원소 수 상한이 없으므로 `BoundedJsonFactory`가 넘길 자리도 없고, 매퍼 쪽에서도 검사하지 않는다.", - "missing-verification": "**백스톱이 없다.** 이 위험을 막을 상위 장치가 `WebMvcBudgetFilter`의 요청 바이트 상한인데, SS4에서 확인했듯 그 필터는 등록되지 않는다(§16.1). 서블릿 컨테이너의 기본값도 임의 본문에는 적용되지 않는다. 따라서 지금 이 플랫폼에는 **JSON 배열 원소 수에 대한 상한이 어느 계층에도 없다.**", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "web-inbound-and-http-surface/case/case-a14-f011-maxarrayelements.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a14-f011-maxarrayelements", - "a14-f011-maxarrayelements-converter", - "a14-f011-maxarrayelements-parser" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a14-f011-maxarrayelements.txt", - "../../../final/evidence/raw/a14-f011-maxarrayelements-converter.txt", - "../../../final/evidence/raw/a14-f011-maxarrayelements-parser.txt" - ] - }, - { - "title": "선언된 Advanced 능력 11개 중 9개는 켜는 방법이 없다", - "kind": "case", - "slug": "a14-f014-advanced", - "readiness": "READY", - "source": [ - "`final/document.md#a14#L1267`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "§35.1. `webAdvancedTest` 레인이 이 능력들을 전부 돌리고(`web-advanced-nightly.yml:46` · `web-advanced-release.yml:53`), `WebAdvancedRollbackIT`가 능력마다 플래그를 켰다 껐다 하며 롤백을 검증한다. 그러나 그 검증은 `WebAdvancedFeatureFlags.of(feature)`라는 테스트 전용 값에 대한 것이고, 배포가 실제로 조작할 수 있는 스위치는 두 개뿐이다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "web-inbound-and-http-surface/case/case-a14-f014-advanced.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a14-f014-advanced", - "a14-f014-advanced-switches" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a14-f014-advanced.txt", - "../../../final/evidence/raw/a14-f014-advanced-switches.txt" - ] - }, - { - "title": "`VirtualThreadProfile.propertyName()`이 아무것도 게이트하지 않는 이름을 반환한다", - "kind": "case", - "slug": "a14-f015-virtualthreadprofile-propertyname", - "readiness": "READY", - "source": [ - "`final/document.md#a14#L1279`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "§35.3. `VirtualThreadProfile:75`는 `\"backend.web.advanced.virtual-threads.enabled\"`를 하드코딩한다. 실제 게이트는 `mvc-virtual-threads`이고, 같은 능력에 대해 enum이 계산하는 이름도 `mvc-virtual-threads`다. 이 메서드는 호출자가 0이므로 지금 오작동을 만들지는 않지만, \"The property that turns this on\"이라는 javadoc과 함께 잘못된 이름을 발행한다 — 운영자가 이 문서를 보고 설정하면 켜지지 않는다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "web-inbound-and-http-surface/case/case-a14-f015-virtualthreadprofile-propertyname.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a14-f015-virtualthreadprofile-propertyname", - "a14-f015-virtualthreadprofile-propertyname-bind" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a14-f015-virtualthreadprofile-propertyname.txt", - "../../../final/evidence/raw/a14-f015-virtualthreadprofile-propertyname-bind.txt" - ] - }, - { - "title": "이 leaf의 리액티브 절반 29개 파일은 어떤 출하 배포에서도 활성화될 수 없다", - "kind": "case", - "slug": "analysis-finding-a14-f016", - "readiness": "READY", - "source": [ - "`final/document.md#a14#L1378`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`@ConditionalOnWebApplication(type = REACTIVE)`는 Spring Boot의 `WebApplicationType`이 `REACTIVE`일 때만 참이다. `deduceFromClasspath()`는 `DispatcherServlet`과 `ServletContainerInitializer`가 있으면 WebFlux가 함께 있어도 `SERVLET`을 고른다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "web-inbound-and-http-surface/case/case-analysis-finding-a14-f016.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a14-f016" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a14-f016.txt" - ] - }, - { - "title": "`SpringMvcRouteInventoryCollector` 138줄에 참조가 하나도 없다", - "kind": "case", - "slug": "a14-f017-springmvcrouteinventorycollector", - "readiness": "READY", - "source": [ - "`final/document.md#a14#L1494`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "저장소 전체에서 이 타입 이름이 등장하는 곳은 자기 파일의 클래스 선언과 생성자 두 줄뿐이다. 테스트도 없다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "web-inbound-and-http-surface/case/case-a14-f017-springmvcrouteinventorycollector.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a14-f017-springmvcrouteinventorycollector", - "a14-f017-springmvcrouteinventorycollector-run" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a14-f017-springmvcrouteinventorycollector.txt", - "../../../final/evidence/raw/a14-f017-springmvcrouteinventorycollector-run.txt" - ] - }, - { - "title": "`WebPlatformStartupValidator`가 시작 시 실행되지 않는다", - "kind": "case", - "slug": "a14-f018-webplatformstartupvalidator", - "readiness": "READY", - "source": [ - "`final/document.md#a14#L1500`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "§43.1·§43.2. 이름이 약속하는 시점에 아무도 부르지 않는다. 같은 leaf의 fileserver 하위 트리는 같은 종류의 시작 검증을 app-bootstrap의 `@Bean`으로 연결했고(SS10 §39.1), 그 근거를 \"Better to refuse to start\"로 적었다. 플랫폼 쪽 검증기에는 그 연결이 없다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "web-inbound-and-http-surface/case/case-a14-f018-webplatformstartupvalidator.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a14-f018-webplatformstartupvalidator", - "a14-f018-webplatformstartupvalidator-run" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a14-f018-webplatformstartupvalidator.txt", - "../../../final/evidence/raw/a14-f018-webplatformstartupvalidator-run.txt" - ] - }, - { - "title": "크로스 스택 게이트가 검증하는 조립은 픽스처의 조립이고, 플랫폼의 조립이 아니다", - "kind": "case", - "slug": "analysis-finding-a14-f019", - "readiness": "READY", - "source": [ - "`final/document.md#a14#L1575`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "이 leaf는 이 저장소에서 가장 정교한 검증 장치를 갖고 있다 — 여섯 소스셋, 다섯 커스텀 레인, 세 런타임 패리티 비교, 실제 Nginx 컨테이너, 실제 소켓 고장 주입. 그리고 §47.1이 보여주듯 그 장치가 세우는 애플리케이션은 픽스처 애플리케이션이다.", - "missing-verification": "이 leaf는 이 저장소에서 가장 정교한 검증 장치를 갖고 있다 — 여섯 소스셋, 다섯 커스텀 레인, 세 런타임 패리티 비교, 실제 Nginx 컨테이너, 실제 소켓 고장 주입. 그리고 §47.1이 보여주듯 **그 장치가 세우는 애플리케이션은 픽스처 애플리케이션이다.**", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "web-inbound-and-http-surface/case/case-analysis-finding-a14-f019.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a14-f019" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a14-f019.txt" - ] - }, - { - "title": "원인 사슬 순회가 2-순환에서 무한 루프에 빠지고, 저장소는 이미 그 사례를 이름으로 적어 두었다", - "kind": "case", - "slug": "analysis-finding-a15-f001", - "readiness": "READY", - "source": [ - "`final/document.md#a15#L171`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "나머지 세 지점의 영향도 — `MvcDisconnectDetector`와 `WebFluxDisconnectDetector`는 요청 처리 중 클라이언트 연결 끊김을 판정하는 곳이고, `TransactionRetryClassifier`는 트랜잭션 재시도 여부를 판정하는 곳이다. 셋 다 요청 스레드 위에서 실행된다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "grpc-and-streaming/case/case-analysis-finding-a15-f001.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a15-f001" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a15-f001.txt" - ] - }, - { - "title": "설정 바인딩이 마스터 스위치 밖에서 일어난다. 컴포지션 루트의 자기 규칙과 어긋난다", - "kind": "case", - "slug": "analysis-finding-a15-f002", - "readiness": "READY", - "source": [ - "`final/document.md#a15#L187`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "§3.4. `GrpcServerProperties`는 두 경로로 등록된다 — `GrpcServerConfig`의 `@EnableConfigurationProperties`(게이트 안쪽)와 `CaSkeletonApplication`의 `@ConfigurationPropertiesScan`(게이트 바깥). 후자가 있으면 `ca-skeleton.grpc.enabled`와 무관하게 바인딩이 일어난다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "grpc-and-streaming/case/case-analysis-finding-a15-f002.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a15-f002" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a15-f002.txt" - ] - }, - { - "title": "스키마 조립·계약 정체성·해시 사슬이 통째로 미배선이고, 그것을 발행할 액추에이터 엔드포인트도 등록되지 않는다", - "kind": "case", - "slug": "analysis-finding-a16-f001", - "readiness": "READY", - "source": [ - "`final/document.md#a16#L258`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "1. 결정적 병합 순서. SDL 조각의 정렬을 조립기가 강제하도록 설계돼 있고(§7.4), 실제로는 Spring GraphQL의 탐색 순서를 그대로 쓴다. 조각이 하나(`skeleton.graphqls`)뿐인 지금은 무해하지만, adopter가 자기 `.graphqls`를 추가하는 순간 — 그것이 이 leaf의 문서화된 확장 방식이다 — 충돌 선언의 승자와 스키마 해시가 패키징 방식에 따라 달라질 수 있다. 2. 네 부분 계약 정체성. `GraphQlSchemaContract`가 \"해시만으로는 호환성을 판정할 수 없다\"는 판단을 타입으로 만들었는데, 그 타입을 만드는 코드가 없다. 호환성 판정이 필요한 곳(릴리스 게이트)은 `compat`의 비교기를 직접 쓴다. 3. 운영 가시성. `GraphQlPlatformActuatorEndpoint.report()`가 배포된 스키마 해시 · 실행 프로파일 · 배포 모드 · 활성 능력 · 등록된 연산/페치 프로파일 수를 하나의 보고서로 낸다. 등록되지 않으므로 운영자가 \"이 배포가 무엇을 켜고 있는가\"를 물을 표면이 없다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "graphql-surface/case/case-analysis-finding-a16-f001.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a16-f001" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a16-f001.txt" - ] - }, - { - "title": "`@oneOf` 게이트와 런타임 검증기가 미배선이고, \"플랫폼이 강제한다\"는 서술이 그것을 넘어선다", - "kind": "case", - "slug": "a16-f002-oneof", - "readiness": "READY", - "source": [ - "`final/document.md#a16#L281`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`GraphQlOneOfPolicy`의 클래스 javadoc은 \"The September 2025 `@oneOf` input rules the platform enforces\"로 시작한다. 강제하는 두 코드 — 시작 게이트와 런타임 검증기 — 는 프로덕션 호출자가 0이다.", - "missing-verification": "기록하는 것은 두 가지다: (a) 플랫폼 계층의 강제가 서술과 달리 존재하지 않는다, (b) `GraphQlOneOfSchemaGate.verify(sdl)`가 확인하는 것은 라이브러리가 확인하지 않는 부분(멤버가 전부 nullable이고 기본값이 없어야 한다는 **선언 시점** 규칙)이므로, adopter가 잘못된 `@oneOf` 입력 타입을 선언하면 시작 시점이 아니라 첫 요청에서 드러난다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "graphql-surface/case/case-a16-f002-oneof.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a16-f002-oneof", - "a16-f002-oneof-run" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a16-f002-oneof.txt", - "../../../final/evidence/raw/a16-f002-oneof-run.txt" - ] - }, - { - "title": "5계층 예산 모델에서 요청 계층만 강제되고, 나머지 파생이 전부 미배선이다", - "kind": "case", - "slug": "analysis-finding-a16-f003", - "readiness": "READY", - "source": [ - "`final/document.md#a16#L382`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`GraphQlDeadlinePropagator`의 javadoc이 계층 분리의 이유를 정확히 적는다:", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "graphql-surface/case/case-analysis-finding-a16-f003.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a16-f003" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a16-f003.txt" - ] - }, - { - "title": "연산 이름 정책의 두 구현 중 하나만 배선되고, 미배선 쪽만 `GraphQlOperationNamePolicy`를 쓴다", - "kind": "case", - "slug": "a16-f004-graphqloperationnamepolicy", - "readiness": "READY", - "source": [ - "`final/document.md#a16#L403`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "§11.2. 익명 연산 거부는 배선된 `GraphQlOperationSelectionHandler`가 수행하므로 강제 자체는 존재한다. 기록하는 것은 정책 객체의 이원화다 — `GraphQlOperationNamePolicy`(85줄)의 참조자가 미배선 인터셉터와 자기 자신뿐이고, 배선된 핸들러는 별개의 `policy`를 쓴다. 두 정책이 \"이름 있는 연산을 요구하는가\"에 대해 서로 다른 답을 낼 수 있는 구조이며, 지금은 한쪽만 답한다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "graphql-surface/case/case-a16-f004-graphqloperationnamepolicy.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a16-f004-graphqloperationnamepolicy", - "a16-f004-graphqloperationnamepolicy-run" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a16-f004-graphqloperationnamepolicy.txt", - "../../../final/evidence/raw/a16-f004-graphqloperationnamepolicy-run.txt" - ] - }, - { - "title": "설정으로 정한 파서 한계가 graphql-java에 설치되지 않는다", - "kind": "case", - "slug": "analysis-finding-a16-f005", - "readiness": "READY", - "source": [ - "`final/document.md#a16#L511`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "파서 계층은 GraphQL DoS 방어의 첫 번째 관문이다 — 복잡도 계산도 구조 분석도 문서를 파싱한 뒤에 일어나므로, 파싱 자체를 폭발시키는 문서는 그 앞에서 막아야 한다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "graphql-surface/case/case-analysis-finding-a16-f005.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a16-f005" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a16-f005.txt" - ] - }, - { - "title": "프로파일별 정책 매니페스트가 미배선이라, 자격에서 해석된 프로파일이 아무 예산도 선택하지 않는다", - "kind": "case", - "slug": "analysis-finding-a16-f006", - "readiness": "READY", - "source": [ - "`final/document.md#a16#L525`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "§15.2. 클라이언트 프로파일은 검증된 principal에서 정확히 해석되고 요청 컨텍스트에 실린다. 그리고 그 값이 선택하는 것은 캐시 키와 지표 태그뿐이다 — 정책은 프로파일과 무관하게 단일 빈이다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "graphql-surface/case/case-analysis-finding-a16-f006.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a16-f006" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a16-f006.txt" - ] - }, - { - "title": "파싱·검증 실패에 플랫폼 매퍼가 없다", - "kind": "case", - "slug": "analysis-finding-a16-f008", - "readiness": "READY", - "source": [ - "`final/document.md#a16#L680`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "§19.3. `GraphQlRequestErrorMapper`(57)가 그 목적으로 존재하고 미배선이다. 배선된 두 매퍼(`GraphQlExceptionResolver` · `GraphQlWireErrorMapper`)는 각각 리졸버 예외와 플랫폼 거부를 덮는다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "graphql-surface/case/case-analysis-finding-a16-f008.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a16-f008" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a16-f008.txt" - ] - }, - { - "title": "기계가 읽는 능력 매니페스트와 사람이 읽는 등급표가 커서 서명에 대해 다르게 답한다", - "kind": "case", - "slug": "analysis-finding-a16-f011", - "readiness": "READY", - "source": [ - "`final/document.md#a16#L889`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "즉 이 매니페스트는 \"Stable 스타터에서 켜도 되는가\"를 판정한다. cursor 서명은 켤 수 있는 것으로 판정되고, 켜는 코드는 없다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "graphql-surface/case/case-analysis-finding-a16-f011.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a16-f011" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a16-f011.txt" - ] - }, - { - "title": "\"기본 비활성\"은 존재하지 않는 스위치의 기본값을 서술한다", - "kind": "case", - "slug": "analysis-finding-a16-f012", - "readiness": "READY", - "source": [ - "`final/document.md#a16#L1002`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "권고 — 그 문단을 등급에 맞춘다: \"Advanced capability 는 현재 `modelled` 등급이며 활성화 경로가 없다. `GraphQlAdvancedFeatureFlags`·`GraphQlAdvancedModuleGuard`는 그 경로가 생길 때 쓸 판정 모델이다.\"", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "graphql-surface/case/case-analysis-finding-a16-f012.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a16-f012" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a16-f012.txt" - ] - }, - { - "title": "연결 티켓·origin 정책·메시지 권한·연결 예산이 요청 경로 밖이고, 그중 일부는 STOMP 어댑터가 다른 방식으로 대체한다", - "kind": "case", - "slug": "a17-f002-stomp", - "readiness": "READY", - "source": [ - "`final/document.md#a17#L273`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "§12.1·§12.2. 실제 배포에서 적용되는 보안은 `stomp` 패키지의 두 인터셉터이고, 그것은 CLAUDE.md가 서술하는 범위다(\"HTTP-handshake principal enforcement and client-inbound STOMP destination authorization\").", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "websocket-and-realtime/case/case-a17-f002-stomp.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a17-f002-stomp" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a17-f002-stomp.txt" - ] - }, - { - "title": "능력 프로퍼티 이름을 만드는 코드와 실제 게이트가 다른 접두사를 쓴다", - "kind": "case", - "slug": "analysis-finding-a17-f003", - "readiness": "READY", - "source": [ - "`final/document.md#a17#L426`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "§21.2. `WebSocketAdvancedCapability.propertyName()`이 반환하는 `backend.websocket.advanced.*`를 읽는 `@ConditionalOnProperty`가 없다. 운영자가 그 메서드가 알려 주는 키를 설정하면 아무 일도 일어나지 않고, 실제로 능력을 켜는 키는 `app.websocket-platform.advanced.*`다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "websocket-and-realtime/case/case-analysis-finding-a17-f003.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a17-f003" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a17-f003.txt" - ] - }, - { - "title": "출하되는 web 어댑터의 스위치가 활성화 모델 밖에 있다", - "kind": "case", - "slug": "analysis-finding-a18-f001", - "readiness": "READY", - "source": [ - "`final/document.md#a18#L214`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "§4.1. `adapter-inbound-web`은 두 런타임 멤버이므로 build-only 예외에 해당하지 않는다. 그런데 그 네 개 스위치가 조건 안의 문자열 리터럴로만 존재해 `MasterSwitch`의 javadoc이 경계하는 상태다 — \"Spread across conditions as string literals, a rename becomes a silent activation change.\"", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "composition-root-and-bootstrap/case/case-analysis-finding-a18-f001.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a18-f001" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a18-f001.txt" - ] - }, - { - "title": "실패는 환경 원인이며, 그 테스트의 도구 가드가 불완전하다", - "kind": "case", - "slug": "analysis-finding-a18-f002", - "readiness": "READY", - "source": [ - "`final/document.md#a18#L549`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "저장소 결함이 아니다. 분석 컨테이너에 `jq`가 없고, 위임된 스크립트가 그것을 요구하며 exit 78로 정직하게 실패한다.", - "missing-verification": "**저장소 결함이 아니다.** 분석 컨테이너에 `jq`가 없고, 위임된 스크립트가 그것을 요구하며 exit 78로 정직하게 실패한다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "composition-root-and-bootstrap/case/case-analysis-finding-a18-f002.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a18-f002" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a18-f002.txt" - ] - }, - { - "title": "capability 12개 중 main 코드가 읽는 것은 3개, 거부하는 것은 1개", - "kind": "case", - "slug": "analysis-finding-a19-f001", - "readiness": "READY", - "source": [ - "`final/document.md#a19#L231`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`MessagingCapabilities`의 클래스 javadoc이 이 record의 계약을 선언한다:", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "messaging-and-outbox/case/case-analysis-finding-a19-f001.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a19-f001" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a19-f001.txt" - ] - }, - { - "title": "8개 profile validator 중 조립에서 실행되는 것은 3개", - "kind": "case", - "slug": "analysis-finding-a19-f002", - "readiness": "READY", - "source": [ - "`final/document.md#a19#L288`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`StartupProfileValidation`의 javadoc이 이미 한 번 고쳐진 같은 결함을 서술한다:", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "messaging-and-outbox/case/case-analysis-finding-a19-f002.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a19-f002" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a19-f002.txt" - ] - }, - { - "title": "`messaging-reliability-api`는 main 13파일 · 817 LOC에 테스트가 0개다", - "kind": "case", - "slug": "a19-f003-messaging-reliability-api", - "readiness": "READY", - "source": [ - "`final/document.md#a19#L325`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "sub-scope 01의 네 leaf 중 유일하게 `src/test`가 없다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "messaging-and-outbox/case/case-a19-f003-messaging-reliability-api.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a19-f003-messaging-reliability-api" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a19-f003-messaging-reliability-api.txt" - ] - }, - { - "title": "스키마 호환성 검증기는 출하 leaf에 있고, main 코드에서 호출되지 않는다", - "kind": "case", - "slug": "analysis-finding-a19-f004", - "readiness": "READY", - "source": [ - "`final/document.md#a19#L390`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`SchemaCompatibilityValidator`(`messaging-schema-api`, 출하)의 main 참조는 0건이다. 테스트 1개뿐.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "messaging-and-outbox/case/case-analysis-finding-a19-f004.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a19-f004" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a19-f004.txt" - ] - }, - { - "title": "호환성 게이트를 가진 두 포맷은 build-only이고, 출하되는 유일한 코덱에는 게이트가 없다", - "kind": "case", - "slug": "analysis-finding-a19-f005", - "readiness": "READY", - "source": [ - "`final/document.md#a19#L415`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "위험 방향이 뒤집혀 있다. 스키마 진화 검사가 존재하는 두 포맷(Avro·Protobuf)은 어떤 런타임에도 오르지 않고, 실제로 wire에 바이트를 쓰는 유일한 코덱(JSON)에는 포맷 수준의 호환성 게이트가 없다. §4.3의 포맷 독립 검증기(`SchemaCompatibilityValidator`)가 그 공백을 메울 자리인데 그것도 호출되지 않는다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "messaging-and-outbox/case/case-analysis-finding-a19-f005.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a19-f005" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a19-f005.txt" - ] - }, - { - "title": "`messaging-cloudevents`는 출하 leaf이고 starter의 의존이며 소비자가 없다", - "kind": "case", - "slug": "a19-f006-messaging-cloudevents", - "readiness": "READY", - "source": [ - "`final/document.md#a19#L431`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "그런데 main 코드에서 `DefaultCloudEventMapper`를 만드는 곳은 0곳이고, `CloudEventMapper`·`CloudEventExtensions`를 참조하는 main 파일은 `DefaultCloudEventMapper` 자신뿐이다. 자동설정 28개 클래스 어디에도 CloudEvents 이름이 없다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "messaging-and-outbox/case/case-a19-f006-messaging-cloudevents.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a19-f006-messaging-cloudevents" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a19-f006-messaging-cloudevents.txt" - ] - }, - { - "title": "브로커 ACL 매니페스트의 자기 점검이 존재하지 않는다", - "kind": "case", - "slug": "a19-f008-acl", - "readiness": "READY", - "source": [ - "`final/document.md#a19#L495`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`BrokerAclManifest`(`messaging-security`, 출하) — main 참조 0건, 테스트 1건.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "messaging-and-outbox/case/case-a19-f008-acl.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a19-f008-acl" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a19-f008-acl.txt" - ] - }, - { - "title": "접근 검사가 두 갈래로 존재하고, 조립된 쪽이 진단이 약한 쪽이다 (§8.3)", - "kind": "case", - "slug": "analysis-finding-a19-f009", - "readiness": "READY", - "source": [ - "`final/document.md#a19#L511`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "(a) 조립된 쪽 — `DefaultMessagePublisher`가 `DestinationAccessPolicy`를 직접 호출:", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "messaging-and-outbox/case/case-analysis-finding-a19-f009.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a19-f009" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a19-f009.txt" - ] - }, - { - "title": "지원 매트릭스가 \"모든 messaging leaf는 build-only\"라고 적고, 가족 권위 문서는 그 문장이 틀렸다고 이미 기록했다", - "kind": "case", - "slug": "analysis-finding-a19-f013", - "readiness": "READY", - "source": [ - "`final/document.md#a19#L682`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "이 드리프트의 실질적 무게는 이 문서 전체의 심각도 판정 축과 같다(§1.1). 지원 매트릭스만 읽은 운영자는 messaging이 아무것도 출하하지 않는다고 결론 내리는데, 실제로는 `messaging-kafka`·`messaging-rabbit`·`messaging-spring-boot-starter`·`messaging-security`·`messaging-observability`·`messaging-cloudevents`·outbox/inbox/claim-check·admin plane이 전부 `app-bootstrap` 아티팩트에 실려 있다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "messaging-and-outbox/case/case-analysis-finding-a19-f013.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a19-f013" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a19-f013.txt" - ] - }, - { - "title": "한 아티팩트 안의 서로 모르는 Kafka 스택 두 개 (MSG-015, 가족 문서가 미해결로 표시)", - "kind": "case", - "slug": "a19-f014-kafka-msg", - "readiness": "READY", - "source": [ - "`final/document.md#a19#L698`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`src/messaging/CLAUDE.md`가 MSG-015를 P0 미해결로 들고 있다. 현재 상태를 코드로 확인했다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "messaging-and-outbox/case/case-a19-f014-kafka-msg.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a19-f014-kafka-msg", - "a19-f014-kafka-msg-probe" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a19-f014-kafka-msg.txt", - "../../../final/evidence/raw/a19-f014-kafka-msg-probe.txt" - ] - }, - { - "title": "`CompatibilityMatrix`에 `EXTENSION` 등급이 있고 항목이 없으며, bridge leaf가 표 밖에 있다", - "kind": "case", - "slug": "a19-f015-compatibilitymatrix-extension", - "readiness": "READY", - "source": [ - "`final/document.md#a19#L757`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`Tier` enum은 세 값을 갖는다 — `STABLE`, `EXPERIMENTAL`, `EXTENSION`(\"Adapter SPI only; outside the supported set\"). `ENTRIES` 5개는 전부 STABLE 또는 EXPERIMENTAL이고 `EXTENSION`을 쓰는 항목은 없다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "messaging-and-outbox/case/case-a19-f015-compatibilitymatrix-extension.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a19-f015-compatibilitymatrix-extension" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a19-f015-compatibilitymatrix-extension.txt" - ] - }, - { - "title": "claim-check는 starter에 배선 코드가 한 줄도 없다", - "kind": "case", - "slug": "analysis-finding-a19-f018", - "readiness": "READY", - "source": [ - "`final/document.md#a19#L926`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`messaging-claim-check`(6 main, 418 LOC, 출하)의 `ClaimCheckPublisher`·`ClaimCheckResolver`는 main 참조 0건이고, `MessagingReliabilityAutoConfiguration`에 `ClaimCheck` 문자열이 등장하지 않는다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "messaging-and-outbox/case/case-analysis-finding-a19-f018.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a19-f018" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a19-f018.txt" - ] - }, - { - "title": "admin 스위치가 가드를 켜고 서비스는 켜지 않는다", - "kind": "case", - "slug": "analysis-finding-a19-f019", - "readiness": "READY", - "source": [ - "`final/document.md#a19#L975`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`app.messaging.admin.enabled=true`가 만드는 bean은 넷이다: `DestructiveOperationGuard`, `AdminOperationJournal`, `MessagingAdminDurabilityValidator`, (`BrokerTopologyInspector`가 있을 때) `CompositeTopologyValidator`.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "messaging-and-outbox/case/case-analysis-finding-a19-f019.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a19-f019" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a19-f019.txt" - ] - }, - { - "title": "`messaging-admin-api`는 main 25파일 · 1,613 LOC에 테스트 파일이 1개다", - "kind": "case", - "slug": "a19-f020-messaging-admin-api", - "readiness": "READY", - "source": [ - "`final/document.md#a19#L997`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`messaging-admin-api`가 담고 있는 것은 승인·다이제스트·토폴로지 계약이다 — `ApprovalVerifier`, `HmacApprovalVerifier`, `ApprovalGrant`, `VerifiedApproval`, `PlanDigest`, `ApprovedRedrivePlan`, `ApprovedReplayPlan`, `DestructiveOperation`, `TopologyManifest`, `TopologyValidationReport` 등 보안에 직결되는 타입들이다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "messaging-and-outbox/case/case-a19-f020-messaging-admin-api.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a19-f020-messaging-admin-api" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a19-f020-messaging-admin-api.txt" - ] - }, - { - "title": "`MessagingPublicSurfaceContractTest`가 가족 밖(app-bootstrap)에 있다", - "kind": "case", - "slug": "a19-f022-messagingpublicsurfacecontracttest", - "readiness": "READY", - "source": [ - "`final/document.md#a19#L1114`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "CLAUDE.md의 public surface 정책이 \"이 규칙은 문서가 아니라 `MessagingPublicSurfaceContractTest`가 붙들고 있다\"고 말한다. 그 테스트의 위치:", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "messaging-and-outbox/case/case-a19-f022-messagingpublicsurfacecontracttest.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a19-f022-messagingpublicsurfacecontracttest" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a19-f022-messagingpublicsurfacecontracttest.txt" - ] - }, - { - "title": "증거 등급 모델 전체가 자동 실행 경로 밖에 있고, CLAUDE.md는 현재 시제로 서술한다", - "kind": "case", - "slug": "a20-f003-claude", - "readiness": "READY", - "source": [ - "`final/document.md#a20#L260`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "> \"현재 in-process·Netty·fault lane은 실제로 실행되어 통과하지만, 실제 배포 환경에서의 soak·performance baseline은 없다.\"", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "grpc-and-streaming/case/case-a20-f003-claude.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a20-f003-claude" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a20-f003-claude.txt" - ] - }, - { - "title": "조립 경계가 정책 객체 9개를 만들고 서버를 만들지 않는다", - "kind": "case", - "slug": "analysis-finding-a20-f004", - "readiness": "READY", - "source": [ - "`final/document.md#a20#L298`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`GrpcPlatformAutoConfiguration`이 등록하는 9개는 전부 프로파일·정책·레지스트리다. 서버도, 인터셉터 체인도, 서비스 어댑터 등록도 없다. 그리고 그것을 담당하는 타입들이 main 참조 0이다:", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "grpc-and-streaming/case/case-analysis-finding-a20-f004.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a20-f004" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a20-f004.txt" - ] - }, - { - "title": "저장소 어디에도 참조가 없는 타입 3개", - "kind": "case", - "slug": "analysis-finding-a20-f005", - "readiness": "READY", - "source": [ - "`final/document.md#a20#L321`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`main = 0`이면서 `test = 0`인 것, 즉 선언 파일 외에 아무 곳에서도 이름이 등장하지 않는 타입:", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "grpc-and-streaming/case/case-analysis-finding-a20-f005.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "analysis-finding-a20-f005" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/analysis-finding-a20-f005.txt" - ] - }, - { - "title": "`GrpcAdmissionController.tryAdmit()`의 동시성 경계가 동시성 아래에서 성립하지 않는다", - "kind": "case", - "slug": "a20-f006-grpcadmissioncontroller-tryadmit", - "readiness": "READY", - "source": [ - "`final/document.md#a20#L518`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "이 클래스는 조립된다 — `GrpcPlatformAutoConfiguration`의 9개 bean 중 하나(`grpcAdmissionController`)다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "grpc-and-streaming/case/case-a20-f006-grpcadmissioncontroller-tryadmit.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a20-f006-grpcadmissioncontroller-tryadmit" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a20-f006-grpcadmissioncontroller-tryadmit.txt" - ] - }, - { - "title": "`GrpcStreamAdmission`도 같은 형태이고, per-caller 맵이 줄지 않는다", - "kind": "case", - "slug": "a20-f007-grpcstreamadmission", - "readiness": "READY", - "source": [ - "`final/document.md#a20#L572`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "§7.1과 동일한 TOCTOU이고, 이쪽은 javadoc이 서술하는 실패 시나리오가 곧 고동시성 상황이다:", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "grpc-and-streaming/case/case-a20-f007-grpcstreamadmission.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a20-f007-grpcstreamadmission" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a20-f007-grpcstreamadmission.txt" - ] - }, - { - "title": "`GrpcSerializedStreamWriter`의 `DROP_OLDEST`가 잘못된 메시지의 바이트를 뺀다", - "kind": "case", - "slug": "a20-f008-grpcserializedstreamwriter-drop-oldest", - "readiness": "READY", - "source": [ - "`final/document.md#a20#L595`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "버려지는 것은 `dropped`인데 빼는 값은 새로 들어오는 메시지의 크기 `nextBytes`다. `GrpcStreamEnvelope`는 7개 성분(`streamId`·`sequence`·`kind`·`snapshotVersion`·`resumeToken`·`terminationReason`·`payload`) 중 크기를 담지 않으므로, 이 지점에서 버려지는 메시지의 크기를 알 방법이 애초에 없다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "grpc-and-streaming/case/case-a20-f008-grpcserializedstreamwriter-drop-oldest.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a20-f008-grpcserializedstreamwriter-drop-oldest" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a20-f008-grpcserializedstreamwriter-drop-oldest.txt" - ] - }, - { - "title": "`GrpcOutcomeReplay`가 제거 경로 없는 인메모리 저장소다", - "kind": "case", - "slug": "a20-f010-grpcoutcomereplay", - "readiness": "READY", - "source": [ - "`final/document.md#a20#L664`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "javadoc은 \"a small inline store\"라고 부르지만 작게 유지하는 장치가 없고, 크기를 넘는 응답은 거부하면서(\"store it behind an object reference instead\") 개수는 거부하지 않는다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "grpc-and-streaming/case/case-a20-f010-grpcoutcomereplay.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a20-f010-grpcoutcomereplay" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a20-f010-grpcoutcomereplay.txt" - ] - }, - { - "title": "`GrpcCompletionReconciler`가 요청 경로에서 동기화 없는 `ArrayList`를 변경한다", - "kind": "case", - "slug": "a20-f011-grpccompletionreconciler-arraylist", - "readiness": "READY", - "source": [ - "`final/document.md#a20#L678`" - ], - "code": [ - "분석 문서의 해당 finding 절에 기록된 production path", - "symbol", - "test/probe를 record 생성 시 재개방" - ], - "evidence": [ - "분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방" - ], - "classification": "`synchronized`·`Concurrent*`·`volatile`·`Lock` 전부 0건이고, 단일 스레드 전용이라는 javadoc 표기도 없다. 이 leaf에서 스레드 안전성을 명시적으로 다루는 유일한 클래스는 `GrpcSerializedStreamWriter`이며(그쪽은 9개 마커로 제대로 닫혀 있다), 그 사실이 이 leaf가 동시성을 인지하고 있음을 보여준다.", - "missing-verification": "분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "grpc-and-streaming/case/case-a20-f011-grpccompletionreconciler-arraylist.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a20-f011-grpccompletionreconciler-arraylist" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a20-f011-grpccompletionreconciler-arraylist.txt" - ] } ], "concept": [ @@ -10364,6 +4409,7 @@ "`reference:hibernate-filter-is-not-a-security-boundary`", "`reference:isolation-settings-must-be-transaction-local`" ], + "basis-version": "clean-architecture-backend-template @ 21234e38 · PostgreSQL Row Level Security · db/experimental-rls 는 Stable location 이 적용하지 않음", "publication": "초안", "file": "multitenancy-isolation/concept/concept-rls-three-preconditions.md", "status": "게시 전", @@ -10372,422 +4418,17 @@ "rls-three-preconditions", "rls-three-preconditions-diagram" ], + "assetFiles": [ + "rls-three-preconditions", + "rls-three-preconditions" + ], "evidenceFiles": [ "../../../final/evidence/raw/rls-three-preconditions.txt" ] - }, - { - "title": "네 가지 멀티테넌시 전략과 각각의 격리 경계", - "kind": "concept", - "slug": "four-multitenancy-strategies", - "readiness": "READY", - "source": [ - "`final/document.md#a05` §13.2" - ], - "code": [ - "`.../experimental/multitenancy/`의 column·rls·schema·database 구현" - ], - "classification": "이 저장소가 네 전략(discriminator column · RLS · schema-per-tenant · database-per-tenant)을 각각 플래그 뒤에 두고 구현한 구조의 설명이다. 각 전략의 격리 경계가 다르고 실패 모드도 다르다 — column은 predicate 누락이 곧 유출, RLS는 세 전제(§concept:rls-three-preconditions), schema는 `search_path` 잔존, database는 커넥션 예산의 곱셈. `TenantId`의 정규식이 네 전략 전부의 공통 기반인 이유(스키마 이름·`set_config` 값·라우팅 키에 들어가므로 `../public` 같은 값이 path-traversal이 된다)도 다룬다.", - "missing-verification": "네 전략 중 어느 것도 실제로 켜서 관측하지 않았다 — 전부 experimental 플래그 뒤에 있다", - "relations": [ - "`concept:rls-three-preconditions`", - "`case:tenant-pools-summed-past-the-server-ceiling`", - "`decision:classpath-presence-is-not-consent`" - ], - "publication": "초안", - "file": "multitenancy-isolation/concept/concept-four-multitenancy-strategies.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "four-multitenancy-strategies", - "four-multitenancy-strategies-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/four-multitenancy-strategies.txt" - ] - } - ], - "reference": [ - { - "title": "Hibernate filter는 보안 경계가 아니다", - "kind": "reference", - "slug": "hibernate-filter-is-not-a-security-boundary", - "readiness": "READY", - "source": [ - "`final/document.md#a05` §13.2" - ], - "classification": "Hibernate filter는 엔티티 쿼리에 적용되고 native SQL·bulk DML·`getReference`·L2 캐시를 통해 도달하는 것에는 적용되지 않는다. 그래서 filter만 믿는 설계는 다른 tenant의 행으로 가는 경로 여러 개를 열어 둔다. 기준은 \"이 격리를 우회하는 경로가 몇 개인가\"이고, ORM 기능은 그 답이 항상 0이 아니다.", - "scope": [ - "tenant·소유자·가시성 격리 전반. 실제 경계는 DB(RLS)나 별도 가드(`TenantAwareRepositoryGuard`)여야 하고, filter는 편의로만 쓴다." - ], - "exceptions": [ - "읽기 경로만 있고 native·bulk가 구조적으로 금지된 좁은 컨텍스트면 filter로 충분할 수 있다. 다만 그 금지를 ArchUnit 같은 것이 강제해야 하고, 강제가 없으면 전제가 유지되지 않는다." - ], - "relations": [ - "`concept:rls-three-preconditions`", - "`case:three-ways-rls-does-nothing`", - "`reference:tenant-column-belongs-in-every-unique-constraint`" - ], - "publication": "초안", - "file": "multitenancy-isolation/reference/reference-hibernate-filter-is-not-a-security-boundary.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "tenant 컬럼이 있는 테이블의 모든 unique 제약에 그 컬럼이 들어가야 한다", - "kind": "reference", - "slug": "tenant-column-belongs-in-every-unique-constraint", - "readiness": "READY", - "source": [ - "`final/document.md#a05` §13.1" - ], - "classification": "tenant 격리가 컬럼에 의존하면 그 테이블의 모든 uniqueness 요구가 그 컬럼을 포함해야 한다. `(value)`만의 unique index는 다른 tenant가 그 값을 이미 썼다는 이유로 한 tenant의 insert를 실패시키는데, 그것은 **버그이자 정보 유출**이다 — 존재하지 않아야 할 행의 존재를 알려준다.", - "scope": [ - "discriminator column 전략을 쓰는 모든 테이블. 같은 논리가 partial unique index와 exclusion constraint에도 적용된다." - ], - "exceptions": [ - "전역적으로 유일해야 하는 값(외부 시스템의 식별자)은 tenant를 포함하지 않는 것이 맞다. 다만 그때는 그 값이 tenant 간에 노출되는 것이 의도임을 적어야 한다." - ], - "relations": [ - "`case:three-ways-rls-does-nothing`", - "`reference:hibernate-filter-is-not-a-security-boundary`" - ], - "publication": "초안", - "file": "multitenancy-isolation/reference/reference-tenant-column-belongs-in-every-unique-constraint.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "격리 설정은 트랜잭션 로컬이어야 한다", - "kind": "reference", - "slug": "isolation-settings-must-be-transaction-local", - "readiness": "READY", - "source": [ - "`final/document.md#a05` §13.1, §13.2" - ], - "classification": "tenant 바인딩·`search_path`처럼 격리를 결정하는 세션 설정은 트랜잭션과 함께 되돌아가야 한다. 세션 스코프면 풀로 돌아간 커넥션이 그것을 들고 있고 다음 borrower가 상속한다. PostgreSQL에서는 `set_config(name, value, true)`의 세 번째 인자가 그것을 보장한다.", - "scope": [ - "RLS tenant 바인딩·schema 라우팅·타임아웃 등 세션 상태로 표현되는 모든 격리. 대응물이 없는 벤더에서는 반환 시 명시적 reset이 대안이다." - ], - "exceptions": [ - "커넥션이 tenant에 고정 할당되는 database-per-tenant에서는 세션 스코프가 문제가 되지 않는다. 다만 그때는 풀 예산이 새 문제가 된다." - ], - "relations": [ - "`case:search-path-survived-the-return-to-the-pool`", - "`reference:session-scoped-settings-outlive-the-transaction`", - "`concept:rls-three-preconditions`" - ], - "publication": "초안", - "file": "multitenancy-isolation/reference/reference-isolation-settings-must-be-transaction-local.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "legacy storage/notification compatibility surface의 제거 조건 추적", - "kind": "reference", - "slug": "analysis-finding-a03-f003", - "readiness": "READY", - "source": [ - "`final/document.md#a03#L360`" - ], - "classification": "deprecated/legacy라는 이름은 삭제 가능성의 증거가 아니다. production wiring에 남은 compatibility surface는 **external production reference 0 + replacement characterization + config path removal**이 함께 확인된 뒤에만 제거 후보가 된다.", - "scope": [ - "deprecated/legacy contract가 아직 production adapter/runtime wiring에 연결된 migration 구간." - ], - "exceptions": [ - "외부 호환 계약을 의도적으로 유지하거나 replacement path가 아직 동일 behavior를 증명하지 못한 경우에는 제거하지 않는다. 실제 제거 여부는 별도 Decision evidence가 필요하다." - ], - "relations": [ - "`case:analysis-finding-a03-f001` 및 실제 legacy consumer Case와 연결; 제거 자체는 Decision evidence가 생길 때 별도 기록" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/reference/reference-analysis-finding-a03-f003.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - } - ], - "question": [ - { - "title": "response/LRO invariant enforcement boundary", - "kind": "question", - "slug": "a02-f001-lro", - "readiness": "OPEN", - "source": [ - "`final/document.md#a02#L155`" - ], - "known": [ - "`Envelope`, `BulkEnvelope`, `Operation`, `PageMeta`의 valid shape는 factory test에 고정돼 있지만 public canonical constructor는 그 invariant를 강제하지 않는다." - ], - "unknown": [ - "raw constructor가 의도된 extension surface인지, invalid state를 constructor에서 차단해야 하는지 프로젝트 선택이 확인되지 않았다." - ], - "next-verification": "production/raw-constructor 호출자를 전수 확인하고 invalid-shape constructor test를 추가해 현재 허용 surface를 고정한다.", - "decision-criterion": "raw constructor가 외부 extension 계약이면 허용 범위와 failure semantics를 문서화한다. 그렇지 않으면 constructor-level invariant를 추가하고 factory와 동일한 규칙을 검증한다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance와 기존 관련 Case/Reference/Decision을 record 생성 시 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/question/openquestion-a02-f001-lro.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "DomainContextKey same-name different-type collision", - "kind": "question", - "slug": "a02-f002-domaincontextkey", - "readiness": "OPEN", - "source": [ - "`final/document.md#a02#L159`" - ], - "known": [ - "`DomainContextKey` identity는 name only이고 retrieval은 요청 타입으로 cast한다." - ], - "unknown": [ - "같은 name의 다른 `Class` key 선언이 forbidden contract인지 의도적으로 허용된 충돌/실패 모델인지 정해져 있지 않다." - ], - "next-verification": "동일 name·상이 type key fixture를 만들고 creation/retrieval failure를 고정한 뒤 registry의 실제 key 선언을 전수 대조한다.", - "decision-criterion": "same-name/different-type가 금지라면 creation/registry 단계에서 충돌을 거부한다. 허용이라면 cast failure semantics와 사용 조건을 계약에 명시한다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance와 기존 관련 Case/Reference/Decision을 record 생성 시 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/question/openquestion-a02-f002-domaincontextkey.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "bounded operational record identifiers", - "kind": "question", - "slug": "analysis-finding-a02-f003", - "readiness": "OPEN", - "source": [ - "`final/document.md#a02#L163`" - ], - "known": [ - "`OperationalRecord` javadoc은 namespace/key를 bounded라고 설명하지만 constructor는 blank 여부만 확인한다." - ], - "unknown": [ - "provider별 key size/character-set 제한을 shared contract가 소유해야 하는지 adapter가 소유해야 하는지 정해지지 않았다." - ], - "next-verification": "실제 provider/adapter의 identifier 제한과 production 생성 지점을 대조하고 경계값 fixture를 추가한다.", - "decision-criterion": "여러 provider가 공통 최소 bound를 요구하면 shared value object에서 강제한다. provider-specific이면 shared javadoc을 좁히고 adapter 경계에서 검증한다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance와 기존 관련 Case/Reference/Decision을 record 생성 시 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/question/openquestion-analysis-finding-a02-f003.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "permission component grammar", - "kind": "question", - "slug": "analysis-finding-a02-f004", - "readiness": "OPEN", - "source": [ - "`final/document.md#a02#L167`" - ], - "known": [ - "permission은 colon segment 수·blank·normalization은 강제하지만 segment character grammar는 제한하지 않는다." - ], - "unknown": [ - "registry SSOT가 shared value object보다 좁은 문자 grammar를 계약으로 요구하는지 확인되지 않았다." - ], - "next-verification": "registry의 모든 permission literal을 수집해 허용 문자 집합과 shared parser를 parity test로 대조한다.", - "decision-criterion": "registry가 더 좁은 grammar를 실제 SSOT로 사용하면 shared value object가 동일 grammar를 강제한다. 아니면 현재 넓은 grammar가 의도임을 문서화한다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance와 기존 관련 Case/Reference/Decision을 record 생성 시 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/question/openquestion-analysis-finding-a02-f004.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "messaging schema qualification boundary", - "kind": "question", - "slug": "analysis-finding-a02-f005", - "readiness": "OPEN", - "source": [ - "`final/document.md#a02#L171`" - ], - "known": [ - "현재 JDK-only test는 exact resource/digest/selected semantic vector를 검증한다." - ], - "unknown": [ - "실제 Draft 2020-12 validator와의 호환성은 qualification evidence가 없어 현재 test 성공만으로 주장할 수 없다." - ], - "next-verification": "채택할 Draft 2020-12 validator로 committed schema와 positive/negative vectors를 실행하고 결과를 evidence로 남긴다.", - "decision-criterion": "실제 validator가 동일 semantic vectors를 통과해야 호환성을 주장한다. 실패하면 schema 또는 지원 범위를 수정하고 JDK-only gate의 표현을 좁힌다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance와 기존 관련 Case/Reference/Decision을 record 생성 시 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/question/openquestion-analysis-finding-a02-f005.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "notification derived idempotency key가 32-bit hash", - "kind": "question", - "slug": "analysis-finding-a03-f002", - "readiness": "OPEN", - "source": [ - "`final/document.md#a03#L352`" - ], - "known": [ - "fallback idempotency key는 `Integer.toHexString(Objects.hash(...))`인 32-bit hash이고 canonical fingerprint 비교는 별도로 존재한다." - ], - "unknown": [ - "서로 다른 canonical request가 실제 collision을 만들 때 downstream이 false conflict로 끝나는지, 이 위험을 허용할지 확인되지 않았다." - ], - "next-verification": "known Java hash collision 또는 property search로 서로 다른 canonical request의 동일 derived key를 만들고 downstream conflict behavior를 고정한다.", - "decision-criterion": "distinct canonical request collision이 재현되면 canonical plan의 SHA-256/HMAC 계열 digest로 교체한다. 재현하지 못해도 uniqueness javadoc은 실제 보장 수준으로 좁힌다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance와 기존 관련 Case/Reference/Decision을 record 생성 시 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/question/openquestion-analysis-finding-a03-f002.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "isolation vocabulary와 legacy routing capability의 시차", - "kind": "question", - "slug": "analysis-finding-a03-f004", - "readiness": "OPEN", - "source": [ - "`final/document.md#a03#L367`" - ], - "known": [ - "public `Isolation`은 stricter level을 표현하지만 legacy `TransactionPort` template은 READ_COMMITTED로 고정되고 test도 stricter routing을 planned로 적는다." - ], - "unknown": [ - "stricter isolation을 legacy port까지 확장할지 canonical policy transaction path에서만 제공할지 아직 정해지지 않았다." - ], - "next-verification": "현재 production use-case의 isolation 요구와 legacy `TransactionPort` 호출자를 대조하고 향후 routing owner를 하나로 정하는 설계/contract test를 만든다.", - "decision-criterion": "선택한 owner에서 use-case policy → adapter transaction definition이 1:1로 검증돼야 하며 지원하지 않는 경로는 capability로 노출하지 않는다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance와 기존 관련 Case/Reference/Decision을 record 생성 시 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/question/openquestion-analysis-finding-a03-f004.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "cache-redis/httpclient의 support project dependency 필요성 재검증", - "kind": "question", - "slug": "analysis-finding-a04-f006", - "readiness": "OPEN", - "source": [ - "`final/document.md#a04#L603`" - ], - "known": [ - "두 leaf 모두 Gradle support dependency는 있지만 현재 production Java support reference와 support resource는 0이다." - ], - "unknown": [ - "build/test/reflection/후속 bounded scope에서 이 edge가 필요한지 확인되지 않아 dead dependency로 확정할 수 없다." - ], - "next-verification": "각 downstream leaf exhaustive analysis 결과를 확인한 뒤 support dependency를 제거한 상태로 focused test와 app composition test를 실행한다.", - "decision-criterion": "production/build/test/runtime 소비자가 0이고 dependency 제거 후 관련 lane이 통과하면 edge를 제거한다. 소비자가 발견되면 그 owner와 이유를 문서화한다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance와 기존 관련 Case/Reference/Decision을 record 생성 시 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/question/openquestion-analysis-finding-a04-f006.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "`CapabilitySupport.constraints`의 bounded/report-safe 계약이 타입에서 강제되지 않음", - "kind": "question", - "slug": "a05-f003-capabilitysupport-constraints", - "readiness": "OPEN", - "source": [ - "`final/document.md#a05#L661`" - ], - "known": [ - "100,000-character constraint가 허용되고 capability list는 actuator report model에 포함되지만 현재 shipped composition은 짧은 static literal만 만든다." - ], - "unknown": [ - "외부/fork/dynamic producer가 public API에 arbitrary constraint를 넣는 실제 경로가 있는지, bound를 타입과 report projection 중 어디가 소유할지 확인되지 않았다." - ], - "next-verification": "public API의 외부 producer와 fork extension point를 추적하고 oversized constraint의 constructor/report boundary test를 추가한다.", - "decision-criterion": "dynamic/external producer가 확인되면 bound를 타입 또는 report projection에서 강제하고 Case로 승격한다. 없다면 public invariant/javadoc을 현재 composition의 보장 수준으로 좁힌다.", - "relations": [ - "candidate-ledger의 topic/disposition provenance와 기존 관련 Case/Reference/Decision을 record 생성 시 연결" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/question/openquestion-a05-f003-capabilitysupport-constraints.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "실제 성능·용량 특성을 어떤 모듈에서도 측정하지 않았다", - "kind": "question", - "slug": "performance-and-capacity-unmeasured", - "readiness": "OPEN", - "source": [ - "`final/document.md#a99` §6" - ], - "known": [ - "cross-scope 분석은 어떤 모듈에서도 성능을 측정하지 않았고 gRPC는 성능 레인이 존재하지만 기본 `test`에서 제외된 사실까지만 확인했다." - ], - "unknown": [ - "현재 revision의 latency·throughput·pool saturation·backpressure·capacity ceiling이 실제 workload에서 어느 수준인지 알 수 없다." - ], - "next-verification": "릴리스 correctness gate와 분리된 dedicated benchmark lane에서 workload·warmup·sample count·환경을 고정하고 baseline을 기록한다.", - "decision-criterion": "재현 가능한 benchmark baseline과 허용 threshold가 생기면 성능/용량 주장을 그 evidence 범위에서만 닫는다. baseline 전에는 성능을 보장한다고 쓰지 않는다.", - "relations": [ - "`decision:performance-measurement-is-not-a-release-gate`", - "`open-question:container-lanes-not-executed`" - ], - "listedInTree": false, - "publication": "초안", - "file": "multitenancy-isolation/question/openquestion-performance-and-capacity-unmeasured.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] } ], + "reference": [], + "question": [], "decision": [ { "title": "클래스패스에 있는 것은 실행 동의가 아니다", @@ -10818,14055 +4459,1623 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] } ] } - }, - "contract-domain-and-bounds": { - "topic": "contract-domain-and-bounds", - "title": "계약 도메인과 경계값 — 타입·문법·상한이 실제 허용 범위와 맞는가", - "readerQuestion": "", - "kinds": { - "case": [ - { - "title": "encode가 발급한 2046~2048-byte cursor를 decode가 거부한다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "canonical transaction boundary documentation과 실제 dual stack 불일치", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`SpecificationPolicy`는 `Specification.unrestricted()`를 bounded로 오인한다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`normalize`는 canonical이 아닌 입력을 받아 다른 UUID로 조용히 바꾼다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "다중 키 fan-in 중 HyperLogLog `merge`만 budget이 없다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`requireIdentifier`의 다섯 검사 중 둘은 도달할 수 없다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`BoundedDataBufferFlux`의 두 연산자가 이름만 있고 아무것도 하지 않는다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "\"상한을 두고 읽는다\"는 본문 핸들러가 전부 읽은 뒤에 자른다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "의미 지문이 길이 프레이밍 없이 구분자로 만들어진다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`maxArrayElements`가 선언만 되고 강제되지 않으며, 바이트 예산 백스톱도 없다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "기계가 읽는 능력 매니페스트와 사람이 읽는 등급표가 커서 서명에 대해 다르게 답한다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "\"실환경 증거\" 가 두 리프에 반씩 있고 서로 만나지 않는다", - "kind": "case", - "slug": "grpc-advanced-diagnostics-f03", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-advanced-diagnostics#L218`" - ], - "owning-module": "`grpc-advanced-diagnostics` · priority: `P3`", - "classification": "이 리프가 능력별로 무엇이 실환경인지 정의한다. 그리고 `grpc-advanced-bootstrap` 이 승격 증거로 그것을 요구한다. `GrpcAdvancedPromotionGate.evaluate` 는 그 불리언이 거짓이면 \"xds has no real environment test\" 를 차단 사유로 낸다. 그 불리언을 무엇으로 채워야 하는지는 그쪽에서 답하지 않고, 답하는 코드가 이 리프에 있는데 두 쪽이 서로를 부르지 않는다. 결과: `GrpcAdvancedPromotionEvidence.complete(XDS, 7일)` 은 `realEnvironmentTest = true` 를 그냥 넣는다. xDS 통제 평면이 실제로 있었는지와 무관하다. 이 리프의 javadoc 이 경계한 상태 — \"a suite that runs without the infrastructure passes and establishes nothing\" — 를 승격 게이트가 그대로 통과시킬 수 있다. 두 리프 모두 배선되지 않았고 승격은 사람이 수행한다. 다만 이 두 조각이 존재하는 이유가 \"그 판단을 코드로 적어 두는 것\" 이므로, 판단의 절반이 다른 절반을 부르지 않는 것은 그 목적에 어긋난다. `grpc-advanced-edition` §17.2 가 같은 가족에서 같은 모양을 기록했다 — 두 승격 게이트가 서로를 부르지 않는다. `GrpcAdvancedPromotionEvidence.realEnvironmentTest` 를 불리언 대신 `Set availableInfrastructure` 로 바꾸고, 게이트가 `missingInfrastructure(capability, available)` 를 불러 그 결과를 차단 사유에 합친다. 그러면 \"실환경 테스트를 했다\" 가 선언이 아니라 능력별 목록에 대한 대조가 된다. 의존 방향도 맞는다 — 이 리프가 이미 bootstrap 을 의존하므로, 게이트가 이쪽을 부르려면 방향을 뒤집거나 `Infrastructu…", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "contract-domain-and-bounds/case/case-grpc-advanced-diagnostics-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-advanced-diagnostics-f03" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-advanced-diagnostics-f03.txt" - ] - }, - { - "title": "대체 선택기는 사용자 정의 선택기가 받는 보호를 받지 않는다", - "kind": "case", - "slug": "grpc-advanced-resilience-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-advanced-resilience#L155`" - ], - "owning-module": "`grpc-advanced-resilience` · priority: `P3`", - "classification": "`fallback.pick(selectable)` 은 감싸이지 않는다. 대체가 던지면 예외가 그대로 올라가고, 널이나 목록 밖 엔드포인트를 돌려주면 그대로 결정이 된다. 기본 생성자는 플랫폼의 라운드 로빈을 대체로 쓰므로 지금은 안전하다. 그러나 두 인자 생성자가 임의의 선택기를 대체로 받고, 그 인자에는 아무 제약이 없다. 이 클래스의 존재 이유가 \"선택기 버그가 가용성이 아니라 균형을 저하시키게 하는 것\" 인데, 대체 선택기의 버그는 가용성을 저하시킨다. 수정은 대체 호출도 같은 검사를 지나게 하거나(그 결과가 널이거나 목록 밖이면 플랫폼 라운드 로빈으로 한 번 더 떨어진다), 두 인자 생성자를 없애 대체를 플랫폼 것으로 고정하는 것이다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "contract-domain-and-bounds/case/case-grpc-advanced-resilience-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-advanced-resilience-f02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-advanced-resilience-f02.txt" - ] - }, - { - "title": "클래스가 비판한 무제한 증가를 형제 맵이 그대로 한다", - "kind": "case", - "slug": "grpc-advanced-streaming-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-advanced-streaming#L125`" - ], - "owning-module": "`grpc-advanced-streaming` · priority: `P3`", - "classification": "클래스 javadoc 이 집합 방식을 거부한 이유가 무제한 증가다 — \"A set grows without bound for the life of a session\". 체크포인트는 그 비판을 지킨다. 세션당 항목 하나이고 순번만 앞으로 간다. 형제 맵은 지키지 않는다. 제거는 `endSession` 뿐이고, 그때 그 세션의 접두를 가진 키를 전부 지운다. 그러므로 결과 참조를 기록하는 세션에서는 적용된 메시지 수만큼 항목이 쌓인다. 상한도 만료도 없다. 클래스 javadoc 은 다르게 말한다. 작은 창이 코드에 없다. 체크포인트가 앞으로 가도 그 이전 결과들은 남는다. 그리고 실제로 필요한 창은 좁다 — 판정이 `alreadyApplied(sequence)` 로 재생을 결정하고, 재생 응답에 쓰이는 것은 그 순번의 결과 하나다. 체크포인트보다 한참 뒤처진 순번의 결과가 필요할 상황은 재개 직후의 좁은 구간뿐이다. 수정은 창을 실제로 만드는 것이다 — 세션당 최근 N개만 유지하거나, 체크포인트가 앞으로 갈 때 그보다 오래된 항목을 지운다. 후자가 자바독의 서술과 정확히 같다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "contract-domain-and-bounds/case/case-grpc-advanced-streaming-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-advanced-streaming-f01" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-advanced-streaming-f01.txt" - ] - }, - { - "title": "클라이언트 스트림 정책의 네 상한 중 둘은 읽는 코드가 없다", - "kind": "case", - "slug": "grpc-advanced-streaming-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-advanced-streaming#L159`" - ], - "owning-module": "`grpc-advanced-streaming` · priority: `P3`", - "classification": "`GrpcClientStreamPolicy` javadoc 이 네 상한을 모두 든다. 저장소 전체에서 접근자 호출을 세면 둘이 0 이다. Advanced 가족이 미배선이라는 사실과는 별개다 — 이 리프 안에도 그 값을 쓰는 코드가 없다. 수요 상한을 강제하는 `GrpcDemandController` 는 `GrpcManualFlowControlPolicy` 를 쓰고, 이 정책을 보지 않는다. `wholeStreamRetryAllowed()` 는 항상 거짓을 돌려주는 형태이므로 그 자체가 문서화 장치다. 나머지 둘은 강제 지점이 필요하다. 수정은 상한을 강제하는 지점을 만들거나(수신 경로에 속도·미확인 수 검사), 강제되지 않는 값이 강제되는 것처럼 읽히지 않도록 자바독을 낮추는 것이다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "contract-domain-and-bounds/case/case-grpc-advanced-streaming-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-advanced-streaming-f02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-advanced-streaming-f02.txt" - ] - }, - { - "title": "메타데이터 예산의 두 성분 중 하나는 강제되지 않고, 나머지 하나는 바이트가 아니라 문자를 센다", - "kind": "case", - "slug": "grpc-core-api-f05", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-core-api#L219`" - ], - "owning-module": "`grpc-core-api` · priority: `P3`", - "classification": "`GrpcMetadataBudget` 은 세 성분을 갖는다 — `maxTotalBytes`·`maxUserDefinedBytes`·`maxEntries`. `check(...)` 가 보는 것은 뒤의 둘뿐이다. 저장소 전체에서 이 접근자를 부르는 곳은 compact constructor 의 순서 가드와 테스트 단언 하나뿐이다. 자바독은 그 이유를 설명한다 — 하드 총계를 넘기는 것은 프레임워크가 던지는 전송 거절이고, 여기서 함께 검사하면 \"고칠 수 있는 쪽\" 과 \"고칠 수 없는 쪽\" 이 한 자리에서 발견된다는 것. 판단은 옳다. 다만 그 결과로 이 record 는 자기가 쓰지 않는 수를 성분으로 들고 있고, 이름은 그것이 강제된다고 읽힌다. 성분 이름은 `...Bytes` 인데 세는 것은 `String.length()`, 즉 UTF-16 코드 단위다. 키는 `[a-z0-9._-]` 로 제한되어 ASCII 지만 값에는 문자 집합 제약이 없다. 다중 바이트 문자를 담은 값은 실제 프레임보다 적게 계산된다. gRPC 의 ASCII 메타데이터 값은 프로토콜 상 인쇄 가능 ASCII 여야 하므로 실무에서는 대개 일치한다. 다만 그 제약을 이 클래스가 검사하지 않으므로, 일치는 보장이 아니라 관행이다. 수정은 둘 다 작다 — `value.getBytes(StandardCharsets.US_ASCII).length` 로 세거나 값의 문자 집합을 `GrpcMetadataKey.Kind.ASCII` 에 맞춰 검증하고, `maxTotalBytes` 는 성분에서 빼고 javadoc 의 서술로 남긴다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "contract-domain-and-bounds/case/case-grpc-core-api-f05.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-core-api-f05" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-core-api-f05.txt" - ] - }, - { - "title": "프로파일이 스트림 재접속 예산을 선언하는데 그것이 함의하는 DNS 갱신 주기를 정하지 않는다", - "kind": "case", - "slug": "grpc-discovery-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-discovery#L152`" - ], - "owning-module": "`grpc-discovery` · priority: `P3`", - "classification": "`GrpcKubernetesProfile` 은 세 시간 값을 다룬다. 검증기는 앞의 둘만 비교한다 — 배수 유예가 재접속 예산보다 짧으면 위반. 셋째는 비교 대상에 없다. 그래서 `headlessStreaming()`(재접속 예산 5초, 배수 유예 30초)에서 갱신 주기는 여전히 30초다. 롤아웃으로 스트림이 끊긴 클라이언트가 5초 예산 안에 재접속하려 할 때, 그 클라이언트의 DNS 캐시는 최대 30초 동안 사라진 파드 주소를 들고 있을 수 있다. 그 실패가 `GrpcResolverProfile` 자신의 javadoc 이 서술한 것이다 — \"A channel that resolved once at startup keeps sending to addresses that stopped existing an hour ago; the calls fail with `UNAVAILABLE` and the deployment looks unhealthy long after it finished.\" 수정은 갱신 주기를 재접속 예산에서 파생시키거나(예: 예산 이하), 검증기에 세 값의 순서 규칙을 추가하는 것이다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "contract-domain-and-bounds/case/case-grpc-discovery-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-discovery-f01" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-discovery-f01.txt" - ] - }, - { - "title": "낙관적 잠금 컬럼이 없어 전이 가드가 메모리 안에만 있다", - "kind": "case", - "slug": "grpc-operation-ledger-jpa-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-operation-ledger-jpa#L184`" - ], - "owning-module": "`grpc-operation-ledger-jpa` · priority: `P3`", - "classification": "`requireInProgress()` 가 두 번째 종결 전이를 막는다. 그 가드는 한 영속성 컨텍스트 안의 인스턴스 상태에만 적용된다. 엔티티에 `@Version` 이 없으므로 두 트랜잭션이 같은 행을 읽어 각각 전이하면 나중 쓰기가 앞의 것을 덮는다. DB 의 세 CHECK 제약은 행의 모양을 지키지 지 전이 순서를 지키지 않는다. `COMMITTED` 행이 다른 결과 참조로 갱신되는 것을 막는 제약이 없다. 청구가 배타적이라는 설계 전제 아래서는 도달성이 낮다. 다만 §17.1 을 고치면 이 전제가 실제로 성립하는지가 함께 확인되어야 한다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "contract-domain-and-bounds/case/case-grpc-operation-ledger-jpa-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-operation-ledger-jpa-f01" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-operation-ledger-jpa-f01.txt" - ] - }, - { - "title": "`WireSafeText`의 규칙이 leaf 경계에서 멈춘다", - "kind": "case", - "slug": "messaging-core-api-f05", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-core-api#L880`" - ], - "owning-module": "`messaging-core-api` · priority: `P3`", - "classification": "`WireSafeText`의 leaf 밖 참조 0. 제어문자·인코딩 경계를 각자 검사하는 곳이 저장소에 최소 15개. javadoc이 \"Each copy of this check ... was one more place for the rule to drift\"라고 적었고 그 통합을 leaf 안에서만 했다. 저장소 수준에서는 같은 drift가 그대로 남아 있다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "contract-domain-and-bounds/case/case-messaging-core-api-f05.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-core-api-f05" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-core-api-f05.txt" - ] - }, - { - "title": "`extract`가 손상된 추적 헤더에 분류되지 않은 예외를 던진다", - "kind": "case", - "slug": "messaging-observability-f07", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-observability#L726`" - ], - "owning-module": "`messaging-observability` · priority: `P3`", - "classification": "`MessagingTracer.extract`가 `new TraceContext(...)`를 부르고, 그 생성자는 W3C 문법·바이트 상한·all-zero를 검사해 `IllegalArgumentException`을 던진다. `extract`는 잡지 않는다. 다른 시스템이 보낸 메시지의 헤더는 신뢰할 수 없는 입력이다. 손상된 `traceparent` 하나가 `MessagingException`이 아닌 예외로 소비 경로를 끊는다 — 추적이 없어야 할 자리에서 메시지 처리가 실패한다. `messaging-cloudevents`의 id 파싱과 같은 형태다(그쪽 §17).", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "contract-domain-and-bounds/case/case-messaging-observability-f07.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-observability-f07" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-observability-f07.txt" - ] - }, - { - "title": "inbox 보존 규칙이 문서로만 있다", - "kind": "case", - "slug": "messaging-reliability-api-f05", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-reliability-api#L725`" - ], - "owning-module": "`messaging-reliability-api` · priority: `P3`", - "classification": "`InboxRepository.purgeProcessedBefore` javadoc이 \"Retention must outlive the broker's maximum redelivery window, otherwise a late redelivery arrives after its inbox row was pruned and is processed a second time\"라고 한다. 그 비교를 하는 코드가 이 leaf에도 `messaging-policy`의 프로파일 검증기에도 없다. 위반의 결과가 **부작용의 이중 실행**이다 — Inbox가 존재하는 이유 그 자체가 무효화된다. 그리고 위반이 조용하다: 짧은 보존은 정상 동작처럼 보이고 늦은 재전달이 올 때만 드러난다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "contract-domain-and-bounds/case/case-messaging-reliability-api-f05.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-reliability-api-f05" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-reliability-api-f05.txt" - ] - }, - { - "title": "포트가 bounded/unbounded purge 두 오버로드를 나란히 노출하고, 호출자가 무제한 쪽을 고른다", - "kind": "case", - "slug": "messaging-reliability-api-f08", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-reliability-api#L752`" - ], - "owning-module": "`messaging-reliability-api` · priority: `P3`", - "classification": "`InboxRepository`와 `OutboxRepository`가 각각 `purge*Before(Instant)`와 `purge*Before(Instant, int)`를 선언한다. 후자에 호출 지점이 0이고 두 cleanup job이 전자를 부른다. §12.1(a)의 두 세대 전이와 같은 형태다 — **한 인터페이스가 안전한 형태와 그렇지 않은 형태를 나란히 두고, `@Deprecated`도 이름 차이도 없으며, 호출자가 짧은 쪽을 골랐다.** 두 경우 모두 포트의 형태가 오용을 가능하게 했다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "contract-domain-and-bounds/case/case-messaging-reliability-api-f08.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-reliability-api-f08" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-reliability-api-f08.txt" - ] - } - ], - "concept": [], - "reference": [ - { - "title": "컬럼 폭은 애플리케이션 검증과 짝을 이룬다", - "kind": "reference", - "slug": "messaging-inbox-jdbc-postgresql-f05", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-inbox-jdbc-postgresql#L684`" - ], - "owning-module": "`messaging-inbox-jdbc-postgresql`", - "rule": "컬럼 폭은 애플리케이션 검증과 짝을 이룬다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — `consumer_id` 길이 제약이 애플리케이션 층에 없다", - "scope": [ - "멱등·아웃박스·인박스 계열. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "contract-domain-and-bounds/reference/reference-messaging-inbox-jdbc-postgresql-f05.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "같은 안전 규칙은 한 공식과 한 강제 시점을 갖는다", - "kind": "reference", - "slug": "messaging-inbox-jdbc-postgresql-f06", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-inbox-jdbc-postgresql#L693`" - ], - "owning-module": "`messaging-inbox-jdbc-postgresql`", - "rule": "같은 안전 규칙은 한 공식과 한 강제 시점을 갖는다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 보존 규칙이 세 곳에 있고 공식이 다르다", - "scope": [ - "멱등·아웃박스·인박스 계열. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "contract-domain-and-bounds/reference/reference-messaging-inbox-jdbc-postgresql-f06.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - } - ], - "question": [ - { - "title": "response/LRO invariant enforcement boundary", - "kind": "question", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "DomainContextKey same-name different-type collision", - "kind": "question", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "bounded operational record identifiers", - "kind": "question", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`CapabilitySupport.constraints`의 bounded/report-safe 계약이 타입에서 강제되지 않음", - "kind": "question", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "보존 sweep이 없다", - "kind": "question", - "slug": "messaging-claim-check-f05", - "readiness": "OPEN", - "source": [ - "`final/document.md#a19-messaging-claim-check#L543`" - ], - "owning-module": "`messaging-claim-check` · priority: `P3`", - "classification": "`ClaimCheckStore.delete`가 선언돼 있고 이 leaf에서 호출되지 않는다. `ClaimCheckPublisher` javadoc이 \"the retention sweep reclaims it\"이라고 그 존재를 전제한다. 실패한 발행이 남긴 객체를 회수할 주체가 없다. 저장소 자체의 lifecycle 정책(예: S3 object expiration)이 대신할 수 있으나 `ClaimCheckPolicy.retention`이 그것과 연결되지 않는다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "contract-domain-and-bounds/question/openquestion-messaging-claim-check-f05.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - } - ], - "decision": [] - } - }, - "runtime-reachability-and-composition": { - "topic": "runtime-reachability-and-composition", - "title": "런타임 도달성과 조립 — 구현된 능력이 실제 경로에 설치되는가", - "readerQuestion": "", - "kinds": { - "case": [ - { - "title": "notification consumer는 diagnostic failure를 authoritative failure로 바꿀 수 있다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "notification fail-open consumer가 logger failure를 격리하지 않는다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`TransactionProfileRegistry`는 declarative retry 제거 후 legacy residue 후보", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "Stable runtime-role verification이 startup에서 실제 policy를 적용하지 않는다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "V8 atomic admin claim은 production service에 연결되지 않았고 completion 모델도 미완성이다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "vendor selector의 fail-fast 계약이 shipped composition에 설치돼 있지 않다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`change-streams=true`는 거부되지 않고 조용히 버려지며, 그 결과 startup validator의 한 분기가 production에서 도달 불가다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "서버 측 deadline이 경로마다 다르게 적용되고, 문서가 지목한 메커니즘은 production 호출자가 0이다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`changeStreams` flag는 `false`로 고정돼 있는데, 소비자 bean은 그것과 무관하게 조립된다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "프로파일의 TLS·타임아웃·풀·Stable API가 driver에 도달하지 않는다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "모듈의 존재 논거인 `UuidCodec`에 production 소비자가 없다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "production 판정이 두 개의 리터럴 프로파일 이름에 걸려 있다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "SDK가 선언한 두 진입점에 구현이 없다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "멱등 실행 계층과 durable-operation 표면이 픽스처에서만 조립된다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "배선된 캐시 필터의 `no-store`가 배선된 조건부 읽기 경로를 무력화하고, 둘을 조정하려고 만든 패키지는 참조 0이다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`VirtualThreadProfile.propertyName()`이 아무것도 게이트하지 않는 이름을 반환한다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "이 leaf의 리액티브 절반 29개 파일은 어떤 출하 배포에서도 활성화될 수 없다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`SpringMvcRouteInventoryCollector` 138줄에 참조가 하나도 없다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`WebPlatformStartupValidator`가 시작 시 실행되지 않는다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "설정 바인딩이 마스터 스위치 밖에서 일어난다. 컴포지션 루트의 자기 규칙과 어긋난다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "스키마 조립·계약 정체성·해시 사슬이 통째로 미배선이고, 그것을 발행할 액추에이터 엔드포인트도 등록되지 않는다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "5계층 예산 모델에서 요청 계층만 강제되고, 나머지 파생이 전부 미배선이다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "연산 이름 정책의 두 구현 중 하나만 배선되고, 미배선 쪽만 `GraphQlOperationNamePolicy`를 쓴다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "설정으로 정한 파서 한계가 graphql-java에 설치되지 않는다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "프로파일별 정책 매니페스트가 미배선이라, 자격에서 해석된 프로파일이 아무 예산도 선택하지 않는다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "\"기본 비활성\"은 존재하지 않는 스위치의 기본값을 서술한다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "출하되는 web 어댑터의 스위치가 활성화 모델 밖에 있다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "8개 profile validator 중 조립에서 실행되는 것은 3개", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`messaging-cloudevents`는 출하 leaf이고 starter의 의존이며 소비자가 없다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "접근 검사가 두 갈래로 존재하고, 조립된 쪽이 진단이 약한 쪽이다 (§8.3)", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "claim-check는 starter에 배선 코드가 한 줄도 없다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "admin 스위치가 가드를 켜고 서비스는 켜지 않는다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "조립 경계가 정책 객체 9개를 만들고 서버를 만들지 않는다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "시작 검증기가 유일한 소비자인 설정 키 넷이 아무것도 게이트하지 않는다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "같은 자동 설정 안에서 검증기 하나만 감싸이지 않아 트랜잭션 조건이 검사되지 않는다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "통합 다리의 메타데이터 조립이 메타데이터 예산을 검사하지 않는다", - "kind": "case", - "slug": "grpc-advanced-compat-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-advanced-compat#L120`" - ], - "owning-module": "`grpc-advanced-compat` · priority: `P3`", - "classification": "허용 목록으로 키를 거르지만 값의 크기도, 합계도 보지 않는다. 클래스 javadoc 자신이 예산을 이 정책의 이유 중 하나로 든다 — 흐름의 내부 배관을 네트워크로 보내면 \"it counts against the metadata budget.\" 그리고 같은 저장소의 다른 메타데이터 경로는 예산을 검사한다. `String.valueOf(value)` 이므로 헤더 값이 임의의 객체일 때 그 문자열 표현이 그대로 실린다. Spring Integration 헤더에는 컬렉션이나 도메인 객체가 흔히 들어가므로 값 하나가 클 수 있다. 수정은 이 record 에 `GrpcMetadataBudget` 를 성분으로 추가하고 `metadataFrom` 끝에서 검사하는 것이다. 형태가 이미 옆 리프에 있다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/case/case-grpc-advanced-compat-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-advanced-compat-f01" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-advanced-compat-f01.txt" - ] - }, - { - "title": "정책 목록의 가장 강한 성질을 이 저장소에서는 쓸 수 없다", - "kind": "case", - "slug": "grpc-core-api-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-core-api#L140`" - ], - "owning-module": "`grpc-core-api` · priority: `P3`", - "classification": "`withDescriptorMethods` 를 부르는 곳은 이 리프의 테스트 두 줄뿐이다. `GrpcMethodPolicyCatalog.builder()` 를 부르는 곳은 저장소 전체에서 전부 테스트다. 그리고 그중 어느 것도 서술자 집합을 선언하지 않는다(위 두 줄 제외). 자바독이 그 상태를 미리 서술한다 — 서술자가 없으면 \"the catalog is materially weaker … there is nothing to compare a policy's method name against.\" 그리고 서술자가 없는 이유는 옆 리프에 있다. `grpc-codegen` 이 서술자 산출물을 정의하지만 저장소에 protobuf 플러그인이 없어 `protoc` 이 돌지 않는다. 즉 이름 변경을 잡는 성질은 코드 생성 레인이 켜지기 전까지 성립할 수 없다. 기록하는 이유는 이것이 이 클래스가 존재하는 첫 번째 이유로 적혀 있기 때문이다. 수정은 코드 생성 레인이 생길 때 그 서술자를 목록 조립에 연결하는 것이고, 그때까지는 자바독이 그 조건을 명시하는 편이 낫다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/case/case-grpc-core-api-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-core-api-f01" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-core-api-f01.txt" - ] - }, - { - "title": "`queueHighWatermark` 는 요구되고 검증되지만 아무도 읽지 않는다", - "kind": "case", - "slug": "grpc-observability-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-observability#L197`" - ], - "owning-module": "`grpc-observability` · priority: `P3`", - "classification": "`GrpcStreamObservation` 의 7성분 중 `queueHighWatermark` 만 소비자가 없다. `tags()` 에 없고, `GrpcObservationConvention.record(GrpcStreamObservation)` 이 등록하는 세 meter(`STREAM_LIFETIME`·`STREAM_MESSAGES`·`STREAM_FLOW_CONTROL_STALLS`) 어디에도 들어가지 않는다. 테스트도 `250L` 을 넘기고 그 값에 대해 아무것도 단언하지 않는다. 클래스 javadoc 이 \"what is recorded instead is …\" 로 세 가지를 열거하는데 그 목록에도 없다. 즉 서술과 구현은 일치하고, 어긋난 것은 **필수 생성자 인자**라는 점이다. 호출자는 측정해서 넘겨야 하고 그 값은 버려진다. 수정은 둘 중 하나다 — `STREAM_QUEUE_HIGH_WATERMARK` gauge/counter 를 추가하거나, 성분에서 뺀다. 큐 최고 수위는 소비자 지연의 직접 지표이므로 전자가 이 클래스의 목적에 맞는다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/case/case-grpc-observability-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-observability-f01" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-observability-f01.txt" - ] - }, - { - "title": "`clearAfterTask` 는 합법 값이 하나뿐인 성분이고, 아무도 읽지 않는다", - "kind": "case", - "slug": "grpc-policy-f07", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-policy#L324`" - ], - "owning-module": "`grpc-policy` · priority: `P3`", - "classification": "`false` 를 무조건 거부하므로 이 성분이 가질 수 있는 값은 `true` 하나다. 그리고 저장소 전체에서 `clearAfterTask()` 를 읽는 production 코드가 없다 — 호출처는 이 생성자의 가드와 테스트의 단언 한 줄뿐이다. 읽지 않아도 되는 이유는 `GrpcContextBinder` 가 옳게 쓰였기 때문이다. `runWith`·`callWith`·`wrap` 이 전부 `finally` 에서 detach 한다. 불변식이 이미 구조로 지켜진다. 그래서 이 성분은 설정처럼 보이지만 설정이 아니다. 읽는 사람은 정책으로 끌 수 있는 것이라고 읽고, 테스트는 그 가드를 시험한다. 수정은 성분을 지우고 javadoc 에 \"always cleared\" 를 남기는 것이다. 그러면 `backgroundWork()`·`stable()` 이 인자 하나가 되고, 불변식은 검증이 아니라 구조가 된다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/case/case-grpc-policy-f07.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-policy-f07" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-policy-f07.txt" - ] - }, - { - "title": "빌더 경로에서 순서 규칙 넷 중 셋이 발화할 수 없다", - "kind": "case", - "slug": "grpc-server-f03", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-server#L196`" - ], - "owning-module": "`grpc-server` · priority: `P3`", - "classification": "`byStage` 는 `EnumMap` 이므로 `keySet()` 은 언제나 열거형 선언 순서다. 그리고 `stage(...)` 가 같은 단계의 두 번째 등록을 이미 거부한다. 따라서 빌더가 만드는 목록에서는 중복도, 역순도, 예외 경계가 최외곽이 아닌 경우도 발생할 수 없다. 발화 가능한 규칙은 필수 단계 누락 하나다. 결함은 아니다 — 나머지 셋은 `violations(List)` 를 직접 부르는 외부 호출자를 위한 것이고, 테스트가 그 경로로 셋을 모두 확인한다. 기록하는 이유는 빌더를 쓰는 조립 코드가 그 셋의 보호를 받는다고 읽기 쉽기 때문이다. 실제 보호는 자료구조가 준다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/case/case-grpc-server-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-server-f03" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-server-f03.txt" - ] - }, - { - "title": "시작 검증기가 시작 시 실행되지 않는다", - "kind": "case", - "slug": "grpc-spring-boot-starter-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-spring-boot-starter#L183`" - ], - "owning-module": "`grpc-spring-boot-starter` · priority: `P2`", - "classification": "`GrpcPlatformStartupValidator` 를 이름으로 부르는 파일은 둘뿐이다 — 자기 자신과 자기 테스트. `GrpcPlatformAutoConfiguration` 은 빈 9개를 만들고 `requireValid` 를 부르지 않는다. 초기화 콜백도, `@PostConstruct` 도, `ApplicationRunner` 도 없다. 그래서 클래스 javadoc 이 약속한 성질이 성립하지 않는다 — \"Refuses to start on a configuration that would be wrong in a way nobody would notice.\" 지금은 그 설정으로 그냥 시작한다. 검증기가 유일한 소비자인 설정 키가 넷이다. `transport` — production 이 아닌 전송을 거부할 곳이 없다. 게다가 자동 설정은 이 값을 보지 않고 `GrpcServerProfile.stableNetty(...)` 를 하드코딩한다(§17.2). `tls-enabled` · `trust-all-certificates` — 배포 환경의 TLS 바닥을 강제할 곳이 없다. `operation-ledger-enabled` — 멱등 키 필수 메서드가 원장 없이 열리는 것을 막을 곳이 없다. 같은 저장소가 이 형태를 두 번 기록했다 — `WebPlatformStartupValidator` 가 시작 시 실행되지 않고, `BrokerAclManifest` 의 시작 자기점검이 없다. 반대로 messaging 의 `StartupProfileValidation` 은 `InitializingBean.afterPropertiesSet` 으로 돌려 그 문제를 이미 한 번 해결했고, fileserver 는 `attestMapping()` 을 app-bootstrap 의 `@Bean` 으로 연결했다. 정본이 저장소 안에 둘 있다. `violations` 는 넷을 받는다. 넷 중 셋에 생산자가 없다. 특히 마지막은 \"스타터가 해석한 모듈 id 집합\" 인데, 그것을 실행 중에 산출하는…", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/case/case-grpc-spring-boot-starter-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-spring-boot-starter-f01", - "grpc-spring-boot-starter-f01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-spring-boot-starter-f01.txt" - ] - }, - { - "title": "`default-unary-deadline` 은 읽는 코드가 저장소에 없다", - "kind": "case", - "slug": "grpc-spring-boot-starter-f03", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-spring-boot-starter#L236`" - ], - "owning-module": "`grpc-spring-boot-starter` · priority: `P3`", - "classification": "`getDefaultUnaryDeadline()` 의 호출자가 0 이다. 검증기도 이 값을 쓰지 않는다 — 검증기가 보는 것은 정책 목록의 `policy.deadline().usable()` 이고 그 값이 0 이면 위반을 낸다. 즉 자바독이 말하는 \"선언하지 않은 메서드에 적용되는 기본 마감\" 을 적용하는 코드가 없다. `ignoreUnknownFields = false` 라서 이 키를 설정하는 것은 성공하고 아무 효과가 없다. 수정은 그 기본값을 실제로 적용하는 지점을 만들거나(정책 목록 조립 시), 필드를 제거하는 것이다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/case/case-grpc-spring-boot-starter-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-spring-boot-starter-f03" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-spring-boot-starter-f03.txt" - ] - }, - { - "title": "계약 스위트 둘이 결과를 만드는 코드를 갖지 않는다", - "kind": "case", - "slug": "grpc-testkit-f05", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-testkit#L245`" - ], - "owning-module": "`grpc-testkit` · priority: `P3`", - "classification": "`GrpcUnaryReliabilityContract` 와 `GrpcServerStreamingContract` 는 순수 평가기다 — `List` 를 받아 위반을 돌려준다. 시나리오 정의(단항 3 · 스트리밍 5)와 그 정합성 검사는 훌륭하다. 스위트가 자기 커버리지를 열거하고, 돌지 않은 시나리오를 침묵이 아니라 위반으로 만든다. 빠진 것은 그 시나리오를 **돌리는** 쪽이다. `GrpcUnaryContractResult`·`GrpcStreamingContractResult` 를 만드는 코드는 저장소 전체에서 두 테스트뿐이고, 둘 다 리터럴로 만든다. 그래서 \"이 플랫폼은 비멱등 변경을 재시도하지 않는다\" 를 뒷받침하는 것은, 그 문장을 리터럴로 적은 뒤 평가기가 그것을 읽고 위반이 없다고 답하는 절차다. 평가기의 산술은 옳고, 대상이 관측이 아니다. in-process 픽스처(§1)는 이 시나리오들을 돌릴 재료를 이미 갖고 있다 — 인터셉터를 끼운 서버, 상태 매핑, 스트리밍 핸들러. 수정은 픽스처 위에서 세 시나리오를 실행해 `attempts`·`businessInvocations` 를 세는 러너를 두는 것이다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/case/case-grpc-testkit-f05.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-testkit-f05" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-testkit-f05.txt" - ] - }, - { - "title": "`TopologyManagementMode` 가 어디에도 연결되어 있지 않다", - "kind": "case", - "slug": "messaging-admin-api-f03", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-admin-api#L952`" - ], - "owning-module": "`messaging-admin-api` · priority: `P3`", - "classification": "자기 선언과 테스트 4건이 전부다. 이 enum 을 읽는 프로덕션 코드도, 이것으로 매핑되는 설정 프로퍼티도 없다(`EVD-302`). 두 선택지가 있다: 실제로 배선하거나(선언된 토폴로지 관리 모드를 설정에서 읽고 `requireSafeFor(isProduction)` 를 기동 시 호출), 제거한다. 지금 상태는 \"규칙이 코드에 있다\" 는 인상만 준다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/case/case-messaging-admin-api-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-admin-api-f03" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-admin-api-f03.txt" - ] - }, - { - "title": "운영자용 표면 전체에 프로덕션 소비자가 없다", - "kind": "case", - "slug": "messaging-admin-api-f04", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-admin-api#L956`" - ], - "owning-module": "`messaging-admin-api` · priority: `P3`", - "classification": "`ReplayPlan.describeImpact`, `RedrivePlan.describeImpact`, `ReplayResult.fellShortOfTheEstimate`, `RedriveResult.isFullyAccounted`, `AdminOperationLease.isResumption` — 다섯 개가 전부 테스트에서만 호출된다(`EVD-302`). 이것들은 잉여 코드가 아니라 **아직 소비자가 없는 잘 설계된 표면**이다. `describeImpact` 의 javadoc 이 \"operator-facing\" 이라고 쓰고 `ApprovedPlanExecutionTest.aReplayIntoTheLiveGroupSaysSoInCapitals` 가 대문자 `LIVE` 까지 검증한다. 문제는 그 문자열이 도달할 화면이 없다는 것이다. admin API·CLI 계층을 만들 때 이 다섯이 그 계층의 명세라는 점을 문서에 남겨 두는 것이 낫다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/case/case-messaging-admin-api-f04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-admin-api-f04" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-admin-api-f04.txt" - ] - }, - { - "title": "`messaging-policy` 의존이 import 0건이다", - "kind": "case", - "slug": "messaging-admin-api-f06", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-admin-api#L968`" - ], - "owning-module": "`messaging-admin-api` · priority: `P3`", - "classification": "선언만 남아 있다. 제거 후보.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/case/case-messaging-admin-api-f06.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-admin-api-f06" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-admin-api-f06.txt" - ] - }, - { - "title": "오케스트레이터가 어디에서도 실행되지 않는다", - "kind": "case", - "slug": "messaging-admin-runtime-f03", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-admin-runtime#L918`" - ], - "owning-module": "`messaging-admin-runtime` · priority: `P2`", - "classification": "`DefaultMessagingAdminService` 257줄과 `ReplayService` 99줄이 프로덕션에서도 테스트에서도 인스턴스화되지 않는다(`EVD-307`). 검사 순서·저널 시퀀스·실패 시 재던짐·실패 코드 정제가 전부 미검증이다. `DefaultMessagingAdminService` 의 생성자는 10개 인자를 받고 그중 8개가 SPI 또는 `Supplier` 이므로, 대역으로 조립하는 테스트를 쓰는 비용은 낮다. §12.1(a)의 회귀 테스트도 이 층에서 쓰는 것이 자연스럽다 — 저널·리스·리드라이브 루프가 함께 도는 것이 결함이 나타나는 조건이기 때문이다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/case/case-messaging-admin-runtime-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-admin-runtime-f03", - "messaging-admin-runtime-f03-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-admin-runtime-f03.txt" - ] - }, - { - "title": "public 인터페이스를 패키지 밖에서 구현할 수 없다", - "kind": "case", - "slug": "messaging-admin-runtime-f04", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-admin-runtime#L924`" - ], - "owning-module": "`messaging-admin-runtime` · priority: `P3`", - "classification": "`RedriveEstimator`(public)의 반환 타입 `RedriveEstimate` 가 package-private 이다(`EVD-308`). `DefaultMessagingAdminService` 의 public 생성자가 그 인터페이스를 요구하므로, 외부 조립이 불가능하다. `RedriveEstimate` 를 public 으로 올리는 것이 최소 수정이다. 더 나은 방향은 `DefaultMessagingAdminService` 밖의 최상위 record 로 꺼내는 것 — 지금은 오케스트레이터의 내부 타입이 SPI 계약의 일부가 되어 있다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/case/case-messaging-admin-runtime-f04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-admin-runtime-f04" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-admin-runtime-f04.txt" - ] - }, - { - "title": "선언된 의존 6개 중 3개가 import 0건", - "kind": "case", - "slug": "messaging-admin-runtime-f09", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-admin-runtime#L957`" - ], - "owning-module": "`messaging-admin-runtime` · priority: `P3`", - "classification": "`messaging-policy`, `messaging-transport-spi`, `messaging-security`. 제거 후보.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/case/case-messaging-admin-runtime-f09.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-admin-runtime-f09" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-admin-runtime-f09.txt" - ] - }, - { - "title": "배포 아티팩트가 싣지만 아무도 부르지 않고, 다른 곳의 에러 메시지가 이 경로를 권한다", - "kind": "case", - "slug": "messaging-claim-check-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-claim-check#L507`" - ], - "owning-module": "`messaging-claim-check` · priority: `P2`", - "classification": "여섯 타입 전부 leaf 밖 참조 0, `ClaimCheckStore` 구현이 테스트 fake뿐, 조립 0건. 그런데 `runtime_memberships`가 `[\"app-bootstrap\"]`이고 starter의 `allowed_dependencies`에 포함된다. 그리고 `messaging-policy`의 `PayloadLimitGuard`가 상한 초과 payload를 거절하며 `\"payload of %d bytes exceeds the %d byte limit for %s; use claim check\"`라고 안내한다. 운영자가 상한 초과 오류를 보고 안내대로 claim check를 켜려 해도 켤 것이 없다 — 저장소 구현도, bean도, 오프로드를 부르는 발행 경로도 없다. 그리고 `DestinationProfile`이 `claimCheckThresholdBytes`를 선언하고 검증까지 하므로 **설정 표면은 존재한다.** 설정할 수 있고 아무 효과가 없는 값이다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/case/case-messaging-claim-check-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-claim-check-f01", - "messaging-claim-check-f01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-claim-check-f01.txt" - ] - }, - { - "title": "배포 아티팩트가 싣지만 아무도 부르지 않는다", - "kind": "case", - "slug": "messaging-cloudevents-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-cloudevents#L529`" - ], - "owning-module": "`messaging-cloudevents` · priority: `P2`", - "classification": "세 타입의 leaf 밖 참조가 0인데 `runtime_memberships`가 `[\"app-bootstrap\"]`이다. `messaging-spring-boot-starter`의 의존 목록에 있어 `cloudevents-api`·`cloudevents-core` 두 jar가 런타임 classpath에 오른다. starter에 `CloudEventMapper`를 만드는 `@Bean`이 없다. 형제 Avro·Protobuf는 소비자 0과 membership `[]`이 일치하는 정합적 incubating 상태다. 이 leaf만 어긋난다. 오늘 실행되는 코드가 없으므로 사고는 아니지만, 아티팩트 크기와 \"이 의존성이 왜 있지\"의 조사 비용이 남는다. 그리고 `support-matrix.md:23`이 \"모든 messaging leaf가 unwired\"라고 적고 있어 문서에서도 이 사실을 알 수 없다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/case/case-messaging-cloudevents-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-cloudevents-f02", - "messaging-cloudevents-f02-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-cloudevents-f02.txt" - ] - }, - { - "title": "bounded purge가 구현돼 있고 호출되지 않아, cleanup이 스스로 막겠다고 한 장애를 일으킨다", - "kind": "case", - "slug": "messaging-inbox-jdbc-postgresql-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-inbox-jdbc-postgresql#L647`" - ], - "owning-module": "`messaging-inbox-jdbc-postgresql` · priority: `P1`", - "classification": "`InboxRepository`·`OutboxRepository` 둘 다 `purge*Before(Instant, int)` 오버로드를 선언하고, `JdbcInboxRepository:141`·`JdbcOutboxRepository:486`이 `LIMIT` + `FOR UPDATE SKIP LOCKED`로 구현한다. 저장소 전체에서 그 시그니처가 등장하는 9곳은 **선언 2 + 구현 2 + 테스트 fake override 5**이고 **호출 지점이 0**이다. `InboxCleanupJob:56`과 `OutboxCleanupJob:50`이 무제한 오버로드를 부른다. `InboxCleanupJob.DEFAULT_BATCH_SIZE = 1_000`은 자기 선언 한 줄만 존재한다. `InboxCleanupJob`의 javadoc이 스스로 적는다 — *\"A single unbounded DELETE over a table that has been accumulating for weeks holds locks long enough to block the very reservations the inbox exists to serve, so the cleanup would cause the outage it is meant to prevent.\"* 실행되는 코드가 정확히 그 문장이 서술하는 동작이다. `OutboxRepository`의 bounded 오버로드 javadoc은 한 발 더 나간다 — *\"The cleanup jobs describe themselves as bounded by batch size; **this is the parameter that makes that true**.\"* 그 파라미터를 아무도 넘기지 않는다. 그리고 두 leaf가 **동일한 형태로** 그렇다. **왜 P1인가.** 두 leaf 다 `runtime_memberships: [\"app-bootstrap\"]`이고 두 cleanup job이 starter에서 bean으로 만들어진다(`Mes…", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/case/case-messaging-inbox-jdbc-postgresql-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-inbox-jdbc-postgresql-f01", - "messaging-inbox-jdbc-postgresql-f01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-inbox-jdbc-postgresql-f01.txt" - ] - }, - { - "title": "관측 구현이 조립되지 않고, 그 재료 둘만 bean으로 존재한다", - "kind": "case", - "slug": "messaging-observability-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-observability#L682`" - ], - "owning-module": "`messaging-observability` · priority: `P2`", - "classification": "`MessagingMetrics`는 `MessagingObservation`의 유일한 구현이고 저장소 전체에서 자기 테스트에서만 생성된다. starter는 그 생성자 인자 둘(`MessagingRedactor:253`, `CardinalityGuard:264`)을 bean으로 만들고 `MessagingMetrics` bean은 만들지 않는다. `DefaultMessagePublisher`는 `NO_OBSERVATION`을 쓰는 6인자 생성자로 조립된다. 재료·구현·seam·호출부가 전부 있고 조립 한 줄이 없다. 그리고 두 재료 bean은 주입처가 0이므로 컨텍스트에 앉아만 있다 — bean 존재 검사는 통과한다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/case/case-messaging-observability-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-observability-f02", - "messaging-observability-f02-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-observability-f02.txt" - ] - }, - { - "title": "정리 작업이 무제한 DELETE 를 쏘고, 그것을 막는 오버로드는 호출되지 않는다", - "kind": "case", - "slug": "messaging-outbox-jdbc-postgresql-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-outbox-jdbc-postgresql#L876`" - ], - "owning-module": "`messaging-outbox-jdbc-postgresql` · priority: `P1`", - "classification": "`OutboxCleanupJob:50` 과 `InboxCleanupJob:56` 이 무제한 오버로드를 부른다. bounded 오버로드(`purgePublishedBefore(Instant, int)` / `purgeProcessedBefore(Instant, int)`)는 두 포트에 선언되고 두 구현에 구현되어 있으며 호출부가 **0건**이다(`EVD-294`, `EVD-311`). 두 잡 모두 starter 빈이지만 스케줄러는 등록되지 않으며, 그것은 의도된 설계다(`EVD-316`). 즉 기본 배포에서는 아무 일도 일어나지 않고, 애플리케이션이 문서 지시대로 잡을 스케줄하는 순간 무제한 DELETE 가 발동한다. 잠재 결함이지 상시 결함이 아니다. bounded 구현의 주석이 결과를 명시한다: *\"An unbounded DELETE holds locks and writes WAL in proportion to the whole backlog, which stalls the relay and the business writes behind retention.\"* 3일치 백로그가 쌓인 테이블에서 이것은 릴레이 정지와 비즈니스 쓰기 정체를 뜻한다. 두 리프 모두 `runtime_memberships: [\"app-bootstrap\"]` 이고 두 잡 모두 starter 빈이다. 수정은 한 줄이다 — `purgePublishedBefore(cutoff, batchLimit)`. `maxBatches` 가 그제서야 의미를 갖는다. 배치 크기는 새 파라미터가 필요하고, `OutboxProperties.batchSize`(100)를 재사용하거나 별도 값을 둔다. 그리고 **회귀 테스트가 성립하려면 `RecordingRepository` 를 고쳐야 한다.** 현재 대역의 bounded 구현은 `Math.min(unbounded(), limit)` 로, 전부 지우고 숫자만 깎는다. 실제 저장소를 흉내 내려면 보유 행 목록을 갖고 `limit` 만큼만 제거해야 한다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/case/case-messaging-outbox-jdbc-postgresql-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-outbox-jdbc-postgresql-f01", - "messaging-outbox-jdbc-postgresql-f01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-outbox-jdbc-postgresql-f01.txt" - ] - }, - { - "title": "두 릴레이 상호배제가 기동에서 강제되지 않는다", - "kind": "case", - "slug": "messaging-outbox-jdbc-postgresql-f04", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-outbox-jdbc-postgresql#L909`" - ], - "owning-module": "`messaging-outbox-jdbc-postgresql` · priority: `P2`", - "classification": "`DebeziumOutboxProfile.requireExactlyOneRelay(...)` 는 프로덕션 호출부가 0건이다. 클래스 javadoc 은 \"the incompatibility is therefore enforced at startup instead of documented\" 라고 쓴다. properties 파일도 같은 경고를 반복한다(\"Enable this OR the in-process polling relay, never both\"). 같은 리프에 정확히 이 형태를 고친 선례가 있다 — `OutboxRelayWorker` 가 \"nothing ever called `runOnce`\" 를 고치고 `MessagingOutboxRelayLifecycle` 로 배선까지 마쳤다. 같은 방식으로 `MessagingReliabilityAutoConfiguration` 에 프로필 빈과 `InitializingBean` 검사를 두면 된다. 배선하려면 CDC 모드를 선택할 프로퍼티도 필요하다 — 지금은 `DebeziumOutboxProfile` 을 만드는 설정 경로 자체가 없다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/case/case-messaging-outbox-jdbc-postgresql-f04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-outbox-jdbc-postgresql-f04", - "messaging-outbox-jdbc-postgresql-f04-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-outbox-jdbc-postgresql-f04.txt" - ] - }, - { - "title": "능력 상수의 `delayedDelivery` 가 무조건 참이고, 그 지연을 제공할 토폴로지는 조립되지 않는다", - "kind": "case", - "slug": "messaging-rabbit-f04", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-rabbit#L307`" - ], - "owning-module": "`messaging-rabbit` · priority: `P3`", - "classification": "그런데 지연을 실제로 만드는 것은 `RabbitRetryQueueTopology` 이고, 그 클래스는 자기 파일과 시험 하나 밖에서 참조되지 않는다. 어떤 production 코드도 그 큐를 선언하지 않는다. 그리고 그 클래스의 javadoc 이 이 지연의 성질을 정확히 적는다. 즉 제공되는 것은 \"메시지별 지연\" 이 아니라 \"재시도 큐 하나당 TTL 하나\" 다. 능력 모델에는 그 구분을 표현하는 자리가 없고, 상수는 프로파일과 무관하게 참을 답한다. Kafka 는 같은 칸을 `false` 로 둔다. 그래서 이 플래그의 두 값이 \"지연 있음/없음\" 이 아니라 \"지연을 흉내낼 토폴로지를 선언할 수 있음/없음\" 을 뜻하게 된다. 수정은 능력을 전송 상수가 아니라 목적지의 재시도 큐 선언에서 파생시키는 것이다. 이 리프가 조립되지 않는 동안에는 P3 이고, `RabbitChannelPublisher` 구현이 생기는 날 함께 봐야 한다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/case/case-messaging-rabbit-f04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-rabbit-f04" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-rabbit-f04.txt" - ] - }, - { - "title": "fencing token 경로가 실제 데이터베이스에 대해 실행되지 않는다", - "kind": "case", - "slug": "messaging-reliability-api-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-reliability-api#L698`" - ], - "owning-module": "`messaging-reliability-api` · priority: `P2`", - "classification": "`OutboxRelay`는 `claimBatch`/lease 기반 전이만 쓴다. `OutboxPostgresIT`는 `leaseBatch`/`MessageId` 기반 전이만 쓴다. 신세대를 쓰는 다른 테스트는 `InMemoryOutboxRepository`와 `RecordingRepository` — SQL이 없는 fake다. fencing의 정확성은 구현의 조건부 UPDATE가 영향 행 수를 정확히 세는지에 달려 있다. `OutboxTransitionResult.STALE_LEASE`는 \"its update matches zero rows\"에서 나오고, 그것은 SQL의 성질이지 Java의 성질이 아니다. in-memory fake는 그 SQL을 실행하지 않는다. 즉 **이중 발행을 막는 장치가 그것을 검증할 수 있는 유일한 환경에서 실행되지 않는다.**", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/case/case-messaging-reliability-api-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-reliability-api-f02", - "messaging-reliability-api-f02-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-reliability-api-f02.txt" - ] - }, - { - "title": "관측이 구현·호출부·주입 자리를 모두 갖추고도 출하에서 no-op이다", - "kind": "case", - "slug": "messaging-runtime-core-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-runtime-core#L710`" - ], - "owning-module": "`messaging-runtime-core` · priority: `P2`", - "classification": "`DefaultMessagePublisher`가 모든 발행 결과를 `observation.recordPublish(...)`로 기록하고, 관측을 \"constructor argument rather than an optional decorator\"로 받는다. `MessagingMetrics`가 `MessagingObservation`을 구현한다. 그런데 출하 조립(`MessagingCoreAutoConfiguration:446`)은 **6인자 생성자**를 써서 `NO_OBSERVATION`을 넣고, `MessagingMetrics`는 저장소 전체에서 자기 테스트에서만 생성된다. starter는 `MessagingMetrics`의 협력자 둘(`MessagingRedactor:253`, `CardinalityGuard:264`)을 bean으로 만든다. 이 필드의 javadoc이 정확히 이 상황을 막으려고 쓰였다 — \"an unobserved publish path is how 'the dashboards were empty during the incident' happens\". 그리고 같은 javadoc이 **이전 결함**을 \"bean은 있고 호출 경로가 없었다\"로 기록한다. 지금은 반대다 — 호출 경로가 있고 bean이 없다. 관측 결과는 같다. **고침이 간극을 닫은 게 아니라 반대편으로 옮겼다.** \"decorator가 아니라 생성자 인자\"라는 선택도 막지 못했는데, 인자를 기본값으로 채우는 짧은 생성자가 함께 있기 때문이다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/case/case-messaging-runtime-core-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-runtime-core-f01", - "messaging-runtime-core-f01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-runtime-core-f01.txt" - ] - }, - { - "title": "소비 오케스트레이터가 조립되지 않는다", - "kind": "case", - "slug": "messaging-runtime-core-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-runtime-core#L719`" - ], - "owning-module": "`messaging-runtime-core` · priority: `P2`", - "classification": "`DefaultDeliveryProcessor`는 leaf 밖 참조 0, `src/main` 생성 0, `src/test` 생성 1이다. 이 클래스가 고친 문제(\"각 어댑터가 retry/dead-letter의 뜻을 각자 결정\")가 배선 없이는 그대로 남는다. 그리고 `DeclaredDestinationAccess`가 consume 권한을 빈 집합으로 두는 것과 정합적이다 — 기본 구성은 소비를 상정하지 않는다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/case/case-messaging-runtime-core-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-runtime-core-f02", - "messaging-runtime-core-f02-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-runtime-core-f02.txt" - ] - }, - { - "title": "포맷 독립 진화 규칙이 호출되지 않고, 그것이 막으려던 중복이 실제로 생겼다", - "kind": "case", - "slug": "messaging-schema-api-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-schema-api#L494`" - ], - "owning-module": "`messaging-schema-api` · priority: `P2`", - "classification": "`SchemaCompatibilityValidator`의 저장소 전체 참조가 자기 선언과 자기 테스트뿐이다. 동시에 `AvroCompatibilityGate`가 `isTransitive`를 글자 그대로 복사했고 방향 판정 둘은 허용목록/거부목록으로 형태가 반대다. 오늘은 7개 모드 전부에서 두 구현의 결과가 같다(`NONE_EXPERIMENTAL`은 gate의 early return이 가린다). 그러나 enum에 값이 하나 추가되면 허용목록은 \"검사 안 함\", 거부목록은 \"양방향 검사\"로 **반대 방향** 기본값을 갖는다. 그리고 `requireProductionMode` — 검사 없는 스키마가 보존 로그를 뒷받침하는 것을 막는 게이트 — 는 호출되는 곳이 없다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/case/case-messaging-schema-api-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-schema-api-f01", - "messaging-schema-api-f01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-schema-api-f01.txt" - ] - }, - { - "title": "종료 시 자격증명 소거가 호출되지 않는다", - "kind": "case", - "slug": "messaging-security-f04", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-security#L661`" - ], - "owning-module": "`messaging-security` · priority: `P3`", - "classification": "`CredentialRuntimeRegistry.clearAll()`의 javadoc이 \"Clears every held credential, for shutdown\"이라고 하고, 호출자가 저장소에 없다. 이 leaf 전체가 \"비밀이 힙에 남지 않게 한다\"를 목적으로 하고(`char[]`, `clear()`, 회전 시 즉시 소거), 종료 경로에서 그 마지막 단계가 빠져 있다. 프로세스가 끝나면 힙도 사라지지만, 종료가 느리거나 힙 덤프가 뜨는 경우가 정확히 이 통제가 노리는 상황이다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/case/case-messaging-security-f04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-security-f04" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-security-f04.txt" - ] - }, - { - "title": "같은 자동 설정 안에서 검증기 하나만 감싸이지 않는다", - "kind": "case", - "slug": "messaging-spring-boot-starter-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-spring-boot-starter#L281`" - ], - "owning-module": "`messaging-spring-boot-starter` · priority: `P2`", - "classification": "`KafkaMessagingAutoConfiguration` 은 검증기 셋을 만든다. `KafkaTransactionProfileValidator` 에는 대응하는 `StartupProfileValidation` 이 없다. 즉 컨텍스트가 그 검증기를 발행하고 아무도 주입하지 않는다 — `StartupProfileValidation` 의 javadoc 이 서술한 이전 상태와 정확히 같은 형태다. `RabbitMessagingAutoConfiguration` 은 검증기 하나이고 그것을 감싼다. 그러므로 이 가족에서 감싸이지 않은 검증기는 이 하나다. 트랜잭션 프로파일 검증이 무엇을 막는지는 그 클래스가 안다 — 비트랜잭션 생산자 위의 정확히 한 번 주장 같은 조합이다. 그 검증이 지금 돌지 않는다. 수정은 한 블록이다. 같은 파일의 `kafkaProfileStartupValidation` 형태를 복사해 세 번째 검증기를 감싼다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/case/case-messaging-spring-boot-starter-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-spring-boot-starter-f01", - "messaging-spring-boot-starter-f01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-spring-boot-starter-f01.txt" - ] - }, - { - "title": "`FaultController` 의 5개 중 2개가 구현만 3벌 있고 호출부가 0건이다", - "kind": "case", - "slug": "messaging-testkit-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-testkit#L940`" - ], - "owning-module": "`messaging-testkit` · priority: `P2`", - "classification": "`rejectPublish()` 와 `reset()` 은 인터페이스에 선언되어 세 하니스가 전부 구현하지만, 계약 스위트를 포함해 어디에서도 호출되지 않는다(`EVD-299`). `rejectPublish` 는 심지어 세 하니스의 `publish()` 경로에 완전히 배선되어 있다(`KafkaContractHarness:119`, `RabbitContractHarness:85`, `InMemoryMessagingHarness:66`) — 켜는 스위치만 아무도 누르지 않는다. 이것이 단순한 미사용 코드가 아닌 이유: 미사용 경로가 **틀린 값을 인코딩하고 있다**. `InMemoryMessagingHarness` 에서 `rejectPublish` 는 `rejected(\"BROKER_REJECTED\", …)` 를 돌려주고, 그 헬퍼는 `PublishEvidence.notTransmitted()` 를 쓴다(`:184-193`). `TransmissionEvidence.NOT_TRANSMITTED` 의 javadoc 은 \"Nothing was written to the broker connection.\" 이다. 그런데 `FaultController.rejectPublish` 의 javadoc 은 \"refused outright by **the broker**\" 다 — 브로커가 거절하려면 바이트가 나갔어야 하므로 `TRANSMITTED` 여야 한다. 이 플랫폼은 전송 증거를 세 값으로 구분하는 것을 핵심 가치로 삼는데, 유일하게 실행되지 않는 경로에 그 구분의 오류가 들어 있다. `connection-refused` 시나리오(유일하게 증거가 없는 시나리오, `Expectation.REJECTED`)와 이 미사용 결함이 같은 빈칸을 가리킨다. 둘 중 하나를 택해야 한다: 계약에 `rejectsWhenBrokerRefusesBeforeTransmission` 를 추가하고 전송 증거를 바로잡거나, `rejectPublish` 를 인터페이스에서 제거해 세 하니스의 구현 부담을 …", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/case/case-messaging-testkit-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-testkit-f01", - "messaging-testkit-f01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-testkit-f01.txt" - ] - }, - { - "title": "1 MiB 한도가 `PayloadPolicy` 를 두고 리터럴로 재선언된다", - "kind": "case", - "slug": "messaging-testkit-f04", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-testkit#L966`" - ], - "owning-module": "`messaging-testkit` · priority: `P3`", - "classification": "`PayloadPolicy.DEFAULT_MAX_BYTES = 1_048_576` 이 정본인데 같은 값이 최소 8곳에 다시 있고(§12.3(b)), 그중 둘이 이 리프다(`InMemoryMessagingHarness:31`, `ContractMessage:50`). `messaging-testkit` 은 `api project(':messaging:messaging-policy')` 를 이미 선언하고 있으므로 import 한 줄이면 된다. 지금은 `messaging-policy` 의존이 import 0건이라 선언만 남아 있는데(§12.4(b)), 이 자리가 그 의존이 실제로 쓰여야 할 곳이다. `ContractMessage.oversized()` 의 `1_048_577` 은 `PayloadPolicy.DEFAULT_MAX_BYTES + 1` 로 쓰면 \"한도 바로 위 한 바이트\" 라는 의도가 코드에 드러난다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/case/case-messaging-testkit-f04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-testkit-f04" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-testkit-f04.txt" - ] - }, - { - "title": "`messaging-transport-spi` 의존이 import 0건이다", - "kind": "case", - "slug": "messaging-testkit-f05", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-testkit#L972`" - ], - "owning-module": "`messaging-testkit` · priority: `P3`", - "classification": "policy 와 달리 transport-spi 는 쓸 자리가 보이지 않는다. 제거 후보.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/case/case-messaging-testkit-f05.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-testkit-f05" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-testkit-f05.txt" - ] - }, - { - "title": "`BrokerFailureMatrix.adapters()` 는 호출부가 0건이다", - "kind": "case", - "slug": "messaging-testkit-f06", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-testkit#L976`" - ], - "owning-module": "`messaging-testkit` · priority: `P3`", - "classification": "public 메서드이나 아무도 쓰지 않는다. 이 리프의 다른 public 표면은 전부 소비자가 있다. 제거하거나, 진단용이라면 그렇게 적는다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/case/case-messaging-testkit-f06.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-testkit-f06" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-testkit-f06.txt" - ] - } - ], - "concept": [], - "reference": [ - { - "title": "검증기는 발행이 아니라 주입이 강제다", - "kind": "reference", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다", - "kind": "reference", - "slug": "messaging-claim-check-f04", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-claim-check#L534`" - ], - "owning-module": "`messaging-claim-check`", - "rule": "leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — `ClaimCheckPublisher`가 이 leaf의 테스트에 등장하지 않는다", - "scope": [ - "멱등·아웃박스·인박스 계열. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/reference/reference-messaging-claim-check-f04.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "허용 의존 목록은 상한이므로 미사용을 잡지 않는다 — 그것을 잡으려면 별도 검사가 필요하다", - "kind": "reference", - "slug": "messaging-kafka-share-experimental-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-kafka-share-experimental#L476`" - ], - "owning-module": "`messaging-kafka-share-experimental`", - "rule": "허용 의존 목록은 상한이므로 미사용을 잡지 않는다 — 그것을 잡으려면 별도 검사가 필요하다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 선언된 의존 셋이 사용되지 않는다", - "scope": [ - "메시징 어댑터의 소비·발행 경로. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/reference/reference-messaging-kafka-share-experimental-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "만들어 두고 흘리지 않는 진단값은 진단이 아니다", - "kind": "reference", - "slug": "messaging-runtime-core-f07", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-runtime-core#L763`" - ], - "owning-module": "`messaging-runtime-core`", - "rule": "만들어 두고 흘리지 않는 진단값은 진단이 아니다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — `missingResult()`가 아무 데도 쓰이지 않는다", - "scope": [ - "메시징 어댑터의 소비·발행 경로. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/reference/reference-messaging-runtime-core-f07.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "배선된 게이트는 자기 leaf 레인에서 검증한다", - "kind": "reference", - "slug": "messaging-security-f07", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-security#L688`" - ], - "owning-module": "`messaging-security`", - "rule": "배선된 게이트는 자기 leaf 레인에서 검증한다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 다섯 타입이 이 leaf의 테스트에 등장하지 않는다", - "scope": [ - "보안·승인 표면. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/reference/reference-messaging-security-f07.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "허용 의존 목록은 상한이므로 미사용을 잡지 않는다", - "kind": "reference", - "slug": "messaging-spring-cloud-stream-bridge-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-spring-cloud-stream-bridge#L552`" - ], - "owning-module": "`messaging-spring-cloud-stream-bridge`", - "rule": "허용 의존 목록은 상한이므로 미사용을 잡지 않는다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 선언된 의존 둘이 사용되지 않는다", - "scope": [ - "정책과 조립 경계. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/reference/reference-messaging-spring-cloud-stream-bridge-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "등록을 받는 컴포넌트는 해제도 제공한다", - "kind": "reference", - "slug": "messaging-spring-cloud-stream-bridge-f05", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-spring-cloud-stream-bridge#L588`" - ], - "owning-module": "`messaging-spring-cloud-stream-bridge`", - "rule": "등록을 받는 컴포넌트는 해제도 제공한다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 등록 해제 경로가 없다", - "scope": [ - "정책과 조립 경계. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/reference/reference-messaging-spring-cloud-stream-bridge-f05.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - } - ], - "question": [ - { - "title": "선언된 핸들러 계약이 배선된 것과 다르다", - "kind": "question", - "slug": "messaging-core-api-f01", - "readiness": "OPEN", - "source": [ - "`final/document.md#a19-messaging-core-api#L835`" - ], - "owning-module": "`messaging-core-api` · priority: `P2`", - "classification": "`MessageHandler`(`delivery/MessageHandler.java:14`)의 저장소 전체 참조가 0이다. 핸들러 결과를 정산으로 바꾸는 유일한 지점 `DefaultDeliveryProcessor`는 `Function, HandleResult>`를 받는다. `MessageDelivery`가 빠지면서 `deliveryAttempt`·`redelivered`·`handlerDeadline`·`shutdownRequested`가 핸들러에 도달할 수 없다. `DeliveryContext`의 javadoc이 설명하는 graceful drain 협력은 현재 배선으로는 성립하지 않는다. 그리고 새 소비자를 붙이는 사람은 공개 API에서 `MessageHandler`를 먼저 보게 되는데, 그것을 구현해도 아무 데도 꽂히지 않는다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/question/openquestion-messaging-core-api-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "12개 예외가 선언만 되어 있다", - "kind": "question", - "slug": "messaging-core-api-f03", - "readiness": "OPEN", - "source": [ - "`final/document.md#a19-messaging-core-api#L862`" - ], - "owning-module": "`messaging-core-api` · priority: `P3`", - "classification": "23개 구체 예외 중 12개가 leaf 밖 참조 0이다(§6.2 표). 지금 당장 깨지는 것은 없다. 다만 `MessagePublishAmbiguousException`처럼 설계의 중심 개념에 이름을 준 타입이 던져지지 않으면, 그 개념이 실제로 어떤 경로로 표현되는지(결과 record)를 읽는 사람이 스스로 알아내야 한다. 그리고 `src/messaging/CLAUDE.md:44` — \"새 public 타입은 그 모듈의 계약이다. 삭제·시그니처 변경은 breaking change로 취급한다\" — 때문에 나중에 정리하는 비용이 계속 커진다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/question/openquestion-messaging-core-api-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "형제 어댑터 넷이 구현하는 SPI를 이 leaf만 구현하지 않는다", - "kind": "question", - "slug": "messaging-kafka-share-experimental-f03", - "readiness": "OPEN", - "source": [ - "`final/document.md#a19-messaging-kafka-share-experimental#L485`" - ], - "owning-module": "`messaging-kafka-share-experimental` · priority: `P3`", - "classification": "`KafkaMessagingTransport`·`RabbitMessagingTransport`·`PulsarMessagingTransport`·`NatsJetStreamTransport`가 전부 `MessagingTransport`를 구현한다. 이 leaf는 `TransportConsumerRegistration`만 부분 구현한다. `KafkaShareWorkQueueCapability`가 존재하는 이유(\"shared validators refuse … before a message is ever produced\")가 실현되려면 `MessagingTransport.capabilities(DestinationName)`를 통해 값이 전달돼야 한다. 그 인터페이스를 구현하지 않으므로 capability는 아무도 읽지 않는 상수다. 두 experimental 형제(pulsar, nats)는 구현하므로 \"experimental이라서\"가 이유가 되지 않는다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/question/openquestion-messaging-kafka-share-experimental-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "출하 컨텍스트가 발행은 하고 소비는 하지 못한다", - "kind": "question", - "slug": "messaging-policy-f02", - "readiness": "OPEN", - "source": [ - "`final/document.md#a19-messaging-policy#L790`" - ], - "owning-module": "`messaging-policy` · priority: `P2`", - "classification": "`KafkaConsumerRegistrar`·`RabbitConsumerRegistrar`·`KafkaBatchConsumerRegistrar`·`RabbitBatchConsumerRegistrar`·`DefaultDeliveryProcessor`·`KafkaRetryExecutor`·`KafkaDeadLetterPublisher`·`RabbitDeadLetterPublisher`가 전부 `src/main` 생성 0이다. 대조군인 발행 경로(`DefaultMessagePublisher`·`TransportMessagingRuntime`)는 `MessagingCoreAutoConfiguration:446,476`에서 생성된다. `messaging-policy`의 두 축이 미배선인 근본 원인이고, `final/document.md#a19-messaging-core-api` §12.1이 관측한 `MessageHandler` 참조 0의 조립 쪽 설명이다. 그리고 `docs/messaging/support-matrix.md`의 브로커 등급표가 소비 측 보장(순서·정산·재시도)을 서술하는데, 그 보장을 수행할 코드가 조립되지 않는다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/question/openquestion-messaging-policy-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "dual-write의 답이라고 선언한 진입점에 구현이 없다", - "kind": "question", - "slug": "messaging-reliability-api-f03", - "readiness": "OPEN", - "source": [ - "`final/document.md#a19-messaging-reliability-api#L707`" - ], - "owning-module": "`messaging-reliability-api` · priority: `P2`", - "classification": "`ReliableMessagePublisher`가 구현 0, 참조 0이다. javadoc은 \"This is the answer to the dual-write problem\"이라고 한다. `OutboxRepository.append`가 있으므로 outbox에 행을 넣을 방법이 없는 것은 아니다. 그러나 그 포트는 저장소 계약이고, `ReliableMessagePublisher`는 애플리케이션이 저장소를 직접 만지지 않게 하려고 존재한다. 그리고 **애플리케이션은 ArchUnit 규칙 때문에 이 leaf를 참조할 수 없으므로** 브리지 어댑터가 필요한데 그것이 없다. 즉 이 leaf의 Outbox 절반은 \"릴레이가 읽는 쪽\"만 배선돼 있고 \"애플리케이션이 쓰는 쪽\"이 비어 있다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/question/openquestion-messaging-reliability-api-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "빈 registry로 조립되면 모든 메시지가 거절된다", - "kind": "question", - "slug": "messaging-schema-json-f03", - "readiness": "OPEN", - "source": [ - "`final/document.md#a19-messaging-schema-json#L481`" - ], - "owning-module": "`messaging-schema-json` · priority: `P3`", - "classification": "`contracts.getIfAvailable(MessageContracts::none)`이 기본값이므로 `MessageContracts` bean이 없으면 빈 registry로 codec이 만들어진다. 그 codec은 시작에 성공하고 첫 publish에서 `UNKNOWN_MESSAGE_TYPE`으로 실패한다. `messaging-core-api` 계열의 다른 leaf에서 관측된 것과 같은 형태다 — \"시작은 하고 첫 쓰기에서 실패한다.\"", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/question/openquestion-messaging-schema-json-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - } - ], - "decision": [] - } - }, - "verification-path-coverage": { - "topic": "verification-path-coverage", - "title": "검증 경로 커버리지 — 초록불이 실제 production 경로를 검증하는가", - "readerQuestion": "", - "kinds": { - "case": [ - { - "title": "Stable completion-evidence capability가 shipped composition에 설치되지 않는다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`collection-fetch-pagination` blocking release gate가 실제 위험을 증명하지 않는다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "nightly workflow가 광고하는 세 가지 중 하나를 lane이 실제로 관측하지 않는다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "selected base card `jpa-flyway-migration`의 producer가 현재 revision에서 실패한다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "selected base card 3개의 evidence tag가 production code 없는 fixture로 충족된다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "D3 gateway가 문서화한 검사 순서에 존재하지 않는 단계가 있다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "admin gateway의 두 audit 경로 중 하나만 fail-closed다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "sharding admin gateway의 네 작업 중 셋은 어떤 입력으로도 완료될 수 없다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "promotion 증거 어휘가 둘이고, gate는 하나만 검사한다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "커버리지 gate 둘이 나란히 있고, 하나는 발화할 수 없다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "release gate가 실제로 차단하는 것은 hermetic test 3개이고, mongo용 CI workflow는 없다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "소비자가 없는 fixture 셋", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "R1과 R2의 설정 취급이 비대칭이고, 검증된 쪽은 하나뿐이다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "문서가 지목한 기본값 위치와 test 목록이 실제와 다르다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`TransferBufferPool.maxBorrowedBytes()`가 자기 회귀 test를 지목하는데 그 test가 읽지 않는다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "README readiness 표와 build.gradle 주석이 실제 소스와 어긋난다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "Pub/Sub 채널만 렌더 크기 검증을 받지 않는다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "위반 코드 34종 중 22종이 어떤 test에서도 이름으로 확인되지 않는다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "용량 보호 계층 전체(41 main files)가 자기 테스트 픽스처 안에서만 실행된다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "크로스 스택 게이트가 검증하는 조립은 픽스처의 조립이고, 플랫폼의 조립이 아니다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`@oneOf` 게이트와 런타임 검증기가 미배선이고, \"플랫폼이 강제한다\"는 서술이 그것을 넘어선다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "파싱·검증 실패에 플랫폼 매퍼가 없다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "실패는 환경 원인이며, 그 테스트의 도구 가드가 불완전하다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`messaging-reliability-api`는 main 13파일 · 817 LOC에 테스트가 0개다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "스키마 호환성 검증기는 출하 leaf에 있고, main 코드에서 호출되지 않는다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`messaging-admin-api`는 main 25파일 · 1,613 LOC에 테스트 파일이 1개다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`MessagingPublicSurfaceContractTest`가 가족 밖(app-bootstrap)에 있다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`rejectNewAdmission()` 이 단계만 기록하고 아무것도 거절하지 않는다", - "kind": "case", - "slug": "grpc-admin-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-admin#L131`" - ], - "owning-module": "`grpc-admin` · priority: `P2`", - "classification": "javadoc 은 \"Starts refusing new calls\" 라고 적는다. 실제로 하는 일은 단계 목록에 표식을 넣는 것뿐이다. 조정자는 `GrpcAdmissionController` 를 협력자로 들고 있는데, 그것을 쓰는 곳은 `inFlightAdmitted()` 의 조회 하나다. 그리고 승인 제어기의 공개 표면에 승인을 멈추는 메서드가 없다. `close`·`drain`·`refuseNew` 에 해당하는 것이 없다. 그러므로 배수가 시작된 뒤에도 `tryAdmit()` 은 용량이 남아 있는 한 계속 승인한다. `admittingNewCalls()` 은 그 사실과 무관하게 거짓을 돌려준다 — 표식을 읽기 때문이다. 운영자나 상위 코드가 이 값을 보고 \"더 이상 받지 않는다\" 고 읽으면 틀린 답을 얻는다. 배수 테스트가 단언하는 것은 `admittingNewCalls()` 의 값이고, 단계 이후에 `tryAdmit()` 이 거절되는지는 어느 테스트도 묻지 않는다. 승인 제어기에 승인 중단 상태를 두고(`stopAdmitting()` 과 그것을 보는 `tryAdmit`), 조정자의 `rejectNewAdmission` 이 그것을 부르게 한다. 지금 형태에서는 배수 순서를 지키는 장치가 순서 표식만 갖고 있다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "verification-path-coverage/case/case-grpc-admin-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-admin-f01", - "grpc-admin-f01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-admin-f01.txt" - ] - }, - { - "title": "등급 재정의에 하한이 없어 \"켤 수 없다\" 는 등급이 켜질 수 있다", - "kind": "case", - "slug": "grpc-advanced-bootstrap-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-advanced-bootstrap#L153`" - ], - "owning-module": "`grpc-advanced-bootstrap` · priority: `P3`", - "classification": "`GrpcCapabilityGrade` 의 javadoc 이 두 등급을 단정한다. 그런데 등급은 런타임에 갈아끼울 수 있다. `withGrade(EDITION_2026, ADVANCED_STABLE).enable(EDITION_2026)` 이면 가드의 두 번째 조건이 통과한다. 등급 올리기 자체는 의도된 기능이다 — 테스트 `a deployment may raise a capability's grade on its own evidence` 가 `HEDGING`(EXPERIMENTAL)을 `ADVANCED_STABLE` 로 올린다. 문제는 그 재정의에 하한이 없다는 것이다. `EXPERIMENTAL` 을 올리는 것은 \"실패 양식이 충분히 규명되지 않은 것을 감수한다\" 는 판단이고 배포가 자기 증거로 내릴 수 있다. `WATCH` 를 올리는 것은 다르다. 그 등급의 뜻이 \"추적할 뿐 구현되지 않았다\" 이므로 배포가 가질 자기 증거가 없다. 그리고 승격 게이트는 `WATCH` 가 `EXPERIMENTAL` 을 먼저 거쳐야 한다는 규칙을 갖는데, 런타임 재정의는 그 게이트를 지나지 않는다. 같은 리프 안에 문이 둘이고 증거 규칙은 한쪽에만 있다. 수정은 `withGrade` 가 현재 등급이 `startable()` 인 능력에만 적용되게 하거나, `WATCH`·`DISABLED` 에서 올리는 재정의를 거부하는 것이다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "verification-path-coverage/case/case-grpc-advanced-bootstrap-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-advanced-bootstrap-f01" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-advanced-bootstrap-f01.txt" - ] - }, - { - "title": "`capabilitiesDraggedAlong` 은 독립성을 증명하지 않는다. 상수를 상수와 비교한다", - "kind": "case", - "slug": "grpc-advanced-bootstrap-f04", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-advanced-bootstrap#L286`" - ], - "owning-module": "`grpc-advanced-bootstrap` · priority: `P3`", - "classification": "javadoc 이 스스로 밝히듯 본문은 무조건 빈 목록이다. 그것을 단언하는 테스트는 리터럴이 리터럴임을 확인한다 — 증거를 능력마다 따로 기록했다는 §4 의 설계 속성과는 아무 연결이 없다. 설계가 무너져 `apply` 가 다른 능력의 등급을 바꾸게 되어도 이 메서드는 여전히 빈 목록을 돌려준다. **진짜 증거는 같은 테스트의 다른 줄에 있다.** 승격을 실제로 적용하고 다른 능력의 등급이 그대로임을 확인한다. 이쪽은 설계가 무너지면 깨진다. 앞선 판에서 이 메서드를 \"주석이 주장하는 대신 테스트가 붙든다\"는 확인된 설계로 분류했다. 다시 읽으니 붙드는 것은 옆줄이고, 이 메서드는 그 옆줄이 있다는 사실을 가린다. 메서드를 지우고 단언을 매트릭스 비교 쪽으로 남긴다. 남겨 둔다면 실제로 매트릭스를 훑어 등급이 바뀐 다른 능력을 돌려주게 만든다 — 그때 비로소 이름이 하는 말과 본문이 맞는다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "verification-path-coverage/case/case-grpc-advanced-bootstrap-f04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-advanced-bootstrap-f04" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-advanced-bootstrap-f04.txt" - ] - }, - { - "title": "반응형 표면 두 타입은 테스트조차 없다", - "kind": "case", - "slug": "grpc-advanced-compat-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-advanced-compat#L149`" - ], - "owning-module": "`grpc-advanced-compat` · priority: `P3`", - "classification": "이 가족의 다른 미참조 Advanced 타입은 전부 테스트가 하나씩 있다 — 같은 패키지의 `GrpcReactorCancellationBridge` 는 2개 파일, `GrpcReactorContextBridge` 는 4개 파일에 등장한다. 두 타입은 채택자가 부를 표면이므로 production 참조 0 이 설계와 모순되지는 않는다. 어긋나는 것은 검증이다. 채택자용 표면이면 그 계약이 무엇인지를 테스트가 붙들어야 하고, 이 가족은 다른 곳에서 정확히 그렇게 한다. `ReactiveGrpcClient` 의 javadoc 이 \"Exposes a unary call as a `Mono` and a server stream as a `Flux`\" 라고 적는데, 그 사상이 취소와 배압에서 어떻게 동작하는지는 어디에서도 확인되지 않는다. 같은 리프의 `GrpcReactorCancellationBridge` 가 취소 전파를 다루므로 둘을 함께 검증할 자리가 이미 있다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "verification-path-coverage/case/case-grpc-advanced-compat-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-advanced-compat-f02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-advanced-compat-f02.txt" - ] - }, - { - "title": "저장소가 참조 프록시 설정을 갖고 있는데, 그것을 판정할 코드에 넣지 않는다", - "kind": "case", - "slug": "grpc-advanced-compat-f03", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-advanced-compat#L162`" - ], - "owning-module": "`grpc-advanced-compat` · priority: `P3`", - "classification": "이 리프에는 두 가지가 함께 있다. `GrpcWebProxyContract.violations(profile, exposedHeaders, allowedOrigins)` — 프록시 설정이 브라우저 클라이언트에게 통할지 판정하는 코드. `src/main/resources/envoy/envoy.yaml` — 그 설정의 참조 구현. 그리고 설정 파일 자신이 그 관계를 주장한다. `GrpcWebProxyContract` 는 이 파일에 대해 아무것도 단언하지 않는다. 판정기는 시험에서 리터럴 집합을 받고, 참조 설정은 시험에서 문자열 포함으로만 확인된다. 그래서 참조 설정이 `grpc-status` 를 노출하는지는 문자열이 확인하고, 그 노출이 **충분한지** 는 `requiredExposedHeaders()` 가 정의하는데, 둘을 잇는 코드가 없다. 필수 트레일러 목록이 늘어나면 판정기는 새 항목을 요구하고 참조 설정은 옛 문자열로 계속 통과한다. 이 리프의 다른 판정기들과 다른 점은 재료가 이미 저장소에 있다는 것이다 — grpc-testkit §17.5·grpc-server §17.1 은 스캔할 대상 자체를 만들어야 하지만, 여기서는 파일 하나를 파싱하면 된다. 수정은 시험이 `envoy.yaml` 의 `expose_headers` 와 `allow_origin`(`exact:`)을 뽑아 `GrpcWebProxyContract.violations` 에 넣고 비어 있음을 단언하는 것이다. 그러면 참조 설정과 계약이 한 곳에서 함께 움직인다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "verification-path-coverage/case/case-grpc-advanced-compat-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-advanced-compat-f03" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-advanced-compat-f03.txt" - ] - }, - { - "title": "Buf 수명주기 태스크 목록이 빌드와 대조되지 않는다. 테스트는 목록을 자기 자신과 비교한다", - "kind": "case", - "slug": "grpc-codegen-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-codegen#L190`" - ], - "owning-module": "`grpc-codegen` · priority: `P3`", - "classification": "정책이 네 태스크 이름을 담고, javadoc 이 그 이유를 적는다. 그런데 그 네 이름은 저장소의 어떤 빌드 파일에도 없다. 그리고 테스트가 비교하는 대상이 실제 등록 태스크 집합이 아니다. 첫 단언은 목록을 리터럴과, 셋째는 목록을 자기 자신과 비교한다. 어느 것도 빌드가 그 단계를 등록했는지 묻지 않는다. Buf CLI 가 이 툴체인에 없다는 것은 build.gradle 이 이미 밝힌 사실이므로 태스크가 없는 것 자체는 놀랍지 않다. 어긋난 것은 javadoc 의 주장이다 — 지금 형태에서 단계가 사라져도 테스트는 초록이다. 수정은 `missingTasks` 에 Gradle 이 실제로 등록한 태스크 이름 집합을 넣는 검사를 만들거나(다른 가족의 레인 등록 검사와 같은 형태), CLI 가 없는 동안에는 그 문장을 \"CI 환경이 채울 계약\" 으로 낮추는 것이다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "verification-path-coverage/case/case-grpc-codegen-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-codegen-f01" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-codegen-f01.txt" - ] - }, - { - "title": "`sha256:` 검사가 길이 15자 이상만 요구한다. 저장소 자신의 테스트가 32자 해시를 통과시킨다", - "kind": "case", - "slug": "grpc-codegen-f04", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-codegen#L287`" - ], - "owning-module": "`grpc-codegen` · priority: `P3`", - "classification": "같은 검사가 두 곳에 손으로 복사돼 있다. `\"sha256:\"` 이 7자이므로 뒤에 8자만 있으면 통과한다. sha256 digest 는 hex 64자다. 그리고 이 헐거움이 테스트에 이미 드러나 있다. 32자 — sha256 이 아니다. 여기서는 \"다른 해시\" 역할이라 결과가 바뀌지 않지만, 형식 검사가 이런 값을 유효한 해시로 받는다는 사실 자체가 이 값 객체의 주장(\"the hashes that prove which bytes it was built from\")을 약하게 만든다. `sha256:` 뒤 64자 hex 를 정규식으로 요구하고, 검사를 한 곳에 둔다 — 두 record 가 같은 규칙을 각자 적고 있는 지금 형태에서는 한쪽만 조여도 다른 쪽이 남는다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "verification-path-coverage/case/case-grpc-codegen-f04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-codegen-f04" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-codegen-f04.txt" - ] - }, - { - "title": "모듈 목록 테스트가 레지스트리와 목록을 붙들지 않는다", - "kind": "case", - "slug": "grpc-core-api-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-core-api#L157`" - ], - "owning-module": "`grpc-core-api` · priority: `P3`", - "classification": "클래스 javadoc 이 두 SSOT 의 관계를 적는다. 그 테스트는 레지스트리를 읽지 않는다. 다섯 테스트가 하는 일은 목록을 리터럴과 대조하고, 두 집합의 서로소를 확인하고, 누출 판정을 확인하는 것이다. `modules.json` 을 읽는 줄도, 파일 경로도 없다. 두 목록은 오늘 일치한다 — 레지스트리의 grpc 계열 리프가 18 개이고 목록이 12 + 6 이다. 어긋난 것은 그 일치를 무엇이 지키는가다. 같은 저장소가 이 형태를 messaging 가족에서 이미 기록했다 — 정확한 목록은 레지스트리가 소유하므로 산문에서 세지 않는다, 세는 순간 다시 표류한다. 수정은 테스트가 `modules.json` 을 읽어 grpc 계열 리프 집합과 두 상수 집합의 합집합을 대조하는 것이다. 그 테스트가 있으면 새 리프가 어느 쪽에도 들어가지 않은 채 추가되는 것을 잡는다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "verification-path-coverage/case/case-grpc-core-api-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-core-api-f02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-core-api-f02.txt" - ] - }, - { - "title": "두 아키텍처 규칙이 저장소 소스에 적용되지 않는다", - "kind": "case", - "slug": "grpc-server-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-server#L140`" - ], - "owning-module": "`grpc-server` · priority: `P2`", - "classification": "`GrpcApplicationBoundaryRules` javadoc: 세 적용처 중 저장소에 존재하는 것이 없다. 그리고 이 리프의 테스트는 저장소 파일을 훑지 않는다. 인라인 소스 문자열을 넣는다. 즉 규칙의 판정 로직은 검증되지만, 저장소의 어떤 파일도 그 판정을 받지 않는다. `GrpcServiceAdapterMarker` 는 규칙이 어댑터를 런타임에 열거할 수 있도록 만든 애너테이션인데, 그것을 붙인 타입도 그것을 읽는 코드도 없다. 이 리프의 테스트에 저장소 소스를 훑는 검사를 추가한다 — `src/**/*.java` 를 읽어 `GrpcRawApiImportRule.violations` 를 돌리고 비어 있음을 단언하는 형태다. 규칙이 이미 파일 이름과 소스 텍스트를 받는 서명이므로 재료는 갖춰져 있다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "verification-path-coverage/case/case-grpc-server-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-server-f01", - "grpc-server-f01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-server-f01.txt" - ] - }, - { - "title": "네 레인이 `check` 에 붙지 않고, 이 가족을 이름으로 부르는 워크플로가 없다", - "kind": "case", - "slug": "grpc-testkit-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-testkit#L153`" - ], - "owning-module": "`grpc-testkit` · priority: `P2`", - "classification": "`ca.strict-test-lane.gradle` 은 레인을 `verification` 그룹의 `Test` 태스크로 **등록만** 한다. `check` 에 연결하는 줄이 없다. 그리고 CI 워크플로에서 이 가족을 이름으로 부르는 것이 없다. `ci-quality-gates.yml` 이 `./gradlew check` 를 돌리므로 각 리프의 기본 `test` 는 돈다(이번에 확인: classes=71 tests=579 failures=0). 네 증거 레인은 그 밖에 있다. 결과적으로 이 플랫폼의 CONTRACT·TRANSPORT·FAULT 등급을 뒷받침하는 것은 25개 테스트이고, 그 25개는 누군가 명령을 직접 입력할 때만 돈다. build.gradle 자신이 그 위험을 적는다 — \"A lane that discovers nothing fails, and **none of them can serve an up-to-date result**\". 첫 성질은 레인 규약이 지킨다. 둘째 성질은 아무도 돌리지 않으면 무의미하다. 같은 저장소가 이 형태를 두 번 기록했다 — 모듈 18 의 \"붉은 게이트는 마지막으로 돌린 사람이 본 것을 보고한다\" 와 mongo 가족의 릴리스 게이트 지형. 차이는 이쪽 레인이 오늘 초록이라는 것이고, 그것을 확인한 방법이 이번 분석에서 직접 돌린 것이라는 점이다. 수정은 세 레인(성능 제외)을 `check` 에 붙이거나, messaging 가족처럼 전용 워크플로를 두는 것이다. 성능 레인을 빼는 판단은 이미 근거와 함께 코드에 있으므로 그대로 두면 된다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "verification-path-coverage/case/case-grpc-testkit-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-testkit-f01", - "grpc-testkit-f01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-testkit-f01.txt" - ] - }, - { - "title": "고장 레인의 유일한 실소켓 시험이 자기가 관측한 것을 버리고 리터럴로 증거를 만든다", - "kind": "case", - "slug": "grpc-testkit-f03", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-testkit#L187`" - ], - "owning-module": "`grpc-testkit` · priority: `P2`", - "classification": "`GrpcTransportEvidenceClassifierTest.aRealConnectionLossAfterAppStartIsCompletionUnknown` 은 이 리프에서 유일하게 실제 연결을 작업 중에 끊는다. 서버 핸들러가 래치로 멈춰 있는 동안 `server.close()` 를 부른다. 거기까지는 진짜 고장이다. 그런데 그 고장이 만들어 낸 관측이 어디에도 남지 않는다. 주석이 \"the exception is the observation\" 이라고 말하는데 그 예외는 `catch` 안에서 사라지고, 변수는 `null` 로 고정되고, 단언은 자기 대입을 확인한다. 그리고 `GrpcFaultResult` 에 들어가는 증거는 방금 일어난 호출에서 오지 않는다. 즉 소켓은 실제로 죽었고, 그 죽음에서 읽어 낸 값은 하나도 쓰이지 않는다. 이 시험이 실제로 증명하는 것은 `applicationStarted == true` 하나다. 나머지는 분류기의 산술이고, 그것은 같은 파일의 다른 일곱 시험이 이미 소켓 없이 증명한다. 이 형태를 이 리프 자신이 이름 붙여 두었다. 여기서는 소켓이 열렸다. 그런데 등급을 뒷받침해야 할 증거가 여전히 손으로 쓴 값이다. 한 단계 아래의 같은 치환이다. `callUnary` 를 부른 스레드가 잡은 예외와 그 시점의 진행 상태를 밖으로 넘겨(`AtomicReference`), 그것으로 `ClientObservation` 을 구성한다. 그러면 `sendCompleted`·`responseHeadersReceived` 가 관측값이 되고, 이 시험이 FAULT 등급을 실제로 뒷받침한다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "verification-path-coverage/case/case-grpc-testkit-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-testkit-f03", - "grpc-testkit-f03-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-testkit-f03.txt" - ] - }, - { - "title": "호환성 표의 레인 이름과 빌드의 레인 이름이 서로 다른 집합이다", - "kind": "case", - "slug": "grpc-testkit-f04", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-testkit#L233`" - ], - "owning-module": "`grpc-testkit` · priority: `P3`", - "classification": "`GrpcStableReleaseGate.evaluate` 의 둘째 인자는 `Map laneResults` 이고, `GrpcCompatibilityMatrix.missingResults` 가 그 키를 자기 목록과 대조한다. 그 목록은 배포 조합의 이름이다 — `boot-managed-platform`, `netty-shaded`, `upstream-grpc-java-override`, `protobuf-edition-2024` … 빌드가 등록하는 레인의 이름은 증거 종류다 — `grpcInProcessContractTest`, `grpcNettyContractTest`, `grpcFaultTest`, `grpcPerformanceTest`. 두 집합의 교집합이 비어 있다. 그래서 §17.1 을 고쳐 네 Gradle 레인을 `check` 에 붙이더라도, 그 결과가 이 게이트의 `laneResults` 를 채우지는 못한다 — 이름이 다른 축을 가리키기 때문이다. 게이트가 요구하는 것은 \"Boot 관리 플랫폼 조합에서 돌았는가\" 이고, 레인이 답할 수 있는 것은 \"전송 증거를 냈는가\" 다. 두 축이 다 필요하다는 것 자체는 옳다. 기록하는 이유는 §17.1·§17.2 의 수정이 이것까지 함께 다루지 않으면 게이트가 여전히 손으로 만든 값을 먹는다는 점이다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "verification-path-coverage/case/case-grpc-testkit-f04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-testkit-f04" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-testkit-f04.txt" - ] - }, - { - "title": "`DestructiveOperationGuard` 의 두 분기가 문서에도 없고 테스트에도 없다", - "kind": "case", - "slug": "messaging-admin-api-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-admin-api#L915`" - ], - "owning-module": "`messaging-admin-api` · priority: `P2`", - "classification": "operation 불일치(`:72-83`)와 source 불일치(`:84-93`)는 클래스 javadoc 의 \"Four conditions\" 에 포함되지 않고, 두 에러 코드를 단언하는 테스트도 저장소 전체에 없다(`EVD-303`). 이 둘은 사소한 검사가 아니다 — 5번 분기의 인라인 주석이 정확히 말한다: \"A guard that only checks presence and window lets a verified redrive approval authorise a destination deletion.\" 즉 **검증된 승인으로 목적지 삭제를 인가하는 것**을 막는 검사다. 혼동을 키우는 정황이 하나 더 있다. `anApplicationRuntimeCannotRedrive` 는 `REDRIVE` 요청에 `REPLAY` 승인을 넘기지만 `adminCredentialPresent=false` 라 분기 2에서 먼저 걸린다. 불일치 조합이 테스트에 등장하지만 그 분기는 실행되지 않는다. 수정: javadoc 을 여섯으로 고치고, `new DestructiveOperationGuard(true)` 위에서 operation 불일치·source 불일치 각각 1건씩 테스트를 추가한다. 이 리프에는 이미 `verified(operation, source, from, until)` 헬퍼가 있어 두 줄이면 된다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "verification-path-coverage/case/case-messaging-admin-api-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-admin-api-f01", - "messaging-admin-api-f01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-admin-api-f01.txt" - ] - }, - { - "title": "상호운용을 위한 매퍼가 명세 준수 이벤트를 분류되지 않은 예외로 거절한다", - "kind": "case", - "slug": "messaging-cloudevents-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-cloudevents#L518`" - ], - "owning-module": "`messaging-cloudevents` · priority: `P2`", - "classification": "`fromCloudEvent`가 `new MessageId(UUID.fromString(event.getId()))`로 id를 파싱한다. CloudEvents 1.0.2는 `id`를 비어 있지 않은 문자열로만 제약한다. 런타임 probe 결과: 명세 예시 id `A234-1234-1234` → `java.lang.IllegalArgumentException: Invalid UUID string`, UUIDv4 → `java.lang.IllegalArgumentException: a message identity is UUIDv7`. **둘 다 `MessagingException`이 아니다.** **(1) 범위.** v4 거절은 의도이고 테스트 주석이 그렇게 적는다. 그러나 **비UUID 거절은 어디에도 언급되지 않았고** 그것은 다른 판단이다 — v4 거절은 \"우리 정책\", 비UUID 거절은 \"CloudEvents 상호운용 포기\"다. 이 leaf의 존재 이유가 상호운용인데 명세 예시조차 받지 못한다. **(2) 실패 어휘.** 같은 메서드의 다른 검증 실패 넷은 전부 `MessageValidationException`이고 안정 코드(`CLOUDEVENT_TIME_REQUIRED` 등)를 갖는다. id 실패만 raw `IllegalArgumentException`이라 `FailureDescriptor`가 없다 — 카테고리도, 코드도, retryable 판정도 없다. DLQ 라우팅과 대시보드가 이 실패를 분류하지 못한다. 같은 문제가 `causationid`·`type`·`correlationid`·`tenantcontext`·`producer`·`datacontenttype`·`schemaversion` 값 범위에도 있다(§6의 \"코드 없음\" 여덟 행).", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "verification-path-coverage/case/case-messaging-cloudevents-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-cloudevents-f01", - "messaging-cloudevents-f01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-cloudevents-f01.txt" - ] - }, - { - "title": "속성을 이름으로 주장하는 테스트가 그 속성을 보일 수 없는 fake 위에서 통과한다", - "kind": "case", - "slug": "messaging-inbox-jdbc-postgresql-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-inbox-jdbc-postgresql#L657`" - ], - "owning-module": "`messaging-inbox-jdbc-postgresql` · priority: `P2`", - "classification": "`InboxOperationsTest.cleanupDeletesInBoundedBatches`가 `InMemoryInbox(List.of(1000, 500))`에 대해 `removed == 1500`과 `cutoffs.hasSize(3)`을 단언한다. 그 fake의 무제한 메서드는 미리 준 목록을 순서대로 반환하는 **대본**이고 아무것도 삭제하거나 제한하지 않는다. bounded 오버로드는 fake에도 있지만 job이 부르지 않아 실행되지 않는다. 이 테스트가 통과로 증명하는 것은 \"0을 받을 때까지 루프를 돈다\"이고 이름이 주장하는 \"배치로 제한된다\"가 아니다. 1000·500은 배치처럼 보이는 숫자다. **P1이 이 테스트를 통과한 채로 존재할 수 있었던 이유**다. 그리고 컨테이너 레인(`InboxPostgresIT.retentionRemovesOldRows`)도 무제한 오버로드를 한 행에 대해 부르므로 실 DB에서도 드러나지 않는다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "verification-path-coverage/case/case-messaging-inbox-jdbc-postgresql-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-inbox-jdbc-postgresql-f02", - "messaging-inbox-jdbc-postgresql-f02-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-inbox-jdbc-postgresql-f02.txt" - ] - }, - { - "title": "`deduplicatedPublish` 를 무조건 참으로 선언하는데 실제 중복 제거는 프로파일에 창이 있을 때만 일어난다", - "kind": "case", - "slug": "messaging-nats-experimental-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-nats-experimental#L146`" - ], - "owning-module": "`messaging-nats-experimental` · priority: `P2`", - "classification": "검증기의 `capabilities()` 도 같은 값을 돌려준다. 그런데 중복 제거 식별자는 프로파일에 창이 있을 때만 만들어진다. `NatsJetStreamProfile.deduplicationWindow` 는 `Optional` 이고, 비어 있는 것이 정상 상태다 — 프로파일 생성자도 검증기도 창을 요구하지 않는다. 창이 없으면 `Nats-Msg-Id` 가 실리지 않고 서버는 중복을 제거하지 않는다. 즉 능력 선언이 프로파일과 무관하게 참이다. 이 저장소에서 능력 열두 개 중 부재가 예외를 만드는 유일한 것이 `deduplicatedPublish` 다(`DefaultMessagePublisher:250`). 나머지는 읽히지 않거나 분기에 쓰인다. 그러므로 이 플래그의 과대 선언은 다른 어느 플래그의 과대 선언보다 직접적이다 — 창 없는 목적지가 그 가드를 통과한다. 클래스 javadoc: \"when the profile enables one\" 이 정확히 능력이 담지 않은 조건이다. 창이 없는 목적지에서 모호를 재시도하면 스트림에 같은 메시지가 두 번 들어간다. `MessagingCapabilities` 의 클래스 javadoc 이 이 상황을 미리 서술한다 — \"a silently weakened guarantee is indistinguishable from a working one until the incident.\" `NatsAdapterContractTest` 안에서, 같은 빈 창 프로파일(`confirming(Optional.empty())`)에 대해: 둘 다 통과한다. 모순이 우연히 남은 것이 아니라 **테스트로 고정되어** 있다는 뜻이고, 수정할 때 함께 고쳐야 할 지점이 어디인지도 이 두 개가 알려 준다. `NatsJetStreamProfile.durable(...)` 는 창을 2분으로 채워 주지만 호출자가 없고, 정규 생성자는 빈 창을 정상값으로 받는다(§4). 능력을 프로파일에서 파생시킨다. 또는 검증기가 최소 한 번 배달 …", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "verification-path-coverage/case/case-messaging-nats-experimental-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-nats-experimental-f01", - "messaging-nats-experimental-f01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-nats-experimental-f01.txt" - ] - }, - { - "title": "경과 시간 회귀를 막으려는 어셈블이 항상 참이다", - "kind": "case", - "slug": "messaging-nats-experimental-f04", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-nats-experimental#L238`" - ], - "owning-module": "`messaging-nats-experimental` · priority: `P3`", - "classification": "`as(...)` 가 막으려는 회귀는 \"모든 결과가 `Duration.ZERO` 를 보고하던 것\"이다. 그런데 어셈블은 `>= Duration.ZERO` 다. `Duration.ZERO` 는 이 조건을 통과한다. 경과 시간은 시작 시점에서 잰 값이라 음수가 될 수 없으므로, 이 어셈블은 **구현이 무엇을 하든 통과한다.** 이름과 `as` 메시지가 정확히 짚은 회귀를, 어셈블만 못 잡는다. 그래서 이 테스트는 회귀 방지가 아니라 회귀 방지의 표시다. `isGreaterThan(Duration.ZERO)` 로 바꾼다. 시간 분해능이 불안하면 전송 람다에 관측 가능한 지연을 넣고 그 하한과 비교한다 — 같은 클래스의 `aPublishThatNeverCompletesIsBoundedByTheCallersTimeout` 가 이미 50밀리초 마감으로 그 방식을 쓴다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "verification-path-coverage/case/case-messaging-nats-experimental-f04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-nats-experimental-f04" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-nats-experimental-f04.txt" - ] - }, - { - "title": "배포되는 Debezium 설정이 수정 이전 버전이다", - "kind": "case", - "slug": "messaging-outbox-jdbc-postgresql-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-outbox-jdbc-postgresql#L888`" - ], - "owning-module": "`messaging-outbox-jdbc-postgresql` · priority: `P2`", - "classification": "`src/main/resources/debezium/outbox-event-router.properties` 가 `event.key=destination` 을 유지하고 있다. 같은 저장소의 Java(`DebeziumOutboxEventRouter`), V4 마이그레이션 주석, 그리고 전용 테스트(`theRoutedKeyIsNotTheTopicName`)가 모두 그것이 결함이라고 말한다 — \"keying by destination puts every message on a topic onto one partition\". 추가로 헤더 매핑이 15개 중 4개뿐이라, 이 파일로 배포한 CDC 는 tenant·correlation·causation·producer·trace·partition/ordering key 를 **전부 잃는다**. V4 가 존재하는 이유가 그 유실을 막는 것이다. 1. properties 를 Java 설정에서 생성하거나, 최소한 **둘을 대조하는 테스트**를 둔다. `DebeziumOutboxEventRouter.connectorConfiguration(\"\")` 의 항목이 파일에 모두 있는지 확인하는 테스트면 충분하다. 지금은 두 표현을 잇는 코드가 한 줄도 없다. 2. `aggregateIdAsPartitionKey` 를 `connectorConfiguration` 에 전달하거나, 전달할 수 없다면 `DebeziumOutboxRecordMapper` 에서 그 분기를 제거한다. 지금은 모델이 커넥터가 하지 않을 일을 예측한다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "verification-path-coverage/case/case-messaging-outbox-jdbc-postgresql-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-outbox-jdbc-postgresql-f02", - "messaging-outbox-jdbc-postgresql-f02-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-outbox-jdbc-postgresql-f02.txt" - ] - }, - { - "title": "역슬래시로 끝나는 헤더 값이 헤더 맵을 깨뜨린다", - "kind": "case", - "slug": "messaging-outbox-jdbc-postgresql-f03", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-outbox-jdbc-postgresql#L899`" - ], - "owning-module": "`messaging-outbox-jdbc-postgresql` · priority: `P2`", - "classification": "`findClosingQuote`(`:657-664`)가 이스케이프된 역슬래시를 고려하지 않는다. 값이 역슬래시로 끝나면 파서가 종료 지점을 놓치고, 뒤에 헤더가 더 있으면 맵 전체가 붕괴한다(`EVD-314`, 런타임 재현). `HeaderValue` 는 제어문자만 금지하므로 이 입력은 플랫폼 검증을 통과한다. 헤더 주입으로 이어지지는 않는다 — 예약 이름은 키가 아니라 값이 되고, 쓰기 경로가 예약 이름을 이미 거절한다. 수정: 종료 판정을 \"앞의 연속된 역슬래시 개수가 짝수\" 로 바꾸거나, 인덱스를 앞에서부터 스캔하며 이스케이프 상태를 추적한다. 후자가 `unescape` 와 대칭이라 낫다. 테스트는 `OutboxPostgresIT.aHeaderValueWithControlCharactersRoundTrips` 옆에 역슬래시 종결 케이스를 추가하면 된다 — 실 DB 왕복까지 확인할 수 있다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "verification-path-coverage/case/case-messaging-outbox-jdbc-postgresql-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-outbox-jdbc-postgresql-f03", - "messaging-outbox-jdbc-postgresql-f03-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-outbox-jdbc-postgresql-f03.txt" - ] - }, - { - "title": "한 인터페이스가 같은 전이의 두 세대를 갖고, 안전하지 않은 쪽에 `@Deprecated`가 없다", - "kind": "case", - "slug": "messaging-reliability-api-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-reliability-api#L689`" - ], - "owning-module": "`messaging-reliability-api` · priority: `P2`", - "classification": "`OutboxRepository`가 다섯 전이 각각에 대해 `MessageId` 기반(반환 `void`)과 `OutboxLease` 기반(반환 `OutboxTransitionResult`) 두 형태를 선언한다. javadoc이 전자를 \"deprecated for the relay's use\"라고 부르지만 `@Deprecated` 애노테이션이 이 leaf 전체에 **0건**이다. 전자에는 fencing이 없다 — `OutboxLease` javadoc이 그 부재가 만든 이중 발행 사고를 기록한다. 컴파일러가 경고하지 않으므로 새 호출자가 그것을 고를 수 있고, **실제로 PostgreSQL 컨테이너 테스트가 그렇게 했다**(§12.1c). 그리고 새 구현자는 17개 메서드를 전부 구현해야 하며 그중 다섯은 안전하지 않은 형태다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "verification-path-coverage/case/case-messaging-reliability-api-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-reliability-api-f01", - "messaging-reliability-api-f01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-reliability-api-f01.txt" - ] - }, - { - "title": "배치 발행자가 `CompletionStage` 를 돌려주면서 동기 예외를 던진다", - "kind": "case", - "slug": "messaging-spring-boot-starter-f05", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-spring-boot-starter#L367`" - ], - "owning-module": "`messaging-spring-boot-starter` · priority: `P3`", - "classification": "같은 클래스가 자기 의존 대상에 대해서는 정확히 이 형태를 방어한다. 즉 \"게으르게 검증하고 실패한 스테이지를 돌려준다\" 가 이 클래스가 아는 계약인데, 자기 호출자에게는 그것을 지키지 않는다. 비동기 파이프라인으로 배치를 부르는 코드는 `.exceptionally(...)` 로 잡히지 않는 예외를 만난다. 등급이 P3 인 이유는 이것이 프로그래밍 오류(배치 크기 초과)이고 결과가 손실이 아니라 예외 형태의 불일치이기 때문이다. 전용 테스트(`aBatchLargerThanItsLimitIsRefusedBeforeAnythingIsPublished`)가 `assertThatThrownBy` 로 현재 동작을 고정하고 있으므로, 고치려면 그 테스트도 함께 바꾼다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "verification-path-coverage/case/case-messaging-spring-boot-starter-f05.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-spring-boot-starter-f05" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-spring-boot-starter-f05.txt" - ] - }, - { - "title": "항등식을 단언하는 테스트가 하나 있다", - "kind": "case", - "slug": "messaging-testkit-f07", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-testkit#L980`" - ], - "owning-module": "`messaging-testkit` · priority: `P3`", - "classification": "`CompatibilityMatrixTest.aCertificationClaimCannotBeMadeWithoutEvidence`(`:48-60`)의 좌변과 우변이 같은 식이다(§12.3(c)). 이름이 약속하는 것을 검사하지 않는다. 실질 검사는 같은 파일의 다른 두 테스트가 하고 있으므로 커버리지 손실은 없다. 이 테스트를 지우거나, \"증거를 비우면 Stable 주장이 무너진다\" 를 실제로 검사하도록 바꾼다 — 후자가 이름에 맞는다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "verification-path-coverage/case/case-messaging-testkit-f07.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-testkit-f07" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-testkit-f07.txt" - ] - }, - { - "title": "드레인 마감 30초가 세 곳에서 독립적으로 결정된다", - "kind": "case", - "slug": "messaging-transport-spi-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-transport-spi#L659`" - ], - "owning-module": "`messaging-transport-spi` · priority: `P3`", - "classification": "`MessagingLifecycle.DEFAULT_DRAIN_DEADLINE`(public), `DefaultMessagingRuntimeRegistry.DEFAULT_DRAIN_DEADLINE`(private), `MessagingShutdownLifecycle`의 생성자 인자. public 상수가 같은 leaf 안에 있는데 다른 클래스가 자기 private 복사본을 쓴다. `MessagingLifecycleTest.theDefaultDrainDeadlineMatchesTheDesign`이 public 쪽만 고정하므로 private 쪽이 바뀌어도 통과한다. 그리고 §17 첫 항목대로 public 상수가 있는 인터페이스는 구현체가 없다 — 즉 살아 있는 값(private)이 죽은 인터페이스의 값(public)을 참조하지 않는다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "verification-path-coverage/case/case-messaging-transport-spi-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-transport-spi-f01" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-transport-spi-f01.txt" - ] - } - ], - "concept": [], - "reference": [ - { - "title": "왕복이라 부르는 테스트는 무엇을 보존하지 않는지도 적는다", - "kind": "reference", - "slug": "messaging-cloudevents-f03", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-cloudevents#L538`" - ], - "owning-module": "`messaging-cloudevents`", - "rule": "왕복이라 부르는 테스트는 무엇을 보존하지 않는지도 적는다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 왕복이 다섯 필드를 버리고, 테스트가 그 필드를 비교하지 않는다", - "scope": [ - "스키마와 코덱 계열. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "verification-path-coverage/reference/reference-messaging-cloudevents-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다", - "kind": "reference", - "slug": "messaging-kafka-share-experimental-f05", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-kafka-share-experimental#L503`" - ], - "owning-module": "`messaging-kafka-share-experimental`", - "rule": "leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 네 타입 중 하나만 테스트된다", - "scope": [ - "메시징 어댑터의 소비·발행 경로. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "verification-path-coverage/reference/reference-messaging-kafka-share-experimental-f05.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "계약만 담는 leaf도 계약의 거절 조건은 자기 레인에서 검증한다", - "kind": "reference", - "slug": "messaging-reliability-api-f04", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-reliability-api#L716`" - ], - "owning-module": "`messaging-reliability-api`", - "rule": "계약만 담는 leaf도 계약의 거절 조건은 자기 레인에서 검증한다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 이 leaf에 테스트가 없다", - "scope": [ - "멱등·아웃박스·인박스 계열. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "verification-path-coverage/reference/reference-messaging-reliability-api-f04.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "record의 `equals`를 좁히면 이유를 적는다", - "kind": "reference", - "slug": "messaging-reliability-api-f07", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-reliability-api#L743`" - ], - "owning-module": "`messaging-reliability-api`", - "rule": "record의 `equals`를 좁히면 이유를 적는다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — `OutboxRecord.equals`가 다섯 필드만 비교하고 이유가 없다", - "scope": [ - "멱등·아웃박스·인박스 계열. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "verification-path-coverage/reference/reference-messaging-reliability-api-f07.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "증가한다고 문서화한 값이 리터럴이면 그 사실을 적는다", - "kind": "reference", - "slug": "messaging-runtime-core-f06", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-runtime-core#L754`" - ], - "owning-module": "`messaging-runtime-core`", - "rule": "증가한다고 문서화한 값이 리터럴이면 그 사실을 적는다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — `generation`이 항상 1이다", - "scope": [ - "메시징 어댑터의 소비·발행 경로. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "verification-path-coverage/reference/reference-messaging-runtime-core-f06.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "모드 enum을 분기 조건으로 쓰면 각 분기에 테스트를 둔다", - "kind": "reference", - "slug": "messaging-schema-avro-f04", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-schema-avro#L551`" - ], - "owning-module": "`messaging-schema-avro`", - "rule": "모드 enum을 분기 조건으로 쓰면 각 분기에 테스트를 둔다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — transitive 분기가 테스트되지 않는다", - "scope": [ - "스키마와 코덱 계열. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "verification-path-coverage/reference/reference-messaging-schema-avro-f04.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "검증되지 않는 스키마 파일은 문서임을 파일 안에 적는다", - "kind": "reference", - "slug": "messaging-schema-protobuf-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-schema-protobuf#L527`" - ], - "owning-module": "`messaging-schema-protobuf`", - "rule": "검증되지 않는 스키마 파일은 문서임을 파일 안에 적는다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — `.proto` fixture와 테스트 descriptor의 일치를 아무도 강제하지 않는다", - "scope": [ - "스키마와 코덱 계열. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "verification-path-coverage/reference/reference-messaging-schema-protobuf-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "신뢰할 수 없는 입력 쪽 경계를 먼저 테스트한다", - "kind": "reference", - "slug": "messaging-schema-protobuf-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-schema-protobuf#L536`" - ], - "owning-module": "`messaging-schema-protobuf`", - "rule": "신뢰할 수 없는 입력 쪽 경계를 먼저 테스트한다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 디코딩 상한 분기가 테스트되지 않는다", - "scope": [ - "스키마와 코덱 계열. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "verification-path-coverage/reference/reference-messaging-schema-protobuf-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "같은 술어가 두 타입에 있으면 하나가 다른 하나를 부른다", - "kind": "reference", - "slug": "messaging-security-f05", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-security#L670`" - ], - "owning-module": "`messaging-security`", - "rule": "같은 술어가 두 타입에 있으면 하나가 다른 하나를 부른다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 회전 술어가 두 번 구현돼 있고, 쓰이지 않는 쪽이 테스트된다", - "scope": [ - "보안·승인 표면. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "verification-path-coverage/reference/reference-messaging-security-f05.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - } - ], - "question": [ - { - "title": "cache-redis/httpclient의 support project dependency 필요성 재검증", - "kind": "question", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "실제 성능·용량 특성을 어떤 모듈에서도 측정하지 않았다", - "kind": "question", - "slug": "", - "readiness": "", - "publication": "미작성" - } - ], - "decision": [] - } - }, - "state-ownership-and-concurrency": { - "topic": "state-ownership-and-concurrency", - "title": "상태 소유권과 동시성 — claim·lease·replay·메모리 상태의 소유자를 지키기", - "readerQuestion": "", - "kinds": { - "case": [ - { - "title": "notification admin atomic claim contract가 service에서 사용되지 않음", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`inspect()`와 `claim()`이 만료된 COMPLETED row를 동시에 다른 상태로 해석한다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`complete()`의 replay 판정이 `replayTtl` 변경을 무시한다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "persistent byte quota가 실제 admission에서 집행되지 않는다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "quota reclaim은 최대 64개 committed row만 처리하고 남은 byte를 조용히 버린다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "direct `FileQuotaService.commit()`은 만료 reservation을 commit한다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "recovery queue의 `enqueue()`는 concurrent upsert가 아니다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "cleanup crash-reclaim은 `MAXIMUM_ATTEMPTS`를 우회해 poison item을 무한 재시도할 수 있다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "provider 호출 뒤 recipient projection write가 lease fencing을 우회한다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "reconciliation `FOR UPDATE SKIP LOCKED`는 worker 처리 구간을 claim하지 않는다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`recordApplied`는 문서화된 fence 계약을 구현하지 않고, 보호를 역전시킨다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "Flamingock lease로는 어떤 migration도 실행할 수 없고, javadoc은 다르게 적는다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "recovery package에 쓰이는 어휘와 쓰이지 않는 어휘가 나란히 있다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "nonce replay 경계가 결과를 읽고 버린다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`Number`가 허용 목록에 있어 가변 숫자 타입이 REPLAYABLE로 인증된다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`GrpcAdmissionController.tryAdmit()`의 동시성 경계가 동시성 아래에서 성립하지 않는다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`GrpcOutcomeReplay`가 제거 경로 없는 인메모리 저장소다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`GrpcCompletionReconciler`가 요청 경로에서 동기화 없는 `ArrayList`를 변경한다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "배정 식별자 때문에 insert-first 청구가 UPSERT 가 되어 커밋된 결과를 덮었다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "배수 완료가 자기가 읽은 값으로 상태를 다시 써서 진행 중인 자격증명 회전을 되돌린다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "배수 조정자가 가변이고 동기화가 없다", - "kind": "case", - "slug": "grpc-admin-f03", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-admin#L181`" - ], - "owning-module": "`grpc-admin` · priority: `P3`", - "classification": "`phasesRun`(`ArrayList`), `startedAt`, `completedUnaryCalls`, `signalledStreams` 가 평범한 필드다. `synchronized`·`volatile`·동시 자료구조가 없다. 같은 리프의 건강 레지스트리는 정반대다 — `ConcurrentHashMap` 둘과 `volatile boolean draining`. 즉 이 리프는 동시성을 인지하고 있고 한 클래스에만 적용했다. 조정자의 javadoc 이 대기를 호출자에게 맡긴다고 적으므로 단일 호출자 전제로 읽을 수 있다. 다만 그 전제가 자바독에 적혀 있지 않고, `unaryDrainComplete` 는 반복 호출을 전제한 형태라 종료 훅과 상태 조회가 다른 스레드에서 닿기 쉽다. 수정은 단일 스레드 전제를 자바독에 적거나, 형제 클래스와 같은 수준으로 맞추는 것이다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-ownership-and-concurrency/case/case-grpc-admin-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-admin-f03" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-admin-f03.txt" - ] - }, - { - "title": "깃발 홀더가 가변이고 동기화가 없다", - "kind": "case", - "slug": "grpc-advanced-bootstrap-f03", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-advanced-bootstrap#L209`" - ], - "owning-module": "`grpc-advanced-bootstrap` · priority: `P3`", - "classification": "`GrpcAdvancedFeatureFlags` 는 두 `EnumMap` 을 `enable`·`withGrade` 로 갱신하고, `available`·`active` 가 같은 맵을 읽는다. `synchronized`·`volatile`·동시 자료구조가 없다. 시작 시 전부 설정하고 그 뒤로 읽기만 한다면 안전 공개 문제만 남는다. 다만 두 메서드가 `this` 를 돌려주는 유창한 형태라 런타임 중 갱신을 권하는 모양이고, `active()` 는 순회 중 갱신에 노출된다. 같은 저장소가 이 형태를 다른 리프에서 결함으로 기록했다(`GrpcCompletionReconciler` 의 동기화 없는 `ArrayList`). 여기서는 등급과 깃발이 요청 경로에서 읽히므로 같은 노출이 생길 수 있다. 수정은 홀더를 불변으로 만들고 `enable`·`withGrade` 가 새 인스턴스를 돌려주게 하는 것이다. 이 저장소가 다른 곳에서 쓰는 형태다(`GrpcProtoStyleManifest.allowingWellKnownTypes` 등).", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-ownership-and-concurrency/case/case-grpc-advanced-bootstrap-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-advanced-bootstrap-f03" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-advanced-bootstrap-f03.txt" - ] - }, - { - "title": "리졸버의 개정 가드가 비교 후 교체가 아니다", - "kind": "case", - "slug": "grpc-advanced-resilience-f03", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-advanced-resilience#L176`" - ], - "owning-module": "`grpc-advanced-resilience` · priority: `P2`", - "classification": "`GrpcCustomResolver` 의 javadoc 이 지키겠다고 하는 것은 명확하다. 빈 집합은 `GrpcEndpointSnapshot` 의 생성자가 지키므로 성립한다. 개정 가드는 그렇지 않다. `AtomicReference` 를 쓰면서 읽기와 쓰기 사이에 원자성이 없다. 개정 5 와 6 을 든 두 스레드가 같은 `applied`(개정 4)를 읽으면 둘 다 `supersedes` 를 통과하고, 나중에 `set` 하는 쪽이 이긴다. 6 이 먼저 쓰이고 5 가 덮으면 **채널이 옛 엔드포인트로 되돌아간다** — 개정 번호가 존재하는 이유가 정확히 그것을 막는 것이다. `listener.accept(update)` 도 `set` 밖에 있으므로, `applied` 의 최종 값이 옳더라도 리스너(=채널)가 받는 순서는 뒤집힐 수 있다. 채널은 마지막으로 받은 것을 믿는다. 같은 형태가 이 가족에 셋이다. 정본이 같은 리프 안에 있다는 점이 §12.2 의 대조와 같다 — 이 리프는 예산에서는 CAS 를 쓰고 리졸버에서는 쓰지 않는다. `a stale revision is dropped rather than applied` 는 단일 스레드에서 개정 2 를 적용한 뒤 개정 1 을 제시한다. 순차적으로는 가드가 정확히 작동한다. 이 리프가 배선되지 않으므로 P2. 리졸버는 정의상 외부 발견 소스가 밀어 넣는 것이고, 그 소스가 한 스레드만 쓴다는 보장은 이 클래스가 하지 않는다. `applied.updateAndGet` 안에서 판정과 교체를 함께 하거나, `compareAndSet(observed, snapshot)` 이 실패하면 다시 읽어 판정한다. 리스너 통지는 성공한 CAS 뒤에 그 CAS 가 이긴 순서로 해야 한다 — 예산 쪽의 `tryConsume` 루프가 같은 리프 안의 본보기다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-ownership-and-concurrency/case/case-grpc-advanced-resilience-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-advanced-resilience-f03", - "grpc-advanced-resilience-f03-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-advanced-resilience-f03.txt" - ] - }, - { - "title": "체크포인트 전진이 `ConcurrentMap` 위의 확인 후 쓰기다", - "kind": "case", - "slug": "grpc-advanced-streaming-f03", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-advanced-streaming#L180`" - ], - "owning-module": "`grpc-advanced-streaming` · priority: `P3`", - "classification": "`GrpcClientStreamCheckpoint.advancedTo` 가 뒤로 가는 것을 거부하고, 그 메시지가 원인을 정확히 짚는다 — \"two writers are checkpointing one session\". 그 가드가 보는 것은 **호출한 스레드가 읽은 값** 이다. 두 스레드가 순번 5 와 6 을 적용하며 같은 체크포인트(4)를 읽으면 둘 다 `advancedTo` 를 통과한다. 5 를 든 쪽이 나중에 `put` 하면 체크포인트는 6 에서 5 로 **뒤로 간다** — `advancedTo` 가 막겠다고 한 바로 그 상태이고, 이번에는 예외 없이 조용히 일어난다. 그러면 순번 6 의 메시지가 다시 `APPLY` 로 판정되어 두 번 적용된다. 이 클래스가 존재하는 이유가 정확히 그것을 막는 것이다. `ConcurrentHashMap` 에는 이 형태를 위한 연산이 있다. `compute` 안에서는 읽기와 쓰기가 원자적이므로, 뒤처진 쪽이 `advancedTo` 의 예외를 실제로 받는다 — 가드가 설계대로 발화한다. 같은 리프의 `GrpcDemandController` 는 모든 공개 메서드가 `synchronized` 이고, `GrpcBidiSequenceTracker` 도 그렇다(§12.2 가 그것을 이 가족의 모범으로 든다). 중복 제거기만 `ConcurrentMap` 의 원자 연산을 쓰지 않는다. 중복 제거기 시험 아홉 개가 전부 단일 스레드다. 순차적으로는 `advancedTo` 가 정확히 작동하고, 전용 시험(`aCheckpointRecordsWhatWasApplied`)이 그것을 확인한다 — 확인하는 것은 record 의 메서드이지 맵에 쓰는 경로가 아니다. 미배선이므로 P3. 다만 이 클래스의 javadoc 이 \"The application effect and this checkpoint belong in one transaction\" 이라고 적어 둔 것과 함께 보면, 이 자리는 배선되는 날 트랜잭션 경계와 함께 다시 설계될 곳이다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-ownership-and-concurrency/case/case-grpc-advanced-streaming-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-advanced-streaming-f03" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-advanced-streaming-f03.txt" - ] - }, - { - "title": "`rotate` 가 비교 후 교체가 아니라 덮어쓰기다", - "kind": "case", - "slug": "grpc-client-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-client#L120`" - ], - "owning-module": "`grpc-client` · priority: `P2`", - "classification": "`install` 은 정확하다. `rotate` 는 그렇지 않다. 두 회전이 동시에 들어오면 둘 다 같은 `previous` 를 읽고, 둘 다 대체본을 만들고, 나중 `set` 이 앞의 대체본을 덮는다. 덮인 대체본은 어디에도 등록되지 않는다 — `draining` 목록에 들어가는 것은 `previous` 뿐이다. 그러므로 그 세대는 배수도 회수도 되지 않고, 그 위에서 시작된 호출은 아무도 세지 않는다. 클래스가 이 문제를 인지하고 있다는 증거가 같은 파일에 있다 — `install` 의 비교 후 교체와 `AtomicReference` 선택이다. 회전 쪽만 그 규율에서 벗어나 있다. 수정은 `holder.compareAndSet(previous, replacement)` 로 바꾸고 실패 시 다시 읽어 판정하거나 던지는 것이다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-ownership-and-concurrency/case/case-grpc-client-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-client-f01", - "grpc-client-f01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-client-f01.txt" - ] - }, - { - "title": "비원자적 감소가 세대를 영구히 회수 불가로 만든다", - "kind": "case", - "slug": "grpc-client-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-client#L149`" - ], - "owning-module": "`grpc-client` · priority: `P2`", - "classification": "카운터가 1 일 때 두 스레드가 동시에 끝나면 둘 다 조건을 통과해 둘 다 감소시켜 −1 이 된다. 그 결과가 이 리프에서는 구체적이다. 정확히 0 을 요구한다. 음수가 되면 조용해짐 판정이 영원히 거짓이고, `retireQuiescent` 가 그 세대를 결코 제거하지 않는다. 회전이 반복될수록 `draining` 목록이 자란다. 같은 형태가 이 가족의 다른 두 곳에도 있다(`GrpcAdmissionController.release`, `GrpcStreamAdmission.release`). 그쪽은 경계가 느슨해지는 결과였고, 이쪽은 자원이 회수되지 않는 결과다. 수정은 `updateAndGet(v -> Math.max(0, v - 1))` 이나 `decrementAndGet()` 후 하한 보정이다. 같은 가족의 `GrpcRetryBudget` 이 정확한 비교 후 교체 루프를 이미 쓴다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-ownership-and-concurrency/case/case-grpc-client-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-client-f02", - "grpc-client-f02-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-client-f02.txt" - ] - }, - { - "title": "배수 목록의 순회가 동기화 밖에서 일어난다", - "kind": "case", - "slug": "grpc-client-f03", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-client#L176`" - ], - "owning-module": "`grpc-client` · priority: `P3`", - "classification": "`Collections.synchronizedList` 는 개별 연산만 동기화한다. 순회는 호출자가 그 목록을 잠그고 해야 한다는 것이 그 API 의 계약이다. `List.copyOf(...)` 와 `stream()` 둘 다 순회다. 회전이 동시에 `add` 하면 동시 변경 예외가 가능하다. 그리고 읽고 지우는 두 단계가 원자적이지 않으므로, 그 사이에 조용해진 세대가 추가되면 이번 회수에서 빠진다. 후자는 다음 호출에서 회수되므로 무해하다. 수정은 `CopyOnWriteArrayList` 로 바꾸는 것이다. 배수 목록은 쓰기가 드물고 읽기가 잦아 그 자료구조의 전형적 용례다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-ownership-and-concurrency/case/case-grpc-client-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-client-f03" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-client-f03.txt" - ] - }, - { - "title": "릴리스 버전 불변성이 프로세스 안에서만 성립한다", - "kind": "case", - "slug": "grpc-codegen-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-codegen#L220`" - ], - "owning-module": "`grpc-codegen` · priority: `P3`", - "classification": "발행 이력이 발행자 인스턴스의 필드다. 새 프로세스는 아무것도 기억하지 못하므로 같은 버전을 다른 해시로 다시 발행하려는 시도가 통과한다. 이 클래스가 존재하는 이유가 그 규칙이다 — \"refuses to let a released version change underneath its consumers.\" 그 규칙이 지켜지는 범위가 한 발행자 인스턴스의 수명이다. 빌드마다 새 프로세스가 도는 것이 정상 형태이므로, 실제로 이 검사가 무언가를 막으려면 이력이 산출물 저장소나 파일에서 와야 한다. `GrpcSchemaBaseline` 이 이미 릴리스된 해시를 들고 있으므로 그 방향의 재료는 있다. 덧붙여 이 맵은 동기화되지 않는다. 발행자를 공유해 병렬로 평가하면 경합한다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-ownership-and-concurrency/case/case-grpc-codegen-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-codegen-f02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-codegen-f02.txt" - ] - }, - { - "title": "목록으로 보고하는 검증기가 주소 수 0 에서 던진다", - "kind": "case", - "slug": "grpc-discovery-f03", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-discovery#L187`" - ], - "owning-module": "`grpc-discovery` · priority: `P3`", - "classification": "`profile.resolverProfile(n)` 이 `new GrpcResolverProfile(DNS, …, n)` 을 만들고, 그 정규 생성자가 거부한다. 그래서 `violations(profile, 0)` 은 빈 목록도 위반 목록도 아닌 `IllegalArgumentException` 이다. 같은 메서드가 `profile == null` 에는 명시적으로 던지고 나머지는 목록으로 답하므로, 호출자는 이 API 를 \"던지지 않고 보고한다\" 로 읽는다. `expectedAddressCount` 는 이 리프가 계산하지 않고 입력으로 받는 값이고(§16), 그 출처는 헤드리스 레코드의 DNS 조회 결과다. 롤아웃 중 파드가 모두 교체되는 순간이나 셀렉터가 어긋난 서비스에서 그 답은 0 이다. 그것은 이 리프가 다루는 문제 영역 안의 상태이지 프로그래밍 오류가 아니다 — 그리고 운영자가 가장 보고받고 싶어 할 상태다. `GrpcResolverProfile` 쪽 거부 자체는 옳다. 값 객체가 \"주소 0 개인 목표\"를 표현하지 않는 것은 §4 의 두 겹 분담과 일치한다. 어긋난 것은 그 위에 얹힌 검증기가 그 예외를 그대로 통과시킨다는 점이다. `violations` 가 `expectedAddressCount < 1` 을 먼저 보고 위반 문자열로 보고한 뒤 나머지 검사를 건너뛴다. 그러면 이 리프가 답할 수 있는 가장 중요한 배포 상태 하나가 예외가 아니라 목록의 한 줄이 된다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-ownership-and-concurrency/case/case-grpc-discovery-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-discovery-f03" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-discovery-f03.txt" - ] - }, - { - "title": "`markCommitted` 는 던지고 `markFailed` 는 조용히 넘어간다", - "kind": "case", - "slug": "grpc-operation-ledger-jpa-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-operation-ledger-jpa#L192`" - ], - "owning-module": "`grpc-operation-ledger-jpa` · priority: `P3`", - "classification": "커밋 쪽의 근거는 자바독에 있다 — 청구 없이 커밋하면 변경은 내구적이고 보호받지 못한다. 실패 쪽에는 근거가 없다. 청구가 사라진 뒤 도착한 종결 실패가 아무 흔적도 남기지 않는다. 회수가 청구를 지운 뒤 원래 소유자가 실패를 기록하려는 경우가 그 형태다. 의도라면 그 이유를 자바독에 적어야 하고, 아니라면 커밋 쪽과 같게 다뤄야 한다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-ownership-and-concurrency/case/case-grpc-operation-ledger-jpa-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-operation-ledger-jpa-f02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-operation-ledger-jpa-f02.txt" - ] - }, - { - "title": "완료 조정자가 요청 경로에서 동기화 없는 가변 리스트를 변경한다", - "kind": "case", - "slug": "grpc-policy-f04", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-policy#L271`" - ], - "owning-module": "`grpc-policy` · priority: `P2`", - "classification": "`synchronized`·`Concurrent*`·`volatile`·`Lock` 전부 0 이고 단일 스레드 전용 표기도 없다. 같은 리프의 `GrpcSerializedStreamWriter` 는 아홉 마커로 제대로 닫혀 있어, 이 리프가 동시성을 인지하고 있음을 보여 준다. `reconcile` 은 완료 결과가 불확실한 호출마다 불린다 — 장애 상황에서 동시에 몰리는 경로다. 그리고 `pending` 이 담는 것은 사람이 조정해야 하는 연산 목록이므로, 유실은 조정되지 않은 채 잊히는 연산이 된다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-ownership-and-concurrency/case/case-grpc-policy-f04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-policy-f04", - "grpc-policy-f04-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-policy-f04.txt" - ] - }, - { - "title": "스트림 수명 조정자의 배수 신호가 스레드를 건너면서 `volatile` 이 아니다", - "kind": "case", - "slug": "grpc-policy-f05", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-policy#L285`" - ], - "owning-module": "`grpc-policy` · priority: `P2`", - "classification": "두 메서드의 호출자가 다른 스레드다. `signalDrain()` 은 서버가 내려갈 때 종료 훅이 부르고, `terminationDue(...)` 는 스트림 자신의 틱에서 불린다 — 클래스 javadoc 이 검사 순서를 \"then drain, because a server that has been told to stop should stop before its own timers fire\" 로 규정한 그 틱이다. 평범한 `boolean` 이고 `volatile`·`synchronized`·`AtomicBoolean` 어느 것도 없다. 자바 메모리 모델 아래서 틱 스레드가 이 쓰기를 관측할 보장이 없다. 관측하지 못하면 스트림은 배수 명령을 받고도 계속 돌고, 최대 수명(기본 1시간)이 차야 끝난다. 같은 저장소가 같은 뜻의 플래그를 두 번은 `volatile` 로 적었다(§12.3). 세 번째만 빠졌다. 수정은 `volatile boolean` 한 단어다. `heartbeat` 의 `lastActivity` 는 같은 문제가 아니다 — 스트림 틱 스레드만 만진다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-ownership-and-concurrency/case/case-grpc-policy-f05.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-policy-f05", - "grpc-policy-f05-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-policy-f05.txt" - ] - }, - { - "title": "릴리스 게이트의 입력이 전부 호출자가 손으로 만드는 값이다", - "kind": "case", - "slug": "grpc-testkit-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-testkit#L172`" - ], - "owning-module": "`grpc-testkit` · priority: `P3`", - "classification": "세 입력 중 어느 것도 실제 레인 결과나 실제 산출물에서 오지 않는다. 게이트를 부르는 곳은 자기 테스트 하나뿐이고, 그 테스트가 세 값을 리터럴로 만든다. 이 형태 자체는 이 저장소의 다른 게이트와 다르다. mongo 가족의 증거 검증기는 테스트 결과 XML 을 읽고 파일의 수정 시각까지 본다. 이쪽 게이트는 그런 산출물 판독기를 갖지 않는다. 지금은 무해하다 — 릴리스 절차가 이 게이트를 부르지 않기 때문이다. 기록하는 이유는 §17.1 을 고쳐 레인을 자동으로 돌리게 되면, 그 결과를 이 게이트에 넣어 주는 코드가 함께 필요하다는 점이다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-ownership-and-concurrency/case/case-grpc-testkit-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-testkit-f02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-testkit-f02.txt" - ] - }, - { - "title": "저널의 `itemsCompleted` 단조성이 인터페이스 계약에 없다", - "kind": "case", - "slug": "messaging-admin-runtime-f06", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-admin-runtime#L936`" - ], - "owning-module": "`messaging-admin-runtime` · priority: `P3`", - "classification": "`AdminOperationJournal` javadoc 은 구현 의무 셋을 명시하면서 이것을 빠뜨렸고, `fail` 의 `@param` 은 오히려 문자 그대로 저장하라고 읽힌다. 유일한 호출자는 낡은 값을 넘긴다. 두 구현이 각각 clamp 해서 무사한 상태다(`EVD-308`). 두 가지 중 하나가 필요하다. 인터페이스 javadoc 에 \"`itemsCompleted` 는 단조 증가해야 하며 구현은 기존 값보다 작은 값을 무시한다\" 를 명시하거나, 호출자가 실제 체크포인트 값을 넘기도록 고친다. 후자가 더 정직하다 — 지금 `journal.fail(lease, lease.resumeFrom(), …)` 은 \"이번 시도가 아무것도 못 했다\" 고 주장하는 것이고, 그것은 대개 사실이 아니다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-ownership-and-concurrency/case/case-messaging-admin-runtime-f06.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-admin-runtime-f06" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-admin-runtime-f06.txt" - ] - }, - { - "title": "리플레이가 리스를 받지만 재개하지 않는다", - "kind": "case", - "slug": "messaging-admin-runtime-f07", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-admin-runtime#L942`" - ], - "owning-module": "`messaging-admin-runtime` · priority: `P3`", - "classification": "`executeReplay` 는 `journal.begin(...)` 으로 리스를 받고 `lease.resumeFrom()` 을 쓰지 않는다. `ReplayService.replay(...)` 시그니처에 재개 지점이 없고 체크포인트 콜백도 없다. 클래스 javadoc 의 \"a retry continues the same operation\" 은 리드라이브에만 해당한다. 리플레이가 재개 불필요하다면(같은 구간을 다시 읽는 것이 멱등이므로) 그 근거를 적고, 저널 사용을 \"중복 실행 방지\" 로만 한정하는 것이 낫다. 재개가 필요하다면 리드라이브와 같은 형태로 맞춘다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-ownership-and-concurrency/case/case-messaging-admin-runtime-f07.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-admin-runtime-f07" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-admin-runtime-f07.txt" - ] - }, - { - "title": "반환을 순번에 맞추는 조각이 production 에 없고, 시험이 그 자리를 스스로 메운다", - "kind": "case", - "slug": "messaging-rabbit-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-rabbit#L238`" - ], - "owning-module": "`messaging-rabbit` · priority: `P2`", - "classification": "이 어댑터의 핵심 보장(§1)은 반환과 확인을 **같은 발행** 에 묶는 데 달려 있다. 묶는 열쇠는 순번이다. 그런데 AMQP 의 `basic.return` 콜백은 순번을 주지 않는다. 교환기·라우팅 키·속성·본문만 온다. 그래서 발행자가 순번을 메시지에 실어 보내고 반환에서 되읽어야 한다. `RabbitHeaderMapper.toProperties` 전문에 그런 헤더가 없다. 쓰는 것은 `msg.*` 예약 헤더들과 AMQP 의 `messageId`·`correlationId`·`timestamp`·`deliveryMode` 뿐이다. 그 조각이 존재하는 곳은 시험 하나다. 그 메서드의 javadoc 이 문제를 정확히 서술한다. \"the adapter has to\" 인데 어댑터는 하지 않는다. `RabbitChannelPublisher` 의 javadoc 은 **등록 경합**(확인이 `basicPublish` 반환보다 먼저 올 수 있다)만 설명하고 이 상관 문제는 언급하지 않는다. 결과는 이렇다. 언젠가 `RabbitChannelPublisher` 를 구현하는 사람은 이 헤더 규약을 다시 발명해야 하고, 발명하지 않으면 `onReturn` 이 호출되지 않아 unroutable 발행이 **`CONFIRMED` 로 보고된다** — 이 어댑터가 존재하는 이유로 든 바로 그 실패다. 수정은 순번 헤더를 `RabbitHeaderMapper` 나 `RabbitPublishMapper` 로 올려 production 계약으로 만들고, 그 이름을 `RabbitChannelPublisher` javadoc 에 적는 것이다. 지금은 그 규약이 시험 파일 20줄에만 있다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-ownership-and-concurrency/case/case-messaging-rabbit-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-rabbit-f02", - "messaging-rabbit-f02-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-rabbit-f02.txt" - ] - } - ], - "concept": [], - "reference": [ - { - "title": "같은 튜닝 값이 두 계층에 있으면 어느 쪽이 이기는지 정한다", - "kind": "reference", - "slug": "messaging-claim-check-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-claim-check#L516`" - ], - "owning-module": "`messaging-claim-check`", - "rule": "같은 튜닝 값이 두 계층에 있으면 어느 쪽이 이기는지 정한다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — claim check 문턱이 두 곳에서 독립적으로 정해진다", - "scope": [ - "멱등·아웃박스·인박스 계열. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "state-ownership-and-concurrency/reference/reference-messaging-claim-check-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "가변 필드로 상태 전이를 표현하면 가시성을 함께 정한다", - "kind": "reference", - "slug": "messaging-security-f08", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-security#L697`" - ], - "owning-module": "`messaging-security`", - "rule": "가변 필드로 상태 전이를 표현하면 가시성을 함께 정한다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — `CredentialRuntime.material`이 동기화되지 않는다", - "scope": [ - "보안·승인 표면. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "state-ownership-and-concurrency/reference/reference-messaging-security-f08.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "함께 읽히는 두 맵은 한 값으로 묶는다", - "kind": "reference", - "slug": "messaging-spring-cloud-stream-bridge-f04", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-spring-cloud-stream-bridge#L579`" - ], - "owning-module": "`messaging-spring-cloud-stream-bridge`", - "rule": "함께 읽히는 두 맵은 한 값으로 묶는다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 두 맵 갱신이 원자적이지 않다", - "scope": [ - "정책과 조립 경계. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "state-ownership-and-concurrency/reference/reference-messaging-spring-cloud-stream-bridge-f04.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "에러 메시지가 지시하는 설정은 그 설정을 읽는 코드와 함께 존재해야 한다", - "kind": "reference", - "slug": "messaging-spring-cloud-stream-bridge-f06", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-spring-cloud-stream-bridge#L597`" - ], - "owning-module": "`messaging-spring-cloud-stream-bridge`", - "rule": "에러 메시지가 지시하는 설정은 그 설정을 읽는 코드와 함께 존재해야 한다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 활성화 프로퍼티 키가 에러 메시지에만 존재한다", - "scope": [ - "정책과 조립 경계. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "state-ownership-and-concurrency/reference/reference-messaging-spring-cloud-stream-bridge-f06.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - } - ], - "question": [ - { - "title": "notification derived idempotency key가 32-bit hash", - "kind": "question", - "slug": "", - "readiness": "", - "publication": "미작성" - } - ], - "decision": [] - } - }, - "schema-and-data-contracts": { - "topic": "schema-and-data-contracts", - "title": "스키마와 데이터 계약 — migration·index·collection 규칙의 실행 의미", - "readerQuestion": "", - "kinds": { - "case": [ - { - "title": "schema version 실패는 두 경로 중 어느 쪽도 온전하지 않다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "collection 이름 불변식이 aggregation executor의 서명에서 깨진다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "index diff가 실제로 비교하는 것은 두 필드뿐이다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "TTL이 두 곳에 선언되고, 규칙을 가진 쪽은 아무도 쓰지 않는다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`check`에 붙은 `verifyJsonSchemaRuntimeGraph`가 실행되면 실패한다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "음수 `Retry-After` 헤더가 throttle 결과 대신 `IllegalArgumentException`을 만든다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "예외가 들고 있는 능력이 `transient` 라 역직렬화 뒤 사라진다", - "kind": "case", - "slug": "grpc-advanced-bootstrap-f05", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-advanced-bootstrap#L312`" - ], - "owning-module": "`grpc-advanced-bootstrap` · priority: `P3`", - "classification": "`transient` 는 보통 직렬화 가능하지 않은 필드를 담은 `Serializable` 클래스에 대한 정적 분석 경고를 끄려고 붙인다. 그런데 열거형은 언제나 직렬화 가능하다 — 여기서 `transient` 가 막을 문제가 애초에 없다. 대가는 있다. 예외가 직렬화를 거쳐 오면 `capability()` 가 `null` 이다. 메시지 문자열은 살아남으므로 사람이 읽는 데는 지장이 없고, 그래서 눈에 띄지 않는다. 이 예외를 던지는 `require` 자체가 리프 밖에서 불리지 않으므로(§12.1) 오늘 도달하지 않는다. `transient` 를 지우는 것이 수정 전부다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-data-contracts/case/case-grpc-advanced-bootstrap-f05.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-advanced-bootstrap-f05" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-advanced-bootstrap-f05.txt" - ] - }, - { - "title": "비교 픽스처에 비교 대상이 없다", - "kind": "case", - "slug": "grpc-advanced-edition-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-advanced-edition#L180`" - ], - "owning-module": "`grpc-advanced-edition` · priority: `P2`", - "classification": "`compatibility.proto` 의 주석이 존재 이유를 적는다. 그 쌍둥이가 저장소에 없다. 한 곳뿐이다. 같은 필드와 번호를 proto3 로 선언한 파일이 없으므로 비교가 성립하지 않는다. 그리고 두 번째 전제도 없다. 이 저장소에는 protobuf 플러그인이 어디에도 없다 — `grpc-proto-contract` 와 `adapter-inbound-grpc` 의 build.gradle 이 그 사실을 주석으로 명시한다. 그러므로 편집 파일도 proto3 파일도 컴파일되지 않고, 유선 바이트와 JSON 을 비교할 산출물 자체가 만들어지지 않는다. 결과적으로 `GrpcEditionCompatibilityReport` 는 사람이 손으로 채우는 기록이 된다. 승격 게이트가 그것을 읽어 판정하므로, 게이트의 입력이 측정이 아니라 선언이다. Advanced 가족이라 오늘의 배포에는 영향이 없다. 기록하는 이유는 이 리프의 목적이 \"공개 서비스가 옮겨 가기 전에 그 실패를 찾는 것\" 이고, 그 실패를 찾을 장치가 픽스처 하나만 있고 짝이 없다는 점이다. `compatibility_proto3.proto` 를 같은 디렉터리에 두어 필드·번호·JSON 이름을 맞추고, 두 파일을 컴파일해 산출물을 비교하는 레인을 만든다. 그 레인이 생기기 전까지는 `GrpcEditionCompatibilityReport` 가 측정이 아니라 선언이라는 것을 자바독에 적는 편이 낫다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-data-contracts/case/case-grpc-advanced-edition-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-advanced-edition-f01", - "grpc-advanced-edition-f01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-advanced-edition-f01.txt" - ] - }, - { - "title": "픽스처의 메서드 경로가 서비스 × 메서드 교차곱이다", - "kind": "case", - "slug": "grpc-codegen-f03", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-codegen#L239`" - ], - "owning-module": "`grpc-codegen` · priority: `P3`", - "classification": "`stub.(` 호출 하나가 그 파일이 import 한 **모든** 서비스에 대해 메서드 경로를 만든다. javadoc 의 규칙 서술은 단수형이다 — \"a method is a `stub.(` call, mapped to `/`\". 서비스가 둘 이상일 때 어느 서비스인지는 소스 텍스트만으로 알 수 없고, 코드는 전부에 붙이는 쪽을 골랐다. 결과는 존재하지 않는 메서드 경로를 요구하는 픽스처다. 서비스 둘과 메서드 셋이면 요구 경로가 여섯 개가 되고, 그중 셋은 어떤 후보 스키마에도 없으므로 `breaksAgainst` 가 항상 `METHOD_PATH` 파괴를 보고한다. 그러면 `GrpcSchemaArtifactPublisher.evaluate` 가 모든 발행을 거부한다. 커밋된 픽스처는 서비스가 하나(`DocumentServiceGrpc`)라 지금은 정확하다. 두 번째 소비자 픽스처를 추가하는 순간 성립한다. 수정은 호출자 변수의 선언 타입을 함께 읽어 메서드를 서비스에 귀속시키거나, 서비스가 둘 이상인 픽스처를 거부하는 것이다. 후자는 지금 형태의 근사를 명시적으로 만든다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-data-contracts/case/case-grpc-codegen-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-codegen-f03" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-codegen-f03.txt" - ] - }, - { - "title": "직렬화 가능하다고 선언한 예외가 자기 내용을 직렬화하지 않는다", - "kind": "case", - "slug": "grpc-core-api-f06", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-core-api#L241`" - ], - "owning-module": "`grpc-core-api` · priority: `P3`", - "classification": "`serialVersionUID` 는 이 타입이 직렬화된다는 선언이고, `transient` 는 유일한 필드가 그 직렬화에서 빠진다는 선언이다. 둘이 함께 있으면 역직렬화된 예외는 `context == null` 이고, 공개 메서드 둘 중 하나(`requiresReconciliation()`)가 NPE 를 던진다. `transient` 자체는 강제된 선택이다 — `GrpcFailureContext` 가 `Serializable` 을 구현하지 않으므로 필드를 남기면 예외가 직렬화되지 않는다. 기록하는 이유는 이 리프의 서술 규율과 대비되기 때문이다. 다른 자리에서는 부재마다 이유가 붙어 있다(\"There is no factory that takes raw metadata, and that absence is the design\"). 여기에는 `transient` 의 이유도, 역직렬화 뒤의 계약도 적혀 있지 않다. 도달성은 낮다. gRPC 예외가 자바 직렬화를 지나는 경로는 이 저장소에 없다. 수정은 셋 중 하나다 — `GrpcFailureContext` 와 그 구성 요소를 `Serializable` 로 만들거나, `serialVersionUID` 를 지워 직렬화를 지원하지 않음을 명시하거나, `context()` 와 `requiresReconciliation()` 이 null 문맥을 다루도록 하고 그 이유를 적는 것.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-data-contracts/case/case-grpc-core-api-f06.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-core-api-f06" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-core-api-f06.txt" - ] - }, - { - "title": "직렬 스트림 기록기의 가장 오래된 것 버리기가 잘못된 메시지의 바이트를 뺀다", - "kind": "case", - "slug": "grpc-policy-f03", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-policy#L249`" - ], - "owning-module": "`grpc-policy` · priority: `P2`", - "classification": "버려지는 것은 꺼낸 봉투인데 빼는 값은 새 메시지의 크기다. 봉투는 크기를 성분으로 담지 않으므로 이 지점에서 버려지는 크기를 알 방법이 없다. 계산을 따라가면 이렇다. 한 번의 DROP_OLDEST 마다 `queuedBytes` 는 `nextBytes` 만큼 빠졌다가 `enqueue` 에서 같은 값만큼 다시 더해진다 — **순변화 0**. 그런데 큐의 실제 내용은 `nextBytes - droppedBytes` 만큼 바뀐다. 그 차이가 매 낙차마다 쌓인다. 방향은 둘 다 틀렸다. 들어오는 메시지가 버려지는 것보다 크면 추적값이 실제보다 **낮아져** 바이트 경계가 늦게 발화한다(메모리). 반대면 실제보다 **높아져** 경계가 이르게 발화한다(불필요한 종료·낙차). 누적 바이트는 흐름 제어 정책의 판정 입력이고, 바이트 경계의 존재 이유가 javadoc 에 있다 — 개수 경계만 있으면 메모리 한도를 가장 큰 메시지가 정한다. `flush()` 가 큐를 비우면서 `queuedBytes = 0L` 로 되돌리므로 오차가 flush 를 건너 누적되지는 않는다. 그래서 이것은 영구 드리프트가 아니라 한 flush 주기 안의 폭주 구간에서 바이트 경계를 잘못 판정하는 결함이다. 낙차가 일어나는 상황이 곧 소비자가 못 따라가는 상황이고, 그때 flush 간격이 가장 길어진다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-data-contracts/case/case-grpc-policy-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-policy-f03", - "grpc-policy-f03-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-policy-f03.txt" - ] - }, - { - "title": "`reserved 2 to 5;` 범위가 개별 숫자로만 수집되어 `RESERVED_HISTORY` 오탐이 된다", - "kind": "case", - "slug": "grpc-proto-contract-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-proto-contract#L175`" - ], - "owning-module": "`grpc-proto-contract` · priority: `P3`", - "classification": "`reserved 2 to 5;` 는 그룹이 `\"2 to 5\"` 이고 수집되는 것은 `{2, 5}` 다. `3`·`4` 는 들어가지 않는다. `reserved 9 to max;` 는 `{9}` 만 남는다. 그러면 삭제 이력이 `3` 을 담고 스키마가 `reserved 2 to 5;` 로 정확히 예약했는데도 `RESERVED_HISTORY` 위반이 보고된다. 범위 예약은 표준 문법이고 여러 필드를 한 번에 지울 때 쓰는 형태이므로 도달 가능하다. 수정은 `to` 를 인식해 범위를 펼치는 것이다. `max` 는 상한 상수로 다루거나 그 메시지에 대해 검사를 통과시킨다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-data-contracts/case/case-grpc-proto-contract-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-proto-contract-f01" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-proto-contract-f01.txt" - ] - }, - { - "title": "커밋 스키마 게이트가 파일 목록을 하드코딩한다", - "kind": "case", - "slug": "grpc-proto-contract-f03", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-proto-contract#L203`" - ], - "owning-module": "`grpc-proto-contract` · priority: `P3`", - "classification": "리소스 디렉터리를 훑지 않는다. 이 리프에 세 번째 `.proto` 를 추가하면 이 테스트를 함께 고치기 전까지 판정되지 않고, 빌드는 초록으로 남는다. 같은 저장소가 다른 곳에서 이 형태를 이미 경계했다 — 빠뜨림이 통과가 되는 게이트다. 수정은 `proto/**` 아래 `.proto` 를 전부 열거해 돌리는 것이다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-data-contracts/case/case-grpc-proto-contract-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-proto-contract-f03" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-proto-contract-f03.txt" - ] - }, - { - "title": "열거형 안의 `reserved` 는 수집되지 않는다", - "kind": "case", - "slug": "grpc-proto-contract-f04", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-proto-contract#L265`" - ], - "owning-module": "`grpc-proto-contract` · priority: `P3`", - "classification": "`scan` 은 스코프 종류로 갈라진다. `reserved` 수집은 `scanMessageMember` 안에만 있다. proto3 는 열거형에도 `reserved 2, 15;` 와 `reserved \"OLD_VALUE\";` 를 허용하고, 열거형 값을 지울 때 번호를 예약하는 것은 필드와 같은 이유로 필요하다 — 예약하지 않고 재사용하면 옛 클라이언트가 보낸 정수가 다른 뜻으로 해석된다. 지금 `SchemaHistory` 에 열거형 이름으로 삭제 이력을 넣으면, 스키마가 정확히 예약했더라도 `scan.reservedNumbers` 에 그 이름이 없으므로 `RESERVED_HISTORY` 오탐이 난다. §17.1 의 범위 문법 문제와 같은 방향(fail-closed)이고 같은 자리에서 고칠 수 있다. `reserved` 수집을 스코프 종류와 무관하게 먼저 시도한 뒤 나머지 판정을 갈래로 보낸다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-data-contracts/case/case-grpc-proto-contract-f04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-proto-contract-f04" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-proto-contract-f04.txt" - ] - }, - { - "title": "감사 sink 인터페이스가 사용처에서 다시 선언된다", - "kind": "case", - "slug": "messaging-observability-f04", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-observability#L699`" - ], - "owning-module": "`messaging-observability` · priority: `P3`", - "classification": "`MessagingAuditSink.record(MessagingAuditEvent)`와 같은 시그니처를 `RedriveService:208`이 자기 중첩 인터페이스로 선언한다. `messaging-admin-runtime`은 `messaging-observability`에 의존할 수 있다(registry 확인). `MessagingAuditSink.inMemory()`가 제공하는 구현을 admin-runtime이 쓸 수 없다. 그리고 감사 sink의 계약(분리된 보존·접근·무결성 요구)이 문서화된 곳과 실제로 구현되는 곳이 다르다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-data-contracts/case/case-messaging-observability-f04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-observability-f04" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-observability-f04.txt" - ] - }, - { - "title": "선언된 content type과 실제 인코딩이 조용히 갈라질 수 있다", - "kind": "case", - "slug": "messaging-runtime-core-f03", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-runtime-core#L727`" - ], - "owning-module": "`messaging-runtime-core` · priority: `P3`", - "classification": "`encode`가 `codecs.find(message.contentType()).orElseGet(codecs::defaultCodec)`으로 폴백한다. 출하 registry에는 JSON codec 하나만 등록된다. 봉투가 `application/avro`를 선언해도 JSON으로 인코딩되고, `EncodedMessage`의 content type은 codec이 정하므로 `application/json`이 된다. 실패하지 않고 **다른 포맷으로 성공**한다. 소비 측이 봉투의 원래 선언을 믿고 디코더를 고르면 어긋난다. `DestinationProfile.schema().codec()`이 목적지의 codec을 선언하는데 그 값과 대조하는 코드가 이 경로에 없다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-data-contracts/case/case-messaging-runtime-core-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-runtime-core-f03" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-runtime-core-f03.txt" - ] - }, - { - "title": "진화 판단이 두 곳에 있고 형태가 반대다", - "kind": "case", - "slug": "messaging-schema-avro-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-schema-avro#L533`" - ], - "owning-module": "`messaging-schema-avro` · priority: `P2`", - "classification": "`isTransitive`는 `SchemaCompatibilityValidator`(public static)와 이 leaf(private static)에 글자까지 같은 복사본이 있다. 방향 판정은 전자가 허용목록, 후자가 거부목록이다. 오늘 7개 모드에서 결과는 같지만 형태가 반대이므로 `SchemaCompatibility`에 값이 추가되는 순간 갈라진다 — 허용목록은 \"검사 안 함\", 거부목록은 \"양방향 검사\". 그리고 이 중복은 schema-api의 javadoc이 명시적으로 막으려던 것이다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-data-contracts/case/case-messaging-schema-avro-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-schema-avro-f02", - "messaging-schema-avro-f02-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-schema-avro-f02.txt" - ] - }, - { - "title": "포맷 중립 payload 정책이, 자기 상수를 두고 JSON codec의 상수를 참조한다", - "kind": "case", - "slug": "messaging-schema-json-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-schema-json#L463`" - ], - "owning-module": "`messaging-schema-json` · priority: `P2`", - "classification": "`MessagingCoreAutoConfiguration:410-413`이 `new PayloadPolicy(JacksonMessageCodec.DEFAULT_MAX_BYTES, JacksonMessageCodec.DEFAULT_MAX_BYTES / 2)`를 만든다. 그런데 `PayloadPolicy` 자신이 같은 값의 public 상수 `PayloadPolicy.DEFAULT_MAX_BYTES`(`messaging-policy/PayloadPolicy.java:17`)를 갖고 있다. `MessagingAdmissionController`는 목적지의 codec이 무엇이든 지나는 관문이다. 그 상한이 **한 포맷 클래스**의 상수에서 나오면 두 가지가 깨진다. (1) `@ConditionalOnMissingBean`이 허용하는 대로 애플리케이션이 자기 `MessageCodecRegistry`를 내놓아 JSON codec을 대체해도, 정책은 여전히 JSON codec의 값을 읽는다. (2) 다섯 곳의 리터럴 중 하나만 바뀌면 조용히 갈라지고, `RawBytesMessageCodec` javadoc이 이미 \"shared with the Stable codecs\"라고 사실과 다르게 부르고 있다. 정책 소유자가 이미 존재하는데 배선이 그것을 지나쳤다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-data-contracts/case/case-messaging-schema-json-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-schema-json-f01", - "messaging-schema-json-f01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-schema-json-f01.txt" - ] - }, - { - "title": "registry 조회 로직이 세 codec에 복제돼 있다", - "kind": "case", - "slug": "messaging-schema-protobuf-f04", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-schema-protobuf#L554`" - ], - "owning-module": "`messaging-schema-protobuf` · priority: `P3`", - "classification": "`requireRegistered`(JSON/Protobuf)와 `schemaFor`(Avro)가 같은 3단 판단을 각자 구현한다. JSON과 Protobuf는 `registeredVersions` 헬퍼까지 사실상 동일하다. 판단은 `MessageContractKey`의 성질이지 포맷의 성질이 아니다. 그리고 실제로 갈라졌다 — Avro만 `AVRO_` 접두 코드를 쓰고 등록 버전 목록을 메시지에 넣지 않는다. `messaging-schema-api`가 흡수할 수 있는 형태다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-data-contracts/case/case-messaging-schema-protobuf-f04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-schema-protobuf-f04" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-schema-protobuf-f04.txt" - ] - } - ], - "concept": [], - "reference": [ - { - "title": "컬렉션 순서가 계약이면 양쪽에서 같은 방향으로 적는다", - "kind": "reference", - "slug": "messaging-schema-avro-f03", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-schema-avro#L542`" - ], - "owning-module": "`messaging-schema-avro`", - "rule": "컬렉션 순서가 계약이면 양쪽에서 같은 방향으로 적는다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — `history` 순서 계약이 port와 게이트에서 반대다", - "scope": [ - "스키마와 코덱 계열. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "schema-and-data-contracts/reference/reference-messaging-schema-avro-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "안정 코드는 판단 단위로 정하고 구현 단위로 정하지 않는다", - "kind": "reference", - "slug": "messaging-schema-avro-f05", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-schema-avro#L560`" - ], - "owning-module": "`messaging-schema-avro`", - "rule": "안정 코드는 판단 단위로 정하고 구현 단위로 정하지 않는다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 에러 코드 어휘가 형제 codec과 갈라진다", - "scope": [ - "스키마와 코덱 계열. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "schema-and-data-contracts/reference/reference-messaging-schema-avro-f05.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - } - ], - "question": [ - { - "title": "messaging schema qualification boundary", - "kind": "question", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`dataschema`가 채워질 경로가 없다", - "kind": "question", - "slug": "messaging-cloudevents-f04", - "readiness": "OPEN", - "source": [ - "`final/document.md#a19-messaging-cloudevents#L547`" - ], - "owning-module": "`messaging-cloudevents` · priority: `P3`", - "classification": "`toCloudEvent`가 `encoded.schemaReference().flatMap(SchemaReference::schemaUri).ifPresent(builder::withDataSchema)`로 `dataschema`를 채운다. 그런데 세 codec(JSON·Avro·Protobuf) 모두 `SchemaReference.of(subject, version)`로 만들고, 그 factory는 `schemaUri`를 `Optional.empty()`로 둔다. `dataschema`는 CloudEvents 소비자가 페이로드를 해석하는 데 쓰는 표준 속성이다. 항상 비어 있으므로 이 프로파일이 만드는 CloudEvent는 스키마 위치를 알리지 않는다. `schemaversion` 확장이 그 자리를 대신하지만 그것은 비표준 확장이다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-data-contracts/question/openquestion-messaging-cloudevents-f04.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "protobuf-java 버전이 저장소에 셋이고 전역 정책이 없다", - "kind": "question", - "slug": "messaging-schema-protobuf-f03", - "readiness": "OPEN", - "source": [ - "`final/document.md#a19-messaging-schema-protobuf#L545`" - ], - "owning-module": "`messaging-schema-protobuf` · priority: `P3`", - "classification": "`ext.protobufVersion = 3.25.5`(grpc 모듈 범위로 한정), 이 leaf `4.29.3`, websocket `4.33.2`. lockfile들이 세 값을 모두 고정한다. 오늘은 사고가 아니다 — 이 leaf의 `runtime_memberships`가 `[]`이라 세 버전이 한 classpath를 공유하지 않는다. **채택 시점의 부채다.** 이 leaf를 런타임에 편입시키는 순간 버전 판정이 필요해지고, 그때 참조할 전역 정책이 없다. 그리고 `src/build.gradle`의 \"the single SSOT\"라는 표현이 전역 정책의 존재를 시사하는데 실제 범위는 그 문장 안에서 grpc 모듈로 한정된다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-data-contracts/question/openquestion-messaging-schema-protobuf-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - } - ], - "decision": [] - } - }, - "transport-and-provider-semantics": { - "topic": "transport-and-provider-semantics", - "title": "전송·프로바이더 의미론 — timeout·TLS·서명·실패 분류의 경계", - "readerQuestion": "", - "kinds": { - "case": [ - { - "title": "timeout 초과 경로가 한 observation에 success와 failure를 모두 기록한다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "동적 대상 DNS 핀 능력 검사가 블로킹 오버로드에만 있다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "SigV4가 서명한 `host`에 포트가 없어, 기본 포트가 아닌 엔드포인트에서 서명이 어긋난다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "SigV4 서명 키 파생이 비밀을 지울 수 없는 `String`으로 승격시킨다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "FCM만 \"커밋 후 응답 손실 = ambiguous\" 규칙 밖에 있고, 그 FCM이 두 계약 집합 어디에도 없다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "공유 provider 계약이 8종 중 3종에서만 상속되고, 강제 장치가 없다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "능력 상수가 프로파일 조건보다 오래 살아서 중복 제거 없는 목적지가 가드를 통과한다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "자동 설정이 `transport` 를 읽지 않고 전송을 하드코딩한다", - "kind": "case", - "slug": "grpc-spring-boot-starter-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-spring-boot-starter#L221`" - ], - "owning-module": "`grpc-spring-boot-starter` · priority: `P3`", - "classification": "`GrpcPlatformProperties.transport` 는 `GrpcServerTransport` 열거형이고 기본값이 `NETTY_SHADED` 다. 그 값을 자동 설정이 보지 않으므로 다른 값을 설정해도 만들어지는 프로파일은 같다. 지금은 무해에 가깝다 — 기본값이 하드코딩된 것과 같고, 다른 값은 §17.1 때문에 거부되지도 않지만 반영되지도 않는다. 그러나 설정 키가 존재하고 문서화되어 있으므로 운영자는 그것이 전송을 고른다고 읽는다. 수정은 프로파일 팩토리를 `transport` 로 분기시키거나, 그 키를 검증 전용임을 자바독에 명시하는 것이다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transport-and-provider-semantics/case/case-grpc-spring-boot-starter-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-spring-boot-starter-f02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-spring-boot-starter-f02.txt" - ] - }, - { - "title": "토폴로지 검증 스택이 두 벌이고 판정이 어긋난다", - "kind": "case", - "slug": "messaging-admin-runtime-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-admin-runtime#L910`" - ], - "owning-module": "`messaging-admin-runtime` · priority: `P2`", - "classification": "Stack A 는 파티션 스케일업을 ADVISORY 로 두어 기동을 허용하고 그 근거를 명시한다. Stack B 는 같은 상황을 차이로 보고 기동을 거부한다. 둘 다 프로덕션 호출부가 0건이라 지금은 충돌하지 않지만, `final/document.md#a19-messaging-admin-api` §17 첫 항목대로 토폴로지 검증을 기동에 배선하는 순간 **어느 스택을 배선하느냐가 스케일업한 배포의 기동 여부를 가른다**. Stack A 가 남아야 할 것으로 보인다 — severity 구분, `physicalName` 검사, 근거 주석이 있고 테스트도 13건으로 더 두껍다. Stack B(`TopologyValidationRuntime`, `TopologyReader`, `ObservedTopology`, 그리고 그것만 쓰는 `TopologyManifest.differencesFrom`)를 제거하는 편이 낫다. 같은 코드 문자열 `TOPOLOGY_MISMATCH` 를 두 예외 타입이 쓰는 것도 정리 대상이다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transport-and-provider-semantics/case/case-messaging-admin-runtime-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-admin-runtime-f02", - "messaging-admin-runtime-f02-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-admin-runtime-f02.txt" - ] - }, - { - "title": "실패한 리드라이브 항목의 사유가 어디에도 남지 않는다", - "kind": "case", - "slug": "messaging-admin-runtime-f10", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-admin-runtime#L961`" - ], - "owning-module": "`messaging-admin-runtime` · priority: `P3`", - "classification": "`attempt(...)` 는 예외와 미확인을 모두 `false` 로 접는다(`RedriveService:142-156`). 감사 이벤트는 `failed` 개수만 담는다(`:135`). 사건 복구 중에 \"왜 이 메시지들이 안 갔는가\" 를 물을 수 있어야 하는데 답이 없다. `RedriveReport` 에 실패 사유별 집계(코드 → 개수) 정도만 추가해도 크게 달라진다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transport-and-provider-semantics/case/case-messaging-admin-runtime-f10.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-admin-runtime-f10" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-admin-runtime-f10.txt" - ] - }, - { - "title": "배치 metadata를 만들고 넘길 곳이 없다", - "kind": "case", - "slug": "messaging-core-api-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-core-api#L844`" - ], - "owning-module": "`messaging-core-api` · priority: `P2`", - "classification": "`KafkaBatchConsumerRegistrar:104`와 `RabbitBatchConsumerRegistrar:139`가 `BatchDeliveryMetadata`를 만들고, 두 javadoc 다 \"the batch-wide metadata handed to the handler\"라고 적는다. `new BatchMessageDelivery`는 저장소 전체에서 0건이고 `BatchMessageHandler` 참조도 0건이다. 두 registrar는 브로커별로 다른 정확한 계산을 한다 — Kafka는 파티션 단위 커밋이라 `settlableAsBatch=true`, Rabbit은 multiple-ack이 in-flight까지 정산하므로 `false`. 이 판단이 계산되어 어디에도 전달되지 않는다. javadoc은 존재하지 않는 수신자를 가리킨다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transport-and-provider-semantics/case/case-messaging-core-api-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-core-api-f02", - "messaging-core-api-f02-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-core-api-f02.txt" - ] - }, - { - "title": "SQL 실패가 재시도 불가로 분류된다", - "kind": "case", - "slug": "messaging-inbox-jdbc-postgresql-f03", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-inbox-jdbc-postgresql#L666`" - ], - "owning-module": "`messaging-inbox-jdbc-postgresql` · priority: `P2`", - "classification": "`INBOX_RESERVE_FAILED`·`INBOX_QUERY_FAILED`·`INBOX_PURGE_FAILED` 셋 다 `MessagingConfigurationException`이고, 그 예외의 카테고리는 `CONFIGURATION`, `retryable = false`다. `SQLException`의 원인 대부분은 구성 오류가 아니라 **일시적 인프라**다 — 연결 끊김, 데드락, 락 타임아웃, 커넥션 풀 고갈. `FailureCategory`는 \"the stable classification a retry engine, DLQ router, and dashboard all agree on\"이고 `retryable = false`는 재시도 엔진이 즉시 파킹한다는 뜻이다. 같은 leaf의 `INBOX_ACTION_FAILED`는 `TRANSIENT_INFRASTRUCTURE`/`retryable = true`로 정확히 분류된다 — 같은 파일 안에서 기준이 갈린다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transport-and-provider-semantics/case/case-messaging-inbox-jdbc-postgresql-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-inbox-jdbc-postgresql-f03", - "messaging-inbox-jdbc-postgresql-f03-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-inbox-jdbc-postgresql-f03.txt" - ] - }, - { - "title": "천장에 닿아 일시정지된 파티션을 재개하는 경로가 없다", - "kind": "case", - "slug": "messaging-kafka-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-kafka#L265`" - ], - "owning-module": "`messaging-kafka` · priority: `P2`", - "classification": "`pollOnce` 의 파티션 루프는 세 경우에 그 파티션을 멈춘다. 이 세 경로 중 어느 것도 `retries.pauseUntil(...)` 을 부르지 않는다. 그런데 폴 루프가 파티션을 재개하는 곳은 하나뿐이다. `retries` 에 항목을 넣는 곳은 `QueuedSettlement.enqueueRequeue` 하나이고, 그것은 핸들러 실패·타임아웃·명시적 requeue 경로다. 천장·배수·풀 거부 경로는 등록하지 않는다. 따라서 천장 때문에 멈춘 파티션은 **폴 루프가 스스로 재개하지 않는다.** 재개할 수 있는 것은 외부에서 부른 `resume(scope)` 이나 재조정뿐이다. `maxInFlightPerOrderingUnit` 의 기본값은 1 이다(`DestinationSettings.Consumer`). 한 폴이 같은 파티션의 레코드를 둘 이상 돌려주는 순간 두 번째에서 `tryAcquire` 가 거짓이 되고, 그 파티션이 멈춘다. 그 뒤 작업자가 끝나 `coordinator.release` 로 슬롯이 비어도 `consumer` 는 여전히 일시정지 상태다. 같은 파일이 `coordinator.pause(...)` 와 `consumer.pause(...)` 를 구분해서 쓴다 — `applySettlements` 의 `PAUSE_AND_SEEK` 는 둘 다 부르고, 천장 경로는 `consumer` 쪽만 부른다. 그래서 조정자는 그 파티션을 멈춘 것으로 알지 못하고, 결과적으로 `tryAcquire` 는 계속 참을 답하는데 브로커에서 레코드가 오지 않는다. 천장 경로가 `retries.pauseUntil(partition, seekBackTo, Duration.ZERO, now)` 를 등록하면 다음 주기의 `applyDueResumes` 가 즉시 재개한다. 지연이 0 이므로 `dueForResume` 이 곧바로 돌려준다. 배수 경로는 재개하지 않는 것이 맞고, 풀 거부 경로는 천장과 같다. 소비 경로가 조립되지 않으므로(§12.1) P2. 배선하는 순간 …", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transport-and-provider-semantics/case/case-messaging-kafka-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-kafka-f02", - "messaging-kafka-f02-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-kafka-f02.txt" - ] - }, - { - "title": "오염된 재시도 헤더가 격리되지 않고 무한 pause-and-seek 을 만든다", - "kind": "case", - "slug": "messaging-kafka-f03", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-kafka#L301`" - ], - "owning-module": "`messaging-kafka` · priority: `P2`", - "classification": "`KafkaRetryMetadataMapper.attemptOf` 는 읽을 수 없는 `msg.retry.attempt` 에 대해 fail-closed 를 택하고, 그 이유를 정확하게 적는다. 메시지가 \"quarantined\" 라고 말한다. 소비자는 그렇게 하지 않는다. 격리 경로는 **디코딩 실패에만** 걸려 있다. `attemptOf` 는 디코딩이 끝난 뒤 두 번째 블록에서 던지고, `MessagingConfigurationException` 은 `MessagingException` 을 통해 `RuntimeException` 이므로 두 번째 `catch` 가 잡는다. 결과는 `requeueAfterFailure()` → `PAUSE_AND_SEEK` → 같은 오프셋 재읽기 → 같은 헤더 → 같은 예외다. 즉 fail-closed 가 막으려던 것(재시도 예산 무력화)보다 나쁜 것을 만든다 — 그 파티션이 영구히 그 레코드에서 멈춘다. 그리고 javadoc 이 지적한 대로 이 헤더는 호출자가 쓸 수 있는 값이므로, 숫자가 아닌 값 하나로 파티션 하나를 정지시킬 수 있다. `ReservedHeaderForgeryTest.aMalformedRetryAttemptIsQuarantined` 는 `attemptOf` 가 던지는 것만 단언한다. 이름은 \"quarantined\" 인데 격리를 확인하지 않는다. `attemptOf` 호출을 디코딩과 같은 블록으로 옮겨 격리 경로에 태우거나, 두 번째 `catch` 가 예외 종류를 나누게 한다 — `MessagingConfigurationException` 은 재시도로 회복되지 않는 종류이므로 격리 대상이고, 핸들러 실패는 재시도 대상이다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transport-and-provider-semantics/case/case-messaging-kafka-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-kafka-f03", - "messaging-kafka-f03-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-kafka-f03.txt" - ] - }, - { - "title": "시계를 주입받는 클래스가 한 곳에서만 벽시계를 읽는다", - "kind": "case", - "slug": "messaging-kafka-f04", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-kafka#L342`" - ], - "owning-module": "`messaging-kafka` · priority: `P3`", - "classification": "`KafkaConsumerRegistrar` 의 설계 성질이 javadoc 에 적혀 있다. 주기마다 `Instant now` 를 받아 `applyDueResumes(now)` 로 넘긴다. 그런데 그 짝인 등록 쪽은 이렇다. 이 리프에서 `Instant.now()` 를 읽는 유일한 자리다. 그리고 그 호출은 작업자 스레드에서 일어나므로 폴 스레드의 `now` 와 다른 순간이다. 결과는 두 가지다. 지연 재시도(`requeue(Duration)`)의 재개 시점을 고정 시계로 시험할 수 없고, 시험이 `Duration.ZERO` 밖의 지연을 다루지 못한다 — 실제로 어떤 시험도 다루지 않는다. 수정은 생성자에 `Supplier` 를 하나 더 받는 것이다. 같은 저장소의 `MessagingShutdownLifecycle` 이 정확히 그 형태로 두 생성자를 둔다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transport-and-provider-semantics/case/case-messaging-kafka-f04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-kafka-f04", - "messaging-kafka-f04-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-kafka-f04.txt" - ] - }, - { - "title": "결함으로 판정된 메서드가 남아 있고, 실브로커 증명이 그것 위에서 돈다", - "kind": "case", - "slug": "messaging-kafka-f05", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-kafka#L362`" - ], - "owning-module": "`messaging-kafka` · priority: `P3`", - "classification": "`KafkaTransactionalPublisher` 에 같은 일을 하는 메서드가 둘 있다. `inTransaction` 의 javadoc 이 둘째를 결함으로 지목한다. `sendInTransaction` 은 public 이고 production 호출자가 없다. 호출하는 것은 시험 다섯 자리뿐이다 — 그리고 그 다섯이 실브로커 트랜잭션 증명 전부다(`KafkaTransactionIT`·`KafkaTransactionFencingIT`·`KafkaReadCommittedIT`). 고쳐진 `inTransaction` 을 시험하는 것은 `KafkaTransactionOrderingTest` 하나이고 `MockProducer` 다. 즉 실브로커에서 커밋·중단·펜싱이 증명된 것은 옛 모양이고, 새 모양은 목 위에서만 증명됐다. 기능적 차이는 크지 않다(`body` 가 비어 있으면 두 메서드는 같은 호출열을 만든다). 그래도 두 가지가 남는다 — 결함으로 판정된 순서를 만드는 public 진입점이 여전히 열려 있다는 것, 그리고 실브로커 증거가 production 경로가 아닌 것 위에 있다는 것. 수정은 ITs 를 `inTransaction(..., () -> null)` 로 옮기고 `sendInTransaction` 을 지우는 것이다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transport-and-provider-semantics/case/case-messaging-kafka-f05.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-kafka-f05" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-kafka-f05.txt" - ] - }, - { - "title": "\"등록\"이 아무것도 등록하지 않고 성공을 반환한다", - "kind": "case", - "slug": "messaging-kafka-share-experimental-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-kafka-share-experimental#L467`" - ], - "owning-module": "`messaging-kafka-share-experimental` · priority: `P2`", - "classification": "`KafkaShareGroupRegistrar.register(profile, spec)`이 `spec`을 `Objects.requireNonNull`로만 처리하고 버린다. `ShareRegistration`은 `profile`과 `AtomicBoolean` 둘만 갖는다. Kafka 소비자가 만들어지지 않고(`import org.apache.kafka` 0건), `spec.sink`가 저장되지 않으므로 어떤 전달도 일어나지 않는다. 반환된 registration은 `isActive() == true`를 보고한다. 같은 클래스의 javadoc이 pause를 조용히 무시하는 것을 거절한 이유로 \"would let a retry policy that depends on pausing appear to work while doing nothing\"을 든다. `register` 자체가 정확히 그 형태다 — 성공을 반환하고 아무것도 하지 않으며 `isActive()`가 true다. 오늘 호출자가 없으므로 사고는 아니지만, 이 leaf를 배선하는 사람이 가장 먼저 부를 메서드다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transport-and-provider-semantics/case/case-messaging-kafka-share-experimental-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-kafka-share-experimental-f01", - "messaging-kafka-share-experimental-f01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-kafka-share-experimental-f01.txt" - ] - }, - { - "title": "닫힌 전송의 거절이 영구 업무 실패로 분류된다", - "kind": "case", - "slug": "messaging-pulsar-experimental-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-pulsar-experimental#L196`" - ], - "owning-module": "`messaging-pulsar-experimental` · priority: `P3`", - "classification": "두 호출자가 이 메서드를 쓴다. 첫째는 영구 업무 실패가 맞다. 둘째는 아니다. 종료 중이라는 것은 이 세대의 사정이고, 다음 세대나 다른 인스턴스에서는 같은 메시지가 발행된다. 같은 파일의 `classify` 가 분류를 신중히 나눈다 — 사전 거절은 `CONFIGURATION`, 모호는 `TRANSIENT_INFRASTRUCTURE`. 닫힘만 그 규율 밖에 있다. 전송되지 않았다는 증거(`notTransmitted`)는 옳다. 어긋난 것은 범주뿐이다. 수정은 닫힘에 `TRANSIENT_INFRASTRUCTURE` 를 주거나, 두 호출자가 범주를 인자로 받게 하는 것이다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transport-and-provider-semantics/case/case-messaging-pulsar-experimental-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-pulsar-experimental-f01" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-pulsar-experimental-f01.txt" - ] - }, - { - "title": "`NatsJetStreamProfileValidator` 를 호출하는 곳이 저장소에 없다. javadoc 링크 하나가 유일한 흔적이다", - "kind": "case", - "slug": "messaging-nats-experimental-f03", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-nats-experimental#L217`" - ], - "owning-module": "`messaging-nats-experimental` · priority: `P2`", - "classification": "75줄짜리 검증기가 이 어댑터의 시작 시점 판단 넷을 들고 있다 — 실험 스위치가 꺼져 있으면 거부, 최소 한 번 배달에 코어 NATS 거부, 순서 있는 소비자와 경쟁 작업자 동시 사용 거부, 키 순서 목적지 거부. 저장소 전역에서 이 클래스 이름이 나오는 곳은 두 줄뿐이다. 하나는 선언이고 하나는 **javadoc 링크**다. 코드 호출자 0, 테스트 0. `validate` 는 `jetStreamEnabled` · `orderedConsumer` · `competingWorkers` · `enabled` 를 전부 인자로 받는다. 즉 스스로 아무것도 관찰하지 않고, 호출자가 이미 알고 있는 사실을 넘겨 줘야만 판단한다. 그런 호출자가 없으니 이 판단들은 한 번도 실행된 적이 없다. 전송의 클래스 javadoc 이 \"그래서 검증기가 시작 시 그 조합을 거부한다\"고 단언한다. 읽는 사람에게 이 어댑터는 코어 NATS 오설정으로부터 보호되는 것처럼 보이는데, 실제로는 아무 게이트도 걸려 있지 않다. 실험 등급이라 지금 당장 사고가 나지는 않지만, 이 어댑터를 실전에 붙이는 사람이 가장 먼저 신뢰할 문장이 지금 사실이 아니다. 같은 형태를 이 저장소에서 여러 번 봤다 — 채점기는 있는데 그 채점기에 값을 넣어 주는 생산자가 없는 구조(`GrpcRawApiImportRule` · `GrpcApplicationBoundaryRules` · `GrpcNettyParityContract` 등). 이쪽이 더 나쁜 쪽인 이유는 그 리프들에서는 최소한 테스트가 리터럴을 먹여 판단 자체는 실행해 보는데, 여기서는 그것조차 없다는 점이다. 어댑터 조립 지점에서 `validate` 를 부르거나, 그럴 지점이 아직 없다면 최소한 프로파일 생성 시점에 걸리도록 옮긴다(§4 의 압축 생성자가 이미 실행되는 유일한 게이트다). 어느 쪽도 못 하겠다면 전송 javadoc 의 \"refuses ... at startup\" 을 사실에 맞게 고친다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transport-and-provider-semantics/case/case-messaging-nats-experimental-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-nats-experimental-f03", - "messaging-nats-experimental-f03-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-nats-experimental-f03.txt" - ] - }, - { - "title": "재시도 엔진과 DLQ 조정자가 bean으로 만들어지고 주입되는 곳이 없다", - "kind": "case", - "slug": "messaging-policy-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-policy#L781`" - ], - "owning-module": "`messaging-policy` · priority: `P2`", - "classification": "`MessagingCoreAutoConfiguration`이 `RetryDecisionEngine`(:167)과 `DeadLetterOrchestrator`(:179)를 `@Bean @ConditionalOnMissingBean`으로 만든다. 두 타입을 받는 production 코드는 각각 `KafkaRetryExecutor`와 `KafkaDeadLetterPublisher`/`RabbitDeadLetterPublisher`뿐이고, **셋 다 저장소 어디에서도 생성되지 않는다.** 같은 설정의 51개 bean 중 두 타입을 인자로 받는 `@Bean` 메서드가 없다. 컨텍스트에 두 bean이 앉아 있고 `MessagingAutoConfigurationTest`류의 `hasSingleBean` 검사는 통과한다 — 즉 **bean 존재 검사가 배선을 증명하지 않는다.** 그리고 이 leaf가 가장 공들인 두 축(6개 재시도 모드·8단 판단 순서·full jitter·capability 인식, DLQ 발행-후-정산 불변식·예약 헤더 6개)이 실행되지 않는다. 42개 테스트 중 16개가 이 두 축을 검증한다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transport-and-provider-semantics/case/case-messaging-policy-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-policy-f01", - "messaging-policy-f01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-policy-f01.txt" - ] - }, - { - "title": "재시도와 DLQ 각각에 두 개의 구현이 있고 정본이 표시되지 않았다", - "kind": "case", - "slug": "messaging-policy-f03", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-policy#L799`" - ], - "owning-module": "`messaging-policy` · priority: `P3`", - "classification": "재시도: `DefaultRetryDecisionEngine`(6모드·백오프·순서 인식) vs `DefaultDeliveryProcessor`(고정 지연·시도 횟수 미확인). DLQ: `DeadLetterOrchestrator`(예약 헤더 6개 부착) vs `DefaultDeliveryProcessor.DeadLetterPublisher`(헤더 없음). 둘 다 조립되지 않았다. 오늘 경쟁하지 않지만, 소비 경로를 배선하는 사람이 둘 중 하나를 고르게 되고 코드가 어느 쪽이 정본인지 말하지 않는다. 두 javadoc이 각각 자기가 플랫폼 규칙의 구현이라고 서술한다. 그리고 선택 결과가 다르다 — `DefaultDeliveryProcessor` 경로로 DLQ된 메시지에는 실패 카테고리·코드·원본 목적지·시도 횟수가 붙지 않는다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transport-and-provider-semantics/case/case-messaging-policy-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-policy-f03" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-policy-f03.txt" - ] - }, - { - "title": "DLQ 메타데이터의 두 시각이 항상 같다", - "kind": "case", - "slug": "messaging-policy-f04", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-policy#L808`" - ], - "owning-module": "`messaging-policy` · priority: `P3`", - "classification": "`DeadLetterMetadata`가 `firstFailureAt`과 `lastFailureAt`을 별도 필드로 선언하는데, 유일한 생산 지점인 `DeadLetterOrchestrator:89-97`이 둘 다 `delivery.metadata().receivedAt()`으로 채운다. 두 헤더(`msg.first-failure-at`, `msg.last-failure-at`)가 DLQ 메시지에 붙는데 항상 같은 값이다. 운영자가 \"이 메시지가 얼마나 오래 실패해 왔는가\"를 헤더에서 알 수 없다. `ReservedHeaders`가 두 이름을 따로 정의한 목적이 실현되지 않는다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transport-and-provider-semantics/case/case-messaging-policy-f04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-policy-f04" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-policy-f04.txt" - ] - }, - { - "title": "확인 등급이 요구에서 파생되고, 그 요구를 뒷받침하는 강제는 목적지 종류 하나에만 걸린다", - "kind": "case", - "slug": "messaging-rabbit-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-rabbit#L210`" - ], - "owning-module": "`messaging-rabbit` · priority: `P3`", - "classification": "증거의 등급이 브로커가 무엇을 했는지가 아니라 프로파일이 무엇을 **요구했는지** 에서 나온다. 대부분의 경우 이 파생은 성립한다. 두 강제가 그것을 받쳐 준다. `RabbitHeaderMapper.toProperties` 가 배달 모드를 무조건 `PERSISTENT` 로 둔다. RabbitMQ 는 지속 메시지를 디스크에 쓴 뒤에 확인한다. `RabbitProfileValidator.validateDestination` 이 내구 작업 큐에 쿼럼 큐를 요구한다. 쿼럼 큐의 확인은 다수 복제 뒤에 온다. 빈틈은 둘째 강제의 범위다. 작업 큐가 아닌 목적지에는 쿼럼 요구가 없다. 교환기로 발행하는 목적지가 `REPLICATION_OR_PERSISTENCE_ACK` 를 요구하면, 그 교환기에 바인딩된 큐가 고전 큐여도 어댑터는 그 등급을 보고한다. 지속 모드 덕분에 디스크 기록은 보장되지만 복제는 보장되지 않는다. 이 저장소의 규율은 증거가 관측에서 나와야 한다는 것이다 — `MessagingCapabilities` 의 javadoc 이 \"a silently weakened guarantee is indistinguishable from a working one until the incident\" 라고 적는다. 수정은 쿼럼 요구를 목적지 종류가 아니라 **요구된 확인 등급** 에 걸거나, 작업 큐가 아닌 목적지에서는 등급을 `BROKER_ACK` 로 낮추는 것이다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transport-and-provider-semantics/case/case-messaging-rabbit-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-rabbit-f01" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-rabbit-f01.txt" - ] - }, - { - "title": "SCRAM 자격을 RabbitMQ 의 데모 기구로 조용히 매핑한다", - "kind": "case", - "slug": "messaging-rabbit-f03", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-rabbit#L274`" - ], - "owning-module": "`messaging-rabbit` · priority: `P3`", - "classification": "`RABBIT-CR-DEMO` 는 RabbitMQ 의 시연용 challenge-response 인증 기구(`rabbit_auth_mechanism_cr_demo`)의 이름이고 기본 활성이 아니다. RabbitMQ 는 SCRAM-SHA 를 구현하지 않으므로 `SaslScram` 에 대응하는 AMQP 기구가 없다는 것 자체는 사실이다. 문제는 그 사실을 다루는 방식이 같은 파일 안에서 일관되지 않다는 것이다. 그리고 이웃 어댑터의 같은 클래스가 정확히 이 상황에 대한 규범을 적어 두었다. `SaslScram` 에도 그 규범이 적용되어야 한다. 플러그인이 없는 브로커에서는 handshake 가 알아보기 어려운 오류로 실패하고, 있는 브로커에서는 시연용 기구로 인증한다. 수정은 `Nkey` 와 같이 거부하거나, `PLAIN` 으로 매핑하고 그 이유를 주석으로 남기는 것이다. 어느 쪽이든 지금처럼 말없이 데모 기구를 고르는 것보다 낫다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transport-and-provider-semantics/case/case-messaging-rabbit-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-rabbit-f03" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-rabbit-f03.txt" - ] - }, - { - "title": "`pause` 의 의미가 SPI 하나 뒤에서 두 브로커에 다르게 구현된다", - "kind": "case", - "slug": "messaging-rabbit-f05", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-rabbit#L335`" - ], - "owning-module": "`messaging-rabbit` · priority: `P3`", - "classification": "호출자가 이 단계를 기다리고 나면 \"일시정지되었다\" 고 읽는다. 실제로 일어난 것은 `onMessage` 가 이후 배달에 `false` 를 답하기 시작한 것뿐이고, 리스너 컨테이너는 계속 배달을 밀며 그 배달들은 미확인 상태로 재배달된다. 즉 정지가 아니라 거부-재배달 루프다. Kafka 쪽은 같은 SPI 를 정반대로 구현하고 그 이유를 적는다. AMQP 에는 대응하는 수단이 있다 — `basicCancel` 로 소비자를 취소하거나 컨테이너를 멈추는 것. 지금 구현이 그것을 하지 않는 이유는 어디에도 없다. 전용 시험(`aPausedQueueRefusesDeliveriesSoTheBrokerRedeliversThem`)의 이름이 이미 실제 동작을 정확히 말한다. 그러므로 수정은 둘 중 하나다 — 컨테이너를 실제로 멈추거나, SPI 의 javadoc 에 \"브로커에 따라 정지가 거부-재배달일 수 있다\" 를 명시하는 것.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transport-and-provider-semantics/case/case-messaging-rabbit-f05.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-rabbit-f05" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-rabbit-f05.txt" - ] - }, - { - "title": "`Faults` 내부클래스 57줄이 3개 모듈에 바이트 단위로 복제되어 있다", - "kind": "case", - "slug": "messaging-testkit-f03", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-testkit#L960`" - ], - "owning-module": "`messaging-testkit` · priority: `P3`", - "classification": "`sha256` 이 세 곳 모두 `3028b459…` 로 동일하다(`EVD-299`). 총 171줄. `messaging-testkit/src/main` 에 `DefaultFaultController` (또는 `RecordingFaultController`) 하나를 두고 세 하니스가 그것을 필드로 갖게 하면 된다. `MessagingAdapterHarness.faults()` 의 반환 타입은 `FaultController` 그대로이므로 외부 API 변경이 없다. 이 복제가 위험한 이유는 P2 와 겹친다: `rejectPublish` 의 전송 증거를 고치려면 지금은 세 파일을 고쳐야 하고, 세 파일이 어긋나면 어댑터마다 다른 결함 의미를 갖게 된다 — 이 리프가 존재하는 이유 자체를 무너뜨린다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transport-and-provider-semantics/case/case-messaging-testkit-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-testkit-f03" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-testkit-f03.txt" - ] - } - ], - "concept": [], - "reference": [ - { - "title": "에러 메시지가 지시하는 설정은 그 설정을 읽는 코드와 함께 존재해야 한다", - "kind": "reference", - "slug": "messaging-kafka-share-experimental-f06", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-kafka-share-experimental#L512`" - ], - "owning-module": "`messaging-kafka-share-experimental`", - "rule": "에러 메시지가 지시하는 설정은 그 설정을 읽는 코드와 함께 존재해야 한다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 활성화 프로퍼티 키가 에러 메시지에만 존재한다", - "scope": [ - "메시징 어댑터의 소비·발행 경로. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "transport-and-provider-semantics/reference/reference-messaging-kafka-share-experimental-f06.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "안정 코드는 운영자의 행동이 갈리는 지점마다 나눈다", - "kind": "reference", - "slug": "messaging-runtime-core-f04", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-runtime-core#L736`" - ], - "owning-module": "`messaging-runtime-core`", - "rule": "안정 코드는 운영자의 행동이 갈리는 지점마다 나눈다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 같은 실패 코드가 두 completion에 쓰인다", - "scope": [ - "메시징 어댑터의 소비·발행 경로. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "transport-and-provider-semantics/reference/reference-messaging-runtime-core-f04.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "실패 코드는 운영자의 다음 행동이 갈리는 지점마다 나눈다", - "kind": "reference", - "slug": "messaging-schema-json-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-schema-json#L472`" - ], - "owning-module": "`messaging-schema-json`", - "rule": "실패 코드는 운영자의 다음 행동이 갈리는 지점마다 나눈다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 파서 방어 여섯 갈래가 하나의 실패 코드로 접힌다", - "scope": [ - "스키마와 코덱 계열. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "transport-and-provider-semantics/reference/reference-messaging-schema-json-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "멱등 종료를 보장하는 컴포넌트는 종료 이후의 등록도 정의한다", - "kind": "reference", - "slug": "messaging-transport-spi-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-transport-spi#L668`" - ], - "owning-module": "`messaging-transport-spi`", - "rule": "멱등 종료를 보장하는 컴포넌트는 종료 이후의 등록도 정의한다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 종료 중 `install`이 닫히지 않는 창", - "scope": [ - "메시징 어댑터의 소비·발행 경로. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "transport-and-provider-semantics/reference/reference-messaging-transport-spi-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "같은 개념의 sentinel은 계층을 넘어 하나로 정한다", - "kind": "reference", - "slug": "messaging-transport-spi-f03", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-transport-spi#L677`" - ], - "owning-module": "`messaging-transport-spi`", - "rule": "같은 개념의 sentinel은 계층을 넘어 하나로 정한다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — pause scope sentinel이 두 인터페이스에서 다르다", - "scope": [ - "메시징 어댑터의 소비·발행 경로. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "transport-and-provider-semantics/reference/reference-messaging-transport-spi-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - } - ], - "question": [ - { - "title": "브로커 홉 추적기가 소비자를 갖지 않는다", - "kind": "question", - "slug": "messaging-observability-f03", - "readiness": "OPEN", - "source": [ - "`final/document.md#a19-messaging-observability#L690`" - ], - "owning-module": "`messaging-observability` · priority: `P3`", - "classification": "`MessagingTracer`의 leaf 밖 참조 0. 이 클래스가 존재하는 이유는 \"the only way the two spans meet is if the context travels in the message headers\"다. `messaging-core-api`의 `TraceContext`가 봉투 필드로 있고(그쪽 §4.11), 어댑터가 헤더를 매핑한다. 그런데 `traceparent`/`tracestate`/`baggage`를 헤더로 옮기는 **명시된 수단**을 아무도 쓰지 않는다. 어댑터가 각자 하고 있다면 `MessageHeaders.platform` 사용 여부와 빈 추적 처리가 어댑터마다 다를 수 있다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transport-and-provider-semantics/question/openquestion-messaging-observability-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "브리지의 바인더 쪽 절반이 없다", - "kind": "question", - "slug": "messaging-spring-cloud-stream-bridge-f02", - "readiness": "OPEN", - "source": [ - "`final/document.md#a19-messaging-spring-cloud-stream-bridge#L561`" - ], - "owning-module": "`messaging-spring-cloud-stream-bridge` · priority: `P3`", - "classification": "`ChannelSend`·`BridgedHandler` 두 함수형 인터페이스가 바인더 접촉면이고, 그 구현이 저장소에 없다. `MessagingBindingBridge` javadoc은 \"a service already has Stream bindings and needs to reach the same destinations without a rewrite\"를 존재 이유로 든다. 정책·검증·정직성 세 층이 완성돼 있고 그것들을 실제 바인딩에 연결하는 코드가 없다. `runtime_memberships: []`와 정합하므로 오늘의 결함은 아니지만, 이 leaf의 이름이 약속하는 것(\"spring-cloud-stream-bridge\")이 절반만 존재한다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transport-and-provider-semantics/question/openquestion-messaging-spring-cloud-stream-bridge-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - } - ], - "decision": [] - } - }, - "security-policy-enforcement": { - "topic": "security-policy-enforcement", - "title": "보안 정책 강제 — 감사·PII·권한·SSRF 규칙이 실제 요청에 닿는가", - "readerQuestion": "", - "kinds": { - "case": [ - { - "title": "property-access `IDENTITY` entity가 batch guard를 우회한다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "\"모든 reveal은 감사된다\"고 선언한 `AccessContext`를 읽는 코드가 저장소에 하나도 없다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`publicPaths`가 `RestrictedPathRule`보다 먼저 등록되어, 넓은 공개 경로 하나가 관리 평면 규칙을 조용히 덮는다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "연결 티켓·origin 정책·메시지 권한·연결 예산이 요청 경로 밖이고, 그중 일부는 STOMP 어댑터가 다른 방식으로 대체한다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "마스킹이 IPv4 만 알아서 검사가 나머지 주소 형태를 전부 통과시킨다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "비밀 필드 검사가 스냅숏의 네 구획 중 하나에만 적용된다", - "kind": "case", - "slug": "grpc-admin-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-admin#L162`" - ], - "owning-module": "`grpc-admin` · priority: `P3`", - "classification": "메시지는 \"a platform snapshot must not carry …\" 로 스냅숏 전체를 말한다. 검사 대상은 `channelProfileHashes` 하나다. 같은 채널 이름 공간을 쓰는 두 맵이 더 있다 — `resolverAndLoadBalancerByChannel`, `retryOwnerByChannel`. 그리고 `registeredServices` 목록과 `serviceHealth` 맵이 있다. 어느 것도 검사되지 않는다. 세 맵의 키 집합이 같아야 한다는 요구가 없으므로, 어떤 채널이 나머지 두 맵에만 있으면 그 이름은 검사를 지나지 않는다. `grpc-advanced-diagnostics` 의 스냅숏은 같은 형태의 자기 검사를 두 구획(주소 목록, 자원 판본 키)에 적용한다. 두 리프의 규율이 갈린다. 수정은 네 구획 전부를 같은 검사에 넣는 것이다. 값이 아니라 키를 보는 검사이므로 비용이 낮다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-policy-enforcement/case/case-grpc-admin-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-admin-f02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-admin-f02.txt" - ] - }, - { - "title": "마스킹이 IPv4 만 알고, 그 결과 \"마스킹되지 않은 주소\" 검사가 나머지 형태를 전부 통과시킨다", - "kind": "case", - "slug": "grpc-advanced-diagnostics-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-advanced-diagnostics#L167`" - ], - "owning-module": "`grpc-advanced-diagnostics` · priority: `P2`", - "classification": "IPv4 가 아닌 주소는 패턴에 맞지 않아 **입력 그대로 반환된다.** 그리고 스냅숏 생성자의 검사는 이렇게 되어 있다. 마스킹 결과가 입력과 같으면 이미 마스킹된 것으로 판정한다. 그러므로 IPv4 가 아닌 주소는 전부 이 검사를 통과한다. 세 번째와 네 번째가 문제다. 이 플랫폼이 겨냥하는 배포 형태가 쿠버네티스이고(`grpc-discovery` 전체가 그 주제다), 헤드리스 레코드의 엔드포인트는 파드 DNS 이름이며 이중 스택 클러스터에서는 IPv6 주소다. 편집기가 막으려 한 것이 정확히 그것이다 — \"a diagnostics endpoint that publishes peer addresses publishes every tenant's connection.\" `unix` 소켓 경로도 통과한다. 그것은 호스트 파일 시스템 경로다. 테스트의 주소 리터럴이 전부 IPv4 다 — `10.4.13.201:9090` · `10.9.13.201` · `10.4.x.x` · `10.5.x.x`. IPv6 도 호스트 이름도 없다. 마스킹을 형태별로 나눈다. IPv6 는 앞 두 그룹만 남기고 나머지를 `:x:x` 로, 호스트 이름은 최상위 라벨 몇 개만 남기고, 그 밖의 형태는 `unknown` 으로 접는다. 그리고 검사를 \"결과가 입력과 같으면 통과\" 가 아니라 \"알려진 마스킹 형태와 일치해야 통과\" 로 뒤집는다. 지금 형태는 마스킹이 모르는 입력을 전부 안전하다고 판정한다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-policy-enforcement/case/case-grpc-advanced-diagnostics-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-advanced-diagnostics-f01", - "grpc-advanced-diagnostics-f01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-advanced-diagnostics-f01.txt" - ] - }, - { - "title": "금지 필드 검사가 키에만 적용되고 값에는 적용되지 않는다", - "kind": "case", - "slug": "grpc-advanced-diagnostics-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-advanced-diagnostics#L206`" - ], - "owning-module": "`grpc-advanced-diagnostics` · priority: `P3`", - "classification": "`redact(...)` 도 같다 — 금지 이름의 키를 버리고, 남은 값은 주소 필드일 때만 마스킹한다. 값 자체가 자격증명 형태인지는 보지 않는다. `grpc-observability` 의 태그 정책은 값도 본다(UUID·`sha256:`·`bearer ` 패턴). 같은 저장소의 두 관측 편집기가 값 검사에서 갈린다. xDS 자원 버전은 보통 짧은 숫자나 해시라 도달성이 낮다. 기록하는 이유는 두 편집기의 규율이 다르다는 점이다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-policy-enforcement/case/case-grpc-advanced-diagnostics-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-advanced-diagnostics-f02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-advanced-diagnostics-f02.txt" - ] - }, - { - "title": "정책의 자바독이 하지 않는 거부를 한다고 적고, 승격 승인이 두 곳에 따로 있다", - "kind": "case", - "slug": "grpc-advanced-edition-f03", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-advanced-edition#L216`" - ], - "owning-module": "`grpc-advanced-edition` · priority: `P3`", - "classification": "**첫째, 서술과 코드가 어긋난다.** \"refuses an approval nobody recorded\" 에 해당하는 검사가 없다. `promotionApproved` 는 읽히지도 검증되지도 않고 그대로 저장된다. `new GrpcEdition2024Policy(Set.of(), Set.of(), true)` — 옵트인한 모듈도 공개 서비스도 없는데 승인만 참인 정책 — 이 아무 저항 없이 만들어지고, `serviceMayMove` 는 모든 서비스에 참을 답한다. **둘째, 같은 사실이 두 곳에 따로 있다.** 게이트는 정책을 인자로 받지도, 참조하지도 않는다. 그래서 \"ADR 이 없다\"고 판정한 게이트와 \"승인되었다\"고 답하는 정책이 동시에 성립할 수 있고, 둘을 맞추는 코드가 없다. §17.2 가 지적한 \"두 게이트가 서로를 부르지 않는다\" 와 같은 구조가 정책과 게이트 사이에도 있다. 정책도 게이트도 production 호출자가 없고(§12.1), 승격은 사람이 수행하는 절차다. 다만 이 리프가 존재하는 이유가 \"그 절차를 코드로 적어 두는 것\" 이므로, 적힌 절차 안에서 같은 사실이 둘로 갈라져 있는 것은 그 목적에 어긋난다. `promotionBlockers` 가 `GrpcEdition2024Policy` 를 받아 `promotionApproved` 를 `promotionAdr` 자리에 쓰고, 정책 생성자가 자바독대로 \"승인이 참이면 그 근거(공개 서비스 집합이 비어 있지 않을 것 등)\"를 요구한다. 어느 쪽도 하지 않겠다면 자바독의 그 문장을 지운다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-policy-enforcement/case/case-grpc-advanced-edition-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-advanced-edition-f03" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-advanced-edition-f03.txt" - ] - }, - { - "title": "`RESOURCE_EXHAUSTED` 매핑이 그 상태의 두 출처 중 하나만 가정한다", - "kind": "case", - "slug": "grpc-core-api-f03", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-core-api#L180`" - ], - "owning-module": "`grpc-core-api` · priority: `P3`", - "classification": "이 분기는 전송이 미시작을 증명하지 못한 뒤에 도달한다. 즉 \"보냈는지 모르지만 이 상태 코드는 거절을 뜻한다\" 는 판정이다. 목록의 나머지 일곱은 서버가 일을 시작하기 전에 답하는 상태다. `RESOURCE_EXHAUSTED` 는 두 출처를 갖는다. 이 플랫폼 자신의 승인 제어기가 부하를 흘려보낼 때 — 일을 쓰기 전이므로 거절이 맞다. 원격 서버가 작업 중 자원(할당량·디스크)을 소진했을 때 — 부분 커밋이 있을 수 있다. 이 클래스의 원칙은 보수적이다. 자바독이 두 기본값(`DEADLINE_EXCEEDED`·`UNAVAILABLE` 를 모호로)을 계획의 전역 제약이라 부르고, 그 이유는 \"보냈는지 모르면 모호\" 다. `RESOURCE_EXHAUSTED` 는 그 원칙에서 벗어난 유일한 항목이다. `ABORTED` 가 모호에 있는 것과 대비된다 — 트랜잭션 충돌은 서버가 일을 시작한 뒤의 상태이고, 그래서 모호다. 수정은 둘 중 하나다. `RESOURCE_EXHAUSTED` 를 모호로 옮기거나, 그 상태를 이 플랫폼이 발행한 것과 원격이 발행한 것으로 구분해 전자만 거절로 두는 것이다. 후자는 증거 축에 발신자 정보를 요구하므로 전자가 현실적이다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-policy-enforcement/case/case-grpc-core-api-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-core-api-f03" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-core-api-f03.txt" - ] - }, - { - "title": "스트림 승인의 경계가 동시성 아래에서 새고, caller별 맵이 줄지 않는다", - "kind": "case", - "slug": "grpc-policy-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-policy#L184`" - ], - "owning-module": "`grpc-policy` · priority: `P2`", - "classification": "읽고 비교한 뒤 별도로 증가한다. 경계에 있는 N 개 스레드가 모두 통과한다. 이 클래스의 javadoc 이 서술하는 실패 상황이 곧 고동시성이다 — \"a client that reconnects on every error opens streams faster than the old ones close.\" 재접속 폭풍에서 경계가 가장 많이 샌다. `release` 도 같은 형태라 음수로 갈 수 있다. 그리고 `perCaller` 에서 항목이 제거되지 않는다. `computeIfAbsent` 가 호출자 지문마다 계수기를 만들고 `release` 는 값만 줄인다. 서로 다른 호출자 수만큼 맵이 자란다 — `grpc-observability` 의 `GrpcMetricCardinalityPolicy` 가 지표 태그에 대해 명시적으로 막는 것과 같은 종류의 증가이고, 여기에는 그 가드가 없다. 정본이 같은 리프에 있다 — `GrpcRetryBudget.tryConsume` 의 비교 후 교체 루프.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-policy-enforcement/case/case-grpc-policy-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-policy-f01", - "grpc-policy-f01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-policy-f01.txt" - ] - }, - { - "title": "자격증명 회전이 비교 후 교체가 아니고, 배수 완료가 진행 중인 회전을 되돌릴 수 있다", - "kind": "case", - "slug": "grpc-policy-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-policy#L204`" - ], - "owning-module": "`grpc-policy` · priority: `P2`", - "classification": "`AtomicReference` 를 쓰면서 두 메서드 모두 읽고 나서 조건 없이 쓴다. 두 회전이 같은 `observed` 를 읽으면 둘 다 승계 검사를 통과할 수 있고, 나중 `set` 이 앞의 것을 덮는다. 덮인 회전이 배수 대상으로 기록해 둔 세대가 상태에서 사라진다. 그 세대 위의 호출은 아무도 배수하지 않는다. javadoc 이 이 상황을 이미 알고 있다 — 승계 검사의 존재 이유로 \"the usual reason for one is two rotators racing\" 를 든다. 검사는 있고 원자성이 없다. `completeDrain()` 이 자기가 읽은 `observed.current()` 로 새 상태를 만든다. 읽기와 쓰기 사이에 회전이 일어나면, 그 회전이 활성화한 세대가 지워지고 **이전 세대가 다시 현재가 된다.** 즉 방금 교체된 자격증명이 되살아난다. 클래스의 존재 이유가 \"in-flight 작업을 떨어뜨리지 않고 자격 자재를 교체하는 것\" 인데, 이 경로는 교체 자체를 되돌린다. 수정은 두 메서드를 비교 후 교체로 바꾸는 것이다. `rotate` 는 `compareAndSet(observed, next)` 가 실패하면 다시 읽어 판정하고, `completeDrain` 은 `updateAndGet(s -> new State(s.current(), null, null))` 로 현재 값을 원자적으로 읽어 쓰면 된다. 후자는 한 줄이다. 같은 형태가 `grpc-client` 의 `GrpcChannelRuntimeRegistry.rotate` 에도 있다. 두 리프가 같은 자료구조를 같은 방식으로 잘못 쓴다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-policy-enforcement/case/case-grpc-policy-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-policy-f02", - "grpc-policy-f02-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-policy-f02.txt" - ] - }, - { - "title": "승인 제어기의 세 메서드가 원자적이지 않고, 큐 계수기를 되돌리는 경로가 없다", - "kind": "case", - "slug": "grpc-server-f04", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-server#L211`" - ], - "owning-module": "`grpc-server` · priority: `P2`", - "classification": "이 리프가 SSOT 이므로 여기에 적는다. `grpc-policy` §17.1 이 이 클래스를 대조군으로 지목하는데, 지목된 쪽 문서에 판정이 없었다. **첫째, 읽고 나서 따로 증가시킨다.** 경계에 있는 N 개 스레드가 모두 통과한다. `AtomicInteger` 를 쓰면서 비교와 증가를 나눈 형태이고, 같은 가족의 정본이 `GrpcRetryBudget.tryConsume` 의 비교 후 교체 루프다. `release()`·`promoteFromQueue()` 도 같다 — `get() > 0` 을 확인한 뒤 별도로 감소시키므로, 두 스레드가 같은 마지막 하나를 보고 둘 다 감소시켜 음수가 될 수 있다. 클래스가 `Math.max(0, …)` 같은 하한도 두지 않는다. **둘째, 큐 계수기를 되돌리는 경로가 없다.** 큐에 들어간 호출도 `admitted=true` 를 받는다. 그런데 그 경로는 `queued` 만 올리고 `inFlight` 는 올리지 않는다. 그리고 끝난 호출을 반납하는 메서드는 하나뿐이다. 따라서 호출자가 `promoteFromQueue()` 를 정확히 한 번 끼워 넣지 않으면 계수기가 어긋난다 — 큐에서 실행된 호출이 끝나면 `queued` 는 그대로이고 `inFlight` 만 줄어든다. `releaseQueued()` 같은 메서드도, 그 짝짓기를 요구하는 서술도 없다. 두 시험 모두 단일 스레드이고, `releaseAndPromotionTrackCapacity` 는 `release()` 와 `promoteFromQueue()` 를 **짝지어** 부른다. 짝짓지 않는 경로는 시험되지 않는다. 오늘 호출자가 없으므로(§12.1) P2. 승인 단계를 배선하는 순간 P1 이다 — 부하 아래에서 경계가 새는 것과, 큐 계수기가 단조 증가해 `at capacity` 가 영구히 참이 되는 것이 함께 온다. 세 메서드를 비교 후 교체 루프로 바꾸고, 큐 경로에 대응하는 반납 메서드를 두거나 `promoteFromQueue` 를 `release` 안으로…", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-policy-enforcement/case/case-grpc-server-f04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-server-f04", - "grpc-server-f04-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-server-f04.txt" - ] - }, - { - "title": "던져 버릴 비밀번호를 만들어 놓고 외부 프로세스의 명령줄에 싣는다", - "kind": "case", - "slug": "grpc-testkit-f06", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-testkit#L262`" - ], - "owning-module": "`grpc-testkit` · priority: `P3`", - "classification": "`GrpcTlsTestMaterial` 이 상수 비밀번호를 피하는 이유를 세 줄로 적는다. 그리고 같은 클래스가 그 값을 `keytool` 인자로 넘긴다. 프로세스 명령줄은 같은 호스트의 다른 사용자가 `ps` 나 `/proc//cmdline` 로 읽을 수 있다. 소스 리터럴보다 관측 가능성이 오히려 높다. 영향은 작다 — 값이 매번 새로 만들어지고, 키스토어는 임시 디렉터리에 있으며 `close()` 가 지운다. 기록하는 이유는 이 클래스가 정확히 그 위험 계층을 스스로 논증했다는 점이다. 완화와 노출이 같은 메서드 안에 있다. `keytool` 은 `-storepass:file` 과 `-keypass:file` 을 받는다. 임시 파일 하나면 명령줄에서 값이 사라진다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-policy-enforcement/case/case-grpc-testkit-f06.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-testkit-f06" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-testkit-f06.txt" - ] - }, - { - "title": "계획 다이제스트가 승인 정규 형식과 다른 인코딩을 쓴다", - "kind": "case", - "slug": "messaging-admin-api-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-admin-api#L944`" - ], - "owning-module": "`messaging-admin-api` · priority: `P3`", - "classification": "`ApprovalGrant.canonicalForm()` 은 길이 접두를, `ReplayPlan.digest()`/`RedrivePlan.digest()` 는 `String.join(\"|\", …)` 를 쓴다(§12.3(b), `EVD-304`). 현재는 충돌을 만들 수 없다 — 자유 형식 필드가 `topologyVersion` 하나뿐이기 때문이다. 그러나 그 조건은 코드 어디에도 적혀 있지 않고, 필드가 하나 추가되면 조용히 깨진다. `ApprovalGrant` 의 `appendField` 를 `PlanDigest` 쪽으로 옮겨 재사용하는 편이 낫다 — 규칙과 그 근거가 이미 같은 리프에 있다. 부수적으로 `topologyVersion` 에 형식 제약을 주는 것도 검토할 만하다. 지금은 `isBlank()` 만 본다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-policy-enforcement/case/case-messaging-admin-api-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-admin-api-f02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-admin-api-f02.txt" - ] - }, - { - "title": "`VerifiedApproval` 의 위조 방지가 package-private 에만 의존한다", - "kind": "case", - "slug": "messaging-admin-api-f05", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-admin-api#L962`" - ], - "owning-module": "`messaging-admin-api` · priority: `P3`", - "classification": "이 저장소는 JPMS 를 쓰지 않는다(`module-info.java` 0개). 따라서 어떤 모듈이든 `package dev.caskeleton.messaging.admin;` 을 선언하면 `VerifiedApproval.of(grant)` 를 호출할 수 있다. 현재 그런 파일은 없지만, 이 타입의 존재 이유가 \"아무도 만들 수 없다\" 이므로 그 조건을 자동으로 지키는 검사가 있어야 한다. `ApprovalForgeryTest.aVerifiedApprovalCannotBeConstructedOutsideTheVerifier` 가 있으나, 그것은 같은 패키지 안에서 API 표면을 확인하는 테스트지 다른 모듈의 패키지 선언을 막지 못한다. ArchUnit 규칙 한 줄 — \"`dev.caskeleton.messaging.admin` 패키지는 `messaging-admin-api` 소스 경로에만 존재한다\" — 이면 된다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-policy-enforcement/case/case-messaging-admin-api-f05.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-admin-api-f05" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-admin-api-f05.txt" - ] - }, - { - "title": "같은 인가 실패 코드가 세 파일에 문자열 리터럴로 흩어져 있다", - "kind": "case", - "slug": "messaging-admin-api-f07", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-admin-api#L972`" - ], - "owning-module": "`messaging-admin-api` · priority: `P3`", - "classification": "`APPROVAL_OPERATION_MISMATCH`, `APPROVAL_SOURCE_MISMATCH`, `APPROVAL_PLAN_MISMATCH`, `APPROVAL_EXPIRED` 가 `HmacApprovalVerifier`, `DestructiveOperationGuard`, `ApprovedReplayPlan`, `ApprovedRedrivePlan` 에 각각 리터럴로 존재하며 메시지 문구가 서로 다르다. 검사의 3중화 자체는 의도된 심층 방어지만(§12.2 대조군 2), 코드 문자열은 상수 하나로 모으는 편이 집계와 검색에 낫다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-policy-enforcement/case/case-messaging-admin-api-f07.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-admin-api-f07" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-admin-api-f07.txt" - ] - }, - { - "title": "파괴적 작업의 승인만 위조 가능한 형태로 남아 있다", - "kind": "case", - "slug": "messaging-admin-runtime-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-admin-runtime#L883`" - ], - "owning-module": "`messaging-admin-runtime` · priority: `P2`", - "classification": "생성자는 null·음수만 본다. `approval` 이 이 `operation` 을 인가하는지, 이 `destination` 을 인가하는지, `estimatedMessagesAffected` 가 승인 상한 이하인지 — 아무것도 검사하지 않는다. 계획 다이제스트 필드 자체가 없다. 이 형태가 정확히 `messaging-admin-api` 가 고쳤다고 기록한 것이다. 수정은 `REPLAY`·`REDRIVE`(복구 가능한 작업)에 적용되었고, `PURGE`·`DELETE_DESTINATION`·`OFFSET_RESET`(복구 불가능한 작업)에는 적용되지 않았다. 현재 구현체가 0건이라 실행되는 결함은 아니다(`EVD-307`). 그러나 이 인터페이스는 운영자 도구가 구현하라고 존재하는 것이고, 그 도구가 생기는 순간의 모양이 이것이다. `Approved` 를 `ApprovedReplayPlan` 과 같은 형태로 — `VerifiedApproval` + 생성자 검사 — 바꾸는 것이 맞다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-policy-enforcement/case/case-messaging-admin-runtime-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-admin-runtime-f01", - "messaging-admin-runtime-f01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-admin-runtime-f01.txt" - ] - }, - { - "title": "감사 싱크가 중복 선언되어 있고 레닥션 계약이 유실된다", - "kind": "case", - "slug": "messaging-admin-runtime-f05", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-admin-runtime#L930`" - ], - "owning-module": "`messaging-admin-runtime` · priority: `P3`", - "classification": "`RedriveService.AuditSink` 는 `MessagingAuditSink` 와 시그니처가 같다. admin-runtime 은 이미 `messaging-observability` 를 의존한다. 표준 싱크를 쓰면 세 가지가 함께 해결된다: `ReplayService` 가 형제의 중첩 타입에 의존하는 것, `InMemory` 구현 재작성, 그리고 무엇보다 **\"모든 기록이 `MessagingRedactor` 를 통과했다\" 는 계약**. 현재 `RedriveService:125-136` 은 목적지 이름과 details 를 그대로 넣는다. 목적지 이름은 `DestinationName` 이라 형식이 제한되어 있어 지금은 문제가 아니지만, 계약이 없는 자리에 값이 늘어나는 것을 막을 것이 없다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-policy-enforcement/case/case-messaging-admin-runtime-f05.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-admin-runtime-f05" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-admin-runtime-f05.txt" - ] - }, - { - "title": "격리 리플레이의 guard 우회가 `dryRun` 파라미터로 표현된다", - "kind": "case", - "slug": "messaging-admin-runtime-f08", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-admin-runtime#L948`" - ], - "owning-module": "`messaging-admin-runtime` · priority: `P3`", - "classification": "판단 자체는 근거가 있다. 다만 \"승인이 필요 없다\" 와 \"실제로는 아무것도 하지 않는다\" 가 guard 입장에서 구별되지 않는다. `DestructiveOperationGuard` 에 `skipAuthorization` 성격의 별도 경로를 두거나, 격리 리플레이는 애초에 guard 를 거치지 않는 편이 의도를 드러낸다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-policy-enforcement/case/case-messaging-admin-runtime-f08.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-admin-runtime-f08" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-admin-runtime-f08.txt" - ] - }, - { - "title": "자격증명 판정이 core-api보다 약하다", - "kind": "case", - "slug": "messaging-observability-f05", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-observability#L708`" - ], - "owning-module": "`messaging-observability` · priority: `P3`", - "classification": "`MessagingRedactor.isDenied`는 27키 **정확 일치**다. `messaging-core-api`의 `MessageHeaders.carriesACredential`은 세그먼트 매칭 + 인접 결합으로 `x-api-key`·`auth-token`·`db_password`를 잡는다. redactor의 denylist에 `api_key`·`apikey`는 있으나 `x-api-key`는 없다. 두 표면이 다르지만 **더 자유로운 입력을 받는 쪽이 더 약하다.** 이 leaf 자신이 진단 맵을 \"the one place where a caller can pass arbitrary keys\"라고 부른다. 그리고 `recordDiagnostics`가 redaction을 첫 단계로 두는 이유가 바로 그것이다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-policy-enforcement/case/case-messaging-observability-f05.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-observability-f05" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-observability-f05.txt" - ] - }, - { - "title": "같은 TLS posture를 두 클래스가 다른 엄격도로 검사한다", - "kind": "case", - "slug": "messaging-security-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-security#L634`" - ], - "owning-module": "`messaging-security` · priority: `P2`", - "classification": "`MessageSecurityValidator`는 hostname 검증을 `production && !hostnameVerification`일 때만 요구하고, `BrokerTlsPolicy`는 `tlsEnabled && !hostnameVerification`일 때 요구한다. 전자는 코드 없는 `IllegalArgumentException`, 후자는 안정 코드가 붙은 `MessagingConfigurationException`을 던진다. 둘 다 같은 `BrokerSecurityProfile`을 받고, 후자만 어댑터에서 실제로 호출된다. 비운영에서 TLS를 켜고 hostname 검증을 끈 구성을 두 검사가 다르게 판정한다. 그리고 이 leaf 자신의 javadoc이 그 구성을 \"looks encrypted in every dashboard while accepting any certificate a man in the middle presents\"라고 부른다 — 즉 더 느슨한 쪽이 그 위험을 통과시킨다. 실패 형태도 달라서 운영자가 두 어휘를 알아야 한다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-policy-enforcement/case/case-messaging-security-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-security-f01", - "messaging-security-f01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-security-f01.txt" - ] - }, - { - "title": "권한 거부가 `AUTHORIZATION`이 아니라 `CONFIGURATION`으로 기록된다", - "kind": "case", - "slug": "messaging-security-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-security#L643`" - ], - "owning-module": "`messaging-security` · priority: `P2`", - "classification": "`DestinationAccessValidator.requirePublish`는 `MessageAuthorizationException(\"DESTINATION_PUBLISH_DENIED\")`을 던지고 그 카테고리는 `AUTHORIZATION`이다. 소비자가 0이다. 실제 발행 경로는 `access.mayPublish`를 직접 묻고 `rejected(\"PUBLISH_FORBIDDEN\", ...)`을 반환하는데, `rejected(...)`는 `FailureCategory.CONFIGURATION`을 붙인다. `FailureCategory`는 \"stable classification a retry engine, DLQ router, and dashboard all agree on\"이다(`messaging-core-api` §4.12). 권한 거부가 구성 오류로 분류되면 보안 대시보드가 그것을 보지 못하고, 구성 오류 알림이 권한 거부로 오염된다. 그리고 `AUTHORIZATION` 카테고리를 쓰는 유일한 코드가 미사용 클래스에 있다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-policy-enforcement/case/case-messaging-security-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-security-f02", - "messaging-security-f02-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-security-f02.txt" - ] - } - ], - "concept": [], - "reference": [ - { - "title": "맵 갱신 함수 안에서 I/O를 하면 그 지연이 락 범위가 된다", - "kind": "reference", - "slug": "messaging-security-f06", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-security#L679`" - ], - "owning-module": "`messaging-security`", - "rule": "맵 갱신 함수 안에서 I/O를 하면 그 지연이 락 범위가 된다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 자격증명 해석이 맵 bin 락 안에서 외부 I/O를 한다", - "scope": [ - "보안·승인 표면. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "security-policy-enforcement/reference/reference-messaging-security-f06.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - } - ], - "question": [ - { - "title": "permission component grammar", - "kind": "question", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "ACL 매니페스트 전체가 쓰이지 않는다", - "kind": "question", - "slug": "messaging-security-f03", - "readiness": "OPEN", - "source": [ - "`final/document.md#a19-messaging-security#L652`" - ], - "owning-module": "`messaging-security` · priority: `P3`", - "classification": "`BrokerAclManifest`의 세 메서드(`requireApplicationRuntime`, `undeclared`, `missing`)와 두 enum이 소비자 0이다. javadoc은 \"The manifest is what the platform checks itself against at startup\"이라고 한다. \"애플리케이션 런타임은 파괴적 권한을 갖지 않는다\"는 이 leaf의 핵심 원칙 중 하나이고, `MessageSecurityValidator`가 admin **자격증명**의 부재만 검사한다. 브로커가 producer 자격증명에 `DELETE`를 준 경우는 아무도 보지 않는다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-policy-enforcement/question/openquestion-messaging-security-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - } - ], - "decision": [] - } - }, - "declaration-and-document-drift": { - "topic": "declaration-and-document-drift", - "title": "선언과 구현 드리프트 — 문서·매트릭스·설정 이름이 현재 코드와 같은가", - "readerQuestion": "", - "kinds": { - "case": [ - { - "title": "support README가 current architecture registry/history와 drift", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`RetryDecision.reason`의 “bounded” 설명과 constructor contract 불일치", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "폐기된 namespace guard의 탐색 domain이 operator가 읽는 두 문서를 덮지 않는다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "예외 계층의 \"cause를 붙이지 않는다\" 규칙에 문서화되지 않은 예외가 하나 있다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "CLAUDE.md의 의존성 서술이 세 항목 모두 틀렸다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "README의 세 가지 사실 오류", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "CLAUDE.md가 대는 두 가드 중 하나는 저장소에 없다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "그러나 R0 경계가 문서에만 있고 compile 경로에서 닫히지 않는다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "README의 `jackson-databind` 부재 주장이 현재 상태와 어긋난다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "능력 프로퍼티 이름을 만드는 코드와 실제 게이트가 다른 접두사를 쓴다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "지원 매트릭스가 \"모든 messaging leaf는 build-only\"라고 적고, 가족 권위 문서는 그 문장이 틀렸다고 이미 기록했다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "한 아티팩트 안의 서로 모르는 Kafka 스택 두 개 (MSG-015, 가족 문서가 미해결로 표시)", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`CompatibilityMatrix`에 `EXTENSION` 등급이 있고 항목이 없으며, bridge leaf가 표 밖에 있다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "증거 등급 모델 전체가 자동 실행 경로 밖에 있고, CLAUDE.md는 현재 시제로 서술한다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "승격 게이트가 하향 전이도 승격 규칙으로 판정하고, javadoc 이 약속한 거부는 없다", - "kind": "case", - "slug": "grpc-advanced-bootstrap-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-advanced-bootstrap#L182`" - ], - "owning-module": "`grpc-advanced-bootstrap` · priority: `P3`", - "classification": "던지는 경우는 널과 `from == to` 둘뿐이다. \"이 게이트가 다루는 전이가 아닐 때\" 라는 조건에 해당하는 검사가 없다. 그래서 하향 전이가 승격 규칙으로 판정된다. 능력을 철회하려는 결정이 증거 부족을 이유로 막힌다. 방향이 뒤집혀 있다. 지금은 도달성이 낮다 — 이 게이트를 부르는 production 코드가 없고 테스트도 상향 전이만 넣는다. 기록하는 이유는 javadoc 이 그 거부를 이미 약속했다는 점이다. 수정은 `to.ordinal()` 이 아니라 등급의 서열을 명시한 뒤 상향 전이만 받고 나머지는 던지는 것이다. 철회는 별도 경로가 필요하다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "declaration-and-document-drift/case/case-grpc-advanced-bootstrap-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-advanced-bootstrap-f02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-advanced-bootstrap-f02.txt" - ] - }, - { - "title": "승격 차단 목록에 담금 기간과 실환경 항목이 없다", - "kind": "case", - "slug": "grpc-advanced-edition-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-advanced-edition#L206`" - ], - "owning-module": "`grpc-advanced-edition` · priority: `P3`", - "classification": "`GrpcEdition2024Gate.promotionBlockers` 가 보는 것은 셋이다 — 호환성 보고서의 문제들, 소비자 이관 계획, 승격 ADR. 같은 가족의 `GrpcAdvancedPromotionGate` 는 `EDITION_2024` 능력에 대해 일곱 증거 항목과 7일 담금을 요구한다. 두 게이트가 같은 능력의 승격을 서로 다른 기준으로 판정한다. 두 게이트가 각각 다른 것을 묻는다고 볼 수도 있다 — 하나는 편집 자체의 호환성, 하나는 능력의 운영 준비도. 다만 어느 쪽도 상대를 부르지 않고, 문서에도 두 게이트의 관계가 적혀 있지 않다. 승격을 실제로 수행할 때 어느 쪽을 만족해야 하는지가 코드에서 답해지지 않는다. 수정은 `promotionBlockers` 가 `GrpcAdvancedPromotionGate.evaluate` 의 결과를 포함하게 하거나, 두 게이트의 역할 분담을 자바독에 적는 것이다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "declaration-and-document-drift/case/case-grpc-advanced-edition-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-advanced-edition-f02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-advanced-edition-f02.txt" - ] - }, - { - "title": "부트스트랩 대조가 문서 어디든의 부분 문자열을 본다", - "kind": "case", - "slug": "grpc-advanced-resilience-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-advanced-resilience#L136`" - ], - "owning-module": "`grpc-advanced-resilience` · priority: `P3`", - "classification": "세 검사가 모두 문서 전체에 대한 부분 문자열 포함이다. JSON 파서를 쓰지 않은 이유는 자바독이 밝힌다 — 세 필드를 보려고 파서를 xDS 를 켜는 모든 배포의 실행 클래스패스에 올리지 않겠다는 것이다. 그 판단 자체는 이 저장소의 다른 결정들과 일관된다. 다만 검사의 형태가 그 판단보다 느슨하다. `\"tls\"` 가 문서 어디에든 있으면 통과한다. 통제 평면 채널이 `insecure` 로 설정되어 있고 다른 곳(예: 서버 리스너 설정)에 `tls` 라는 낱말이 있으면 두 번째 검사가 지나간다. 자원 이름공간이 주석·다른 필드·다른 서버 항목에 있어도 통과한다. 세 번째 검사가 막으려는 것은 \"이 클라이언트가 자기 이름공간 밖을 구독하는 것\" 인데, 문자열이 어딘가에 있다는 것은 그것이 이 클라이언트의 구독 대상이라는 뜻이 아니다. 그리고 이 검사가 막으려는 실패는 자바독이 스스로 \"조용하다\" 고 적은 것이다 — 아무것도 오류가 되지 않는 종류다. 느슨한 검사와 조용한 실패의 조합이 이 항목을 기록하는 이유다. 수정은 파서를 들이지 않고도 가능하다 — `\"channel_creds\"` 를 포함하는 객체 범위 안에서 `\"type\"` 값을 찾는 정도의 구조 인식이면 두 번째 검사가 실제 조건에 가까워진다. 또는 파서를 테스트 범위에만 두고 이 가드는 형태를 좁힌 정규식으로 바꾼다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "declaration-and-document-drift/case/case-grpc-advanced-resilience-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-advanced-resilience-f01" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-advanced-resilience-f01.txt" - ] - }, - { - "title": "프로파일 검증기가 javadoc 이 든 두 실수 중 하나만 검사한다", - "kind": "case", - "slug": "grpc-client-f04", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-client#L199`" - ], - "owning-module": "`grpc-client` · priority: `P3`", - "classification": "구현된 것은 첫째와 **다른 것**이다. 이름이 같은 프로파일이 두 번 선언된 경우를 잡는다. javadoc 이 든 둘째는 **이름이 다르고 대상이 같은** 경우인데, 그 검사가 없다. 지도는 이름을 키로 쓰므로 같은 대상을 가리키는 두 이름은 서로를 만나지 않는다. 그리고 둘째가 실제로 더 찾기 어려운 형태다 — 이름이 같으면 설정 결속이 먼저 실패하거나 나중 것이 이기지만, 이름이 다르면 조용히 두 채널이 생긴다. 수정은 대상과 설정을 키로 하는 두 번째 지도를 두고 역방향 중복을 보고하는 것이다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "declaration-and-document-drift/case/case-grpc-client-f04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-client-f04" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-client-f04.txt" - ] - }, - { - "title": "리졸버 검증기의 규칙이 하나뿐인데 javadoc 은 복수형으로 서술한다", - "kind": "case", - "slug": "grpc-discovery-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-discovery#L177`" - ], - "owning-module": "`grpc-discovery` · priority: `P3`", - "classification": "javadoc 은 \"Checks a discovery configuration for **the things** that look right and are not\" 라고 적는다. 실제로 담긴 규칙은 균형 정책의 무의미함 하나다. 나머지 위험 조합은 `GrpcResolverProfile` 정규 생성자가 이미 거부하므로 결과적으로 빈틈은 아니다. 다만 목록으로 보고하는 API 형태와 규칙 하나라는 내용이 어긋나 있어, 다음 사람이 여기에 규칙을 더할 자리로 읽거나 이미 여러 규칙이 있다고 읽는다. §17.1 이 실제로 그 자리다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "declaration-and-document-drift/case/case-grpc-discovery-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-discovery-f02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-discovery-f02.txt" - ] - }, - { - "title": "`deadlineRemaining` 도 meter 가 없다. javadoc 은 그것이 기록된다고 말한다", - "kind": "case", - "slug": "grpc-observability-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-observability#L213`" - ], - "owning-module": "`grpc-observability` · priority: `P3`", - "classification": "§17.1 과 같은 형태가 `GrpcRpcObservation` 에도 있고, 이쪽은 클래스 javadoc 이 명시적으로 어긋난다. 두 값을 함께 들면서 \"기록된다\"고 단언하는데, `record(GrpcRpcObservation)` 이 등록하는 meter 는 넷이다. `queueWaitTime` 은 `QUEUE_WAIT` 타이머로 나간다. `deadlineRemaining` 은 나가는 곳이 없다 — meter 이름 상수 일곱 개 중에도 마감 잔량에 해당하는 것이 없고, `tags()` 에도 들어가지 않는다(태그로 넣으면 카디널리티가 터지므로 그것이 옳다). 그래서 이 성분을 읽는 코드는 `unusedDeadline()` 하나이고, 그 메서드의 production 호출자는 0 이다(§12.1). §4.6 은 이 성분의 검증 비대칭(음수 허용)이 \"마감을 넘긴 호출을 표현하기 위한 것\" 이라고 읽었다. 그 해석은 그대로 유효하다 — 다만 그 표현이 도달하는 곳이 아직 없다. 관측값으로서는 §17.1 의 `queueHighWatermark` 와 같은 처지다. `queueWaitTime` 과 같은 형태로 타이머를 하나 더 둔다(마감을 넘긴 경우는 `unusedDeadline()` 이 이미 빈 값으로 구분해 주므로 기록 대상에서 빼면 된다). 아니면 javadoc 의 \"recorded\" 를 \"carried\" 로 낮춘다. 지금은 관측 대상 둘을 나란히 약속하고 하나만 내보낸다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "declaration-and-document-drift/case/case-grpc-observability-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-observability-f02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-observability-f02.txt" - ] - }, - { - "title": "오류 노출 거부 목록의 \"호스트와 포트\" 규칙이 IPv4 점표기만 본다", - "kind": "case", - "slug": "grpc-policy-f06", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-policy#L305`" - ], - "owning-module": "`grpc-policy` · priority: `P3`", - "classification": "아홉 패턴을 전부 읽으면 주소 형태를 보는 것은 이 하나다. 클래스 javadoc 은 거부 대상을 \"a stack frame, a SQL fragment, a JDBC URL, a bearer token, **a host and port**, a file path\" 로 서술하는데, 실제로 걸리는 host 는 IPv4 점표기뿐이다. IPv6 리터럴 — `fe80::1`, `[2001:db8::1]:5432` DNS 이름과 포트 — `documents-db.internal:5432`, `kafka-0.kafka-headless:9092` `jdbc:postgresql://db/app` 이 막히는 것은 host 규칙이 아니라 `jdbc:` 규칙 때문이다. 즉 이 구멍은 테스트에도 없다 — `exposurePolicyRefusesLeakyStrings` 의 아홉 사례 중 주소는 `upstream 10.0.3.14:5432 refused` 하나이고 IPv4 다. 닿는 경로는 `mapUnknown` 이다. 인식되지 않은 예외의 메시지를 `safeToExpose` 가 통과시키면 그대로 클라이언트로 간다. IPv6 클러스터나 쿠버네티스 서비스 이름을 쓰는 배포에서 상류 좌표가 밖으로 나간다. 등급이 P3 인 이유는 두 가지다. 이 리프가 build-only 라 오늘 닿지 않고, 노출되는 것이 자격증명이 아니라 내부 좌표다. 다만 이 정책이 존재하는 이유 자체가 \"부분 마스킹이 아니라 통째 교체\" 이므로, 목록에 빠진 형태는 통째로 통과한다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "declaration-and-document-drift/case/case-grpc-policy-f06.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-policy-f06" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-policy-f06.txt" - ] - }, - { - "title": "반환 목록이 자바독이 약속한 source order 가 아니다", - "kind": "case", - "slug": "grpc-proto-contract-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-proto-contract#L191`" - ], - "owning-module": "`grpc-proto-contract` · priority: `P3`", - "classification": "`validate` 의 javadoc 은 \"@return every violation found, **in source order**\" 라고 적는다. 실제로는 파일 앞머리의 `syntax`·`package` 위반이 40번째 줄의 `map` 위반보다 뒤에 온다. `describe()` 가 `file:line rule — detail` 형태를 만들고 그 형태의 목적이 빌드 로그를 읽는 것이므로, 정렬이 어긋나면 리뷰 목록으로서의 값이 줄어든다. 수정은 반환 직전에 `line` 으로 안정 정렬하는 것이다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "declaration-and-document-drift/case/case-grpc-proto-contract-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-proto-contract-f02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-proto-contract-f02.txt" - ] - }, - { - "title": "원시 API 규칙이 import 문만 보므로 완전 수식 사용과 와일드카드를 놓친다", - "kind": "case", - "slug": "grpc-server-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-server#L167`" - ], - "owning-module": "`grpc-server` · priority: `P3`", - "classification": "이것이 가정에 그치지 않는 이유는 이 저장소 자신의 문체다. 같은 가족의 여러 파일이 완전 수식 참조를 본문에 그대로 쓴다. 즉 이 코드베이스에서 완전 수식 사용은 예외가 아니라 흔한 형태다. 규칙 클래스의 자바독은 \"there is nothing to reach for\" 를 목표로 든다. 지금 형태는 손이 닿는 경로 하나만 본다. 수정은 정규식을 타입 이름의 등장 자체로 넓히거나(오탐이 생기므로 주석·문자열 제거가 필요), 바이트코드 기반 검사로 옮기는 것이다. 후자가 이 저장소의 다른 아키텍처 게이트와 형태가 같다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "declaration-and-document-drift/case/case-grpc-server-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-server-f02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-server-f02.txt" - ] - }, - { - "title": "지원 문서가 `deduplicatedPublish` 를 지원으로 적고, 코드는 거짓이며, 그 차이가 정확히 코드가 경고한 피해다", - "kind": "case", - "slug": "messaging-kafka-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-kafka#L208`" - ], - "owning-module": "`messaging-kafka` · priority: `P1`", - "classification": "코드의 판정이 옳고 그 근거가 javadoc 에 있다. `docs/messaging/support-matrix.md:55` 의 능력 표는 이 칸을 `O` 로 적는다. 그 차이가 무거운 이유는 이 플랫폼에서 이 플래그가 특별하기 때문이다. 능력 열둘 중 **부재가 예외를 만드는 유일한 플래그**다. 그래서 표를 읽고 중복 제거를 전제한 목적지를 설계한 팀은 실행 시점에 능력 예외를 만난다. 반대로 표를 읽고 \"중복 제거가 있으니 모호를 그냥 재시도해도 된다\" 고 결론지으면, 실제로는 중복이 저장된다. `MessagingCapabilities` 의 클래스 javadoc 이 그 피해를 미리 적는다 — \"a silently weakened guarantee is indistinguishable from a working one until the incident.\" 수정은 문서 쪽이다. 코드가 이미 옳다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "declaration-and-document-drift/case/case-messaging-kafka-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-kafka-f01", - "messaging-kafka-f01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-kafka-f01.txt" - ] - }, - { - "title": "태그 어휘가 존재하고 유일한 호출부가 우회해, 실패 분류가 기록되지 않는다", - "kind": "case", - "slug": "messaging-observability-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-observability#L673`" - ], - "owning-module": "`messaging-observability` · priority: `P2`", - "classification": "`DefaultMessagingObservationConvention`은 소비자가 0이다. 유일한 production 호출부(`DefaultMessagePublisher.observe:260-269`)가 `\"publish\"` 리터럴과 **4인자** `MessagingTags.of(...)`를 쓴다. 그 factory는 `failureCategory`와 `retryStage`를 `NONE`으로 고정한다. convention의 `publish(broker, dest, completion, Optional)`는 정확히 `failureCategory`를 채우려고 존재한다. convention javadoc이 \"an adapter inventing its own spelling of 'rejected' silently breaks every alert that was watching for it\"를 이유로 중앙화를 선언했고, 첫 호출부가 그것을 지나쳤다. 그리고 결과가 철자 문제에 그치지 않는다 — **`MessagingTags`가 선언한 6차원 중 4개만 채워진다.** 메트릭이 배선되더라도(§다음 항목) 실패한 발행이 `failureCategory=none`으로 기록되어, \"왜 실패했는가\"를 메트릭에서 나눌 수 없다. `PublishResult.failure()`에 `FailureDescriptor`가 이미 있으므로 값은 손에 있다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "declaration-and-document-drift/case/case-messaging-observability-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-observability-f01", - "messaging-observability-f01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-observability-f01.txt" - ] - }, - { - "title": "백오프 지터가 인스턴스를 분산시키지 못한다", - "kind": "case", - "slug": "messaging-outbox-jdbc-postgresql-f06", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-outbox-jdbc-postgresql#L923`" - ], - "owning-module": "`messaging-outbox-jdbc-postgresql` · priority: `P3`", - "classification": "`jittered = capped - (capped/8) * (exponent % 3)` 는 `exponent` 만의 함수다. 같은 상태의 복제본들은 같은 값을 계산한다. javadoc 이 약속하는 \"thundering herd 방지\" 가 성립하지 않는다. `OutboxRelay` 가 이미 `defaultOwner()` 로 프로세스별 안정 식별자를 만든다(`pid@uuid8`). 그것의 해시를 지터에 섞으면 결정성(같은 프로세스에서 재현 가능)을 유지하면서 인스턴스 간 위상차가 생긴다. javadoc 이 난수를 거부한 이유(\"a random source would make the schedule impossible to test\")도 그대로 지켜진다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "declaration-and-document-drift/case/case-messaging-outbox-jdbc-postgresql-f06.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-outbox-jdbc-postgresql-f06" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-outbox-jdbc-postgresql-f06.txt" - ] - }, - { - "title": "커넥션 획득 방식이 리프 안에서 갈린다", - "kind": "case", - "slug": "messaging-outbox-jdbc-postgresql-f07", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-outbox-jdbc-postgresql#L929`" - ], - "owning-module": "`messaging-outbox-jdbc-postgresql` · priority: `P3`", - "classification": "`JdbcOutboxRepository.append` 는 `DataSourceUtils`, 나머지는 raw `dataSource.getConnection()`, `JdbcAdminOperationJournal` 은 전부 `DataSourceUtils`. 릴레이 연산이 비즈니스 트랜잭션에 합류하면 안 된다는 판단은 타당하지만 어디에도 적혀 있지 않고, 같은 리프의 저널이 반대로 한다. `withConnection` 에 한 문장 — \"릴레이 연산은 호출자 트랜잭션에 합류하지 않는다\" — 을 붙이면 `append` 의 상세한 주석과 짝이 맞는다. 저널이 `DataSourceUtils` 를 쓰는 것이 의도인지도 확인이 필요하다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "declaration-and-document-drift/case/case-messaging-outbox-jdbc-postgresql-f07.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-outbox-jdbc-postgresql-f07" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-outbox-jdbc-postgresql-f07.txt" - ] - }, - { - "title": "CI에서 돈다고 선언한 게이트를 부르는 CI가 없다", - "kind": "case", - "slug": "messaging-schema-avro-f01", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-schema-avro#L524`" - ], - "owning-module": "`messaging-schema-avro` · priority: `P2`", - "classification": "`AvroCompatibilityGate` javadoc이 \"Run in CI rather than at runtime\"이라고 선언한다. 저장소 전체에서 이 클래스 참조는 자기 선언과 자기 테스트뿐이고, `src/build.gradle`의 9개 `verifyMessaging*` task 중 스키마 진화를 검사하는 것이 없다. 게이트의 존재 이유가 \"한 번 발행되면 보존 로그에 영구히 남는다\"인데, 그 보호가 어느 파이프라인에도 붙어 있지 않다. `AvroMessageCodec`의 미사용과 달리 이것은 membership으로 설명되지 않는다 — 런타임 편입 여부와 무관하게 CI 게이트는 붙었어야 한다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "declaration-and-document-drift/case/case-messaging-schema-avro-f01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-schema-avro-f01", - "messaging-schema-avro-f01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-schema-avro-f01.txt" - ] - }, - { - "title": "출고되는 신뢰성 체인 전체가 아무도 공급하지 않는 빈 뒤에 있고, 그 사슬이 자기 클래스 안을 가리킨다", - "kind": "case", - "slug": "messaging-spring-boot-starter-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-spring-boot-starter#L300`" - ], - "owning-module": "`messaging-spring-boot-starter` · priority: `P2`", - "classification": "여섯 빈 전부가 애플리케이션이 공급해야 하는 타입에 걸려 있다. 조건 자체는 옳고 근거도 정확하다 — 플랫폼이 기본 구현을 주면 조용히 엉뚱한 곳에, 또는 아무 데도 쓰지 않게 된다. 문제는 저장소 안에 그 타입을 공급하는 코드가 없다는 것이다. `messaging-outbox-jdbc-postgresql` 의 `JdbcOutboxRepository` 는 스프링 스테레오타입도 `@Bean` 선언도 없고, `new JdbcOutboxRepository` 가 main 에 0 건이다. 그래서 이 스타터를 켠 배포는 발행 경로는 얻고 발신함 경로는 얻지 못하며, 그 사실이 시작 시점에 어떤 신호도 내지 않는다. 둘째와 셋째가 같은 설정 클래스 안에서 방금 선언된 빈의 존재를 조건으로 삼는다. 스프링은 `@ConditionalOnBean` 을 자동 설정 클래스에서만, 그리고 등록 순서에 의존하는 방식으로만 신뢰할 수 있다고 문서화한다. 지금은 첫 조건이 이미 거짓이라 결과가 드러나지 않는다. 발신함을 배선하는 순간 이 사슬이 실제로 평가된다. 같은 가족의 다른 결정과 대비된다. 관리 평면은 스위치가 켜졌을 때 만들어지지 **않는** 타입의 부재를 javadoc 에 명시한다(`DestructiveMessagingAdmin` 하나). 이쪽은 여섯이 조용히 빠진다. 수정은 둘이다. 발신함을 요구하는 설정에서 저장소 빈이 없으면 시작을 거부하는 검증(이 가족의 `StartupProfileValidation` 형태), 그리고 중계·작업자·수명을 하나의 `@Bean` 으로 합치거나 조건을 전부 최초 두 타입으로 표현하는 것.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "declaration-and-document-drift/case/case-messaging-spring-boot-starter-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-spring-boot-starter-f02", - "messaging-spring-boot-starter-f02-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-spring-boot-starter-f02.txt" - ] - }, - { - "title": "설정 경로의 재시도가 예외 분류를 표현할 수 없다", - "kind": "case", - "slug": "messaging-spring-boot-starter-f04", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-spring-boot-starter#L342`" - ], - "owning-module": "`messaging-spring-boot-starter` · priority: `P3`", - "classification": "`RetryPolicy` 는 성분 열이고 그중 둘이 분류 집합이다. `DestinationSettings.Retry` 에는 이 둘에 대응하는 키가 없고, 컴파일러가 상수로 채운다. 빈 집합은 \"기본 분류 그대로\" 라는 중립값이므로 오동작은 아니다. 문제는 비대칭이다. `DestinationProfile` 을 자바로 선언한 배포는 두 집합을 조정할 수 있고, 문서대로 YAML 로 설정한 배포는 할 수 없다. 이 리프가 반복해서 근거로 든 규칙이 정확히 그 비대칭을 금지한다. 수정은 `Retry` 에 두 키를 더하는 것이다. `FailureCategory` 는 열거이므로 relaxed binding 이 그대로 처리한다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "declaration-and-document-drift/case/case-messaging-spring-boot-starter-f04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-spring-boot-starter-f04" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-spring-boot-starter-f04.txt" - ] - }, - { - "title": "클래스 javadoc 이 강제되지 않는 규칙을 강제된다고 말한다", - "kind": "case", - "slug": "messaging-testkit-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-testkit#L950`" - ], - "owning-module": "`messaging-testkit` · priority: `P2`", - "classification": "`BrokerFailureMatrix.java:18-20` 이 \"A Stable adapter must cover every scenario. That rule is enforced by a test rather than documented\" 라고 쓰고 있으나, `isComplete` 를 Stable 어댑터에 거는 테스트는 없다(`EVD-300`). 유일한 호출부는 Experimental 어댑터가 불완전함을 단언한다. 실제 Stable 인 `messaging-kafka` 는 `connection-refused` gap 을 가진 채 통과하며, 그 gap 은 같은 모듈이 명시적으로 단언한다. 코드 쪽 결정(\"gap 을 열거하되 비어 있음을 단언하지 않는다\")은 옳고, 그 이유도 `CrossBrokerContractSuite.java:45-47` 에 적혀 있다. 문제는 **javadoc 이 갱신되지 않은 것**이다. 이 리프의 다른 javadoc 여섯 곳이 자기 이력을 정확히 남긴 것과 대비되어 더 눈에 띈다. 같은 이유로 테스트 메서드 이름 `everyStableAdapterCoversEveryFaultScenario` 도 본문과 맞지 않는다. `everyStableAdaptersGapsAreExactlyWhatTheEvidenceShows` 같은 이름이 본문을 정확히 기술한다. 수정 방향: javadoc 을 현재 규칙(\"Stable 은 live-broker 증거를 하나 이상 요구한다. 전 시나리오 커버리지는 목표이지 게이트가 아니며, gap 은 `knownGaps` 로 명명된다\")으로 바꾸고, 테스트 이름을 본문에 맞춘다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "declaration-and-document-drift/case/case-messaging-testkit-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-testkit-f02", - "messaging-testkit-f02-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-testkit-f02.txt" - ] - }, - { - "title": "`gitCommit` 은 기록되지만 읽혀 판정되지 않는다", - "kind": "case", - "slug": "messaging-testkit-f08", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-testkit#L984`" - ], - "owning-module": "`messaging-testkit` · priority: `P3`", - "classification": "`BrokerCertificationEvidence` javadoc 이 \"without them the evidence cannot be checked against anything later\" 라고 쓰지만, 실제로 `gitCommit` 을 읽어 무언가를 결정하는 코드는 없고 게이트는 오히려 그 필드를 비교에서 제외한다(§12.4(c)). 현재 매니페스트의 커밋은 HEAD 가 아니다(`e98b56eb` vs `21234e38`). \"증거가 얼마나 오래된 트리에서 나왔는가\" 를 보고하는 것은 유용한 진단이 될 수 있다 — 게이트로 만들 필요는 없고, `knownGaps` 처럼 사실로 노출하면 이 리프의 나머지 설계와 결이 맞는다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "declaration-and-document-drift/case/case-messaging-testkit-f08.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-testkit-f08" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-testkit-f08.txt" - ] - } - ], - "concept": [], - "reference": [ - { - "title": "문서가 범위를 제한하면 그 제한을 누가 강제하는지 같이 적는다", - "kind": "reference", - "slug": "messaging-cloudevents-f05", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-cloudevents#L556`" - ], - "owning-module": "`messaging-cloudevents`", - "rule": "문서가 범위를 제한하면 그 제한을 누가 강제하는지 같이 적는다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — `CloudEventMapper` javadoc의 범위 제한이 강제되지 않는다", - "scope": [ - "스키마와 코덱 계열. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "declaration-and-document-drift/reference/reference-messaging-cloudevents-f05.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "타입이 문서화한 불변식은 타입이 강제한다", - "kind": "reference", - "slug": "messaging-observability-f06", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-observability#L717`" - ], - "owning-module": "`messaging-observability`", - "rule": "타입이 문서화한 불변식은 타입이 강제한다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 감사 이벤트가 redaction을 강제하지 않는다", - "scope": [ - "정책과 조립 경계. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "declaration-and-document-drift/reference/reference-messaging-observability-f06.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "구성 오류는 한 예외 타입과 안정 코드로 보고한다", - "kind": "reference", - "slug": "messaging-policy-f06", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-policy#L826`" - ], - "owning-module": "`messaging-policy`", - "rule": "구성 오류는 한 예외 타입과 안정 코드로 보고한다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 프로파일 검증 실패가 플랫폼 예외 계층 밖이다", - "scope": [ - "정책과 조립 경계. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "declaration-and-document-drift/reference/reference-messaging-policy-f06.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "저장소 밖 문서를 절 번호로 인용하지 않는다", - "kind": "reference", - "slug": "messaging-policy-f07", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-policy#L835`" - ], - "owning-module": "`messaging-policy`", - "rule": "저장소 밖 문서를 절 번호로 인용하지 않는다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — javadoc이 해소되지 않는 설계 문서를 인용한다", - "scope": [ - "정책과 조립 경계. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "declaration-and-document-drift/reference/reference-messaging-policy-f07.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "호출 컨텍스트가 계약이면 그 컨텍스트를 검증할 수단을 함께 정한다", - "kind": "reference", - "slug": "messaging-reliability-api-f06", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-reliability-api#L734`" - ], - "owning-module": "`messaging-reliability-api`", - "rule": "호출 컨텍스트가 계약이면 그 컨텍스트를 검증할 수단을 함께 정한다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 트랜잭션 계약 셋이 타입으로 강제되지 않는다", - "scope": [ - "멱등·아웃박스·인박스 계열. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "declaration-and-document-drift/reference/reference-messaging-reliability-api-f06.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "port 계약은 동시성 요구를 적는다", - "kind": "reference", - "slug": "messaging-schema-api-f02", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-schema-api#L503`" - ], - "owning-module": "`messaging-schema-api`", - "rule": "port 계약은 동시성 요구를 적는다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — port 구현의 스레드 안전성 요구가 문서화되어 있지 않다", - "scope": [ - "스키마와 코덱 계열. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "declaration-and-document-drift/reference/reference-messaging-schema-api-f02.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "도달성 판정은 단어가 아니라 import로 확인한다", - "kind": "reference", - "slug": "messaging-schema-api-f03", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-schema-api#L512`" - ], - "owning-module": "`messaging-schema-api`", - "rule": "도달성 판정은 단어가 아니라 import로 확인한다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — `SchemaRegistry`라는 이름이 저장소에서 두 가지를 가리킨다", - "scope": [ - "스키마와 코덱 계열. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "declaration-and-document-drift/reference/reference-messaging-schema-api-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "한 개념의 등록 상태를 두 객체가 나눠 갖지 않는다", - "kind": "reference", - "slug": "messaging-spring-cloud-stream-bridge-f03", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-spring-cloud-stream-bridge#L570`" - ], - "owning-module": "`messaging-spring-cloud-stream-bridge`", - "rule": "한 개념의 등록 상태를 두 객체가 나눠 갖지 않는다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 인터페이스를 publisher만 구현하고 두 클래스가 같은 바인딩에 각자 상태를 갖는다", - "scope": [ - "정책과 조립 경계. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "declaration-and-document-drift/reference/reference-messaging-spring-cloud-stream-bridge-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - } - ], - "question": [ - { - "title": "세 갈래 판정이 포트의 `boolean`에서 두 갈래로 접힌다", - "kind": "question", - "slug": "messaging-inbox-jdbc-postgresql-f04", - "readiness": "OPEN", - "source": [ - "`final/document.md#a19-messaging-inbox-jdbc-postgresql#L675`" - ], - "owning-module": "`messaging-inbox-jdbc-postgresql` · priority: `P3`", - "classification": "`InboxResult`가 세 값과 `isSafeToSettle()`을 갖는데 production은 `APPLIED`만 만든다. `InboxRepository.reserve`가 `boolean`을 반환하므로 `ALREADY_APPLIED`와 `CLAIMED_ELSEWHERE`가 같은 `false`로 들어온다. `TransactionalInboxHandler`는 그 경우 `HandleResult.success()`를 반환한다 — 정산한다. `InboxResult` javadoc이 세 값이 필요한 이유로 정확히 그 정산을 든다 — \"would settle a message whose effect is still only half-written by another instance\". **다만 그 상황이 PostgreSQL에서 실제로 발생 가능한지 확인하지 않았다**(§16). `ON CONFLICT DO NOTHING`이 미커밋 충돌에 대해 대기한다면 `CLAIMED_ELSEWHERE`는 도달 불가능한 상태이고 enum이 과설계인 것이며, 즉시 0을 반환한다면 이것은 실제 결함이다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "declaration-and-document-drift/question/openquestion-messaging-inbox-jdbc-postgresql-f04.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - } - ], - "decision": [] - } - }, - "runtime-contract-correctness": { - "topic": "runtime-contract-correctness", - "title": "런타임 계약 정확성 — 개별 정책·가드·연산이 자기 계약을 지키는가", - "readerQuestion": "", - "kinds": { - "case": [ - { - "title": "`IdFactory.newId()`의 “never-before-used” 문구 정밀화", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "application-supplied `JpaRetryPolicy`가 valid execution에서 무시된다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "query SQL naming/observability composition 부재", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "export surface split SSOT", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`MongoRegexPolicy.forbidden()`은 금지하지 않는다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "태그 allowlist는 규약이지 강제가 아니다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "구현 없는 4개의 계약 중 셋은 그 사실을 적고, 하나는 적지 않는다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "발행 rename만 경로 기반이고, 그것을 지키는 것은 이 모듈이 \"근사에 불과하다\"고 적은 사전검사다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "패턴 구독의 R2 승인만 호출자가 아니라 배포에 대해 이루어진다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "permit 정책 이름이 세 곳에 문자열로 존재하고 교차 검사가 없다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`close()`가 실패하면 drain 스케줄러 스레드가 남는다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`POOL_ROUTE_EXCEEDS_TOTAL` 위반 코드는 발화할 수 없다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`AUTHENTICATION_FAILED`를 지우지 않는다는 `resumeHealthy`의 보장이, 관리자 평면에 노출된 2단계 시퀀스로 우회된다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "Thymeleaf 예외 메시지 삭제 가드가 프로덕션이 타지 않는 오버로드에만 있다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`WebProblemSanitizer.alreadySafe`가 죽은 메서드이고 그 안의 조건도 죽어 있다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "플랫폼 요청 컨텍스트가 서블릿에는 생산자가 없고, 리액티브에는 익명 액터로 고정되어 있다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "프레임워크 자유 신원 모델과 교차 테넌트 가드가 프로덕션에서 한 번도 참조되지 않는다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "리액티브 전송에는 속도 제한 경로가 하나도 없다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "선언된 Advanced 능력 11개 중 9개는 켜는 방법이 없다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "원인 사슬 순회가 2-순환에서 무한 루프에 빠지고, 저장소는 이미 그 사례를 이름으로 적어 두었다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "capability 12개 중 main 코드가 읽는 것은 3개, 거부하는 것은 1개", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "호환성 게이트를 가진 두 포맷은 build-only이고, 출하되는 유일한 코덱에는 게이트가 없다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "브로커 ACL 매니페스트의 자기 점검이 존재하지 않는다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "저장소 어디에도 참조가 없는 타입 3개", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`GrpcStreamAdmission`도 같은 형태이고, per-caller 맵이 줄지 않는다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "`GrpcSerializedStreamWriter`의 `DROP_OLDEST`가 잘못된 메시지의 바이트를 뺀다", - "kind": "case", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "하나의 상태 코드가 같은 메서드 안에서 두 답을 갖는다", - "kind": "case", - "slug": "grpc-core-api-f04", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-core-api#L200`" - ], - "owning-module": "`grpc-core-api` · priority: `P3`", - "classification": "`forMutation` 은 스위치에 닿기 전에 `OK` 를 먼저 처리한다. 스위치의 `OK` 분기는 도달하지 않는다. 열거형 전수 처리를 컴파일러가 요구하므로 항목 자체는 필요하지만, 그 값이 위의 가드와 반대다. 결과는 잠재적 함정이다. 누군가 위의 `OK` 가드를 \"중복이니까\" 지우면 컴파일은 통과하고 `OK` 인 변경이 `COMPLETION_UNKNOWN` 이 된다 — 성공한 변경마다 대사(reconciliation)를 요구하게 된다. 이 리프의 다른 자리들은 그런 편집이 눈에 띄도록 설계되어 있다(예: 승격 메서드를 하나로 좁힌 것). 수정은 한 글자다. 스위치의 `OK` 를 `COMPLETED` 로 옮기면 두 자리의 답이 같아지고, 가드가 사라져도 결과가 바뀌지 않는다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-contract-correctness/case/case-grpc-core-api-f04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-core-api-f04" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-core-api-f04.txt" - ] - }, - { - "title": "허용 태그 8개 중 둘은 값이 자유 문자열이고, 그중 하나는 bounded 열거형이 이미 존재한다", - "kind": "case", - "slug": "grpc-observability-f03", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-observability#L238`" - ], - "owning-module": "`grpc-observability` · priority: `P3`", - "classification": "값 검사는 키가 allowlist 를 통과한 뒤 `UNBOUNDED_VALUE` 세 형태만 본다. 그런데 태그 값의 출처는 균일하지 않다. `GrpcStreamObservation` 의 검증은 `terminationReason` 이 널이 아니고 공백이 아닌지만 본다. 호출자가 예외 메시지나 원격 상태 문자열을 그대로 넣으면 그 태그의 값 공간이 트래픽과 함께 자란다 — 이 클래스가 존재하는 이유로 든 바로 그 실패다. 그리고 그 개념의 bounded 열거형이 이미 저장소에 있다 — `grpc-policy` 의 `GrpcStreamTerminationReason`. 쓰지 않은 이유는 의존 방향으로 설명된다. 이 리프의 `allowed_dependencies` 는 `[\"grpc-core-api\"]` 뿐이고 그 열거형은 `grpc-policy` 에 있다. 그래서 수정은 열거형을 `grpc-core-api` 로 옮기거나, `violations` 가 두 자유 문자열 태그에 대해 허용값 집합을 받도록 서명을 넓히는 것이다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-contract-correctness/case/case-grpc-observability-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-observability-f03" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-observability-f03.txt" - ] - }, - { - "title": "반사 모드를 명시하면 서비스·역할 허용 목록이 조용히 하드코딩으로 바뀐다", - "kind": "case", - "slug": "grpc-spring-boot-starter-f04", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-spring-boot-starter#L249`" - ], - "owning-module": "`grpc-spring-boot-starter` · priority: `P3`", - "classification": "두 갈래가 만드는 것이 같은 종류의 값이 아니다. 설정하지 않으면 `defaultFor(environment)` — 환경이 서비스 목록과 역할 목록을 함께 결정한다. 설정하면 모드만 운영자 것이고, **허용 서비스와 허용 역할은 이 자동 설정에 박힌 리터럴이 된다.** 운영자가 조정한다고 생각하는 것은 노출 수위 하나인데, 실제로는 노출 대상 집합까지 바뀐다. 그리고 그 두 리터럴은 설정 표면에 노출되어 있지 않으므로 되돌릴 방법이 `reflection-mode` 를 다시 비우는 것뿐이다. `ca-skeleton.grpc.platform` 은 `ignoreUnknownFields = false` 를 걸어 \"오타가 조용히 기본값으로 남지 않게\" 한 설정 표면이다. 같은 규율로 보면, 값을 하나 설정했을 때 설정하지 않은 두 값이 함께 바뀌는 것도 같은 종류의 침묵이다. 허용 서비스·역할을 `GrpcPlatformProperties` 에 올리거나, 명시 모드에서도 `defaultFor(environment)` 가 만든 정책의 모드만 바꾼 사본을 쓴다. 후자가 이 저장소의 다른 곳에서 쓰는 형태다(`GrpcProtoStyleManifest.allowingWellKnownTypes` 처럼 넓힌 사본).", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-contract-correctness/case/case-grpc-spring-boot-starter-f04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-spring-boot-starter-f04" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-spring-boot-starter-f04.txt" - ] - }, - { - "title": "예외 승격이 에러 코드 문자열 접미사에 의존한다", - "kind": "case", - "slug": "messaging-claim-check-f03", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-claim-check#L525`" - ], - "owning-module": "`messaging-claim-check` · priority: `P3`", - "classification": "`ClaimCheckResolver.verify`가 `validation.failure().code().endsWith(\"_MISMATCH\")`로 `ClaimCheckIntegrityException` 승격을 결정한다. `ClaimCheckIntegrityGuard`의 세 코드 중 둘이 그 접미사를 갖는다. 두 클래스 사이의 계약이 **문자열 명명 규약**이고 어디에도 선언되지 않았다. guard가 코드를 바꾸면(예: `CLAIM_CHECK_DIGEST_INVALID`) 승격이 조용히 멈추고 poison message가 `PERMANENT_BUSINESS`로 분류된다 — 재시도 정책이 달라진다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-contract-correctness/case/case-messaging-claim-check-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-claim-check-f03" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-claim-check-f03.txt" - ] - }, - { - "title": "`MessagingRedactor`가 상수 대신 문자열 리터럴을 쓴다", - "kind": "case", - "slug": "messaging-core-api-f04", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-core-api#L871`" - ], - "owning-module": "`messaging-core-api` · priority: `P3`", - "classification": "`messaging-observability/.../MessagingRedactor.java:24`가 `\"msg.id\"`를 리터럴로 갖는다. `ReservedHeaders.MESSAGE_ID` 상수가 있다. 상수가 바뀌면 redaction이 조용히 대상을 잃는다. 컴파일러가 잡지 않는다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-contract-correctness/case/case-messaging-core-api-f04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-core-api-f04" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-core-api-f04.txt" - ] - }, - { - "title": "구세대 전이 메서드가 신세대와 다른 행 상태를 남긴다", - "kind": "case", - "slug": "messaging-outbox-jdbc-postgresql-f05", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-outbox-jdbc-postgresql#L917`" - ], - "owning-module": "`messaging-outbox-jdbc-postgresql` · priority: `P3`", - "classification": "`markPublished(MessageId, Instant)` 는 `lease_owner` 와 `next_attempt_at` 을 지우지 않는다. `markPublished(OutboxLease, Instant)` 는 지운다. `markAmbiguous`/`markFailed` 도 같다. 두 컬럼은 청구 술어와 부분 인덱스가 읽는 값이다. 이 저장소에 구세대를 부르는 프로덕션 코드는 없다. 그러나 포트에 남아 있고 `@Deprecated` 도 아니므로, 외부 구현이나 향후 코드가 부를 수 있다. 최소한 `@Deprecated` 와 \"신세대를 쓰라\"는 문장이 필요하고, 더 나은 것은 제거다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-contract-correctness/case/case-messaging-outbox-jdbc-postgresql-f05.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-outbox-jdbc-postgresql-f05" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-outbox-jdbc-postgresql-f05.txt" - ] - }, - { - "title": "`maxBatches` 가 하드코딩이고 현재는 의미가 없다", - "kind": "case", - "slug": "messaging-outbox-jdbc-postgresql-f08", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-outbox-jdbc-postgresql#L935`" - ], - "owning-module": "`messaging-outbox-jdbc-postgresql` · priority: `P3`", - "classification": "starter 가 `20` 을 박아 넣는다(`:141`, `:170`). P1 을 고치기 전에는 이 값이 아무 일도 하지 않고, 고친 뒤에는 배치 크기와 함께 조정 대상이 된다. `OutboxProperties`/`InboxRetentionPolicy` 로 옮기는 것이 맞다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-contract-correctness/case/case-messaging-outbox-jdbc-postgresql-f08.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-outbox-jdbc-postgresql-f08" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-outbox-jdbc-postgresql-f08.txt" - ] - }, - { - "title": "죽은 매개변수 하나가 유일한 비기본값에서 NPE 를 낳는다", - "kind": "case", - "slug": "messaging-spring-boot-starter-f03", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-spring-boot-starter#L319`" - ], - "owning-module": "`messaging-spring-boot-starter` · priority: `P3`", - "classification": "호출처가 셋이고 전부 `() -> true` 다. 그래서 이 매개변수는 값을 하나만 갖는다. 그리고 그것이 죽어 있다는 것보다 나쁜 성질이 있다 — 이 매개변수가 존재하는 이유(\"이 역할은 선택적이다\")대로 `() -> false` 를 넘기면 `credential == null` 인 경로가 가드를 지나 다음 줄의 `credential.type()` 에서 NPE 로 죽는다. 즉 이 매개변수의 유일한 비기본값이 의도한 동작이 아니라 널 역참조다. 수정은 매개변수를 지우고 널 검사를 무조건으로 만드는 것이다. 선택적 역할이 필요해지는 날에는 `Optional` 을 돌려주는 별도 메서드가 그 자리다 — `admin` 이 이미 호출처에서 그렇게 다뤄진다.", - "missing-verification": "이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "runtime-contract-correctness/case/case-messaging-spring-boot-starter-f03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-spring-boot-starter-f03" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-spring-boot-starter-f03.txt" - ] - } - ], - "concept": [], - "reference": [ - { - "title": "legacy storage/notification compatibility surface의 제거 조건 추적", - "kind": "reference", - "slug": "", - "readiness": "", - "publication": "미작성" - }, - { - "title": "구성 오류는 한 예외 타입과 안정 코드로 보고한다", - "kind": "reference", - "slug": "messaging-kafka-share-experimental-f04", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-kafka-share-experimental#L494`" - ], - "owning-module": "`messaging-kafka-share-experimental`", - "rule": "구성 오류는 한 예외 타입과 안정 코드로 보고한다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 두 거절이 다른 예외 계층을 쓴다", - "scope": [ - "메시징 어댑터의 소비·발행 경로. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "runtime-contract-correctness/reference/reference-messaging-kafka-share-experimental-f04.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "부팅 경로의 알고리즘 복잡도는 문서화한다", - "kind": "reference", - "slug": "messaging-policy-f05", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-policy#L817`" - ], - "owning-module": "`messaging-policy`", - "rule": "부팅 경로의 알고리즘 복잡도는 문서화한다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 사이클 검사가 경로마다 집합을 복사한다", - "scope": [ - "정책과 조립 경계. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "runtime-contract-correctness/reference/reference-messaging-policy-f05.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "`CompletionStage`를 반환하는 메서드는 동기적으로 던지지 않는다", - "kind": "reference", - "slug": "messaging-runtime-core-f05", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-runtime-core#L745`" - ], - "owning-module": "`messaging-runtime-core`", - "rule": "`CompletionStage`를 반환하는 메서드는 동기적으로 던지지 않는다", - "purpose": "이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — admission 실패만 예외로 전파된다", - "scope": [ - "메시징 어댑터의 소비·발행 경로. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다." - ], - "exceptions": [ - "SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 \"예외 미관측\"이지 \"예외 없음\"이 아니다." - ], - "grounds": [ - "근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다." - ], - "listedInTree": false, - "publication": "초안", - "file": "runtime-contract-correctness/reference/reference-messaging-runtime-core-f05.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - } - ], - "question": [ - { - "title": "isolation vocabulary와 legacy routing capability의 시차", - "kind": "question", - "slug": "", - "readiness": "", - "publication": "미작성" - } - ], - "decision": [] - } - }, - "capability-declaration-vs-proof": { - "topic": "capability-declaration-vs-proof", - "title": "", - "readerQuestion": "", - "kinds": { - "case": [ - { - "title": "브로커 트랜잭션을 무조건 참으로 선언하고, 그 조건을 검사하는 검증기는 기동 시 돌지 않는다", - "kind": "case", - "slug": "a-transaction-capability-true-and-its-validator-never-run", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-kafka` §17.2", - "`final/document.md#a19-messaging-spring-boot-starter` §17.2" - ], - "code": [ - "`.../messaging-kafka/.../KafkaMessagingTransport.java`(`CAPABILITIES`)", - "`.../messaging-kafka/.../KafkaTransactionProfileValidator.java`", - "`.../messaging-spring-boot-starter/.../MessagingPlatformAutoConfiguration`(`StartupProfileValidation` 감싸기)" - ], - "evidence": [ - "없음 — 능력 상수와 검증기의 감싸임 여부 대조로 판정했다" - ], - "classification": "능력 상수의 `brokerTransaction` 이 프로파일과 무관하게 참이다. Kafka 트랜잭션은 `transactional.id` 와 그에 맞는 소비자 격리 수준이 있어야 성립하고, 그 조건을 검사하는 `KafkaTransactionProfileValidator` 가 이 저장소에 있다. 그런데 스타터가 검증기를 빈으로 발행하면서 `StartupProfileValidation` 으로 감싸지 않는다 — 같은 자동 설정 안에서 다른 검증기들은 감싸인다. 검증기가 빈으로 존재하는 것과 기동 시 실행되는 것이 다르다는 것을 그 클래스의 javadoc 이 이미 이름 붙였다 — \"the context published a validator per broker and validated nothing.\" 그 수정이 이 가족에 적용됐고 한 곳만 남았다. 결과: 트랜잭션 없이 구성된 배포가 트랜잭션 능력을 참으로 광고한 채 기동한다.", - "missing-verification": "컨텍스트를 세워 검증기가 실제로 건너뛰는지 확인하지 않았다. 감싸기 목록과 검증기 목록 대조로 판정했다", - "relations": [ - "`concept:three-sources-of-a-capability-answer`", - "`reference:a-validator-is-enforced-by-injection`", - "`reference:a-capability-constant-must-derive-from-the-profile`", - "`case:capability-constant-outlives-its-condition`" - ], - "listedInTree": false, - "publication": "초안", - "file": "capability-declaration-vs-proof/case/case-a-transaction-capability-true-and-its-validator-never-run.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a-transaction-capability-true-and-its-validator-never-run" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a-transaction-capability-true-and-its-validator-never-run.txt" - ] - }, - { - "title": "지연 배달을 참으로 선언하는데 그 지연을 제공할 토폴로지가 조립되지 않는다", - "kind": "case", - "slug": "a-delayed-delivery-flag-without-the-topology-that-delivers-it", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-rabbit` §17.4" - ], - "code": [ - "`.../messaging-rabbit/.../RabbitMessagingTransport.java`(능력 상수)", - "같은 리프의 토폴로지 선언부" - ], - "evidence": [ - "없음 — 능력 상수와 토폴로지 조립 경로 대조로 판정했다" - ], - "classification": "RabbitMQ 의 지연 배달은 브로커가 기본으로 주는 기능이 아니다. 지연 교환 플러그인이나 TTL + 데드레터 라우팅으로 만들어야 하고, 그 토폴로지가 없으면 지연 요청은 즉시 배달로 조용히 강등된다. 능력 상수는 그 조건과 무관하게 참이다. 그리고 이 리프 전체가 production 호출자를 갖지 않으며 `MessagingProviderSelection.BROKERS_WITHOUT_A_TRANSPORT` 가 `rabbit` 을 이름으로 거절한다 — 즉 선언은 미래의 배선을 위해 남아 있고, 그 배선이 생기는 날 이 플래그는 이미 참이다.", - "missing-verification": "브로커를 띄워 지연 요청의 실제 배달 시점을 관측하지 않았다. 토폴로지 선언의 부재로 판정했다", - "relations": [ - "`concept:three-sources-of-a-capability-answer`", - "`reference:a-capability-constant-must-derive-from-the-profile`", - "`case:an-unselectable-broker-listed-with-features`" - ], - "listedInTree": false, - "publication": "초안", - "file": "capability-declaration-vs-proof/case/case-a-delayed-delivery-flag-without-the-topology-that-delivers-it.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a-delayed-delivery-flag-without-the-topology-that-delivers-it" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a-delayed-delivery-flag-without-the-topology-that-delivers-it.txt" - ] - }, - { - "title": "같은 어댑터의 능력을 전송과 검증기가 다르게 답하고, 런타임이 쓰는 쪽이 record 의 의미와 어긋난다", - "kind": "case", - "slug": "the-transport-and-the-validator-answer-differently", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-pulsar-experimental` §17.1" - ], - "code": [ - "`.../messaging-pulsar-experimental/.../PulsarMessagingTransport.java`(`KEY_SHARED_CAPABILITIES`)", - "`.../PulsarProfileValidator.java`(`capabilities(...)`)", - "`.../messaging-core-api/.../destination/MessagingCapabilities.java:11-36`" - ], - "evidence": [ - "없음 — 두 열두-성분 리터럴의 성분별 대조로 판정했다" - ], - "classification": "Key_Shared 구독에 대해 전송은 `orderedStream=false, keyedOrdering=true` 를, 검증기는 `orderedStream=true, keyedOrdering=true` 를 답한다. 성분 문서가 판정 기준이다 — `orderedStream` 은 \"순서 단위 안에서 순서가 보존되는가\" 이고 Key_Shared 의 순서 단위는 키다. 그러므로 검증기 쪽이 문서화된 의미와 맞고, 전송 쪽은 자기 안에서 모순이다. 그리고 어긋난 쪽이 런타임이 읽는 쪽이다 — `capabilities(DestinationName)` 이 SPI 메서드이고 `orderedStream` 은 production 코드가 실제로 읽는 세 능력 중 하나다(`DefaultRetryDecisionEngine` 이 그 값으로 순서 보존 재시도를 고른다). 두 리터럴을 묶는 것은 아무것도 없고, 테스트는 `keyedOrdering` 만 단언해 `orderedStream` 을 보지 않는다. 자매 어댑터 NATS 는 두 곳이 같은 값을 답하지만 그 일치도 공유가 아니라 손으로 복사한 리터럴이다.", - "missing-verification": "두 답이 실제 재시도 선택을 어떻게 가르는지 실행으로 재현하지 않았다", - "relations": [ - "`concept:three-sources-of-a-capability-answer`", - "`reference:the-weight-of-a-flag-is-set-by-the-code-that-reads-it`", - "`reference:check-which-duplicate-is-wired`", - "`case:capability-constant-outlives-its-condition`" - ], - "listedInTree": false, - "publication": "초안", - "file": "capability-declaration-vs-proof/case/case-the-transport-and-the-validator-answer-differently.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "the-transport-and-the-validator-answer-differently" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/the-transport-and-the-validator-answer-differently.txt" - ] - }, - { - "title": "운영자용 지원 매트릭스가 런타임 편입을 반대로 적고, 틀린 쪽이 옳은 쪽을 권위로 지목한다", - "kind": "case", - "slug": "the-support-matrix-says-nothing-is-deployed-and-eighteen-are", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-core-api` §17" - ], - "code": [ - "`docs/messaging/support-matrix.md:23`", - "`src/messaging/CLAUDE.md:46-59`", - "`src/config/architecture/modules.json`" - ], - "evidence": [ - "`evidence/raw/270-messaging-runtime-membership-drift.txt`" - ], - "classification": "지원 매트릭스가 \"registry 의 messaging leaf 는 모두 `runtime_memberships` 가 비어 있고 어느 composition root 에도 편입되지 않았다\" 고 적는다. 현재 레지스트리는 25개 중 18개가 `[\"app-bootstrap\"]` 이고 `messaging-core-api` 자신이 그 안에 있다. 형태가 특이한 것은 **틀린 문단이 권위로 지목하는 문서가 이미 정정을 마쳤다**는 점이다 — `src/messaging/CLAUDE.md` 는 같은 사실을 고쳤고 \"정확한 목록은 registry 가 소유하므로 여기서 세지 않는다 — 세는 순간 다시 drift 한다\" 는 결론까지 적었다. 그 결론이 지원 매트릭스에는 적용되지 않았다. 운영자는 배포 아티팩트가 실제로 이 리프들을 싣고 `app.messaging.enabled` 하나로 켜진다는 사실을 문서에서 알 수 없다.", - "missing-verification": "없음 — 레지스트리와 두 문서를 전수 대조했다", - "relations": [ - "`reference:numbers-in-docs-should-be-derived`", - "`reference:fix-overstatement-before-understatement`", - "`concept:three-sources-of-a-capability-answer`", - "`case:five-documents-say-nineteen-leaves`" - ], - "listedInTree": false, - "publication": "초안", - "file": "capability-declaration-vs-proof/case/case-the-support-matrix-says-nothing-is-deployed-and-eighteen-are.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "the-support-matrix-says-nothing-is-deployed-and-eighteen-are" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/the-support-matrix-says-nothing-is-deployed-and-eighteen-are.txt" - ] - } - ], - "concept": [ - { - "title": "능력 선언의 세 출처와 그것이 파생되지 않을 때", - "kind": "concept", - "slug": "three-sources-of-a-capability-answer", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-core-api` §4.12", - "`final/document.md#a99` §3.2" - ], - "code": [ - "`.../messaging-core-api/.../destination/MessagingCapabilities.java`", - "각 어댑터의 `CAPABILITIES` 상수", - "각 어댑터의 `*ProfileValidator.capabilities(...)`", - "`docs/messaging/support-matrix.md`" - ], - "classification": "이 플랫폼에서 \"이 어댑터가 무엇을 증명할 수 있는가\" 에 답하는 곳이 셋이다. 전송의 `MessagingCapabilities` 상수(SPI `capabilities(DestinationName)` 가 런타임에 돌려주는 값), 검증기의 같은 이름 메서드(기동 시점 판정용), 그리고 운영자가 읽는 지원 매트릭스 문서. 열두 성분은 전부 `boolean` 이고 의미는 record javadoc 이 소유한다 — `brokerAcknowledgement` · `replicationOrPersistenceEvidence` · `perMessageSettlement` · `batchSettlement` · `orderedStream` · `keyedOrdering` · `replay` · `delayedDelivery` · `brokerTransaction` · `deduplicatedPublish` · `nativeDeadLetter` · `topologyManagement`. 세 출처가 같은 값을 답해야 한다는 것이 계약인데, 그것을 붙드는 장치가 없다. 그리고 열둘의 무게가 같지 않다 — 부재가 예외를 만드는 것은 `deduplicatedPublish` 하나이고(`DefaultMessagePublisher`), 나머지는 읽히지 않거나 분기에만 쓰인다. record javadoc 이 그 위험을 미리 서술한다 — \"a silently weakened guarantee is indistinguishable from a working one until the incident.\"", - "missing-verification": "세 출처를 전수 대조하는 스크립트를 돌리지 않았다. 어댑터별 SSOT 의 §능력 절을 읽어 대조했다", - "relations": [ - "`reference:a-capability-constant-must-derive-from-the-profile`", - "`reference:the-weight-of-a-flag-is-set-by-the-code-that-reads-it`", - "`case:capability-constant-outlives-its-condition`", - "`case:support-matrix-said-the-opposite-of-the-code`" - ], - "listedInTree": false, - "publication": "초안", - "file": "capability-declaration-vs-proof/concept/concept-three-sources-of-a-capability-answer.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "three-sources-of-a-capability-answer", - "three-sources-of-a-capability-answer-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/three-sources-of-a-capability-answer.txt" - ] - } - ], - "reference": [ - { - "title": "능력 선언은 프로파일에서 파생되어야 하고 상수는 그것을 할 수 없다", - "kind": "reference", - "slug": "a-capability-constant-must-derive-from-the-profile", - "readiness": "READY", - "source": [ - "`final/document.md#a99` §3.2", - "`final/document.md#a19-messaging-nats-experimental` §17.1" - ], - "classification": "능력은 \"이 어댑터가 무엇을 증명할 수 있는가\" 가 아니라 \"이 프로파일로 구성된 이 목적지에서 무엇이 성립하는가\" 에 대한 답이다. 두 질문의 답이 갈리는 조건이 프로파일에 있으면 상수는 그 조건을 담을 수 없다. 판정은 한 줄이다 — 이 플래그가 참이 되는 조건을 문장으로 쓰고, 그 문장에 프로파일 필드가 등장하는지 본다. 등장하면 상수는 틀린 표현이다.", - "scope": [ - "`MessagingCapabilities` 열두 성분과 gRPC 쪽의 대응 선언 전부. 이 저장소의 실제 사례 — NATS `deduplicatedPublish`(창이 있을 때만), Kafka `brokerTransaction`(`transactional.id` 가 있을 때만), Rabbit `delayedDelivery`(지연 토폴로지가 있을 때만)." - ], - "exceptions": [ - "어댑터가 브로커와 무관하게 항상 제공하는 성질은 상수가 맞다. 구분 기준은 \"이 값을 거짓으로 만드는 구성이 존재하는가\" 이고, 존재하지 않으면 상수다." - ], - "relations": [ - "`concept:three-sources-of-a-capability-answer`", - "`case:capability-constant-outlives-its-condition`", - "`case:a-transaction-capability-true-and-its-validator-never-run`", - "`case:a-delayed-delivery-flag-without-the-topology-that-delivers-it`" - ], - "listedInTree": false, - "publication": "초안", - "file": "capability-declaration-vs-proof/reference/reference-a-capability-constant-must-derive-from-the-profile.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "능력 플래그의 무게는 그것을 읽는 코드가 정한다", - "kind": "reference", - "slug": "the-weight-of-a-flag-is-set-by-the-code-that-reads-it", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-nats-experimental` §17.1", - "`final/document.md#a19-messaging-pulsar-experimental` §17.1" - ], - "classification": "같은 record 의 성분이라고 무게가 같지 않다. 어떤 플래그는 아무도 읽지 않고, 어떤 플래그는 분기에만 쓰이며, 어떤 플래그는 부재가 예외를 만든다. 과대 선언의 대가는 그 플래그를 읽는 코드가 무엇을 하느냐로 정해지므로, 심각도를 매기기 전에 소비자를 먼저 세어야 한다.", - "scope": [ - "능력·기능 플래그 record 전부. 세는 방법은 성분 접근자 이름으로 저장소를 훑어 production 호출자를 분류하는 것이다 — 미사용", - "분기", - "예외 발생." - ], - "exceptions": [ - "아직 배선되지 않은 블록에서는 모든 플래그의 현재 무게가 0 이다. 그때의 판정은 \"배선되면 무엇이 그것을 읽게 되는가\" 이고, 그 답은 같은 가족의 배선된 리프에서 가져온다." - ], - "relations": [ - "`concept:three-sources-of-a-capability-answer`", - "`case:the-transport-and-the-validator-answer-differently`", - "`reference:runtime-membership-decides-severity`", - "`case:capability-constant-outlives-its-condition`" - ], - "listedInTree": false, - "publication": "초안", - "file": "capability-declaration-vs-proof/reference/reference-the-weight-of-a-flag-is-set-by-the-code-that-reads-it.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - } - ], - "question": [], - "decision": [] - } - }, - "non-atomic-check-then-act": { - "topic": "non-atomic-check-then-act", - "title": "", - "readerQuestion": "", - "kinds": { - "case": [ - { - "title": "회전이 비교 후 교체가 아니라 덮어쓰기이고, 세대 계수기는 음수가 되면 회수되지 않는다", - "kind": "case", - "slug": "a-rotation-that-overwrites-and-a-generation-that-cannot-be-reclaimed", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-client` §17.1, §17.2" - ], - "code": [ - "`.../grpc-client/.../GrpcChannelRuntimeRegistry.java`(`rotate`)", - "`.../grpc-client/.../GrpcChannelRuntime.java`(`finishUnaryCall`·`closeStream`)" - ], - "evidence": [ - "없음 — 읽기와 쓰기가 별개 연산이라는 코드 형태로 판정했다" - ], - "classification": "두 결함이 같은 형태다. `rotate` 는 현재 런타임을 `get()` 으로 읽어 판단한 뒤 조건 없는 `set` 을 한다 — 두 회전이 겹치면 나중 것이 먼저 것을 덮고, 덮인 쪽이 이미 반환한 채널은 회수되지 않는다. `finishUnaryCall` 과 `closeStream` 은 `get() > 0` 을 확인하고 별도 연산으로 감소시킨다 — 계수기가 음수가 되면 `quiescent()` 가 영원히 거짓이 되어 그 세대를 회수할 수 없다. 후자가 더 나쁜 이유는 되돌릴 경로가 없다는 것이다. 자격증명 회전에서 같은 형태가 이미 두 가족을 건너 재현됐고, 이 리프는 그 사슬의 세 번째 지점이다.", - "missing-verification": "경합을 실행으로 재현하지 않았다", - "relations": [ - "`concept:check-then-act-on-atomic-types`", - "`case:the-same-rotation-defect-closed-once-and-reproduced`", - "`case:complete-drain-rolls-back-a-rotation`", - "`reference:atomic-type-is-not-atomicity`" - ], - "listedInTree": false, - "publication": "초안", - "file": "non-atomic-check-then-act/case/case-a-rotation-that-overwrites-and-a-generation-that-cannot-be-reclaimed.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a-rotation-that-overwrites-and-a-generation-that-cannot-be-reclaimed" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a-rotation-that-overwrites-and-a-generation-that-cannot-be-reclaimed.txt" - ] - }, - { - "title": "승인 경계가 동시성 아래에서 새고, 큐 계수기를 되돌리는 경로가 없다", - "kind": "case", - "slug": "an-admission-boundary-that-leaks-and-a-counter-that-cannot-return", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-server` §17.4", - "`final/document.md#a20-grpc-policy` §17.1" - ], - "code": [ - "`.../grpc-server/.../GrpcAdmissionController.java`(`tryAdmit`·`release`·`promoteFromQueue`)", - "`.../grpc-policy/.../GrpcStreamAdmission.java`" - ], - "evidence": [ - "없음 — 세 메서드의 읽기·쓰기 분리와 계수기 쌍 대조로 판정했다" - ], - "classification": "승인 제어기의 세 메서드가 전부 읽은 뒤 별도로 쓴다. 그 자체로 경계가 초과되고, 여기에 계수기 쌍의 비대칭이 겹친다 — 큐를 거쳐 들어온 승인은 `queued` 만 증가시키는데 `release()` 는 `inFlight` 만 감소시킨다. 그래서 큐를 거친 요청이 끝날 때마다 `queued` 가 줄지 않고 남는다. 경계가 한 번 새는 것과 경계가 영구히 느슨해지는 것은 다른 결함이고, 이 리프에는 둘 다 있다. 스트림 승인 쪽은 여기에 하나를 더한다 — 호출자별 맵이 줄지 않아 재접속 폭풍에서 가장 많이 샌다. 세 메서드 모두 production 호출자가 0 이므로 오늘의 사고는 아니다.", - "missing-verification": "경합과 큐 경로를 실행으로 재현하지 않았다", - "relations": [ - "`concept:check-then-act-on-atomic-types`", - "`reference:beginning-a-transition-and-the-set-it-covers-are-one-operation`", - "`reference:atomic-type-is-not-atomicity`" - ], - "listedInTree": false, - "publication": "초안", - "file": "non-atomic-check-then-act/case/case-an-admission-boundary-that-leaks-and-a-counter-that-cannot-return.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "an-admission-boundary-that-leaks-and-a-counter-that-cannot-return" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/an-admission-boundary-that-leaks-and-a-counter-that-cannot-return.txt" - ] - }, - { - "title": "배수를 시작한 뒤에도 한 서비스가 다시 SERVING 이 될 수 있다", - "kind": "case", - "slug": "draining-began-and-a-service-came-back-serving", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-admin` §17.4" - ], - "code": [ - "`.../grpc-admin/.../GrpcServiceHealthRegistry.java`(`beginDraining`·`markServing`·`recomputeGlobal`)" - ], - "evidence": [ - "없음 — 두 메서드의 순서와 가드 위치 대조로 판정했다" - ], - "classification": "`beginDraining()` 이 두 단계다 — 먼저 `draining = true` 를 쓰고, 그다음 `states.replaceAll(...)` 로 모든 서비스를 DRAINING 으로 바꾼다. `markServing` 은 첫 줄에서 `if (draining) return;` 으로 자기를 막는다. 그 가드를 통과한 스레드가 두 단계 사이에 쓰면, 그 서비스만 SERVING 으로 남고 `recomputeGlobal()` 이 전체 상태를 SERVING 으로 되돌린다. 배수 중인 인스턴스가 로드밸런서에 준비됐다고 답하는 상태이고, 배수의 목적이 정확히 그것을 막는 것이다. 같은 리프의 `rejectNewAdmission()` 이 단계만 기록하고 아무것도 거절하지 않는 것과 짝을 이룬다.", - "missing-verification": "경합을 실행으로 재현하지 않았다", - "relations": [ - "`concept:check-then-act-on-atomic-types`", - "`reference:beginning-a-transition-and-the-set-it-covers-are-one-operation`", - "`case:reject-new-admission-that-rejects-nothing`" - ], - "listedInTree": false, - "publication": "초안", - "file": "non-atomic-check-then-act/case/case-draining-began-and-a-service-came-back-serving.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "draining-began-and-a-service-came-back-serving" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/draining-began-and-a-service-came-back-serving.txt" - ] - } - ], - "concept": [ - { - "title": "원자 타입 위의 검사 후 실행과 비교 후 교체 루프", - "kind": "concept", - "slug": "check-then-act-on-atomic-types", - "readiness": "READY", - "source": [ - "`final/document.md#a99` §3.5", - "`final/document.md#a20-grpc-policy` §17.1, §17.2" - ], - "code": [ - "`.../grpc-policy/.../resilience/GrpcRetryBudget.java`(`tryConsume`)", - "`.../resilience/GrpcHedgingBudget.java`", - "`.../streaming/GrpcDemandController.java`" - ], - "classification": "이 저장소가 같은 문제를 세 가지로 푼다. (1) 비교 후 교체 루프 — `GrpcRetryBudget.tryConsume` 이 `get()` 으로 현재 값을 읽고 `compareAndSet` 이 실패하면 다시 읽는다. (2) `synchronized` — `GrpcDemandController` 가 같은 형태를 락으로 닫는다. (3) 검사 후 실행 — `get()` 으로 조건을 확인하고 별도 연산으로 `set`·`incrementAndGet`·`put` 한다. 셋째는 원자 타입을 쓰면서 원자성을 얻지 못하는 형태이고, 두 스레드가 같은 조건을 통과한 뒤 각자 쓴다. `AtomicReference` 에서는 나중 쓰기가 먼저 쓰기를 덮고, `AtomicInteger` 에서는 경계가 초과되며, `ConcurrentMap` 에서는 `get` 뒤의 `put` 이 다른 스레드의 갱신을 지운다. 정본이 같은 저장소에 있다는 것이 이 개념의 핵심이다 — 저자들이 올바른 형태를 알고 있었고, 열 곳 남짓에서 쓰지 않았다.", - "missing-verification": "어느 사례도 경합을 실행으로 재현하지 않았다. 전부 읽기와 쓰기가 별개 연산이라는 코드 형태로 판정했다", - "relations": [ - "`reference:atomic-type-is-not-atomicity`", - "`reference:beginning-a-transition-and-the-set-it-covers-are-one-operation`", - "`case:the-same-rotation-defect-closed-once-and-reproduced`", - "`case:complete-drain-rolls-back-a-rotation`" - ], - "listedInTree": false, - "publication": "초안", - "file": "non-atomic-check-then-act/concept/concept-check-then-act-on-atomic-types.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "check-then-act-on-atomic-types", - "check-then-act-on-atomic-types-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/check-then-act-on-atomic-types.txt" - ] - } - ], - "reference": [ - { - "title": "전이를 시작하는 쓰기와 그 전이가 덮는 집합은 한 연산이어야 한다", - "kind": "reference", - "slug": "beginning-a-transition-and-the-set-it-covers-are-one-operation", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-admin` §17.4", - "`final/document.md#a20-grpc-policy` §17.2" - ], - "classification": "상태 전이가 \"플래그를 세운다\" 와 \"그 플래그가 지배하는 대상을 갱신한다\" 두 단계로 나뉘면, 그 사이에 플래그를 읽고 통과한 쓰기가 존재한다. 그 쓰기는 전이 이전의 판단으로 전이 이후의 상태를 만든다. 판정은 \"플래그를 읽는 가드와 그 플래그가 덮는 쓰기 사이에 다른 스레드가 낄 수 있는가\" 이고, 낄 수 있으면 두 단계를 하나로 합치거나 그 구간을 락으로 닫아야 한다.", - "scope": [ - "배수·종료·회전·차단처럼 \"이제부터 다르게 동작한다\" 를 선언하는 모든 전이. 이 저장소의 사례 — 헬스 레지스트리의 배수 시작, 자격증명 회전의 배수 완료, 채널 런타임의 세대 교체." - ], - "exceptions": [ - "전이 이후의 쓰기가 무해하면(집합에 다시 넣어도 결과가 같으면) 두 단계로 나눠도 된다. 다만 그 무해함은 전이가 덮는 대상 전체에 대해 성립해야 하고, 대상이 늘어나면 다시 확인해야 한다." - ], - "relations": [ - "`concept:check-then-act-on-atomic-types`", - "`case:draining-began-and-a-service-came-back-serving`", - "`case:complete-drain-rolls-back-a-rotation`", - "`reference:atomic-type-is-not-atomicity`" - ], - "listedInTree": false, - "publication": "초안", - "file": "non-atomic-check-then-act/reference/reference-beginning-a-transition-and-the-set-it-covers-are-one-operation.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - } - ], - "question": [], - "decision": [] - } - }, - "retention-and-unbounded-growth": { - "topic": "retention-and-unbounded-growth", - "title": "", - "readerQuestion": "", - "kinds": { - "case": [ - { - "title": "cleanup 이 스스로 막겠다고 적은 장애를 일으키는 형태로 호출된다", - "kind": "case", - "slug": "the-cleanup-that-causes-the-outage-it-prevents", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-inbox-jdbc-postgresql` §17 P1", - "`final/document.md#a19-messaging-outbox-jdbc-postgresql` §17 P1" - ], - "code": [ - "`.../messaging-inbox-jdbc-postgresql/.../InboxCleanupJob.java:56`", - "`.../messaging-outbox-jdbc-postgresql/.../OutboxCleanupJob.java:50`", - "`.../JdbcInboxRepository.java:141`", - "`.../JdbcOutboxRepository.java:486`" - ], - "evidence": [ - "`evidence/raw/294-bounded-purge-never-called.txt`" - ], - "classification": "두 cleanup 잡이 무제한 오버로드를 부른다. bounded 구현은 `LIMIT` + `FOR UPDATE SKIP LOCKED` 로 두 리프 모두에 존재하고 호출 지점이 0 이다. `InboxCleanupJob` 의 javadoc 이 실행되는 코드의 동작을 그대로 서술한다 — \"A single unbounded DELETE over a table that has been accumulating for weeks holds locks long enough to block the very reservations the inbox exists to serve, so the cleanup would cause the outage it is meant to prevent.\" bounded 쪽 javadoc 은 한 발 더 나간다 — 배치 크기로 제한된다는 잡의 자기 서술을 참으로 만드는 것이 바로 이 파라미터라고 적는다. 그 파라미터를 아무도 넘기지 않는다. 두 리프 다 `app-bootstrap` 소속이고 두 잡 다 스타터 빈이지만 스케줄러가 등록되지 않는다 — 그것이 의도된 설계다. 그래서 상시 결함이 아니라 잠재 결함이고, 애플리케이션이 문서 지시대로 잡을 스케줄하는 순간 첫 스윕에서 발현한다. 회귀 테스트가 성립하려면 대역도 고쳐야 한다 — 현재 대역의 bounded 구현은 전부 지우고 숫자만 깎는 형태라 차이를 재현하지 못한다.", - "missing-verification": "백로그가 쌓인 실제 테이블에서 두 형태의 락 보유 시간을 측정하지 않았다. 호출 지점 부재와 두 SQL 의 형태로 판정했다", - "relations": [ - "`concept:bounded-and-unbounded-side-by-side`", - "`reference:a-port-that-offers-both-forms-has-chosen-the-unsafe-one`", - "`reference:a-fake-that-cannot-show-the-property-is-not-a-witness`", - "`case:outbox-chain-behind-an-unsatisfiable-condition`" - ], - "listedInTree": false, - "publication": "초안", - "file": "retention-and-unbounded-growth/case/case-the-cleanup-that-causes-the-outage-it-prevents.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "the-cleanup-that-causes-the-outage-it-prevents" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/the-cleanup-that-causes-the-outage-it-prevents.txt" - ] - }, - { - "title": "재생 저장소와 중복 제거 맵에 제거 경로가 없다", - "kind": "case", - "slug": "a-replay-store-with-no-eviction-path", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-policy` §17.3", - "`final/document.md#a20-grpc-advanced-streaming` §17.1" - ], - "code": [ - "`.../grpc-policy/.../GrpcResultReplayStore.java`", - "`.../grpc-advanced-streaming/.../GrpcClientMessageDeduplicator.java`" - ], - "evidence": [ - "없음 — 두 자료구조의 삽입·제거 경로 대조로 판정했다" - ], - "classification": "결과 재생 저장소가 항목을 넣기만 하고 지우지 않는다. 만료·용량·세션 종료 어느 축으로도 제거 경로가 없으므로, 프로세스 수명 동안 단조 증가한다. 형제 리프의 중복 제거기가 같은 형태다 — 그 클래스는 다른 무제한 증가를 비판하는 javadoc 을 갖고 있으면서 자기 `replayableOutcomes` 맵을 세션 안에서 무제한으로 늘린다. 두 사례가 한 쌍인 이유는 둘 다 \"정확히 한 번\" 계열의 보장을 위해 과거를 기억하는 자료구조라는 점이다. 그 종류의 자료구조에서 보존 경계는 기능이 아니라 전제다 — 무엇을 언제까지 기억하는지가 정해지지 않으면 그 보장은 메모리가 버티는 동안만 성립한다.", - "missing-verification": "장시간 실행으로 증가를 관측하지 않았다. 배선 경로가 없어 실행 대상이 없다", - "relations": [ - "`concept:bounded-and-unbounded-side-by-side`", - "`reference:one-formula-and-one-enforcement-point-per-safety-rule`", - "`reference:runtime-membership-decides-severity`" - ], - "listedInTree": false, - "publication": "초안", - "file": "retention-and-unbounded-growth/case/case-a-replay-store-with-no-eviction-path.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a-replay-store-with-no-eviction-path" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a-replay-store-with-no-eviction-path.txt" - ] - } - ], - "concept": [ - { - "title": "bounded 와 unbounded 오버로드를 나란히 둔 포트", - "kind": "concept", - "slug": "bounded-and-unbounded-side-by-side", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-reliability-api` §17", - "`final/document.md#a19-messaging-inbox-jdbc-postgresql` §17" - ], - "code": [ - "`.../messaging-reliability-api/.../InboxRepository.java:36-52`", - "`.../OutboxRepository.java:132-151`" - ], - "classification": "두 포트가 각각 `purge*Before(Instant)` 와 `purge*Before(Instant, int)` 를 나란히 선언한다. 뒤쪽이 안전한 형태이고 그 이유가 javadoc 에 있다 — \"The cleanup jobs describe themselves as bounded by batch size; this is the parameter that makes that true.\" 두 형태를 가르는 표시는 없다. `@Deprecated` 도, 이름 차이도, 호출을 막는 가시성 차이도 없다. 그래서 호출자는 인자가 적은 쪽을 고른다. 같은 리프가 다른 곳에서 같은 형태를 이미 기록했다 — 한 인터페이스가 같은 전이의 두 세대를 갖고 안전하지 않은 쪽에 표시가 없다. 이 개념의 요지는 결함이 호출자에게 있지 않다는 것이다. 포트의 형태가 오용을 가능하게 했고, 두 리프에서 같은 방향으로 발생했다.", - "missing-verification": "없음 — 두 포트의 선언과 저장소 전역 호출 지점을 전수 확인했다", - "relations": [ - "`case:the-cleanup-that-causes-the-outage-it-prevents`", - "`reference:a-port-that-offers-both-forms-has-chosen-the-unsafe-one`", - "`reference:two-vocabularies-for-one-concept`" - ], - "listedInTree": false, - "publication": "초안", - "file": "retention-and-unbounded-growth/concept/concept-bounded-and-unbounded-side-by-side.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "bounded-and-unbounded-side-by-side", - "bounded-and-unbounded-side-by-side-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/bounded-and-unbounded-side-by-side.txt" - ] - } - ], - "reference": [ - { - "title": "두 형태를 나란히 내놓는 포트는 이미 안전하지 않은 쪽을 고른 것이다", - "kind": "reference", - "slug": "a-port-that-offers-both-forms-has-chosen-the-unsafe-one", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-reliability-api` §17", - "`final/document.md#a99` §3.1" - ], - "classification": "인터페이스가 안전한 형태와 그렇지 않은 형태를 함께 노출하면 호출자는 짧은 쪽을 고른다. 그것은 호출자의 부주의가 아니라 포트가 만든 기본값이다. 판정 기준은 \"안전하지 않은 쪽을 부르는 것이 컴파일되는가\" 이고, 컴파일된다면 그 형태는 언젠가 호출된다.", - "scope": [ - "정리·삭제·조회처럼 결과 크기가 데이터에 비례하는 모든 연산. 이 저장소의 사례 — inbox·outbox 의 두 purge 오버로드, outbox 전이의 두 세대 메서드." - ], - "exceptions": [ - "두 형태가 진짜로 다른 용도를 가지면 공존이 맞다. 그때는 이름이 그 차이를 말해야 하고(`purgeAll` 대 `purgeBatch`), 위험한 쪽에는 그것을 부르는 조건이 javadoc 에 있어야 한다. 오버로드로 두는 것은 그 차이를 이름에서 지우는 선택이다." - ], - "relations": [ - "`concept:bounded-and-unbounded-side-by-side`", - "`case:the-cleanup-that-causes-the-outage-it-prevents`", - "`reference:two-vocabularies-for-one-concept`" - ], - "listedInTree": false, - "publication": "초안", - "file": "retention-and-unbounded-growth/reference/reference-a-port-that-offers-both-forms-has-chosen-the-unsafe-one.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "같은 안전 규칙은 하나의 공식과 하나의 강제 시점을 갖는다", - "kind": "reference", - "slug": "one-formula-and-one-enforcement-point-per-safety-rule", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-inbox-jdbc-postgresql` §17", - "`final/document.md#a19-messaging-transport-spi` §17" - ], - "classification": "같은 종류의 안전 여유가 여러 곳에서 독립적으로 정해지면 공식과 강제 시점이 갈린다. 이 저장소에서 inbox 보존 여유는 한 곳에서 곱셈으로, 다른 곳에서 덧셈으로 표현되고, 한쪽은 cleanup 잡을 만들 때만 검증하며 다른 쪽은 항상 검증한다. 드레인 마감 30초는 세 곳에서 각자 정해지고 그중 public 상수만 테스트가 붙든다. 판정은 \"이 값을 바꾸려면 몇 군데를 고쳐야 하는가\" 이고, 하나가 아니면 나머지는 조용히 옛 값으로 남는다.", - "scope": [ - "보존 기간·마감·재시도 상한·배치 크기처럼 안전을 위해 고른 모든 수치. 소유자를 한 곳으로 정하고 나머지가 그것을 참조하게 한다." - ], - "exceptions": [ - "층마다 다른 값이 정당한 경우가 있다 — 클라이언트 마감이 서버 마감보다 짧아야 하는 것처럼. 그때는 값이 아니라 **관계**가 한 곳에 있어야 하고, 그 관계를 검증하는 코드가 있어야 한다." - ], - "relations": [ - "`case:a-replay-store-with-no-eviction-path`", - "`case:an-order-contract-with-no-implementation`", - "`reference:numbers-in-docs-should-be-derived`" - ], - "listedInTree": false, - "publication": "초안", - "file": "retention-and-unbounded-growth/reference/reference-one-formula-and-one-enforcement-point-per-safety-rule.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - } - ], - "question": [], - "decision": [] - } - }, - "drain-and-shutdown-ordering": { - "topic": "drain-and-shutdown-ordering", - "title": "", - "readerQuestion": "", - "kinds": { - "case": [ - { - "title": "순서 계약을 구현하는 것이 없고, 그것을 검증한다는 테스트는 enum 선언 순서만 본다", - "kind": "case", - "slug": "an-order-contract-with-no-implementation", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-transport-spi` §17 P2" - ], - "code": [ - "`.../messaging-transport-spi/.../MessagingLifecycle.java`", - "`.../MessagingLifecycleTest.java`" - ], - "evidence": [ - "`evidence/raw/280-transport-spi-lifecycle-unimplemented.txt`" - ], - "classification": "세 겹이다. 첫째, `MessagingLifecycle` 의 구현체가 저장소에 없고 `ShutdownPhase` 의 외부 소비자도 없다 — `MessagingTransport` 를 구현하는 네 어댑터 중 어느 것도 이 인터페이스를 구현하지 않는다. 둘째, 순서를 검증한다는 다섯 테스트가 전부 `List.of(ShutdownPhase.values()).indexOf(A) < indexOf(B)` 형태다. 통과하는 것은 시스템 동작이 아니라 **소스에 상수가 적힌 순서**이고, 테스트 이름과 `as(...)` 문구는 시스템 동작을 서술한다 — \"flushing before the handlers finish would lose the settlements they produce\". 이 테스트들은 enum 상수를 재배열하면 실패하고, 재배열해도 시스템은 바뀌지 않는다. 셋째, 인터페이스가 컴파일 강제를 만들지 않는다. `MessagingTransport` 는 구현하지 않으면 빌드가 깨지고, 이것은 아무것도 깨지지 않는다. 같은 리프의 드레인 마감 30초가 세 곳에서 독립적으로 정해지는 것이 같은 사건의 일부다 — 살아 있는 값(private 복사본)이 죽은 인터페이스의 public 상수를 참조하지 않는다.", - "missing-verification": "없음 — 구현체 검색과 테스트 단언 형태를 전수 확인했다", - "relations": [ - "`concept:the-eight-phase-shutdown-contract`", - "`reference:a-declaration-order-test-is-a-gate-only-if-something-reads-that-order`", - "`reference:one-formula-and-one-enforcement-point-per-safety-rule`", - "`reference:omission-that-passes-is-not-a-gate`" - ], - "listedInTree": false, - "publication": "초안", - "file": "declared-contract-without-enforcement/case/case-an-order-contract-with-no-implementation.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "an-order-contract-with-no-implementation" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/an-order-contract-with-no-implementation.txt" - ] - }, - { - "title": "새 승인을 거절한다는 메서드가 단계만 기록하고 아무것도 거절하지 않는다", - "kind": "case", - "slug": "reject-new-admission-that-rejects-nothing", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-admin` §17.1" - ], - "code": [ - "`.../grpc-admin/.../GrpcDrainCoordinator.java`(`rejectNewAdmission`)" - ], - "evidence": [ - "없음 — 메서드 본문과 승인 경로 대조로 판정했다" - ], - "classification": "배수의 첫 단계는 새 요청을 받지 않는 것이다. `rejectNewAdmission()` 은 이름이 그것을 말하고, 본문은 현재 단계를 기록하는 것이 전부다. 승인을 실제로 판정하는 곳은 다른 리프의 승인 제어기이고, 그 제어기는 이 조정자의 단계를 읽지 않는다. 그래서 배수를 시작해도 새 요청은 계속 승인된다. 같은 리프의 헬스 레지스트리가 배수 중에 서비스를 다시 SERVING 으로 돌릴 수 있다는 것과 겹치면, 배수라는 절차 전체가 상태 기록으로만 존재하고 트래픽에 대해서는 아무 효과가 없다.", - "missing-verification": "배수 중 승인 시도를 실행으로 재현하지 않았다. 두 리프 사이에 참조가 없다는 것으로 판정했다", - "relations": [ - "`concept:the-eight-phase-shutdown-contract`", - "`case:draining-began-and-a-service-came-back-serving`", - "`reference:a-validator-is-enforced-by-injection`" - ], - "listedInTree": false, - "publication": "초안", - "file": "drain-and-shutdown-ordering/case/case-reject-new-admission-that-rejects-nothing.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "reject-new-admission-that-rejects-nothing" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/reject-new-admission-that-rejects-nothing.txt" - ] - }, - { - "title": "비밀을 힙에서 지우는 마지막 단계가 종료 경로에 연결되지 않았다", - "kind": "case", - "slug": "the-last-step-of-secret-erasure-is-not-wired", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-security` §17" - ], - "code": [ - "`.../messaging-security/.../CredentialRuntimeRegistry.java`(`clearAll`)" - ], - "evidence": [ - "없음 — `clearAll` 의 호출자 전수 검색으로 판정했다" - ], - "classification": "이 리프 전체가 \"비밀이 힙에 남지 않게 한다\" 를 목적으로 설계됐다 — 자격증명을 `char[]` 로 들고, 사용 후 `clear()` 하고, 회전 시 즉시 소거한다. `clearAll()` 의 javadoc 이 \"Clears every held credential, for shutdown\" 이라고 적고, 호출자가 저장소에 없다. 프로세스가 끝나면 힙도 사라지므로 사소해 보이지만, 종료가 느리거나 힙 덤프가 뜨는 경우가 정확히 이 통제가 겨냥하는 상황이다. 8단계 종료 계약에 자격증명 소거 단계가 있고 그 단계를 수행하는 코드가 없다는 점에서, 이 사례는 계약이 구현되지 않았다는 사실의 구체적 결과 하나다.", - "missing-verification": "힙 덤프로 잔존을 확인하지 않았다. 호출자 부재로 판정했다", - "relations": [ - "`concept:the-eight-phase-shutdown-contract`", - "`case:an-order-contract-with-no-implementation`", - "`reference:a-validator-is-enforced-by-injection`" - ], - "listedInTree": false, - "publication": "초안", - "file": "drain-and-shutdown-ordering/case/case-the-last-step-of-secret-erasure-is-not-wired.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "the-last-step-of-secret-erasure-is-not-wired" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/the-last-step-of-secret-erasure-is-not-wired.txt" - ] - } - ], - "concept": [ - { - "title": "8단계 종료 순서 계약과 실제 종료 경로", - "kind": "concept", - "slug": "the-eight-phase-shutdown-contract", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-transport-spi` §17" - ], - "code": [ - "`.../messaging-transport-spi/.../MessagingLifecycle.java`(`ShutdownPhase`)", - "`.../messaging-spring-boot-starter/.../MessagingShutdownLifecycle.java`" - ], - "classification": "`MessagingLifecycle` 이 종료를 8단계로 선언하고 javadoc 이 그 순서를 계약이라고 못 박는다 — \"The order in ShutdownPhase is the contract, not an implementation detail. Each adapter implements the phases; none of them chooses the order.\" 순서가 계약인 이유는 각 단계가 앞 단계의 결과 위에 서기 때문이다. 새 승인을 멈추기 전에 드레인하면 드레인이 끝나지 않고, 핸들러가 끝나기 전에 플러시하면 그 핸들러가 만들 정착을 잃는다. 실제 종료는 스타터의 `MessagingShutdownLifecycle` 이 하고 8단계 중 셋만 명시적으로 수행한다 — 승인 정지 · 새 핸들러 정지 · 드레인. 나머지는 Spring 의 `getPhase()` 정수와 빈 소멸 순서에 위임되거나 명시 단계가 없다. 즉 순서를 결정하는 것은 `ShutdownPhase` 가 아니다.", - "missing-verification": "실제 종료 시퀀스를 부팅해 관측하지 않았다. 두 클래스의 코드로 판정했다", - "relations": [ - "`case:an-order-contract-with-no-implementation`", - "`case:reject-new-admission-that-rejects-nothing`", - "`case:the-last-step-of-secret-erasure-is-not-wired`", - "`reference:a-declaration-order-test-is-a-gate-only-if-something-reads-that-order`" - ], - "listedInTree": false, - "publication": "초안", - "file": "drain-and-shutdown-ordering/concept/concept-the-eight-phase-shutdown-contract.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "the-eight-phase-shutdown-contract", - "the-eight-phase-shutdown-contract-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/the-eight-phase-shutdown-contract.txt" - ] - } - ], - "reference": [ - { - "title": "선언 순서를 단언하는 테스트는 그 순서를 읽는 코드가 있을 때만 게이트다", - "kind": "reference", - "slug": "a-declaration-order-test-is-a-gate-only-if-something-reads-that-order", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-transport-spi` §17 P2" - ], - "classification": "enum 상수의 순서나 목록의 원소 순서를 단언하는 테스트는 소스에 적힌 순서를 지킨다. 그것이 시스템 동작을 지키려면 그 순서를 읽어 동작을 결정하는 코드가 있어야 한다. 없으면 그 테스트가 지키는 것은 타이핑 순서이고, 이름과 설명 메시지가 시스템 동작을 서술할수록 그 어긋남은 커진다. 판정은 한 줄이다 — 이 enum 의 `values()` 나 `ordinal()` 을 읽는 production 코드가 있는가.", - "scope": [ - "순서가 의미를 갖는 모든 열거형과 목록 — 종료 단계, 필터 체인, 실패 번역 사슬, 마이그레이션 순서." - ], - "exceptions": [ - "순서 자체가 문서인 경우가 있다. 그때는 테스트 이름이 \"이 순서가 문서에 적힌 것과 같다\" 여야 하고 시스템 동작을 주장하면 안 된다." - ], - "relations": [ - "`case:an-order-contract-with-no-implementation`", - "`reference:omission-that-passes-is-not-a-gate`", - "`case:test-names-that-assert-what-their-bodies-do-not`", - "`concept:the-eight-phase-shutdown-contract`" - ], - "listedInTree": false, - "publication": "초안", - "file": "drain-and-shutdown-ordering/reference/reference-a-declaration-order-test-is-a-gate-only-if-something-reads-that-order.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "그 속성을 보일 수 없는 대역 위에서 통과한 테스트는 증인이 아니다", - "kind": "reference", - "slug": "a-fake-that-cannot-show-the-property-is-not-a-witness", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-inbox-jdbc-postgresql` §17", - "`final/document.md#a19-messaging-admin-runtime` §17 P1" - ], - "classification": "테스트 대역이 실제 구현의 불변식을 재현하지 못하면, 그 대역 위에서 통과한 단언은 그 불변식에 대해 아무 말도 하지 않는다. 이름이 속성을 주장할수록 그 공백은 커진다. 판정은 \"이 대역이 결함 있는 구현과 올바른 구현을 구분할 수 있는가\" 이고, 구분하지 못하면 그 테스트는 회귀를 잡지 못한다.", - "scope": [ - "저장소·브로커·파일 시스템처럼 상태를 갖는 협력자의 인메모리 대역 전부. 이 저장소의 사례 — bounded purge 대역이 `Math.min(unbounded(), limit)` 로 전부 지우고 숫자만 깎는 것, 리드라이브 대역의 `settle` 이 staged 목록을 줄이지 않는 것, 청구 대역의 `save` 가 INSERT 를 흉내 내 UPSERT 와의 차이를 가리는 것." - ], - "exceptions": [ - "대역이 협력자의 존재만 필요로 하는 테스트에는 해당하지 않는다. 구분 기준은 \"단언하는 속성이 그 협력자의 상태 변화에 달려 있는가\" 이다." - ], - "relations": [ - "`case:the-cleanup-that-causes-the-outage-it-prevents`", - "`case:a-resumed-redrive-skips-what-it-could-not-move`", - "`case:assigned-id-turns-claim-into-upsert`", - "`reference:omission-that-passes-is-not-a-gate`" - ], - "listedInTree": false, - "publication": "초안", - "file": "retention-and-unbounded-growth/reference/reference-a-fake-that-cannot-show-the-property-is-not-a-witness.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - } - ], - "question": [], - "decision": [] - } - }, - "operator-approval-and-destructive-operations": { - "topic": "operator-approval-and-destructive-operations", - "title": "", - "readerQuestion": "", - "kinds": { - "case": [ - { - "title": "재개된 리드라이브가 옮기지 못한 메시지를 영구히 건너뛴다", - "kind": "case", - "slug": "a-resumed-redrive-skips-what-it-could-not-move", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-admin-runtime` §17 P1" - ], - "code": [ - "`.../messaging-admin-runtime/.../RedriveService.java`(`resumeFrom`", - "`subList`)", - "`.../DefaultMessagingAdminService.executeRedrive`", - "`.../RedriveResumptionTest.java:192-200`" - ], - "evidence": [ - "`evidence/raw/306-redrive-resume-skips-failed-items.txt`", - "`evidence/raw/302-redrive-result-predicate-uncalled.txt`" - ], - "classification": "재개 지점 `resumeFrom` 은 \"시도한 개수\"(`moved + failed`)인데, `subList` 로 건너뛰는 대상은 매번 새로 peek 한 목록이고 그 목록에서 사라진 것은 성공한 것뿐이다. 실패분과 미시도분이 앞쪽에 남아 있으므로 건너뛰기가 정확히 그것들을 지운다. 결과: 리드라이브가 성공으로 보고되고, 승인이 소진되고, 일부 메시지가 DLQ 에 남으며, 어떤 기록도 그것들을 지목하지 않는다. 사건 복구 중에 실행되는 작업이라는 점이 심각도를 올린다. 그리고 이 결함을 잡는 술어가 이미 존재한다 — `RedriveResult.isFullyAccounted()` 의 production 호출부가 0 이다. 회귀 테스트가 성립하려면 대역도 고쳐야 한다. 현재 대역의 `settle` 은 `settled` 에 추가만 하고 `staged` 를 줄이지 않아, 실제 불변식인 \"정착된 것은 다음 peek 에서 사라진다\" 를 재현하지 못한다.", - "missing-verification": "실제 브로커로 리드라이브를 재개해 관측하지 않았다. 인덱스 계산과 peek 결과의 변화로 판정했다", - "relations": [ - "`concept:approval-verification-execution`", - "`reference:resume-by-identity-not-by-index`", - "`reference:a-fake-that-cannot-show-the-property-is-not-a-witness`", - "`reference:unknown-is-a-third-result`" - ], - "listedInTree": false, - "publication": "초안", - "file": "operator-approval-and-destructive-operations/case/case-a-resumed-redrive-skips-what-it-could-not-move.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a-resumed-redrive-skips-what-it-could-not-move" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a-resumed-redrive-skips-what-it-could-not-move.txt" - ] - }, - { - "title": "위조 가능한 승인이 하필 되돌릴 수 없는 작업 쪽에만 남았다", - "kind": "case", - "slug": "the-forgeable-approval-survived-on-the-irreversible-half", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-admin-runtime` §17", - "`final/document.md#a19-messaging-admin-api` §17" - ], - "code": [ - "`.../messaging-admin-runtime/.../DestructiveMessagingAdmin.java:23-38`(`Approved`)", - "`.../messaging-admin-api/.../VerifiedApproval.java:9-13`" - ], - "evidence": [ - "`evidence/raw/307-destructive-approval-not-verified.txt`" - ], - "classification": "승인된 계획을 위조할 수 없게 만드는 수정이 `REPLAY` 와 `REDRIVE` 에는 적용됐고 `PURGE` · `DELETE_DESTINATION` · `OFFSET_RESET` 에는 적용되지 않았다. 남은 쪽의 `Approved` record 는 public 생성자를 갖고 생성자가 null 과 음수만 본다 — 그 승인이 이 작업을 인가하는지, 이 목적지를 인가하는지, 영향 메시지 수가 승인 상한 이하인지 아무것도 검사하지 않고 계획 다이제스트 필드 자체가 없다. 방향이 뒤집혀 있다는 것이 이 사례의 요점이다. 적용된 두 작업은 복구 가능하고, 빠진 세 작업은 복구 불가능하다. 현재 구현체가 0 건이라 실행되는 결함은 아니지만, 이 인터페이스는 운영자 도구가 구현하라고 존재하는 것이고 그 도구가 생기는 순간의 모양이 이것이다.", - "missing-verification": "없음 — 두 record 의 생성자와 적용 범위를 대조했다", - "relations": [ - "`concept:approval-verification-execution`", - "`reference:the-powerful-half-must-not-be-one-setting-away`", - "`reference:signing-and-verifying-do-not-share-an-object`", - "`case:apply-is-a-setting-approval-is-not`" - ], - "listedInTree": false, - "publication": "초안", - "file": "operator-approval-and-destructive-operations/case/case-the-forgeable-approval-survived-on-the-irreversible-half.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "the-forgeable-approval-survived-on-the-irreversible-half" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/the-forgeable-approval-survived-on-the-irreversible-half.txt" - ] - }, - { - "title": "BLOCKING 이면 기동이 실패한다는 보장이 어떤 배선에서도 실행되지 않는다", - "kind": "case", - "slug": "blocking-means-startup-fails-and-nothing-runs-it", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-admin-api` §17 P2" - ], - "code": [ - "`.../messaging-admin-api/.../TopologyIssue.java`(`Severity.BLOCKING`)", - "`.../TopologyValidationReport.java`(`requireAcceptable`)", - "`.../MessagingAdminDurabilityValidator.java`(대조군)" - ], - "evidence": [ - "`evidence/raw/302-messaging-admin-service-unwired.txt`" - ], - "classification": "`BLOCKING` 의 javadoc 이 \"The destination cannot deliver a declared guarantee; startup must fail\" 이라고 적고, `requireAcceptable()` 의 javadoc 이 \"Fails startup when any blocking issue was found\" 라고 적는다. 그 메서드의 production 호출부가 0 이고, 그것을 부를 수 있는 유일한 진입점도 호출부가 0 이며, 그 서비스 빈을 스타터가 만들지 않는다. 결과: 복제 계수 1인 목적지에 내구성을 선언해도 컨텍스트는 정상 기동한다. 타입은 그 상황을 정확히 표현할 수 있고 표현한 것을 아무도 읽지 않는다. 고치는 방법이 같은 리프에 이미 있다 — `MessagingAdminDurabilityValidator` 가 `InitializingBean` 으로 저널 내구성을 기동 시점에 검사하고 실패시킨다. 같은 모양의 빈 하나면 된다. 이 항목이 무거운 이유는 이 리프가 `app-bootstrap` 소속이고 보장이 문서·타입·테스트 세 겹으로 존재하는데 배선만 없다는 점이다 — 읽는 사람은 보장이 있다고 믿을 근거를 세 개 갖는다. 그리고 토폴로지 검증 스택이 이 리프에 두 벌 있고 파티션 스케일업에 대한 판정이 서로 반대라, 배선하는 순간 어느 스택을 고르느냐가 스케일업한 배포의 기동 여부를 가른다.", - "missing-verification": "컨텍스트를 세워 기동 성공을 관측하지 않았다. 호출부 부재로 판정했다", - "relations": [ - "`concept:approval-verification-execution`", - "`reference:a-validator-is-enforced-by-injection`", - "`reference:the-startup-validator-follows-the-autoconfiguration-root`", - "`case:startup-validator-is-the-only-reader-of-four-keys`" - ], - "listedInTree": false, - "publication": "초안", - "file": "operator-approval-and-destructive-operations/case/case-blocking-means-startup-fails-and-nothing-runs-it.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "blocking-means-startup-fails-and-nothing-runs-it" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/blocking-means-startup-fails-and-nothing-runs-it.txt" - ] - } - ], - "concept": [ - { - "title": "승인·검증·실행의 분리와 그것을 타입으로 표현하기", - "kind": "concept", - "slug": "approval-verification-execution", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-admin-api` §4", - "§17" - ], - "code": [ - "`.../messaging-admin-api/.../VerifiedApproval.java`", - "`.../ApprovalGrant.java`(`canonicalForm`)", - "`.../HmacApprovalVerifier.java`", - "`.../DestructiveOperationGuard.java`" - ], - "classification": "운영자 도구가 파괴적 작업을 부를 때 세 가지가 분리되어야 한다. 승인을 발급하는 능력, 그 승인이 진짜인지 검증하는 능력, 그리고 작업을 실행하는 능력. 이 리프가 그 분리를 타입으로 표현한 이력이 javadoc 에 남아 있다 — 예전에는 승인된 계획 타입이 public 생성자를 가진 평범한 record 라서 \"이 계획은 승인됐다\" 가 호출자가 자기에 대해 한 주장이었고, 실행 메서드에 닿을 수 있는 코드는 무엇이든 승인을 지어낼 수 있었다. 그 수정이 `VerifiedApproval` 이다 — 검증을 통과했다는 사실 자체를 타입으로 만들어 생성자를 막았다. 가드는 네 조건이 아니라 여섯을 본다(작업 종류 일치와 출처 일치가 javadoc 목록에 빠져 있다). 그중 다섯 번째의 인라인 주석이 이 개념의 요점을 말한다 — \"A guard that only checks presence and window lets a verified redrive approval authorise a destination deletion.\"", - "missing-verification": "이 표면 전체에 production 소비자가 없어 실행으로 확인한 것이 없다", - "relations": [ - "`case:the-forgeable-approval-survived-on-the-irreversible-half`", - "`case:blocking-means-startup-fails-and-nothing-runs-it`", - "`reference:signing-and-verifying-do-not-share-an-object`", - "`reference:the-powerful-half-must-not-be-one-setting-away`" - ], - "listedInTree": false, - "publication": "초안", - "file": "operator-approval-and-destructive-operations/concept/concept-approval-verification-execution.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "approval-verification-execution", - "approval-verification-execution-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/approval-verification-execution.txt" - ] - } - ], - "reference": [ - { - "title": "서명 능력과 검증 능력은 같은 객체에 두지 않는다", - "kind": "reference", - "slug": "signing-and-verifying-do-not-share-an-object", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-admin-api` §17" - ], - "classification": "대칭키 서명에서는 검증하려면 서명할 수 있는 키를 가져야 한다. 그러므로 승인을 검증하는 프로세스는 정의상 승인을 발급할 수 있고, 그 프로세스가 운영자 도구라면 \"운영자가 자기 승인을 지어낼 수 없다\" 는 성립하지 않는다. 판정은 객체가 아니라 키의 소재로 한다 — 검증하는 쪽이 서명 키를 쥐는가.", - "scope": [ - "승인·토큰·커서처럼 발급자와 검증자가 다른 모든 서명. 두 방향의 해법이 있다. 비대칭 서명으로 바꿔 검증 측이 공개키만 갖게 하거나, 발급자 타입을 분리해 \"누가 서명 능력을 쥐는가\" 를 타입에 드러낸다. 후자를 고를 때 정규 형식은 한 곳에 남겨야 두 번째 구현이 드리프트하지 않는다." - ], - "exceptions": [ - "발급과 검증이 같은 신뢰 경계 안에서만 일어나면 분리가 필요 없다. 다만 그 경계는 코드가 아니라 배포가 정하므로, 경계가 바뀔 수 있으면 지금 분리해 두는 편이 싸다." - ], - "relations": [ - "`concept:approval-verification-execution`", - "`case:the-forgeable-approval-survived-on-the-irreversible-half`", - "`decision:legacy-adoption-requires-two-approvers`", - "`reference:a-binary-approval-codec-must-round-trip`" - ], - "listedInTree": false, - "publication": "초안", - "file": "operator-approval-and-destructive-operations/reference/reference-signing-and-verifying-do-not-share-an-object.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "재개는 인덱스가 아니라 신원으로 한다", - "kind": "reference", - "slug": "resume-by-identity-not-by-index", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-admin-runtime` §17 P1" - ], - "classification": "중단된 배치 작업을 \"몇 개까지 했다\" 로 재개하면, 재개 시점의 목록이 처음과 같아야 그 숫자가 유효하다. 목록이 처리 결과에 따라 줄어드는 종류라면 그 전제가 깨지고, 건너뛴 구간은 처리되지 않은 항목이 된다. 판정은 \"재개 시점에 목록을 다시 만드는가\" 이고, 다시 만든다면 재개 지점은 개수가 아니라 이미 처리한 항목의 신원이거나 브로커 오프셋이어야 한다.", - "scope": [ - "리드라이브·재생·마이그레이션처럼 승인과 저널을 갖는 모든 재개 가능 작업. 저널이 개수만 들고 있으면 필드를 늘려야 하고, 그것이 이 규칙의 실제 비용이다." - ], - "exceptions": [ - "목록이 불변이면 인덱스로 충분하다. 다만 \"불변\" 은 재개 사이에 다른 생산자가 없다는 뜻이며, DLQ 처럼 계속 유입되는 대상에는 성립하지 않는다." - ], - "relations": [ - "`case:a-resumed-redrive-skips-what-it-could-not-move`", - "`reference:unknown-is-a-third-result`", - "`reference:a-fake-that-cannot-show-the-property-is-not-a-witness`" - ], - "listedInTree": false, - "publication": "초안", - "file": "operator-approval-and-destructive-operations/reference/reference-resume-by-identity-not-by-index.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - } - ], - "question": [], - "decision": [] - } - }, - "failure-category-across-adapters": { - "topic": "failure-category-across-adapters", - "title": "", - "readerQuestion": "", - "kinds": { - "case": [ - { - "title": "권한 거부가 보안이 아니라 구성 오류로 기록된다", - "kind": "case", - "slug": "an-authorization-denial-recorded-as-a-configuration-error", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-security` §17 P2" - ], - "code": [ - "`.../messaging-security/.../DestinationAccessValidator.java`(`requirePublish`)", - "`.../messaging-runtime-core/.../DefaultMessagePublisher.java:104-116`(`rejected`)" - ], - "evidence": [ - "`evidence/raw/287-messaging-security-access-path.txt`" - ], - "classification": "권한 거부를 `AUTHORIZATION` 범주로 기록하는 코드가 있고 소비자가 0 이다. 실제 발행 경로는 접근 정책을 직접 묻고 거절을 만드는데, 그 거절 헬퍼가 붙이는 범주는 `CONFIGURATION` 이다. `FailureCategory` 는 \"재시도 엔진과 DLQ 라우터와 대시보드가 함께 합의하는 안정된 분류\" 로 정의되어 있으므로, 권한 거부가 구성 오류로 분류되면 보안 대시보드가 그것을 보지 못하고 구성 오류 알림이 권한 거부로 오염된다. 그리고 `AUTHORIZATION` 을 쓰는 유일한 코드가 미사용 클래스 안에 있다 — 올바른 분류를 아는 코드와 실행되는 코드가 서로 다른 파일이다.", - "missing-verification": "실제 거부를 발생시켜 대시보드 분류를 관측하지 않았다. 두 경로의 범주 상수 대조로 판정했다", - "relations": [ - "`reference:a-category-is-a-contract-between-retry-dlq-and-dashboard`", - "`reference:check-which-duplicate-is-wired`", - "`concept:transport-failure-stage-and-category`" - ], - "listedInTree": false, - "publication": "초안", - "file": "failure-category-across-adapters/case/case-an-authorization-denial-recorded-as-a-configuration-error.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "an-authorization-denial-recorded-as-a-configuration-error" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/an-authorization-denial-recorded-as-a-configuration-error.txt" - ] - }, - { - "title": "종료 중이라는 사정이 업무의 영구 실패로 분류된다", - "kind": "case", - "slug": "a-closing-transport-reported-as-a-permanent-business-failure", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-nats-experimental` §17.2", - "`final/document.md#a19-messaging-pulsar-experimental` §17.2" - ], - "code": [ - "`.../messaging-nats-experimental/.../NatsJetStreamTransport.java`(`rejectedLocally`)", - "`.../messaging-pulsar-experimental/.../PulsarMessagingTransport.java`(`rejectedLocally`)" - ], - "evidence": [ - "없음 — 두 헬퍼의 고정 범주와 호출자 목록 대조로 판정했다" - ], - "classification": "두 실험 어댑터가 같은 형태로 로컬 거절 헬퍼를 갖고, 그 헬퍼가 `FailureCategory.PERMANENT_BUSINESS` 를 고정으로 붙인다. 호출자가 둘씩이다 — 적재물이 상한을 넘은 경우와 전송이 종료 중인 경우. 첫째는 영구 업무 실패가 맞다. 둘째는 아니다. 종료 중이라는 것은 이 세대의 사정이고, 다음 세대나 다른 인스턴스에서는 같은 메시지가 정상 발행된다. 영구 업무 실패로 분류되면 재시도 엔진이 재시도하지 않고 DLQ 라우터가 그것을 최종 실패로 처리한다. 같은 파일의 실패 분류기는 범주를 신중히 나눈다 — 사전 거절은 `CONFIGURATION`, 모호는 `TRANSIENT_INFRASTRUCTURE`. 종료만 그 규율 밖에 있고, 두 리프가 같은 형태를 공유하므로 수정도 함께 해야 한다.", - "missing-verification": "종료 중 발행을 실행해 재시도 엔진의 판정을 관측하지 않았다", - "relations": [ - "`reference:a-category-is-a-contract-between-retry-dlq-and-dashboard`", - "`reference:this-generations-circumstance-is-not-a-permanent-failure`", - "`reference:retryability-needs-both-idempotency-and-category`" - ], - "listedInTree": false, - "publication": "초안", - "file": "failure-category-across-adapters/case/case-a-closing-transport-reported-as-a-permanent-business-failure.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "a-closing-transport-reported-as-a-permanent-business-failure" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/a-closing-transport-reported-as-a-permanent-business-failure.txt" - ] - } - ], - "concept": [], - "reference": [ - { - "title": "실패 범주는 재시도·DLQ·대시보드 사이의 계약이다", - "kind": "reference", - "slug": "a-category-is-a-contract-between-retry-dlq-and-dashboard", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-core-api` §4.12", - "`final/document.md#a19-messaging-security` §17" - ], - "classification": "실패 범주는 로그 문자열이 아니라 세 소비자가 각자 다른 행동을 고르는 입력이다. 재시도 엔진은 재시도 여부를, DLQ 라우터는 최종 처리를, 대시보드는 어느 담당자에게 보일지를 그 값으로 정한다. 그러므로 범주를 고르는 것은 \"이 실패를 뭐라고 부를까\" 가 아니라 \"이 세 소비자가 각각 무엇을 해야 하는가\" 를 동시에 정하는 결정이다. 판정은 범주를 붙이는 자리마다 세 질문에 답해 보는 것이다.", - "scope": [ - "어댑터가 만드는 모든 실패 서술자. 범주가 헬퍼 안에 고정되어 있으면 그 헬퍼의 호출자 전부에 대해 같은 답이 성립하는지 확인해야 한다 — 호출자가 둘 이상이면 대개 성립하지 않는다." - ], - "exceptions": [ - "분류할 수 없는 실패는 억지로 좁히지 말고 가장 보수적인 범주로 둔다. 이 저장소의 기본값이 일시적 인프라 실패인 것이 그 선택이다." - ], - "relations": [ - "`case:an-authorization-denial-recorded-as-a-configuration-error`", - "`case:a-closing-transport-reported-as-a-permanent-business-failure`", - "`concept:transport-failure-stage-and-category`", - "`reference:retryability-needs-both-idempotency-and-category`" - ], - "listedInTree": false, - "publication": "초안", - "file": "failure-category-across-adapters/reference/reference-a-category-is-a-contract-between-retry-dlq-and-dashboard.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - }, - { - "title": "이 세대의 사정은 업무의 영구 실패가 아니다", - "kind": "reference", - "slug": "this-generations-circumstance-is-not-a-permanent-failure", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-nats-experimental` §17.2", - "`final/document.md#a19-messaging-pulsar-experimental` §17.2" - ], - "classification": "종료 중 · 회전 중 · 재연결 중처럼 프로세스나 연결 세대의 상태 때문에 거절된 요청은 그 세대 밖에서 성립한다. 영구 실패는 요청 자체의 성질이어야 한다 — 적재물이 상한을 넘거나, 스키마가 맞지 않거나, 업무 규칙이 거절하는 경우다. 판정은 \"다음 세대에서 같은 요청이 성공하는가\" 이고, 성공한다면 그것은 일시적 실패다.", - "scope": [ - "로컬에서 만들어지는 모든 거절 — 전송 종료, 승인 거부, 자격증명 회전 중 거절. 거절 헬퍼가 범주를 고정으로 들고 있으면 호출자마다 이 질문을 다시 해야 한다." - ], - "exceptions": [ - "전송되지 않았다는 증거는 세대와 무관하게 유효하다. 범주가 틀렸다고 해서 전송 증거까지 바꾸면 안 된다 — 두 축은 독립이다." - ], - "relations": [ - "`case:a-closing-transport-reported-as-a-permanent-business-failure`", - "`reference:a-category-is-a-contract-between-retry-dlq-and-dashboard`", - "`decision:three-axes-of-evidence`" - ], - "listedInTree": false, - "publication": "초안", - "file": "failure-category-across-adapters/reference/reference-this-generations-circumstance-is-not-a-permanent-failure.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - } - ], - "question": [], - "decision": [] - } - }, - "cross-leaf-integration-facts": { - "topic": "cross-leaf-integration-facts", - "title": "", - "readerQuestion": "", - "kinds": { - "case": [ - { - "title": "messaging 마이그레이션 스트림을 적용하는 곳이 없고, 적용하려는 순간 버전이 충돌한다", - "kind": "case", - "slug": "messaging-migration-stream-has-no-applier-and-collides-on-adoption", - "readiness": "READY", - "source": [ - "`final/document.md#a19#L860`" - ], - "owning-module": "`integration/19-messaging-platform` · priority: `P2`", - "classification": "리프 하나만 읽어서는 보이지 않는 사실이다. 두 messaging 리프가 각자 마이그레이션을 갖는데 그 스트림을 적용하는 Flyway location 이 어떤 컴포지션에도 없다. 그리고 적용하려는 순간 두 리프가 같은 디렉터리에서 같은 버전 번호를 만들어 둔 것이 드러난다 — 즉 배선 부재가 번호 충돌을 가려 왔다. 두 사실이 한 사건인 이유는 순서다. 적용이 시작되는 날 첫 실패가 충돌이고, 그때까지는 어느 쪽도 관측되지 않는다.", - "missing-verification": "가족 문서의 판정을 옮긴 것이다. 실행 확인은 그 문서의 §확인하지 못한 것이 소유한다.", - "relations": [ - "`case:messaging-migrations-collide-at-v2`" - ], - "listedInTree": false, - "publication": "초안", - "file": "cross-leaf-integration-facts/case/case-messaging-migration-stream-has-no-applier-and-collides-on-adoption.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-migration-stream-has-no-applier-and-collides-on-adoption", - "messaging-migration-stream-has-no-applier-and-collides-on-adoption-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-migration-stream-has-no-applier-and-collides-on-adoption.txt" - ] - }, - { - "title": "claim-check는 starter에 배선 코드가 한 줄도 없다", - "kind": "case", - "slug": "claim-check-has-no-wiring-line-in-the-starter", - "readiness": "READY", - "source": [ - "`final/document.md#a19#L926`" - ], - "owning-module": "`integration/19-messaging-platform` · priority: `P3`", - "classification": "claim-check 리프는 `runtime_memberships` 가 출하 컴포지션이고 starter 의 허용 의존에도 들어 있다. 그런데 starter 에 저장소 구현도, 빈도, 오프로드를 부르는 발행 경로도 없다. 리프 SSOT 는 자기 안에서 '소비자 0' 까지만 말할 수 있고, '배포에는 실려 있다' 는 레지스트리와 starter 를 함께 읽어야 나온다.", - "missing-verification": "가족 문서의 판정을 옮긴 것이다. 실행 확인은 그 문서의 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "cross-leaf-integration-facts/case/case-claim-check-has-no-wiring-line-in-the-starter.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "claim-check-has-no-wiring-line-in-the-starter" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/claim-check-has-no-wiring-line-in-the-starter.txt" - ] - }, - { - "title": "admin 스위치가 가드를 켜고 서비스는 켜지 않는다", - "kind": "case", - "slug": "the-admin-switch-turns-on-the-guard-and-not-the-service", - "readiness": "READY", - "source": [ - "`final/document.md#a19#L975`" - ], - "owning-module": "`integration/19-messaging-platform` · priority: `P2`", - "classification": "운영자가 admin 기능을 켜는 프로퍼티 하나가 파괴적 작업 가드를 활성화한다. 같은 스위치가 그 가드를 실제로 부르는 admin 서비스 빈은 만들지 않는다. 결과적으로 스위치는 '켜졌다' 는 상태를 만들고 그 상태를 소비하는 경로가 없다. 두 리프의 SSOT 를 겹쳐야만 보이는 형태다 — 한쪽은 가드의 조건을, 다른 쪽은 서비스 빈의 부재를 소유한다.", - "missing-verification": "가족 문서의 판정을 옮긴 것이다. 실행 확인은 그 문서의 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "cross-leaf-integration-facts/case/case-the-admin-switch-turns-on-the-guard-and-not-the-service.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "the-admin-switch-turns-on-the-guard-and-not-the-service", - "the-admin-switch-turns-on-the-guard-and-not-the-service-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/the-admin-switch-turns-on-the-guard-and-not-the-service.txt" - ] - }, - { - "title": "`messaging-admin-api`는 main 25파일 · 1,613 LOC에 테스트 파일이 1개다", - "kind": "case", - "slug": "twenty-five-main-files-and-one-test-file", - "readiness": "READY", - "source": [ - "`final/document.md#a19#L997`" - ], - "owning-module": "`integration/19-messaging-platform` · priority: `P3`", - "classification": "가족 관점에서만 나오는 비율 관측이다. 이 리프의 운영자 표면 전체가 테스트 한 파일에 기대고 있고, 그 파일이 겨냥하지 않는 타입들이 §17 에서 각각 미검증으로 잡힌다. 리프 SSOT 는 개별 타입의 미검증을 말하고, 이 관측은 그것들이 한 원인에서 나온다는 것을 말한다.", - "missing-verification": "가족 문서의 판정을 옮긴 것이다. 실행 확인은 그 문서의 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "cross-leaf-integration-facts/case/case-twenty-five-main-files-and-one-test-file.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "twenty-five-main-files-and-one-test-file" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/twenty-five-main-files-and-one-test-file.txt" - ] - }, - { - "title": "`MessagingPublicSurfaceContractTest`가 가족 밖(app-bootstrap)에 있다", - "kind": "case", - "slug": "the-public-surface-contract-test-lives-outside-the-family", - "readiness": "READY", - "source": [ - "`final/document.md#a19#L1114`" - ], - "owning-module": "`integration/19-messaging-platform` · priority: `P3`", - "classification": "messaging 가족의 공개 표면을 붙드는 계약 테스트가 그 가족이 아니라 컴포지션 리프에 있다. 그래서 messaging 리프만 빌드하는 경로에서는 그 계약이 돌지 않고, 가족 안의 어느 SSOT 도 자기 표면이 어디서 검증되는지 알 수 없다. 테스트의 소재가 곧 그 테스트가 도는 조건이라는 점이 이 관측의 요지다.", - "missing-verification": "가족 문서의 판정을 옮긴 것이다. 실행 확인은 그 문서의 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "cross-leaf-integration-facts/case/case-the-public-surface-contract-test-lives-outside-the-family.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "the-public-surface-contract-test-lives-outside-the-family" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/the-public-surface-contract-test-lives-outside-the-family.txt" - ] - } - ], - "concept": [], - "reference": [], - "question": [], - "decision": [] - } - }, - "identity-and-value-contracts": { - "topic": "identity-and-value-contracts", - "title": "", - "readerQuestion": "", - "kinds": { - "case": [], - "concept": [ - { - "title": "(8.2) 조건 형제 비교 — 두 개의 계약 강제 형태", - "kind": "concept", - "slug": "adapter-inbound-web-c19", - "readiness": "READY", - "source": [ - "`final/document.md#a14#L1555`" - ], - "owning-module": "`adapter-inbound-web`", - "classification": "web 쪽이 구조적으로 우월하다. `build.gradle`이 그 이유를 적는다 — \"a parity check that compares whatever happens to be present would report agreement across a matrix with a hole in it.\"", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "identity-and-value-contracts/concept/concept-adapter-inbound-web-c19.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-web-c19" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-web-c19.txt" - ] - }, - { - "title": "Confirmed — \"설계상 부재\" 주장 6건이 구현·정책 계층까지 일치한다", - "kind": "concept", - "slug": "adapter-outbound-cache-redis-c03", - "readiness": "READY", - "source": [ - "`final/document.md#a10#L235`" - ], - "owning-module": "`adapter-outbound-cache-redis`", - "classification": "주장이 API 문서에만 있는지 확인했다(`160-...` §8.4). `sdk/api` 전체에서 `SETNX`·`SETEX`·`PSETEX`·`ZREVRANGE`·`RPOPLPUSH`·`BRPOPLPUSH`·`GEORADIUS`가 등장하는 곳은 **\"없다\"고 적는 javadoc 네 줄뿐**이다. Lettuce 구현 계층에서 걸린 둘은 무해하다 — `HashOperationRequests:128`의 `HSETNX`(다른 명령이다), `WritePresence:6`의 주석(\"This is what replaces `SETNX` and `SETEX`\"). 명령 정책 SSOT(`redis-command-policy.yml`, 1,406줄)에서 `KEYS`는 **`risk: R4`, `support: BLOCKED`**이고, 파일 머리의 표에 따르면 `BLOCKED`의 access는 `NONE`이다. 같은 자리에 `RANDOMKEY`(R2 BLOCKED)·`DUMP`·`RESTORE`·`MIGRATE`·`SELECT`·`SWAPDB`도 BLOCKED다. 즉 raw gateway로도 `KEYS`에 닿을 수 없다. 정책 파일 자체의 구조는 sub-scope 05에서 다룬다 — 머리 주석이 \"Official server metadata … decides what a command *is*. This file decides what this SDK is willing to *do* with it. **The catalog drift gate compares the two and fails the build when the server grows a command this file has not judged.**\"라고 적는다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "identity-and-value-contracts/concept/concept-adapter-outbound-cache-redis-c03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-cache-redis-c03", - "adapter-outbound-cache-redis-c03-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-cache-redis-c03.txt" - ] - }, - { - "title": "(8.1) 도달성 — SSRF 가드가 도달하는 호출처 전수", - "kind": "concept", - "slug": "adapter-outbound-notification-c05", - "readiness": "READY", - "source": [ - "`final/document.md#a13#L703`" - ], - "owning-module": "`adapter-outbound-notification`", - "classification": "`NotificationEndpoints.requireExternallyRoutable`의 프로덕션 호출처는 **둘**뿐이다: 가드 자신의 javadoc이 지목하는 대상은 다른 둘이다: Web Push는 목록에 없다. §25.1.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "identity-and-value-contracts/concept/concept-adapter-outbound-notification-c05.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-notification-c05" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-notification-c05.txt" - ] - }, - { - "title": "(8.1) 도달성 — 공유 계약을 실제로 상속하는 어댑터", - "kind": "concept", - "slug": "adapter-outbound-notification-c06", - "readiness": "READY", - "source": [ - "`final/document.md#a13#L913`" - ], - "owning-module": "`adapter-outbound-notification`", - "classification": "**8종 중 3종.** 클래스 javadoc은 \"Subclasses supply an adapter and a fault harness; the assertions are here **so that a new provider cannot be added without answering the same three questions**\"라고 쓰지만, 상속을 강제하는 장치는 없다. 이 저장소는 같은 종류의 강제를 다른 곳에서는 만들어 두었다 — `EndpointGuardCallSiteTest`(가드가 호출처에서 실제로 도달하는가), `verifyNotificationApiSurface`(공개 타입 586개 스냅샷 고정). 여기에는 없다. `ContractAdapters`가 크로스-프로바이더 스위트에 등록하는 것은 **5종**(ses · twilio · apns · webpush · webhook)이다. 빠진 둘은 fcm과 smtp이고, 그것은 harness의 구조적 한계로 설명된다 — 스위트는 HTTP 루프백 서버 위에서 돌고, SMTP는 JavaMail 릴레이로, FCM은 `FcmGateway` 심으로 나간다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "identity-and-value-contracts/concept/concept-adapter-outbound-notification-c06.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-notification-c06" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-notification-c06.txt" - ] - }, - { - "title": "Confirmed — local-dev provider의 경로 방어와 publication", - "kind": "concept", - "slug": "adapter-outbound-objectstorage-c09", - "readiness": "READY", - "source": [ - "`final/document.md#a09#L601`" - ], - "owning-module": "`adapter-outbound-objectstorage`", - "classification": "`LocalObjectPathGuard`는 이 저장소에서 반복해 본 강한 형태다 — root 정규화 + `startsWith` 봉쇄 + root 자신 거부에 더해, 부모 경로를 **root부터 한 세그먼트씩 내려가며** 심링크와 비디렉터리를 거부하고(`createParentsWithoutLinks` / `rejectExistingLinks`), 대상 자신도 심링크면 거부한다. control key는 `control/v1/` 접두사 + `[a-z0-9._/-]+` + `//`·`/./`·`/../` 금지 + 세그먼트별 재검사다. 그리고 control 레코드는 물리 파일명에 `.record`를 붙인다 — 객체 저장소가 허용하는 `reference`와 `reference/lifecycle` 쌍이 파일시스템에서 파일/디렉터리 충돌을 일으키지 않도록. 논리 키는 그대로 유지된다. 발행은 **배타적 하드링크**다. `LocalDevObjectDataStore.create`가 임시 파일에 쓰고 `channel.force(true)` 후 `Files.createLink(target, temporary)`를 하며, `FileAlreadyExistsException`을 `CONFLICT`로, `UnsupportedOperationException`을 \"local filesystem cannot prove immutable create\"로 번역한다 — 하드링크를 지원하지 않는 파일시스템에서 조용히 약한 방식으로 내려가지 않는다. 앞선 `Files.exists(NOFOLLOW)` 검사는 빠른 경로일 뿐이고 배타성은 `createLink`가 준다. POSIX면 소유자 읽기 전용 권한을 씌운다. test가 이것들을 이름으로 잡는다 — `traversalAbsoluteUnicodePercentAndSymlinkEscapesAreRejected`, `exclusiveCreateRaceHasOneWinner`, `injectedDiskFailureLeavesNoFinalOrTemporaryData`, `restartInspectsCommittedDataWithoutReplayingProducer`, `corruptControlRecordRemainsPresentAn…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "identity-and-value-contracts/concept/concept-adapter-outbound-objectstorage-c09.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-objectstorage-c09" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-objectstorage-c09.txt" - ] - }, - { - "title": "(8.1)·(8.2) 도달성과 게이트", - "kind": "concept", - "slug": "app-bootstrap-c04", - "readiness": "READY", - "source": [ - "`final/document.md#a18#L386`" - ], - "owning-module": "`app-bootstrap`", - "classification": "7개 main 파일 전부 `@Configuration`이고 `@ConditionalOnProperty`/`@ConditionalOnBean`으로 게이트된다. `MongoPlatformHealthConfig`가 `@ConditionalOnBean` + `@ConditionalOnMissingBean` + `@ConditionalOnProperty` 셋을 함께 쓰는데, 자동설정 안에서의 `@ConditionalOnBean`은 Boot가 평가 순서를 통제하므로 모듈 14 §7.1이 경고한 컴포넌트 스캔 상의 위험이 없다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "identity-and-value-contracts/concept/concept-app-bootstrap-c04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "app-bootstrap-c04" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/app-bootstrap-c04.txt" - ] - }, - { - "title": "Stable 모듈 목록과 불변식", - "kind": "concept", - "slug": "grpc-core-api-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-core-api#L105`" - ], - "owning-module": "`grpc-core-api`", - "classification": "`GrpcStableModuleCatalog` 이 Stable 12 와 Advanced 6 을 상수로 든다. `GrpcStableBuildInvariant.advancedDependencyAllowed()` 가 인자를 받지 않는 이유가 적혀 있다. 그리고 누출을 던지지 않고 집합으로 돌려주는 이유도 적혀 있다 — 첫 하나만 보고하는 게이트는 넷을 지우는 일을 네 번의 대화로 만든다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "identity-and-value-contracts/concept/concept-grpc-core-api-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-core-api-c01" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-core-api-c01.txt" - ] - }, - { - "title": "계약·불변식·상태 모델", - "kind": "concept", - "slug": "messaging-testkit-c04", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-testkit#L169`" - ], - "owning-module": "`messaging-testkit`", - "classification": "7개 테스트와 각각이 못 박는 것: 계약을 `abstract class` + `@Test` 로 만든 결정의 효과는 `InMemoryHarnessContractTest` 의 javadoc 에 있다. 즉 어댑터가 `@Test` 를 **삭제하는 방법이 없다**. 상속받는 순간 7개가 전부 실행된다. 어댑터 쪽에서 하나를 빼려면 이 파일을 고쳐야 하고, 그것은 리뷰에 보인다. 그리고 그 7개가 침묵으로 줄어드는 것을 막는 자물쇠가 하나 더 있다. 리플렉션으로 `@Test` 가 붙은 메서드 이름 집합을 상수와 정확히 대조한다. 계약에서 테스트 하나를 지우면 이 테스트가 깨진다. 추가해도 깨진다. 계약의 크기 자체가 잠겨 있다. 5개 시나리오, 그리고 각각이 `rationale` 을 **비어 있으면 생성 자체가 실패하도록** 강제한다. `REJECTED` 가 `BEFORE_TRANSMISSION` 하나뿐이라는 사실이 테스트로 잠겨 있다(`CrossBrokerContractSuite.aFailureBeforeTransmissionIsTheOnlyOneReportedAsRejected`). `byName` 은 알 수 없는 이름을 건너뛰지 않고 거절한다. 이 리프에서 가장 밀도 높은 설계다. 두 javadoc 이 **자기가 고친 결함을 이름 붙여** 남겼다. 네 가지 결정이 한 문단에 압축되어 있고, 넷 다 코드에서 확인된다. **(a) 커밋한다.** `src/main/resources/messaging/broker-certification-evidence.jsonl`. `build/` 를 읽으면 깨끗한 체크아웃에서 답이 달라진다는 이유가 명시되어 있다. 확인: `src` 와 `build/resources` 사본이 diff 로 동일(`EVD-300`). **(b) 손으로 못 쓰게 하는 게이트.** `messaging-kafka/build.gradle:82` 의 `verifyMessagingCertificationEvidence`. `gitCommit` 과 `observedAt` 을 정규식으로 지우고 나머지 집합을 비교한다. 그 둘은 매 실행마다 달라지므로 비교 대상이 아니라는 주석이 붙어 있다. `outputs.upToDateWhen { false }…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "identity-and-value-contracts/concept/concept-messaging-testkit-c04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-testkit-c04", - "messaging-testkit-c04-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-testkit-c04.txt" - ] - } - ], - "reference": [], - "question": [], - "decision": [] - } - }, - "result-and-failure-algebra": { - "topic": "result-and-failure-algebra", - "title": "", - "readerQuestion": "", - "kinds": { - "case": [], - "concept": [ - { - "title": "(8.3) 중복 메커니즘 — 인증과 예외 처리의 인터셉터 순서", - "kind": "concept", - "slug": "adapter-inbound-grpc-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a15#L140`" - ], - "owning-module": "`adapter-inbound-grpc`", - "classification": "`ServerInterceptors.intercept(service, exceptionInterceptor, authenticationInterceptor)` — gRPC 규약상 **마지막 인터셉터의 `interceptCall`이 먼저** 호출되므로 인증이 바깥, 예외 처리가 안쪽이다. 인증 인터셉터가 예외 처리 바깥에 있는데도 안전한 이유는 그것이 스스로 예외를 삼키기 때문이다: CLAUDE.md의 약속(\"정책이 `false`를 반환하거나 예외를 던진 요청은 ... 안정적인 `UNAUTHENTICATED` status/code/category로 종료된다\")이 코드와 일치하고, 두 경우 모두 같은 `call.close(Status.UNAUTHENTICATED.withDescription(OperationalError.UNAUTHENTICATED.code()), trailersFor(...))`로 끝난다. 정책 진단은 클라이언트에 닿지 않는다. 중복 아님.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "result-and-failure-algebra/concept/concept-adapter-inbound-grpc-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-grpc-c01", - "adapter-inbound-grpc-c01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-grpc-c01.txt" - ] - }, - { - "title": "Confirmed — guard를 지나지 않는 경로가 하나 있고, 그것이 선언돼 있다", - "kind": "concept", - "slug": "adapter-outbound-cache-redis-c09", - "readiness": "READY", - "source": [ - "`final/document.md#a10#L477`" - ], - "owning-module": "`adapter-outbound-cache-redis`", - "classification": "`PubSubOperationRequests`의 javadoc이 예외를 스스로 밝힌다. 대체 검사가 실제로 있다. `channelTargets`·`shardTargets`·`patternTargets` 셋 다 빈 컬렉션을 거부하고 **모든 대상의 네임스페이스가 이 프로세스의 것과 같은지** 확인한다(`requireNamespace`, 다르면 \"channel belongs to a namespace this process may not use\"). `patternTargets`는 추가로 `context.sdkPermit(PATTERN_SUBSCRIBE)`를 호출한다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "result-and-failure-algebra/concept/concept-adapter-outbound-cache-redis-c09.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-cache-redis-c09" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-cache-redis-c09.txt" - ] - }, - { - "title": "Success / failure mechanics", - "kind": "concept", - "slug": "domain-core-c03", - "readiness": "READY", - "source": [ - "`final/document.md#a01#L158`" - ], - "owning-module": "`domain-core`", - "classification": "이 module의 runtime executable behavior는 매우 작다. annotation 자체에는 success/failure path가 없고, interface도 implementation을 가지지 않는다. 주요 failure mechanics는 **build-time architecture violation**이다. forbidden framework/domain dependency → `DOMAIN_IS_PURE` domain logger dependency → `DOMAIN_HAS_NO_LOGGER` public no-arg value object → `VALUE_OBJECTS_HAVE_NO_PUBLIC_NO_ARG_CONSTRUCTOR` public `set*` aggregate mutator → `AGGREGATE_ROOT_SETTERS_ARE_NOT_PUBLIC` non-record domain event → `DOMAIN_EVENTS_ARE_RECORDS` enumerated transport dependency → `DOMAIN_EVENTS_ARE_TRANSPORT_FREE` `id` field raw type not assignable to `ResourceId` → `NO_LONG_ID_PK` project dependency not in registry → `verifyCleanArchitectureDependencies` production -> `sample-portfolio` edge → settings registry validation and root dependency verification, plus cross-module ArchUnit rule", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "result-and-failure-algebra/concept/concept-domain-core-c03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "domain-core-c03" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/domain-core-c03.txt" - ] - }, - { - "title": "Git/설계 문서에서 확인한 변화와 실패 기록", - "kind": "concept", - "slug": "messaging-transport-spi-c07", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-transport-spi#L579`" - ], - "owning-module": "`messaging-transport-spi`", - "classification": "코드 주석이 네 결함을 보존한다. 전부 **장기 실행에서만 드러나는** 종류다. 네 번째와 `LeakTrackingRuntime.closeCount()` javadoc(\"Closing twice is as much a defect as never closing\")이 같은 주제를 반대편에서 말한다 — **해제는 정확히 한 번이어야 하고, 0번도 2번도 결함이다.**", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "result-and-failure-algebra/concept/concept-messaging-transport-spi-c07.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-transport-spi-c07" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-transport-spi-c07.txt" - ] - }, - { - "title": "실패 분류 — 타입 있는 신호만 본다", - "kind": "concept", - "slug": "messaging-pulsar-experimental-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-pulsar-experimental#L50`" - ], - "owning-module": "`messaging-pulsar-experimental`", - "classification": "javadoc 이 이전 구현과 그 결함을 적는다. 기본값이 모호로 바뀐 것이 핵심이다. 알 수 없는 실패에서 안전한 방향은 모호다. 확인된 성공은 복제 증거로 기록된다 — 전송 미래가 설정된 수의 저장 노드에 기록된 뒤에야 해소되므로 영수증이 아니라 복제 증거다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "result-and-failure-algebra/concept/concept-messaging-pulsar-experimental-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-pulsar-experimental-c01", - "messaging-pulsar-experimental-c01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-pulsar-experimental-c01.txt" - ] - }, - { - "title": "실패 경로와 복구/번역", - "kind": "concept", - "slug": "messaging-testkit-c06", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-testkit#L492`" - ], - "owning-module": "`messaging-testkit`", - "classification": "이 리프의 실패 처리 원칙은 하나다: **모르는 것을 아는 척하지 않는다.** 마지막 두 줄의 구분이 의도적이다. `CompatibilityMatrix.of(\"messaging-artemis\")` 는 던지고(`anUnknownAdapterIsNotSilentlyTreatedAsSupported`), `matrix.coverageOf(\"messaging-artemis\", …)` 는 `NOT_COVERED` 를 돌려준다(`aFaultThatWasNeverRecordedReadsAsUncoveredRatherThanPassing`). 전자는 \"지원 목록에 없는 것을 지원인 척\"을 막고, 후자는 \"기록 없음\"이 곧 \"커버 안 됨\"이라는 자연스러운 읽기다. `DockerAvailability` 는 반대 방향의 실패 처리다. `Class.forName(\"org.testcontainers.DockerClientFactory\")` 를 리플렉션으로 부르고 어떤 예외든 `false` 로 삼킨다(`:26-34`). 이 리프가 testcontainers 에 의존하지 않으면서 그 존재를 물어볼 수 있게 하는 유일한 방법이고, 결과를 `static final` 로 1회만 캐시한다. 주목할 점: **\"skip 은 성공이 아니다\"** 라는 반대 규칙이 인증 레인에는 적용되어 있다. 일반 컨테이너 스위트는 `DockerAvailability` 로 skip 하고, 인증 레인만 **가드 없이 실패**한다. 대신 `test` 태그에서 빼서 노트북 빌드를 깨지 않는다. 두 규칙이 충돌하지 않게 배치되어 있다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "result-and-failure-algebra/concept/concept-messaging-testkit-c06.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-testkit-c06" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-testkit-c06.txt" - ] - }, - { - "title": "Activation and health snapshot", - "kind": "concept", - "slug": "shared-contract-c03", - "readiness": "READY", - "source": [ - "`final/document.md#a02#L108`" - ], - "owning-module": "`shared-contract`", - "classification": "`MasterSwitchParser`는 unset=false, true/false case-insensitive만 허용하며 `yes`, `1`, `on`, whitespace-padded value를 invalid로 처리한다. canonical+legacy가 동시에 있으면 값이 같아도 ambiguous로 실패하고 legacy-only는 replacement property를 반환한다. 이는 operator configuration을 permissive coercion하지 않는 fail-closed contract다. `RedisHealthSnapshotProvider`는 Redis client/connection/credential을 shared boundary로 새지 않게 role/capability/state/reason/semantic freshness만 projection한다. eviction policy는 runtime CONFIG 조회 증명이 아니라 configured expectation임을 enum 이름과 javadoc으로 명시한다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "result-and-failure-algebra/concept/concept-shared-contract-c03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "shared-contract-c03" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/shared-contract-c03.txt" - ] - } - ], - "reference": [], - "question": [], - "decision": [] - } - }, - "state-machines-and-ownership": { - "topic": "state-machines-and-ownership", - "title": "", - "readerQuestion": "", - "kinds": { - "case": [], - "concept": [ - { - "title": "(8.2) 조건 형제 비교 — 두 능력 목록이 커서에 대해 다르게 답한다", - "kind": "concept", - "slug": "adapter-inbound-graphql-c12", - "readiness": "READY", - "source": [ - "`final/document.md#a16#L867`" - ], - "owning-module": "`adapter-inbound-graphql`", - "classification": "`GraphQlStableCapabilityManifest.STABLE`에 `SIGNED_CURSOR_CONNECTION`이 들어 있다. `CLAUDE.md` 등급표는 cursor 서명을 `modelled`(요청 경로에 없음)로 매긴다. 두 목록의 용도가 다르다 — 매니페스트는 `requireStable(capability)`로 **릴리스 게이트가 소비하는 기계 판정**이고, 등급표는 사람이 읽는 공시다. 그러나 같은 능력에 대해 하나는 \"Stable에서 지원\"이라 하고 하나는 \"요청 경로에 없음\"이라 한다. §29.2.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-inbound-graphql-c12.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-graphql-c12" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-graphql-c12.txt" - ] - }, - { - "title": "(8.1) 도달성 — 릴리스 게이트 자체", - "kind": "concept", - "slug": "adapter-inbound-graphql-c13", - "readiness": "READY", - "source": [ - "`final/document.md#a16#L873`" - ], - "owning-module": "`adapter-inbound-graphql`", - "classification": "`release` 9개 파일 전부 autoconf=0이다. 릴리스 게이트는 런타임 컴포넌트가 아니라 빌드·릴리스 시점 도구이므로 정상이다. 다만 `GraphQlReleaseReportWriter`(57)는 main_other=0 · test=1로, 게이트 결과를 기록할 작성기에 호출자가 없다. CLAUDE.md가 그 상태를 명시한다 — \"`graphqlPerformanceTest` 레인이 자리를 예약, **증거 없으면 릴리스 게이트가 거부**\". 즉 게이트는 CI 레인에서 호출되도록 설계됐다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-inbound-graphql-c13.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-graphql-c13" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-graphql-c13.txt" - ] - }, - { - "title": "(8.2) 조건 형제 비교 — 캐시 정책이 두 벌이다", - "kind": "concept", - "slug": "adapter-inbound-web-c09", - "readiness": "READY", - "source": [ - "`final/document.md#a14#L864`" - ], - "owning-module": "`adapter-inbound-web`", - "classification": "`@Component`이므로 컴포넌트 스캔이 잡고 Spring이 `Filter` 빈을 체인에 넣는다. **모든 응답에 `no-store`를 붙인다.** 배선되지 않은 것: `cache` 패키지 4개 파일 310 LOC. `web.cache.` 패키지를 참조하는 파일이 자기 패키지 밖에 **0개**다. `SecurityConfig:80`이 Spring Security의 기본 캐시 헤더 작성기를 끄면서 그 이유를 적는다 — \"`CacheControlFilter` **owns the cache header policy**\". 소유자는 24줄짜리 상수 두 개이고, 프로파일·지시자·`Vary` 규칙을 갖춘 310줄은 소유하지 않는다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-inbound-web-c09.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-web-c09" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-web-c09.txt" - ] - }, - { - "title": "(8.4) 카운트 드리프트 — 선언된 능력 11개, 활성화 게이트 2개", - "kind": "concept", - "slug": "adapter-inbound-web-c16", - "readiness": "READY", - "source": [ - "`final/document.md#a14#L1217`" - ], - "owning-module": "`adapter-inbound-web`", - "classification": "`WebAdvancedFeature`의 상수: `advanced/**` 전체에서 프로덕션 `@Configuration`은 셋이고(`MvcStreamingExecutorConfiguration` · `VirtualThreadMvcConfiguration` · `VirtualThreadSettings`) 실제 `@ConditionalOnProperty` 접두사는 둘이다: 나머지 아홉(`WEBFLUX_BLOCKING_BRIDGE` · `JSON_MERGE_PATCH` · `JSON_PATCH` · `SSE` · `JSON_SEQUENCE` · `FUNCTIONAL_WEBFLUX` · `CBOR` · `XML` · `RATELIMIT_DRAFT_HEADERS`)에는 프로퍼티도, `@Configuration`도, 빈도 없다. §36.1.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-inbound-web-c16.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-web-c16" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-web-c16.txt" - ] - }, - { - "title": "(8.3) 중복 메커니즘 — 승격 게이트", - "kind": "concept", - "slug": "adapter-inbound-websocket-c05", - "readiness": "READY", - "source": [ - "`final/document.md#a17#L420`" - ], - "owning-module": "`adapter-inbound-websocket`", - "classification": "`advanced/release/AdvancedPromotionGate`(120)와 `release/WebSocketStableReleaseGate`(106, §11)가 각각 Advanced 승격과 Stable 릴리스를 판정한다. 둘 다 main 참조 0이고 테스트만 있다 — 릴리스 시점 도구이므로 런타임 미배선이 정상이다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-inbound-websocket-c05.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-websocket-c05" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-websocket-c05.txt" - ] - }, - { - "title": "Confirmed — 두 프로그래밍 모델이 같은 request builder를 공유한다", - "kind": "concept", - "slug": "adapter-outbound-cache-redis-c08", - "readiness": "READY", - "source": [ - "`final/document.md#a10#L456`" - ], - "owning-module": "`adapter-outbound-cache-redis`", - "classification": "`ValueOperationRequests`의 javadoc이 불변식을 적는다 — \"Both the blocking and the reactive string operations call exactly these methods, so **a change to a permit, a budget, an encoding, or a command choice cannot apply to one API and not the other.**\" 구조가 그것을 보장한다. `LettuceRedisValueOperations`와 `LettuceReactiveRedisValueOperations`는 둘 다 생성자에서 `new ValueOperationRequests(gateway, context, counters)`를 만들고, 차이는 `SyncRedisCommandExecutor` vs `ReactiveRedisCommandExecutor` 하나뿐이다. 각 메서드는 `executor.execute(requests.xxx(...))` 한 줄이고, reactive 쪽은 그 위에 `flatMap`/`then` 같은 형태 변환만 얹는다. 11개 계열 전부에서 확인했다(`162-...` §8.2) — Value·Hash·List·Set·SortedSet·Key·Geo·Bitmap·Stream·HyperLogLog·PubSub 모두 sync와 reactive 양쪽이 같은 `*OperationRequests`를 생성한다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-cache-redis-c08.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-cache-redis-c08", - "adapter-outbound-cache-redis-c08-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-cache-redis-c08.txt" - ] - }, - { - "title": "`CommandPolicyGuard` — 순서가 고정된 단일 입장 지점", - "kind": "concept", - "slug": "adapter-outbound-cache-redis-c10", - "readiness": "READY", - "source": [ - "`final/document.md#a10#L560`" - ], - "owning-module": "`adapter-outbound-cache-redis`", - "classification": "javadoc이 순서와 그 이유를 적는다 — \"Validation order is fixed and **each step is cheaper than the one after it**, so an obviously inadmissible command is refused before anything is encoded or sent.\" 각 단계가 구체적이다. `requireReachable`은 `BLOCKED`거나 `access == NONE`이면 거부하고 R3/R4를 애플리케이션 경로에서 배제한다. `requireCapability`는 명령의 최소 버전을 프로브된 서버 버전과 대조한다. `requireNamespace`는 모든 키의 네임스페이스를 확인하고 렌더까지 수행한다. `requireSameSlot`은 Cluster에서 두 개 이상 슬롯이면 `RedisCrossSlotException`을 **서버를 부르기 전에** 던진다. `effectiveTimeout`은 블로킹 명령이 유한한 server block을 선언하지 않으면 거부하고, 설정 상한을 넘으면 거부하며, 통과하면 `BLOCKING_MARGIN`(2초)을 더한다. 이 클래스에는 두 개의 수정 이력이 주석으로 남아 있고, 둘 다 이 저장소에서 반복해 본 종류다. **(a) 죽은 중복 mechanism을 지운 기록.** `validateReply(...)`가 있었고 아무도 부르지 않았다. **(b) 절대 발화하지 못하던 조건.** 다중 키 permit 검사가 advanced permit 검사와 한 조건으로 접혀 있었고, \"둘 다 없음\"이 위에서 이미 던지므로 다중 키 절은 도달 불가였다 — \"set algebra over any number of keys was admitted on an advanced permit alone.\" 지금은 `request.keys().size() > 1 && request.multiKeyPermit().isEmpty()`가 독립 조건이다. test `rejectsAMultiKeyCommandCarryingOnlyAnAdvancedPermit`과 `aSingleKeyAdvancedCommandStillNeedsNoMultiKeyPermit`가 양쪽…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-cache-redis-c10.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-cache-redis-c10", - "adapter-outbound-cache-redis-c10-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-cache-redis-c10.txt" - ] - }, - { - "title": "Confirmed — 상태 전이가 인접 행렬이고 terminal이 진짜 terminal이다", - "kind": "concept", - "slug": "adapter-outbound-fileserver-c03", - "readiness": "READY", - "source": [ - "`final/document.md#a08#L184`" - ], - "owning-module": "`adapter-outbound-fileserver`", - "classification": "`validateOperationTransition`이 여섯 가지를 순서대로 강제한다: requestFingerprint 불변 → 불변 identity 10개 필드 불변 → `stateRevision` 감소 금지 → 동일 revision 다른 내용 금지 → **정확히 +1** 증가 → 인접 전이 행렬. 행렬은 README가 적은 사슬과 일치하고, 모든 비terminal 상태에서 `QUARANTINED`로만 이탈할 수 있으며 `PUBLISHED`·`QUARANTINED`는 후속 전이가 없다(`case PUBLISHED, QUARANTINED -> false`). 봉인 이후 사실은 얼어붙는다 — `requireSealedFactsUnchanged`가 byteSize·rowCount·columnCount·sha256·formulaMitigatedCount·sealedAt을 고정하고, `MANIFEST_PUBLISHED` 이후에는 `manifestDigest`, `REFERENCE_PUBLISHED` 이후에는 `referenceDigest`도 고정된다. `current.equals(candidate)`는 전이가 아니라 **복구(repair)**로 취급된다 — 부모 디렉터리를 다시 force하고 정확 read-back만 수행한다. 크래시 후 같은 레코드를 다시 쓰는 재시도가 conflict가 되지 않게 하는 처리이고, `parentForcedCallbackFailureIsRepairedByIdenticalOperationRetry`가 이를 고정한다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-fileserver-c03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-fileserver-c03", - "adapter-outbound-fileserver-c03-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-fileserver-c03.txt" - ] - }, - { - "title": "Confirmed — 두 개의 락 형태가 각자의 쓰기 원시연산에 맞춰져 있다", - "kind": "concept", - "slug": "adapter-outbound-fileserver-c04", - "readiness": "READY", - "source": [ - "`final/document.md#a08#L192`" - ], - "owning-module": "`adapter-outbound-fileserver`", - "classification": "한 클래스 안에 락이 두 종류다. 얼핏 비대칭으로 보이지만 각자의 커밋 방식이 다르다. 즉 배타성이 필요한 쪽(replace)에는 OS 락을 두고, 원시연산 자체가 배타적인 쪽(create-link)에는 JVM 스트라이프만 둔 것이다. 후자의 root 미포함은 **과잉 직렬화** 방향이라(다른 root의 같은 fileId가 같은 스트라이프를 공유) 배타 누락으로는 이어지지 않고, `fileId`는 `SecureRandom` 16바이트라 실질 충돌도 없다. 결함이 아니라 설계로 기록한다. collision 경로도 닫혀 있다 — `createLink`가 충돌하면 임시 파일을 정확히 지우고, 기존 레코드를 읽어 identity와 내용 동등성을 확인한 뒤 같으면 repair, 다르면 `CONFLICT`다. `concurrentCrossInstanceImmutableCollisionIsNeverClassifiedAsStorage`가 그 분류를 고정한다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-fileserver-c04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-fileserver-c04" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-fileserver-c04.txt" - ] - }, - { - "title": "Confirmed — payload 계층이 자신의 잔여 위험을 먼저 선언한다", - "kind": "concept", - "slug": "adapter-outbound-fileserver-c08", - "readiness": "READY", - "source": [ - "`final/document.md#a08#L558`" - ], - "owning-module": "`adapter-outbound-fileserver`", - "classification": "`LocalPersistentPayloadOperations`의 클래스 javadoc이 무엇이 서술자 상대이고 무엇이 아닌지를 앞에서 밝힌다. 전수 검사가 그 서술과 일치한다(`147-...` §8.2). 이 파일의 `Files.*` 호출은 정확히 그 셋 — `Files.createLink`(:941), `Files.createDirectory`(:802), 그리고 force/stat/FileStore 조회 — 뿐이고, 각각 앞뒤로 `fileKey`·소유자·권한·FileStore 재확인이 붙는다. JDK가 `linkat`/`mkdirat`를 노출하지 않으므로 서술자 상대 대응물이 없고, 그 사실을 숨기는 대신 적었다. **이것이 §32와의 차이다.** 여기서는 잔여 경로 연산이 (a) 문서에 선언되고 (b) identity 검사로 감싸인다. `AtomicMoveContentPublisher`의 발행 rename은 (a) 어디에도 선언되지 않고 (b) 같은 모듈이 \"a precheck could only ever approximate\"라고 적은 사전검사 하나로만 보호된다. 같은 저장소가 같은 문제를 한 번은 정직하게, 한 번은 그렇지 않게 다룬 대비다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-fileserver-c08.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-fileserver-c08", - "adapter-outbound-fileserver-c08-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-fileserver-c08.txt" - ] - }, - { - "title": "`ClientRuntimeRegistry` — 세대 교체가 틈으로 관측되지 않는다", - "kind": "concept", - "slug": "adapter-outbound-httpclient-c02", - "readiness": "READY", - "source": [ - "`final/document.md#a11#L112`" - ], - "owning-module": "`adapter-outbound-httpclient`", - "classification": "\"A swap publishes the replacement first and drains the predecessor afterwards, so a rotation is never observable as a gap.\" `acquire`는 관측한 세대가 예약 직전에 draining으로 넘어가면 **새로 발행된 세대에 대해 재시도**한다. 과거 누수 두 건이 코드와 주석에 남아 있다. 교체된 세대가 `runtimes`에서 빠지고 스케줄된 drain 작업만 소유하게 되어, 그 작업이 발화하기 전에 레지스트리가 닫히면 \"leaked the whole generation — and the resource-bound suite could not see it, because nothing enumerated it.\" 지금은 `retired` 집합이 추적한다. `close()`가 `forEach`로 닫다가 첫 예외에서 멈춰 \"a single misbehaving pool left every remaining connection, thread and socket open — **shutdown leaked more the worse the failure was.**\" 지금은 전부 닫고 실패를 suppressed로 모은다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-httpclient-c02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-httpclient-c02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-httpclient-c02.txt" - ] - }, - { - "title": "재시도 결정표가 순서로 표현돼 있다", - "kind": "concept", - "slug": "adapter-outbound-httpclient-c04", - "readiness": "READY", - "source": [ - "`final/document.md#a11#L305`" - ], - "owning-module": "`adapter-outbound-httpclient`", - "classification": "`DefaultRetryEligibilityEngine.decide`의 javadoc이 규칙이다 — \"The order is the point. Cheap absolute blockers come first (attempts, budget, replayability, first byte, deadline, draining), then ambiguity, then status- and failure-specific rules. **A later rule can never re-enable something an earlier rule forbade.**\" 절대 차단 여섯이 먼저다 — 시도 수 소진 · 예산 소진 · 본문 재생 불가 · **첫 바이트 전달됨** · 런타임 draining · 남은 deadline이 최소 시도 예산 이하. 그다음 영구 실패 범주, 그다음 증거, 그다음 상태/실패별. 상태별 규칙에 수정 이력이 붙어 있다. 그리고 `RetryContext.safelyIdempotent()`가 이 모듈의 D-09를 구현한다 — HTTP 메서드는 `RetryContext`에 **아예 없다**(\"so a POST with a registered idempotency key and a GET against a non-idempotent RPC endpoint are both handled correctly instead of by method-name folklore\"). 키 기반 멱등성은 **키가 실제로 전송됐는지**까지 요구한다. test `aKeyThatWasNeverSentDoesNotMakeARepeatSafe`가 그것을 고정한다. `Retry-After`는 남은 deadline 안에 들어갈 때만 존중된다(`allowWithin`), 그리고 존중된 `Retry-After`는 `maxBackoff`로 잘리지 **않는다** — 잘라 버리면 업스트림이 요청한 대기보다 일찍 다시 두드리게 되기 때문이다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-httpclient-c04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-httpclient-c04", - "adapter-outbound-httpclient-c04-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-httpclient-c04.txt" - ] - }, - { - "title": "계약이 컴파일되어 닫힌다", - "kind": "concept", - "slug": "adapter-outbound-messaging-c03", - "readiness": "READY", - "source": [ - "`final/document.md#a12#L237`" - ], - "owning-module": "`adapter-outbound-messaging`", - "classification": "`ContractCatalogCompiler`가 **정확한 record 타입 토큰**으로부터 불변 카탈로그를 만들고, test 이름이 무엇을 거부하는지 전부 적는다 — 중복 stable/schema/payload 신원, 음수 버전, 잘못된 payload kind, null·공백·중복·반사 불일치 성분 순서, 서술자 누락, **payload 버전 사이의 logical destination 드리프트**. 특히 두 test가 이 계층의 성격을 보여 준다. `recursivelyFreezesOnlyTheClosedDeclaredGenericPayloadGraph` / `rejectsOpenRawWildcardMapJsonTreeInterfaceAndGenericRecordGraphs` — 열린 타입(raw·wildcard·`Map`·JSON 트리·인터페이스·제네릭 record 그래프)을 페이로드로 받지 않는다. 봉투 작성기가 shape을 따라 스냅샷할 수 있으려면 그래프가 닫혀 있어야 한다(§11). `snapshotsEveryContributionAccessorExactlyOnceIncludingAStatefulSchemaHash` / `statefulDescriptorCannotBypassCrossVersionLogicalDestinationDrift` — 기여 접근자를 **정확히 한 번만** 호출한다. 가변 서술자가 검사와 저장 사이에 값을 바꿔 규칙을 우회하는 경로를 닫는다. `compiledContractUsesOnlyAStaticPublicCompositionBridgeWithoutReflectionLeak` — 컴파일된 계약이 반사를 밖으로 새게 하지 않는다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-messaging-c03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-messaging-c03" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-messaging-c03.txt" - ] - }, - { - "title": "Confirmed — 다섯 개의 닫힌 전이표가 있고 terminal이 진짜 terminal이다", - "kind": "concept", - "slug": "adapter-outbound-objectstorage-c05", - "readiness": "READY", - "source": [ - "`final/document.md#a09#L244`" - ], - "owning-module": "`adapter-outbound-objectstorage`", - "classification": "`ObjectOperationStateMachine`이 publication·scan·reference·direct-grant·multipart 다섯 계열의 전이를 각각 switch로 적는다. terminal 처리가 계열마다 명시적이다. 뒤 둘은 **branch 전이**(EXPIRED/ABORTED/FAILED/CORRUPT)를 별도로 허용해, 정상 사슬 어디서든 실패로 빠질 수 있되 terminal에서는 나올 수 없게 한다. `requireNextRevision(current, next)`가 `next == current + 1`을 강제한다 — revision은 건너뛰지도 되돌아가지도 못한다. `ObjectOperationStateMachineTest`가 그 셋을 이름으로 고정한다: `publicationFollowsScanFreeAndScanRequiredPaths`, `terminalOutOfOrderAndStaleRevisionTransitionsFailClosed`, `independentStateFamiliesDoNotImplyEachOther`.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-objectstorage-c05.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-objectstorage-c05", - "adapter-outbound-objectstorage-c05-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-objectstorage-c05.txt" - ] - }, - { - "title": "이 sub-scope의 설계 — 비밀은 durable하지 않고, 승인은 명시적으로 닫힌다", - "kind": "concept", - "slug": "adapter-outbound-objectstorage-c07", - "readiness": "READY", - "source": [ - "`final/document.md#a09#L420`" - ], - "owning-module": "`adapter-outbound-objectstorage`", - "classification": "durable record가 **비밀을 담지 않는다**는 것이 출발점이다. `DirectTransferSessionRecord`의 한 줄 javadoc이 그것이다 — \"Durable non-secret direct-transfer session state; bearer material is deliberately absent.\" 저장되는 것은 generation·제약 다이제스트·서명 시각·만료·credential revision·reference revision뿐이고, presigned URI와 서명 헤더는 **process-local 캐시**에만 남는다. 프로세스가 재시작하면 이미 발급된 grant는 재현되지 않고 `\"issued direct grant bearer material is unavailable after process restart\"`로 명시적으로 실패한다 — 조용히 새로 서명해서 두 번째 bearer를 만드는 대신이다. 상태 전이도 CAS로 순서가 고정된다. `SESSION_RESERVED → GRANT_PREPARED → GRANT_ISSUED → DATA_UPLOADED`이고, test 이름이 그 순서를 그대로 못박는다 — `preparedCasPrecedesSigningAndIssuedCasPrecedesReturningTheBearerGrant`. 서명 **전에** prepared가 durable해야 하고, bearer를 **반환하기 전에** issued가 durable해야 한다. multipart 쪽에서 가장 흥미로운 것은 완료 시점의 **admission drain**이다. `DirectMultipartCompletionVerifier.requireAdmissionDrained`는 provider가 \"controlled ingress가 비었다\"고 권위 있게 말해 주지 않으면, `마지막 grant 만료 + 검증된 시계 오차 + 최대 in-flight 지평` 이 지나기 전에는 완료를 거부한다. 이미 발급된 part PUT이 아직 날아가고 있을 수 있기 때문이다. test 이름이 `completionHorizonRejectsWhileIssuedPartRequestsMayStillArrive`와 `lateGrant…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-objectstorage-c07.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-objectstorage-c07" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-objectstorage-c07.txt" - ] - }, - { - "title": "Sub-scope 02 — API contracts (`api/**`)", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L70`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "public top-level production type도 정확히 49개다. `docs/architecture/jpa-api-surface.txt`의 committed API baseline 역시 `api` namespace에서 49개를 기록하고 있어 현재 이름 목록 drift는 없다. Gradle `verifyJpaApiSurface`가 이 surface의 추가/삭제를 fail-closed로 검증한다. 이 package에는 Spring/JPA/Repository/Entity/Configuration annotation이 하나도 없다. 즉 JPA adapter 안에 위치하지만 **API vocabulary 자체는 Spring bean discovery나 JPA mapping으로 활성화되지 않는다.** 실제 composition은 `app-bootstrap` 및 implementation package가 소유한다. `api/**`는 implementation package와 달리 의도적으로 외부 adopter surface다. committed API baseline 상단도 `api`를 intended external package로 명시한다. 따라서 다음 두 사실을 구분해야 한다. 1. repository 내부 production consumer가 있는가 2. public library contract로 존재할 이유가 있는가 예를 들어 `JpaEntityNotFoundException`은 현재 repository production에서 자신을 제외한 참조 파일이 0개다. 하지만 이 한 사실만으로 dead type이라고 판정하지 않았다. external API surface는 repository 내부에서 직접 생성되지 않더라도 adopter가 catch/translate하는 계약일 수 있기 때문이다. 반대로 public API라는 이유로 내부 invariant 결함까지 “미사용이라 안전”으로 넘기지는 않는다. `SignedJsonCursorCodec`처럼 codec 자체가 public contract이고 자기 encode/decode algebra가 불일치하면 repository 내부 consumer 유무와 무관하게 API defect…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c01" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c01.txt" - ] - }, - { - "title": "Error API — provider exception을 stable failure algebra로 변환", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c03", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L208`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "error hierarchy의 핵심은 “예외 class를 많이 만든 것”이 아니라 provider-specific signal을 bounded semantic category로 변환하는 것이다. 대표 category는: serialization failure optimistic conflict lock not available connection unavailable unique/FK/not-null/check constraint entity not found schema mismatch data corruption completion unknown 이 category는 뒤의 retry policy/metric이 SQLSTATE/provider message를 직접 해석하지 않게 하는 중간 vocabulary다. `JpaFailureContext`가 가지는 정보는 operation, SQLSTATE/constraint, attempt, retryability, completion-unknown, elapsed, trace 등으로 제한된다. 중요한 invariant는 다음이다. arbitrary identifier는 그대로 담지 않고 bounded/redacted form으로 축약 malformed SQLSTATE는 `redacted` absent SQLSTATE는 sentinel로 표현 completion unknown과 retryable=true를 동시에 표현할 수 없음 completion-unknown factory는 항상 automatic retry를 차단하는 형태를 만든다 즉 “exception이 발생한 뒤 로그에서 실수하지 말자”보다 앞선 위치에서 **failure context가 위험한 shape 자체를 표현하기 어렵게** 만든다. base exception message는 provider cause message를 그대로 복사하지 않고 category + bounded context로 만든다. dedicated test도 provider cause에 email marker를 넣었을 때 top-level exception message에 노출되지 않는 것을 검증한다. 동시에 raw `Throwable cause`는 보존한다.…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c03" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c03.txt" - ] - }, - { - "title": "Transaction API — 실행체보다 먼저 retry 가능 상태를 제한한다", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c06", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L462`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "retryProfile write profile은 positive timeout이 필수다. read-only는 zero timeout을 “connection default” 의미로 허용한다. 지원 propagation을 REQUIRED / MANDATORY / REQUIRES_NEW로 좁혀 SUPPORTS/NESTED/NOT_SUPPORTED/NEVER처럼 “실제로 transaction 안에 있는가”를 흐리는 mode를 surface에서 제거했다. isolation 역시 PostgreSQL에서 의미가 겹치는 READ_UNCOMMITTED를 expose하지 않는다. retryable category allowlist는 다음 contender 계열로 제한된다. serialization failure optimistic conflict lock not available connection unavailable `COMPLETION_UNKNOWN`을 넣으면 constructor가 즉시 거부한다. Unique constraint 같은 ineligible category도 거부한다. 즉 failure translator가 retryability를 판단하고, profile이 category allowlist를 가진다고 해서 “어떤 failure도 설정으로 retry 가능하게” 만들 수 없다. RETRY_FULL_TRANSACTION 세 가지이며 retry만 non-zero delay를 가질 수 있다. 이 분리 덕분에 completion unknown이 `delay=0 retry`처럼 표현되지 않는다. “모르겠음”을 “즉시 한 번 더”와 구분한다. `RetryDecision.reason`은 javadoc상 bounded diagnostic/low-cardinality-safe string으로 설명된다. 그러나 constructor는 non-null/nonblank만 확인하고 길이/형식 상한은 없다. runtime constructor probe에서는 100,000-character reason도 accepted됐다. 다만 actual `JpaRetryObservation`은 decision.reason을 metric tag로 사용하지 않는다. met…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c06.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c06", - "adapter-outbound-persistence-jpa-c06-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c06.txt" - ] - }, - { - "title": "API surface verification", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c07", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L639`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "`verifyJpaApiSurface --rerun-tasks`가 통과했다. 이 task가 증명하는 것은 **public type names가 committed baseline과 동일하다**는 것이다. method semantics나 constructor invariant까지 ABI/API compatibility를 검증하는 것은 아니다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c07.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c07" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c07.txt" - ] - }, - { - "title": "Sub-scope 03 — transaction + persistence failure", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c08", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L721`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "모든 51개 source/test를 FULL_READ했다. 이 scope에서는 implementation class를 샘플링하지 않고 transaction state machine, retry budget, Spring mapping, failure translation, root wiring, consumer reachability까지 연결했다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c08.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c08" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c08.txt" - ] - }, - { - "title": "같은 leaf 안에 두 개의 transaction model이 존재한다", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c09", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L737`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "현재 persistence-jpa에는 transaction을 표현하는 두 계열이 동시에 존재한다. input/output vocabulary: `TransactionRequest` `TransactionPolicyId` `CallBudget` `TransactionResult` `TransactionOutcome` `OperationId` `TransactionPhase` `ReconciliationReference` 이 모델은 application-core가 소유한다. use case가 outbound adapter type을 import하지 않아도 transaction policy와 uncertain outcome을 표현할 수 있다. input/output vocabulary: `PersistenceOperationName` `TransactionProfile` `RetryProfile` `JpaPersistenceException` `TransactionCompletionEvidence` `JpaPlatformRuntimeAutoConfiguration`은 `PlatformTransactionManager`가 있으면 `SpringJpaTransactionExecutor` bean을 만들고, 그 executor가 있으면 `FullTransactionRetryCoordinator` bean도 만든다. 따라서 source tree 수준에서는 B가 단순 historical class가 아니라 **현재 runtime bean graph에도 포함되는 구현**이다. 그러나 repository production call search에서는 `FullTransactionRetryCoordinator.execute(...)`를 실제 business/application code가 호출하는 경로가 확인되지 않았다. 반대로 application-core transaction port는 sample/use-case/composition에서 canonical contract로 사용된다. 이 공존 자체는 곧바로 defect가 아니다. `api/**`는 intended external surface이므로 fork/application이 B를 programma…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c09.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c09" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c09.txt" - ] - }, - { - "title": "`SpringPolicyTransactionPort`: transaction result를 boolean 성공/실패보다 세밀하게 표현", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c12", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L832`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "`PolicyTransactionPort.inTransaction(...)`은 단순 예외 기반 wrapper가 아니다. 결과는 최소 다음 상태를 구분한다. `CommittedWithPostCommitFailure` `Participating` `DeterminateRollback` `Indeterminate` 핵심은 **commit exception = rollback**으로 가정하지 않는 것이다. commit 호출 전에 phase를 `COMMIT_REQUESTED`로 올리고, Spring `TransactionSynchronization` sentinel로 실제 callback을 관찰한다. commit에서 exception이 발생해도: 1. `afterCommit()`이 이미 확인됐으면 `CommittedWithPostCommitFailure` 2. rollback callback/`UnexpectedRollbackException`/replay-candidate가 확인되면 `DeterminateRollback` 3. 그 외에는 `Indeterminate` 즉 연결 끊김 같은 애매한 exception을 “rollback이겠지”라고 간주하지 않는다. `Indeterminate`는 retry 대상이 아니다. replay 조건은 모두 만족해야 한다. policy = `COMMAND_SERIALIZABLE_REPLAY_SAFE` 현재 attempt가 physical transaction owner attempt < configured max current thread not interrupted result가 `DeterminateRollback` failure가 40001 serialization 또는 40P01 deadlock replay candidate 따라서 commit ack를 못 받은 상태는 replay되지 않는다. 이 점은 뒤에서 다룰 JPA public API completion-evidence wiring gap의 중요한 mitigation이다. **현재 canonical application path는 completion evidence infrastructure가 없어도 불확정 commit을 자동 재실행하지 않는다.**…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c12.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c12" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c12.txt" - ] - }, - { - "title": "CallBudget를 transaction timeout보다 먼저 적용한다", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c13", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L879`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "application policy path는 timeout을 단순히 `TransactionDefinition.setTimeout()` 하나로 끝내지 않는다. `ca-skeleton.jpa.transaction` settings는 transaction/resource-budget defaults를 가진다. 주요 invariant: duration positive duration <= 1 day retry max attempts 1..5 statement timeout <= transaction timeout lock timeout < statement timeout completion/acquisition/action margin hierarchy 즉 runtime에서 무한 retry나 무한 transaction timeout을 property 하나로 열 수 없게 hard cap을 둔다. 이 점은 API `RetryProfile.maxAttempts`가 upper bound를 갖지 않는 것과 대비된다. canonical application path는 실제 deployment settings에서 최대 5회를 강제한다. CallBudget admission은 connection pool을 빌리기 **전**부터 시작한다. transaction을 열 가치가 있으려면 남은 budget이 최소 다음을 감당해야 한다. begin 후에는 실제 남은 budget으로: Spring whole-transaction timeout statement timeout lock timeout idle-in-transaction timeout 따라서 pool에서 오래 기다린 요청이 “원래 5초 timeout이었으니 DB에서 다시 5초”를 받지 않는다. 이미 소비한 wall-clock budget을 transaction layer가 다시 주지 않는 구조다. canonical path의 retry backoff도 CallBudget-aware다. 다음 attempt를 시작하기 전에: jitter delay 다음 acquisition reserve 다음 최소 transaction/action margin 을 모두 감당할 수 있는지 확인한다. budget이 부족하면 sle…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c13.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c13" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c13.txt" - ] - }, - { - "title": "`CommitFailureClassifier`", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c17", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L1074`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "completion unknown candidate: SQLSTATE 40003 connection class 08* admin shutdown / crash / cannot-connect-now 계열 transport break cause 중요한 건 이 classifier를 generic SQLSTATE translator 대신 **commit call 내부에서만** 적용한다는 것이다. connection reset이 query 실행 중 발생했다면 connection unavailable일 수 있지만, provider에게 COMMIT을 보낸 후 reset됐다면 “commit됐는지 모름”이다. SQLSTATE만으로 이 둘을 구분할 수 없고 transaction phase가 필요하다. PostgreSQL classifier source도 이 이유를 직접 설명한다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c17.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c17" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c17.txt" - ] - }, - { - "title": "completion-unknown metric도 현재 transaction path에서 호출되지 않는다", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c19", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L1210`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "`JpaTransactionObservation.recordCompletionUnknown(...)` 구현은 존재한다. 하지만 production에서: `JpaObservabilityAutoConfiguration` construction = 0 `JpaTransactionObservation.recordCompletionUnknown(...)` call = 0 `recordCommitted/recordRolledBack/recordTimedOut` call도 0 `JpaPlatformRuntimeAutoConfiguration`이 만드는 default `RetryEventListener`도 empty implementation이며, `JpaObservabilityAutoConfiguration`을 통해 metric listener로 합성하지 않는다. 따라서 runbook의 `jpa.transaction.completion.unknown` signal은 현재 source wiring으로는 생성 근거를 찾지 못했다. 이 observability factory 전체의 reachability 문제는 later observation/baseline capability sub-scope에서 다시 exhaustive하게 확인한다. 여기서는 completion-unknown path의 cross-scope evidence로만 기록한다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c19.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c19" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c19.txt" - ] - }, - { - "title": "Querydsl integration은 production runtime classpath를 강제로 오염시키지 않는다", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c30", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L2087`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "`QuerydslJpaSupport`는 Advanced opt-in으로 설계돼 있다. lockfile에서 Querydsl은: compileClasspath test/integration/performance classpaths 에는 나타나지만 production `runtimeClasspath` configuration에는 포함되지 않는다. 따라서 JPA leaf를 사용하는 것만으로 Querydsl runtime dependency가 Stable deployment에 따라오는 구조는 아니다. `QuerydslJpaSupport`도: bounded page size <= 500 null predicate는 explicit unbounded opt-in 없으면 거부 registered `QueryName`을 Hibernate comment hint로 적용 현재 production consumer는 확인되지 않았다. 이는 Advanced opt-in helper의 미채택 상태로 기록하며 dead-code defect로 단정하지 않는다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c30.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c30" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c30.txt" - ] - }, - { - "title": "release-task existence validator", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c34", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L2523`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "fresh `verifyJpaReleaseGateTasks`도 성공했다. 이 success는 오히려 validator limitation의 evidence다. task semantic coverage/tag를 검사하지 않기 때문이다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c34.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c34" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c34.txt" - ] - }, - { - "title": "PostgreSQL failure translation: SQLSTATE 분류는 맞지만 `40003` 의미가 translator에서 소실된다", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c35", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L2632`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "`PostgreSqlFailureClassifier`는 PostgreSQL SQLSTATE를 bounded `FailureCategory`로 분류한다. serialization failure, deadlock, lock-not-available, constraint family, timeout, connection failure, schema/data 문제를 문자열 메시지가 아니라 SQLSTATE/structured server field 기준으로 다루는 방향은 적절하다. constraint 이름도 server error field에서 꺼내 catalog로 번역하므로 localized message parsing에 의존하지 않는다. 문제는 `COMPLETION_UNKNOWN`이다. 현재 `PostgreSqlExceptionTranslator.translate()`는 classifier 결과가 `COMPLETION_UNKNOWN`이어도 `JpaFailureContext`의 `completionUnknown`을 항상 `false`로 만들고, switch에서 `COMPLETION_UNKNOWN`을 `UNKNOWN`과 함께 일반 `JpaPersistenceException(FailureCategory.UNKNOWN, ...)`으로 강등한다. 직접 probe에서 SQLSTATE `40003`은 다음처럼 변환됐다. 여기서 단순 진단 정보만 사라지는 것이 아니다. 현재 `DefaultJpaRetryPolicy`는 `TransactionCompletionUnknownException` 또는 `FailureCategory.COMPLETION_UNKNOWN`을 가장 먼저 검사해 `RECONCILE`로 보낸다. 그런데 실제 translator를 통과시키면 focused policy probe 결과가 다음과 같다. 즉 **재실행은 막지만, commit 결과를 확인해야 하는 reconciliation 경로도 잃는다.** fail-closed라는 이유로 안전하다고 볼 수 없는 이유다. commit이 실제로 적용됐는지 알 수 없는 상태를 terminal failure로 바꾸면 caller는 설계된 recovery protocol을 실행할 근거를 잃는다. 현재 r…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c35.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c35" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c35.txt" - ] - }, - { - "title": "PostgreSQL Idempotency V2: owner/CAS 구조는 강하지만 replay 경계가 두 군데 어긋난다", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c36", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L2669`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "`PostgreSqlOwnerSafeIdempotencyStore`는 row lock, owner token, attempt, state revision, operation id와 transition digest를 결합해 claim/renew/fail/complete를 보호한다. `renew`와 `markFailed`는 동일 operation id replay에서도 semantic argument를 digest에 넣어 `SAME_ARGUMENTS`와 `DIFFERENT_ARGUMENTS`를 분리한다. 이 구조 자체는 강하다. 현재 revision에서는 `PostgreSqlIdempotencyProviderConfig`가 이 store를 production provider로 실제 생성하므로 아래 두 finding은 dormant helper 문제가 아니다. `inspect()`는 row가 `COMPLETED`이고 response payload가 있으면 `replayUntil`이 이미 지난 값인지 확인하지 않고 무조건 `COMPLETED_REPLAY`를 반환한다. 반면 claim path는 DB time과 expiry를 보고 만료된 row를 takeover 가능 상태로 처리한다. 실제 PostgreSQL 16에서 replay TTL 25ms로 완료한 뒤 50ms를 기다린 probe 결과: 즉 같은 시점의 같은 row가: Application의 `IdempotencyExecutorV2`는 reconciliation에서 `COMPLETED_REPLAY`를 실제 저장 응답 반환 신호로 사용한다. 따라서 이 불일치는 단순 introspection 문제가 아니라 **만료 후 새 실행이 허용된 시점에도 이전 응답을 reconciliation 결과로 반환할 수 있는 lifecycle correctness 문제**다. JPA 설계 문서가 동일 Idempotency V2 contract를 구현한다고 참조하는 Redis state machine도 `COMPLETED -> [*] : replay TTL expires`로 수명을 끝낸다. JPA `inspect()`만 이 만료를 무시한다. **판정: P1 — production idempotency lifecy…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c36.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c36", - "adapter-outbound-persistence-jpa-c36-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c36.txt" - ] - }, - { - "title": "확인된 안전 경계", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c38", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L2780`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "native write와 COPY는 caller가 임의 SQL identifier를 조립하도록 두지 않고 registered statement/name boundary를 사용한다. 값은 JDBC parameter 또는 COPY stream으로 전달된다. COPY에는 format/size bound와 transaction requirement가 있고, work claiming은 등록된 queue definition과 PostgreSQL `FOR UPDATE ... SKIP LOCKED` 경계를 사용한다. JSON path/query support와 range query support도 registry/typed value boundary를 두고 실제 값은 bind한다. constraint translation 역시 structured SQLSTATE/server fields를 사용한다. 이번 sub-scope에서 이 영역의 새로운 SQL-injection/runtime-wiring defect는 확인되지 않았다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c38.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c38" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c38.txt" - ] - }, - { - "title": "Vendor migrations", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c39", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L2805`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "다음 9개 migration을 모두 읽었다. 확인한 경계는 다음과 같다. idempotency owner/state/replay/transition metadata의 persisted shape outbox claim/delivery/index shape integer advisory/row-lock support table와 expiry extension capability schema registry adoption/widening request hash `char`/`varchar` drift 보정 durable operation / live-event log schema real PostgreSQL probe에서 Flyway는 vendor 9 migrations를 모두 validate/apply했다. 이번 sub-scope에서 migration 순서, 현재 schema 제약, index 선언 자체로 승격할 신규 defect는 확인하지 못했다. capability-specific migration의 완전한 cross-stream adoption은 각 owning capability scope에서 다시 본다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c39.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c39" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c39.txt" - ] - }, - { - "title": "Idempotency real-PostgreSQL TTL boundaries", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c40", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L2883`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "`evidence/raw/066-postgresql-idempotency-replay-boundary-probe.txt` exact `postgresqlIdempotencyIntegrationTest` lane `complete()` changed replay TTL false-same replay 재현 expired COMPLETED row의 `inspect()`/`claim()` lifecycle 불일치 재현 temporary test는 실행 후 source에서 복원 BUILD SUCCESSFUL", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c40.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c40" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c40.txt" - ] - }, - { - "title": "이번 scope에서 finding으로 승격하지 않은 항목", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c41", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L2913`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "registered native write/COPY의 SQL/value boundary work-claim `SKIP LOCKED` 기본 구조 structured SQLSTATE/constraint-name 추출 array/json helper의 bounded value handling polling outbox cutover sentinel의 transition별 반복 검사 차이: claim 자체가 immutable sentinel을 요구하고 current evidence만으로 stale claim이 cutover를 우회한다고 입증되지 않아 보류 vendor migration 9개의 현재 적용 순서/문법", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c41.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c41" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c41.txt" - ] - }, - { - "title": "H2 idempotency와 V2 owner 필드", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c43", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L3119`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "처음에는 `H2IdempotencyClaimRepository`의 MERGE/takeover가 V2 owner/transition field를 초기화하지 않는 점을 의심했다. 그러나 baseline `IdempotencyRecordEntity` 자체가 V1 field만 mapping하고, owner-safe V2는 PostgreSQL capability stream으로 분리돼 현재 별도 activation contract를 가진다. 서로 다른 schema generation의 field를 H2 V1이 reset하지 않는 것은 현 계약 위반이 아니다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c43.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c43" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c43.txt" - ] - }, - { - "title": "quota FIFO settlement 자체", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c46", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L3350`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "reservation row가 upload id와 연결되지 않아 `JpaQuotaCommitGateway`가 scope의 가장 오래된 live reservation부터 정산하는 것은 `docs/fileserver/design-deviations.md`에 명시적으로 기록된 adaptation이다. row identity와 실제 upload identity가 1:1이 아닌 것 자체는 현재 설계 계약이다. 다만 그 문서가 전제로 둔 aggregate byte enforcement가 실제로 없다는 점은 §78의 별도 P1 finding으로 올렸다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c46.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c46" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c46.txt" - ] - }, - { - "title": "cleanup fenced lease의 expiry-after / takeover-before window", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c47", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L3354`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "cleanup settlement query는 claim token을 fence하고 reaper takeover가 token을 교체한다. lease expiry 직후 아직 takeover 전인 worker가 settle할 수 있는 window는 보이지만, 새 owner가 생긴 뒤 stale worker가 상태를 덮어쓰는 race는 token CAS가 막는다. durable-operation과 달리 현재 계약만으로 “expiry 순간부터 절대 settle 금지”라고 확정할 충분한 근거가 없어 finding으로 올리지 않았다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c47.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c47" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c47.txt" - ] - }, - { - "title": "Notification composition과 schema lifecycle", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c48", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L3401`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "Notification JPA capability는 production opt-in path로 실제 composition된다. `PersistenceJpaRootAutoConfiguration`이 `NotificationJpaPersistenceFacade`를 import한다. facade가 `NotificationJpaPersistenceConfig`를 import하고 entity/repository/store bean을 조립한다. application-side worker/config가 recipient lease, reconciliation, provider-event ledger, admin operation store를 실제 소비한다. `NotificationSchemaActivation`은 capability registry를 읽어 startup activation을 검사한다. schema stream은 V1~V10까지 진화했지만 registry는 V4에서 `jpa-notification-platform-v4`, `feature_revision=4`, `INSTALLED_INACTIVE`를 기록한 뒤 더 이상 revision을 올리지 않는다. 반면 current Java mapping과 SQL은 V5~V10에서 추가된 column/constraint에 실제 의존한다. 이 drift가 §88의 startup false-positive를 만든다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c48.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c48" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c48.txt" - ] - }, - { - "title": "tenant-bound repository guard", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c50", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L3565`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "tenant-sensitive lookup이 전부 완전하다고 corpus 전체 결론을 내리지는 않았지만, `TenantBoundRepositoryGuard`와 tenant-qualified repository method가 존재하고 이번 68-file owning scope에서 즉시 재현 가능한 cross-tenant bypass는 확정하지 못했다. 별도 inbound/application authorization 조합은 cross-scope 단계가 소유한다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c50.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c50" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c50.txt" - ] - }, - { - "title": "schema identifier selection/reset", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c52", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L3780`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "`SchemaTenantRegistry`는 unquoted PostgreSQL identifier shape를 제한하고, connection provider는 schema 값을 statement text에 직접 붙이지 않고 bound `set_config`로 적용하며 release 시 neutral `pg_catalog`로 reset한다. 별도 failure-in-reset / pool-implementation semantics까지 corpus 전체 보장은 하지 않지만, 현재 happy-path isolation contract를 뒤집을 evidence는 없었다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c52.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c52" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c52.txt" - ] - }, - { - "title": "`CommitAmbiguityProxy` / `PostgreSqlContractExtension`", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c54", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L4017`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "두 public helper는 defining file 밖 exact FQN reference가 0이다. 특히 `PostgreSqlContractExtension.serverVersion()`은 이름과 달리 database의 `SHOW server_version`이 아니라 Docker image + container id를 반환하지만 현재 integration support는 별도 `JpaPlatformContractSupport.serverVersion()`로 실제 server version을 읽는다. 따라서 잘못된 current evidence로 분류하지 않고 dead/unadopted helper로 기록한다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c54.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c54" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c54.txt" - ] - }, - { - "title": "`JpaReleaseManifest`의 regex parser", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c55", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L4021`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "Java testkit parser 자체는 정규식 기반이라 일반-purpose JSON parser가 아니다. 그러나 실제 registry 파일은 root Gradle `verifyJpaReleaseGateTasks`에서 `JsonSlurper`로 다시 parse되고 real task graph까지 resolve한다. 현재 malformed JSON을 Java regex parser 하나가 받아들일 가능성만으로 release fail-open을 별도 finding으로 중복 승격하지 않는다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c55.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c55" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c55.txt" - ] - }, - { - "title": "release gate 소속은 양방향으로 검증되지 않는다", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c57", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L4306`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "근거: `evidence/raw/112-pool-lane-claim-registry-reachability.txt`. `config/jpa/release-registry.json`의 gate는 6개이고 모두 blocking이다. pool lane은 registry에도, `docs/jpa/support-matrix.md` §Release gates 6행에도, testkit `JpaReleaseGate.required()`에도 없다(세 곳 모두 grep exit=1). 그런데 `jpaPlatformReleaseGate`는 `dependsOn jpaPlatformPoolContractTest`를 갖고, root `jpaReleaseGate`가 그것을 다시 의존한다. `verifyJpaReleaseGateTasks`는 registry → task graph 한 방향만 검사한다(registry의 각 gate가 실제 `Test` task로 resolve되는가). 반대 방향 — release gate에 들어 있는 lane이 registry에 있는가 — 은 어디서도 검사되지 않는다. 따라서 `jpaPlatformReleaseGate`에서 pool lane 의존을 지워도 어떤 verifier도 반응하지 않고, 남는 실행 경로는 nightly workflow 한 줄뿐이다. 이것을 결함으로 올리지는 않는다. `jpaPlatformReleaseGate`의 주석이 밝힌 집계 기준은 \"documented gate가 검증되지 않은 채 통과하게 만드는 lane\"이고, pool lane은 문서화된 gate를 뒷받침하지 않으므로 기준상 registry에 없는 것이 일관적이다. 다만 그 결과로 이 lane의 release gate 소속만은 아무 계약도 보호하지 않는다는 사실을 기록한다. P3.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c57.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c57" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c57.txt" - ] - }, - { - "title": "`JpaPlatformContractSupport`의 컨테이너 수명 서술은 실제와 다르다", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c58", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L4541`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "클래스 javadoc은 이렇게 말한다. 실제 사용은 정확히 그 \"per class\"다. `JpaPlatformContractSupport.start()` 호출 지점은 31곳이고 대부분 `@BeforeAll`에서 시작해 `@AfterAll`에서 `close()`한다. JVM 수준 공유 인스턴스나 static holder는 없다. `StablePostgreSqlMatrixContractTest`는 test마다, `JpaPlatformContractSupportOwnershipTest`는 test마다(5개) 컨테이너를 새로 띄운다. 실측치는 다음과 같다(`115-integration-lane-original-verification.txt`, XML의 Testcontainers 로그 집계). 한 번의 전체 tag lane 통과에 PostgreSQL 컨테이너가 87번 기동한다. 그럼에도 5개 lane 전체가 3분 10초에 끝났으므로 비용 주장이 무너지는 수준은 아니다. 기록하는 이유는 서술과 구현의 불일치다 — 클래스가 자기 설계 근거로 내세운 \"JVM 공유\"가 소비자 31곳 어디에서도 성립하지 않는다. P3. 같은 클래스의 다른 서술은 사실이다. multi-version 선택을 fail-closed로 거부하는 것(`start()`가 `selected.size() != 1`이면 예외), 그리고 \"the CI matrix fans out\"은 `jpa-release.yml`(16/17/18), `jpa-pr.yml`(16/18), `jpa-nightly.yml`이 `-Pjpa.matrix.versions`로 실제 fan-out하는 것으로 확인된다. `JpaPlatformContractSupportOwnershipTest`가 지키는 pool 소유권(호출당 새 pool을 만들어 참조를 잃던 과거 결함)도 실제 assertion으로 고정돼 있다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-persistence-jpa-c58.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c58" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c58.txt" - ] - }, - { - "title": "reactive 경로가 명시적으로 배치한 세 가지", - "kind": "concept", - "slug": "adapter-outbound-persistence-mongo-c04", - "readiness": "READY", - "source": [ - "`final/document.md#a06#L648`" - ], - "owning-module": "`adapter-outbound-persistence-mongo`", - "classification": "`DefaultReactiveMongoExecutor`의 javadoc이 blocking 경로가 공짜로 얻는 것과 여기서 직접 배치해야 하는 것을 대비한다. observation scope를 Reactor 자원(`Mono.using`/`Flux.using`)으로 두어 완료·오류·**취소** 모두에서 닫는다. HTTP 클라이언트 연결 해제가 취소를 일으키므로 취소가 흔한 경우다. timeout을 조립된 publisher에 적용한다. 구독 전에 적용하면 \"람다를 만드는 데 걸린 시간\"을 재게 된다. context를 Reactor Context로 옮긴다(`ReactiveMongoContextKeys`). 체인은 operator 경계마다 스레드를 바꾸므로 구독 시점의 `ThreadLocal`은 driver 응답 시점에 이미 없다. 기록해 둘 관측 하나: `executeMany(...)`는 성공을 `doOnComplete`로 기록하므로 **취소된 stream은 success도 failure도 기록하지 않는다.** observation은 `close()`되고 초기 tag(`result=unknown`, `failureCategory=none`)로 한 번 계수된다. 취소가 흔한 경로라는 점을 감안하면 이는 의도된 분류로 보이지만, `result=unknown` bucket이 \"취소\"와 \"관측 시작 직후 예외\"를 함께 담는다는 사실은 계약에 없다. P3/기록.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-persistence-mongo-c04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-mongo-c04", - "adapter-outbound-persistence-mongo-c04-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-mongo-c04.txt" - ] - }, - { - "title": "migration은 fencing을 정면으로 다룬다", - "kind": "concept", - "slug": "adapter-outbound-persistence-mongo-c06", - "readiness": "READY", - "source": [ - "`final/document.md#a06#L868`" - ], - "owning-module": "`adapter-outbound-persistence-mongo`", - "classification": "`MongoMigrationLock.fence()`의 javadoc이 이 sub-scope에서 가장 정확한 문장을 담고 있다. 그래서 lease 위에 monotonic fencing token을 얹고, `MongoCollectionMigrationLock.tryAcquire`가 그 token을 **lease를 부여하는 같은 조건부 update 안에서 서버가 증가**시킨다(\"A token handed out anywhere else could be handed out twice\"). `held()`는 owner 이름이 같아도 fence가 다르면 false를 반환한다 — 프로세스가 재시작했거나 운영자가 owner 문자열을 재사용한 경우다. `matchedCount`를 쓰는 이유(같은 값을 다시 쓰면 `modifiedCount`가 0이라 소유권 판정이 뒤집힌다)도 두 곳에 적혀 있다. `MongoMigrationHeartbeat`은 이미 고쳐진 결함의 산물이다: runner가 `execute`가 **반환된 뒤에** 한 번만 refresh했으므로, 40분짜리 `execute`는 35분 동안 만료된 lease를 들고 있었고 그 사이 두 번째 runner가 정당하게 획득해 같은 migration을 동시에 돌렸다. 이제 heartbeat이 migration에게 넘겨진다 — batch 경계를 아는 것은 migration뿐이기 때문이다. `MongoCollectionMigrationLedger.saveCheckpoint`에는 **두 개의** 결함 이력이 주석으로 남아 있다. upsert 하나로는 \"매치할 게 없었다\"와 \"fence filter가 배제했다\"를 구분할 수 없어 *모든 migration의 첫 checkpoint*가 \"a newer migration runner owns the lease\"로 거부됐고, 동시에 진짜 배제 경로는 unique index의 duplicate-key로 죽어 그 문장을 만드는 분기가 **도달 불가**였다. 지금은 replace-then-insert로 두 경우를 분리한다. `MongoMigration`에 `rollback`이 없는 것도 명시적 결정이다 — \"A rollback method implies the rever…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-persistence-mongo-c06.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-mongo-c06" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-mongo-c06.txt" - ] - }, - { - "title": "`OutboundSupportConfig`: unconditional shared bean seam과 실제 runtime wiring", - "kind": "concept", - "slug": "adapter-outbound-support-c04", - "readiness": "READY", - "source": [ - "`final/document.md#a04#L318`" - ], - "owning-module": "`adapter-outbound-support`", - "classification": "`OutboundSupportConfig`는 `@Configuration`이며 `FailOpenDependencyLogger` bean 하나만 제공한다. 별도 master property condition은 없다. 이는 support 자체를 optional capability로 취급하지 않고, 실제 provider/client capability의 on/off를 sibling adapter가 소유하게 하려는 구조다. `OutboundSupportConfig`를 support 밖 production Java에서 명시적으로 참조하는 파일은 0개다. 그러나 실제 composition root `CaSkeletonApplication`은 다음 broad package를 component scan한다. `AUTO_CONFIGURED_PACKAGES` exclusion에는 messaging/notification/persistence 등은 들어가지만 support package는 포함되지 않는다. 따라서 support config는 broad component scan으로 도달한다. registry도 support runtime membership을 `app-bootstrap`으로 선언하고 `app-bootstrap/build.gradle`이 support project를 직접 `implementation`한다. 따라서 이 configuration은 현재 **active scanned path**다. support config 자체에는 `@ConditionalOnProperty`가 없고 `@ConditionalOnMissingBean`만 있다. 이것은 같은 optional adapter들의 master switch 누락으로 판정하지 않았다. support는 provider/client를 생성하지 않는다. logger bean 하나만 default로 제공한다. actual messaging/notification/httpclient 등은 자기 capability root에서 activation을 소유한다. support README와 config javadoc 모두 이 비대칭을 의도적으로 설명한다. `OptionalAdapterBeanGatingT…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-adapter-outbound-support-c04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-support-c04" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-support-c04.txt" - ] - }, - { - "title": "idempotency, inbox, outbox: uncertainty를 상태로 보존한다", - "kind": "concept", - "slug": "application-core-c04", - "readiness": "READY", - "source": [ - "`final/document.md#a03#L118`" - ], - "owning-module": "`application-core`", - "classification": "초기 contract는 scope + request fingerprint로 claim/replay를 제공하고, same key/different fingerprint를 conflict로 분리한다. completed result는 replay하고 in-flight는 bounded poll한다. 이 버전은 “DB operation의 효과가 이미 발생했지만 응답만 잃은 상태”를 충분히 표현하지 못한다. V2는 owner-safe CAS handle에 scope/token/attempt/revision/claimOperationId를 넣고 stale owner mutation을 거부한다. processing-start를 durable하게 확인하기 전에는 body를 실행하지 않으며 claim/start/completion의 unknown result는 inspect/reconcile 대상으로 남긴다. processing start 이후 ordinary RuntimeException은 효과가 없다고 증명할 수 없으므로 `EFFECT_UNKNOWN_ABANDONED` 쪽으로 분류되고 자동 replay 권한을 주지 않는다. 명시적인 `RetryableNoEffect`만 안전 재시도 근거로 취급한다. scope digest는 versioned keyed digest + operation code로 정규화되고 raw identity는 외부 surface에서 제거된다. lease/replay TTL과 owner token grammar도 bounded다. **Historical evidence.** V2 contract가 인접한 package에 중복 복제돼 구현체들이 서로 다른 nominal type을 참조한 문제가 있었고, singular contract를 유지하는 regression test가 존재한다. Inbox contract는 same-store 처리와 owner-safe receive/process state를 모델링한다. `RECEIVED -> PROCESSING -> COMPLETED/RETRYABLE/DEAD` 상태를 가지고 ACK는 handler transaction commit 이후에만 가능하다. expired owner가 늦게 결과를…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-application-core-c04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "application-core-c04", - "application-core-c04-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/application-core-c04.txt" - ] - }, - { - "title": "cache, lease, lock: 동시성 완화와 correctness authority를 구분한다", - "kind": "concept", - "slug": "application-core-c05", - "readiness": "READY", - "source": [ - "`final/document.md#a03#L152`" - ], - "owning-module": "`application-core`", - "classification": "`CacheAsideExecutor`는 fresh/negative hit, hard miss, stale, incompatible schema, provider unavailable을 명시적으로 구분한다. source load에는 key-local single-flight와 global source bulkhead를 함께 적용한다. in-flight key 수, waiter 수, source concurrency, admission wait, load deadline이 모두 bounded다. stale value는 hard expiry 이전이며 **classified transient failure**일 때만 fallback될 수 있다. permanent failure에는 stale을 반환하지 않는다. source load 중 invalidation이 발생하면 lookup 때 캡처한 `CacheWriteCondition`이 더 이상 일치하지 않아 이전 source result의 refill을 거부한다. 이는 invalidate 직후 늦게 끝난 source load가 stale value를 resurrect하는 race를 막는다. optional distributed refresh coordination은 soft lease로 한 pod만 refresh하도록 하지만 correctness lock은 아니다. owner는 lease 획득 후 cache를 재확인해 다른 pod가 이미 fill했다면 source를 호출하지 않는다. claim 결과가 indeterminate이면 **동일 attempt token으로 한 번만 재시도**한다. contender는 stale이 아직 valid하면 즉시 stale을 반환할 수 있다. `CacheSingleFlight`는 waiter timeout/interruption을 보존하고 완료된 flight를 제거한다. leader가 영원히 남아 key bound를 점유하지 않도록 monotonic deadline 이후 abandoned flight를 opportunistic reap한다. V2 `DistributedLeasePort`는 caller가 provider send 전에 owner/operation t…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-application-core-c05.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "application-core-c05" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/application-core-c05.txt" - ] - }, - { - "title": "fileserver: DB metadata와 physical content 사이의 실패 seam을 명시한다", - "kind": "concept", - "slug": "application-core-c09", - "readiness": "READY", - "source": [ - "`final/document.md#a03#L200`" - ], - "owning-module": "`application-core`", - "classification": "fileserver는 application-core 안의 가장 큰 독립 orchestration 중 하나다. 핵심 contract는 “metadata transaction과 filesystem/object I/O가 원자적이지 않다”는 사실을 숨기지 않고 recovery model을 두는 것이다. upload admission은 authorization을 quota/storage admission보다 먼저 수행해 denial이 side-effect-free이도록 한다. reservation metadata/session은 DB transaction에서 만들지만 staging physical object는 외부 작업이므로 실패 시 compensation/reconciliation 대상이 된다. writer는 one-writer lease + fencing token을 사용한다. stale token은 append/finalize를 진행할 수 없고 takeover는 새 token을 만든다. append 시 metadata offset과 physical length가 다르면 자동 repair하지 않고 conflict로 중단한다. finalize는 declared length, server-computed digest, optional client digest를 순서대로 검사한다. client digest는 server digest를 대체하지 않는다. 이후 VERIFYING으로 이동하고 verifier가 publish 승인해야 READY가 된다. READY가 유일한 public/downloadable state다. publish physical success 뒤 READY metadata transaction이 실패하면 결과는 단순 retryable failure가 아니라 `AmbiguousCompletionException`과 recovery queue로 간다. physical publish가 이미 발생했을 수 있기 때문이다. cancel/cleanup race를 막기 위해 cleanup claim은 writer/cleaner barrier를 형성하며 stale cleanup claim을 reclaim하는 경로가 실제로 호출된다. physic…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-application-core-c09.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "application-core-c09" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/application-core-c09.txt" - ] - }, - { - "title": "관찰: 재사용 가능한 도메인 “내용”보다 도메인 모델링 계약을 소유한다", - "kind": "concept", - "slug": "domain-core-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a01#L68`" - ], - "owning-module": "`domain-core`", - "classification": "현재 `domain-core`에는 WorkLog 같은 실제 aggregate가 없다. 실제 샘플 aggregate/value object/event는 `sample-portfolio`에 있다. 이 모듈에 남은 production surface는 다음 두 종류다. 1. **식별자 추상화** — `ResourceId`, `IdFactory` 2. **모델링 표식** — `@ValueObject`, `@AggregateRoot`, `@DomainEvent` 따라서 “business concepts, entities, value objects…”를 둘 수 있는 계층이라는 정책과 달리, 현재 snapshot의 실제 contents는 skeleton 전반에서 사용할 **domain-layer contract/marker**에 가깝다. 이는 현재 source에 대한 관찰이며, 향후 실제 production domain type이 이 module에 추가되지 않는다는 뜻은 아니다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-domain-core-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "domain-core-c01", - "domain-core-c01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/domain-core-c01.txt" - ] - }, - { - "title": "헤징 예산", - "kind": "concept", - "slug": "grpc-advanced-resilience-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-advanced-resilience#L58`" - ], - "owning-module": "`grpc-advanced-resilience`", - "classification": "토큰 버킷이다. 헤지 하나가 `round(1/ratio)` 토큰을 쓰고, 완료된 호출 하나가 토큰 하나를 돌려준다. 상한이 조용한 구간 뒤의 폭주를 제한한다. 비율 상한이 0.5 이고 그 근거가 적혀 있다. 그리고 왜 재시도 예산보다 더 급한지도 적는다. 소비는 정확한 비교 후 교체 루프다 — 이 가족에서 원자성을 제대로 다룬 몇 안 되는 곳이다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-grpc-advanced-resilience-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-advanced-resilience-c01", - "grpc-advanced-resilience-c01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-advanced-resilience-c01.txt" - ] - }, - { - "title": "소비자 컴파일 게이트", - "kind": "concept", - "slug": "grpc-codegen-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-codegen#L104`" - ], - "owning-module": "`grpc-codegen`", - "classification": "`GrpcConsumerFixture.fromJavaSource` 가 릴리스된 소비자의 자바 소스에서 요구 사항 셋을 기계적으로 유도한다. 그리고 그것이 컴파일의 근사라는 것과, 근사인 이유(ADR-GRPC-002)를 함께 적는다. `breaksAgainst` 는 세 종류를 따로 보고한다 — 서비스 경로, 메서드 경로, 자바 패키지. 하나의 개수로 합치지 않는다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-grpc-codegen-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-codegen-c01", - "grpc-codegen-c01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-codegen-c01.txt" - ] - }, - { - "title": "생성자가 거부하는 것과 검증기가 보고하는 것", - "kind": "concept", - "slug": "grpc-discovery-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-discovery#L85`" - ], - "owning-module": "`grpc-discovery`", - "classification": "`GrpcResolverProfile` 정규 생성자가 네 조합을 아예 만들 수 없게 한다 — 주소 0 이하, 단일 엔드포인트 리졸버에 복수 주소, 음수 갱신 주기, DNS 인데 갱신 주기 0. `GrpcKubernetesProfile` 정규 생성자는 셋을 막는다 — 메시 라우팅에 in-process 재시도 소유자, 긴 스트림인데 재접속 예산 0, 긴 스트림인데 배수 유예 0. 두 겹의 역할 분담이 이 저장소의 다른 곳에 적힌 규칙과 같다 — 위험한 조합은 정책이 아니라 생성자가 거부하게 만든다. 그리고 그 분담 때문에 검증기의 재시도 소유자 규칙은 일부 조합에서만 발화한다. `MESH` + `GRPC_PLATFORM` 은 생성자가 먼저 던지므로(둘 다 in-process 재시도) 검증기까지 오지 않고, `MESH` + `NONE` 이나 `K8S_VIP` + `SERVICE_MESH` 는 생성자를 통과해 검증기가 잡는다. 도달 불가 분기가 아니라 역할 분담이다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-grpc-discovery-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-discovery-c01", - "grpc-discovery-c01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-discovery-c01.txt" - ] - }, - { - "title": "스키마가 계약이다", - "kind": "concept", - "slug": "grpc-operation-ledger-jpa-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-operation-ledger-jpa#L53`" - ], - "owning-module": "`grpc-operation-ledger-jpa`", - "classification": "마이그레이션 헤더가 왜 애플리케이션 검사가 아니라 제약인지 적는다. 커밋 행이 결과를 반드시 갖는다는 검사를 자바 record 와 DB 양쪽에 둔 이유도 적혀 있다 — 마이그레이션·백필·지원 스크립트가 쓴 행은 record 를 지나지 않는다. 전용 Flyway 위치(`db/migration/grpc`)를 쓰는 이유도 적혀 있다. gRPC 플랫폼을 채택하지 않은 배포가 이 테이블을 만들도록 강요받지 않기 위해서다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-grpc-operation-ledger-jpa-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-operation-ledger-jpa-c01" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-operation-ledger-jpa-c01.txt" - ] - }, - { - "title": "저장 키와 유니크 제약이 같은 행을 가리킨다", - "kind": "concept", - "slug": "grpc-operation-ledger-jpa-c02", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-operation-ledger-jpa#L76`" - ], - "owning-module": "`grpc-operation-ledger-jpa`", - "classification": "`GrpcOperationIdentity`(grpc-core-api): 즉 기본 키는 유니크 제약의 세 컬럼을 이어 붙인 파생값이다. 엔티티 javadoc 이 그 이중 저장을 설명한다 — 복합 쪽이 원자성을 주고, 파생 키가 조회에 단일 컬럼 기본 키를 준다. 같은 신원의 두 번째 청구는 **같은 기본 키 행**을 겨냥한다. §17.1 이 그 사실에서 나온다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-grpc-operation-ledger-jpa-c02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-operation-ledger-jpa-c02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-operation-ledger-jpa-c02.txt" - ] - }, - { - "title": "상태 전이", - "kind": "concept", - "slug": "grpc-operation-ledger-jpa-c03", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-operation-ledger-jpa#L108`" - ], - "owning-module": "`grpc-operation-ledger-jpa`", - "classification": "`IN_PROGRESS` 에서만 전이할 수 있다(`requireInProgress`). 커밋은 결과 참조가 비면 거부한다. `EnumType.STRING` 을 쓰는 이유가 javadoc 에 있다 — 서수 컬럼은 열거형에 값이 끼어들면 저장된 모든 행을 조용히 다른 값으로 만든다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-grpc-operation-ledger-jpa-c03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-operation-ledger-jpa-c03", - "grpc-operation-ledger-jpa-c03-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-operation-ledger-jpa-c03.txt" - ] - }, - { - "title": "검증기가 담은 규칙", - "kind": "concept", - "slug": "grpc-spring-boot-starter-c02", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-spring-boot-starter#L87`" - ], - "owning-module": "`grpc-spring-boot-starter`", - "classification": "javadoc 이 선정 기준을 적는다. 전송·보안 — production 전송이 아니면 거부, 배포 환경에서 TLS 미사용·trust-all·반사 전체 공개 거부 실행기 — 큐 용량 1 미만(무제한) 거부, 풀 크기 양수 요구 메서드 — 단항인데 사용 가능한 마감이 0, 명시적 재시도가 멱등 프로파일과 모순, 멱등 키 필수인데 원장 비활성, Stable 범위 밖 RPC 종류 채널 — Stable 스킴 요구, 두 재시도 소유자가 동시에 in-process 재시도 고급 격리 — Stable 스타터가 advanced 의존을 끌면 위반 그리고 한 번에 전부 모아 실패한다 — \"so a deployment learns the whole list in one restart.\"", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-grpc-spring-boot-starter-c02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-spring-boot-starter-c02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-spring-boot-starter-c02.txt" - ] - }, - { - "title": "릴리스 게이트 — 문서가 후속이 아니라 차단 사유다", - "kind": "concept", - "slug": "grpc-testkit-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-testkit#L102`" - ], - "owning-module": "`grpc-testkit`", - "classification": "차단 사유가 다섯 갈래다 — 호환성 표의 누락 결과, 생산되지 않은 증거 등급, 스키마 발행 거부, 런북 부재, 결정 기록 부재, 지원 표 부재. `requireCertified` 는 능력이 이번 릴리스가 낸 증거로 인증되지 않으면 던지고, 메시지에 실제로 돈 등급을 나열한다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-grpc-testkit-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-testkit-c01" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-testkit-c01.txt" - ] - }, - { - "title": "트랜잭션·동시성·수명주기", - "kind": "concept", - "slug": "messaging-transport-spi-c06", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-transport-spi#L352`" - ], - "owning-module": "`messaging-transport-spi`", - "classification": "이 leaf는 messaging family에서 **동시성 밀도가 가장 높다.** **주목할 비대칭:** `DefaultMessagingRuntimeRegistry`가 `current`는 lock-free(`ConcurrentHashMap`)로, `draining`은 `synchronized ArrayList`로 다룬다. `draining`은 회전 때만 접근하므로 경합이 없다 — 합리적 선택이지만 주석이 없다. 수명주기는 §4.4의 8단계가 **선언**이고 §12.1이 실현 상태를 다룬다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-messaging-transport-spi-c06.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-transport-spi-c06" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-transport-spi-c06.txt" - ] - }, - { - "title": "Git/설계 문서에서 확인한 변화와 실패 기록", - "kind": "concept", - "slug": "messaging-testkit-c08", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-testkit#L848`" - ], - "owning-module": "`messaging-testkit`", - "classification": "커밋 메시지는 정보가 거의 없다. 그러나 이 리프는 **코드 주석이 커밋 로그를 대신하는 드문 사례**다. 세 개의 javadoc 이 각각 \"무엇이 틀렸었고 왜 지금 형태인가\" 를 남겼다. 여섯 곳이 같은 결함의 여섯 얼굴이다: **자기 자신을 검증하는 상수**. 그리고 여섯 곳 모두 지금은 매니페스트를 가리킨다. `messaging-rabbit` 이 Stable 에서 Experimental 로 **강등된 흔적**도 남아 있다: `CompatibilityMatrixTest.theStableSetIsExactlyWhatALaneHasCertified` 의 `.as(\"RabbitMQ passes the shared contract, but no fault scenario has been run against it\")`. 강등의 근거가 \"계약은 통과하지만 결함 증거가 없다\" 로 정확히 적혀 있다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-messaging-testkit-c08.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-testkit-c08" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-testkit-c08.txt" - ] - }, - { - "title": "계약·불변식·상태 모델", - "kind": "concept", - "slug": "messaging-transport-spi-c03", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-transport-spi#L124`" - ], - "owning-module": "`messaging-transport-spi`", - "classification": "세 타입이 그 모델을 이룬다. 이 leaf의 유일한 실질 구현이고 동시성 설계가 조밀하다. `ConcurrentHashMap.put`이 원자적이므로 호출자는 옛 세대 또는 새 세대만 본다 — javadoc: \"never a half-rebuilt connection pool\". `computeIfPresent`의 리맵 함수가 **버킷 잠금 안에서** 실행되므로, 조회와 증가가 원자적이다. `get` 후 증가였다면 그 사이에 `install`이 세대를 교체해 이미 은퇴한 세대의 계수를 올릴 수 있다. `closed`가 CAS로 보호되므로 **정확히 한 번만** `runtime.close()`가 불린다. 테스트가 그것을 직접 단언한다(`aRetiredGenerationIsClosedExactlyOnce`, `as(\"a second close on a real connection pool throws from a shutdown hook\")`). `Lease.close()`도 자체 `AtomicBoolean released`로 멱등이다 — 두 층의 멱등성이다. **세대별 은퇴 시각** 이전 결함의 기록이다. 하나의 타임스탬프를 전체 목록에 적용하면 회전이 겹칠 때 판정이 호출자가 우연히 넘긴 값에 좌우된다. **닫힌 세대의 목록 제거** `drainingCount()`가 관측 지표이므로, 이미 닫힌 세대가 목록에 남으면 지표가 영원히 0으로 안 떨어진다. **`close()`가 현재 세대까지 닫는다** 이것도 이전 결함이다. 회전 없이 종료하는 프로세스(=대부분의 프로세스)가 연결을 정리하지 않았다. **동시성 미세 결함 하나.** `close()`가 `draining`은 `synchronized`로 비우지만 `current`는 `List.copyOf(current.keySet())` 후 하나씩 `remove`한다. 그 사이에 `install`이 새 세대를 넣으면 그 세대는 닫히지 않는다. 종료 중 설치는 정상 시나리오가 아니므로 실질 위험은 낮다 — §17의 P3. `tryBeginWork`가 **이중 검사**다. 증가 후 다시 확인해서, 증가와 `beginDrain` 사이의 경합에서 계수를 되돌린다. 이 패턴이 없으면 드레인 시작 직후 …", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-messaging-transport-spi-c03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-transport-spi-c03", - "messaging-transport-spi-c03-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-transport-spi-c03.txt" - ] - }, - { - "title": "모듈의 정체와 경계", - "kind": "concept", - "slug": "messaging-transport-spi-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-transport-spi#L62`" - ], - "owning-module": "`messaging-transport-spi`", - "classification": "브로커 어댑터가 구현할 **SPI**와, 그 어댑터들의 **수명주기·세대 관리**를 소유한다. 벤더 의존성이 0이다. 가장 중요한 경계 규칙이 `MessagingTransport`의 javadoc에 있다. 13개 타입 중 어느 것도 브로커 네이티브 타입을 시그니처에 노출하지 않는다. `BrokerPosition`(core-api)이 `Map diagnosticAttributes()`로 좌표를 문자열로만 내보내는 것과 같은 규율이다. 두 번째 경계는 **인코딩 위치**다. `TransportPublishRequest`도 대칭이다 — \"The payload arrives already encoded and the profile arrives already validated, so an adapter never chooses a codec or a limit for itself. That is what keeps two adapters from disagreeing about what 'the same message' means.\"", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-messaging-transport-spi-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-transport-spi-c01", - "messaging-transport-spi-c01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-transport-spi-c01.txt" - ] - }, - { - "title": "의존성과 런타임 배선", - "kind": "concept", - "slug": "messaging-schema-protobuf-c02", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-schema-protobuf#L76`" - ], - "owning-module": "`messaging-schema-protobuf`", - "classification": "들어오는 것: `messaging-core-api`(api), `messaging-schema-api`(api), `protobuf-java:4.29.3`(api). 나가는 것: **없다.** 어떤 leaf의 `allowed_dependencies`에도 없고 starter 목록에도 없다. 런타임 배선: 없음. bean 없음(Spring 주석 0개). lockfile이 확인하는 실제 해석: 컴파일/런타임은 4.29.3, annotation processor 경로만 4.33.2다. §12.4에서 저장소 전체의 protobuf 버전 지형을 다룬다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-messaging-schema-protobuf-c02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-schema-protobuf-c02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-schema-protobuf-c02.txt" - ] - }, - { - "title": "주요 실행 경로", - "kind": "concept", - "slug": "messaging-transport-spi-c04", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-transport-spi#L326`" - ], - "owning-module": "`messaging-transport-spi`", - "classification": "**발행:** 상위(`DefaultMessagePublisher`)가 `TransportPublishRequest`를 만들어 `MessagingTransport.publish` → 어댑터가 `TransportPublishResult(PublishResult)` 반환 **수신:** 상위가 `TransportConsumerSpec(profile, sink)`로 `register` → 어댑터가 메시지마다 `sink.apply(TransportDelivery)` → 상위가 `TransportSettlement`으로 정산 **회전:** 새 `MessagingRuntime` 생성 → `registry.install(runtime, now)` → 옛 세대 `retire` → lease가 0이면 즉시 close, 아니면 `draining`에 적재 → 스케줄러가 `closeExpiredDraining(now)` 호출 **종료:** (실제 경로) `MessagingShutdownLifecycle.stop()` → `admission.stopAcceptingNewWork()` → `drain.beginDrain(now)` → 50 ms 폴링으로 `isDrained` 대기 → 마감 도달 시 중단", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-messaging-transport-spi-c04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-transport-spi-c04" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-transport-spi-c04.txt" - ] - }, - { - "title": "실패 경로와 복구/번역", - "kind": "concept", - "slug": "messaging-transport-spi-c05", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-transport-spi#L338`" - ], - "owning-module": "`messaging-transport-spi`", - "classification": "이 leaf가 직접 던지는 예외는 **하나**다. 나머지는 `IllegalArgumentException`(생성자 인자 검증)과 `NullPointerException`(`Objects.requireNonNull`)이다. 이 leaf가 다루는 실패의 대부분은 **예외가 아니라 상태**다 — 드레인 마감 초과는 `abandonedWorkAtDeadline(now)`가 true를 반환하는 것이고, 세대 강제 종료는 `closeExpiredDraining`의 반환 계수다. **포기가 조용하지 않다는 것이 설계다.** 마감에 도달한 작업은 정산되지 않은 채 버려지고, 브로커가 재전달한다. `GracefulShutdownCoordinator` javadoc: \"rather than the platform pretending it completed.\"", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-messaging-transport-spi-c05.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-transport-spi-c05" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-transport-spi-c05.txt" - ] - }, - { - "title": "패키지/컴포넌트 지도", - "kind": "concept", - "slug": "messaging-core-api-c03", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-core-api#L123`" - ], - "owning-module": "`messaging-core-api`", - "classification": "`MessageEnvelope`가 중심이고 나머지 11개가 그 필드 타입이다. 부속: `UuidV7`(생성기), `WireSafeText`(검증 유틸). 봉투는 불변이고 네 가지 파생 메서드가 있다 — `withPayload`, `withContentType`, `withTenant`, `withHeaders`. 넷 다 `messageId`를 복사한다. `withPayload`의 javadoc이 그 이유를 적는다: \"Encoding, decoding, Claim Check offloading, and DLQ forwarding all need this, and every one of them must keep `messageId()` intact — which is exactly what this method guarantees by construction\"(`MessageEnvelope.java:80-82`). `HeaderName`, `HeaderValue`, `MessageHeaders`, `ReservedHeaders`, `CanonicalEnvelopeHeaders`. `ReservedHeaders`는 23개 이름 상수와 `msg.` **prefix 전체**를 소유한다. `CanonicalEnvelopeHeaders`는 그 예약 네임스페이스를 둘로 쪼갠다 — 봉투 필드가 이미 갖고 있는 15개(`ENVELOPE_FIELDS`)와, 봉투에 대응 필드가 없어서 헤더로만 이동할 수 있는 나머지 8개(`REDRIVE_ID`, `REDRIVE_COUNT`, `RETRY_ATTEMPT`, `FIRST_FAILURE_AT`, `LAST_FAILURE_AT`, `FAILURE_CATEGORY`, `FAILURE_CODE`, `ORIGIN_DESTINATION`). `MessageDestination`, `DestinationName`, `DestinationKind`(7), `MessagingCapabilities`(boolean 12), `DestinationCapabilities`, `ConfirmationRequirement`(3), `CapabilityRegistry`. 퍼블리셔 4종(`MessagePublisher`, `…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-messaging-core-api-c03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-core-api-c03" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-core-api-c03.txt" - ] - }, - { - "title": "커밋은 연속 워터마크로만 전진한다", - "kind": "concept", - "slug": "messaging-kafka-c02", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-kafka#L87`" - ], - "owning-module": "`messaging-kafka`", - "classification": "그 대가도 적혀 있다 — 느린 메시지 하나가 그 파티션의 워터마크를 붙든다. 그것이 옳은 교환이라는 근거는 대안이 메시지를 잃는다는 것이고, 지연은 소비자 랙으로 보인다는 것이다. 그리고 등록만 되고 제출되지 않은 오프셋을 되돌리는 경로가 있다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "state-machines-and-ownership/concept/concept-messaging-kafka-c02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-kafka-c02", - "messaging-kafka-c02-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-kafka-c02.txt" - ] - } - ], - "reference": [], - "question": [], - "decision": [] - } - }, - "transaction-and-consistency-models": { - "topic": "transaction-and-consistency-models", - "title": "", - "readerQuestion": "", - "kinds": { - "case": [], - "concept": [ - { - "title": "(8.1) 도달성 — 정책의 실제 적용 지점", - "kind": "concept", - "slug": "adapter-inbound-websocket-c02", - "readiness": "READY", - "source": [ - "`final/document.md#a17#L251`" - ], - "owning-module": "`adapter-inbound-websocket`", - "classification": "이 sub-scope에서 실제로 요청 경로에 있는 것은 **`stomp` 패키지가 참조하는 것뿐**이다. `stomp/WebSocketInboundAuthorizationInterceptor`(53)와 `stomp/AuthenticatedHandshakeInterceptor`(34)가 `WebSocketConfig`에 등록되고, 그 둘은 `stomp/WebSocketProperties`를 쓴다. `security`(4) · `authz`(1) · `idempotency`(4) · `budget`(1)의 플랫폼 정책 타입은 `stomp`가 참조하지 않는다. 즉 **인증 프로파일 · 티켓 · origin 정책 · 메시지 권한 · 연결 예산 · 명령 멱등성이 모두 요청 경로 밖이다.**", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transaction-and-consistency-models/concept/concept-adapter-inbound-websocket-c02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-websocket-c02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-websocket-c02.txt" - ] - }, - { - "title": "스크립트와 트랜잭션 — 등록이 배포 단계이고, 창(window)은 노드에 고정된다", - "kind": "concept", - "slug": "adapter-outbound-cache-redis-c13", - "readiness": "READY", - "source": [ - "`final/document.md#a10#L701`" - ], - "owning-module": "`adapter-outbound-cache-redis`", - "classification": "`RedisScriptRegistry`의 규칙 — \"Registration is a deployment step, not a request-time one. A script that was never registered has no digest and therefore **no way to reach the server**, which is what makes 'only reviewed scripts run' a structural property rather than a convention.\" 같은 identity에 다른 body를 등록하면 거부하고, 실행 시점에도 body가 등록본과 같은지 다시 본다. README가 주장하는 복구 사슬 `EVALSHA → NOSCRIPT → SCRIPT LOAD → digest verify → EVALSHA`는 **실재한다** — `LettuceRedisScriptOperations:26` javadoc이 \"NOSCRIPT is the one failure retried automatically\"라고 적고, `:129-133`이 `RedisNoScriptException` 또는 메시지 접두 `NOSCRIPT`를 잡아 `:100`에서 `registry.forget(script.id())`를 호출한다. 다음 호출이 `digest(...)`에서 다시 `SCRIPT LOAD`한다. `RedisTransactionRunner`는 Cluster에서의 `MULTI` 문제를 정면으로 다룬다. javadoc이 문제와 해법을 적는다 — 다른 레인은 명령마다 슬롯 소유 노드로 라우팅하는데 \"that is exactly what a `MULTI` window must not do: the queued commands would be spread across nodes and none of them would be part of the same window.\" 해법은 연결이 아니라 **라우팅 결정**이었다 — 감시 키(또는 명시적 슬롯 태그)에서 노드를 정해 레인을 고정한다. 감시 키가 없는 Cluster 트랜잭션은 거부하고 그 이유를 적는다 — \"the keys the callback will queue are not …", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transaction-and-consistency-models/concept/concept-adapter-outbound-cache-redis-c13.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-cache-redis-c13", - "adapter-outbound-cache-redis-c13-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-cache-redis-c13.txt" - ] - }, - { - "title": "Capability API — 실행 기능과 지원 등급을 reportable contract로 분리", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c02", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L126`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "현재 enum은 16개 capability id를 갖는다. app-bootstrap의 `JpaPlatformAutoConfiguration.capabilities()` 역시 16개를 선언하므로 **enum catalog와 current composition count는 일치**한다. Stable composition은 대표적으로 다음을 기본 지원으로 보고한다. transaction retry completion evidence keyset pagination runtime-role verification observability Advanced capability는 PostgreSQL native write/work claim/JSONB/array-range, bulk DML, stateless session, COPY, L2 cache, Envers 등을 constraints와 함께 보고한다. 이 분리는 “classpath에 코드가 있다”와 “현재 composition이 기본 지원한다고 약속한다”를 동일시하지 않는다. capability enum은 vocabulary이고, `CapabilitySupport`가 support level을 결합하며, app-bootstrap composition이 실제 현재 report를 구성한다. constructor가 보장하는 것은: capability non-null level non-null constraints list defensive copy 각 constraint non-null / non-blank `usableByDefault()`는 STABLE만 true다. Advanced/Experimental이 “존재하므로 기본 사용 가능”으로 오해되지 않게 support level을 코드에 남긴다. `CapabilitySupport`는 단순 문서용 record가 아니다. 실제 production 흐름은: `JpaPlatformReport`는 JDBC URL/user/password/SQL/entity catalog를 필드로 갖지 않도록 설계되어 있고, privilege detail도 boolean으로 축약한다. 즉 management endpoint의 reconnaissance surface를 …", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c02", - "adapter-outbound-persistence-jpa-c02-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c02.txt" - ] - }, - { - "title": "B. persistence-jpa public API boundary", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c10", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L763`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "input/output vocabulary: `PersistenceOperationName` `TransactionProfile` `RetryProfile` `JpaPersistenceException` `TransactionCompletionEvidence` `JpaPlatformRuntimeAutoConfiguration`은 `PlatformTransactionManager`가 있으면 `SpringJpaTransactionExecutor` bean을 만들고, 그 executor가 있으면 `FullTransactionRetryCoordinator` bean도 만든다. 따라서 source tree 수준에서는 B가 단순 historical class가 아니라 **현재 runtime bean graph에도 포함되는 구현**이다. 그러나 repository production call search에서는 `FullTransactionRetryCoordinator.execute(...)`를 실제 business/application code가 호출하는 경로가 확인되지 않았다. 반대로 application-core transaction port는 sample/use-case/composition에서 canonical contract로 사용된다. 이 공존 자체는 곧바로 defect가 아니다. `api/**`는 intended external surface이므로 fork/application이 B를 programmatically 사용할 수 있다. 문제는 문서가 두 boundary의 관계를 일관되게 설명하지 못하고, 일부 composition helper는 실제 type relationship과 다른 설명을 한다는 점이다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c10.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c10" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c10.txt" - ] - }, - { - "title": "`SpringTransactionPort`: application-core의 실제 Spring 구현", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c11", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L788`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "`SpringTransactionPort`는 `PolicyTransactionPort`를 구현하며 `JpaAdapterComponentsConfig`의 narrow component scan으로 등록된다. 이 wiring은 중요하다. root `CaSkeletonApplication`은 persistence package를 broad scan에서 의도적으로 제외한다. 그래서 adapter leaf 내부의 `@Component`를 “annotation이 있으니 알아서 등록될 것”이라고 볼 수 없다. `JpaAdapterComponentsConfig` source에는 과거 실제 회귀가 기록돼 있다. persistence package를 broad scan에서 제외 `SpringTransactionPort` 같은 component를 별도 scan하지 않음 처음 transaction port가 필요한 capability가 조립될 때 unsatisfied dependency로 드러남 해결: JPA master switch 아래에서만 persistence adapter package를 narrow scan 즉 이 module에서 Spring stereotype의 존재와 runtime reachability는 별개다. current root는 `PersistenceJpaRootAutoConfiguration -> JpaAdapterComponentsConfig -> component scan` 체인을 통해 이를 해결한다. `TransactionPort` primitive는 다음으로 매핑된다. 특히 vendor default isolation에 맡기지 않고 READ_COMMITTED를 명시한다. `inRootWrite`는 REQUIRED이지만 일반 `inWrite`와 의미가 다르다. 시작 전에 `TransactionSynchronizationManager.isActualTransactionActive()`를 확인해 ambient physical transaction이 있으면 manager/action 호출 전에 거부한다. “root boundary”를 REQUIRED의 join semantics로 조용히 바꾸지 않는다. focused test는 실제…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c11.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c11" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c11.txt" - ] - }, - { - "title": "retry classification은 structured state로 제한한다", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c14", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L938`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "`TransactionRetryClassifier`는 cause chain에서 SQLSTATE를 찾지만 automatic replay candidate는: `08007` 같은 transaction-resolution-unknown은 candidate가 아니다. `SpringPolicyTransactionPort`는 ordinary command에서 40001이 나더라도 `COMMAND_SERIALIZABLE_REPLAY_SAFE`가 아니면 retry하지 않는다. failure 종류뿐 아니라 **업무 side-effect가 replay-safe하다고 application policy가 선언했는가**가 함께 필요하다. 이것은 “DB가 retryable이라고 말하니 use case를 다시 실행”하는 구조와 다르다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c14.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c14" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c14.txt" - ] - }, - { - "title": "public JPA path: `SpringJpaTransactionExecutor`", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c15", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L953`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "이 executor는 한 번의 physical attempt만 담당한다. 자체 retry는 하지 않는다. attempt boundary에서 operation, attempt number, elapsed time, reconciliation key를 알고 있으므로 raw provider exception을 `JpaPersistenceException`으로 변환하는 위치로 사용된다. vendor translator가 조립되면 PostgreSQL 40001/40P01 같은 structured SQLSTATE가 stable exception으로 바뀌어 coordinator가 처리할 수 있다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c15.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c15" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c15.txt" - ] - }, - { - "title": "`FullTransactionRetryCoordinator`: whole-use-case retry 의도", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c16", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L974`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "coordinator는 `JpaPersistenceException`만 catch하고, retry decision에 따라 **새 transaction / 새 persistence context에서 전체 work를 다시 호출**한다. 설계상 중요한 guard: completion unknown -> no retry irreversible side effect context -> no retry retry budget elapsed -> stop max attempts -> stop backoff interrupt -> stop retry listener는 observation only 이 모델 자체의 unit tests는 강하다. serialization/deadlock retry, exhaustion, completion unknown no-retry, interrupted sleep, irreversible side effect 등을 검증한다. 하지만 current implementation에는 public composition contract와 맞지 않는 별도 defect가 있다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c16.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c16" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c16.txt" - ] - }, - { - "title": "reconciliation record production path = 0", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c18", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L1184`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "`CompletionUnknownRecord`와 `CompletionUnknownRecorder`는 current production에서 자신들의 정의 외 consumer/implementation이 없다. 그런데 documentation은 훨씬 강한 계약을 선언한다. support matrix: 그리고 operator procedure는 그 record의 `transactionKey`를 사용하라고 한다. 현재 이 record를 실제로 쓰는 production channel은 확인되지 않았다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c18.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c18" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c18.txt" - ] - }, - { - "title": "zero-reference지만 dead가 아닌 `JpaTransactionConfig`", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c20", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L1334`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "반대로 `JpaTransactionConfig`도 direct production reference는 거의 없다. 하지만 이 class는: 이고 `JpaAdapterComponentsConfig`가 transaction package를 component scan한다. 따라서 direct Java call/import가 0이어도 runtime reachability가 있다. 이 class source 자체도 historical reason을 기록한다. root `@ConfigurationPropertiesScan`에서 optional persistence tree 제외 JPA on 상태에서도 settings가 아무도 bind하지 않던 문제 발생 transaction port construction 실패 package-local configuration으로 JPA master switch 안에서만 settings enable 이 사례는 mandatory public-reachability probe가 필요한 이유를 잘 보여준다. static reference count만으로 dead code를 찾으면 Spring discovery path를 오탐한다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c20.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c20" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c20.txt" - ] - }, - { - "title": "두 failure translator 계열은 현재 역할이 다르다", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c21", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L1358`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "이 scope에는 이름이 비슷한 두 translation mechanism이 있다. 목적은 SQLSTATE/optimistic conflict를 retry/completion semantics에 필요한 stable persistence failure로 바꾸는 것이다. consumer는 adapter/application error boundary 쪽이다. 따라서 동일 이름 영역을 다루지만 current evidence로는 competing duplicate implementation이 아니다. **transaction retry algebra와 platform operational error mapping이라는 서로 다른 output contract**를 가진다. PostgreSQL vendor translator와 exact SQLSTATE catalog correctness는 vendor sub-scope에서 계속 검증한다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c21.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c21" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c21.txt" - ] - }, - { - "title": "`failure.PersistenceExceptionTranslator`", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c22", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L1384`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "consumer는 adapter/application error boundary 쪽이다. 따라서 동일 이름 영역을 다루지만 current evidence로는 competing duplicate implementation이 아니다. **transaction retry algebra와 platform operational error mapping이라는 서로 다른 output contract**를 가진다. PostgreSQL vendor translator와 exact SQLSTATE catalog correctness는 vendor sub-scope에서 계속 검증한다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c22.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c22" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c22.txt" - ] - }, - { - "title": "runtime bean-factory-owned", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c23", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L1416`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "`JpaPlatformRuntimeAutoConfiguration`: `SpringJpaTransactionExecutor` — `PlatformTransactionManager`가 있을 때 `FullTransactionRetryCoordinator` — executor가 있을 때 default empty `RetryEventListener`", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c23.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c23" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c23.txt" - ] - }, - { - "title": "bulk DML과 StatelessSession은 일반 repository path와 다른 비용 모델을 명시한다", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c26", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L1819`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "`HibernateBulkDmlExecutor`는 arbitrary JPQL string을 아무 데서나 실행하는 helper가 아니다. operation name 등록 affected-row expectation persistence-context cleanup transaction requirement 를 contract로 둔다. bulk DML은 managed entity lifecycle을 우회하므로 ordinary entity save와 같은 audit/lifecycle guarantee를 기대하면 안 된다. support matrix도 이를 Advanced capability로 분리한다. 현재 production business consumer는 확인되지 않았고 PostgreSQL integration fixture에서 실제 behavior를 qualification한다. 따라서 “runtime에서 사용 중”이라고 주장하지 않는다. `HibernateStatelessSessionRunner`는 오히려 이 platform에서 transaction ownership 예외를 명시적으로 드러낸다. 일반 repository adapter: StatelessSession runner: 과거 review에서는 caller가 선언한 maxRows가 실제 affected rows와 연결되지 않는 문제가 있었다. 현재는 `StatelessWorkResult(value, affectedRows)`를 요구하고 cap 초과 시 commit 전에 rollback한다. 즉 과거의 “이름만 row cap” 문제는 현재 코드에서 수정돼 있다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c26.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c26", - "adapter-outbound-persistence-jpa-c26-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c26.txt" - ] - }, - { - "title": "Same-store inbox / polling outbox: 구현 계약은 강하지만 현재 미조립 candidate에 replay holes가 있다", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c37", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L2734`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "`PostgreSqlSameStoreInboxAdapter`와 `PostgreSqlPollingDeliveryAdapter`는 `application-core`의 owner-safe transition contract를 구현하지만, 현재 production composition에서 bean construction이나 stereotype은 확인되지 않았다. 따라서 아래 finding은 **현재 배포 기본 경로의 즉시 장애가 아니라, 이 candidate adapter를 채택할 때 활성화되는 latent defect**로 분리한다. `markProcessing()`은 같은 `START + operationId`를 발견하면 `classifyMismatch()`보다 먼저 `owner(row)`를 반환한다. 이 때문에 scope/operation id만 맞춘 forged owner로 replay하면 DB에 저장된 실제 owner token을 돌려받을 수 있다. 실제 PostgreSQL probe: 즉 duplicate handling이 owner capability recovery oracle처럼 동작한다. 채택 전에는 duplicate replay에서도 persisted owner tuple/revision과 supplied owner를 먼저 검증하도록 고쳐야 한다. `markRetryable`/`markDead`의 `retention`은 실제 SQL update에는 들어가지만 transition digest에는 들어가지 않는다. 동일 operation id로 retention만 바꾼 replay가 same-operation으로 흡수된다. retention은 terminal row 보존 기간을 결정하는 semantic argument이므로 digest에 canonical millis를 포함해야 한다. `markRetryable()`은 `nextAttemptAt`을 DB에 기록하지만 transition digest는 kind + operation + owner + errorCode만 포함한다. 재시도 시각은 delivery scheduling 자체를 바꾸는 semantic argument다. 동일 operation replay consisten…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c37.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c37" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c37.txt" - ] - }, - { - "title": "Baseline composition을 먼저 분리해야 하는 이유", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c42", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L2971`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "`JpaAdapterComponentsConfig`는 adapter 전체를 넓게 scan하지 않고 다음 package만 명시적으로 component scan한다. `idempotency` `transaction` 따라서 같은 leaf 안에 있어도 reachability가 다르다. `OutboxStoreAdapter`는 baseline scan에 들어가고 `app-bootstrap`의 `OutboxConfig`가 `OutboxStorePort`로 사용한다. `DurableOperationStoreAdapter`, `JpaLiveEventReplayAdapter`는 현재 baseline component scan에 들어가지 않고 별도 production constructor/reference도 확인되지 않았다. `HibernateCacheGuard`, `HibernateEnversHistoryReader`와 Spring Data auditing candidate도 default composition에 들어가지 않는다. runtime-role verifier 자체는 app-bootstrap bean으로 구성되지만, policy를 적용하는 `requireSafe()` caller가 없다. 이 차이 때문에 아래 finding은 `production`, `conditional-production`, `latent`를 분리해 판정한다. 정적 composition snapshot은 `evidence/raw/072-baseline-capability-reachability.txt`에 남겼다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c42.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c42", - "adapter-outbound-persistence-jpa-c42-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c42.txt" - ] - }, - { - "title": "현재 production composition은 Experimental을 실행하지 않지만 opt-in 경계는 완전히 구조적이지 않다", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c51", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L3609`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "현재 repository 내부 production call graph에서는 `TenantDataSourceRegistry`, `TenantEntityManagerFactoryRegistry`, `SchemaMultiTenantConnectionProvider`, `ConsistencyAwareDataSourceRouter`, `RlsTenantSessionBinder`, `SchemaTenantMigrationOrchestrator` 등을 app-bootstrap이나 다른 production leaf가 조립하는 경로를 찾지 못했다. `backend.jpa.experimental.*` property도 production configuration에서 읽어 bean을 만드는 경로가 없고, 실제 문자열은 `ExperimentalFeature` enum의 property vocabulary에만 존재한다. 따라서 아래 semantic finding은 **현재 app-bootstrap runtime에서 즉시 활성화된 production defect가 아니라 latent experimental defect**로 분류한다. 이 구분은 중요하다. public API surface에 올라 있고 같은 artifact에 포함된 library code가 잘못된 것과, 현재 기본 애플리케이션이 그 code를 실제 실행하는 것은 다른 주장이다. 반면 structural opt-in은 완전히 닫혀 있지 않다. `PersistenceJpaConfig`의 Stable `@EntityScan`과 `@EnableJpaRepositories` 문자열 목록에는 이미 `dev.caskeleton.adapter.outbound.persistence.experimental`이 들어 있다. 현재 experimental package에는 `@Entity`, `@Repository`, `JpaRepository`, `@MappedSuperclass`가 없어서 당장 persistence unit에 들어오는 concrete JPA type은 없지만, 이후 experimental entity/repository 하나가 추가되면 별도 feature condition 없이 Stable pers…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c51.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c51", - "adapter-outbound-persistence-jpa-c51-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c51.txt" - ] - }, - { - "title": "실행 scope의 고정된 순서가 이 sub-scope의 중심이다", - "kind": "concept", - "slug": "adapter-outbound-persistence-mongo-c03", - "readiness": "READY", - "source": [ - "`final/document.md#a06#L586`" - ], - "owning-module": "`adapter-outbound-persistence-mongo`", - "classification": "`DefaultMongoImperativeExecutor.executeInternal(...)`은 순서를 고정한다 — collection profile 해석 → observation 개시 → consistency 바인딩 → callback 실행 → 실패 번역(최대 한 번) → observation 종료. javadoc이 이유를 적는다: \"Fixing it here is what makes the invariants hold for operations nobody has written yet.\" 세 가지 방어가 눈에 띈다. 이미 번역된 `MongoPersistenceException`은 그대로 통과시킨다. 재번역하면 bulk partial failure나 guardrail 거절처럼 **그것을 던진 계층이 더 잘 아는** category를, driver 코드에서 유도한 일반 category로 덮어쓰게 된다. Spring이 감싼 driver 예외를 `unwrap(...)`으로 되꺼낸다. Spring의 번역은 error label을 잃는데, label이야말로 replayable transaction과 unknown commit을 가르는 값이다. `MongoCompletion.successOutcomeFor(operationType)`가 read와 write의 성공 outcome을 나눈다. 과거에는 두 executor 모두 성공을 `WRITE_CONFIRMED`로 기록해, \"write가 acknowledge되고 있는가\"를 답하는 지표가 read 트래픽의 함수가 됐다. `default` 분기가 `READ_CONFIRMED`로 떨어지는 것도 의도적이다 — \"the honest answer is the one that claims least\". `MongoCollectionProfileRegistry`가 \"동적 collection 이름 금지\"를 강제 가능하게 만드는 지점이다. 애플리케이션은 profile을 부르고 물리 이름은 이 registry만 안다. `ScopedAccess.collection(String)`은 요청된 collection이 scope의 것과 다르면 거부하고, `ScopedMongoOperations`의 어떤 메서드도 collect…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-mongo-c03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-mongo-c03", - "adapter-outbound-persistence-mongo-c03-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-mongo-c03.txt" - ] - }, - { - "title": "transaction: framework vocabulary 대신 application semantic policy", - "kind": "concept", - "slug": "application-core-c03", - "readiness": "READY", - "source": [ - "`final/document.md#a03#L88`" - ], - "owning-module": "`application-core`", - "classification": "`TransactionPort`는 `inWrite`, `inRootWrite`, `inRead`, `inNew` 네 개의 framework-neutral boundary를 노출한다. `PolicyTransactionPort`는 기존 surface를 깨지 않고 `TransactionRequest -> TransactionResult` 정책 기반 API를 추가한다. `TransactionPolicyId`는 Spring propagation 숫자가 아니라 `COMMAND_DEFAULT`, `COMMAND_SERIALIZABLE_REPLAY_SAFE`, `QUERY_PRIMARY`, `QUERY_REPLICA_ELIGIBLE`, `OUTBOX_APPEND`, `INBOX_AND_HANDLER`, `MAINTENANCE_NEW`처럼 application semantic ID를 노출한다. `TransactionRequest` constructor는 read policy의 consistency allowlist, non-read의 readConsistency 금지, operationId-required policy의 stable id 존재를 fail-fast한다. `TransactionResult`는 commit 결과를 다섯 상태로 분리한다. `Committed`: physical commit을 확인한 결과. `Participating`: outer transaction에 참여했지만 아직 commit을 주장할 수 없는 결과. `DeterminateRollback`: rollback이 확정된 실패. `Indeterminate`: commit 여부를 확정할 수 없는 결과. `CommittedWithPostCommitFailure`: commit은 됐지만 이후 operational cleanup이 실패한 결과. 이 algebra의 핵심은 “exception이 발생했다 = rollback”으로 단순화하지 않는 것이다. 특히 `Indeterminate`는 last observed transaction phase와 optional reconciliation reference를 보존하며, `CompletionResolution`은 `STILL_UNKNOWN`을…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transaction-and-consistency-models/concept/concept-application-core-c03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "application-core-c03", - "application-core-c03-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/application-core-c03.txt" - ] - }, - { - "title": "인터셉터 순서 계약", - "kind": "concept", - "slug": "grpc-server-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-server#L53`" - ], - "owning-module": "`grpc-server`", - "classification": "열 단계이고 선언 순서가 계약이다. 각 위치의 이유가 열거형 javadoc 에 있다. 예외 경계가 가장 바깥 — 이후 단계의 실패가 매핑되지 않은 상태로 새지 않는다 인증 → 행위자·소속 → 인가 — 각 단계가 앞 단계의 답을 필요로 한다 승인이 마감보다 먼저 — 부하 중 서버가 일을 쓰기 전에 흘려보낸다 멱등이 검증보다 먼저 — 재생된 요청이 이미 받아들인 본문을 다시 검증하지 않고 저장된 결과를 돌려준다 검증이 어댑터 직전 — 사용 사례는 믿을 수 있는 메시지를 받는다 필수가 아닌 단계는 멱등 하나다 — 상태 변경 키 메서드가 없는 서버에는 할 일이 없기 때문이다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transaction-and-consistency-models/concept/concept-grpc-server-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-server-c01", - "grpc-server-c01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-server-c01.txt" - ] - }, - { - "title": "모듈의 정체와 경계", - "kind": "concept", - "slug": "messaging-outbox-jdbc-postgresql-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-outbox-jdbc-postgresql#L66`" - ], - "owning-module": "`messaging-outbox-jdbc-postgresql`", - "classification": "**트랜잭셔널 아웃박스의 PostgreSQL 구현**이다. 비즈니스 트랜잭션이 쓰고 릴레이가 배출한다. 여기에 더해 `messaging-admin-api` 의 파괴적 작업 저널 구현도 같이 산다 — 그 이유가 build.gradle 에 적혀 있다. 이 리프의 축은 하나다: **\"모르는 것을 실패로 취급하지 않는다.\"** 마지막 문장이 중요하다 — 이 리프가 자기 보장의 상한을 스스로 명시한다. 경계: 브로커를 모른다(`MessagePublisher` 포트만 안다). 스프링 컨텍스트를 모른다(`spring-jdbc`/`spring-tx` 는 `implementation` 이며 트랜잭션 동기화 조회에만 쓴다). 배선은 starter 몫이다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transaction-and-consistency-models/concept/concept-messaging-outbox-jdbc-postgresql-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-outbox-jdbc-postgresql-c01", - "messaging-outbox-jdbc-postgresql-c01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-outbox-jdbc-postgresql-c01.txt" - ] - }, - { - "title": "의존성과 런타임 배선", - "kind": "concept", - "slug": "messaging-outbox-jdbc-postgresql-c02", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-outbox-jdbc-postgresql#L102`" - ], - "owning-module": "`messaging-outbox-jdbc-postgresql`", - "classification": "testcontainers 주석이 이 리프의 성격을 요약한다 — \"신뢰성 패턴은 트랜잭션 경계와 유일성 제약에 대한 주장이고, 그것을 결판낼 수 있는 것은 실제 데이터베이스뿐이다.\" 그리고 그 레인이 **실제로 돈다**(§10). starter 가 만드는 빈(`EVD-312`): starter 가 만들지 **않는** 것: `JdbcOutboxRepository`, `OutboxEnvelopeFactory`, `JdbcAdminOperationJournal`. 셋 다 애플리케이션이 `DataSource`/`ProducerId` 를 알고 직접 등록해야 한다. `AdminOperationJournal` 의 기본값은 `InMemoryAdminOperationJournal` 이며, 프로덕션 프로파일에서는 `MessagingAdminDurabilityValidator` 가 그것을 거부한다(`final/document.md#a19-messaging-admin-runtime` §4.4 참조).", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transaction-and-consistency-models/concept/concept-messaging-outbox-jdbc-postgresql-c02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-outbox-jdbc-postgresql-c02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-outbox-jdbc-postgresql-c02.txt" - ] - }, - { - "title": "계약·불변식·상태 모델", - "kind": "concept", - "slug": "messaging-inbox-jdbc-postgresql-c03", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-inbox-jdbc-postgresql#L146`" - ], - "owning-module": "`messaging-inbox-jdbc-postgresql`", - "classification": "이 leaf에서 가장 중요한 안전 장치이고 이전 결함이 javadoc에 있다. **두 개의 오버로드가 있었고 호출되는 쪽이 틀린 쪽이었다.** 현재는 interface 메서드가 세 가지를 확인한다. 세 번째가 특히 정교하다 — **트랜잭션이 활성이어도 다른 DataSource에 묶여 있으면 거절한다.** 멀티 데이터소스 배포에서 실제로 발생하는 형태이고, 그 경우 예약과 부작용이 서로 다른 트랜잭션에 들어간다. 세 검사 전부 같은 코드 `INBOX_TRANSACTION_REQUIRED`를 쓴다 — 메시지만 다르다. **\"the path nobody exercises before production\"**가 이 leaf의 테스트 전략을 설명한다 — `InboxPostgresIT.aRolledBackTransactionLeavesNoReservationAndNoSideEffect`가 정확히 그 경로를 실 DB에서 돈다. `TransactionRunner`가 함수형 인터페이스이고 ` T inTransaction(Supplier work)` 하나다. 즉 이 leaf는 Spring `@Transactional`에 의존하지 않고 **경계 제공을 호출자에게 위임**한다. `JdbcInboxRepository.requireActiveTransaction`이 그 위임이 지켜졌는지를 런타임에 확인한다 — **위임과 검증이 짝을 이룬다.** 중복이 정상 결과라는 것도 명시돼 있다 — \"A duplicate is not an error. It is the expected consequence of at-least-once delivery, so the skip path is a normal outcome rather than an exception.\" 세 금지가 `messaging-reliability-api`의 `TransactionalMessageAction` javadoc이 구현자에게 요구한 것과 대칭이다 — 그쪽은 action에게, 이쪽은 handler에게. 예외 처리가 그 세 번째를 지킨다. `ActionFailedException`이 private `RuntimeException`이고, 바깥에서 잡아 `HandleResult.Retry`…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transaction-and-consistency-models/concept/concept-messaging-inbox-jdbc-postgresql-c03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-inbox-jdbc-postgresql-c03", - "messaging-inbox-jdbc-postgresql-c03-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-inbox-jdbc-postgresql-c03.txt" - ] - }, - { - "title": "주요 실행 경로", - "kind": "concept", - "slug": "messaging-outbox-jdbc-postgresql-c04", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-outbox-jdbc-postgresql#L430`" - ], - "owning-module": "`messaging-outbox-jdbc-postgresql`", - "classification": "**쓰기** — 비즈니스 트랜잭션 → `append(record)` → 트랜잭션 3중 검사 → `DataSourceUtils.getConnection` → INSERT(22컬럼). **배출** — `MessagingOutboxRelayLifecycle` → `worker.start()` → `runPass()` → `relay.runOnce(now)` → 청구/발행/종결 → `scheduler.backoff(unproductive)` → 다음 패스 자기 스케줄링. **정리** — `OutboxCleanupJob.runOnce(now)` → `cutoff = now - retention` → `purgePublishedBefore(cutoff)` **무제한 오버로드** ×(최대 `maxBatches`, 실제로는 2회) → §12.1(a). **admin 저널** — `begin` → INSERT ON CONFLICT / TAKE_OVER → `checkpoint` × N → `complete` 또는 `fail`.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transaction-and-consistency-models/concept/concept-messaging-outbox-jdbc-postgresql-c04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-outbox-jdbc-postgresql-c04" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-outbox-jdbc-postgresql-c04.txt" - ] - }, - { - "title": "Git/설계 문서에서 확인한 변화와 실패 기록", - "kind": "concept", - "slug": "messaging-outbox-jdbc-postgresql-c06", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-outbox-jdbc-postgresql#L767`" - ], - "owning-module": "`messaging-outbox-jdbc-postgresql`", - "classification": "SQL 마이그레이션과 javadoc 이 함께 이력을 이룬다. 여덟 개의 \"이전에는 이랬다\". 마지막 두 개(`OutboxRelay:117-123`, `OutboxRelayWorker:18-21`)가 이 저장소 전체에서 반복되는 결함 계열 — **\"만들어졌지만 아무도 부르지 않는다\"** — 을 명시적으로 이름 붙인 유일한 자리다. 그리고 이 리프에서는 그 둘이 실제로 고쳐졌다. §12.1(c)의 `requireExactlyOneRelay` 만 같은 상태로 남았다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "transaction-and-consistency-models/concept/concept-messaging-outbox-jdbc-postgresql-c06.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-outbox-jdbc-postgresql-c06" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-outbox-jdbc-postgresql-c06.txt" - ] - } - ], - "reference": [], - "question": [], - "decision": [] - } - }, - "delivery-and-settlement-models": { - "topic": "delivery-and-settlement-models", - "title": "", - "readerQuestion": "", - "kinds": { - "case": [], - "concept": [ - { - "title": "(8.4) 문서/구현 드리프트 — 보고되는 HTTP 프로파일", - "kind": "concept", - "slug": "adapter-inbound-graphql-c08", - "readiness": "READY", - "source": [ - "`final/document.md#a16#L612`" - ], - "owning-module": "`adapter-inbound-graphql`", - "classification": "`GraphQlPlatformConfigurationReport`(§8.1)가 `GraphQlHttpProfile.V1.name()`을 배포 상태의 일부로 보고한다. `GraphQlHttpProfile`은 autoconf=2로 참조되지만, 그 프로파일이 규정하는 전송 동작(상태 매핑 · Accept 협상 · 응답 형태)을 수행하는 코드는 미배선이다(§19.1). 그리고 그 보고서를 발행할 액추에이터 엔드포인트 자체도 등록되지 않는다(§8.1).", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "delivery-and-settlement-models/concept/concept-adapter-inbound-graphql-c08.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-graphql-c08" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-graphql-c08.txt" - ] - }, - { - "title": "실패: 재시도 가능성과 모호성이 배타로 강제된다", - "kind": "concept", - "slug": "adapter-outbound-cache-redis-c06", - "readiness": "READY", - "source": [ - "`final/document.md#a10#L332`" - ], - "owning-module": "`adapter-outbound-cache-redis`", - "classification": "`RedisFailureMetadata`는 \"Low-cardinality, payload-free description\"이고, 불변식 하나가 이 SDK의 재시도 규칙 전체다. 그리고 팩토리 두 개가 그 규칙을 실제 상황에 적용한다. `notSent(...)`는 `retryable = readOperation`으로 유도한다 — 서버에 닿지 않은 읽기는 재시도해도 안전하다. `storedDataCorruption(...)`은 **일부러 `notSent`가 아니고**, javadoc이 그 이유를 적는다. 그 팩토리는 실제로 쓰인다 — `JsonEnvelopeFraming:202`, `VersionedJsonCodec:103` 두 곳(sub-scope 05 범위)이 디코딩 실패에서 호출한다. 예외 계층은 12종이고 전부 `RedisOperationException`을 상속한다. 메시지는 reason + `command=` 계열 + `mode=` + `ambiguous=`만 조립하고, javadoc이 경계를 적는다 — \"keys, fields, members, values, arguments, and authentication material never appear.\"", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "delivery-and-settlement-models/concept/concept-adapter-outbound-cache-redis-c06.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-cache-redis-c06" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-cache-redis-c06.txt" - ] - }, - { - "title": "Confirmed — canonical digest가 길이 프레이밍이고, route token 충돌을 명시적으로 검사한다", - "kind": "concept", - "slug": "adapter-outbound-fileserver-c05", - "readiness": "READY", - "source": [ - "`final/document.md#a08#L300`" - ], - "owning-module": "`adapter-outbound-fileserver`", - "classification": "`FilePublicationCanonicalDigests.digestOrderedValues`는 값 개수를 먼저 넣고, 값마다 **길이(4바이트) + 엄격 UTF-8 바이트**를 넣는다. 구분자를 쓰지 않으므로 값 안에 어떤 문자가 있어도 경계가 흐려지지 않는다. `FilePublishRequestFingerprint`도 같은 방식이다. `routeToken`은 정책 다이제스트의 앞 31자에 `r`을 붙인 것이라 **잘린 값**이다. 그래서 `FileserverBindingCompiler.deriveUniqueRouteTokens`가 컴파일 시점에 토큰 충돌을 검사하고, 충돌하면 두 destination 이름을 모두 담아 거부한다. 잘림이 만들 수 있는 유일한 문제를 그 자리에서 닫는다. 컴파일 후에도 `compiled.forEach`로 각 destination의 토큰이 레지스트리와 같은지 다시 확인한다. `CompiledFileDestination`의 compact 생성자는 넘겨받은 `effectivePolicyDigest`를 **다시 계산해 대조**하고, `routeToken`이 그 다이제스트에서 유도됐는지, `formatPolicyDigest`가 정본과 같은지도 확인한다. 값이 아니라 관계를 검증한다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "delivery-and-settlement-models/concept/concept-adapter-outbound-fileserver-c05.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-fileserver-c05" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-fileserver-c05.txt" - ] - }, - { - "title": "Confirmed — 실패를 \"재시도 안전한가\"로 분류한다", - "kind": "concept", - "slug": "adapter-outbound-fileserver-c07", - "readiness": "READY", - "source": [ - "`final/document.md#a08#L517`" - ], - "owning-module": "`adapter-outbound-fileserver`", - "classification": "`AmbiguousFilesystemOperationDetector`는 `IOException`을 네 결과로 나눈다(`NOT_SENT` / `DEFINITELY_REJECTED` / `AMBIGUOUS_COMPLETION` / `RECONCILIATION_REQUIRED`). 기본값이 보수적이다 — 인식하지 못한 실패는 **변경 연산이면 ambiguous**다. javadoc이 비대칭을 적는다: \"the cost of a wrong 'safe to retry' is a corrupted object, while the cost of a wrong 'ambiguous' is one reconciliation entry.\" `mutating` 인자로 순수 읽기는 결코 ambiguous가 되지 않게 하고, stale handle은 변경 연산일 때 `RECONCILIATION_REQUIRED`로 격상한다 — 에러만으로는 결과를 알 수 없으므로 물리 증거를 다시 읽어야 한다. 다만 `isStaleHandle`·`isLostResponse`와 `FilesystemFailureClassifier.isOutOfSpace`가 **메시지 텍스트 매칭**에 의존한다(\"stale file handle\", \"estale\", \"timed out\", \"No space left on device\", \"Disk quota exceeded\"). 후자에는 주석이 붙어 있다 — \"The JDK has no dedicated exception for this, so the reason text is the only available signal.\" 로케일이나 JDK 판본에 따라 문구가 달라지면 분류가 기본값으로 떨어지는데, 기본값이 보수적(변경 연산 → ambiguous)이므로 안전한 방향이다. 기록만 한다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "delivery-and-settlement-models/concept/concept-adapter-outbound-fileserver-c07.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-fileserver-c07" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-fileserver-c07.txt" - ] - }, - { - "title": "두 발행 경로의 실패 정책이 정반대이고 그 이유가 적혀 있다", - "kind": "concept", - "slug": "adapter-outbound-messaging-c04", - "readiness": "READY", - "source": [ - "`final/document.md#a12#L284`" - ], - "owning-module": "`adapter-outbound-messaging`", - "classification": "`OutboundMessagePublisher.publish`에 이 저장소에서 반복해 본 종류의 수정 이력이 있다. 그리고 관측 자체가 결과를 바꾸지 못한다 — `observeQuietly`가 진단 예외를 흡수하며 \"Diagnostics are non-authoritative. **An appender that is out of disk must not change what the caller believes about the broker.**\" 비활성 sentinel 둘은 조용한 no-op이 아니라 `AdapterDisabledException`을 던지고, 서로 다른 클래스로 분리된 이유가 bean 조회 모호성이다(§2).", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "delivery-and-settlement-models/concept/concept-adapter-outbound-messaging-c04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-messaging-c04" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-messaging-c04.txt" - ] - }, - { - "title": "(8.1) 도달성 — provider가 준 `Retry-After`는 실제로 쓰이는가", - "kind": "concept", - "slug": "adapter-outbound-notification-c03", - "readiness": "READY", - "source": [ - "`final/document.md#a13#L582`" - ], - "owning-module": "`adapter-outbound-notification`", - "classification": "`ProviderResults.retryAfter`가 파싱한 값이 종단까지 도달하는지 추적했다. 도달한다: 그리고 `RetryBackoff.delay`가 소비한다: 힌트는 계산값보다 **길 때만** 채택되고, 그 뒤 설정된 `max`(기본 5분)로 **상한이 걸린다**. javadoc의 주장 — \"A provider-supplied `Retry-After` always wins over the computed value, but never over the configured maximum: a provider asking for an hour must not silently extend a delivery deadline\" — 이 코드와 정확히 일치한다. 악의적 provider가 `Retry-After: 999999999`로 배달을 수십 년 뒤로 미루는 경로는 **없다**. §17.1의 `AccessContext`와 대조되는, 회로가 닫힌 사례다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "delivery-and-settlement-models/concept/concept-adapter-outbound-notification-c03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-notification-c03" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-notification-c03.txt" - ] - }, - { - "title": "Query API — pagination 비용과 trust boundary를 type shape로 제한", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c04", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L302`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "offset/page number를 아예 표현하지 않으므로 keyset API를 사용하는 consumer가 실수로 large offset pagination으로 회귀하기 어렵다. `fetchSize()`는 요청 size + 1을 반환한다. 즉 별도 count query 없이 한 row를 더 읽어 `hasNext`를 판단하는 계약이다. hasNext=true -> nextCursor 필수 terminal slice -> nextCursor 금지 items defensive copy page number/total count가 없다는 것은 API omission이 아니라 의도된 성능 정책이다. “keyset을 쓰면서 매번 count(*)도 수행”하는 모순을 contract shape에서 제거한다. `QueryName`도 bounded registry key다. raw SQL을 metric/trace identity로 사용할 수 없다. `QueryObservation.start(QueryName)` → `QueryScope` 구조에서 scope는: failure(Throwable) 특히 `QueryScope.failure` 문서가 “throwable message를 log하지 말 것”을 직접 계약한다. Micrometer implementation이 이를 실제로 지키는지는 observation sub-scope에서 확인한다. `NoopQueryObservation`은 backend가 없을 때도 caller control flow가 갈라지지 않게 singleton no-op scope를 제공한다. app-bootstrap `JpaObservabilityAutoConfiguration`에서 actual fallback consumer가 존재한다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "delivery-and-settlement-models/concept/concept-adapter-outbound-persistence-jpa-c04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c04", - "adapter-outbound-persistence-jpa-c04-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c04.txt" - ] - }, - { - "title": "대부분의 optimization helper가 production에서 직접 소비되지 않는다는 사실은 이미 repository가 알고 있다", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c31", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L2151`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "negative-space search에서 다음 implementation roots는 repository production consumer가 확인되지 않았다. `HibernateJpaBatchExecutor` `JpaBatchProfileRegistry` `HibernateBulkDmlExecutor` `HibernateStatelessSessionRunner` `FetchPlanApplier` `JpaKeysetQuerySupport` `JpaRepositoryFragmentSupport` `JpaStreamExecutor` `SpecificationPolicy` `QuerydslJpaSupport` 하지만 이것을 곧바로 “dead code가 대량 존재한다”라고 해석하면 안 된다. 이 repository의 기존 study/review 문서도 이미 JPA platform helper가 **구현/qualification되어 있지만 sample production path가 대부분 채택하지 않은 상태**라고 기록한다. 또한 batch/bulk/stateless helper는 real PostgreSQL integration tests에서 직접 실행된다. 따라서 현재 판단은 capability별로 나눈다. 이들은 library capability로 유지할 수 있다. `NamedStatementInspector`처럼 global Hibernate hook이 필요한 기능 이 경우는 “아무 use case가 안 쓴다”와 다르다. feature를 사용하려면 composition이 먼저 존재해야 한다. transaction scope의 `TransactionProfileRegistry`처럼 history를 통해 실제 residue로 판정해야 한다. 즉 `grep refs=0`은 finding의 시작점이지 결론이 아니다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "delivery-and-settlement-models/concept/concept-adapter-outbound-persistence-jpa-c31.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c31" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c31.txt" - ] - }, - { - "title": "실제 app-bootstrap consumer rule은 별도 allowlist를 다시 가진다", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c32", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L2217`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "`CleanArchitectureTest.BOOTSTRAP_USES_ONLY_THE_PERSISTENCE_EXPORT_SURFACE`는 또 다른 `EXPORTED` set을 정의한다. 여기에는 root composition이 vendor entry point를 import해야 하므로: 즉 두 목록은 이미 동일하지 않다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "delivery-and-settlement-models/concept/concept-adapter-outbound-persistence-jpa-c32.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c32" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c32.txt" - ] - }, - { - "title": "leaf list 자체는 outside consumer를 검사하지 않는다", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c33", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L2230`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "`JpaModuleBoundaryTest`의 local export test는: export package가 실제 존재하는지 새 top-level package가 governance 대상인지 를 보지만 repository의 outside consumer import를 직접 스캔하지 않는다. 실제 consumer restriction은 app-bootstrap의 별도 ArchUnit rule이 담당한다. 따라서 current architecture fitness function은: fresh architecture tests는 모두 통과했다. 이것은 현재 import graph가 각자의 rule을 만족한다는 뜻이지 **A와 B가 서로 drift하지 않는다는 증명은 아니다.** 우선순위: **P2/P3 architecture-governance hardening** 권장 방향은 exported package registry를 한 곳으로 옮기고 leaf package DAG와 consumer ArchUnit rule이 같은 데이터를 읽게 하는 것이다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "delivery-and-settlement-models/concept/concept-adapter-outbound-persistence-jpa-c33.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c33" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c33.txt" - ] - }, - { - "title": "이 sub-scope는 이 leaf에서 유일하게 \"조립까지 된\" 대형 서브시스템이다", - "kind": "concept", - "slug": "adapter-outbound-persistence-mongo-c07", - "readiness": "READY", - "source": [ - "`final/document.md#a06#L1015`" - ], - "owning-module": "`adapter-outbound-persistence-mongo`", - "classification": "앞선 sub-scope들과 다르다. `MongoPlatformAutoConfiguration`이 두 개의 bean을 실제로 만든다. `mongoChangeStreamSource`(209행) — `SpringReactiveChangeStreamSource`, 무조건. `reactiveMongoChangeStreamConsumer`(235행) — fork만 공급할 수 있는 5종(`MongoChangeStreamSubscription`, `MongoResumeCheckpointStore`, `MongoResumeTokenCodec`, `MongoChangeProjector`, `MongoChangeDeduplicationStore`)에 `@ConditionalOnBean`. pipeline·runner·recovery policy·invalidate recovery는 auto-configuration이 직접 `new`한다. 즉 fork가 설계가 요구하는 다섯 개를 그대로 제공하면 **완성된 소비자가 돈다**. 이 사실이 아래 §67의 심각도를 결정한다. 설계 자체는 이 leaf에서 가장 정교한 축에 속한다. **순서가 계약이다.** `MongoChangeStreamRunner`: 투영 먼저, checkpoint 나중. \"Checkpointing first would mean a crash between the two loses the event permanently, with no trace.\" 그래서 중복을 택하고 중복을 제거한다. **claim은 3-state다.** 과거 `alreadyProjected` + `markProjected`(읽고-쓰기)는 동시에 `false`를 읽은 두 subscriber가 둘 다 투영했다 — \"the deduplication that exists precisely because redelivery is guaranteed did not survive concurrency\". 지금은 `CLAIMED`/`ALREADY_COMPLETED`/`BUSY`의 원자적 전이다. **빈 완료는 프로토콜 위반이다.** `Mono`이 empty로 완료되면 `flatMap`을 그냥 통과해 \"투영도 checkpo…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "delivery-and-settlement-models/concept/concept-adapter-outbound-persistence-mongo-c07.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-mongo-c07" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-mongo-c07.txt" - ] - }, - { - "title": "messaging과 realtime은 provider/transport vocabulary를 밖으로 밀어낸다", - "kind": "concept", - "slug": "application-core-c06", - "readiness": "READY", - "source": [ - "`final/document.md#a03#L174`" - ], - "owning-module": "`application-core`", - "classification": "messaging application contract catalog는 contract id, logical destination, schema resource, ordering, payload/envelope bounds, sensitivity, retry/requeue horizon 등 semantic 정보만 가진다. Kafka topic/provider runtime type은 public contract에 없다. validated integration event는 partition key, schema/content hash, catalog/binding revision 같은 immutable evidence를 보존한다. strict `messagingApplicationContractQualificationTest`는 normal test source set의 세 required class를 no-skip 조건으로 실행한다. 처음 digest property 없이 실행했을 때 `prepareMessagingContractEvidence`가 fail-closed로 거부했다. current source/archive, current application-core JAR, exact profile file의 SHA-256을 공급한 재실행에서는 **15 tests, 0 skipped, BUILD SUCCESSFUL**이었다. 즉 qualification은 단순 테스트 이름이 아니라 evidence provenance property까지 요구한다. realtime contract는 durable fanout과 ephemeral fanout을 분리한다. durable은 accepted와 delivered를 동일시하지 않고 stream+position dedupe/replay를 모델링한다. stale cursor는 resnapshot 요구로 분리된다. presence는 non-authoritative이며 TTL/heartbeat failure 시 empty로 degrade할 뿐 security 판단에 사용하지 않는다. logical channel은 WebSocket/STOMP 같은 transport 명칭을 소유하지 않는다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "delivery-and-settlement-models/concept/concept-application-core-c06.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "application-core-c06" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/application-core-c06.txt" - ] - }, - { - "title": "storage/file publication: legacy 경로와 semantic 경로가 공존한다", - "kind": "concept", - "slug": "application-core-c07", - "readiness": "READY", - "source": [ - "`final/document.md#a03#L182`" - ], - "owning-module": "`application-core`", - "classification": "`application.storage.ObjectStoragePort`는 raw object key/whole-byte 방식의 legacy contract이며 `forRemoval` 표시가 있지만 실제 production consumer가 남아 있다. sample poster upload, adapter/config, characterization test에서 사용되므로 dead code로 분류할 수 없다. 제거 시점은 날짜가 아니라 실제 migration/zero usage로 판단하도록 문서화돼 있다. `fileexport` 역시 raw filesystem path를 반환하는 opt-in legacy capability이며 `FilesystemCsvExportAdapter`/configuration을 통해 조건부 활성화된다. 반대로 `filepublication`은 logical destination, operation/reference/version, schema, row streaming/checkpoint, durability semantic을 provider-neutral 계약으로 만든다. CSV formula injection(`=`, `+`, `-`, `@`, tab, CR)을 reject하는 정책이 테스트로 고정돼 있고, raw Path/SFTP/fileserver 타입이 receipt surface에 나오지 않는다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "delivery-and-settlement-models/concept/concept-application-core-c07.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "application-core-c07" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/application-core-c07.txt" - ] - }, - { - "title": "실제 production reachability와 legacy/dead-path 판정", - "kind": "concept", - "slug": "application-core-c11", - "readiness": "READY", - "source": [ - "`final/document.md#a03#L270`" - ], - "owning-module": "`application-core`", - "classification": "static production reference scan에서 주요 application package는 모두 외부 production consumer를 확인했다. 이 count는 “모든 type이 각각 호출된다”는 의미가 아니라 package-level runtime/repository reachability의 evidence다. 세부 파일은 `evidence/raw/013-application-core-reachability.txt`에 보존했다. legacy surface도 무조건 dead로 분류하지 않았다. `application.storage.ObjectStoragePort`, root notification `NotificationPort`, `NotificationVariablesCodecPort`, old idempotency-related exception 등은 adapter/config/characterization path에서 실제 reference가 남아 있다. 현재 상태는 dead code가 아니라 migration/compatibility surface다. 반대로 notification admin atomic `claim()`은 adapter 구현까지 존재하지만 application service consumer가 없는 **unwired corrective path**로 판정했다. 이것이 이번 scope의 가장 중요한 reachability finding이다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "delivery-and-settlement-models/concept/concept-application-core-c11.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "application-core-c11" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/application-core-c11.txt" - ] - }, - { - "title": "Runtime reachability / wiring", - "kind": "concept", - "slug": "domain-core-c02", - "readiness": "READY", - "source": [ - "`final/document.md#a01#L146`" - ], - "owning-module": "`domain-core`", - "classification": "`domain-core` 자체에는 Spring bean/configuration/entry point가 없다. Registry상 `app-bootstrap`, `sample-portfolio` 두 runtime composition에 membership이 있고, concrete consumers가 compile-time type/annotation으로 이 module을 참조한다. `ResourceId`: application-core messaging contract 및 sample IDs에서 참조 `IdFactory`: sample factory/use-case/identifier adapter에서 참조 `AggregateRoot`: sample aggregate에서 사용 `DomainEvent`: sample events와 websocket broadcaster qualification에서 사용 `ValueObject`: sample IDs/value objects에서 사용 따라서 major public abstraction이 완전히 dead/unwired인 상태는 아니다. 반대로 `domain-core`가 runtime service를 직접 수행한다는 근거도 없다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "delivery-and-settlement-models/concept/concept-domain-core-c02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "domain-core-c02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/domain-core-c02.txt" - ] - }, - { - "title": "건강 레지스트리 — 낙관에서 시작하지 않는다", - "kind": "concept", - "slug": "grpc-admin-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-admin#L50`" - ], - "owning-module": "`grpc-admin`", - "classification": "모든 등록 서비스가 `UNKNOWN` 에서 시작한다. 그리고 배수 중에는 `markServing`·`markNotServing` 이 무시된다. 전역 상태 계산은 세 단계다 — 임계 의존이 하나라도 불건강하면 `NOT_SERVING`, 아니면 하나라도 `NOT_SERVING` 이면 `NOT_SERVING`, 하나라도 `SERVING` 이면 `SERVING`, 그 밖에는 `UNKNOWN`.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "delivery-and-settlement-models/concept/concept-grpc-admin-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-admin-c01", - "grpc-admin-c01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-admin-c01.txt" - ] - }, - { - "title": "배수 순서", - "kind": "concept", - "slug": "grpc-admin-c02", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-admin#L65`" - ], - "owning-module": "`grpc-admin`", - "classification": "`beginDrain` 이 앞의 둘을 한 번에 수행하고, 그 전에 `rejectNewAdmission` 을 부르면 던진다. 조정자는 잠들지 않는다. 예산은 누적이다 — 스트림 신호 완료 판정이 `unaryDrainBudget + streamSignalBudget` 을 기준으로 한다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "delivery-and-settlement-models/concept/concept-grpc-admin-c02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-admin-c02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-admin-c02.txt" - ] - }, - { - "title": "xDS 시작 가드", - "kind": "concept", - "slug": "grpc-advanced-resilience-c02", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-advanced-resilience#L75`" - ], - "owning-module": "`grpc-advanced-resilience`", - "classification": "두 거절이 있고 javadoc 이 둘째를 더 중요하다고 적는다. 시작 차단 사유는 둘 — 능력이 사용 가능하지 않음, 그리고 애플리케이션이 재시도 정책을 함께 정의함. 부트스트랩 대조는 세 가지를 본다 — `xds_servers` 선언, 통제 평면 채널의 TLS, 프로파일의 자원 이름공간.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "delivery-and-settlement-models/concept/concept-grpc-advanced-resilience-c02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-advanced-resilience-c02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-advanced-resilience-c02.txt" - ] - }, - { - "title": "모듈의 정체와 경계", - "kind": "concept", - "slug": "messaging-testkit-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-testkit#L68`" - ], - "owning-module": "`messaging-testkit`", - "classification": "이 리프는 **\"지원한다(supported)\"라는 단어의 정의를 코드로 못 박는 곳**이다. 플랫폼의 다른 어떤 리프도 \"Kafka 는 Stable 이다\" 를 주장하지 않는다. 그 주장은 여기에만 있고, 여기서만 검증된다. 세 개의 층으로 되어 있다. 1. **공유 계약** (`MessagingAdapterContract` + `MessagingAdapterHarness` + `ContractMessage`/`ContractAssertions`/`ObservedDelivery`/`HandleOutcome`/`FaultController`) — 브로커가 무엇이든 똑같이 답해야 하는 7가지 행동. 2. **결함 시나리오와 그 증거** (`NetworkFaultScenario` + `BrokerCertificationEvidence` + `CertifiedEvidence` + `BrokerFailureMatrix`) — 어떤 장애를 실제로 돌려 봤는가. 3. **지원 등급** (`CompatibilityMatrix`) — 위 두 층의 결과로 어댑터가 얻는 등급. 경계는 명확하다. 이 리프는 어댑터를 **구현하지 않고**, 어댑터를 **실행하지도 않는다**. 하니스 구현은 각 어댑터 리프의 `src/test` 에 있다(`KafkaContractHarness`, `RabbitContractHarness`). 이 리프가 가진 유일한 하니스는 `InMemoryMessagingHarness` 이며 `src/test` 에 있고, 그 javadoc 이 스스로 선을 긋는다. `final` + package-private + `private` 생성자 + 정적 팩토리. \"프로덕션 어댑터가 되어서는 안 된다\" 는 문장이 접근 제어자로도 강제되어 있다. `src/main` 이 아니라 `src/test` 에 둔 것도 같은 결정이다 — 다른 리프의 test 클래스패스에 올라가는 것은 `src/main` 뿐이므로, 이 하니스는 물리적으로 이 리프 밖으로 나갈 수 없다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "delivery-and-settlement-models/concept/concept-messaging-testkit-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-testkit-c01", - "messaging-testkit-c01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-testkit-c01.txt" - ] - }, - { - "title": "주요 실행 경로", - "kind": "concept", - "slug": "messaging-spring-cloud-stream-bridge-c04", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-spring-cloud-stream-bridge#L309`" - ], - "owning-module": "`messaging-spring-cloud-stream-bridge`", - "classification": "**검증:** `validator.validate(profile, bindingName, extendedProperties, enabled)` → guard 4검사 → 이름 → 속성 8개 → production → `BindingCapabilityReport.bridged(...)` **발행:** `bridge.bindPublisher(dest, binding)` → `bridge.publish(dest, payload, headers)` → `send.send(...)` → true면 `AMBIGUOUS`, false면 `REJECTED` **수신:** `consumerBridge.register(dest, binding, handler)` → 바인더가 `dispatch(binding, payload, headers)` → `handler.handle(...)` (예외 그대로 전파)", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "delivery-and-settlement-models/concept/concept-messaging-spring-cloud-stream-bridge-c04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-spring-cloud-stream-bridge-c04" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-spring-cloud-stream-bridge-c04.txt" - ] - }, - { - "title": "계약·불변식·상태 모델", - "kind": "concept", - "slug": "messaging-schema-protobuf-c03", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-schema-protobuf#L112`" - ], - "owning-module": "`messaging-schema-protobuf`", - "classification": "이 leaf에서 가장 밀도 높은 결정이다. 증명 방법이 영리하다. proto3에서 모든 필드가 wire상 optional이므로 **빈 바이트는 항상 유효한 메시지**다. 그것을 파싱하면 default instance가 나오고 그 클래스가 곧 parser의 산출 타입이다. 별도 리플렉션 없이 짝을 확인한다. 에러 메시지가 실패 지점을 명시한다 — \"a mismatched pairing fails at decode time on a broker thread, not here\". 즉 **여기서 실패하는 것이 목적**임을 메시지가 스스로 말한다. 두 코드가 다르다: `PROTOBUF_CONTRACT_UNUSABLE`(파싱 자체 실패)과 `PROTOBUF_CONTRACT_MISMATCH`(파싱은 되는데 타입이 다름). 카테고리는 둘 다 `CONFIGURATION`이다. 테스트가 이 성질을 붙든다 — `aParserThatDoesNotProduceTheDeclaredClassIsRejectedAtConstruction`, `as(\"the mismatch used to surface as a ClassCastException on a broker thread\")`. 주석이 이유를 적는다. **세 codec 중 유일하게 사전 거절이 가능한 포맷이다.** `BoundedByteSink.requireFits`가 이 leaf를 위해 존재하고, schema-api의 javadoc이 그것을 명시한다 — \"Protobuf knows its serialized size exactly, so the whole encode can be refused before the first byte is written.\" 그리고 사전 검사가 사후 경계를 대체하지 않는다 — `writeTo(sink)`가 여전히 sink를 통과하므로 이중 방어다. schema-api javadoc: \"this is a cheaper refusal, not a replacement for the bound.\" `Message`인지와 등록된 클래스의 인스턴스인지를 함께 본다. 후자만으로 충분해 보이지만 전자가 `writeTo`를 부를 수 있음을 보장한다. JSON codec과 같은 비대칭이다 — e…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "delivery-and-settlement-models/concept/concept-messaging-schema-protobuf-c03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-schema-protobuf-c03", - "messaging-schema-protobuf-c03-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-schema-protobuf-c03.txt" - ] - }, - { - "title": "실패 경로와 복구/번역", - "kind": "concept", - "slug": "messaging-spring-cloud-stream-bridge-c05", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-spring-cloud-stream-bridge#L319`" - ], - "owning-module": "`messaging-spring-cloud-stream-bridge`", - "classification": "**아홉 개의 구성 실패가 전부 `MessagingConfigurationException` + 안정 코드다.** 이 저장소 messaging family에서 예외 어휘가 가장 일관된 leaf다 — `messaging-security`(두 계층 혼용)·`messaging-kafka-share-experimental`(두 계층 혼용)·`messaging-policy`(검증기가 `IllegalArgumentException`)와 대비된다. 발행 결과 둘은 예외가 아니라 값이다 — `messaging-core-api`의 설계를 그대로 따른다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "delivery-and-settlement-models/concept/concept-messaging-spring-cloud-stream-bridge-c05.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-spring-cloud-stream-bridge-c05" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-spring-cloud-stream-bridge-c05.txt" - ] - }, - { - "title": "소비자 런타임 — 스레드 규율이 설계다", - "kind": "concept", - "slug": "messaging-kafka-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-kafka#L69`" - ], - "owning-module": "`messaging-kafka`", - "classification": "공개 API 인 `pause`/`resume` 도 제어 큐를 통해 폴 스레드로 넘어가고, 반환된 단계는 **다음 폴 주기** 에 완료된다. `close()` 만 예외이고 그 예외에 근거가 붙어 있다 — 이후 폴 루프가 멈추므로 큐에 넣으면 영원히 배수되지 않는다. 이 규율은 실제로 지켜진다. 작업자 람다가 만지는 것은 `settlements`·`coordinator`·`shutdown`·`retries` 뿐이고 `consumer` 는 한 번도 없다. 통독으로 확인했다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "delivery-and-settlement-models/concept/concept-messaging-kafka-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-kafka-c01", - "messaging-kafka-c01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-kafka-c01.txt" - ] - }, - { - "title": "능력 선언", - "kind": "concept", - "slug": "messaging-nats-experimental-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-nats-experimental#L82`" - ], - "owning-module": "`messaging-nats-experimental`", - "classification": "`nativeDeadLetter=false` 의 근거가 클래스 javadoc 과 검증기 javadoc 양쪽에 있다 — 없는 큐를 찾아 나서게 만들지 않는다. `keyedOrdering=false` 도 검증기가 강제한다 — 키 순서를 요구하는 목적지를 거부한다. `deduplicatedPublish=true` 는 §17.1 이 다룬다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "delivery-and-settlement-models/concept/concept-messaging-nats-experimental-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-nats-experimental-c01" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-nats-experimental-c01.txt" - ] - }, - { - "title": "의존성과 런타임 배선", - "kind": "concept", - "slug": "messaging-transport-spi-c02", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-transport-spi#L90`" - ], - "owning-module": "`messaging-transport-spi`", - "classification": "들어오는 것: `messaging-core-api`(api), `messaging-schema-api`(api), `messaging-policy`(api). 셋 다 `api`인 이유는 세 leaf의 타입이 이 leaf의 public 시그니처에 직접 등장하기 때문이다 — `TransportPublishRequest`가 `DestinationProfile`(policy)·`MessageEnvelope`(core-api)·`EncodedMessage`(schema-api)를 필드로 갖는다. 나가는 것: `messaging-runtime-core`, `messaging-kafka`, `messaging-kafka-share-experimental`, `messaging-rabbit`, `messaging-pulsar-experimental`, `messaging-nats-experimental`, `messaging-admin-runtime`, `messaging-spring-cloud-stream-bridge`, `messaging-spring-boot-starter`, `messaging-testkit`. 런타임 편입은 starter closure를 통해서다. 이 leaf 자체는 bean을 만들지 않는다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "delivery-and-settlement-models/concept/concept-messaging-transport-spi-c02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-transport-spi-c02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-transport-spi-c02.txt" - ] - }, - { - "title": "E. control: the publish path IS constructed in production", - "kind": "concept", - "slug": "messaging-policy-c07", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-policy#L603`" - ], - "owning-module": "`messaging-policy`", - "classification": "DefaultMessagePublisher MessagingCoreAutoConfiguration.java:446 TransportMessagingRuntime MessagingCoreAutoConfiguration.java:476 DefaultRetryDecisionEngine MessagingCoreAutoConfiguration.java:168 DeadLetterOrchestrator MessagingCoreAutoConfiguration.java:180", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "delivery-and-settlement-models/concept/concept-messaging-policy-c07.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-policy-c07" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-policy-c07.txt" - ] - }, - { - "title": "G. every file that acts on a RetryDecision variant", - "kind": "concept", - "slug": "messaging-policy-c08", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-policy#L617`" - ], - "owning-module": "`messaging-policy`", - "classification": "messaging-kafka/.../KafkaRetryExecutor.java (생성되지 않음) messaging-policy/.../DefaultRetryDecisionEngine.java (생산자) messaging-policy/.../RetryDecision.java (선언) messaging-policy/.../RetryDecisionEngineTest.java (테스트)", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "delivery-and-settlement-models/concept/concept-messaging-policy-c08.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-policy-c08" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-policy-c08.txt" - ] - }, - { - "title": "소비·정착·죽은 편지의 세 규율", - "kind": "concept", - "slug": "messaging-rabbit-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-rabbit#L91`" - ], - "owning-module": "`messaging-rabbit`", - "classification": "**좁은 catch.** `RabbitConsumerRegistrar.onMessage` 가 디코딩만 감싸는 안쪽 `try` 를 따로 둔다. **정착하지 않은 핸들러.** 완료했는데 정착하지 않으면 대신 ack 하지 않고 requeue 한다 — \"acknowledging on its behalf would silently drop it\". **네이티브 죽은 편지.** `RabbitNativeDeadLetterCapability` 가 두 조건을 모두 요구한다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "delivery-and-settlement-models/concept/concept-messaging-rabbit-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-rabbit-c01", - "messaging-rabbit-c01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-rabbit-c01.txt" - ] - }, - { - "title": "설정이 프로파일이 된다", - "kind": "concept", - "slug": "messaging-spring-boot-starter-c02", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-spring-boot-starter#L111`" - ], - "owning-module": "`messaging-spring-boot-starter`", - "classification": "`MessagingConfigurationCompiler` 가 닫는 것은 기능이 아니라 바인더의 부재다. 컴파일과 검증을 나눈 이유도 적혀 있다. 컴파일은 객체 모델이 표현할 수 없는 것만 본다 — 목적지의 브로커가 존재하는지, 사후 처리 목적지가 선언되었는지, 보안 항목이 실재하는 브로커를 지키는지. 프로파일이 자체로 정합한지는 `DestinationProfileValidator` 의 질문이고 레지스트리 전체에 대해 던져진다. 그래서 설정으로 만든 프로파일과 빈으로 선언한 프로파일이 **같은 규칙**을 받는다. 그리고 모든 거부가 키를 부른다. 타입을 부르는 오류는 운영자가 고칠 줄을 알려 주지 않기 때문이다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "delivery-and-settlement-models/concept/concept-messaging-spring-boot-starter-c02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-spring-boot-starter-c02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-spring-boot-starter-c02.txt" - ] - }, - { - "title": "종료 순서가 두 수명 주기의 phase 로 표현된다", - "kind": "concept", - "slug": "messaging-spring-boot-starter-c03", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-spring-boot-starter#L155`" - ], - "owning-module": "`messaging-spring-boot-starter`", - "classification": "`SmartLifecycle` 은 내림차순으로 멈추므로 중계가 먼저, 승인 차단과 배수가 다음이다. 두 클래스의 javadoc 이 서로를 근거로 든다 — 중계가 발행 중일 때 승인을 닫으면 그 회차의 행이 모호해지고, 그 모호함이야말로 배수가 없애려는 것이다. 그리고 브로커 연결을 쥔 빈(`@Bean(destroyMethod = \"close\")` 인 생산자)은 `Lifecycle` 이 아니므로 컨텍스트가 `destroyBeans()` 에 도달할 때, 즉 두 수명 주기가 모두 끝난 뒤에 닫힌다. 순서가 맞는다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "delivery-and-settlement-models/concept/concept-messaging-spring-boot-starter-c03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-spring-boot-starter-c03", - "messaging-spring-boot-starter-c03-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-spring-boot-starter-c03.txt" - ] - }, - { - "title": "주요 계약과 불변식", - "kind": "concept", - "slug": "shared-contract-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a02#L51`" - ], - "owning-module": "`shared-contract`", - "classification": "`ApiErrorCode`는 code/category/httpStatus/retryable의 최소 표면을 제공하고 `OperationalError`가 registry mirror 역할을 한다. `Category`는 VALIDATION, AUTH, AUTHZ, NOT_FOUND, CONFLICT, RATE_LIMIT, TRANSIENT_DEPENDENCY, PERMANENT_DEPENDENCY, DATA_INTEGRITY, INTERNAL의 10개 값으로 고정되어 있으며 테스트가 정확한 vocabulary를 pin 한다. `OperationalErrorTest`는 단순 enum 존재보다 category × retryable 의미를 강하게 검증한다. deterministic VALIDATION/AUTHZ/NOT_FOUND는 retryable=false이고, transient INTERNAL은 기본적으로 retryable=true이되 deploy-time/configuration/terminal 상태인 일부 code는 명시적 예외로 false다. `AUTH_KID_UNKNOWN`은 key rotation 중 JWKS refresh 가능성을 이유로 AUTH 중 유일한 retryable case로 pin 되어 있다. upstream 4xx 전체를 permanent/non-retryable로 분류하면서 408/429의 의미 차이가 남는다는 점은 source comment와 README가 이미 known edge로 기록한다. `DependencyFailureException`과 `PersistenceFailureException`은 `ApiErrorCarrier`를 통해 transport adapter에 stable error code를 전달하면서 raw cause/diagnostic message를 server-side 정보로 남긴다. `AdapterDisabledException`은 carrier를 구현하지 않고 별도 mapping 대상이다. `Envelope`, `BulkEnvelope`, `ResponseMeta`, `PageMeta`, `Operation`은 framework-neutral record/factory로 API shape를 …", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "delivery-and-settlement-models/concept/concept-shared-contract-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "shared-contract-c01", - "shared-contract-c01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/shared-contract-c01.txt" - ] - } - ], - "reference": [], - "question": [], - "decision": [] - } - }, - "schema-and-wire-models": { - "topic": "schema-and-wire-models", - "title": "", - "readerQuestion": "", - "kinds": { - "case": [], - "concept": [ - { - "title": "(8.2) 조건 형제 비교 — 스키마 해시의 생산자와 소비자", - "kind": "concept", - "slug": "adapter-inbound-graphql-c02", - "readiness": "READY", - "source": [ - "`final/document.md#a16#L221`" - ], - "owning-module": "`adapter-inbound-graphql`", - "classification": "`GraphQlSchemaHash`의 유일한 생산 경로는 `GraphQlSchemaAssemblyResult.schemaHash()`(`:94-95`)이고, 그 결과 타입은 `GraphQlSchemaAssembler.assemble(...)`만 만든다. 둘 다 프로덕션 호출자가 없다(§7.1). 소비 쪽은 `GraphQlPlatformActuatorEndpoint`가 생성자로 받는다. 그 클래스의 저장소 전체 참조는: `GraphQlPlatformAutoConfiguration`의 39개 `@Bean` 중 이것을 만드는 것이 없다. §8.1. *(이 파일은 `autoconfigure` 패키지에 있어 sub-scope 01의 분모에 포함된다. 스키마 해시 사슬의 소비 쪽이므로 여기서 함께 다룬다.)*", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-wire-models/concept/concept-adapter-inbound-graphql-c02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-graphql-c02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-graphql-c02.txt" - ] - }, - { - "title": "(8.3) 중복 메커니즘 — 실행 전 실패의 매퍼", - "kind": "concept", - "slug": "adapter-inbound-graphql-c07", - "readiness": "READY", - "source": [ - "`final/document.md#a16#L604`" - ], - "owning-module": "`adapter-inbound-graphql`", - "classification": "`GraphQlRequestErrorMapper`의 javadoc이 자기 존재 이유를 적는다: 진단이 정확하고, 배선된 것은 리졸버 쪽(`GraphQlExceptionResolver`, autoconf=4)뿐이다. 파싱·검증 실패의 와이어 형식을 이 플랫폼이 정하지 않는다는 뜻이다 — 다만 `runtime/GraphQlWireErrorMapper`(autoconf=4)와 `runtime/GraphQlPlatformRejectionMapper`(main_other=3)가 배선돼 있어 플랫폼이 거부하는 실패(익명 연산, 복잡도 초과 등)는 안정 코드로 매핑된다. 덮이지 않는 것은 graphql-java 자신이 만드는 구문/검증 오류다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-wire-models/concept/concept-adapter-inbound-graphql-c07.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-graphql-c07" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-graphql-c07.txt" - ] - }, - { - "title": "(8.2) 조건 형제 비교 — 커서 서명 키의 두 소비처", - "kind": "concept", - "slug": "adapter-inbound-graphql-c11", - "readiness": "READY", - "source": [ - "`final/document.md#a16#L725`" - ], - "owning-module": "`adapter-inbound-graphql`", - "classification": "`backend.graphql.cursor.key-ids`를 읽는 프로덕션 코드는 둘이다: **서명하는 코드는 없다.** `HmacGraphQlCursorCodec`과 `GraphQlCursorKeyRing`은 autoconf=0 · main_other=0이다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-wire-models/concept/concept-adapter-inbound-graphql-c11.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-graphql-c11" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-graphql-c11.txt" - ] - }, - { - "title": "(8.1) 도달성 — 라이브러리 타입의 소비자", - "kind": "concept", - "slug": "adapter-inbound-web-c08", - "readiness": "READY", - "source": [ - "`final/document.md#a14#L843`" - ], - "owning-module": "`adapter-inbound-web`", - "classification": "다섯 패키지 중 소비 모듈이 실제로 부르는 것은 `ETags` 하나다 — `sample-portfolio`의 `WorkLogController`가 세 곳에서 쓴다(`:144` `If-None-Match` 비교, `:213` 버전에서 약한 ETag 생성, `:226` `If-Match` 검사). 나머지는 전부 테스트 전용이다. 라이브러리이므로 그 자체가 결함은 아니지만, 페이지네이션 어휘 19개 파일·버전 관리 7개 파일이 **한 번도 컨트롤러에 붙어 본 적이 없다**는 사실은 기록해 둘 값이 있다 — 이 저장소가 다른 곳에서 \"타입은 있고 호출자가 없다\"를 반복해서 결함으로 취급했기 때문이다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-wire-models/concept/concept-adapter-inbound-web-c08.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-web-c08" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-web-c08.txt" - ] - }, - { - "title": "(8.3) 중복 메커니즘 — 커서 코덱도 두 벌", - "kind": "concept", - "slug": "adapter-inbound-web-c10", - "readiness": "READY", - "source": [ - "`final/document.md#a14#L889`" - ], - "owning-module": "`adapter-inbound-web`", - "classification": "`pagination/WebCursorCodec`(인터페이스) + `pagination/HmacWebCursorCodec`(135, 서명된 구현) + `pagination/WebCursorPayload` + `pagination/WebCursorKeyRing`, 그리고 별도로 `cursor/CursorCodec`(100) + `cursor/CursorException`. `WebStableModule`은 둘을 다른 모듈로 선언한다(`CURSOR` = \"Opaque keyset cursor encoding and its failure type\", `PAGINATION`). 둘 다 프로덕션 소비자가 없어 어느 쪽이 정본인지 코드로는 판정할 수 없다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-wire-models/concept/concept-adapter-inbound-web-c10.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-web-c10" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-web-c10.txt" - ] - }, - { - "title": "(8.2) 조건 형제 비교 — `OpenApiCustomizer` 가 두 개다", - "kind": "concept", - "slug": "adapter-inbound-web-c11", - "readiness": "READY", - "source": [ - "`final/document.md#a14#L978`" - ], - "owning-module": "`adapter-inbound-web`", - "classification": "배선된 것 — `config/OpenApiContractConfig`(37줄, `@Configuration`)가 익명 람다 `OpenApiCustomizer` 하나를 빈으로 등록한다. 하는 일은 `ApiError.details` 스키마를 `ObjectSchema`로 되돌리는 것 한 가지다. 배선되지 않은 것 — `openapi/WebOpenApiCustomizer`(74줄)와 그것이 쓰는 `ProblemSchemaContributor`(88) · `CursorSchemaContributor`(47) · `WebOpenApiProfile`(72) · `WebOpenApiBreakingPolicy`(187) · `WebOpenApiReleaseGate`(100) · `WebOpenApiDiffResult`(39). 합 607줄. 빈으로 등록하는 코드가 main·app-bootstrap에 없고, 참조는 자기들끼리와 테스트뿐이다. 즉 springdoc이 생성하는 문서에는 RFC 9457 problem 스키마도 커서 스키마도 기여되지 않는다 — 그 기여자들이 커스터마이저에 도달하지 않기 때문이다. SS2에서 확인한 \"problem 계약이 문서에 없다\"(§7.3)와 같은 방향의 사실이 스키마 쪽에서도 성립한다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-wire-models/concept/concept-adapter-inbound-web-c11.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-web-c11" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-web-c11.txt" - ] - }, - { - "title": "(8.4) 카운트 — `WebSocketFailureCategory`", - "kind": "concept", - "slug": "adapter-inbound-websocket-c03", - "readiness": "READY", - "source": [ - "`final/document.md#a17#L267`" - ], - "owning-module": "`adapter-inbound-websocket`", - "classification": "`error` 패키지의 `WebSocketFailureCategory`(main_other=10)가 이 sub-scope에서 가장 널리 참조되는 타입이고, `WebSocketErrorMessage`(75, main 참조 0)와 `WebSocketErrorTransport`가 그것을 전송으로 옮긴다. 실제 STOMP 오류는 `stomp/SafeStompSubProtocolErrorHandler`(32)가 만든다 — 세 번째 오류 형식이다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-wire-models/concept/concept-adapter-inbound-websocket-c03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-websocket-c03" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-websocket-c03.txt" - ] - }, - { - "title": "(8.2) 조건 형제 비교 — 재개 토큰 서명", - "kind": "concept", - "slug": "adapter-inbound-websocket-c04", - "readiness": "READY", - "source": [ - "`final/document.md#a17#L381`" - ], - "owning-module": "`adapter-inbound-websocket`", - "classification": "`ResumeTokenCodec`(217) + `ResumeTokenKeyRing`(87)이 서명된 재개 토큰을 만든다. 이 조합은 graphql §24.1의 `HmacGraphQlCursorCodec` + `GraphQlCursorKeyRing`과 같은 형태다. **차이는 이쪽에는 그 키를 요구하는 시작 검증기가 없다는 것** — 즉 \"키를 요구하고 서명하지 않는\" 잘못된 확인 신호가 없다. 면책 목록에 replay/resume이 있으므로 문서·코드·검증이 일치한다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-wire-models/concept/concept-adapter-inbound-websocket-c04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-websocket-c04" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-websocket-c04.txt" - ] - }, - { - "title": "명령 기술: 정책 파일과 서버 메타데이터의 접합점", - "kind": "concept", - "slug": "adapter-outbound-cache-redis-c07", - "readiness": "READY", - "source": [ - "`final/document.md#a10#L350`" - ], - "owning-module": "`adapter-outbound-cache-redis`", - "classification": "`RedisCommandDescriptor`는 \"the join between official server metadata and organization policy\"이고, 그 아래를 못박는다 — \"Nothing downstream of the guard is allowed to re-derive risk, access, or timeout from a command name.\" 생성자가 그 접합의 모순 네 가지를 거부한다. 마지막 하나가 §23의 런타임 불변식과 같은 규칙의 **선언 시점** 짝이다 — 하나는 정책 파일이 거짓말하지 못하게 하고, 하나는 실패 객체가 거짓말하지 못하게 한다. `KeySpec`은 공식 Redis 규약(1-based, 음수 lastKey는 뒤에서부터, `movable`은 정적 유도 불가)을 그대로 따르고, `movable`이면 `resolvePositions`가 던진다 — \"movable key specification must be resolved by the server\". `CommandId`는 항상 대문자로 정규화해 \"a policy file, a server metadata reply, and an SDK call site cannot disagree because of casing.\" permit 세 종은 인터페이스이고 javadoc이 경계를 명확히 한다 — \"Application code may implement this interface, but a self-made instance never passes `RedisPermitVerifier`… The final enforcement boundary remains the Redis ACL account, which a permit never widens.\" `PersistentKeyPermit`은 한 줄 더 붙인다: \"Cache, session, lock, idempotency, and rate-limit APIs never accept this permit.\" `OperationBudget`도 규칙을 문서로 못박는다 — \"Every R2 API requires a budget. The budget is never optional and n…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-wire-models/concept/concept-adapter-outbound-cache-redis-c07.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-cache-redis-c07" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-cache-redis-c07.txt" - ] - }, - { - "title": "정책 문서를 일반 YAML 파서로 읽지 않는다", - "kind": "concept", - "slug": "adapter-outbound-cache-redis-c11", - "readiness": "READY", - "source": [ - "`final/document.md#a10#L579`" - ], - "owning-module": "`adapter-outbound-cache-redis`", - "classification": "`RedisCommandPolicyLoader`의 javadoc이 이유를 적는다. 파서는 그만큼 좁다 — 탭 금지, 들여쓰기 0/2/4만 허용, `commands:` 루트 정확히 하나, 명령 블록 중복 금지, 필드 이름 allowlist(12종) 밖이면 거부, 빈 값 거부, 필드 중복 거부. test가 `rejectsUnknownFieldsEnumsAndDuplicates`로 잡는다. `RedisCommandPolicy`/`RedisCommandDescriptor`의 교차 필드 불변식(§24)이 로딩 시점에 적용되므로, \"R4인데 BLOCKED이 아닌\" 정책 파일은 **읽히지 않는다**. test가 `everyDestructiveCommandIsBlockedAndUnreachable`·`deprecatedCommandNamesAreNotReachable`·`arbitraryScriptSourceExecutionIsBlocked`·`theCatalogFailsClosedForAnUnclassifiedCommand`로 그 집합을 고정한다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-wire-models/concept/concept-adapter-outbound-cache-redis-c11.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-cache-redis-c11" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-cache-redis-c11.txt" - ] - }, - { - "title": "Confirmed — codec이 \"canonical\"을 왕복으로 강제한다", - "kind": "concept", - "slug": "adapter-outbound-fileserver-c02", - "readiness": "READY", - "source": [ - "`final/document.md#a08#L168`" - ], - "owning-module": "`adapter-outbound-fileserver`", - "classification": "`FileserverControlRecordCodec`은 세 레코드와 receipt snapshot에 대해 **decode 직후 재encode해 바이트를 비교**한다(`requireCanonical(bytes, encodeOperation(record))`). 그래서 \"파싱은 되지만 우리가 쓰지 않았을 형태\"가 전부 거부된다 — 공백, 필드 재배열, `A` 같은 이스케이프, `-0`/선행 0 같은 숫자 표기, 후행 콘텐츠. 파서 자체도 좁다. 필드 집합을 **정확히 일치**시킨다(`values.keySet().equals(allowedFields)`) — 누락도 미지 필드도 거부. 중복 키를 거부한다(`putIfAbsent`). UTF-8 디코딩이 `REPORT` 모드라 malformed 바이트가 대체문자로 조용히 바뀌지 않는다. `\\b \\f \\n \\r \\t` 이스케이프를 **문법 수준에서 거부**한다(\"control characters are forbidden\") — 제어문자가 이스케이프로 밀입되는 경로를 닫는다. 짝 없는 서로게이트를 거부한다(`requireWellFormedUnicode`). `Instant.parse` 후 `result.toString().equals(value)`로 **canonical UTC 표기**만 받는다. receipt snapshot은 `rsv1.` 접두사 + unpadded base64url이고, 디코딩 후 **재인코딩 문자열 비교**로 alias(후행 비트가 0이 아닌 변형)를 거부한다. test가 그 하나하나를 이름으로 고정한다 — `canonicalDecoderRejectsWhitespaceReorderingEscapesNumbersUtf8AndTrailingContent`, `receiptSnapshotRejectsBase64urlAliasWithNonZeroTrailingBits`, `canonicalCodecRoundTripsSupplementaryUnicodeInOpaqueText`, `formulaMitigationCountCannotExceedCellsAndUsesOverflowSafeBounds`. 마지막 것은 코드에서도 확인된다. `requireFormulaCountWithinCe…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-wire-models/concept/concept-adapter-outbound-fileserver-c02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-fileserver-c02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-fileserver-c02.txt" - ] - }, - { - "title": "`ObjectBody`의 재생 가능성 판정 — 값의 성질이지 코덱의 성질이 아니다", - "kind": "concept", - "slug": "adapter-outbound-httpclient-c03", - "readiness": "READY", - "source": [ - "`final/document.md#a11#L237`" - ], - "owning-module": "`adapter-outbound-httpclient`", - "classification": "이 sub-scope에서 가장 신중한 코드다. 과거 동작과 그 결과가 적혀 있다. 지금은 `deeplyImmutable(value)`가 구조적으로 판정한다 — 문자열·숫자·불리언·문자·enum·UUID·`Temporal`은 통과, 컬렉션과 맵은 **JDK의 불변 뷰인지 이름으로 확인**하고 원소까지 재귀, record는 모든 성분을 반사로 재귀 확인, 그 외는 전부 `ONE_SHOT`. 반사가 실패하면 \"A component the platform cannot inspect cannot be certified, and an uncertified body is one-shot rather than optimistically replayable.\" 컬렉션 판정이 이름 기반인 이유도 적혀 있다 — \"`List.of(...)` and `Collections.unmodifiableList(...)` return package-private classes with no shared marker interface. An ordinary `ArrayList` the caller still holds is exactly the case this must not accept.\" test 넷이 네 갈래를 고정한다 — `anImmutableRecordReplays`, `aMutableValueIsOneShot`, `aRecordWrappingMutableStateIsOneShot`, `anArbitraryBeanIsOneShot`.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-wire-models/concept/concept-adapter-outbound-httpclient-c03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-httpclient-c03" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-httpclient-c03.txt" - ] - }, - { - "title": "두 예산, 두 계층, 그리고 읽는 도중의 강제", - "kind": "concept", - "slug": "adapter-outbound-httpclient-c06", - "readiness": "READY", - "source": [ - "`final/document.md#a11#L403`" - ], - "owning-module": "`adapter-outbound-httpclient`", - "classification": "`ResponseSizeLimiter`가 **와이어 바이트와 디코드 바이트를 따로** 센다 — \"a compressed payload passes a wire check and then expands, so a single limit either rejects legitimate traffic or lets a **decompression bomb** through.\" 그리고 `CountingBoundedInputStream`이 상한을 **읽는 도중에** 적용한다 — \"a response that is discovered to be too large only once it is fully buffered has already cost the memory the limit exists to protect.\" `BoundedErrorBody`는 오류 본문을 RFC 9457 문서를 해독할 만큼만 읽고, \"The bytes never reach an exception message or a log.\" `toString()`은 길이와 truncated 여부만 낸다. `RemoteProblemDecoder`의 규칙 한 줄이 이 계층의 성격을 요약한다 — \"**The wire status wins.** A remote `status` member is read and discarded, because trusting it would let an upstream **relabel a 503 as a 400 and change our retry behaviour from its own body.**\" 확장 속성도 allowlist로 걸러 \"an upstream cannot inject unbounded attributes into our telemetry.\" test 넷이 그 갈래를 고정한다(`mapsProblemJsonWithoutTrustingBodyStatus`·`dropsExtensionsThatAreNotAllowlisted`·`treatsANonProblemContentTypeAsAnEmptyProblem`·`survivesAnUnparseableProblemDocument`).", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-wire-models/concept/concept-adapter-outbound-httpclient-c06.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-httpclient-c06", - "adapter-outbound-httpclient-c06-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-httpclient-c06.txt" - ] - }, - { - "title": "레지스트리가 \"닫혀 있다\"는 것의 의미", - "kind": "concept", - "slug": "adapter-outbound-messaging-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a12#L174`" - ], - "owning-module": "`adapter-outbound-messaging`", - "classification": "`LocalJsonSchemaRegistry`의 한 줄 요약이 계약이다 — \"Immutable, startup-compiled Draft 2020-12 registry backed **only by explicitly supplied bytes**. Every reference is checked before NetworkNT compilation. After construction this type exposes **no loader, URL, file or classpath fetch operation**.\" 닫힘이 네 겹으로 표현된다. 1. **어휘 allowlist** — `KNOWN_VOCABULARIES` 8종(core·applicator·unevaluated·validation·meta-data·format-annotation·format-assertion·content) 밖의 `$vocabulary` 항목은 거부된다. 2. **키워드 부분집합** — `$anchor`·`$dynamicRef`·`$dynamicAnchor`·`$recursiveRef`·`$recursiveAnchor` 다섯이 `UNSUPPORTED_CLOSED_SUBSET_KEYWORDS`로 **문서 어디에서든** 거부된다(test `rejectsDynamicRecursiveAndAnchorKeywordsEverywhereInTheClosedSubset`). 3. **참조 사전 검사** — `validateAllReferences`가 NetworkNT 컴파일 **전에** 모든 `$ref`를 확인하고, 원격 참조와 설정된 깊이를 넘는 참조 그래프를 거부한다. 4. **핀 고정된 메타스키마 권위** — 9개 Draft 2020-12 메타 문서를 리소스로 동봉하고 `authority.sha256` 매니페스트로 해시를 고정하며, 도메인 분리 상수(`ca-skeleton.messaging.draft-2020-12-authority.v1`)를 섞는다. 매니페스트는 UTF-8 디코딩을 `REPORT` 모드로 읽어 잘못된 바이트를 조용히 대체하지 않는다. **실행 probe로 매니페스트를 검증했다** — 동봉된 9개 파일의 SHA-256이 `authority.sha256…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-wire-models/concept/concept-adapter-outbound-messaging-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-messaging-c01", - "adapter-outbound-messaging-c01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-messaging-c01.txt" - ] - }, - { - "title": "봉투 작성이 파서를 거치지 않는다", - "kind": "concept", - "slug": "adapter-outbound-messaging-c02", - "readiness": "READY", - "source": [ - "`final/document.md#a12#L189`" - ], - "owning-module": "`adapter-outbound-messaging`", - "classification": "`DeterministicEnvelopeWriter`는 페이로드를 **선언된 shape을 따라 스냅샷**한 뒤 그 정확한 바이트를 봉투에 끼워 넣는다 — \"those exact trusted bytes are then embedded in the envelope **without any raw JSON parser or generator API**.\" `embedExactPayload`가 `,\"payload\":` 리터럴로 이어 붙이는 방식이다. 입력 검증이 촘촘하다 — draft의 페이로드가 **정확히 등록된 final record 클래스**여야 하고(`exactPayloadClassIsRequiredAndNoAssignableTypeSearchOccurs`), contractId와 payloadVersion이 컴파일된 계약과 같아야 하며, 레코드 성분 수·문자열 UTF-8 길이·배열/객체 크기·깊이가 모두 `EnvelopeAdmissionLimits`로 유계다. 그리고 **쓰는 도중에** 출력 크기를 본다(`boundsJsonOutputDuringWritesInsteadOfOnlyInspectingTheCompletedBuffer`). 가변 페이로드 처리도 명시적이다 — `snapshotsStatefulMutablePayloadAccessorsOnceAndEmbedsThoseExactBytes`. 접근자를 한 번만 부르고 그 바이트를 고정하므로, httpclient의 `ObjectBody` 문제(같은 키로 다른 바이트)가 여기서는 구조적으로 불가능하다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-wire-models/concept/concept-adapter-outbound-messaging-c02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-messaging-c02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-messaging-c02.txt" - ] - }, - { - "title": "Confirmed — 계열이 닫혀 있고 스키마가 fail-closed다", - "kind": "concept", - "slug": "adapter-outbound-objectstorage-c04", - "readiness": "READY", - "source": [ - "`final/document.md#a09#L173`" - ], - "owning-module": "`adapter-outbound-objectstorage`", - "classification": "`ObjectControlRecord`는 열한 개 구현만 허용하는 `sealed interface`이고, javadoc이 규칙을 적는다 — \"Unknown families and schemas fail closed.\" codec의 `payload(...)`가 그 계열에 대해 **exhaustive switch**를 쓰므로, 새 레코드를 추가하면 컴파일이 강제로 codec을 갱신하게 만든다. 스키마 버전은 `ControlRecordSupport.header`가 `schemaVersion != 1`을 거부한다 — \"only control schema version 1 is writable\". 더 새로운 스키마를 만나면 덮어쓰지 않고 `UnsupportedObjectControlSchemaException`으로 격리한다(\"A newer or unknown durable schema that must be quarantined rather than overwritten\"). `objectstorage.control` 패키지를 leaf 밖에서 참조하는 코드는 **0**이다(`151-...` §8.1, exit=1). CLAUDE.md의 \"control-record types leaking into application-core\" 금지가 가시성으로 성립한다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-wire-models/concept/concept-adapter-outbound-objectstorage-c04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-objectstorage-c04" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-objectstorage-c04.txt" - ] - }, - { - "title": "Confirmed — 모든 키가 단일 인코더에서 나오고 route를 벗어날 수 없다", - "kind": "concept", - "slug": "adapter-outbound-objectstorage-c06", - "readiness": "READY", - "source": [ - "`final/document.md#a09#L273`" - ], - "owning-module": "`adapter-outbound-objectstorage`", - "classification": "`ObjectControlKeyCodec`과 `ObjectDataKeyCodec`이 각각 \"Sole encoder\"를 자칭하고, 저장소에서 `\"control/v1/\"`·`\"data/v1/\"` 리터럴은 이 두 파일에만 있다(`152-...` §8.3). 키 형태는 `control/v1////`이고 shard는 identity의 SHA-256 앞 두 자리다. route 격리가 구조적이다 — `requireMatchingRoute(route, routedIdentity)`가 routed identity의 route 구획을 파싱해 현재 route와 다르면 거부한다(\"routed identity belongs to a different route\"). reference·session·stage handle 키 모두 이 검사를 지난다. 핸들 계열도 접두사로 분리된다 — `osh1`(stage), `osu1`(direct upload), `osm1`(multipart), `osv1`(version), `osr1`(reference). `ObjectNamespaceCodecTest.referenceAndHandleFamiliesRemainSeparated`가 그 분리를 고정하고, `dataKeyApiHasNoRawNameStringParameter`는 **API 서명 자체에 raw 이름 문자열이 없음**을 단언한다. `CrockfordBase32`는 소문자 정규 알파벳(`0123456789abcdefghjkmnpqrstvwxyz` — I·L·O·U 제외)을 쓰고, 인코딩 후 남은 값이 있으면 거부한다(\"base32 output length is too small\") — 잘림을 조용히 넘기지 않는다. test에 property-based 검사가 있다 — `routeParserRejectsArbitraryNonCanonicalText(@ForAll String candidate)`(jqwik), `namespaceRejectsAliasesAndTraversalInputs`, 그리고 fingerprint codec에는 **golden vector**가 고정돼 있다(`canonicalIntentHas…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-wire-models/concept/concept-adapter-outbound-objectstorage-c06.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-objectstorage-c06" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-objectstorage-c06.txt" - ] - }, - { - "title": "`SignedJsonCursorCodec`: 좋은 trust-boundary 설계와 경계값 결함이 동시에 존재", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c05", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L358`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "codec은 다음 token을 만든다. 확인한 방어는 다음과 같다. signing key 최소 32 bytes URL-safe Base64 / no padding version까지 MAC input에 포함 token 전체 길이 4096-character cap payload 2048-byte cap presented MAC 32-byte exact length 확인 `MessageDigest.isEqual` constant-time comparison MAC 검증 전에 application payload decoder를 호출하지 않음 oversized public input을 substring/decode/MAC allocation 전에 거부하려는 선행 check 기존 dedicated tests도 tampering, foreign key, unknown version, short key, oversized token, wrong-length MAC, oversized payload 등을 폭넓게 검증한다. 문제는 decoded payload size를 decode **전에** 추정하는 helper다. 이 함수는 “최대 decoded size”를 빠르게 계산하려는 의도로 commit `2f5d2fc`에서 hostile-input bounds와 함께 추가됐다. 그러나 codec은 **unpadded Base64URL**을 사용한다. 실제 self-round-trip probe: 즉 현재 accepted encode domain과 accepted decode domain이 다르다. 이건 hostile token을 더 엄격히 거부하는 정도가 아니다. **codec의 자기 round-trip contract를 깨는 boundary defect**다. 실행 evidence: `evidence/raw/035a-jpa-cursor-boundary-probe.java` `evidence/raw/035-jpa-cursor-boundary-probe.txt` 현재 `SignedJsonCursorCodecTest`는: ordinary round-trip 2049-byte encode rejection decode 쪽 arbitrary oversized pa…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-wire-models/concept/concept-adapter-outbound-persistence-jpa-c05.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c05", - "adapter-outbound-persistence-jpa-c05-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c05.txt" - ] - }, - { - "title": "Hibernate provider policy는 declared baseline과 실제 runtime을 분리한다", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c25", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L1643`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "`HibernateProviderPolicy`는 상수로 선언된 Stable provider baseline과 실제 classpath에서 읽은 runtime version을 구분한다. 이 설계가 필요한 이유는 repository가 과거 “7.4를 Stable baseline이라고 문서화하면서 실제 Spring Boot BOM은 7.1.x를 resolve”한 상태를 경험했기 때문이다. declared baseline: policy constant runtime provider: `org.hibernate.Version`에서 읽음 drift 여부: `driftsFromDeclaredBaseline()` app-bootstrap capability/report가 runtime value를 사용 즉 “문서 상수와 같은 상수를 assert해서 green”인 self-fulfilling test는 피한다. 이 sub-scope에서 outside-leaf production consumer가 명확히 존재하는 핵심 Hibernate type도 `HibernateProviderPolicy`다. app-bootstrap이 이를 composition/report에 사용한다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-wire-models/concept/concept-adapter-outbound-persistence-jpa-c25.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c25", - "adapter-outbound-persistence-jpa-c25-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c25.txt" - ] - }, - { - "title": "Fileserver composition과 schema lifecycle", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c45", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L3166`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "Fileserver persistence는 latent helper가 아니라 실제 opt-in production capability다. `PersistenceJpaRootAutoConfiguration`이 `FileserverJpaPersistenceConfig`를 import한다. `app.fileserver-platform.enabled=true`이면 Fileserver entity/repository/component scan이 열린다. `FileserverStorageConfiguration.fileserverSchemaActivation()`은 `JdbcOperations`가 있으면 startup에서 `requireActive()`를 호출한다. 따라서 schema activation, quota, cleanup, recovery adapter는 Fileserver capability가 켜진 배포에서 production-reachable하다. V1은 registry에 `jpa-fileserver-metadata-v1`, `feature_revision=1`, `INSTALLED_INACTIVE`를 기록하고, V2는 recovery schema를 추가한 뒤 revision을 2로 올린다. 이후 V3는 fenced cleanup lease column을, V4는 upload terminal lifecycle column을 추가하지만 registry revision은 더 이상 갱신하지 않는다. 이 차이는 아래 startup fail-open finding의 직접 원인이다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-wire-models/concept/concept-adapter-outbound-persistence-jpa-c45.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c45" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c45.txt" - ] - }, - { - "title": "crypto envelope와 contact-point secret protection", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c49", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L3561`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "request variable payload는 `NotificationPayloadProtection`을 필수 collaborator로 받아 보호된 envelope를 저장하고, contact point는 ciphertext/nonce/lookup HMAC/key id로 분리된다. V10은 plaintext-looking request envelope를 DB constraint로도 거부한다. 이번 완독에서 이 경계 자체를 우회하는 production write path는 확인하지 못했다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-wire-models/concept/concept-adapter-outbound-persistence-jpa-c49.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c49" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c49.txt" - ] - }, - { - "title": "mapping의 나머지는 manifest를 실제로 강제한다", - "kind": "concept", - "slug": "adapter-outbound-persistence-mongo-c02", - "readiness": "READY", - "source": [ - "`final/document.md#a06#L493`" - ], - "owning-module": "`adapter-outbound-persistence-mongo`", - "classification": "P1과 별개로, 이 package의 나머지는 api manifest를 말이 아니라 코드로 만든다. `MongoCustomConversionsFactory.converters(...)`가 변환기를 **명시적 List 순서로** 조립한다. 이유가 주석에 있다 — Spring의 conversion service는 첫 매칭 변환기를 쓰므로 `Set`이나 classpath 스캔에서 조립하면 JVM 실행마다 다른 변환기가 선택될 수 있다. `fingerprint(manifest)`가 manifest fingerprint에 변환기 클래스 이름을 이어 붙여 golden BSON snapshot이 비교할 identity를 만든다. 같은 factory가 `requireEveryAxisImplemented(...)`로 `LOCAL_DATE_TIME_WITH_REGISTERED_CONVERTER`를 startup에서 거부한다. enum 상수 자신이 \"selecting this without registering the named converter is a startup failure\"라고 적어 둔 규칙을 실제로 집행하는 지점이다. `BigIntegerRepresentationConverters.forRepresentation(...)`은 manifest의 BigInteger 축을 세 변환기 쌍으로 컴파일한다. 주석이 과거 상태를 기록한다 — 이 축은 선언만 있고 컴파일되지 않아 `STRING`과 `DECIMAL128`이 동일한 document를 만들었고, 하나는 사전식으로 다른 하나는 수치로 정렬된다. `LocalDateTimeMappingGuard`는 `MongoMappingConfiguration`이 **실제 등록된 변환기**로 만든다. javadoc이 이전 결함을 적는다 — guard를 `withoutConverters()`로 만들고 manifest를 검증하게 해서, 명명된 변환기를 등록한 배포와 등록하지 않은 배포를 똑같이 거부했다. `BigDecimalToDecimal128Converter`는 driver 호출 전에 34 유효숫자·지수 범위를 검사한다. `Decimal128`은 초과 정밀도를 조용히 반올림하므로, 검사가 없으면 금액이 다른 값으로 …", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-wire-models/concept/concept-adapter-outbound-persistence-mongo-c02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-mongo-c02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-mongo-c02.txt" - ] - }, - { - "title": "Confirmed — 이 sub-scope는 정책과 값 객체이고, 배선된 것은 하나뿐이다", - "kind": "concept", - "slug": "adapter-outbound-persistence-mongo-c05", - "readiness": "READY", - "source": [ - "`final/document.md#a06#L709`" - ], - "owning-module": "`adapter-outbound-persistence-mongo`", - "classification": "auto-configuration이 이 sub-scope에서 만드는 bean은 **`MongoBudgetEnforcer` 하나**다(`132-...` §8.1). `MongoQueryPolicy`·`PolicyAwareMongoQueryBuilder`·`MongoRegexPolicy`·`MongoBudgetPolicyRegistry`·`MongoKeysetCursorCodec`·`PolicyAwareMongoAggregationExecutor`는 bean도 아니고 `main` 안에 소비자도 없다(§8.1 세 번째 검색 exit=1). 그 하나조차 짝이 없다. `MongoBudgetEnforcer`의 유일한 production 소비자는 `PolicyAwareMongoAggregationExecutor`인데 그것이 미배선이므로, 배선된 enforcer는 현재 아무도 호출하지 않는다. `MongoKeysetCursorCodec`은 32바이트 이상 서명 키를 요구하는데 그 키를 공급하는 production 코드가 없다 — 생성자 호출은 test 3곳뿐이다. 이것 자체는 결함이 아니다. 이 leaf는 가짜 도메인을 두지 않고 collection profile·field descriptor·budget을 fork가 선언하도록 설계돼 있으며, CLAUDE.md가 \"Real forks add their own document, repository, mapper\"라고 명시한다. 기록하는 이유는 두 가지다. (a) README의 D1/D2 표는 \"typed query, mapping manifest, atomic update, optimistic revision\"을 노출 계층의 내용으로 제시하는데, 그중 typed query 계열은 배선 없이 fork가 조립해야 한다는 사실이 그 표에 없다. (b) §41의 다음 항목이 그 조립 시점에만 문제가 된다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-wire-models/concept/concept-adapter-outbound-persistence-mongo-c05.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-mongo-c05" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-mongo-c05.txt" - ] - }, - { - "title": "Confirmed — 분류 불변식이 실제로 성립한다", - "kind": "concept", - "slug": "adapter-outbound-persistence-mongo-c09", - "readiness": "READY", - "source": [ - "`final/document.md#a06#L1269`" - ], - "owning-module": "`adapter-outbound-persistence-mongo`", - "classification": "세어 봤다(`137-...` §8.1b·§8.1c). `@MongoAdvancedEntryPoint` **7개**: `MongoChangeMessagingBridge`(CHANGE_STREAM), `MongoCsfleClientFactory`(CSFLE), `MongoQueryableEncryptionCollectionManager`(QUERYABLE_ENCRYPTION), `MongoGridFsMigrationJob`(GRIDFS_COMPATIBILITY), `MongoShardingAdminGateway`(SHARDING), `MongoTenantClientRegistry`·`MongoTenantMigrationCoordinator`(DATABASE_PER_TENANT). `@MongoAdvancedPolicy` **11개**. 어느 쪽도 아닌 구체 클래스 **1개**: `MongoAdvancedConfiguration`. 이것은 누락이 아니다 — `MongoAdvancedRules.concreteClass()`가 `@Configuration`을 명시적으로 제외하며 이유를 적는다: \"A `@Configuration` class is the package's composition root: it builds entry points through the guard rather than being one, and **gating it would gate the thing that supplies the guard**.\" interface·enum·record·익명·private 중첩·abstract도 같은 방식으로 제외되고 각각 근거가 붙어 있다. 즉 §83이 말하는 불변식은 문서가 아니라 코드로 서 있다. 이 leaf에서 \"문서가 주장하고 코드가 지키지 않는다\"를 여러 번 본 뒤라, 여기서는 그 반대가 성립한다는 것을 명시해 둘 가치가 있다. 또 하나의 confirmed: **`throw new UnsupportedOperationException`만 하는 public 메서드를 값으로 바꾼 수리**가 두 곳에서 같은 형태로 이루어졌다. `MongoTimeSeriesCapabilityValidator`는 네 개의 던지기만 하는 메서드를 …", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-wire-models/concept/concept-adapter-outbound-persistence-mongo-c09.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-mongo-c09" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-mongo-c09.txt" - ] - }, - { - "title": "public API와 secret boundary", - "kind": "concept", - "slug": "application-core-c10", - "readiness": "READY", - "source": [ - "`final/document.md#a03#L230`" - ], - "owning-module": "`application-core`", - "classification": "public contract는 arbitrary `Object`/`Map`를 허용하지 않고 sealed `NotificationVariable` algebra를 사용한다. 과거 mutable/arbitrary variable 때문에 serialization/fingerprint drift와 `toString` collision이 가능했던 것이 변경 근거다. structural test는 public API에 arbitrary Object가 다시 들어오지 않는지 검사하며, 과거 잘못된 test root로 vacuous pass했던 문제도 regression guard로 남아 있다. `NotificationPlan`은 exact template version을 pin한다. recipient/metadata/variable count/depth가 bounded되어 있고 receipt는 “durable logical acceptance”이지 provider delivery를 의미하지 않는다. contact point는 encrypted value + keyed fingerprint로 분리되고 protected contact rendering은 원문을 노출하지 않는다. template variable 자체에 reset token 같은 secret이 들어갈 수 있어 payload protection이 존재하며 decrypt 실패를 빈 값으로 degrade하지 않는다. contact lookup/provider request/callback fingerprint는 HMAC purpose를 분리해 동일 secret-purpose reuse를 피한다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-wire-models/concept/concept-application-core-c10.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "application-core-c10", - "application-core-c10-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/application-core-c10.txt" - ] - }, - { - "title": "재개 토큰 — 서명하고, 구분자를 봉인한다", - "kind": "concept", - "slug": "grpc-policy-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-policy#L73`" - ], - "owning-module": "`grpc-policy`", - "classification": "`GrpcResumeToken` 의 아홉 성분 각각이 왜 필요한지가 javadoc 에 있다 — 스냅숏 판본 없이는 사라진 뷰의 위치에서 재개하고, 만료 없이는 이력이 사라진 커서에서 재개하고, 필터 지문 없이는 남의 필터를 자기 위치에서 재개해 요청하지 않은 행을 받는다. 그리고 문자열 성분이 구분자를 담지 못하게 생성자가 거부한다. `GrpcResumeTokenCodec` 의 검증이 세 성질을 지킨다 — 상수 시간 비교(`MessageDigest.isEqual`), 알 수 없는 키 식별자 거부, 세 실패의 구분 불가.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-wire-models/concept/concept-grpc-policy-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-policy-c01" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-policy-c01.txt" - ] - }, - { - "title": "계약·불변식·상태 모델", - "kind": "concept", - "slug": "messaging-schema-json-c03", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-schema-json#L124`" - ], - "owning-module": "`messaging-schema-json`", - "classification": "여섯 가지 방어가 한 곳에 있다. 그리고 **polymorphic default typing을 켜지 않는다.** javadoc이 그것이 대부분의 JSON gadget chain의 기반이라고 적는다. `maxDocumentLength`가 `maxBytes`와 같다는 점이 중요하다 — 인코딩 상한과 디코딩 파서 상한이 하나의 값에서 나온다. 따로 두면 둘이 갈라진다. 주석이 이유를 적는다 — \"Jackson writes incrementally, so a payload whose serialized form is far larger than the limit stops at the limit instead of after the whole graph has been rendered into a buffer nobody bounded.\" `unwrapTooLarge`가 필요한 이유도 명시돼 있다. `for (Throwable cause = exception; cause != null; cause = cause.getCause())` — 원인 사슬을 끝까지 훑어 `MessageTooLargeException`을 찾는다. 못 찾으면 `MessageSerializationException(\"JSON_ENCODE_FAILED\")`. `SCHEMA_VERSION_NOT_REGISTERED` 메시지에는 `registeredVersions(type)`가 정렬되어 포함된다. 테스트가 그 내용을 직접 단언한다 — `hasMessageContaining(\"order.created v999\").hasMessageContaining(\"[1, 2]\")`(`JsonContractRegistryTest.java:58-61`). 운영자가 \"1과 2는 있고 999는 없다\"를 에러 메시지만으로 알 수 있다. 비대칭이 합리적이다. 인코딩에서 `OrderCreated`의 하위 타입을 넘기면 Jackson이 등록된 형태로 직렬화한다. 디코딩에서 하위 타입을 허용하면 등록된 계약과 다른 클래스로 역직렬화되므로 정확 일치여야 한다. 다만 이 비대칭은 주석으로 설명되지 않았다 — §15의 추론 항목이다. 명시 검사 하나(`encoded.length`)와 파서 내부 검사 하나(`max…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-wire-models/concept/concept-messaging-schema-json-c03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-schema-json-c03" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-schema-json-c03.txt" - ] - }, - { - "title": "모듈의 정체와 경계", - "kind": "concept", - "slug": "messaging-schema-protobuf-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-schema-protobuf#L49`" - ], - "owning-module": "`messaging-schema-protobuf`", - "classification": "선택적 Protobuf codec. `runtime_memberships: []`이고 starter의 codec registry에도 등록되지 않는다 — build-only / incubating. protobuf를 `api`로 선언한 이유가 build.gradle 주석에 있다. `src/messaging/CLAUDE.md:40-43`의 vendor `api` 게이트를 통과한다 — `ProtobufMessageContract(Class, Parser)`가 public record이므로 소비자가 그 타입을 이름 부르지 않고는 계약을 등록할 수 없다. **이 leaf의 핵심 문제 인식**은 클래스 javadoc이 한 문장으로 적는다. `messaging-schema-avro`의 \"does not fail — it produces plausible garbage\"와 같은 성질이다. **JSON은 틀린 스키마로 디코딩하면 대개 실패하고, Avro와 Protobuf는 실패하지 않는다.** 그래서 두 leaf 모두 registry를 계약의 중심에 둔다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-wire-models/concept/concept-messaging-schema-protobuf-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-schema-protobuf-c01" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-schema-protobuf-c01.txt" - ] - }, - { - "title": "의존성과 런타임 배선", - "kind": "concept", - "slug": "messaging-schema-json-c02", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-schema-json#L71`" - ], - "owning-module": "`messaging-schema-json`", - "classification": "들어오는 것: `messaging-core-api`(api), `messaging-schema-api`(api), `jackson-databind`(implementation). 나가는 것: `messaging-spring-boot-starter`(registry `allowed_dependencies`에 포함). **실제 배선 지점이 하나 있다** — 이 플랫폼에서 유일하게 조립되는 codec이다. `RegisteredMessageCodecs.of(defaultCodec, codecs...)`의 varargs 자리가 비어 있다. 즉 **출하 구성의 codec registry에는 JSON 하나만 들어간다.** Avro·Protobuf·raw bytes는 등록되지 않는다. 두 번째 배선 지점은 상수 참조다. payload 정책의 상한이 **JSON codec의 상수에서 파생된다.** 포맷 중립이어야 할 admission 정책이 한 포맷의 클래스 상수를 참조한다 — §17에서 다룬다. `contracts.getIfAvailable(MessageContracts::none)`이 기본값이므로, 애플리케이션이 `MessageContracts` bean을 내놓지 않으면 **빈 registry**로 codec이 만들어진다. 그 codec은 모든 `encode`/`decode`를 `UNKNOWN_MESSAGE_TYPE`으로 거절한다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-wire-models/concept/concept-messaging-schema-json-c02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-schema-json-c02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-schema-json-c02.txt" - ] - }, - { - "title": "패키지/컴포넌트 지도", - "kind": "concept", - "slug": "messaging-testkit-c03", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-testkit#L140`" - ], - "owning-module": "`messaging-testkit`", - "classification": "단일 패키지 `dev.caskeleton.messaging.testkit`. 세 소스셋이 같은 패키지를 공유하므로 `InMemoryMessagingHarness`(test)가 `FaultController`(main)를 package-private 없이 구현할 수 있고, `EnvelopeCodecBenchmark`(jmh)도 같은 패키지에 있다. 데이터 흐름은 한 방향이다. 핵심은 **화살표 방향이 한 번도 역전되지 않는다**는 것이다. 등급이 증거를 만들지 않고 증거가 등급을 만든다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-wire-models/concept/concept-messaging-testkit-c03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-testkit-c03" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-testkit-c03.txt" - ] - }, - { - "title": "주요 실행 경로", - "kind": "concept", - "slug": "messaging-schema-protobuf-c04", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-schema-protobuf#L229`" - ], - "owning-module": "`messaging-schema-protobuf`", - "classification": "**계약 등록:** `new ProtobufMessageContract(class, parser)` → 빈 입력 파싱 → 클래스 일치 확인 → 실패 시 `MessagingConfigurationException` **encode:** `requireRegistered` → `Message`이고 등록 클래스인지 → `requireFits(getSerializedSize())` → `writeTo(sink)` → `EncodedMessage(bytes, PROTOBUF, SchemaReference)` **decode:** `requireRegistered` → 요청 클래스 정확 일치 → `encoded.length` 상한 → `parser.parseFrom`", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-wire-models/concept/concept-messaging-schema-protobuf-c04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-schema-protobuf-c04" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-schema-protobuf-c04.txt" - ] - }, - { - "title": "실패 경로와 복구/번역", - "kind": "concept", - "slug": "messaging-schema-protobuf-c05", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-schema-protobuf#L239`" - ], - "owning-module": "`messaging-schema-protobuf`", - "classification": "**Avro와 다른 점 하나.** Avro는 `catch (IOException | RuntimeException)` 안에서 `MessageTooLargeException`을 `instanceof`로 통과시킨다. Protobuf는 `catch (IOException failure)`만 잡으므로 sink가 던지는 `MessageTooLargeException`(`RuntimeException`)이 그대로 전파된다. 별도 통과 로직이 필요 없다 — protobuf-java가 예외를 감싸지 않기 때문이다. 세 codec이 같은 문제를 세 가지로 푸는데(JSON은 원인 사슬 탐색, Avro는 즉시 `instanceof`, Protobuf는 아무것도 안 함) 각각 라이브러리 동작에 맞는 최소 해법이다. 다만 그 이유가 코드에 적혀 있지 않다. **계약 위반은 `CONFIGURATION`이고 메시지 실패가 아니다.** `ProtobufMessageContract` 생성 실패는 registry를 조립하는 시점, 즉 시작 시점에 난다. `MessagingConfigurationException` javadoc이 그 의도를 적는다 — \"Raised at startup wherever possible.\"", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-wire-models/concept/concept-messaging-schema-protobuf-c05.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-schema-protobuf-c05" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-schema-protobuf-c05.txt" - ] - }, - { - "title": "Git/설계 문서에서 확인한 변화와 실패 기록", - "kind": "concept", - "slug": "messaging-schema-protobuf-c07", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-schema-protobuf#L458`" - ], - "owning-module": "`messaging-schema-protobuf`", - "classification": "`ProtobufMessageContract` javadoc이 두 결함을 보존한다. 두 번째가 특히 이 저장소의 반복 주제다 — **실패의 종류가 바뀌면 운영자가 읽을 정보가 사라진다.** `messaging-core-api`의 `FailureDescriptor` 설계, `MessageContractKey`의 2단 에러, JSON codec의 `unwrapTooLarge`가 전부 같은 관심사다. `.proto` 파일의 주석도 설계 이유를 남긴다 — \"Field numbers are the contract, not the field names ... Tags are never reused, and removed fields are reserved so that a later edit cannot take the number back.\" 이 규칙 셋 중 둘(개명 안전, 태그 재사용 위험)이 테스트로 증명되고 하나(reserved)는 증명되지 않는다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-wire-models/concept/concept-messaging-schema-protobuf-c07.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-schema-protobuf-c07" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-schema-protobuf-c07.txt" - ] - }, - { - "title": "Messaging envelope schema", - "kind": "concept", - "slug": "shared-contract-c04", - "readiness": "READY", - "source": [ - "`final/document.md#a02#L114`" - ], - "owning-module": "`shared-contract`", - "classification": "`contracts/messaging/envelope/v1.schema.json`은 Draft 2020-12 schema resource이며 envelopeVersion/eventId/contractId/payloadVersion/logicalDestination/aggregate/occurredAt/correlationId/contentType/payload를 required로 고정하고 top-level/aggregate에 `unevaluatedProperties:false`를 둔다. checked-in SHA-256은 `bf6f2e13fafe01b8ef4cbb73d7ba3f5703bfc68d145bdfe43190bf606dbd00b1`이다. `MessagingEnvelopeSchemaResourceTest`는 schema text 자체, identifier regex parity, Java int/long 경계 vector, strict UTF-8, exact digest를 JDK API로 검증한다. 이 테스트는 resource drift와 digest mismatch를 강하게 막지만 README가 명시하듯 실제 Draft 2020-12 validator interoperability나 broker runtime discovery를 증명하지는 않는다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "schema-and-wire-models/concept/concept-shared-contract-c04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "shared-contract-c04" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/shared-contract-c04.txt" - ] - } - ], - "reference": [], - "question": [], - "decision": [] - } - }, - "admission-budget-and-backpressure": { - "topic": "admission-budget-and-backpressure", - "title": "", - "readerQuestion": "", - "kinds": { - "case": [], - "concept": [ - { - "title": "(8.3) 중복 메커니즘 — 예산 계층", - "kind": "concept", - "slug": "adapter-inbound-graphql-c04", - "readiness": "READY", - "source": [ - "`final/document.md#a16#L354`" - ], - "owning-module": "`adapter-inbound-graphql`", - "classification": "설계 §10이 다섯 계층을 정의하고 `GraphQlDeadlinePropagator`가 그 파생을 담는다. 실제 강제 상태: `GraphQlTimeoutPolicy`와 `GraphQlResolverBudget`의 main 참조자를 전수하면 전부 `execution` 패키지 안(그리고 미배선 클러스터 안)이다:", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "admission-budget-and-backpressure/concept/concept-adapter-inbound-graphql-c04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-graphql-c04", - "adapter-inbound-graphql-c04-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-graphql-c04.txt" - ] - }, - { - "title": "(8.4) 문서/구현 드리프트 — 취소 경로", - "kind": "concept", - "slug": "adapter-inbound-graphql-c05", - "readiness": "READY", - "source": [ - "`final/document.md#a16#L376`" - ], - "owning-module": "`adapter-inbound-graphql`", - "classification": "`GraphQlCancellation`(93)은 `cost/GraphQlRuntimeBudgetTracker` · `advanced/incremental` · `advanced/subscription` 세 곳에서 쓰인다. 요청 데드라인이 실제로 실행을 끊는 경로가 존재한다는 뜻이고, `GraphQlRequestContext.withDeadline`의 단조 조이기와 함께 요청 계층은 완결돼 있다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "admission-budget-and-backpressure/concept/concept-adapter-inbound-graphql-c05.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-graphql-c05" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-graphql-c05.txt" - ] - }, - { - "title": "(8.4) 게이트 프로퍼티가 존재하는가", - "kind": "concept", - "slug": "adapter-inbound-web-c05", - "readiness": "READY", - "source": [ - "`final/document.md#a14#L666`" - ], - "owning-module": "`adapter-inbound-web`", - "classification": "자바 한 줄뿐이다. 어떤 `application.yml`에도 `backend.web.budgets`가 없고 `matchIfMissing`도 없으므로 이 핸들러는 **기본 꺼짐**이다. 그리고 켜더라도 그 생성자가 요구하는 `BudgetProblemMapper` 빈을 선언하는 코드가 main·app-bootstrap 어디에도 없다 — 참조자는 두 필터와 이 핸들러 자신뿐이고, 셋 다 빈 정의가 아니다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "admission-budget-and-backpressure/concept/concept-adapter-inbound-web-c05.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-web-c05" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-web-c05.txt" - ] - }, - { - "title": "(8.1) 도달성 — forwarded 헤더 신뢰 정책", - "kind": "concept", - "slug": "adapter-inbound-web-c14", - "readiness": "READY", - "source": [ - "`final/document.md#a14#L1098`" - ], - "owning-module": "`adapter-inbound-web`", - "classification": "`proxy` 패키지 421 LOC이 프로덕션 조립에 들어가지 않는다. 실제로 forwarded 헤더를 해석하는 것은 Spring Boot의 `server.forward-headers-strategy=framework`(app-bootstrap `application.yml:321` 기본값)가 등록하는 `ForwardedHeaderFilter`/`ForwardedHeaderTransformer`이고, 그것은 **피어가 신뢰된 프록시인지 검사하지 않는다**. §32.2.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "admission-budget-and-backpressure/concept/concept-adapter-inbound-web-c14.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-web-c14" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-web-c14.txt" - ] - }, - { - "title": "(8.1) 도달성 — 시작 검증과 조립", - "kind": "concept", - "slug": "adapter-inbound-web-c18", - "readiness": "READY", - "source": [ - "`final/document.md#a14#L1336`" - ], - "owning-module": "`adapter-inbound-web`", - "classification": "`FileserverPlatformAutoConfiguration`이 `DefaultNginxInternalUriMapper`(`:215-216`) · `NginxDownloadStrategy`(`:221-223`) · `FileserverRequestContextFactory`(`:159-161`)를 만든다. 회로 닫힘.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "admission-budget-and-backpressure/concept/concept-adapter-inbound-web-c18.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-web-c18" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-web-c18.txt" - ] - }, - { - "title": "Confirmed — 두 프로그래밍 모델의 대칭이 기계 검사되고, 검사기 자신도 검사된다", - "kind": "concept", - "slug": "adapter-outbound-cache-redis-c04", - "readiness": "READY", - "source": [ - "`final/document.md#a10#L245`" - ], - "owning-module": "`adapter-outbound-cache-redis`", - "classification": "`ReactiveRedisOperations`는 \"Mirrors `RedisOperations` method for method\"라고 주장한다. `ApiParityTest`가 그것을 반사로 강제한다 — `PAIRS` 맵에 14쌍의 sync/reactive 인터페이스를 놓고 `everySyncOperationHasReactiveCounterpart`, `everyTypedSurfaceIsInParity`, `theTwoEntryPointsExposeTheSameStructureAccessors`, `everyReactiveMethodReturnsAPublisher`를 돌린다. 두 facade의 접근자 12개는 실제로 동일하다(diff 공백). 두 가지가 특히 좋다. 첫째, **예외가 이유와 함께 목록에서 빠져 있다** — Pub/Sub은 sync가 핸들러+closeable subscription이고 reactive는 publisher 자신이 전달하며 취소로 구독을 끊으므로 \"different shapes on purpose, so mechanical parity would be the wrong check for them\". 둘째, `theInspectorDetectsADivergentReturnShape`라는 **검사기에 대한 메타 test**가 있다 — 대칭 검사기가 고장 나 항상 통과하는 상태를 잡는다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "admission-budget-and-backpressure/concept/concept-adapter-outbound-cache-redis-c04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-cache-redis-c04", - "adapter-outbound-cache-redis-c04-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-cache-redis-c04.txt" - ] - }, - { - "title": "키: 렌더된 문자열을 받는 API가 존재하지 않는다", - "kind": "concept", - "slug": "adapter-outbound-cache-redis-c05", - "readiness": "READY", - "source": [ - "`final/document.md#a10#L324`" - ], - "owning-module": "`adapter-outbound-cache-redis`", - "classification": "`QualifiedRedisKey`의 javadoc이 이 계층의 규칙이다 — \"This is the only key shape the SDK accepts. **There is no API that takes an already rendered key string**, so namespace, slot, and size rules cannot be bypassed.\" 구조가 그것을 강제한다. `RedisTypedKey`는 9종만 허용하는 sealed interface고(`ValueKey`·`HashKey`·`ListKey`·`SetKey`·`SortedSetKey`·`BitmapKey`·`HyperLogLogKey`·`GeoKey`·`StreamKey`), 전부 `QualifiedRedisKey` + 코덱으로 구성된다. `QualifiedRedisKey`는 `RedisNamespace`(토큰 3개) + `RedisKeyName`(entity 토큰 + identifier) + 선택적 `RedisSlotTag`다. 그리고 `RedisKeyRenderer`가 **중괄호를 쓰는 유일한 장소**라서 Cluster 해시 태그가 \"the tag and nothing else\"를 덮는다. `RedisKeyRules`의 자기 한정도 정직하다 — 규칙은 \"mechanical\"이며 \"Values that are indistinguishable from an ordinary surrogate identifier, such as a bare digit string, cannot be rejected here; those must be fingerprinted by the caller before they become a key part.\" 즉 이 검사가 PII 방지의 완결이 아님을 명시한다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "admission-budget-and-backpressure/concept/concept-adapter-outbound-cache-redis-c05.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-cache-redis-c05" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-cache-redis-c05.txt" - ] - }, - { - "title": "raw gateway — \"escape hatch\"가 두 겹의 사전 승인으로 닫혀 있다", - "kind": "concept", - "slug": "adapter-outbound-cache-redis-c12", - "readiness": "READY", - "source": [ - "`final/document.md#a10#L684`" - ], - "owning-module": "`adapter-outbound-cache-redis`", - "classification": "`RedisRawGateway`의 javadoc이 존재 이유와 한계를 함께 적는다 — \"There is no `execute(String, byte[]...)` here or anywhere else in the SDK. The escape hatch exists because **some commands genuinely have no typed form worth building**, not because arbitrary command execution is acceptable; every one of them is named, bounded, and audited before it can be sent.\" 승인이 **두 개의 독립된 문**을 모두 통과해야 한다(`RawCommandApprovals`). 1. 명령이 정책 카탈로그에서 `RAW_ONLY`로 분류돼 있어야 한다 — \"the organization's decision about which commands may ever leave through this door\" 2. 배포가 그 명령에 대한 승인(`ApprovedRawCommand`)을 등록해야 한다 \"Neither alone is enough, and neither is decided at request time.\" 그리고 R3/R4는 어느 쪽이든 거부된다. `ApprovedRawCommand`는 **배포 산출물**이다 — 명령 identity, 최대 인자 수, 요청/응답 바이트 상한, 타임아웃, 응답 디코더를 프로세스 시작 전에 고정한다. 토큰은 `RawCommandApprovals`만 발급하고, 검증은 (a) 토큰 타입이 내부 record인지, (b) **발급 레지스트리 인스턴스가 같은지**(`issued.origin != this`), (c) 정책 id가 일치하는지, (d) 제시된 승인이 등록된 것과 같은지 넷을 본다. **`RawMovableKeys`가 이 패키지에서 가장 흥미롭다.** movable key spec(예: `SORT`)은 키 위치를 인자 목록이 결정하므로 정적으로 알 수 없고, 그러면 네임스페이스 검사를 할 수 없다. 기본은 여전히 거부다. 예외로 `SORT`/`SORT_RO` 파서 하나가 등…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "admission-budget-and-backpressure/concept/concept-adapter-outbound-cache-redis-c12.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-cache-redis-c12", - "adapter-outbound-cache-redis-c12-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-cache-redis-c12.txt" - ] - }, - { - "title": "Confirmed — 인가와 감사가 정보를 흘리지 않는다", - "kind": "concept", - "slug": "adapter-outbound-fileserver-c06", - "readiness": "READY", - "source": [ - "`final/document.md#a08#L507`" - ], - "owning-module": "`adapter-outbound-fileserver`", - "classification": "`RoleBasedFileAccessPolicy`는 열 개 연산을 READ/WRITE/ADMIN 세 계층으로 접는다. 근거가 적혀 있다 — 연산별 역할 맵은 `COPY`를 주고 `CREATE`를 안 주는 조합을 허용하는데 \"a copy creates a file\"이므로 제한처럼 보이고 제한이 아니다. **admin은 write를 상속하지 않는다** — 삭제할 수 있다는 이유로 force-delete까지 되면 감사되는 관리 평면이 일반 데이터 평면으로 도달 가능해진다. 빈 admin 역할 집합은 생성자가 거부한다(\"would leave the management plane unreachable rather than protected\"). 거부 메시지는 필요한 역할도 주체의 역할도 말하지 않는다 — \"a denial that reported what was missing would turn every 403 into a readable description of the role model\". `UnenforcedFileAccessPolicy`의 설계도 기록할 만하다. 이름 자체가 장치다 — composition root가 **타입 이름으로 매치해** production startup을 거부한다. \"A permissive default that looked like a real policy would ship as one.\" 실패 메시지 위생도 일관된다. `LocalStorageFailures`의 어떤 메시지도 경로·마운트·루트를 담지 않고, 감사 어댑터가 쓰는 필드는 전부 지문·코드·불투명 식별자다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "admission-budget-and-backpressure/concept/concept-adapter-outbound-fileserver-c06.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-fileserver-c06" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-fileserver-c06.txt" - ] - }, - { - "title": "`ClientProfileValidator` — 34개 위반 코드가 각각 과거 사고를 적는다", - "kind": "concept", - "slug": "adapter-outbound-httpclient-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a11#L90`" - ], - "owning-module": "`adapter-outbound-httpclient`", - "classification": "이 저장소에서 본 가장 조밀한 설정 검증기다. `validate(profile, environment)`가 11개 검사 그룹을 돌리고 결과를 정렬해 \"a configuration error reports deterministically across runs and machines\"를 보장한다. 특히 이 leaf에서만 보이는 태도가 하나 있다 — **바인딩은 되지만 어떤 전송에도 닿지 않는 설정을 무시하지 않고 거부한다.** 기본값은 통과시키므로 \"only a deliberate, unmet request fails\"다. 앞선 열 개 모듈에서 반복해 발견한 \"선언되었으나 아무것도 하지 않는 설정\" 패턴을, 이 모듈은 **명시적 거부로 처리한다.** 같은 논리가 관측 설정에도 적용된다 — `full-url-recording`은 아무도 읽지 않았고 `body-logging`은 actuator 보고에만 닿았다. \"Leaving them that way is the worse of the two failure modes — an operator who set them believed the platform was recording full URLs or bodies, and an operator who left them false had no assurance that it was not.\" 지금은 production에서 둘 다 거부된다. 나머지 검사도 각각 구체적인 다운그레이드를 막는다. **`REACTIVE_REDIRECT_UNSUPPORTED`** — 엔진 리다이렉트는 모든 전송에서 꺼져 있고 hop별 재검증을 하는 coordinator는 블로킹 스택에만 있다. 리액티브 프로파일이 redirect를 켜면 \"the caller received the 302 as an ordinary response and read its empty body as the answer.\" 거부가 정직한 결과다 — \"a configured guarantee that silently does nothing is worse than one the platform declines to offer.\" **`HTTP2_REQUIRED_TRANSPORT_UNSUPPORTE…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "admission-budget-and-backpressure/concept/concept-adapter-outbound-httpclient-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-httpclient-c01" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-httpclient-c01.txt" - ] - }, - { - "title": "가드 순서와 그 근거", - "kind": "concept", - "slug": "adapter-outbound-httpclient-c05", - "readiness": "READY", - "source": [ - "`final/document.md#a11#L323`" - ], - "owning-module": "`adapter-outbound-httpclient`", - "classification": "`AttemptResiliencePipeline`이 물리 시도마다 **Circuit Breaker → Rate Limiter → Bulkhead → HTTP 호출**을 고정 순서로 적용하고 역순으로 해제한다. 그리고 브레이커가 무엇을 보는지에 대한 수정 이력이 하나 더 있다 — 이전에는 원시 전송만 파이프라인 안에서 돌고 응답→예외 매핑이 밖에서 일어나서 \"a 503 completed the call normally, the breaker recorded a success, and **an upstream that answered nothing but 503 never opened its circuit. The thing the breaker is for was the one thing it could not see.**\" 지금은 `remoteFailure` 분류기가 반환값을 보고 브레이커에 알린다. test 47개가 이 규칙들을 촘촘히 덮는다 — `appliesCircuitThenRateLimiterThenBulkheadPerAttempt`, `openCircuitDoesNotConsumeRateOrBulkheadPermit`, `bulkheadRejectionReleasesTheRateLimiterAndIsNotACircuitError`, `answeredStatusesDoNotRetryANonIdempotentOperation`, `deniesOneShotBodyEvenForPut`, `honorsRetryAfterOnlyInsideDeadline`, `protocolProofOfNonProcessingWinsOverEverything`, `streamAfterGoAwayLastIdIsPeerNotProcessed` 등. `Http2ProtocolEvidence`는 프로토콜 수준 증거를 다룬다 — `REFUSED_STREAM`과 GOAWAY의 last-stream-id보다 큰 스트림 id는 **피어가 처리하지 않았음의 증명**이라 `NOT_SENT`로 승격되고, 그 이하 id의 리셋은 여전히 모호하다(`streamAtOrBelowGoAwayLastIdStaysAmbiguous`, `aBareStreamResetProvesNothi…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "admission-budget-and-backpressure/concept/concept-adapter-outbound-httpclient-c05.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-httpclient-c05", - "adapter-outbound-httpclient-c05-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-httpclient-c05.txt" - ] - }, - { - "title": "전송은 능력을 선언하고, 프로파일보다 약하면 startup이 실패한다", - "kind": "concept", - "slug": "adapter-outbound-httpclient-c08", - "readiness": "READY", - "source": [ - "`final/document.md#a11#L573`" - ], - "owning-module": "`adapter-outbound-httpclient`", - "classification": "`TransportCapabilityValidator`가 프로파일이 요구하는 것과 전송이 선언한 것을 대조해 부족분을 이름으로 모아 거부한다 — 프로토콜, route pool, 유계 pending 큐, proxy, mutual TLS, 동적 대상 안정성. 메시지는 \"profile settings and capability names only — never a URL, address, or secret.\" 능력 레코드가 그 선언을 데이터로 만든다. `ReactiveTransportCapabilities.reactorNetty()`는 9개 능력을 전부 `true`로, `jettyHttp3Experimental()`은 route pool·유계 큐·DNS 핀·동적 안정성을 `false`로 선언한다. HTTP/3는 `compileOnly` 의존이라 클래스가 없으면 `Http3CapabilityReport`가 전송을 거부한다 — \"the failure mode is a startup error rather than a `NoClassDefFoundError` mid-call\"(§0). testkit이 별도 source set인 것도 이 sub-scope의 성격이다 — 계약을 담은 클래스 35개(`BlockingTransportContract`·`ReactiveTransportContract`·`RetrySafetyContract`·`ResourceLifecycleContract`·`ObservabilityContract`·`DynamicTargetSecurityContract`)를 test·performance·jmh 세 lane이 공유한다. `NettyLeakDetectionExtension`은 leak detector 레벨을 **믿지 않고 확인한다** — \"asserts the level rather than trusting the flag reached the forked JVM\"(§0). 성능 lane 7개는 자원 상한을 검증한다 — `PoolSaturationPerformanceTest`·`RetryStormBudgetTest`·`RuntimeRotationDrainTest`·`OAuthRefreshContentionTest`·`Larg…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "admission-budget-and-backpressure/concept/concept-adapter-outbound-httpclient-c08.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-httpclient-c08" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-httpclient-c08.txt" - ] - }, - { - "title": "Confirmed — legacy가 세 겹으로 격리돼 있다", - "kind": "concept", - "slug": "adapter-outbound-objectstorage-c02", - "readiness": "READY", - "source": [ - "`final/document.md#a09#L92`" - ], - "owning-module": "`adapter-outbound-objectstorage`", - "classification": "폐기 경로가 셋인데 서로 다른 스위치를 쓰고 서로를 배제한다. `ObjectStorageBindingCompiler.rejectLegacyOverlap`가 legacy filesystem 루트와 canonical provider 루트가 **어느 방향으로든 포함 관계**면 거부한다. `LegacyObjectAdoptionSettings`는 `APPLY` 모드일 때 검토된 manifest 경로와 64자리 SHA-256을 요구하고, batch size 1–1000, timeout 5분 이내를 강제한다. legacy runtime은 `AutoCloseable` holder로 감싸 S3 client 수명을 정확히 소유하고, `@Bean(destroyMethod = \"close\")`로 등록된다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "admission-budget-and-backpressure/concept/concept-adapter-outbound-objectstorage-c02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-objectstorage-c02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-objectstorage-c02.txt" - ] - }, - { - "title": "objectstorage: staged lifecycle, opaque identity, privilege separation", - "kind": "concept", - "slug": "application-core-c08", - "readiness": "READY", - "source": [ - "`final/document.md#a03#L190`" - ], - "owning-module": "`application-core`", - "classification": "semantic objectstorage API는 provider/filesystem type을 노출하지 않는다. object identity/reference는 prefix + check digit를 포함한 opaque routed representation이고 redacted rendering을 제공한다. tampered/cross-prefix reference를 거부한다. content I/O는 bounded pull/push callback context와 budget/cancellation/chunk contract를 사용하며 callback lifetime 밖에서 context를 재사용할 수 없다. zero-progress가 무한 loop로 이어지지 않도록 bounded 후 실패한다. lifecycle은 staged -> verified -> published를 분리한다. scanner verdict는 exact stage/version/operation/policy revision에 결합되고 publish/cleanup mutation은 exact-version/fencing을 요구한다. scanner 권한과 purge 권한은 분리돼 검증 주체가 임의 삭제까지 할 수 없게 한다. transient bearer grant는 URI/header를 redaction하고 TTL은 최대 24시간으로 제한한다. multipart part count는 1..10000이고 completion은 expected content identity를 요구한다. `FullContentIdentity`는 SHA-256 기반으로 ETag를 content identity로 오인하지 않는다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "admission-budget-and-backpressure/concept/concept-application-core-c08.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "application-core-c08" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/application-core-c08.txt" - ] - }, - { - "title": "능력 15종과 등급 4종", - "kind": "concept", - "slug": "grpc-advanced-bootstrap-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-advanced-bootstrap#L53`" - ], - "owning-module": "`grpc-advanced-bootstrap`", - "classification": "능력을 하나씩 등급 매기는 것이 설계다. 기본 등급 분포는 `ADVANCED_STABLE` 11, `EXPERIMENTAL` 3(`HEDGING`·`CUSTOM_LOAD_BALANCER`·`XDS`), `WATCH` 1(`EDITION_2026`)이다. `EXPERIMENTAL` 에 두 번째 승인을 요구하는 근거가 적혀 있다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "admission-budget-and-backpressure/concept/concept-grpc-advanced-bootstrap-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-advanced-bootstrap-c01" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-advanced-bootstrap-c01.txt" - ] - }, - { - "title": "수동 흐름 제어", - "kind": "concept", - "slug": "grpc-advanced-streaming-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-advanced-streaming#L86`" - ], - "owning-module": "`grpc-advanced-streaming`", - "classification": "승인이 record 의 필드이고 거짓이면 생성자가 거부한다. 수요 상한과 교착 감시가 필수다 — 상한 없는 `request(n)` 은 단계만 늘린 무제한 버퍼링이다. 감시견은 잠들지 않고 두 시각을 비교한다 — 마지막으로 수요를 요청한 때와 마지막으로 메시지가 움직인 때. 둘 다 시간 제한만큼 멈춰 있으면 교착이다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "admission-budget-and-backpressure/concept/concept-grpc-advanced-streaming-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-advanced-streaming-c01" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-advanced-streaming-c01.txt" - ] - }, - { - "title": "주요 실행 경로", - "kind": "concept", - "slug": "messaging-admin-api-c04", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-admin-api#L493`" - ], - "owning-module": "`messaging-admin-api`", - "classification": "**경로 A — 계획 (승인 불필요, dry run 무료)** **경로 B — 승인 발급 (이 리프 밖, 변경관리 시스템)** **경로 C — 실행** 경로 C 에서 검사가 세 지점(verify / Approved*Plan / guard)에 걸쳐 겹친다. `ApprovalVerifier` javadoc 이 그 이유를 설명한다. 즉 서명 검증이 통과하면 나머지는 이미 보장되지만, `ApprovedReplayPlan` 생성자와 guard 가 같은 것을 다시 본다. 방어적 중복이며 §12.3(a) 에서 다시 다룬다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "admission-budget-and-backpressure/concept/concept-messaging-admin-api-c04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-admin-api-c04" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-admin-api-c04.txt" - ] - }, - { - "title": "실패 경로와 복구/번역", - "kind": "concept", - "slug": "messaging-policy-c05", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-policy#L423`" - ], - "owning-module": "`messaging-policy`", - "classification": "**배치 상한이 두 축인 이유**가 적혀 있다. `checkBatch`가 각 항목에 대해 `checkPayload`도 부르므로 **개별 상한 · 개수 상한 · 총합 상한** 셋이 함께 적용된다. 프로파일 검증 실패는 `IllegalArgumentException`이다 — `MessagingException` 계층 밖이다. 시작 시점의 구성 오류이지 메시지 실패가 아니므로 일관적이다. 다만 `MessagingConfigurationException`(\"Raised at startup wherever possible\")이 존재하는데 쓰이지 않는다 — §17의 P3.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "admission-budget-and-backpressure/concept/concept-messaging-policy-c05.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-policy-c05" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-policy-c05.txt" - ] - }, - { - "title": "Git/설계 문서에서 확인한 변화와 실패 기록", - "kind": "concept", - "slug": "messaging-schema-avro-c07", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-schema-avro#L457`" - ], - "owning-module": "`messaging-schema-avro`", - "classification": "테스트 클래스 javadoc이 세 결함을 보존한다. 세 번째가 형태상 가장 흥미롭다 — **바이트 상한이라는 올바른 도구가 잘못된 공격에 적용되어 있었다.** 테스트 javadoc이 그것을 한 문장으로 적는다: \"The byte limit is the wrong instrument for this attack and was the only one in place.\"", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "admission-budget-and-backpressure/concept/concept-messaging-schema-avro-c07.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-schema-avro-c07" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-schema-avro-c07.txt" - ] - }, - { - "title": "계약·불변식·상태 모델", - "kind": "concept", - "slug": "messaging-spring-cloud-stream-bridge-c03", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-spring-cloud-stream-bridge#L146`" - ], - "owning-module": "`messaging-spring-cloud-stream-bridge`", - "classification": "**세 거절이 `DestinationProfile`의 세 필드를 직접 본다.** 즉 **`messaging-policy`가 정의한 세 보장 각각에 대해 \"이것을 선언했으면 브리지를 쓸 수 없다\"**를 강제한다. 세 코드 전부 `MessagingConfigurationException`이고 안정 코드를 갖는다 — `messaging-kafka-share-experimental`이 두 거절에 다른 예외 타입을 쓴 것(그쪽 §17)과 대비된다. `!enabled`도 같은 예외 타입이다 — 일관적이다. 에러 메시지가 **두 선택지를 명시한다** — \"remove it from the binding or move the destination to the native adapter\". 무엇을 하라고만 하지 않고 어느 쪽을 포기할지를 준다. **production 목적지는 무조건 거절한다.** guard의 세 조건을 통과한 목적지(순서 없음·재시도 없음·DLQ 없음)라도 production이면 막는다. **네 번째 게이트**다. 바인딩 이름 패턴 `[a-zA-Z][a-zA-Z0-9-]{0,63}` — 언더스코어와 점을 배제한다. **\"nothing at runtime will show it\"**이 이 record가 존재하는 이유다. 네 boolean과 두 factory: `gaps()`가 각 `false`마다 **문장 하나**를 만든다. **각 문장이 결과까지 적는다** — \"indistinguishable\", \"loses the message\". 상태 플래그가 아니라 운영자가 읽는 진술이다. `isFullyGuaranteed()`가 `gaps().isEmpty()`다 — 매 호출마다 네 문장을 다시 만든다. 성능 문제는 아니지만 순수 조회가 문자열을 할당한다. `accepted == true`일 때의 결과: **`messaging-core-api`의 `PublishResult` 14개 금지 조합을 전부 통과하도록 정확히 구성돼 있다** — `AMBIGUOUS`는 `confirmationLevel == NONE`, `brokerAccepted == false`, `transmission != NOT_TRANSMITTED`, `routingOutco…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "admission-budget-and-backpressure/concept/concept-messaging-spring-cloud-stream-bridge-c03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-spring-cloud-stream-bridge-c03", - "messaging-spring-cloud-stream-bridge-c03-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-spring-cloud-stream-bridge-c03.txt" - ] - }, - { - "title": "Permission", - "kind": "concept", - "slug": "shared-contract-c02", - "readiness": "READY", - "source": [ - "`final/document.md#a02#L69`" - ], - "owning-module": "`shared-contract`", - "classification": "`Permission`은 정확히 한 개의 colon으로 `resource:action`을 분리하고 trim/lowercase normalization을 수행한다. 테스트는 mixed case, surrounding whitespace, blank component, 0/2+ colon을 검증한다. 다만 source는 component 내부 character set을 제한하지 않는다. 즉 \"lowercase colon-delimited\"는 normalization 결과이지 `[a-z0-9-]+` 같은 strict grammar는 아니다. 현재 test 역시 이를 요구하지 않으므로 observed contract로만 기록한다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "admission-budget-and-backpressure/concept/concept-shared-contract-c02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "shared-contract-c02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/shared-contract-c02.txt" - ] - } - ], - "reference": [], - "question": [], - "decision": [] - } - }, - "security-and-trust-boundaries": { - "topic": "security-and-trust-boundaries", - "title": "", - "readerQuestion": "", - "kinds": { - "case": [], - "concept": [ - { - "title": "(8.2) 조건 형제 비교 — off 계약의 두 절반", - "kind": "concept", - "slug": "adapter-inbound-graphql-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a16#L130`" - ], - "owning-module": "`adapter-inbound-graphql`", - "classification": "두 번째 테스트가 특히 정교하다. 리터럴 404를 단언하지 않고, 매핑된 적 없는 경로의 상태 코드와 **같은지**를 본다 — 주석이 그 이유를 적는다: \"Asserting a literal 404 would have been wrong: the security filter chain runs before ...\". 그리고 그 테스트는 이 leaf가 아니라 app-bootstrap에 있다. off 계약은 출하 조립에서만 검증할 수 있으므로 옳은 위치다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-and-trust-boundaries/concept/concept-adapter-inbound-graphql-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-graphql-c01" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-graphql-c01.txt" - ] - }, - { - "title": "(8.1) 도달성 — 신원 모델의 프로덕션 참조 수", - "kind": "concept", - "slug": "adapter-inbound-web-c03", - "readiness": "READY", - "source": [ - "`final/document.md#a14#L426`" - ], - "owning-module": "`adapter-inbound-web`", - "classification": "`AuthenticationView`를 만드는 코드도 테스트뿐이다: 즉 `security` 패키지 전체가 **자기 안에서만 서로를 부르는 닫힌 섬**이고, 바깥에서 들어오는 화살표가 없다. 교차 테넌트 가드 `rejectTenantInput`은 그 섬 안에만 있다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-and-trust-boundaries/concept/concept-adapter-inbound-web-c03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-web-c03", - "adapter-inbound-web-c03-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-web-c03.txt" - ] - }, - { - "title": "(8.3) 필터 체인 순서 — `publicPaths` 대 `RestrictedPathRule`", - "kind": "concept", - "slug": "adapter-inbound-web-c04", - "readiness": "READY", - "source": [ - "`final/document.md#a14#L473`" - ], - "owning-module": "`adapter-inbound-web`", - "classification": "Spring Security는 첫 일치가 이긴다. 주석은 \"Ordered before the authenticated catch-all: **a management path must be refused at the transport**\"라고 하는데, 그 순서는 `anyRequest()`에 대해서만 성립하고 `publicPaths`에 대해서는 반대다. §12.3. 프로덕션 `RestrictedPathRule` 생산자는 하나다 — `FileserverAdminPlaneConfiguration:36`이 fileserver 관리 경로를 등록한다. `publicPaths`의 기본값은 `${SECURITY_PUBLIC_PATHS:${PRESENTATION_API_BASE_PATH:/v1}/healthcheck}`로, 환경변수 하나로 전체가 대체된다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-and-trust-boundaries/concept/concept-adapter-inbound-web-c04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-web-c04", - "adapter-inbound-web-c04-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-web-c04.txt" - ] - }, - { - "title": "(8.4) 지문 정규화가 길이 프레이밍인가", - "kind": "concept", - "slug": "adapter-inbound-web-c07", - "readiness": "READY", - "source": [ - "`final/document.md#a14#L785`" - ], - "owning-module": "`adapter-inbound-web`", - "classification": "`SemanticRequestFingerprintFactory`는 U+001F 한 글자를 구분자로 쓰고, 경로 변수는 `SEP + name + \"=\" + value`, 헤더는 `SEP + name + \":\" + value`로 이어붙인다. 값에 대한 이스케이프나 길이 접두사가 없다. 같은 저장소의 다른 다이제스트들(notification `NotificationCatalogException.update`, messaging의 도메인 분리 상수)은 **4바이트 길이 프레이밍**을 쓰고, 그 이유를 \"인접 필드 연결로 인한 충돌이 구조적으로 불가능\"으로 적는다. §20.3.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-and-trust-boundaries/concept/concept-adapter-inbound-web-c07.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-web-c07" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-web-c07.txt" - ] - }, - { - "title": "목적지 정책 — 절대 URI를 정화하지 않고 거부한다", - "kind": "concept", - "slug": "adapter-outbound-httpclient-c07", - "readiness": "READY", - "source": [ - "`final/document.md#a11#L474`" - ], - "owning-module": "`adapter-outbound-httpclient`", - "classification": "`TrustedTargetPolicy`의 규칙 — \"An absolute URI is **rejected here rather than sanitised**: H2 exists to vary method, relative path, query, approved headers, and body — not the destination. Changing the destination is what H3 is for, and H3 has its own policy, credentials, and DNS validation.\" `requireRelativeTemplate`가 빈 템플릿, `//` 시작, `://` 포함, `/`로 시작하지 않음을 거부한다. 확장은 문자열 연결이 아니라 Spring `DefaultUriBuilderFactory`의 `TEMPLATE_AND_VALUES` 인코딩이라 \"a value containing `/`, `?`, or `#` cannot change the shape of the request.\" 그리고 확장 **후에** `requireAllowedOrigin`이 host/port allowlist를 다시 본다. 멱등성 키 처리에 수정 이력 둘이 붙어 있다. 그리고 리다이렉트 hop에 allowlist를 다시 적용하는 `requireAllowedTarget`이 public인 이유도 적혀 있다 — 조정자가 이전에는 리다이렉트 정책만 보고 프로파일 allowlist를 보지 않아 \"An upstream could therefore redirect a trusted profile to any origin the redirect policy tolerated, including one the operator had explicitly excluded.\"", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-and-trust-boundaries/concept/concept-adapter-outbound-httpclient-c07.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-httpclient-c07" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-httpclient-c07.txt" - ] - }, - { - "title": "교정 — 영구 TLS 실패의 `CONNECT` 분류는 분류기 결함이 아니라 픽스처의 듀얼스택 호스트명이다", - "kind": "concept", - "slug": "adapter-outbound-httpclient-c09", - "readiness": "READY", - "source": [ - "`final/document.md#a11#L627`" - ], - "owning-module": "`adapter-outbound-httpclient`", - "classification": "이 절은 이전 사이클이 여기에 적었던 **P1 진단을 철회하고 교체한다**. 관측된 실패는 그대로 재현되지만, 그 원인으로 지목했던 기전은 측정으로 반증되었다. 근거는 `EVD-332`다. `:adapter:outbound:httpclient:test` 는 HEAD 에서도 **283 중 3건 실패**한다. 세 건 모두 `MutualTlsHandshakeContractTest.java:168` — `assertThat(classified.stage()).isEqualTo(TLS_HANDSHAKE)` 다. 바로 앞줄인 167행(`evidence == NOT_SENT`)은 통과한다. 이전 사이클의 주장은 이랬다. **틀렸다.** 그 진단은 `ApacheFailureClassifier.recognize`의 분기 순서(pool → DNS → CONNECT → TLS)를 읽고 사슬의 모양을 추론한 것이지, 사슬을 실제로 떠본 것이 아니다. 잡힌 예외를 그대로 출력하면 이렇다. 사슬에 `SSLHandshakeException`이 **없다**. 그림자에 가려진 것이 아니라 애초에 도착하지 않았다. 분류기는 자기가 받은 것을 정확히 분류했다. 동일한 서버 객체, 동일한 클라이언트 신뢰재료. `baseUrl`의 호스트 문자열만 바꿨다. 이 컨테이너의 `/etc/hosts`는 `localhost`를 두 패밀리에 준다. `MockWebServer`는 IPv4 루프백에만 바인딩하고, `MockHttpServer.uri()`는 호스트명 `localhost`를 돌려준다 (`MockHttpServer.java:70-72`). Apache HttpClient 5의 연결 오퍼레이터는 해석된 주소를 순회하면서 **마지막이 아닌 주소의 실패를 삼킨다**. 호출자에게 도달하는 유일한 예외는 두 번째 주소의 연결 거부다. 핸드셰이크가 **성공하는** test가 통과하는 이유도 같은 루프다. 127.0.0.1 에서 성공하면 루프가 즉시 반환하므로 `::1`을 시도하지 않는다. 따라서 처음 눈에 띄었던 `startTls(..., true/false)` 차이는 원인이 아니라 상관관계였다 — 실패하는 케이스가 곧 두 번째 주소까지 가는 케이스다. `startTls(..., fals…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-and-trust-boundaries/concept/concept-adapter-outbound-httpclient-c09.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-httpclient-c09" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-httpclient-c09.txt" - ] - }, - { - "title": "(8.4) 문서/카운트 드리프트 — 어떤 상태가 unhealthy인가", - "kind": "concept", - "slug": "adapter-outbound-notification-c04", - "readiness": "READY", - "source": [ - "`final/document.md#a13#L619`" - ], - "owning-module": "`adapter-outbound-notification`", - "classification": "`NotificationHealthReporter.snapshot()`이 `healthy = false`로 넘어가는 조건은 넷이다: 세 번째 조건에는 이 저장소 특유의 자기고발이 붙어 있다: \"A platform with providers but no route accepts every request and delivers none. It was reported healthy because every runtime was healthy — **which was true and beside the point**.\" 여기서 눈에 띄는 것은 **`DRAINING`이 목록에 없다**는 점이다. 로테이션 중 드레인은 정상 운영이므로 그 자체로는 옳다. 그러나 §13의 P2와 겹치면 부작용이 하나 더 생긴다 — 아래 §21.2.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-and-trust-boundaries/concept/concept-adapter-outbound-notification-c04.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-notification-c04" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-notification-c04.txt" - ] - }, - { - "title": "Confirmed — 후보로 본 unguarded split은 값 타입이 막고 있다", - "kind": "concept", - "slug": "adapter-outbound-objectstorage-c03", - "readiness": "READY", - "source": [ - "`final/document.md#a09#L135`" - ], - "owning-module": "`adapter-outbound-objectstorage`", - "classification": "`RoutingObjectReadAdapter.load`가 `reference.canonicalText().split(\"\\\\.\", -1)[1]`로 route token을 꺼낸다. 인덱스 검사가 없어 처음에는 `ArrayIndexOutOfBoundsException` 후보로 봤다. `ObjectReference`를 확인한 결과 생성자가 `ObjectIdentitySupport.requireRouted(canonicalText, \"osr1\")`로 형태를 강제하므로, 유효하게 만들어진 참조에는 항상 route 구획이 있다(`150-...` §8.4d). 결함이 아니다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-and-trust-boundaries/concept/concept-adapter-outbound-objectstorage-c03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-objectstorage-c03" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-objectstorage-c03.txt" - ] - }, - { - "title": "tenant repository/listener guard가 곧 production isolation이라는 주장", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c53", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L3784`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "`TenantAwareRepositoryGuard`와 `TenantEntityListenerGuard`의 local behavior는 fail-closed지만 현재 production repository/entity에 연결된 caller/listener registration은 없다. 따라서 이 type들이 존재한다는 이유만으로 현재 application의 tenant isolation이 보장된다고 쓰지 않는다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-and-trust-boundaries/concept/concept-adapter-outbound-persistence-jpa-c53.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c53" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c53.txt" - ] - }, - { - "title": "global masking도 이 보장을 복구하지 않는다", - "kind": "concept", - "slug": "adapter-outbound-support-c03", - "readiness": "READY", - "source": [ - "`final/document.md#a04#L207`" - ], - "owning-module": "`adapter-outbound-support`", - "classification": "`app-bootstrap`의 `LogMaskingPatterns`는 방어 심층화로 다음과 같은 secret 형태를 mask한다. password/secret/token/api-key 계열 key=value Authorization credentials standalone Bearer token 그러나 arbitrary email address나 free-form body PII를 일반적으로 제거하는 규칙은 없다. app-bootstrap README 자체도 regex masking을 **보증이 아니라 defence-in-depth**라고 설명한다. 따라서 현재 “logger signature 때문에 PII가 들어올 수 없다”는 1차 방어선 설명은 사실과 맞지 않는다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-and-trust-boundaries/concept/concept-adapter-outbound-support-c03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-support-c03" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-support-c03.txt" - ] - }, - { - "title": "outbound peer isolation", - "kind": "concept", - "slug": "adapter-outbound-support-c05", - "readiness": "READY", - "source": [ - "`final/document.md#a04#L368`" - ], - "owning-module": "`adapter-outbound-support`", - "classification": "`CleanArchitectureTest.OUTBOUND_ADAPTERS_ARE_PEERS_SHARING_ONLY_SUPPORT`는 outbound adapter family를 slice로 나누고 서로 직접 의존하지 못하게 한다. 유일한 shared-code 예외는 target package가: 인 dependency다. 따라서 messaging → notification 같은 peer coupling은 금지하지만 messaging → support는 허용한다. fresh `CleanArchitectureTest --rerun-tasks`도 통과했다. 이 구조는 support 모듈이 단순 편의 library가 아니라 **outbound family에서 sanctioned shared dependency point**라는 점을 build-time fitness function으로 고정한다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-and-trust-boundaries/concept/concept-adapter-outbound-support-c05.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-support-c05" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-support-c05.txt" - ] - }, - { - "title": "(8.1) 도달성 — 레지스트리 계약이 실제 레지스트리 파일을 읽는가", - "kind": "concept", - "slug": "app-bootstrap-c05", - "readiness": "READY", - "source": [ - "`final/document.md#a18#L460`" - ], - "owning-module": "`app-bootstrap`", - "classification": "`MasterSwitchRegistryContractTest`가 `docs/registries/env-keys.yaml`과 `src/app-bootstrap/src/main/resources/application.yml`을 실제로 읽어 대조한다(§2). `ErrorCodeRegistryMappingTest`·`SecretsClassificationRegistryTest`도 `docs/registries/` 아래 파일을 읽는다. 파일 기반 SSOT가 테스트로 고정돼 있다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-and-trust-boundaries/concept/concept-app-bootstrap-c05.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "app-bootstrap-c05" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/app-bootstrap-c05.txt" - ] - }, - { - "title": "authorization: permission과 object access를 분리한다", - "kind": "concept", - "slug": "application-core-c02", - "readiness": "READY", - "source": [ - "`final/document.md#a03#L78`" - ], - "owning-module": "`application-core`", - "classification": "`AuthorizationPort`는 principal의 raw role/permission을 기준으로 “이 종류의 작업을 수행할 수 있는가”를 판정하는 framework-free PEP다. `AuthorizationPrincipal`은 role set을 defensive copy + unmodifiable로 만들고 null roles는 empty set으로 정규화한다. `AuthorizationDeniedException`은 Spring `AccessDeniedException` 대신 application-owned failure를 사용한다. object-level access는 별도 `ObjectAccessPolicy`가 담당한다. 같은 permission을 가진 사용자라도 ownership, membership, workflow state에 따라 특정 object 접근 결과가 달라질 수 있기 때문이다. `ObjectAccessDecision`은 denial에 stable code를 요구하고 `hideExistence`를 별도 boolean으로 보존해 transport가 403/404 disclosure 정책을 추측하지 않게 한다. **Historical evidence.** `ObjectAccessPolicyTest`에는 이 계약이 과거 inbound GraphQL adapter에 있었고 GraphQL request context를 signature에 포함해 application-core가 구현하려면 transport에 역의존해야 했던 문제가 기록돼 있다. 현재 regression test는 policy/request/decision signature에 `dev.caskeleton.adapter.*` 타입이 다시 등장하면 실패한다. 이 프로젝트에서 “여러 호출자가 공유해야 하는 계약을 inbound adapter가 소유하면 Core가 Adapter에 의존하게 된다”는 문제가 실제로 있었던 근거다. `decideAll()`의 default는 요청 순서를 보존하지만 object마다 `decide()`를 호출한다. set-based authorization을 제공하는 구현체가 override하지 않으면 batched loading 안에…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-and-trust-boundaries/concept/concept-application-core-c02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "application-core-c02", - "application-core-c02-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/application-core-c02.txt" - ] - }, - { - "title": "승격 게이트", - "kind": "concept", - "slug": "grpc-advanced-bootstrap-c03", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-advanced-bootstrap#L89`" - ], - "owning-module": "`grpc-advanced-bootstrap`", - "classification": "증거는 능력마다 따로 기록된다. 일곱 항목(호환성·보안 검토·고장·성능·ADR·런북·실환경 테스트)과 담금 기간을 본다. 임계값이 둘이다. 그리고 `WATCH` 는 `EXPERIMENTAL` 을 먼저 거쳐야 한다. `GrpcAdvancedSupportMatrix.apply` 는 결정의 시작 등급이 현재 등급과 다르면 거부한다 — 두 승격이 경합했거나 하나가 재생된 경우다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-and-trust-boundaries/concept/concept-grpc-advanced-bootstrap-c03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-advanced-bootstrap-c03" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-advanced-bootstrap-c03.txt" - ] - }, - { - "title": "모듈의 정체", - "kind": "concept", - "slug": "grpc-advanced-diagnostics-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-advanced-diagnostics#L41`" - ], - "owning-module": "`grpc-advanced-diagnostics`", - "classification": "진단 표면(Channelz·CSDS)과 그것을 게시 가능하게 만드는 편집기, 그리고 고급 능력이 무엇을 상대로 검증되어야 하는지를 이름 짓는 테스트킷 계약을 담는다. 편집기 javadoc 이 왜 이것이 필요한지 적는다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-and-trust-boundaries/concept/concept-grpc-advanced-diagnostics-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-advanced-diagnostics-c01", - "grpc-advanced-diagnostics-c01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-advanced-diagnostics-c01.txt" - ] - }, - { - "title": "모듈의 정체와 경계", - "kind": "concept", - "slug": "grpc-observability-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-observability#L46`" - ], - "owning-module": "`grpc-observability`", - "classification": "세 층위를 구별한다 — 논리 RPC, 물리 시도, 스트림 수명주기. Micrometer 를 `api` 로 노출하는 이유도 build.gradle 에 적혀 있다 — \"the observation convention's public signatures name Micrometer types, so wiring it requires naming them.\" 실제로 `GrpcObservationConvention` 의 생성자와 `boundedTags` 반환형이 Micrometer 타입(`MeterRegistry`, `Tags`)이므로 그 서술은 코드와 일치한다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-and-trust-boundaries/concept/concept-grpc-observability-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-observability-c01", - "grpc-observability-c01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-observability-c01.txt" - ] - }, - { - "title": "모듈의 정체와 격리 규칙", - "kind": "concept", - "slug": "grpc-spring-boot-starter-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-spring-boot-starter#L42`" - ], - "owning-module": "`grpc-spring-boot-starter`", - "classification": "격리 규칙은 세 겹이다 — 레지스트리, 빌드 검증 태스크, 그리고 자바 쪽 단언. 세 번째는 `validateAdvancedIsolation` 이 `GrpcStableBuildInvariant.requireNoAdvancedDependency` 를 부르는 형태다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-and-trust-boundaries/concept/concept-grpc-spring-boot-starter-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-spring-boot-starter-c01", - "grpc-spring-boot-starter-c01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-spring-boot-starter-c01.txt" - ] - }, - { - "title": "의존성과 런타임 배선", - "kind": "concept", - "slug": "messaging-admin-api-c02", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-admin-api#L98`" - ], - "owning-module": "`messaging-admin-api`", - "classification": "`messaging-core-api` 에서 쓰는 것: `DestinationName`, `MessageAuthorizationException`, `MessagingConfigurationException`. `messaging-policy` 는 import 0건이다(§12.4). leaf 밖 소비자 21개 파일 / 4개 모듈: 부팅된 애플리케이션에서 이 리프의 타입 중 실제로 살아나는 것은 **둘뿐**이다(`EVD-302`, `EVD-303`). `MessagingAdminService` 빈은 없고 `ApprovalVerifier` 빈도 없다. 이는 명시된 설계다. 그러나 이 스탠스가 **토폴로지 검증까지 덮지는 않는다** — §12.1 과 §17 의 첫 항목이 그것이다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-and-trust-boundaries/concept/concept-messaging-admin-api-c02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-admin-api-c02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-admin-api-c02.txt" - ] - }, - { - "title": "실패 경로와 복구/번역", - "kind": "concept", - "slug": "messaging-admin-api-c05", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-admin-api#L543`" - ], - "owning-module": "`messaging-admin-api`", - "classification": "전부 `MessageAuthorizationException` 또는 `MessagingConfigurationException` 이고, 코드가 붙어 있다. 메시지가 전부 \"무엇이 왜 거절되었는가\" 를 서술형으로 쓴다. 예: `IllegalArgumentException` 은 **구조적으로 불가능한 값**에만 쓴다 — 음수 카운터, 빈 문자열, 역전된 윈도우, 4-eyes 위반. 인가 실패와 프로그래밍 오류가 예외 타입으로 갈린다. `toString()` 하나가 로그 위생을 명시적으로 다룬다. `ApprovalGrant` 는 record 라 기본 `toString()` 이 전 필드를 찍는다는 점은 대비된다 — 다만 `ApprovalGrant` 자체가 로그에 닿는 경로는 확인되지 않았다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-and-trust-boundaries/concept/concept-messaging-admin-api-c05.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-admin-api-c05" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-admin-api-c05.txt" - ] - }, - { - "title": "자격증명은 연결 시도마다 해석된다", - "kind": "concept", - "slug": "messaging-rabbit-c02", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-rabbit#L106`" - ], - "owning-module": "`messaging-rabbit`", - "classification": "`AmqpCredentials` 가 record 가 아니라 class 인 이유도 적혀 있다 — 비밀을 지우려면 가변이어야 하고, record 가 `char[]` 를 동등성에 쓰면 같은 자재를 가진 둘이 서로 다르다고 판정된다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "security-and-trust-boundaries/concept/concept-messaging-rabbit-c02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-rabbit-c02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-rabbit-c02.txt" - ] - } - ], - "reference": [], - "question": [], - "decision": [] - } - }, - "capability-and-disclosure-models": { - "topic": "capability-and-disclosure-models", - "title": "", - "readerQuestion": "", - "kinds": { - "case": [], - "concept": [ - { - "title": "(8.2) 조건 형제 비교 — 두 자동설정의 게이트", - "kind": "concept", - "slug": "adapter-inbound-web-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a14#L116`" - ], - "owning-module": "`adapter-inbound-web`", - "classification": "둘 다 `matchIfMissing = true` — **기본 켜짐**이다. notification·messaging·cache-redis가 전부 `matchIfMissing = false`(옵트인)인 것과 반대인데, 이유가 다르다: 저쪽은 선택적 능력이고 이쪽은 웹 애플리케이션의 본체다. 상호배타성은 프로퍼티가 아니라 `@ConditionalOnWebApplication`의 타입 수준에서 온다 — \"a reactive application cannot accidentally activate the servlet filters even if both artifacts are on the classpath.\" 두 자동설정이 등록하는 빈은 MVC 12개, WebFlux 11개다. `AutoConfiguration.imports`에는 이 둘만 있다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "capability-and-disclosure-models/concept/concept-adapter-inbound-web-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-web-c01", - "adapter-inbound-web-c01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-web-c01.txt" - ] - }, - { - "title": "(8.3) 중복 메커니즘 — 하나의 스위치가 두 능력을 켠다", - "kind": "concept", - "slug": "adapter-inbound-web-c17", - "readiness": "READY", - "source": [ - "`final/document.md#a14#L1255`" - ], - "owning-module": "`adapter-inbound-web`", - "classification": "`NDJSON`과 `JSON_SEQUENCE`는 enum에서 서로 다른 상수이고 각자 프로퍼티 이름을 갖는데, 실제로는 `ndjson` 스위치 하나가 둘을 함께 켠다. `WebAdvancedFeature`의 javadoc이 금지한 형태다 — \"A single switch would make those one decision.\"", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "capability-and-disclosure-models/concept/concept-adapter-inbound-web-c17.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-web-c17" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-web-c17.txt" - ] - }, - { - "title": "Confirmed — \"컴파일이 먼저, 생성은 나중\"이 실제 순서다", - "kind": "concept", - "slug": "adapter-outbound-objectstorage-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a09#L63`" - ], - "owning-module": "`adapter-outbound-objectstorage`", - "classification": "`ObjectStorageProviderContribution`이 두 메서드의 계약을 나눈다. `ObjectStorageCapabilityAssembler.assemble`이 그 순서를 지킨다 — `compiler.compile(settings)`가 **전부** 끝난 뒤(`:25`)에야 선택된 destination을 돌며 `contribution.create(provider)`를 부른다(`:48`). 그리고 도중에 실패하면 이미 만든 것을 **역순으로** 닫는다(`:53–56`). `AssembledCapability.close()`도 역순이고 `AtomicBoolean`으로 정확히 한 번만 실행된다. README의 \"Settings compile fully before any selected provider creates a directory, client, thread, scheduler, or credential lookup\"이 코드 구조로 성립한다. 컴파일러 자체가 fail-closed다. 비활성이면 빈 바인딩을 돌려주고, 활성인데 provider·destination·default destination 중 하나라도 비면 거부한다. provider마다 `describe`가 돌려준 서술자와 설정을 **대조**한다 — providerType 일치, version 일치, `maximumObjectBytes`가 서술자 상한 이하, `chunkBytes`가 서술자 상한 이하. chunk는 추가로 `1 ≤ chunk ≤ min(maxObject, 16 MiB)`이고 `Integer.MAX_VALUE`를 넘지 못한다. destination은 route token 중복을 거부하고, 요구한 capability를 provider가 `SUPPORTED`로 신고하지 않으면 거부하며, `SCAN_CLEAN`을 요구하는데 scanner seam이 없으면 이름을 대며 거부한다. 식별자 검증도 좁다 — `canonicalId`는 64자 이내, `[a-z0-9][a-z0-9_-]*`, 소문자, 그리고 **0x20–0x7e 밖 문자를 전부 거부**한다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "capability-and-disclosure-models/concept/concept-adapter-outbound-objectstorage-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-objectstorage-c01", - "adapter-outbound-objectstorage-c01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-objectstorage-c01.txt" - ] - }, - { - "title": "§41 보강 — 레지스트리는 문서 주장을 얼어붙히지만 런타임 설정 경로는 덮지 않는다", - "kind": "concept", - "slug": "adapter-outbound-objectstorage-c08", - "readiness": "READY", - "source": [ - "`final/document.md#a09#L560`" - ], - "owning-module": "`adapter-outbound-objectstorage`", - "classification": "§41에서 \"R0 경계가 문서에만 있다\"고 적었다. §47을 반영해 정확히 다시 말한다. R0 경계는 **문서 주장에 대해서는** 기계 검사된다(§47). 그러나 그 검사의 대상은 `docs/registries/object-storage-readiness.yaml`이고, `KNOWN_PROVIDERS`는 `filesystem-local-dev` 하나다. 운영자가 `app.object-storage` 설정에 AWS provider용 qualification profile을 쓰면서 `DIRECT_UPLOAD` capability를 주장하는 경로는 이 레지스트리를 **거치지 않는다**. `S3ProviderBinding.compileProfiles`가 그 주장을 MinIO에 대해서만 거부하므로, AWS + DIRECT_* 조합은 여전히 compile을 통과하고 presigner를 할당한다(§41). 따라서 §41의 판정은 유지되고 오히려 선명해진다 — 이 저장소에는 \"이 카드는 R0\"를 강제하는 장치가 이미 있는데, 런타임 설정 경로가 그 장치의 사정권 밖에 있다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "capability-and-disclosure-models/concept/concept-adapter-outbound-objectstorage-c08.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-objectstorage-c08" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-objectstorage-c08.txt" - ] - }, - { - "title": "이 sub-scope는 하나의 query framework가 아니라 세 단계의 정책층이다", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c24", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L1610`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "현재 code shape는 대략 다음처럼 읽는 것이 맞다. 중요한 점은 `springdata`와 `querydsl`이 application-core의 repository contract를 대체하는 generic CRUD layer가 아니라는 것이다. `JpaRepositoryFragmentSupport`에는 범용 `save/findAll/delete`가 없고, domain-owned adapter가 필요한 query mechanism만 조합하게 설계돼 있다. 이 방향은 support matrix의 “platform-owned generic CRUD repository는 unsupported”와 일치한다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "capability-and-disclosure-models/concept/concept-adapter-outbound-persistence-jpa-c24.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c24", - "adapter-outbound-persistence-jpa-c24-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c24.txt" - ] - }, - { - "title": "(8.3) 중복 메커니즘 — 세 개의 환경 검증기", - "kind": "concept", - "slug": "app-bootstrap-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a18#L179`" - ], - "owning-module": "`app-bootstrap`", - "classification": "`MasterSwitchEnvironmentPostProcessor`(스위치 값 문법) · `RuntimeEnvironmentProfileValidator`(93, 프로파일) · `CapabilityDependencyEnvironmentValidator`(62 → `CapabilityDependencyValidator` 156, 능력 간 의존). 셋 다 `EnvironmentPostProcessor`이고 관심사가 다르다. 중복 아님.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "capability-and-disclosure-models/concept/concept-app-bootstrap-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "app-bootstrap-c01" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/app-bootstrap-c01.txt" - ] - }, - { - "title": "— 다섯 어댑터 범위는 런타임 멤버십 레지스트리와 일치한다 (결함 아님)", - "kind": "concept", - "slug": "app-bootstrap-c02", - "readiness": "READY", - "source": [ - "`final/document.md#a18#L185`" - ], - "owning-module": "`app-bootstrap`", - "classification": "§3.1의 표를 처음에는 \"출하되는 스위치가 다섯보다 많다\"는 결함으로 기록했다. 그 판정은 **틀렸다**. `src/config/architecture/modules.json`의 `runtime_memberships`가 결정적이다: **gRPC와 WebSocket은 build-only leaf다** — 어떤 런타임에도 올라가지 않는다. 그리고 그 사실이 기계로 강제된다: 그 테스트의 javadoc이 세 전송의 등급을 나눈다: 따라서 두 어댑터가 `MasterSwitch`·`env-keys.yaml`·`AdapterActivationReport`에 없는 것은 누락이 아니라 **일관성**이다. 런타임에 오르지 않는 어댑터에는 운영자용 활성화 스위치가 필요하지 않다. **남는 것은 `backend.web.*` 하나다.** `adapter-inbound-web`은 두 런타임에 올라가고(`[\"app-bootstrap\",\"sample-portfolio\"]`), 그 스위치들 — `backend.web.mvc.enabled` · `backend.web.webflux.enabled`(둘 다 `matchIfMissing=true`, **기본 켜짐**) · `backend.web.budgets.enabled` · `app.web-platform.durable-operations.enabled` — 은 `MasterSwitch`에도 `env-keys.yaml` 341개 키에도 없다. §4.1b.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "capability-and-disclosure-models/concept/concept-app-bootstrap-c02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "app-bootstrap-c02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/app-bootstrap-c02.txt" - ] - }, - { - "title": "(8.4) 카운트 — `.imports` 여섯 줄과 다섯 능력", - "kind": "concept", - "slug": "app-bootstrap-c03", - "readiness": "READY", - "source": [ - "`final/document.md#a18#L264`" - ], - "owning-module": "`app-bootstrap`", - "classification": "`.imports`의 여섯 항목 중 다섯이 능력 루트이고 하나(`AdapterActivationAutoConfiguration`)가 자기 액추에이터다. `MasterSwitch`의 다섯과 일치한다 — 단 `PERSISTENCE_MONGO`는 `.imports`에 루트가 없고 컴포넌트 스캔 제외 정규식(`adapter\\.outbound\\.mongo\\..*`)으로만 관리된다. §7.1.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "capability-and-disclosure-models/concept/concept-app-bootstrap-c03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "app-bootstrap-c03" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/app-bootstrap-c03.txt" - ] - }, - { - "title": "두 겹의 게이트", - "kind": "concept", - "slug": "grpc-advanced-diagnostics-c02", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-advanced-diagnostics#L51`" - ], - "owning-module": "`grpc-advanced-diagnostics`", - "classification": "`GrpcChannelDiagnosticsPolicy` 는 네트워크와 역할 두 게이트를 모두 요구하고, 하나라도 비면 생성자가 거부한다. 그리고 등록 판정이 능력 깃발에 걸려 있다. CSDS 는 Channelz 가 켜져 있고 xDS 도 켜져 있을 때만 등록된다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "capability-and-disclosure-models/concept/concept-grpc-advanced-diagnostics-c02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-advanced-diagnostics-c02", - "grpc-advanced-diagnostics-c02-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-advanced-diagnostics-c02.txt" - ] - }, - { - "title": "Git/설계 문서에서 확인한 변화와 실패 기록", - "kind": "concept", - "slug": "messaging-kafka-share-experimental-c07", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-kafka-share-experimental#L399`" - ], - "owning-module": "`messaging-kafka-share-experimental`", - "classification": "이 leaf의 javadoc에 **이전 결함 서술이 없다.** 다른 messaging leaf 대부분이 \"X used to …\" 형태의 기록을 갖는 것과 대비된다. 대신 **막으려는 것**을 셋 적는다. 세 번째가 이 leaf에서 가장 성숙한 판단이다 — **거절이 무시보다 낫다**는 원칙이고, `messaging-core-api`의 `MessagingCapabilityUnavailableException` javadoc과 같은 계열이다. 역설적으로 **그 원칙이 `register(...)`에는 적용되지 않았다** — spec을 받아 무시하고 성공을 반환한다(§17).", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "capability-and-disclosure-models/concept/concept-messaging-kafka-share-experimental-c07.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-kafka-share-experimental-c07" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-kafka-share-experimental-c07.txt" - ] - }, - { - "title": "주요 실행 경로", - "kind": "concept", - "slug": "messaging-testkit-c05", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-testkit#L452`" - ], - "owning-module": "`messaging-testkit`", - "classification": "**경로 A — 어댑터 계약 실행 (컨테이너 불필요, 항상 실행)** `MessagingAdapterHarness extends AutoCloseable` 이고 `close()` 가 checked exception 을 던지지 않도록 재선언되어 있다(`MessagingAdapterHarness.java:71-72`). 7개 테스트 전부 `try (…)` 로 감싸므로 하니스 누수 경로가 없다. **경로 B — 인증 증거 생산 (컨테이너 필요, `test` 에서 제외)** **경로 C — 등급 판정 (컨테이너 불필요, 매 빌드)** 경로 C 가 경로 B 없이도 돌고, 경로 B 가 없으면 매니페스트가 비어 등급 주장이 무너진다는 것이 설계의 핵심이다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "capability-and-disclosure-models/concept/concept-messaging-testkit-c05.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-testkit-c05" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-testkit-c05.txt" - ] - } - ], - "reference": [], - "question": [], - "decision": [] - } - }, - "query-and-pagination-models": { - "topic": "query-and-pagination-models", - "title": "", - "readerQuestion": "", - "kinds": { - "case": [], - "concept": [ - { - "title": "(8.1) 도달성 — 네 패키지의 배선 상태", - "kind": "concept", - "slug": "adapter-inbound-graphql-c10", - "readiness": "READY", - "source": [ - "`final/document.md#a16#L719`" - ], - "owning-module": "`adapter-inbound-graphql`", - "classification": "58개 main 파일 중 자동설정이 참조하는 것은 **둘**이다 — `GraphQlBatchPolicyRegistry`(4) · `GraphQlDataLoaderFactory`(4). 나머지 56개는 autoconf=0이다. `dataloader`는 `runtime/GraphQlBatchLoaderRegistrar`를 통해 도달하므로 배선돼 있다. `fetch`·`pagination`·`mutation` 41개 파일은 어떤 배선 경로에도 없다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "query-and-pagination-models/concept/concept-adapter-inbound-graphql-c10.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-graphql-c10", - "adapter-inbound-graphql-c10-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-graphql-c10.txt" - ] - }, - { - "title": "(8.2) durable-operation HTTP 표면의 두 게이트", - "kind": "concept", - "slug": "adapter-inbound-web-c06", - "readiness": "READY", - "source": [ - "`final/document.md#a14#L762`" - ], - "owning-module": "`adapter-inbound-web`", - "classification": "`app.web-platform.durable-operations` 문자열은 저장소의 어떤 yaml에도 없다. 그리고 켜더라도 생성자가 요구하는 `OperationQueryService` 빈을 선언하는 코드가 main·app-bootstrap에 없다(testkit에만 생성). §16.3과 같은 형태 — 게이트를 켜면 부팅이 실패한다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "query-and-pagination-models/concept/concept-adapter-inbound-web-c06.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-web-c06" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-web-c06.txt" - ] - }, - { - "title": "Spring Data repository support는 generic CRUD보다 query execution policy에 가깝다", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c27", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L1860`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "`JpaRepositoryFragmentSupport`는 domain-specific repository adapter가 사용할 공통 실행 support다. `EntityManager` access query name context fetch plan application bounded query observation scope 이고 범용 business repository contract는 제공하지 않는다. 이 구조는 Clean Architecture 관점에서 의미가 있다. application-core가 `JpaRepository`, `EntityManager`, `Specification`을 알 필요가 없고, 실제 domain repository port를 구현하는 outbound adapter 내부에서만 Spring Data/JPA mechanics를 사용한다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "query-and-pagination-models/concept/concept-adapter-outbound-persistence-jpa-c27.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c27" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c27.txt" - ] - }, - { - "title": "keyset predicate는 mixed type / mixed direction을 표현하도록 진화했다", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c28", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L1933`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "`KeysetPredicateBuilder`는 conjunction이 아니라 lexicographic predicate를 만든다. 예를 들어 `(createdAt ASC, id DESC)`라면 cursor 뒤는 개념적으로: 현재 `KeysetTerm`는 각 term마다: cursor value 그래서 `(Instant, UUID)`처럼 term type이 다르고 direction도 다른 ordering을 표현할 수 있다. source history에는 과거 one-type/one-direction API가 mixed order에서 rows를 skip/repeat했던 이유가 주석으로 남아 있고, 현재 code/test는 이를 보완했다. builder는 “마지막 term이 unique tie-breaker여야 한다”고 문서화하지만 runtime에서 uniqueness를 증명할 metadata는 받지 않는다. 검사할 수 있는 것은: 따라서 uniqueness는 caller/registry contract다. 현재 evidence만으로 이를 defect라 단정하지 않는다. platform이 이를 fail-closed invariant로 승격하려면 unique-key metadata까지 contract에 포함해야 한다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "query-and-pagination-models/concept/concept-adapter-outbound-persistence-jpa-c28.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c28" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c28.txt" - ] - }, - { - "title": "keyset execution은 `size + 1`로 hasNext를 판정하고 count query를 제거한다", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c29", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L1973`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "`JpaKeysetQuerySupport`는: 반환은 최대 `size`개이고 추가 1개로 `hasNext`를 판단한다. 이 path에는 `COUNT(*)`가 없다. 즉 keyset을 도입해 OFFSET full-walk 비용을 줄여 놓고 total count로 다시 full-work를 추가하는 구조를 피한다. 실제 PostgreSQL readiness query도 `(occurred_at,id) > (?,?) ORDER BY ... LIMIT ?` 형태와 representative index 사용을 별도 integration lane에서 검증한다. 해당 entire integration lane 자체는 later sub-scope 11의 denominator이므로 여기서는 cross-scope evidence로만 사용한다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "query-and-pagination-models/concept/concept-adapter-outbound-persistence-jpa-c29.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c29" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c29.txt" - ] - }, - { - "title": "모듈 경계와 빌드 의존성", - "kind": "concept", - "slug": "application-core-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a03#L58`" - ], - "owning-module": "`application-core`", - "classification": "**Observed.** `build.gradle`의 production project dependency는 `:shared-contract` 하나뿐이다. application-core가 Spring, JPA, Redis, Kafka, filesystem provider 같은 구현 모듈을 직접 참조하지 않고, 외부 구현은 composition root와 adapter가 역으로 이 모듈의 port를 구현한다. **Observed.** `CommandUseCase`와 `QueryUseCase`는 `UseCase.handle(I)`를 write/read intent에 맞게 타입으로 좁힌다. 자체적으로 transaction을 열거나 security interceptor를 실행하지 않는다. 실행 정책은 `@UseCaseCapability`에 별도로 선언된다. **Observed.** `@UseCaseCapability`는 runtime TYPE annotation이며 `transactionMode`, `idempotency`, `repositoryAccess`를 필수로 받고 `externalOutboundAllowed`, `sensitiveRead`, `bulkWrite`, `crossTenantAdmin`을 추가 선언한다. annotation 자체는 metadata에 불과하지만 `CleanArchitectureTest`가 concrete Command/Query use case에 annotation 존재를 강제한다. **Observed.** architecture fitness function은 다음 coherence를 직접 검사한다. `READ_ONLY + READ_REPOSITORY`는 `TransactionPort.inRead`를 직접 호출해야 한다. `WRITE + WRITE_REPOSITORY`는 `inWrite` 또는 `inRootWrite`를 직접 호출해야 한다. `REQUIRES_NEW`는 `inNew`를 직접 호출해야 한다. `repositoryAccess != WRITE_REPOSITORY`인 use case가 repository write ve…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "query-and-pagination-models/concept/concept-application-core-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "application-core-c01", - "application-core-c01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/application-core-c01.txt" - ] - } - ], - "reference": [], - "question": [], - "decision": [] - } - }, - "observability-models": { - "topic": "observability-models", - "title": "", - "readerQuestion": "", - "kinds": { - "case": [], - "concept": [ - { - "title": "(8.3) 중복 메커니즘 — 상관 식별자가 세 벌이다", - "kind": "concept", - "slug": "adapter-inbound-web-c15", - "readiness": "READY", - "source": [ - "`final/document.md#a14#L1108`" - ], - "owning-module": "`adapter-inbound-web`", - "classification": "세 번째는 전송이 달라 공존이 정상이다. 앞의 둘은 같은 서블릿 체인에서 같은 헤더를 두 번 처리한다. `traceparent`도 마찬가지로 두 번 파싱되며, `RequestLoggingFilter`는 응답에도 `traceparent`를 쓰고(`:68`) `WebMvcRequestIdFilter`는 쓰지 않는다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "observability-models/concept/concept-adapter-inbound-web-c15.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-web-c15" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-web-c15.txt" - ] - }, - { - "title": "`audit`와 `auditing` 두 경로", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c44", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L3123`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "manual `AuditableEntity`/`AuditContextPort` 경로와 Spring Data `AuditMetadata`/`JpaAuditingConfiguration`이 함께 존재하지만 tests/docs가 후자를 candidate/dormant로 명시하고 default composition도 canonical manual audit 경로만 사용한다. 현재 중복 활성화 defect로 판정하지 않는다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "observability-models/concept/concept-adapter-outbound-persistence-jpa-c44.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c44" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c44.txt" - ] - }, - { - "title": "`FailOpenDependencyLogger`: 진단을 business outcome과 분리하려는 계약", - "kind": "concept", - "slug": "adapter-outbound-support-c02", - "readiness": "READY", - "source": [ - "`final/document.md#a04#L119`" - ], - "owning-module": "`adapter-outbound-support`", - "classification": "`logSuccess(...)`는 DEBUG로 다음 정보를 기록한다. dependency_name dependency_type outcome=`SUCCESS` correlation_id `logFailure(...)`는 WARN으로 다음을 추가한다. outcome=`FAILURE` error=`: ` README와 javadoc은 WARN을 선택한 이유를 “optional fail-open dependency가 실패해도 core use case 자체는 성공했기 때문”이라고 설명한다. 이 logger 자체는 retry, recovery, fallback을 수행하지 않는다. **실패 정책을 결정하는 주체가 아니라 이미 결정된 fail-open outcome을 관측하는 기술 seam**이다. repository-wide production reference scan에서 support package를 직접 import하는 current production files는 네 개뿐이었다. `MessagingConfig` `OutboundMessagePublisher` Notification: `NotificationConfig` `FailOpenNotificationProvider` 반대로 support README가 “공유 consumer”로 설명하는 `cache-redis`, `httpclient`는 Gradle dependency는 유지하지만 support production type을 직접 참조하지 않는다. 이 차이는 §8에서 별도로 다룬다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "observability-models/concept/concept-adapter-outbound-support-c02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-support-c02", - "adapter-outbound-support-c02-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-support-c02.txt" - ] - }, - { - "title": "architecture suite / dependency registry", - "kind": "concept", - "slug": "adapter-outbound-support-c06", - "readiness": "READY", - "source": [ - "`final/document.md#a04#L558`" - ], - "owning-module": "`adapter-outbound-support`", - "classification": "`CleanArchitectureTest --rerun-tasks`: BUILD SUCCESSFUL `verifyCleanArchitectureDependencies`: BUILD SUCCESSFUL 이 둘은 source/package/project dependency constraint를 증명하며 diagnostics runtime failure나 PII behavior를 증명하지 않는다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "observability-models/concept/concept-adapter-outbound-support-c06.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-support-c06" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-support-c06.txt" - ] - }, - { - "title": "계약·불변식", - "kind": "concept", - "slug": "grpc-observability-c03", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-observability#L74`" - ], - "owning-module": "`grpc-observability`", - "classification": "`violations(Map)` 의 판정 순서가 셋이다. 클래스 javadoc 이 두 목록이 겹치는 이유를 적는다 — \"Everything unlisted is refused anyway; naming the dangerous ones gives the refusal a message that says why rather than just that.\" 즉 `FORBIDDEN_TAGS` 는 판정을 바꾸지 않고 진단만 바꾼다. 두 번째 분기가 이미 그것들을 거절한다. 허용 태그 8개: `grpc.service` · `grpc.method` · `grpc.rpc_type` · `grpc.status` · `grpc.channel_profile` · `grpc.completion_outcome` · `grpc.retry_bucket` · `grpc.stream_termination_reason`. 명시적 거절 11개: `actor_id` · `tenant_id` · `object_id` · `stream_id` · `idempotency_key` · `request` · `response` · `metadata` · `authorization` · `error_detail` · `trace_id`. UUID · `sha256:` 접두 · `bearer ` 접두. 숫자 id, 이메일, 호스트명은 잡히지 않는다. 그리고 `.` 은 기본적으로 개행에 맞지 않으므로 값에 개행이 섞이면 `matches()` 가 거짓이 된다. `retryBucket(int)` 이 1-based 시도 수를 받아 `0`/`1`/`2`/`3+` 로 접는다. 0 이하는 던진다. javadoc 이 이유를 적는다 — \"an attempt count is unbounded in principle and the distinction anyone acts on is first attempt, one retry, several.\" `GrpcRpcObservation` javadoc: 그 분리가 `GrpcObservationConvention.record(GrpcRpcObservation)` 에서 실제로 그렇게 구현되어 있다 — `RPC_DURATION` 타이머는 1회, `RPC_ATTE…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "observability-models/concept/concept-grpc-observability-c03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-observability-c03" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-observability-c03.txt" - ] - }, - { - "title": "실패 경로와 복구/번역", - "kind": "concept", - "slug": "messaging-admin-runtime-c05", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-admin-runtime#L472`" - ], - "owning-module": "`messaging-admin-runtime`", - "classification": "마지막 두 줄이 §12.3(a)의 요약이다 — 같은 코드 문자열, 다른 예외 타입, 다른 판정 규칙. `attempt(...)` 가 모든 `RuntimeException` 을 삼키는 것은 근거가 있지만 대가도 있다: 실패 사유가 어디에도 남지 않는다. 감사 이벤트는 `failed` 개수만 담고(`:135`), 어떤 메시지가 왜 실패했는지는 기록되지 않는다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "observability-models/concept/concept-messaging-admin-runtime-c05.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-admin-runtime-c05" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-admin-runtime-c05.txt" - ] - }, - { - "title": "의존성과 런타임 배선", - "kind": "concept", - "slug": "messaging-observability-c02", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-observability#L70`" - ], - "owning-module": "`messaging-observability`", - "classification": "들어오는 것: `messaging-core-api`(api), `micrometer-core`(api). 나가는 것: `messaging-runtime-core`, `messaging-kafka`, `messaging-rabbit`, `messaging-admin-runtime`, `messaging-pulsar-experimental`, `messaging-nats-experimental`, `messaging-spring-boot-starter`. **출하 조립은 두 개뿐이다.** 두 클래스는 `MessagingMetrics`의 생성자 인자다. 그런데 `MessagingMetrics` bean이 없다(§12.1). 즉 **재료 둘만 bean으로 있고 그것을 조립하는 것이 없다.** `MessagingTracer`·`MessagingAuditSink`·`DefaultMessagingObservationConvention`은 bean도 없고 소비자도 없다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "observability-models/concept/concept-messaging-observability-c02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-observability-c02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-observability-c02.txt" - ] - } - ], - "reference": [], - "question": [], - "decision": [] - } - }, - "composition-and-lifecycle-models": { - "topic": "composition-and-lifecycle-models", - "title": "", - "readerQuestion": "", - "kinds": { - "case": [ - { - "title": "배정 식별자 때문에 insert-first 청구가 UPSERT 가 되어 커밋된 결과를 덮었다", - "kind": "case", - "slug": "assigned-id-turns-claim-into-upsert", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-operation-ledger-jpa#L133`" - ], - "code": [ - "`.../grpc-operation-ledger-jpa/.../ledger/JpaGrpcOperationLedger.java`", - "`.../GrpcOperationLedgerEntity.java`", - "`.../GrpcOperationIdentity.java:36`" - ], - "evidence": [ - "없음 — 코드 통독과 Spring Data `save` 계약 대조로 판정했다. build-only 리프라 실행 경로가 없다" - ], - "classification": "`claim` 은 insert-first, read-on-conflict 를 주장한다. 그러나 엔티티의 `@Id` 가 배정값(`caller|method|keyHash`)이라 `SimpleJpaRepository.save` 가 `persist` 가 아니라 `merge` 로 간다. 그 파생 키가 유니크 제약의 세 컬럼과 같은 행을 가리키므로 두 번째 청구는 위반을 일으키지 않고 기존 행을 갱신한다 — 상태가 `IN_PROGRESS` 로, `outcome_reference` 가 널로 되돌아가고 `claim` 은 빈 값을 돌려줘 호출자가 소유를 얻었다고 읽는다. 인메모리 테스트 이중의 `save` 는 키가 있으면 던지므로 INSERT 를 흉내 내고 그 차이를 가린다.", - "missing-verification": "실제 데이터베이스로 두 번 청구해 재현하지 않았다.", - "relations": [ - "`reference:atomic-type-is-not-atomicity`", - "`reference:check-which-duplicate-is-wired`", - "`case:two-owners-popped-the-evidence-frame`" - ], - "listedInTree": false, - "publication": "초안", - "file": "state-ownership-and-concurrency/case/case-assigned-id-turns-claim-into-upsert.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "assigned-id-turns-claim-into-upsert" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/assigned-id-turns-claim-into-upsert.txt" - ] - }, - { - "title": "배수 완료가 자기가 읽은 값으로 상태를 다시 써서 진행 중인 자격증명 회전을 되돌린다", - "kind": "case", - "slug": "complete-drain-rolls-back-a-rotation", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-policy#L158`" - ], - "code": [ - "`.../grpc-policy/.../security/GrpcCredentialRotationManager.java`", - "`.../grpc-client/.../GrpcChannelRuntimeRegistry.java`" - ], - "evidence": [ - "없음 — 원자성 분석으로 판정했다. 두 리프 모두 배선 경로가 없다" - ], - "classification": "`AtomicReference` 를 들고 있으면서 `rotate` 와 `completeDrain` 이 모두 `get()` 후 조건 없는 `set()` 을 한다. 회전 경합에서는 덮인 세대가 배수 목록에 오르지 못하고, 배수 완료에서는 자기가 읽은 `observed.current()` 로 새 상태를 만들기 때문에 그 사이에 일어난 회전이 지워지고 이전 세대가 다시 현재가 된다. 자격 자재를 떨어뜨리지 않고 교체하려고 만든 클래스가 교체 자체를 되돌린다. 같은 파일의 형제(`install`)와 같은 가족의 `GrpcRetryBudget` 이 비교 후 교체를 정확히 쓴다.", - "missing-verification": "동시 회전과 동시 배수 완료를 실행으로 재현하지 않았다.", - "relations": [ - "`reference:atomic-type-is-not-atomicity`", - "`case:assigned-id-turns-claim-into-upsert`", - "`decision:retry-safety-is-decided-by-evidence`" - ], - "listedInTree": false, - "publication": "초안", - "file": "state-ownership-and-concurrency/case/case-complete-drain-rolls-back-a-rotation.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "complete-drain-rolls-back-a-rotation" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/complete-drain-rolls-back-a-rotation.txt" - ] - }, - { - "title": "시작 검증기가 유일한 소비자인 설정 키 넷이 아무것도 게이트하지 않는다", - "kind": "case", - "slug": "startup-validator-is-the-only-reader-of-four-keys", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-spring-boot-starter#L139`" - ], - "code": [ - "`.../grpc-spring-boot-starter/.../boot/GrpcPlatformStartupValidator.java`", - "`.../boot/GrpcPlatformAutoConfiguration.java`", - "`.../boot/GrpcPlatformProperties.java`" - ], - "evidence": [ - "없음 — 호출자 전수 검색으로 판정했다" - ], - "classification": "검증기를 이름으로 부르는 파일은 자기 자신과 자기 테스트뿐이다. 자동 설정은 빈 아홉 개를 만들고 `requireValid` 를 부르지 않으며 초기화 콜백도 없다. 그 검증기가 유일한 소비자인 설정 키가 넷이다 — `transport`·`tls-enabled`·`trust-all-certificates`·`operation-ledger-enabled`. 따라서 운영 환경의 TLS 바닥도, 비운영 전송 거부도, 멱등 키 필수 메서드의 원장 요구도 강제되지 않는다. 같은 저장소가 정본을 둘 갖고 있다 — messaging 의 `StartupProfileValidation` 과 fileserver 의 증명 호출.", - "missing-verification": "스타터를 올려 컨텍스트를 세우지 않았다. build-only 리프다.", - "relations": [ - "`case:a14-f018-webplatformstartupvalidator`", - "`reference:a-bean-is-not-composition-evidence`", - "`case:validator-declared-and-never-injected`" - ], - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/case/case-startup-validator-is-the-only-reader-of-four-keys.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "startup-validator-is-the-only-reader-of-four-keys" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/startup-validator-is-the-only-reader-of-four-keys.txt" - ] - }, - { - "title": "같은 자동 설정 안에서 검증기 하나만 감싸이지 않아 트랜잭션 조건이 검사되지 않는다", - "kind": "case", - "slug": "validator-declared-and-never-injected", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-spring-boot-starter#L148`" - ], - "code": [ - "`.../messaging-spring-boot-starter/.../autoconfigure/KafkaMessagingAutoConfiguration.java:41,54,74`", - "`.../autoconfigure/StartupProfileValidation.java`", - "`.../messaging-kafka/.../KafkaTransactionProfileValidator.java`" - ], - "evidence": [ - "없음 — 자동 설정의 빈 선언 대조로 판정했다" - ], - "classification": "`KafkaProfileValidator` 는 `StartupProfileValidation` 으로 감싸여 `afterPropertiesSet` 에서 돌고, 같은 파일의 `KafkaTransactionProfileValidator` 는 빈으로 발행만 된다. Rabbit 쪽은 하나뿐인 검증기를 감싼다. 그래서 트랜잭션 식별자 접두·멱등 생산자·`acks=all`·수동 커밋 요구가 시작 시 검사되지 않고, 어댑터는 `brokerTransaction=true` 를 무조건 답한다. 검증기의 절반은 `messaging-kafka` 가, 배선의 절반은 스타터가 소유하므로 어느 문서도 혼자서는 이 사실을 말할 수 없다.", - "missing-verification": "조건을 어긴 프로파일로 컨텍스트를 세워 재현하지 않았다.", - "relations": [ - "`case:startup-validator-is-the-only-reader-of-four-keys`", - "`reference:a-bean-is-not-composition-evidence`", - "`case:capability-constant-outlives-its-condition`" - ], - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/case/case-validator-declared-and-never-injected.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "validator-declared-and-never-injected" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/validator-declared-and-never-injected.txt" - ] - }, - { - "title": "능력 상수가 프로파일 조건보다 오래 살아서 중복 제거 없는 목적지가 가드를 통과한다", - "kind": "case", - "slug": "capability-constant-outlives-its-condition", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-nats-experimental#L111`" - ], - "code": [ - "`.../messaging-nats-experimental/.../NatsJetStreamTransport.java`", - "`.../NatsJetStreamProfileValidator.java`", - "`.../messaging-core-api/.../destination/MessagingCapabilities.java`" - ], - "evidence": [ - "없음 — 능력 상수와 식별자 생성 경로 대조로 판정했다" - ], - "classification": "어댑터가 `deduplicatedPublish=true` 를 상수로 답한다. 그런데 중복 제거 식별자는 프로파일에 창이 있을 때만 만들어지고, 창은 선택 사항이며 생성자도 검증기도 요구하지 않는다. 창이 없으면 `Nats-Msg-Id` 가 실리지 않아 서버가 중복을 제거하지 않는다. 이 플래그는 능력 열둘 중 부재가 예외를 만드는 유일한 것이라, 창 없는 목적지가 그 가드를 통과한 뒤 모호 재발행에서 스트림에 같은 메시지를 두 번 넣는다. 어댑터 자신의 javadoc 이 그 조건을 알고 있다 — 재시도를 안전하게 만드는 것은 창이며 '프로파일이 그것을 켰을 때' 라고 적는다.", - "missing-verification": "창 없는 프로파일로 모호 재발행을 재현하지 않았다.", - "relations": [ - "`case:validator-declared-and-never-injected`", - "`reference:a-bean-is-not-composition-evidence`", - "`case:support-matrix-said-the-opposite-of-the-code`" - ], - "listedInTree": false, - "publication": "초안", - "file": "transport-and-provider-semantics/case/case-capability-constant-outlives-its-condition.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "capability-constant-outlives-its-condition" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/capability-constant-outlives-its-condition.txt" - ] - }, - { - "title": "마스킹이 IPv4 만 알아서 검사가 나머지 주소 형태를 전부 통과시킨다", - "kind": "case", - "slug": "ipv4-only-mask-passes-every-other-form", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-advanced-diagnostics#L123`" - ], - "code": [ - "`.../grpc-advanced-diagnostics/.../diagnostics/GrpcDiagnosticsRedactor.java`", - "`.../diagnostics/GrpcChannelDiagnosticsSnapshot.java`" - ], - "evidence": [ - "없음 — 정규식과 생성자 검사 대조로 판정했다" - ], - "classification": "`maskAddress` 는 IPv4 패턴 하나만 갖고 맞지 않는 입력을 그대로 돌려준다. 그리고 스냅숏 생성자는 마스킹 결과가 입력과 같으면 이미 마스킹된 것으로 판정한다. 두 규칙이 겹치면 IPv6 주소·파드 DNS 이름·유닉스 소켓 경로가 전부 검사를 통과한다. 이 플랫폼이 겨냥하는 배포가 쿠버네티스이고 헤드리스 레코드의 엔드포인트가 DNS 이름이므로 도달 가능한 형태다. 테스트의 주소 리터럴은 전부 IPv4 다. 같은 사이클이 철회한 P1 의 원인도 듀얼스택 호스트명이었다 — 판정이 모르는 형태를 통과 쪽으로 접는 같은 계열이다.", - "missing-verification": "IPv6 주소로 스냅숏을 만들어 재현하지 않았다.", - "relations": [ - "`reference:make-the-unsafe-state-unrepresentable`", - "`case:a-red-test-misread-as-a-product-defect`", - "`reference:agreement-between-documents-proves-nothing`" - ], - "listedInTree": false, - "publication": "초안", - "file": "security-policy-enforcement/case/case-ipv4-only-mask-passes-every-other-form.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "ipv4-only-mask-passes-every-other-form" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/ipv4-only-mask-passes-every-other-form.txt" - ] - } - ], - "concept": [ - { - "title": "(8.2) 조건 형제 비교 — 연산 정체성을 정하는 두 구현", - "kind": "concept", - "slug": "adapter-inbound-graphql-c03", - "readiness": "READY", - "source": [ - "`final/document.md#a16#L336`" - ], - "owning-module": "`adapter-inbound-graphql`", - "classification": "`GraphQlOperationNameInterceptor.apply(...)`(미배선)와 `runtime/GraphQlOperationSelectionHandler`(autoconf=2, 배선됨)가 같은 일을 한다. **배선된 쪽이 더 많이 한다**: 익명 연산 거부 · 다중 연산 시 `operationName` 요구 · 연산 정체성 정규화가 전부 배선된 경로에 있다. 정규화 규칙에는 근거도 붙어 있다 — \"any name that cannot survive normalisation becomes the anonymous identity rather than being rejected — **a naming convention is not a reason to refuse an otherwise valid request**.\" 따라서 미배선 인터셉터는 **누락이 아니라 중복**이다. 다만 그것이 쓰는 `GraphQlOperationNamePolicy`(85줄, 참조자 = 인터셉터와 자기 자신뿐)도 함께 미배선이고, 배선된 핸들러는 다른 정책 객체를 쓴다. §12.2.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "composition-and-lifecycle-models/concept/concept-adapter-inbound-graphql-c03.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-graphql-c03" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-graphql-c03.txt" - ] - }, - { - "title": "(8.2) 조건 형제 비교 — 클라이언트 정책이 어떻게 정해지는가", - "kind": "concept", - "slug": "adapter-inbound-graphql-c06", - "readiness": "READY", - "source": [ - "`final/document.md#a16#L471`" - ], - "owning-module": "`adapter-inbound-graphql`", - "classification": "설계는 매니페스트 조회를 말한다: 자동설정은 단일 빈을 만든다: 그리고 그 하나가 여덟 개 빈(`:123` · `:237` · `:380` · `:389` · `:397` · `:453` · `:463` …)에 주입된다. 매니페스트는 만들어지지 않는다. §16.2. **프로파일 자체는 신뢰된 경로에서 온다** — `GraphQlAuthenticationContextFactory:59`가 `principal.clientProfile()`을 쓰고(검증된 principal), 미인증 호출자에는 `GraphQlPlatformWebInterceptor`의 `anonymousProfile`이 붙는다. 즉 `GraphQlClientProfileResolver`가 막으려는 노출(호출자가 자기 프로파일을 지정)은 배선된 경로에서도 발생하지 않는다. 그 타입은 중복이다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "composition-and-lifecycle-models/concept/concept-adapter-inbound-graphql-c06.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-graphql-c06" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-graphql-c06.txt" - ] - }, - { - "title": "b 그 결과 — HTTP 전송 계약 계층이 미배선이고 실제 전송은 프레임워크가 정한다", - "kind": "concept", - "slug": "adapter-inbound-graphql-c09", - "readiness": "READY", - "source": [ - "`final/document.md#a16#L660`" - ], - "owning-module": "`adapter-inbound-graphql`", - "classification": "`http` 패키지 19개 파일 중 자동설정이 값으로 소비하는 둘(`GraphQlHttpProfile` autoconf=2, `GraphQlJsonStructurePolicy` autoconf=4)을 빼면, 나머지는 실행되지 않는다: GraphQL-over-HTTP에서 상태 코드 규칙은 미디어 타입에 달려 있다 — `application/json`은 실행 오류에도 200을, `application/graphql-response+json`은 실제 상태를 쓴다. 그 규칙을 `GraphQlHttpStatusMapper`와 `GraphQlAcceptHeader`가 담고 있고, 실제로 응답을 만드는 것은 Spring GraphQL이다. **노출이 아니라 통제권의 문제다.** Spring GraphQL 자신이 GraphQL-over-HTTP 스펙을 구현하므로 동작은 합리적이다. 잃는 것은 (a) 이 플랫폼이 선언한 프로파일(`V1`)이 실제 동작과 일치한다는 보장, (b) 사전 파싱 한계 중 봉투 검증기에만 있는 부분, (c) \"새 결과 종류가 임의 상태를 갖고 한 호출 지점에 생기는 것\"을 막겠다는 단일 팩토리의 목적. **실패 시나리오** — 운영자가 `GraphQlPlatformConfigurationReport`(§8.1을 고쳐 발행하게 된 뒤)에서 `httpProfile=V1`을 읽고 그 프로파일 문서대로 클라이언트를 작성한다. 실제 응답 상태와 미디어 타입은 Spring GraphQL이 정하며, 두 문서가 다른 지점에서 클라이언트가 깨진다. **권고** — 둘 중 하나다. (a) 프레임워크 전송을 정본으로 인정하고 `http` 패키지에서 전송 기계를 제거한 뒤 `GraphQlHttpProfile`을 프레임워크 동작의 서술로 좁힌다. (b) `WebGraphQlInterceptor`(`GraphQlPlatformWebInterceptor`가 이미 그 자리에 있다)에서 봉투 검증과 응답 정책을 적용해 프로파일을 실제로 강제한다. 지금은 선언과 실행이 분리돼 있다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "composition-and-lifecycle-models/concept/concept-adapter-inbound-graphql-c09.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-graphql-c09", - "adapter-inbound-graphql-c09-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-graphql-c09.txt" - ] - }, - { - "title": "(8.4) 문서/구현 드리프트 — 모듈 경계 선언과 실제 트리", - "kind": "concept", - "slug": "adapter-inbound-web-c02", - "readiness": "READY", - "source": [ - "`final/document.md#a14#L140`" - ], - "owning-module": "`adapter-inbound-web`", - "classification": "`WebModuleBoundaryTest`가 다섯 개의 긍정 규칙과 **네 개의 부정 픽스처**를 갖는다: 부정 픽스처의 존재 이유가 명시돼 있다: \"A boundary test that has never been shown to fail is indistinguishable from one that scans the wrong directory.\" 그리고 프로덕션 스캔에 `fileCount() > 100` 하한과 `packages()`에 특정 패키지 두 개가 있어야 한다는 확인이 함께 붙는다. 프레임워크 탐지 정규식에는 Jackson 2와 3이 **둘 다** 들어 있고 그 근거가 적혀 있다: \"this repository runs on Spring 7, whose message converters take Jackson 3 — so a CORE module could have imported a mapper without this detector noticing, which is **a hole in exactly the check that is supposed to have none**.\" 이것은 이 저장소에서 확인한 경계 강제 중 가장 강하다. notification의 `EndpointGuardCallSiteTest`(호출처 목록이 가드 javadoc과 달랐던)와 달리, 여기서는 목록 자체가 스캔으로 생성된다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "composition-and-lifecycle-models/concept/concept-adapter-inbound-web-c02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-web-c02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-web-c02.txt" - ] - }, - { - "title": "(8.3) XML/CBOR 표현의 런타임 배선", - "kind": "concept", - "slug": "adapter-inbound-web-c12", - "readiness": "READY", - "source": [ - "`final/document.md#a14#L986`" - ], - "owning-module": "`adapter-inbound-web`", - "classification": "`build.gradle`이 두 백엔드를 `compileOnly`로 두고 그 이유를 길게 적는다(§2) — `implementation`이었을 때 \"silently began parsing `application/xml` request bodies... an XXE surface nobody chose\"였기 때문이다. 의도된 설계다. 그런데 그 잭슨 백엔드를 배포가 추가하더라도 메시지 컨버터를 등록하는 코드가 없다. `WebXmlMapperFactory`·`WebCborMapperFactory`·`RepresentationNegotiationPolicy`를 참조하는 파일은 자기 패키지와 테스트뿐이고, 두 자동설정(MVC 12빈 · WebFlux 11빈)에도 없다. `WebRepresentation.available()`이 \"absent backend를 문장으로 바꾼다\"는 장치는 그 문장을 낼 호출자가 없다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "composition-and-lifecycle-models/concept/concept-adapter-inbound-web-c12.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-web-c12" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-web-c12.txt" - ] - }, - { - "title": "(8.2) 조건 형제 비교 — `X-Request-Id`에 대해 배선된 두 필터가 반대 정책을 쓴다", - "kind": "concept", - "slug": "adapter-inbound-web-c13", - "readiness": "READY", - "source": [ - "`final/document.md#a14#L1071`" - ], - "owning-module": "`adapter-inbound-web`", - "classification": "두 필터 모두 서블릿 배포에서 등록되고 둘 다 `X-Request-Id` 응답 헤더를 쓴다. 순서상 `WebMvcRequestIdFilter`(`HIGHEST_PRECEDENCE + 10`)가 먼저 돌아 새 UUID를 헤더에 쓰고, `RequestLoggingFilter`(`LOWEST_PRECEDENCE`)가 나중에 돌아 **클라이언트가 보낸 값으로 덮어쓴다**. MDC의 `request_id`와 접근 로그도 클라이언트 값이다. §32.1.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "composition-and-lifecycle-models/concept/concept-adapter-inbound-web-c13.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-web-c13" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-web-c13.txt" - ] - }, - { - "title": "이 모듈의 형태 — 하나의 leaf, 세 개의 설정 네임스페이스", - "kind": "concept", - "slug": "adapter-inbound-websocket-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a17#L24`" - ], - "owning-module": "`adapter-inbound-websocket`", - "classification": "여섯 소스셋(inbound-web과 같은 형태)이고 `META-INF` 자동설정 리소스가 **없다**. 조립은 전적으로 컴포넌트 스캔에 달려 있으며, 컴포지션 루트는 이 leaf를 스캔에서 제외하지 **않는다**(graphql과 반대). 그런데 스캔이 잡을 수 있는 Spring 애노테이션을 가진 파일이 **169개 중 7개**다: **세 개의 설정 접두사가 있고 그중 하나에는 소비자가 없다:** 세 번째가 이 모듈의 핵심 사실이다. `backend.websocket` 네임스페이스가 규정하는 \"플랫폼\"이 main 169 파일 중 약 90개를 차지하고, 그것을 조립하는 `@Configuration`이 하나도 없다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "composition-and-lifecycle-models/concept/concept-adapter-inbound-websocket-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-inbound-websocket-c01", - "adapter-inbound-websocket-c01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-inbound-websocket-c01.txt" - ] - }, - { - "title": "조립의 순서가 클래스 하나에 고정돼 있다", - "kind": "concept", - "slug": "adapter-outbound-cache-redis-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a10#L85`" - ], - "owning-module": "`adapter-outbound-cache-redis`", - "classification": "`RedisSdkAutoConfiguration`의 javadoc이 규칙을 적는다 — \"`app.redis.enabled` is the whole switch. While it is false this class contributes nothing, and because `RedisSdkSettings` is registered here rather than by the application-wide `@ConfigurationPropertiesScan`, **'contributes nothing' is literal**: the properties are not bound, the cross-field rules are not run, no credential is resolved, and no policy resource, TLS material, client, connection or thread is created.\" 그 문장이 구조로 뒷받침된다. `RedisSdkSettings`는 `@ConfigurationPropertiesScan` 대상이 아니라 이 클래스의 `@Bean` + `@ConfigurationProperties`로만 존재한다. 그래서 Redis를 쓰지 않는 배포는 Redis 설정을 들고 다니지 않고, **켠 적 없는 잘못된 Redis 설정 때문에 벌을 받지도 않는다**. test가 그 넷을 이름으로 고정한다 — `absentSwitchRegistersNothing`, `disabledRegistersNothing`, `disabledIgnoresMalformedRedisConfiguration`, `disabledNeverAsksForASecretOrAConnection`. 순서도 bind → validate → build로 고정된다. 검증이 `@PostConstruct`나 리스너가 아니라 **bean factory 메서드 안**에 있어서, 설정 오류가 \"그 bean을 만들지 못했다\"는 실패로 보고되고 그 아래 어떤 것도 검증되지 않은 settings를 잡을 수 없다. 그리고 `redisSdkSettingsValidation`이 별도 bean인 이유도 적혀 있다 — Spring은 factory 메서드가 **반환한…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "composition-and-lifecycle-models/concept/concept-adapter-outbound-cache-redis-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-cache-redis-c01", - "adapter-outbound-cache-redis-c01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-cache-redis-c01.txt" - ] - }, - { - "title": "Confirmed — raw allowlist 기본값은 없는 리소스를 가리키고, 그것이 의도다", - "kind": "concept", - "slug": "adapter-outbound-cache-redis-c02", - "readiness": "READY", - "source": [ - "`final/document.md#a10#L107`" - ], - "owning-module": "`adapter-outbound-cache-redis`", - "classification": "`RedisSdkSettings.Raw.policyResource` 기본값은 `classpath:redis-sdk/raw-command-allowlist.yml`인데, 저장소에 그 파일은 **없다**(`159-...` §8.3b, `git ls-files` 매치 0. 이 leaf의 main resource는 `AutoConfiguration.imports`와 `redis-sdk/redis-command-policy.yml` 둘뿐). 이것은 결함이 아니라 이미 잡혀 있는 함정이다. `requireRawPolicyResource`가 그 사실과 과거 증상을 함께 적는다 — \"`validate()` only checks that the setting is non-blank, and the default points at … a resource this module does not ship. So enabling the raw gateway passed configuration validation and then **failed at the first raw command, from inside a request, against a live connection.** The allowlist is the entire authorisation model for that gateway; not being able to read it is a startup failure.\" test `enabledRejectsAMissingRawAllowlistResource`와 `enabledAcceptsAReadableRawAllowlistResource`가 양쪽을 고정한다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "composition-and-lifecycle-models/concept/concept-adapter-outbound-cache-redis-c02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-cache-redis-c02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-cache-redis-c02.txt" - ] - }, - { - "title": "Confirmed — 적재 경로는 auto-configuration이 아니라 명시적 component scan이다", - "kind": "concept", - "slug": "adapter-outbound-fileserver-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a08#L127`" - ], - "owning-module": "`adapter-outbound-fileserver`", - "classification": "이 leaf에는 `META-INF/spring/…AutoConfiguration.imports`가 **없다**(`142-...` §8.1). `FileExportConfig`/`FileserverR2Config`를 leaf 밖에서 이름으로 참조하는 production 코드도 없고, 유일한 외부 참조는 app-bootstrap의 test(`OptionalAdapterBeanGatingTest`)다. 실제 적재는 `CaSkeletonApplication`의 명시적 `@ComponentScan`이 `dev.caskeleton.adapter.outbound.fileserver`를 목록에 올려서 이루어진다(`:75`). 즉 CLAUDE.md의 \"never activates unexpectedly when merely present on the classpath\"는 **classpath 존재만으로 bean이 생기지 않는다**는 뜻으로는 정확하지만, 기전은 import filter가 아니라 \"@Configuration은 스캔되고 bean 생성만 `@ConditionalOnProperty`로 막힌다\"이다. fail-closed는 성립한다 — 기록해 두는 이유는 mongo leaf의 4중 opt-in(§sub-scope 01, 06번 문서)과 기전이 다르기 때문이다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "composition-and-lifecycle-models/concept/concept-adapter-outbound-fileserver-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-fileserver-c01" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-fileserver-c01.txt" - ] - }, - { - "title": "\"이름 없는 상태\"를 없애는 것이 이 sub-scope의 주제다", - "kind": "concept", - "slug": "adapter-outbound-notification-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a13#L84`" - ], - "owning-module": "`adapter-outbound-notification`", - "classification": "세 클래스가 각각 이전에는 **구분되지 않던 두 상황**을 구분한다. **`NotificationPlatformMode`** — 공급자가 하나도 조립되지 않은 플랫폼이 공급자가 있는 플랫폼과 똑같이 보였다. `INGEST_ONLY`는 **명시적으로 선택해야** 하고(\"A deployment that reaches zero providers by accident is a misconfiguration, and the whole point of this enum is that the two are told apart\"), `NotificationProviderAssembly:183`이 그것을 강제한다 — 경로가 비었는데 모드가 `INGEST_ONLY`가 아니면 조립을 거부하고 메시지로 그 모드를 안내한다. app-bootstrap 쪽에서도 `NotificationPlatformWorkerConfig`가 \"everything that starts a thread, and therefore everything `INGEST_ONLY` must not have\"를 그 모드로 가른다. **`ProviderType`** — 설정이 타입을 자유 문자열로 날랐고 \"the only thing that read it was a\" 비교였다. 지금은 닫힌 enum이라 \"the unknown type a binding failure at startup\"이고 채널도 타입에서 유도된다. **`NotificationSecretRequirements`** — 이 sub-scope에서 가장 미묘한 판단이다. 이전에는 여덟 개 키를 **항상** 요구했다. 지금은 네 개(`CONTACT_ENCRYPTION`·`CONTACT_LOOKUP_HMAC`·`PAYLOAD_ENCRYPTION`·`PROVIDER_REQUEST_LOOKUP_HMAC`)가 모든 모드에 필요하고 — **수용 경로**에 있으므로 `INGEST_ONLY`에서도 필요하다 — 나머지 넷은 능력을 따라간다. 약해지면 안 되는 방향은 명시된다: \"a capability that is switched *on* and whose key is missing still refuses the boot, because the …", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "composition-and-lifecycle-models/concept/concept-adapter-outbound-notification-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-notification-c01", - "adapter-outbound-notification-c01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-notification-c01.txt" - ] - }, - { - "title": "(8.3) 중복 메커니즘 — 종료 경로", - "kind": "concept", - "slug": "adapter-outbound-notification-c02", - "readiness": "READY", - "source": [ - "`final/document.md#a13#L306`" - ], - "owning-module": "`adapter-outbound-notification`", - "classification": "`NotificationSchedulerWorker.close`와 `NotificationBackgroundWorkers.close` 둘 다 \"취소 → shutdown → awaitTermination(grace) → shutdownNow\"를 수행한다. 형태는 같지만 대상이 다르다(폴링 스레드 + virtual-thread executor vs. 단일 데몬 scheduler). 중복 아님. 한 가지 기록: 스케줄러의 `close()`는 폴링 스레드를 `interrupt()`하지만(`:173`), `runOnce`의 `globalConcurrency.acquireUninterruptibly()`(`:90`)는 인터럽트에 반응하지 않는다. 주석(`:169`)은 \"인터럽트가 poll-interval sleep을 깬다\"고만 말하고 그 점은 정확하다. 세마포어는 in-flight 작업이 `finally`에서 반납하므로 결국 풀리고, 최악의 경우 `join(shutdownGrace)`가 만료된 뒤 종료가 계속된다. 결함 아님.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "composition-and-lifecycle-models/concept/concept-adapter-outbound-notification-c02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-notification-c02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-notification-c02.txt" - ] - }, - { - "title": "always-install scan과 opt-in scan의 경계는 실제로 지켜지고 있다", - "kind": "concept", - "slug": "adapter-outbound-persistence-jpa-c56", - "readiness": "READY", - "source": [ - "`final/document.md#a05#L4143`" - ], - "owning-module": "`adapter-outbound-persistence-jpa`", - "classification": "`PersistenceJpaConfig`는 persistence root를 통째로 스캔하지 않고 20개 package를 열거한다. 빠진 것은 `config`, `h2`(JPA stereotype 없음)와 opt-in 두 개(`notification`, `fileserver`)다. 두 opt-in은 각자의 `@ConditionalOnProperty` configuration이 자기 package만 스캔한다. 이 배치의 이유는 javadoc과 `PersistenceEntityScanCoverageTest`에 기록돼 있다 — 과거에 root를 스캔해서 capability를 끈 배포가 `ddl-auto=validate`에서 `notification_request` / `fs_cleanup_item`을 요구하며 부팅에 실패했다. 측정 결과 always-install scan의 건전성은 유지되고 있다. `@Entity` 25개 중 opt-in package(`notification` 13, `fileserver` 6) 밖의 4개는 `idempotency_record`, `outbox_event`, `live_event_log`, `durable_operation`이고, 이 네 테이블은 모두 default location `db/migration/postgresql`(V1/V3/V11/V12)이 만든다. `postgresql` package는 scan 대상이지만 그 안의 candidate adapter들(`inbox`, `outbox` v2, `idempotency` v2)은 `@Entity`가 아니라 native SQL 기반이라 persistence unit에 들어오지 않는다. 즉 sub-scope 08이 발견한 \"opt-in stream을 always-install scan이 끌고 들어온다\" 유형의 결함은 현재 남아 있지 않다. 다만 `PersistenceEntityScanCoverageTest`가 지키는 범위에는 비대칭이 하나 있다. opt-in configuration 두 개에 대해서는 `@EntityScan` 목록과 `@EnableJpaRepositories` 목록이 **정확히 같은지** `containsExactly`로 검사한다(\"ent…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "composition-and-lifecycle-models/concept/concept-adapter-outbound-persistence-jpa-c56.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-jpa-c56", - "adapter-outbound-persistence-jpa-c56-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-jpa-c56.txt" - ] - }, - { - "title": "opt-in은 네 겹이고, 각 겹이 서로 다른 실패를 막는다", - "kind": "concept", - "slug": "adapter-outbound-persistence-mongo-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a06#L98`" - ], - "owning-module": "`adapter-outbound-persistence-mongo`", - "classification": "네 겹 모두 `ca-skeleton.persistence-mongo.enabled=true`라는 같은 조건을 읽는다(`evidence/raw/123-...` §8.2). 이것은 중복이 아니라 계층별 차단이다: filter는 Boot의 후보군, 나머지 셋은 자기 bean 그래프를 담당한다. `MongoPersistenceConfigTest`가 실제 `@EnableAutoConfiguration` context로 default/false에서 `MongoClient`·`MongoTemplate` 부재를, `enabled=true` + mock client에서 `MongoTemplate` 단일 bean을 확인한다. `MongoPlatformAutoConfiguration`(443줄)은 이 leaf에서 가장 밀도가 높은 파일이고, 거의 모든 `@Bean`의 javadoc이 **과거에 \"shipped했지만 아무 configuration도 만들지 않던\" 경로**를 기록한다 — atomic/bulk template, reactive 실행 경로 일체, change-stream source와 consumer, startup validator, client generation registry, health indicator. 이 leaf는 그 미연결들을 한 번 훑어 고친 이력을 갖고 있고, 그 사실이 이 sub-scope의 판단 기준을 바꾼다: 남아 있는 미연결은 \"아직 안 한 것\"이 아니라 \"훑고도 남은 것\"이다. startup 검증 쪽 설계도 눈여겨볼 만하다. `mongoPlatformStartupCheck`는 `MongoTopologyProbe` bean이 있을 때만 돌지만, 그 조건이 곧 탈출구가 되는 것을 막기 위해 `mongoTopologyProbeRequirement`가 **probe 조건 없이** 등록되어 \"platform profile이 있는데 probe가 없으면\" 실패시킨다. javadoc이 그 이유를 한 줄로 적는다 — \"a requirement that only applies when the thing it requires is present is not a requirement\".", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "composition-and-lifecycle-models/concept/concept-adapter-outbound-persistence-mongo-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-mongo-c01", - "adapter-outbound-persistence-mongo-c01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-mongo-c01.txt" - ] - }, - { - "title": "`failure`는 이 leaf에서 가장 잘 배선되고 가장 잘 논증된 부분이다", - "kind": "concept", - "slug": "adapter-outbound-persistence-mongo-c08", - "readiness": "READY", - "source": [ - "`final/document.md#a06#L1142`" - ], - "owning-module": "`adapter-outbound-persistence-mongo`", - "classification": "`MongoFailureClassifier`와 `MongoFailureTranslator`는 auto-configuration의 실제 bean이고(`MongoPlatformAutoConfiguration:85·92`), imperative·reactive 두 executor가 모두 그것을 통해 번역한다. 규칙 사슬도 순서까지 논증돼 있다 — **label → phase → 적용 가능성 → code table → fail closed**. 고쳐진 결함 이력이 촘촘하다. **번역기가 phase를 버렸다.** `DefaultMongoFailureTranslator`가 operationType을 들고도 context-free overload를 불러서, \"FIND의 응답 유실 = 재현 가능한 읽기 / UPDATE의 같은 유실 = 결과 불명 쓰기\"라는 구분이 **transaction이 아닌 모든 경로에서** 버려졌다 — 즉 모든 평범한 연산에서. 실패한 읽기가 ambiguous write로 보고됐다. **server-selection이 terminal이었다.** label도 code도 없는 실패가 `UNCLASSIFIED`로 떨어져 재시도 불가로 처리됐다 — 재시도가 명백히 안전한 유일한 경우인데. **Spring 래핑이 분류를 통째로 건너뛰었다.** `MongoFailureExtractor`가 그 수리다. cause 사슬을 깊이 16까지, `IdentityHashMap`으로 순환 안전하게 탐색한다(\"a cycle is about the same object appearing twice\"). **message는 절대 읽지 않는다.** `MongoDriverFailureView`가 driver 예외를 label·code·boolean 둘로 좁히는 지점이고, 그 이후 어느 계층도 나머지에 닿을 수 없다 — \"no later layer can reach the rest, because no later layer is ever handed it\". `MongoFailureClassification`의 생성자가 `COMMIT_ONLY`를 `TRANSACTION_COMMIT_UNKNOWN`에만 허용하는 것도 §15의 불변식과 맞물린다. `security`도…", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "composition-and-lifecycle-models/concept/concept-adapter-outbound-persistence-mongo-c08.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "adapter-outbound-persistence-mongo-c08" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/adapter-outbound-persistence-mongo-c08.txt" - ] - }, - { - "title": "모듈의 정체와 경계", - "kind": "concept", - "slug": "messaging-spring-cloud-stream-bridge-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-spring-cloud-stream-bridge#L56`" - ], - "owning-module": "`messaging-spring-cloud-stream-bridge`", - "classification": "Spring Cloud Stream 바인딩을 이미 쓰는 서비스가 같은 논리 목적지에 닿게 하는 **상호운용 seam**이다. **\"look enough like the platform's to be mistaken for them\"**이 이 leaf 전체의 위협 모델이다. 브리지는 기능을 추가하지 않고 **차이를 드러낸다.** 세 층으로 그것을 한다. 세 번째가 특이하다 — 거절할 수 없는 차이를 문서화 가능한 값으로 만든다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "composition-and-lifecycle-models/concept/concept-messaging-spring-cloud-stream-bridge-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-spring-cloud-stream-bridge-c01" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-spring-cloud-stream-bridge-c01.txt" - ] - }, - { - "title": "게이트가 세 조건을 순서대로 본다", - "kind": "concept", - "slug": "grpc-advanced-bootstrap-c02", - "readiness": "READY", - "source": [ - "`final/document.md#a20-grpc-advanced-bootstrap#L76`" - ], - "owning-module": "`grpc-advanced-bootstrap`", - "classification": "`requireStableStarterIsClean` 이 같은 불변식을 런타임에서도 확인한다 — 팻 자, 셰이드 산출물, 테스트 하네스처럼 다른 방식으로 조립된 런타임을 위해서다. **다만 그 메서드를 부르는 런타임이 없다**(§12.1). 지금 그 검사를 실행하는 것은 이 리프의 자기 테스트뿐이다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "composition-and-lifecycle-models/concept/concept-grpc-advanced-bootstrap-c02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "grpc-advanced-bootstrap-c02", - "grpc-advanced-bootstrap-c02-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/grpc-advanced-bootstrap-c02.txt" - ] - }, - { - "title": "의존성과 런타임 배선", - "kind": "concept", - "slug": "messaging-security-c02", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-security#L91`" - ], - "owning-module": "`messaging-security`", - "classification": "들어오는 것: `messaging-core-api`(api) 하나. 나가는 것: `messaging-runtime-core`, `messaging-kafka`, `messaging-rabbit`, `messaging-admin-runtime`, `messaging-pulsar-experimental`, `messaging-nats-experimental`, `messaging-spring-boot-starter`. **이 leaf는 messaging family에서 배선이 가장 잘 된 축에 속한다.** 어댑터 두 곳이 직접 소비한다. 이 leaf 자체는 Spring 주석을 갖지 않는다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "composition-and-lifecycle-models/concept/concept-messaging-security-c02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-security-c02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-security-c02.txt" - ] - }, - { - "title": "프로파일이 스스로 거부하는 것", - "kind": "concept", - "slug": "messaging-nats-experimental-c02", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-nats-experimental#L94`" - ], - "owning-module": "`messaging-nats-experimental`", - "classification": "`NatsJetStreamProfile` 은 record 이고, 압축 생성자가 이 어댑터의 불변식을 전부 들고 있다. 검증기가 호출되지 않는 지금, **실제로 실행되는 유일한 게이트가 여기다.** `ackMode` 거부 사유는 `NatsAckMode` 자신이 문장으로 들고 있고(`rejectionReason()`), 프로파일이 그 문장을 예외 메시지에 그대로 싣는다. `NONE` 은 \"forgotten\", `ALL` 은 \"still in flight\" — 테스트가 그 두 낱말로 각각 걸어 잠근다. `PARKING_HEADROOM = 1` 상수와 `parkAtDelivery() = maxDeliver - PARKING_HEADROOM` 가 §2 의 시점 선택을 숫자로 못 박는다. `NatsMaxDeliverParkingWorkflow.parkingThreshold()` 는 이 값을 그대로 위임한다 — 임계값의 정의가 한 곳에만 있다. **주의할 비대칭.** 편의 팩토리 `durable(subject, stream, durableName)` 는 중복 제거 창을 `Optional.of(2분)` 으로 채운다. 즉 팩토리를 거친 프로파일은 §17.1 의 구멍에 빠지지 않는다. 그러나 팩토리에도 호출자가 없고(저장소 전역 grep 0건), 정규 생성자는 빈 창을 정상값으로 받는다. 기본 경로가 안전하다는 사실이 그 구멍을 닫아 주지 않는다.", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "composition-and-lifecycle-models/concept/concept-messaging-nats-experimental-c02.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-nats-experimental-c02" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-nats-experimental-c02.txt" - ] - }, - { - "title": "Git/설계 문서에서 확인한 변화와 실패 기록", - "kind": "concept", - "slug": "messaging-runtime-core-c07", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-runtime-core#L631`" - ], - "owning-module": "`messaging-runtime-core`", - "classification": "이 leaf는 **통째로 하나의 수정**이다. MSG-INT-003이라는 식별자가 세 파일의 javadoc에 나온다(`DeclaredDestinationAccess`, `TransportMessagingRuntime`, `MessagingCoreAutoConfiguration:461`). `build.gradle` 주석의 마지막 문장이 이 leaf 전체의 교훈이다 — \"A starter that filled the gap with an application-supplied fake would pass a context test while running none of them.\"", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "composition-and-lifecycle-models/concept/concept-messaging-runtime-core-c07.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-runtime-core-c07" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-runtime-core-c07.txt" - ] - }, - { - "title": "선택은 닫힌 레지스트리이고, 등록과 조립은 다르다", - "kind": "concept", - "slug": "messaging-spring-boot-starter-c01", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-spring-boot-starter#L94`" - ], - "owning-module": "`messaging-spring-boot-starter`", - "classification": "`MessagingProviderSelection` 에 지도가 셋이다. 셋째 지도가 이 클래스의 판단이다. 등록되어 있다는 것과 조립할 수 있다는 것을 분리했고, 그 이유를 적었다 — Rabbit 을 고르면 핵심 설정 깊은 곳에서 `MessagingTransport` 빈이 없다는 오류가 나는데, 그것은 운영자에게 빈이 없다고만 말하지 고른 전송이 완성되지 않았다고는 말하지 않는다. 결과로 오늘 조립 가능한 전송은 `kafka` 하나다. `RabbitMessagingAutoConfiguration` 98줄은 선택 단계에서 거부되므로 **어떤 경로로도 도달하지 않는다**(§12.3).", - "missing-verification": "SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.", - "listedInTree": false, - "publication": "초안", - "file": "composition-and-lifecycle-models/concept/concept-messaging-spring-boot-starter-c01.md", - "status": "게시 전", - "studioId": "", - "assets": [ - "messaging-spring-boot-starter-c01", - "messaging-spring-boot-starter-c01-diagram" - ], - "evidenceFiles": [ - "../../../final/evidence/raw/messaging-spring-boot-starter-c01.txt" - ] - } - ], - "reference": [ - { - "title": "검증기는 발행이 아니라 주입이 강제다", - "kind": "reference", - "slug": "a-validator-is-enforced-by-injection", - "readiness": "READY", - "source": [ - "`final/document.md#a19-messaging-spring-boot-starter#L148`" - ], - "code": [ - "`.../autoconfigure/StartupProfileValidation.java`", - "`.../boot/GrpcPlatformStartupValidator.java`" - ], - "evidence": [ - "없음 — 이 저장소의 네 사례에서 뽑은 규칙이다" - ], - "classification": "검증기를 빈으로 만드는 것과 그것이 도는 것은 다른 사실이다. 이 저장소는 그 차이를 한 번 겪고 `StartupProfileValidation` 으로 고쳤으며, 그 javadoc 이 이전 상태를 기록한다 — 컨텍스트가 브로커마다 검증기를 발행하고 아무것도 검증하지 않았다. 같은 형태가 네 곳에 남아 있다.", - "scope": [ - "기동 시점 검증기 전부. 판정은 두 질문이다 — 이 검증기를 부르는 초기화 콜백이나 러너가 있는가, 그리고 그 호출자가 실제로 만들어지는 조건에 있는가." - ], - "exceptions": [ - "검증기가 값 객체의 생성자 안에서 도는 형태라면 주입이 필요 없다. 그때는 그 값 객체를 만드는 경로가 하나뿐인지가 대신 확인 대상이다." - ], - "relations": [ - "`case:validator-declared-and-never-injected`", - "`case:startup-validator-is-the-only-reader-of-four-keys`", - "`case:a14-f018-webplatformstartupvalidator`" - ], - "candidate-ledger": "`candidate-ledger.json`", - "source-manifest": "`root-tree-source-manifest.json` — 소스 68개", - "emitted": "`915`", - "merged": "`49`", - "blocked": "`1`", - "rejected": "`0`", - "unmapped": "`0`", - "listedInTree": false, - "publication": "초안", - "file": "runtime-reachability-and-composition/reference/reference-a-validator-is-enforced-by-injection.md", - "status": "게시 전", - "studioId": "", - "assets": [], - "evidenceFiles": [] - } - ], - "question": [], - "decision": [] - } } }, "candidates": [ + { + "id": "P1-a-bean-is-not-composition-evidence", + "kindCandidate": "REFERENCE", + "sourceRefs": [ + "`final/document.md#9` 규칙 2·3" + ], + "summary": "`@Bean`이 있다는 것은 조립 증거가 아니다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "reference:a-bean-is-not-composition-evidence", + "topic": "assembly-ownership", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-a-boundary-that-leaks-only-under-load", + "kindCandidate": "CASE", + "sourceRefs": [ + "final/document.md#5-5", + "final/document.md#8-2 항목 6" + ], + "summary": "부하 아래에서 지키라고 만든 경계가 부하 아래에서만 샌다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:a-boundary-that-leaks-only-under-load", + "topic": "duplicate-mechanisms", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-a-build-gate-that-is-not-in-the-build", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#4-4`" + ], + "summary": "\"build gate\"라 불리는 catalog drift 검사가 어디에서도 실행되지 않는다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:a-build-gate-that-is-not-in-the-build", + "topic": "redis-command-admission", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-a-certifying-lane-that-compared-nothing", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#6-6`" + ], + "summary": "\"certified\"라 불리던 레인이 threshold를 하나도 비교하지 않고 있었다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:a-certifying-lane-that-compared-nothing", + "topic": "what-a-gate-does-not-prove", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-a-contract-test-must-run-the-adapters-statement", + "kindCandidate": "REFERENCE", + "sourceRefs": [ + "`final/document.md#9` 규칙 19" + ], + "summary": "계약 테스트는 어댑터가 실제로 돌리는 statement를 실행해야 한다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "reference:a-contract-test-must-run-the-adapters-statement", + "topic": "what-a-gate-does-not-prove", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-a-five-second-string-that-broke-every-prod-deploy", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#4-1`" + ], + "summary": "`connection-timeout: 5s`가 모든 prod 배포를 시작 실패시켰고 local만 통과했다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:a-five-second-string-that-broke-every-prod-deploy", + "topic": "transaction-deadline-and-pool", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-a-flag-that-validates-an-unwired-subsystem", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#4-2`" + ], + "summary": "subsystem 전체가 미배선인데 그것을 켜는 flag는 startup 검사를 수행한다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:a-flag-that-validates-an-unwired-subsystem", + "topic": "assembly-ownership", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-a-gate-declared-in-prose-is-not-in-the-build", + "kindCandidate": "REFERENCE", + "sourceRefs": [ + "`final/document.md#14-2`" + ], + "summary": "산문이 선언한 게이트는 빌드에 있는 게이트가 아니다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "reference:a-gate-declared-in-prose-is-not-in-the-build", + "topic": "what-a-gate-does-not-prove", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-a-gate-nobody-runs-reports-the-last-run", + "kindCandidate": "REFERENCE", + "sourceRefs": [ + "`final/document.md#9` 규칙 6" + ], + "summary": "아무도 돌리지 않는 레인의 게이트는 마지막으로 돌린 사람이 본 것을 보고한다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "reference:a-gate-nobody-runs-reports-the-last-run", + "topic": "what-a-gate-does-not-prove", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-a-keyset-page-has-no-offset-field", + "kindCandidate": "DECISION", + "sourceRefs": [ + "final/document.md#10-4" + ], + "summary": "keyset 페이지에 offset 필드를 두지 않는다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "decision:a-keyset-page-has-no-offset-field", + "topic": "bounding-by-type", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-a-lane-that-discovers-nothing-must-fail", + "kindCandidate": "REFERENCE", + "sourceRefs": [ + "`final/document.md#6-3`" + ], + "summary": "아무것도 발견하지 못한 레인은 성공이 아니라 실패여야 한다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "reference:a-lane-that-discovers-nothing-must-fail", + "topic": "what-a-gate-does-not-prove", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-a-lease-without-an-owner", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#4-3`" + ], + "summary": "lease가 만료 시각만 기록하고 소유자를 기록하지 않아 terminal state가 되돌려졌다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:a-lease-without-an-owner", + "topic": "owner-safe-state-machines", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-a-mark-that-meant-seen-not-projected", + "kindCandidate": "CASE", + "sourceRefs": [ + "final/document.md#4-2", + "final/document.md#8-1 항목 9" + ], + "summary": "「본 적 있는 위치」를 「투영이 끝난 위치」로 쓴 mark 가 재전달된 이벤트를 삼켰다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:a-mark-that-meant-seen-not-projected", + "topic": "owner-safe-state-machines", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-a-policy-reversed-by-a-later-filter", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#7-3`" + ], + "summary": "요청 식별자를 클라이언트가 고를 수 없다는 정책이 뒤에 도는 필터에 뒤집혔다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:a-policy-reversed-by-a-later-filter", + "topic": "duplicate-mechanisms", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-a-red-test-misread-as-a-product-defect", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#14`" + ], + "summary": "붉은 테스트를 제품 결함으로 읽은 오진 — 듀얼스택 `localhost`가 TLS 실패를 가린다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:a-red-test-misread-as-a-product-defect", + "topic": "http-failure-classification", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-a-release-gate-with-no-evidence-producer", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#7-6`" + ], + "summary": "릴리스 게이트가 읽는 증거를 아무도 생산하지 않는다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:a-release-gate-with-no-evidence-producer", + "topic": "what-a-gate-does-not-prove", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-a-retry-implementation-nobody-calls", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#8-2` 항목 7" + ], + "summary": "재시도 구현이 둘이고 정교한 쪽을 아무도 호출하지 않는다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:a-retry-implementation-nobody-calls", + "topic": "duplicate-mechanisms", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-a-startup-probe-that-never-runs", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#4-4`" + ], + "summary": "startup probe가 production에서 한 번도 실행되지 않는다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:a-startup-probe-that-never-runs", + "topic": "redis-command-admission", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-a-startup-validator-that-requires-a-key-nothing-signs-with", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#7-1`" + ], + "summary": "시작 검증기가 커서 서명 키를 요구하는데 그 키로 서명하는 코드가 없다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:a-startup-validator-that-requires-a-key-nothing-signs-with", + "topic": "self-disclosure-grading", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-a-test-that-passed-on-the-wrong-guard", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#3-5`" + ], + "summary": "다중 타깃 검증을 확인한다는 테스트가 다른 가드에 걸려 통과했다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:a-test-that-passed-on-the-wrong-guard", + "topic": "what-a-gate-does-not-prove", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-a-validator-that-demands-tls-and-an-assembly-that-omits-it", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#14-2`" + ], + "summary": "검증기가 운영에 TLS를 요구하고, 실제로 조립되는 생산자에는 그 설정이 없다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:a-validator-that-demands-tls-and-an-assembly-that-omits-it", + "topic": "assembly-ownership", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-a-weaker-private-copy-on-the-wired-path", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#7-3`" + ], + "summary": "클라이언트가 준 엔드포인트가 SSRF 가드가 아니라 약한 private 사본을 지났다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:a-weaker-private-copy-on-the-wired-path", + "topic": "duplicate-mechanisms", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-agreement-between-documents-proves-nothing", + "kindCandidate": "REFERENCE", + "sourceRefs": [ + "`final/document.md#9` 규칙 5" + ], + "summary": "문서와 상수가 서로 일치하는 것으로는 아무것도 증명되지 않는다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "reference:agreement-between-documents-proves-nothing", + "topic": "what-a-gate-does-not-prove", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-an-active-transaction-check-that-asked-the-wrong-question", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#4-1`" + ], + "summary": "활성 트랜잭션 검사가 data source를 묻지 않아 다른 커넥션에서 커밋됐다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:an-active-transaction-check-that-asked-the-wrong-question", + "topic": "owner-safe-state-machines", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-an-unrecognised-sqlstate-is-not-guessed", + "kindCandidate": "DECISION", + "sourceRefs": [ + "final/document.md#10-2", + "final/document.md#5-1" + ], + "summary": "인식하지 못한 SQLSTATE 는 추측하지 않는다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "decision:an-unrecognised-sqlstate-is-not-guessed", + "topic": "http-failure-classification", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-an-unselectable-broker-listed-with-features", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#2-4`, `#3-3`" + ], + "summary": "선택할 수 없는 브로커가 지원 매트릭스에 기능 목록과 함께 실려 있다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:an-unselectable-broker-listed-with-features", + "topic": "drift-direction", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-apply-is-a-setting-approval-is-not", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#4-4`" + ], + "summary": "APPLY를 켜는 설정은 있고 승인을 검증하는 bean은 없다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:apply-is-a-setting-approval-is-not", + "topic": "objectstorage-staged-lifecycle", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-atomic-type-is-not-atomicity", + "kindCandidate": "REFERENCE", + "sourceRefs": [ + "`final/document.md#5-5`" + ], + "summary": "`Atomic*` 타입의 존재는 원자성의 증거가 아니다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "reference:atomic-type-is-not-atomicity", + "topic": "duplicate-mechanisms", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-build-only-exempts-ninety-files-from-todays-incident", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#8-3`" + ], + "summary": "build-only 등급이 90개 파일의 미조립을 오늘의 사고에서 면제한다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:build-only-exempts-ninety-files-from-todays-incident", + "topic": "self-disclosure-grading", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-candidate-evidence-stays-at-r1", + "kindCandidate": "DECISION", + "sourceRefs": [ + "`final/document.md#6-5`" + ], + "summary": "후보 증거는 통과해도 R1에 머무르고 R2는 별도 게이트가 판정한다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "decision:candidate-evidence-stays-at-r1", + "topic": "what-a-gate-does-not-prove", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-capability-grade-is-declared-not-inferred", + "kindCandidate": "DECISION", + "sourceRefs": [ + "`final/document.md#10-5`" + ], + "summary": "지원 등급은 추론이 아니라 선언이고 증거 없이는 올라가지 않는다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "decision:capability-grade-is-declared-not-inferred", + "topic": "drift-direction", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-capability-schema-registry", + "kindCandidate": "CONCEPT", + "sourceRefs": [ + "`final/document.md#4-1`" + ], + "summary": "capability_schema_registry — 스키마 적용과 사용 승인의 분리", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "concept:capability-schema-registry", + "topic": "owner-safe-state-machines", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-capability-separates-installation-from-activation", + "kindCandidate": "DECISION", + "sourceRefs": [ + "`final/document.md#4-1`" + ], + "summary": "capability는 스키마 적용과 사용 승인을 분리한다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "decision:capability-separates-installation-from-activation", + "topic": "owner-safe-state-machines", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-cardinality-bounds-as-types", + "kindCandidate": "CONCEPT", + "sourceRefs": [ + "`final/document.md#4-1`, `#5-2`" + ], + "summary": "카디널리티 경계를 타입으로 표현하기", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "concept:cardinality-bounds-as-types", + "topic": "bounding-by-type", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-cas-tuple-and-update-count", + "kindCandidate": "CONCEPT", + "sourceRefs": [ + "`final/document.md#4-1`" + ], + "summary": "CAS 튜플과 update count가 답이 되는 구조", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "concept:cas-tuple-and-update-count", + "topic": "owner-safe-state-machines", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-cas-tuple-in-the-where-clause", + "kindCandidate": "REFERENCE", + "sourceRefs": [ + "`final/document.md#9` 규칙 17" + ], + "summary": "CAS 튜플을 where 절에 전부 반복하고 update count를 답으로 쓴다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "reference:cas-tuple-in-the-where-clause", + "topic": "owner-safe-state-machines", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-check-which-duplicate-is-wired", + "kindCandidate": "REFERENCE", + "sourceRefs": [ + "`final/document.md#7-3`" + ], + "summary": "중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "reference:check-which-duplicate-is-wired", + "topic": "duplicate-mechanisms", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-classpath-presence-is-not-consent", + "kindCandidate": "DECISION", + "sourceRefs": [ + "`final/document.md#10-3`" + ], + "summary": "클래스패스에 있는 것은 실행 동의가 아니다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "decision:classpath-presence-is-not-consent", + "topic": "multitenancy-isolation", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-commit-ambiguity-is-not-only-sqlstate-08", + "kindCandidate": "CASE", + "sourceRefs": [ + "final/document.md#4-1" + ], + "summary": "pg_terminate_backend 가 57P01 로 도착하고 커밋 레코드는 이미 WAL 에 있었다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:commit-ambiguity-is-not-only-sqlstate-08", + "topic": "commit-ambiguity-as-a-result", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-commit-ambiguity-lane-not-rerun-at-head", + "kindCandidate": "OPEN_QUESTION", + "sourceRefs": [ + "final/document.md#6-2", + "final/document.md#11", + "final/document.md#13" + ], + "summary": "커밋 모호성 레인이 HEAD 에서도 통과하는지는 실행이 아니라 드리프트 0 으로 답했다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "question:commit-ambiguity-lane-not-rerun-at-head", + "topic": "commit-ambiguity-as-a-result", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-completion-unknown-is-never-retried", + "kindCandidate": "DECISION", + "sourceRefs": [ + "final/document.md#10-2", + "final/document.md#4-1" + ], + "summary": "completion-unknown 은 자동으로도 수동으로도 재시도하지 않는다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "decision:completion-unknown-is-never-retried", + "topic": "commit-ambiguity-as-a-result", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-conditional-evaluation-order-unverified", + "kindCandidate": "OPEN_QUESTION", + "sourceRefs": [ + "`final/document.md#11` 항목 4" + ], + "summary": "`@ConditionalOnBean` 사슬의 실제 평가 순서를 확인하지 않았다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "question:conditional-evaluation-order-unverified", + "topic": "assembly-ownership", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-conditionalonbean-must-be-satisfiable", + "kindCandidate": "REFERENCE", + "sourceRefs": [ + "`final/document.md#9` 규칙 4" + ], + "summary": "`@ConditionalOnBean`은 조건이 만족될 수 있는지까지 확인해야 한다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "reference:conditionalonbean-must-be-satisfiable", + "topic": "assembly-ownership", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-container-lanes-not-executed", + "kindCandidate": "OPEN_QUESTION", + "sourceRefs": [ + "`final/document.md#6-2`", + "`#11` 항목 1" + ], + "summary": "컨테이너가 필요한 레인의 실제 결과를 실행으로 확인하지 않았다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "question:container-lanes-not-executed", + "topic": "what-a-gate-does-not-prove", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-cursors-are-signed-for-integrity", + "kindCandidate": "DECISION", + "sourceRefs": [ + "`final/document.md#10-4`" + ], + "summary": "커서에 서명하는 이유는 기밀성이 아니라 무결성이다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "decision:cursors-are-signed-for-integrity", + "topic": "bounding-by-type", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-deadline-propagation", + "kindCandidate": "CONCEPT", + "sourceRefs": [ + "`final/document.md#3-2`" + ], + "summary": "호출 예산에서 DB 로컬 타임아웃까지의 데드라인 전파", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "concept:deadline-propagation", + "topic": "transaction-deadline-and-pool", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-destructive-admin-operations-are-not-autoconfigured", + "kindCandidate": "DECISION", + "sourceRefs": [ + "final/document.md#10-3", + "final/document.md#5-4" + ], + "summary": "파괴적 admin 작업은 자동설정하지 않는다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "decision:destructive-admin-operations-are-not-autoconfigured", + "topic": "assembly-ownership", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-doc-contract-test-boundary-predicted-the-drift", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#7-4`" + ], + "summary": "문서 계약 테스트의 단언 경계 밖에 발견된 드리프트 세 건이 전부 있었다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:doc-contract-test-boundary-predicted-the-drift", + "topic": "what-a-gate-does-not-prove", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-documented-uuidv7-generates-v4", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#7-4`" + ], + "summary": "문서가 UUIDv7이라 말하고 생성되는 것은 v4다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:documented-uuidv7-generates-v4", + "topic": "drift-direction", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-evidence-grades-and-provenance", + "kindCandidate": "CONCEPT", + "sourceRefs": [ + "`final/document.md#6-5`" + ], + "summary": "증거 등급과 provenance — R1과 R2를 가르는 것", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "concept:evidence-grades-and-provenance", + "topic": "what-a-gate-does-not-prove", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-fenced-lease", + "kindCandidate": "CONCEPT", + "sourceRefs": [ + "`final/document.md#4-1`, `#4-3`" + ], + "summary": "fenced lease — 만료 시각만으로는 부족한 이유", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "concept:fenced-lease", + "topic": "owner-safe-state-machines", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-file-state-machine-and-ready", + "kindCandidate": "CONCEPT", + "sourceRefs": [ + "`final/document.md#4-4`" + ], + "summary": "파일 상태 기계와 READY가 뜻하는 것", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "concept:file-state-machine-and-ready", + "topic": "fileserver-state-and-fencing", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-five-adapters-bypass-the-single-admission-point", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#4-4`" + ], + "summary": "의미 어댑터 다섯이 gateway를 직접 불러 admission 아홉 단계를 건너뛴다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:five-adapters-bypass-the-single-admission-point", + "topic": "redis-command-admission", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-five-copies-of-noscript-recovery", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#4-4`" + ], + "summary": "NOSCRIPT 복구가 다섯 벌이고 넷은 스크립트 레지스트리를 지나지 않는다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:five-copies-of-noscript-recovery", + "topic": "redis-command-admission", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-five-documents-say-nineteen-leaves", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#7-4`" + ], + "summary": "다섯 문서가 \"exactly 19 leaf\"라고 적고 레지스트리는 62다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:five-documents-say-nineteen-leaves", + "topic": "drift-direction", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-fix-overstatement-before-understatement", + "kindCandidate": "REFERENCE", + "sourceRefs": [ + "`final/document.md#7-4`" + ], + "summary": "과대 진술 문서를 과소보다 먼저 고친다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "reference:fix-overstatement-before-understatement", + "topic": "drift-direction", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-flyway-owns-the-schema", + "kindCandidate": "DECISION", + "sourceRefs": [ + "`final/document.md#4-1`, `#10-3`" + ], + "summary": "Flyway가 스키마를 소유하고 런타임 롤은 DDL 권한을 갖지 않는다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "decision:flyway-owns-the-schema", + "topic": "schema-ownership-and-capability-streams", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-four-grade-disclosure", + "kindCandidate": "CONCEPT", + "sourceRefs": [ + "`final/document.md#7-5`" + ], + "summary": "네 단계 공시 등급 — modelled에서 production-verified까지", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "concept:four-grade-disclosure", + "topic": "self-disclosure-grading", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-grades-derive-from-executed-evidence", + "kindCandidate": "DECISION", + "sourceRefs": [ + "`final/document.md#10-5`" + ], + "summary": "능력 등급은 코드가 아니라 실행된 증거에서 파생한다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "decision:grades-derive-from-executed-evidence", + "topic": "self-disclosure-grading", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-grades-may-understate-never-overstate", + "kindCandidate": "REFERENCE", + "sourceRefs": [ + "`final/document.md#7-5`" + ], + "summary": "등급은 네 단계로 나누고 관측보다 높게 적지 않는다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "reference:grades-may-understate-never-overstate", + "topic": "self-disclosure-grading", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-grpc-stays-build-only-until-the-bridge-is-decided", + "kindCandidate": "DECISION", + "sourceRefs": [ + "`final/document.md#2-3`" + ], + "summary": "gRPC 플랫폼은 build-only로 두고 애플리케이션 도달 경로를 먼저 정한다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "decision:grpc-stays-build-only-until-the-bridge-is-decided", + "topic": "learning-transfer-between-families", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-h2-hid-a-column-type-mismatch", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#4-1`" + ], + "summary": "`char(64)`와 `varchar(64)` 불일치를 H2가 가리고 있었다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:h2-hid-a-column-type-mismatch", + "topic": "schema-ownership-and-capability-streams", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-in-root-write-fails-fast", + "kindCandidate": "DECISION", + "sourceRefs": [ + "`final/document.md#3-2`" + ], + "summary": "`inRootWrite`는 suspend하지 않고 fail-fast한다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "decision:in-root-write-fails-fast", + "topic": "transaction-deadline-and-pool", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-independent-flyway-streams", + "kindCandidate": "CONCEPT", + "sourceRefs": [ + "`final/document.md#4-1`" + ], + "summary": "독립 Flyway 스트림과 baseline version 0", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "concept:independent-flyway-streams", + "topic": "schema-ownership-and-capability-streams", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-jpa-platform-capabilities-have-no-consumer", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#8-2` 항목 8" + ], + "summary": "JPA 플랫폼 capability 대부분에 production 소비자가 없다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:jpa-platform-capabilities-have-no-consumer", + "topic": "duplicate-mechanisms", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-local-with-a-different-db-is-a-different-system", + "kindCandidate": "REFERENCE", + "sourceRefs": [ + "`final/document.md#9` 규칙 18" + ], + "summary": "로컬이 다른 DB면 로컬 테스트는 다른 시스템에 대한 진술이다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "reference:local-with-a-different-db-is-a-different-system", + "topic": "schema-ownership-and-capability-streams", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-messaging-migrations-collide-at-v2", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#4-3`" + ], + "summary": "messaging 마이그레이션 두 leaf가 같은 디렉터리에서 `V2`를 둘 만들었다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:messaging-migrations-collide-at-v2", + "topic": "schema-ownership-and-capability-streams", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-mongo-default-throws-on-first-write", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#4-2`" + ], + "summary": "출하 default 조합이 첫 write에서 예외를 던진다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:mongo-default-throws-on-first-write", + "topic": "drift-direction", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-names-are-registry-keys-not-values", + "kindCandidate": "REFERENCE", + "sourceRefs": [ + "`final/document.md#9` 규칙 15" + ], + "summary": "이름은 값이 아니라 registry key다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "reference:names-are-registry-keys-not-values", + "topic": "bounding-by-type", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-native-claim-did-not-bump-the-version", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#4-1`" + ], + "summary": "native claim이 `@Version`을 올리지 않아 충돌을 보고하지 않는 낙관적 잠금이 됐다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:native-claim-did-not-bump-the-version", + "topic": "owner-safe-state-machines", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-no-type-metadata-inside-a-jsonb-document", + "kindCandidate": "DECISION", + "sourceRefs": [ + "final/document.md#10-4" + ], + "summary": "JSONB 문서 안에 타입 메타데이터를 넣지 않는다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "decision:no-type-metadata-inside-a-jsonb-document", + "topic": "bounding-by-type", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-observation-downgraded-by-the-composition", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#5-2`" + ], + "summary": "관측을 필수 생성자 인자로 만든 수정을 조립이 6인자 생성자로 되돌렸다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:observation-downgraded-by-the-composition", + "topic": "assembly-ownership", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-off-must-be-structural", + "kindCandidate": "REFERENCE", + "sourceRefs": [ + "`final/document.md#10-3`" + ], + "summary": "\"꺼짐\"은 조건의 반복이 아니라 구조여야 한다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "reference:off-must-be-structural", + "topic": "assembly-ownership", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-omission-that-passes-is-not-a-gate", + "kindCandidate": "REFERENCE", + "sourceRefs": [ + "`final/document.md#9` 규칙 12" + ], + "summary": "빠뜨림이 통과가 되는 게이트는 게이트가 아니다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "reference:omission-that-passes-is-not-a-gate", + "topic": "what-a-gate-does-not-prove", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-one-boot-would-settle-two-findings", + "kindCandidate": "OPEN_QUESTION", + "sourceRefs": [ + "`final/document.md#11` 항목 3" + ], + "summary": "부팅 한 번으로 확증 가능한 두 건이 아직 정적 추론으로만 남아 있다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "question:one-boot-would-settle-two-findings", + "topic": "assembly-ownership", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-one-root-owns-the-master-switch", + "kindCandidate": "DECISION", + "sourceRefs": [ + "`final/document.md#10-3`" + ], + "summary": "마스터 스위치는 루트 하나가 소유하고 자식 설정은 조건을 갖지 않는다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "decision:one-root-owns-the-master-switch", + "topic": "assembly-ownership", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-one-sqlstate-with-two-contributors-fails-startup", + "kindCandidate": "REFERENCE", + "sourceRefs": [ + "final/document.md#5-1" + ], + "summary": "같은 SQLState 를 둘이 등록하면 값이 같아도 시작을 실패시킨다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "reference:one-sqlstate-with-two-contributors-fails-startup", + "topic": "http-failure-classification", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-only-the-certification-lane-carries-no-docker-guard", + "kindCandidate": "DECISION", + "sourceRefs": [ + "`final/document.md#6-4`" + ], + "summary": "인증 레인만 Docker 가드를 달지 않는다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "decision:only-the-certification-lane-carries-no-docker-guard", + "topic": "what-a-gate-does-not-prove", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-outbox-chain-behind-an-unsatisfiable-condition", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#4-3`" + ], + "summary": "outbox 가 둘이고, 출하되는 것은 messaging 플랫폼 쪽이 아니다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:outbox-chain-behind-an-unsatisfiable-condition", + "topic": "assembly-ownership", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-performance-measurement-is-not-a-release-gate", + "kindCandidate": "DECISION", + "sourceRefs": [ + "`final/document.md#10-5`" + ], + "summary": "성능 측정은 릴리스 게이트에 넣지 않는다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "decision:performance-measurement-is-not-a-release-gate", + "topic": "what-a-gate-does-not-prove", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-pii-through-an-exception-message", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#8-1`" + ], + "summary": "시그니처가 payload를 받지 않는데 예외 메시지로 PII가 로그에 남았다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:pii-through-an-exception-message", + "topic": "bounding-by-type", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-pool-contract-lane-not-executed", + "kindCandidate": "OPEN_QUESTION", + "sourceRefs": [ + "`final/document.md#6-2`" + ], + "summary": "풀 계약 레인이 실행되지 않아 포화 동작이 확인되지 않았다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "question:pool-contract-lane-not-executed", + "topic": "transaction-deadline-and-pool", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-pool-need-is-a-capability-question", + "kindCandidate": "DECISION", + "sourceRefs": [ + "`final/document.md#10-3`" + ], + "summary": "풀이 필요한지는 \"JPA가 켜졌나\"가 아니라 \"커넥션이 필요한 capability가 있나\"로 묻는다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "decision:pool-need-is-a-capability-question", + "topic": "assembly-ownership", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-publish-evidence-and-completion", + "kindCandidate": "CONCEPT", + "sourceRefs": [ + "final/document.md#3-3" + ], + "summary": "발행 증거와 완료 판정이 따로 있는 이유", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "concept:publish-evidence-and-completion", + "topic": "commit-ambiguity-as-a-result", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-read-the-assembling-side-first", + "kindCandidate": "REFERENCE", + "sourceRefs": [ + "`final/document.md#13` 항목 5" + ], + "summary": "조립 결함을 판정하려면 조립하는 쪽을 먼저 읽어야 한다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "reference:read-the-assembling-side-first", + "topic": "assembly-ownership", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-read-the-clock-after-the-lock", + "kindCandidate": "REFERENCE", + "sourceRefs": [ + "`final/document.md#9` 규칙 17" + ], + "summary": "시간은 DB에서, 그리고 행을 잠근 다음에 읽는다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "reference:read-the-clock-after-the-lock", + "topic": "owner-safe-state-machines", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-readme-said-no-beans-there-are-eight", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#4-4`" + ], + "summary": "README가 \"노출된 setting도 bean도 없다\"고 적은 능력에 production bean 여덟이 있다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:readme-said-no-beans-there-are-eight", + "topic": "fileserver-state-and-fencing", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-record-the-evidence-first-choose-the-conclusion-later", + "kindCandidate": "DECISION", + "sourceRefs": [ + "final/document.md#10-2", + "final/document.md#3-3" + ], + "summary": "증거를 먼저 기록하고 결론은 나중에 고른다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "decision:record-the-evidence-first-choose-the-conclusion-later", + "topic": "commit-ambiguity-as-a-result", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-redis-admission-stages", + "kindCandidate": "CONCEPT", + "sourceRefs": [ + "`final/document.md#4-4`" + ], + "summary": "명령 카탈로그와 admission 아홉 단계", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "concept:redis-admission-stages", + "topic": "redis-command-admission", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-redis-topology-lane-not-executed", + "kindCandidate": "OPEN_QUESTION", + "sourceRefs": [ + "`final/document.md#6-2`" + ], + "summary": "Redis 토폴로지 레인이 실행되지 않아 key spec 드리프트가 확인되지 않았다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "question:redis-topology-lane-not-executed", + "topic": "redis-command-admission", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-register-paths-bind-values", + "kindCandidate": "REFERENCE", + "sourceRefs": [ + "`final/document.md#9` 규칙 16" + ], + "summary": "path·identifier는 등록하고 value는 바인딩한다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "reference:register-paths-bind-values", + "topic": "bounding-by-type", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-registry-rules-transfer-ci-rules-do-not", + "kindCandidate": "REFERENCE", + "sourceRefs": [ + "`final/document.md#7-6`" + ], + "summary": "레지스트리로 표현된 규칙은 전이되고 CI로 표현된 규칙은 전이되지 않는다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "reference:registry-rules-transfer-ci-rules-do-not", + "topic": "learning-transfer-between-families", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-requires-new-connection-cost", + "kindCandidate": "CONCEPT", + "sourceRefs": [ + "`final/document.md#3-2`" + ], + "summary": "`REQUIRES_NEW`의 커넥션 비용과 풀 사이징 제약", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "concept:requires-new-connection-cost", + "topic": "transaction-deadline-and-pool", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-requires-new-pins-the-outer-connection", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#3-2`" + ], + "summary": "`REQUIRES_NEW`가 바깥 커넥션을 핀한 채 새 커넥션을 딴다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:requires-new-pins-the-outer-connection", + "topic": "transaction-deadline-and-pool", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-retry-safety-is-decided-by-evidence", + "kindCandidate": "DECISION", + "sourceRefs": [ + "`final/document.md#10-2`" + ], + "summary": "재시도 안전성은 증거에 기반해 판정한다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "decision:retry-safety-is-decided-by-evidence", + "topic": "http-failure-classification", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-rls-three-preconditions", + "kindCandidate": "CONCEPT", + "sourceRefs": [ + "`final/document.md#4-1`" + ], + "summary": "RLS가 성립하기 위한 세 전제", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "concept:rls-three-preconditions", + "topic": "multitenancy-isolation", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-runtime-membership-decides-severity", + "kindCandidate": "REFERENCE", + "sourceRefs": [ + "`final/document.md#9` 규칙 1" + ], + "summary": "`runtime_memberships`를 먼저 읽고 심각도를 정한다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "reference:runtime-membership-decides-severity", + "topic": "self-disclosure-grading", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-scan-exclusion-without-an-owner", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#3-1`" + ], + "summary": "스캔에서 뺀 다섯 패키지의 컴포넌트 여섯을 두 자동설정 어느 쪽도 소유하지 않았다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:scan-exclusion-without-an-owner", + "topic": "assembly-ownership", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-scriptable-detection-bypassed-by-a-bom", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#4-4`" + ], + "summary": "scriptable 콘텐츠 탐지가 BOM·NUL·주석으로 우회된다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:scriptable-detection-bypassed-by-a-bom", + "topic": "fileserver-state-and-fencing", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-seven-rows-downgraded-by-the-family-itself", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#7-5`" + ], + "summary": "등급표 13행 중 일곱을 스스로 강등하고 한 행만 관측과 어긋났다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:seven-rows-downgraded-by-the-family-itself", + "topic": "self-disclosure-grading", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-signed-cursor-structure", + "kindCandidate": "CONCEPT", + "sourceRefs": [ + "`final/document.md#10-4`" + ], + "summary": "서명된 커서의 구조와 검증 순서", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "concept:signed-cursor-structure", + "topic": "bounding-by-type", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-staged-lifecycle-and-signed-grants", + "kindCandidate": "CONCEPT", + "sourceRefs": [ + "`final/document.md#4-4`" + ], + "summary": "staged lifecycle과 서명된 grant", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "concept:staged-lifecycle-and-signed-grants", + "topic": "objectstorage-staged-lifecycle", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-strict-test-lane", + "kindCandidate": "CONCEPT", + "sourceRefs": [ + "`final/document.md#6-3`" + ], + "summary": "strict test lane — 발견하지 못하면 실패하는 레인", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "concept:strict-test-lane", + "topic": "what-a-gate-does-not-prove", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-support-matrix-said-the-opposite-of-the-code", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#8-1`" + ], + "summary": "지원 매트릭스가 코드와 반대를 적었고, 그 오해가 소비자에게 자기 멱등성을 생략하게 한다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:support-matrix-said-the-opposite-of-the-code", + "topic": "drift-direction", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-telemetry-can-be-more-dangerous-than-its-subject", + "kindCandidate": "REFERENCE", + "sourceRefs": [ + "`final/document.md#5-2`" + ], + "summary": "관측을 위해 수집한 데이터가 관측 대상보다 위험할 수 있다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "reference:telemetry-can-be-more-dangerous-than-its-subject", + "topic": "bounding-by-type", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-templates-are-built-once-per-mode", + "kindCandidate": "DECISION", + "sourceRefs": [ + "`final/document.md#3-2`" + ], + "summary": "트랜잭션 템플릿은 모드별로 미리 만들어 둔다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "decision:templates-are-built-once-per-mode", + "topic": "transaction-deadline-and-pool", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-tenant-id-is-never-a-metric-tag", + "kindCandidate": "DECISION", + "sourceRefs": [ + "`final/document.md#10-4`" + ], + "summary": "tenant id는 메트릭 태그가 되지 않는다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "decision:tenant-id-is-never-a-metric-tag", + "topic": "bounding-by-type", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-the-guard-is-on-and-the-service-is-not", + "kindCandidate": "CASE", + "sourceRefs": [ + "final/document.md#5-4" + ], + "summary": "가드와 journal 과 durability 검증기는 켜지고, 부를 서비스가 없었다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:the-guard-is-on-and-the-service-is-not", + "topic": "assembly-ownership", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-the-readme-recipe-does-not-start", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#7-4`" + ], + "summary": "README의 활성화 recipe를 그대로 따르면 애플리케이션이 시작되지 않는다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:the-readme-recipe-does-not-start", + "topic": "drift-direction", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-the-same-rotation-defect-closed-once-and-reproduced", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#5-5`" + ], + "summary": "같은 자격 증명 회전 결함이 한 가족에서 닫히고 다른 가족에서 재현됐다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:the-same-rotation-defect-closed-once-and-reproduced", + "topic": "learning-transfer-between-families", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-the-second-platform-carried-the-design-not-the-wiring", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#7-6`" + ], + "summary": "두 번째 플랫폼이 첫 번째의 bridge 부재는 막고 게이트 배선은 옮기지 않았다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:the-second-platform-carried-the-design-not-the-wiring", + "topic": "learning-transfer-between-families", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-the-startup-validator-follows-the-autoconfiguration-root", + "kindCandidate": "REFERENCE", + "sourceRefs": [ + "`final/document.md#5-3`" + ], + "summary": "시작 검증기가 도는지는 그 능력에 자동설정 루트가 있는지와 일치한다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "reference:the-startup-validator-follows-the-autoconfiguration-root", + "topic": "assembly-ownership", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-thirteen-startup-rules-never-run", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#5-3`" + ], + "summary": "시작 검증기 13개 규칙이 유일한 조립 지점에서 호출되지 않는다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:thirteen-startup-rules-never-run", + "topic": "assembly-ownership", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-three-assembly-paths", + "kindCandidate": "CONCEPT", + "sourceRefs": [ + "`final/document.md#1-4`, `#7-1`" + ], + "summary": "Spring 조립의 세 경로와 각각이 결정하는 것", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "concept:three-assembly-paths", + "topic": "assembly-ownership", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-three-failure-vocabularies", + "kindCandidate": "CONCEPT", + "sourceRefs": [ + "final/document.md#5-1" + ], + "summary": "실패 어휘 세 층과 그 사이를 잇는 SQLState 매트릭스", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "concept:three-failure-vocabularies", + "topic": "http-failure-classification", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-three-ways-rls-does-nothing", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#4-1`" + ], + "summary": "RLS가 아무것도 하지 않는 세 가지 방법", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:three-ways-rls-does-nothing", + "topic": "multitenancy-isolation", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-transaction-result-algebra", + "kindCandidate": "CONCEPT", + "sourceRefs": [ + "final/document.md#3-2" + ], + "summary": "트랜잭션 결과 대수 — 다섯 변형이 각각 답하는 질문", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "concept:transaction-result-algebra", + "topic": "commit-ambiguity-as-a-result", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-transport-failure-stage-and-category", + "kindCandidate": "CONCEPT", + "sourceRefs": [ + "`final/document.md#8-1`" + ], + "summary": "전송 실패의 단계와 범주 — `AttemptStage`와 `FailureCategory`", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "concept:transport-failure-stage-and-category", + "topic": "http-failure-classification", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-trust-policy-lives-in-nginx-not-in-the-code", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#3-1`" + ], + "summary": "forwarded 헤더 신뢰 판정이 Nginx에만 있고 Java 정책 421 LOC은 배선되지 않았다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:trust-policy-lives-in-nginx-not-in-the-code", + "topic": "duplicate-mechanisms", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-two-trees-both-numbered-from-v1", + "kindCandidate": "CASE", + "sourceRefs": [ + "`final/document.md#4-1`" + ], + "summary": "두 트리가 다 V1부터 번호를 매겨 공유 history가 하나를 건너뛸 수 있었다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "case:two-trees-both-numbered-from-v1", + "topic": "schema-ownership-and-capability-streams", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-unknown-is-a-third-result", + "kindCandidate": "REFERENCE", + "sourceRefs": [ + "final/document.md#9", + "final/document.md#3-3" + ], + "summary": "모르는 것은 성공도 실패도 아닌 세 번째 결과여야 한다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "reference:unknown-is-a-third-result", + "topic": "commit-ambiguity-as-a-result", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-v2-state-machine-lanes-not-executed", + "kindCandidate": "OPEN_QUESTION", + "sourceRefs": [ + "`final/document.md#6-2`" + ], + "summary": "V2 상태 기계 넷의 컨테이너 레인이 실행되지 않았다", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "question:v2-state-machine-lanes-not-executed", + "topic": "owner-safe-state-machines", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, + { + "id": "P1-widen-doc-contract-assertions", + "kindCandidate": "OPEN_QUESTION", + "sourceRefs": [ + "`final/document.md#7-4`" + ], + "summary": "문서 계약 테스트의 단언 범위를 capability 표까지 넓힐 것인가", + "disposition": "PROMOTE", + "dispositionReview": "CONFIRMED", + "target": "question:widen-doc-contract-assertions", + "topic": "drift-direction", + "reason": "제1부가 이 주장을 자기 문장으로 채택했고(§3~§11), 없애고 다른 기록의 한 절로 넣으면 이해나 재사용이 달라진다" + }, { "id": "A01-F001", "kindCandidate": "CASE", @@ -24890,11 +6099,9 @@ ], "sourceHeading": "P3 — `IdFactory.newId()`의 “never-before-used” 문구 정밀화", "summary": "`IdFactory.newId()`의 “never-before-used” 문구 정밀화", - "disposition": "PROMOTE", - "target": "case:a01-f002-idfactory-newid", - "reason": null, - "topic": "runtime-contract-correctness", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A02-F001", @@ -24905,12 +6112,10 @@ ], "sourceHeading": "P1 — response/LRO invariant enforcement boundary", "summary": "response/LRO invariant enforcement boundary", - "disposition": "PROMOTE", - "target": "open-question:a02-f001-lro", - "reason": null, - "topic": "contract-domain-and-bounds", + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", "classificationCorrection": "analysis-authored unresolved/kind hint applied during recall-gate review", - "dispositionReview": "PENDING" + "dispositionReview": "CONFIRMED" }, { "id": "A02-F002", @@ -24921,12 +6126,10 @@ ], "sourceHeading": "P1 — DomainContextKey same-name different-type collision", "summary": "DomainContextKey same-name different-type collision", - "disposition": "PROMOTE", - "target": "open-question:a02-f002-domaincontextkey", - "reason": null, - "topic": "contract-domain-and-bounds", + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", "classificationCorrection": "analysis-authored unresolved/kind hint applied during recall-gate review", - "dispositionReview": "PENDING" + "dispositionReview": "CONFIRMED" }, { "id": "A02-F003", @@ -24937,12 +6140,10 @@ ], "sourceHeading": "P2 — bounded operational record identifiers", "summary": "bounded operational record identifiers", - "disposition": "PROMOTE", - "target": "open-question:analysis-finding-a02-f003", - "reason": null, - "topic": "contract-domain-and-bounds", + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", "classificationCorrection": "analysis-authored unresolved/kind hint applied during recall-gate review", - "dispositionReview": "PENDING" + "dispositionReview": "CONFIRMED" }, { "id": "A02-F004", @@ -24953,12 +6154,10 @@ ], "sourceHeading": "P2 — permission component grammar", "summary": "permission component grammar", - "disposition": "PROMOTE", - "target": "open-question:analysis-finding-a02-f004", - "reason": null, - "topic": "security-policy-enforcement", + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", "classificationCorrection": "analysis-authored unresolved/kind hint applied during recall-gate review", - "dispositionReview": "PENDING" + "dispositionReview": "CONFIRMED" }, { "id": "A02-F005", @@ -24969,12 +6168,10 @@ ], "sourceHeading": "P2 — messaging schema qualification boundary", "summary": "messaging schema qualification boundary", - "disposition": "PROMOTE", - "target": "open-question:analysis-finding-a02-f005", - "reason": null, - "topic": "schema-and-data-contracts", + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", "classificationCorrection": "analysis-authored unresolved/kind hint applied during recall-gate review", - "dispositionReview": "PENDING" + "dispositionReview": "CONFIRMED" }, { "id": "A03-F001", @@ -24985,11 +6182,9 @@ ], "sourceHeading": "P1 — notification admin atomic claim contract가 service에서 사용되지 않음", "summary": "notification admin atomic claim contract가 service에서 사용되지 않음", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a03-f001", - "reason": null, - "topic": "state-ownership-and-concurrency", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A03-F002", @@ -25000,12 +6195,10 @@ ], "sourceHeading": "P2 — notification derived idempotency key가 32-bit hash", "summary": "notification derived idempotency key가 32-bit hash", - "disposition": "PROMOTE", - "target": "open-question:analysis-finding-a03-f002", - "reason": null, - "topic": "state-ownership-and-concurrency", + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", "classificationCorrection": "analysis-authored unresolved/kind hint applied during recall-gate review", - "dispositionReview": "PENDING" + "dispositionReview": "CONFIRMED" }, { "id": "A03-F003", @@ -25016,12 +6209,10 @@ ], "sourceHeading": "P2 — legacy storage/notification compatibility surface의 제거 조건 추적", "summary": "legacy storage/notification compatibility surface의 제거 조건 추적", - "disposition": "PROMOTE", - "target": "reference:analysis-finding-a03-f003", - "reason": null, - "topic": "runtime-contract-correctness", + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", "classificationCorrection": "analysis-authored unresolved/kind hint applied during recall-gate review", - "dispositionReview": "PENDING" + "dispositionReview": "CONFIRMED" }, { "id": "A03-F004", @@ -25032,12 +6223,10 @@ ], "sourceHeading": "P3 — isolation vocabulary와 legacy routing capability의 시차", "summary": "isolation vocabulary와 legacy routing capability의 시차", - "disposition": "PROMOTE", - "target": "open-question:analysis-finding-a03-f004", - "reason": null, - "topic": "runtime-contract-correctness", + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", "classificationCorrection": "analysis-authored unresolved/kind hint applied during recall-gate review", - "dispositionReview": "PENDING" + "dispositionReview": "CONFIRMED" }, { "id": "A04-F001", @@ -25062,11 +6251,9 @@ ], "sourceHeading": "5. Confirmed P1 — notification consumer는 diagnostic failure를 authoritative failure로 바꿀 수 있다", "summary": "notification consumer는 diagnostic failure를 authoritative failure로 바꿀 수 있다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a04-f002", - "reason": null, - "topic": "runtime-reachability-and-composition", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A04-F003", @@ -25091,11 +6278,9 @@ ], "sourceHeading": "P1 — notification fail-open consumer가 logger failure를 격리하지 않는다", "summary": "notification fail-open consumer가 logger failure를 격리하지 않는다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a04-f004", - "reason": null, - "topic": "runtime-reachability-and-composition", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A04-F005", @@ -25106,11 +6291,9 @@ ], "sourceHeading": "P3 — support README가 current architecture registry/history와 drift", "summary": "support README가 current architecture registry/history와 drift", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a04-f005", - "reason": null, - "topic": "declaration-and-document-drift", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A04-F006", @@ -25121,12 +6304,10 @@ ], "sourceHeading": "P3 — cache-redis/httpclient의 support project dependency 필요성 재검증", "summary": "cache-redis/httpclient의 support project dependency 필요성 재검증", - "disposition": "PROMOTE", - "target": "open-question:analysis-finding-a04-f006", - "reason": null, - "topic": "verification-path-coverage", + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", "classificationCorrection": "analysis-authored unresolved/kind hint applied during recall-gate review", - "dispositionReview": "PENDING" + "dispositionReview": "CONFIRMED" }, { "id": "A05-F001", @@ -25137,11 +6318,9 @@ ], "sourceHeading": "6.2 Confirmed P2 — encode가 발급한 2046~2048-byte cursor를 decode가 거부한다", "summary": "encode가 발급한 2046~2048-byte cursor를 decode가 거부한다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a05-f001", - "reason": null, - "topic": "contract-domain-and-bounds", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A05-F002", @@ -25152,9 +6331,8 @@ ], "sourceHeading": "P2 — `SignedJsonCursorCodec` accepted encode domain과 decode domain 불일치", "summary": "`SignedJsonCursorCodec` accepted encode domain과 decode domain 불일치", - "disposition": "MERGE_INTO", - "target": "case:analysis-finding-a05-f001", - "reason": "A05-F001와 같은 분석 사건을 후속 sub-scope/pass에서 다시 기록한 중복 finding이다. 별도 Case를 만들지 않고 primary candidate에 병합한다.", + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", "dispositionReview": "CONFIRMED" }, { @@ -25166,12 +6344,10 @@ ], "sourceHeading": "P2 — `CapabilitySupport.constraints`의 bounded/report-safe 계약이 타입에서 강제되지 않음", "summary": "`CapabilitySupport.constraints`의 bounded/report-safe 계약이 타입에서 강제되지 않음", - "disposition": "PROMOTE", - "target": "open-question:a05-f003-capabilitysupport-constraints", - "reason": null, - "topic": "contract-domain-and-bounds", + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", "classificationCorrection": "analysis-authored unresolved/kind hint applied during recall-gate review", - "dispositionReview": "PENDING" + "dispositionReview": "CONFIRMED" }, { "id": "A05-F004", @@ -25182,11 +6358,9 @@ ], "sourceHeading": "P3 — `RetryDecision.reason`의 “bounded” 설명과 constructor contract 불일치", "summary": "`RetryDecision.reason`의 “bounded” 설명과 constructor contract 불일치", - "disposition": "PROMOTE", - "target": "case:a05-f004-retrydecision-reason", - "reason": null, - "topic": "declaration-and-document-drift", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A05-F005", @@ -25197,11 +6371,9 @@ ], "sourceHeading": "20. Confirmed P2 — application-supplied `JpaRetryPolicy`가 valid execution에서 무시된다", "summary": "application-supplied `JpaRetryPolicy`가 valid execution에서 무시된다", - "disposition": "PROMOTE", - "target": "case:a05-f005-jparetrypolicy", - "reason": null, - "topic": "runtime-contract-correctness", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A05-F006", @@ -25212,11 +6384,9 @@ ], "sourceHeading": "23. Confirmed P1 — Stable completion-evidence capability가 shipped composition에 설치되지 않는다", "summary": "Stable completion-evidence capability가 shipped composition에 설치되지 않는다", - "disposition": "PROMOTE", - "target": "case:a05-f006-stable", - "reason": null, - "topic": "verification-path-coverage", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A05-F007", @@ -25227,11 +6397,9 @@ ], "sourceHeading": "25. P3 — `TransactionProfileRegistry`는 declarative retry 제거 후 legacy residue 후보", "summary": "`TransactionProfileRegistry`는 declarative retry 제거 후 legacy residue 후보", - "disposition": "PROMOTE", - "target": "case:a05-f007-transactionprofileregistry", - "reason": null, - "topic": "runtime-reachability-and-composition", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A05-F008", @@ -25242,9 +6410,8 @@ ], "sourceHeading": "P1 — completion-evidence Stable contract가 actual composition에 연결되지 않음", "summary": "completion-evidence Stable contract가 actual composition에 연결되지 않음", - "disposition": "MERGE_INTO", - "target": "case:a05-f006-stable", - "reason": "A05-F006와 같은 분석 사건을 후속 sub-scope/pass에서 다시 기록한 중복 finding이다. 별도 Case를 만들지 않고 primary candidate에 병합한다.", + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", "dispositionReview": "CONFIRMED" }, { @@ -25256,9 +6423,8 @@ ], "sourceHeading": "P2 — custom `JpaRetryPolicy`가 silently ignored", "summary": "custom `JpaRetryPolicy`가 silently ignored", - "disposition": "MERGE_INTO", - "target": "case:a05-f005-jparetrypolicy", - "reason": "A05-F005와 같은 분석 사건을 후속 sub-scope/pass에서 다시 기록한 중복 finding이다. 별도 Case를 만들지 않고 primary candidate에 병합한다.", + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", "dispositionReview": "CONFIRMED" }, { @@ -25270,11 +6436,9 @@ ], "sourceHeading": "P2 — canonical transaction boundary documentation과 실제 dual stack 불일치", "summary": "canonical transaction boundary documentation과 실제 dual stack 불일치", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a05-f010", - "reason": null, - "topic": "contract-domain-and-bounds", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A05-F011", @@ -25285,9 +6449,8 @@ ], "sourceHeading": "P3 — TransactionProfileRegistry legacy residue", "summary": "TransactionProfileRegistry legacy residue", - "disposition": "MERGE_INTO", - "target": "case:a05-f007-transactionprofileregistry", - "reason": "A05-F007와 같은 분석 사건을 후속 sub-scope/pass에서 다시 기록한 중복 finding이다. 별도 Case를 만들지 않고 primary candidate에 병합한다.", + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", "dispositionReview": "CONFIRMED" }, { @@ -25299,11 +6462,9 @@ ], "sourceHeading": "38. Confirmed P2 — property-access `IDENTITY` entity가 batch guard를 우회한다", "summary": "property-access `IDENTITY` entity가 batch guard를 우회한다", - "disposition": "PROMOTE", - "target": "case:a05-f012-identity", - "reason": null, - "topic": "security-policy-enforcement", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A05-F013", @@ -25314,11 +6475,9 @@ ], "sourceHeading": "47. Confirmed P2 — `SpecificationPolicy`는 `Specification.unrestricted()`를 bounded로 오인한다", "summary": "`SpecificationPolicy`는 `Specification.unrestricted()`를 bounded로 오인한다", - "disposition": "PROMOTE", - "target": "case:a05-f013-specificationpolicy-specification-unrestricted", - "reason": null, - "topic": "contract-domain-and-bounds", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A05-F014", @@ -25329,11 +6488,9 @@ ], "sourceHeading": "52. Confirmed P1 — `collection-fetch-pagination` blocking release gate가 실제 위험을 증명하지 않는다", "summary": "`collection-fetch-pagination` blocking release gate가 실제 위험을 증명하지 않는다", - "disposition": "PROMOTE", - "target": "case:a05-f014-collection-fetch-pagination", - "reason": null, - "topic": "verification-path-coverage", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A05-F015", @@ -25344,9 +6501,8 @@ ], "sourceHeading": "P1 — blocking `collection-fetch-pagination` release gate false evidence", "summary": "blocking `collection-fetch-pagination` release gate false evidence", - "disposition": "MERGE_INTO", - "target": "case:a05-f014-collection-fetch-pagination", - "reason": "A05-F014와 같은 분석 사건을 후속 sub-scope/pass에서 다시 기록한 중복 finding이다. 별도 Case를 만들지 않고 primary candidate에 병합한다.", + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", "dispositionReview": "CONFIRMED" }, { @@ -25358,9 +6514,8 @@ ], "sourceHeading": "P2 — property-access IDENTITY가 batching-required guard를 우회", "summary": "property-access IDENTITY가 batching-required guard를 우회", - "disposition": "MERGE_INTO", - "target": "case:a05-f012-identity", - "reason": "A05-F012와 같은 분석 사건을 후속 sub-scope/pass에서 다시 기록한 중복 finding이다. 별도 Case를 만들지 않고 primary candidate에 병합한다.", + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", "dispositionReview": "CONFIRMED" }, { @@ -25372,9 +6527,8 @@ ], "sourceHeading": "P2 — `SpecificationPolicy`가 unrestricted non-null Specification을 허용", "summary": "`SpecificationPolicy`가 unrestricted non-null Specification을 허용", - "disposition": "MERGE_INTO", - "target": "case:a05-f013-specificationpolicy-specification-unrestricted", - "reason": "A05-F013와 같은 분석 사건을 후속 sub-scope/pass에서 다시 기록한 중복 finding이다. 별도 Case를 만들지 않고 primary candidate에 병합한다.", + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", "dispositionReview": "CONFIRMED" }, { @@ -25386,11 +6540,9 @@ ], "sourceHeading": "Cross-scope P1/P2 — query SQL naming/observability composition 부재", "summary": "query SQL naming/observability composition 부재", - "disposition": "PROMOTE", - "target": "case:a05-f018-sql", - "reason": null, - "topic": "runtime-contract-correctness", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A05-F019", @@ -25401,11 +6553,9 @@ ], "sourceHeading": "P2/P3 — export surface split SSOT", "summary": "export surface split SSOT", - "disposition": "PROMOTE", - "target": "case:a05-f019-ssot", - "reason": null, - "topic": "runtime-contract-correctness", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A05-F020", @@ -25416,11 +6566,9 @@ ], "sourceHeading": "59.1 P1 — `inspect()`와 `claim()`이 만료된 COMPLETED row를 동시에 다른 상태로 해석한다", "summary": "`inspect()`와 `claim()`이 만료된 COMPLETED row를 동시에 다른 상태로 해석한다", - "disposition": "PROMOTE", - "target": "case:a05-f020-inspect-claim", - "reason": null, - "topic": "state-ownership-and-concurrency", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A05-F021", @@ -25431,11 +6579,9 @@ ], "sourceHeading": "59.2 P2 — `complete()`의 replay 판정이 `replayTtl` 변경을 무시한다", "summary": "`complete()`의 replay 판정이 `replayTtl` 변경을 무시한다", - "disposition": "PROMOTE", - "target": "case:a05-f021-complete-replayttl", - "reason": null, - "topic": "state-ownership-and-concurrency", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A05-F022", @@ -25446,11 +6592,9 @@ ], "sourceHeading": "69. P1 — Stable runtime-role verification이 startup에서 실제 policy를 적용하지 않는다", "summary": "Stable runtime-role verification이 startup에서 실제 policy를 적용하지 않는다", - "disposition": "PROMOTE", - "target": "case:a05-f022-stable", - "reason": null, - "topic": "runtime-reachability-and-composition", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A05-F023", @@ -25461,11 +6605,9 @@ ], "sourceHeading": "78. P1 — persistent byte quota가 실제 admission에서 집행되지 않는다", "summary": "persistent byte quota가 실제 admission에서 집행되지 않는다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a05-f023", - "reason": null, - "topic": "state-ownership-and-concurrency", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A05-F024", @@ -25476,11 +6618,9 @@ ], "sourceHeading": "80. P2 — quota reclaim은 최대 64개 committed row만 처리하고 남은 byte를 조용히 버린다", "summary": "quota reclaim은 최대 64개 committed row만 처리하고 남은 byte를 조용히 버린다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a05-f024", - "reason": null, - "topic": "state-ownership-and-concurrency", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A05-F025", @@ -25491,11 +6631,9 @@ ], "sourceHeading": "81. P2 — direct `FileQuotaService.commit()`은 만료 reservation을 commit한다", "summary": "direct `FileQuotaService.commit()`은 만료 reservation을 commit한다", - "disposition": "PROMOTE", - "target": "case:a05-f025-filequotaservice-commit", - "reason": null, - "topic": "state-ownership-and-concurrency", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A05-F026", @@ -25506,11 +6644,9 @@ ], "sourceHeading": "82. P2 — recovery queue의 `enqueue()`는 concurrent upsert가 아니다", "summary": "recovery queue의 `enqueue()`는 concurrent upsert가 아니다", - "disposition": "PROMOTE", - "target": "case:a05-f026-enqueue", - "reason": null, - "topic": "state-ownership-and-concurrency", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A05-F027", @@ -25521,11 +6657,9 @@ ], "sourceHeading": "82.1. P2 — cleanup crash-reclaim은 `MAXIMUM_ATTEMPTS`를 우회해 poison item을 무한 재시도할 수 있다", "summary": "cleanup crash-reclaim은 `MAXIMUM_ATTEMPTS`를 우회해 poison item을 무한 재시도할 수 있다", - "disposition": "PROMOTE", - "target": "case:a05-f027-maximum-attempts", - "reason": null, - "topic": "state-ownership-and-concurrency", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A05-F028", @@ -25536,11 +6670,9 @@ ], "sourceHeading": "89. P1 — provider 호출 뒤 recipient projection write가 lease fencing을 우회한다", "summary": "provider 호출 뒤 recipient projection write가 lease fencing을 우회한다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a05-f028", - "reason": null, - "topic": "state-ownership-and-concurrency", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A05-F029", @@ -25551,11 +6683,9 @@ ], "sourceHeading": "90. P2 — reconciliation `FOR UPDATE SKIP LOCKED`는 worker 처리 구간을 claim하지 않는다", "summary": "reconciliation `FOR UPDATE SKIP LOCKED`는 worker 처리 구간을 claim하지 않는다", - "disposition": "PROMOTE", - "target": "case:a05-f029-for-update-skip-locked", - "reason": null, - "topic": "state-ownership-and-concurrency", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A05-F030", @@ -25566,11 +6696,9 @@ ], "sourceHeading": "91. P2 — V8 atomic admin claim은 production service에 연결되지 않았고 completion 모델도 미완성이다", "summary": "V8 atomic admin claim은 production service에 연결되지 않았고 completion 모델도 미완성이다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a05-f030", - "reason": null, - "topic": "runtime-reachability-and-composition", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A05-F031", @@ -25581,11 +6709,9 @@ ], "sourceHeading": "116. Confirmed P2 — vendor selector의 fail-fast 계약이 shipped composition에 설치돼 있지 않다", "summary": "vendor selector의 fail-fast 계약이 shipped composition에 설치돼 있지 않다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a05-f031", - "reason": null, - "topic": "runtime-reachability-and-composition", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A05-F032", @@ -25596,11 +6722,9 @@ ], "sourceHeading": "125. Confirmed P2 — nightly workflow가 광고하는 세 가지 중 하나를 lane이 실제로 관측하지 않는다", "summary": "nightly workflow가 광고하는 세 가지 중 하나를 lane이 실제로 관측하지 않는다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a05-f032", - "reason": null, - "topic": "verification-path-coverage", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A05-F033", @@ -25611,11 +6735,9 @@ ], "sourceHeading": "132. Confirmed P1 — selected base card `jpa-flyway-migration`의 producer가 현재 revision에서 실패한다", "summary": "selected base card `jpa-flyway-migration`의 producer가 현재 revision에서 실패한다", - "disposition": "PROMOTE", - "target": "case:a05-f033-jpa-flyway-migration", - "reason": null, - "topic": "verification-path-coverage", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A05-F034", @@ -25626,11 +6748,9 @@ ], "sourceHeading": "133. Confirmed P2 — selected base card 3개의 evidence tag가 production code 없는 fixture로 충족된다", "summary": "selected base card 3개의 evidence tag가 production code 없는 fixture로 충족된다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a05-f034", - "reason": null, - "topic": "verification-path-coverage", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A06-F001", @@ -25655,11 +6775,9 @@ ], "sourceHeading": "5. Confirmed P3 — 폐기된 namespace guard의 탐색 domain이 operator가 읽는 두 문서를 덮지 않는다", "summary": "폐기된 namespace guard의 탐색 domain이 operator가 읽는 두 문서를 덮지 않는다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a06-f002", - "reason": null, - "topic": "declaration-and-document-drift", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A06-F003", @@ -25670,11 +6788,9 @@ ], "sourceHeading": "6. Confirmed P3 — `change-streams=true`는 거부되지 않고 조용히 버려지며, 그 결과 startup validator의 한 분기가 production에서 도달 불가다", "summary": "`change-streams=true`는 거부되지 않고 조용히 버려지며, 그 결과 startup validator의 한 분기가 production에서 도달 불가다", - "disposition": "PROMOTE", - "target": "case:a06-f003-change-streams-true", - "reason": null, - "topic": "runtime-reachability-and-composition", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A06-F004", @@ -25685,11 +6801,9 @@ ], "sourceHeading": "16. Confirmed P2 — schema version 실패는 두 경로 중 어느 쪽도 온전하지 않다", "summary": "schema version 실패는 두 경로 중 어느 쪽도 온전하지 않다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a06-f004", - "reason": null, - "topic": "schema-and-data-contracts", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A06-F005", @@ -25700,11 +6814,9 @@ ], "sourceHeading": "17. Confirmed P3 — 예외 계층의 \"cause를 붙이지 않는다\" 규칙에 문서화되지 않은 예외가 하나 있다", "summary": "예외 계층의 \"cause를 붙이지 않는다\" 규칙에 문서화되지 않은 예외가 하나 있다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a06-f005", - "reason": null, - "topic": "declaration-and-document-drift", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A06-F006", @@ -25729,11 +6841,9 @@ ], "sourceHeading": "25. Confirmed P2 — D3 gateway가 문서화한 검사 순서에 존재하지 않는 단계가 있다", "summary": "D3 gateway가 문서화한 검사 순서에 존재하지 않는 단계가 있다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a06-f007", - "reason": null, - "topic": "verification-path-coverage", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A06-F008", @@ -25744,11 +6854,9 @@ ], "sourceHeading": "32. Confirmed P2 — 서버 측 deadline이 경로마다 다르게 적용되고, 문서가 지목한 메커니즘은 production 호출자가 0이다", "summary": "서버 측 deadline이 경로마다 다르게 적용되고, 문서가 지목한 메커니즘은 production 호출자가 0이다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a06-f008", - "reason": null, - "topic": "runtime-reachability-and-composition", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A06-F009", @@ -25759,11 +6867,9 @@ ], "sourceHeading": "33. P3 — timeout 초과 경로가 한 observation에 success와 failure를 모두 기록한다", "summary": "timeout 초과 경로가 한 observation에 success와 failure를 모두 기록한다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a06-f009", - "reason": null, - "topic": "transport-and-provider-semantics", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A06-F010", @@ -25774,11 +6880,9 @@ ], "sourceHeading": "42. P2 — collection 이름 불변식이 aggregation executor의 서명에서 깨진다", "summary": "collection 이름 불변식이 aggregation executor의 서명에서 깨진다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a06-f010", - "reason": null, - "topic": "schema-and-data-contracts", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A06-F011", @@ -25789,11 +6893,9 @@ ], "sourceHeading": "43. P3 — `MongoRegexPolicy.forbidden()`은 금지하지 않는다", "summary": "`MongoRegexPolicy.forbidden()`은 금지하지 않는다", - "disposition": "PROMOTE", - "target": "case:a06-f011-mongoregexpolicy-forbidden", - "reason": null, - "topic": "runtime-contract-correctness", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A06-F012", @@ -25818,11 +6920,9 @@ ], "sourceHeading": "56. P2 — `recordApplied`는 문서화된 fence 계약을 구현하지 않고, 보호를 역전시킨다", "summary": "`recordApplied`는 문서화된 fence 계약을 구현하지 않고, 보호를 역전시킨다", - "disposition": "PROMOTE", - "target": "case:a06-f013-recordapplied", - "reason": null, - "topic": "state-ownership-and-concurrency", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A06-F014", @@ -25833,11 +6933,9 @@ ], "sourceHeading": "57. P2 — index diff가 실제로 비교하는 것은 두 필드뿐이다", "summary": "index diff가 실제로 비교하는 것은 두 필드뿐이다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a06-f014", - "reason": null, - "topic": "schema-and-data-contracts", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A06-F015", @@ -25848,11 +6946,9 @@ ], "sourceHeading": "58. P3 — TTL이 두 곳에 선언되고, 규칙을 가진 쪽은 아무도 쓰지 않는다", "summary": "TTL이 두 곳에 선언되고, 규칙을 가진 쪽은 아무도 쓰지 않는다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a06-f015", - "reason": null, - "topic": "schema-and-data-contracts", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A06-F016", @@ -25863,11 +6959,9 @@ ], "sourceHeading": "59. P3 — Flamingock lease로는 어떤 migration도 실행할 수 없고, javadoc은 다르게 적는다", "summary": "Flamingock lease로는 어떤 migration도 실행할 수 없고, javadoc은 다르게 적는다", - "disposition": "PROMOTE", - "target": "case:a06-f016-flamingock", - "reason": null, - "topic": "state-ownership-and-concurrency", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A06-F017", @@ -25878,10 +6972,9 @@ ], "sourceHeading": "67. P1 — high-water mark가 재전달된 이벤트를 삼켜, failover 중이던 변경이 조용히 영구 소실된다", "summary": "high-water mark가 재전달된 이벤트를 삼켜, failover 중이던 변경이 조용히 영구 소실된다", - "disposition": "MERGE_INTO", - "target": null, - "reason": "제1부 §4.2·§8 이 채택한 Mongo high-water mark 사건이다. 이 Topic 의 독자 질문에 답하지 않아 밖으로 내보냈고, 체크포인트 의미를 다루는 Topic 이 재선별될 때 처분한다", - "dispositionReview": "PENDING", + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED", "deferred": "이 Topic 밖으로 나가는 후보다. 소관 Topic 이 재선별될 때 처분한다" }, { @@ -25893,11 +6986,9 @@ ], "sourceHeading": "68. P2 — `changeStreams` flag는 `false`로 고정돼 있는데, 소비자 bean은 그것과 무관하게 조립된다", "summary": "`changeStreams` flag는 `false`로 고정돼 있는데, 소비자 bean은 그것과 무관하게 조립된다", - "disposition": "PROMOTE", - "target": "case:a06-f018-changestreams-false", - "reason": null, - "topic": "runtime-reachability-and-composition", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A06-F019", @@ -25908,11 +6999,9 @@ ], "sourceHeading": "69. P3 — recovery package에 쓰이는 어휘와 쓰이지 않는 어휘가 나란히 있다", "summary": "recovery package에 쓰이는 어휘와 쓰이지 않는 어휘가 나란히 있다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a06-f019", - "reason": null, - "topic": "state-ownership-and-concurrency", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A06-F020", @@ -25923,11 +7012,9 @@ ], "sourceHeading": "75. P1 — 프로파일의 TLS·타임아웃·풀·Stable API가 driver에 도달하지 않는다", "summary": "프로파일의 TLS·타임아웃·풀·Stable API가 driver에 도달하지 않는다", - "disposition": "PROMOTE", - "target": "case:a06-f020-tls-stable", - "reason": null, - "topic": "runtime-reachability-and-composition", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A06-F021", @@ -25938,11 +7025,9 @@ ], "sourceHeading": "76. P3 — admin gateway의 두 audit 경로 중 하나만 fail-closed다", "summary": "admin gateway의 두 audit 경로 중 하나만 fail-closed다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a06-f021", - "reason": null, - "topic": "verification-path-coverage", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A06-F022", @@ -25953,11 +7038,9 @@ ], "sourceHeading": "77. P3 — 태그 allowlist는 규약이지 강제가 아니다", "summary": "태그 allowlist는 규약이지 강제가 아니다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a06-f022", - "reason": null, - "topic": "runtime-contract-correctness", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A06-F023", @@ -25968,11 +7051,9 @@ ], "sourceHeading": "85. P2 — sharding admin gateway의 네 작업 중 셋은 어떤 입력으로도 완료될 수 없다", "summary": "sharding admin gateway의 네 작업 중 셋은 어떤 입력으로도 완료될 수 없다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a06-f023", - "reason": null, - "topic": "verification-path-coverage", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A06-F024", @@ -25983,11 +7064,9 @@ ], "sourceHeading": "86. P3 — promotion 증거 어휘가 둘이고, gate는 하나만 검사한다", "summary": "promotion 증거 어휘가 둘이고, gate는 하나만 검사한다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a06-f024", - "reason": null, - "topic": "verification-path-coverage", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A06-F025", @@ -25998,11 +7077,9 @@ ], "sourceHeading": "88. P3 — 구현 없는 4개의 계약 중 셋은 그 사실을 적고, 하나는 적지 않는다", "summary": "구현 없는 4개의 계약 중 셋은 그 사실을 적고, 하나는 적지 않는다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a06-f025", - "reason": null, - "topic": "runtime-contract-correctness", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A06-F026", @@ -26013,11 +7090,9 @@ ], "sourceHeading": "94. P2 — 커버리지 gate 둘이 나란히 있고, 하나는 발화할 수 없다", "summary": "커버리지 gate 둘이 나란히 있고, 하나는 발화할 수 없다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a06-f026", - "reason": null, - "topic": "verification-path-coverage", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A06-F027", @@ -26028,11 +7103,9 @@ ], "sourceHeading": "95. P2 — release gate가 실제로 차단하는 것은 hermetic test 3개이고, mongo용 CI workflow는 없다", "summary": "release gate가 실제로 차단하는 것은 hermetic test 3개이고, mongo용 CI workflow는 없다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a06-f027", - "reason": null, - "topic": "verification-path-coverage", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A06-F028", @@ -26043,11 +7116,9 @@ ], "sourceHeading": "96. P3 — 소비자가 없는 fixture 셋", "summary": "소비자가 없는 fixture 셋", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a06-f028", - "reason": null, - "topic": "verification-path-coverage", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A07-F001", @@ -26058,11 +7129,9 @@ ], "sourceHeading": "3. P2 — 모듈의 존재 논거인 `UuidCodec`에 production 소비자가 없다", "summary": "모듈의 존재 논거인 `UuidCodec`에 production 소비자가 없다", - "disposition": "PROMOTE", - "target": "case:a07-f001-uuidcodec", - "reason": null, - "topic": "runtime-reachability-and-composition", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A07-F002", @@ -26073,11 +7142,9 @@ ], "sourceHeading": "4. P2 — `normalize`는 canonical이 아닌 입력을 받아 다른 UUID로 조용히 바꾼다", "summary": "`normalize`는 canonical이 아닌 입력을 받아 다른 UUID로 조용히 바꾼다", - "disposition": "PROMOTE", - "target": "case:a07-f002-normalize", - "reason": null, - "topic": "contract-domain-and-bounds", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A07-F003", @@ -26102,11 +7169,9 @@ ], "sourceHeading": "6. P3 — CLAUDE.md의 의존성 서술이 세 항목 모두 틀렸다", "summary": "CLAUDE.md의 의존성 서술이 세 항목 모두 틀렸다", - "disposition": "PROMOTE", - "target": "case:a07-f004-claude", - "reason": null, - "topic": "declaration-and-document-drift", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A07-F005", @@ -26117,11 +7182,9 @@ ], "sourceHeading": "7. P3 — README의 세 가지 사실 오류", "summary": "README의 세 가지 사실 오류", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a07-f005", - "reason": null, - "topic": "declaration-and-document-drift", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A07-F006", @@ -26132,11 +7195,9 @@ ], "sourceHeading": "8. P3 — CLAUDE.md가 대는 두 가드 중 하나는 저장소에 없다", "summary": "CLAUDE.md가 대는 두 가드 중 하나는 저장소에 없다", - "disposition": "PROMOTE", - "target": "case:a07-f006-claude", - "reason": null, - "topic": "declaration-and-document-drift", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A08-F001", @@ -26161,11 +7222,9 @@ ], "sourceHeading": "5. P3 — R1과 R2의 설정 취급이 비대칭이고, 검증된 쪽은 하나뿐이다", "summary": "R1과 R2의 설정 취급이 비대칭이고, 검증된 쪽은 하나뿐이다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a08-f002", - "reason": null, - "topic": "verification-path-coverage", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A08-F003", @@ -26176,11 +7235,9 @@ ], "sourceHeading": "6. P3 — 문서가 지목한 기본값 위치와 test 목록이 실제와 다르다", "summary": "문서가 지목한 기본값 위치와 test 목록이 실제와 다르다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a08-f003", - "reason": null, - "topic": "verification-path-coverage", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A08-F004", @@ -26191,11 +7248,9 @@ ], "sourceHeading": "32. P3 — 발행 rename만 경로 기반이고, 그것을 지키는 것은 이 모듈이 \"근사에 불과하다\"고 적은 사전검사다", "summary": "발행 rename만 경로 기반이고, 그것을 지키는 것은 이 모듈이 \"근사에 불과하다\"고 적은 사전검사다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a08-f004", - "reason": null, - "topic": "runtime-contract-correctness", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A08-F005", @@ -26206,11 +7261,9 @@ ], "sourceHeading": "34. P3 — `TransferBufferPool.maxBorrowedBytes()`가 자기 회귀 test를 지목하는데 그 test가 읽지 않는다", "summary": "`TransferBufferPool.maxBorrowedBytes()`가 자기 회귀 test를 지목하는데 그 test가 읽지 않는다", - "disposition": "PROMOTE", - "target": "case:a08-f005-transferbufferpool-maxborrowedbytes", - "reason": null, - "topic": "verification-path-coverage", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A08-F006", @@ -26235,11 +7288,9 @@ ], "sourceHeading": "5. P3 — production 판정이 두 개의 리터럴 프로파일 이름에 걸려 있다", "summary": "production 판정이 두 개의 리터럴 프로파일 이름에 걸려 있다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a09-f001", - "reason": null, - "topic": "runtime-reachability-and-composition", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A09-F002", @@ -26250,9 +7301,8 @@ ], "sourceHeading": "38. P2 — 직접 multipart의 마지막 part는 grant를 받을 수 없다", "summary": "직접 multipart의 마지막 part는 grant를 받을 수 없다", - "disposition": "MERGE_INTO", - "target": "case:the-last-part-cannot-get-a-grant", - "reason": "기존 Root Tree Case가 이 finding의 동일 사건과 검증 단위를 명시적으로 포함한다. 관련 Topic/Reference가 아니라 사건 자체가 같은 경우에만 병합했다.", + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", "dispositionReview": "CONFIRMED" }, { @@ -26264,9 +7314,8 @@ ], "sourceHeading": "39. P2 — 서명된 grant의 endpoint 검증이 upload 경로에만 있다", "summary": "서명된 grant의 endpoint 검증이 upload 경로에만 있다", - "disposition": "MERGE_INTO", - "target": "case:endpoint-check-only-on-upload", - "reason": "기존 Root Tree Case가 이 finding의 동일 사건과 검증 단위를 명시적으로 포함한다. 관련 Topic/Reference가 아니라 사건 자체가 같은 경우에만 병합했다.", + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", "dispositionReview": "CONFIRMED" }, { @@ -26278,11 +7327,9 @@ ], "sourceHeading": "41. P2 — 그러나 R0 경계가 문서에만 있고 compile 경로에서 닫히지 않는다", "summary": "그러나 R0 경계가 문서에만 있고 compile 경로에서 닫히지 않는다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a09-f004", - "reason": null, - "topic": "declaration-and-document-drift", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A09-F005", @@ -26307,11 +7354,9 @@ ], "sourceHeading": "50. P3 — nonce replay 경계가 결과를 읽고 버린다", "summary": "nonce replay 경계가 결과를 읽고 버린다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a09-f006", - "reason": null, - "topic": "state-ownership-and-concurrency", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A10-F001", @@ -26322,11 +7367,9 @@ ], "sourceHeading": "5. P2 — README readiness 표와 build.gradle 주석이 실제 소스와 어긋난다", "summary": "README readiness 표와 build.gradle 주석이 실제 소스와 어긋난다", - "disposition": "PROMOTE", - "target": "case:a10-f001-readme", - "reason": null, - "topic": "verification-path-coverage", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A10-F002", @@ -26351,11 +7394,9 @@ ], "sourceHeading": "15. P2 — SDK가 선언한 두 진입점에 구현이 없다", "summary": "SDK가 선언한 두 진입점에 구현이 없다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a10-f003", - "reason": null, - "topic": "runtime-reachability-and-composition", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A10-F004", @@ -26366,11 +7407,9 @@ ], "sourceHeading": "16. P3 — Pub/Sub 채널만 렌더 크기 검증을 받지 않는다", "summary": "Pub/Sub 채널만 렌더 크기 검증을 받지 않는다", - "disposition": "PROMOTE", - "target": "case:a10-f004-pub-sub", - "reason": null, - "topic": "verification-path-coverage", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A10-F005", @@ -26381,11 +7420,9 @@ ], "sourceHeading": "17. P3 — 다중 키 fan-in 중 HyperLogLog `merge`만 budget이 없다", "summary": "다중 키 fan-in 중 HyperLogLog `merge`만 budget이 없다", - "disposition": "PROMOTE", - "target": "case:a10-f005-hyperloglog-merge", - "reason": null, - "topic": "contract-domain-and-bounds", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A10-F006", @@ -26396,11 +7433,9 @@ ], "sourceHeading": "26. P3 — `requireIdentifier`의 다섯 검사 중 둘은 도달할 수 없다", "summary": "`requireIdentifier`의 다섯 검사 중 둘은 도달할 수 없다", - "disposition": "PROMOTE", - "target": "case:a10-f006-requireidentifier", - "reason": null, - "topic": "contract-domain-and-bounds", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A10-F007", @@ -26411,11 +7446,9 @@ ], "sourceHeading": "36. P3 — 패턴 구독의 R2 승인만 호출자가 아니라 배포에 대해 이루어진다", "summary": "패턴 구독의 R2 승인만 호출자가 아니라 배포에 대해 이루어진다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a10-f007", - "reason": null, - "topic": "runtime-contract-correctness", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A10-F008", @@ -26426,11 +7459,9 @@ ], "sourceHeading": "37. P3 — permit 정책 이름이 세 곳에 문자열로 존재하고 교차 검사가 없다", "summary": "permit 정책 이름이 세 곳에 문자열로 존재하고 교차 검사가 없다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a10-f008", - "reason": null, - "topic": "runtime-contract-correctness", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A10-F009", @@ -26483,11 +7514,9 @@ ], "sourceHeading": "4. P3 — `close()`가 실패하면 drain 스케줄러 스레드가 남는다", "summary": "`close()`가 실패하면 drain 스케줄러 스레드가 남는다", - "disposition": "PROMOTE", - "target": "case:a11-f001-close", - "reason": null, - "topic": "runtime-contract-correctness", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A11-F002", @@ -26498,11 +7527,9 @@ ], "sourceHeading": "5. P3 — `POOL_ROUTE_EXCEEDS_TOTAL` 위반 코드는 발화할 수 없다", "summary": "`POOL_ROUTE_EXCEEDS_TOTAL` 위반 코드는 발화할 수 없다", - "disposition": "PROMOTE", - "target": "case:a11-f002-pool-route-exceeds-total", - "reason": null, - "topic": "runtime-contract-correctness", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A11-F003", @@ -26513,11 +7540,9 @@ ], "sourceHeading": "6. P3 — 위반 코드 34종 중 22종이 어떤 test에서도 이름으로 확인되지 않는다", "summary": "위반 코드 34종 중 22종이 어떤 test에서도 이름으로 확인되지 않는다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a11-f003", - "reason": null, - "topic": "verification-path-coverage", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A11-F004", @@ -26528,11 +7553,9 @@ ], "sourceHeading": "14. P3 — `Number`가 허용 목록에 있어 가변 숫자 타입이 REPLAYABLE로 인증된다", "summary": "`Number`가 허용 목록에 있어 가변 숫자 타입이 REPLAYABLE로 인증된다", - "disposition": "PROMOTE", - "target": "case:a11-f004-number", - "reason": null, - "topic": "state-ownership-and-concurrency", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A11-F005", @@ -26543,9 +7566,8 @@ ], "sourceHeading": "22. P2 — 로컬 거부 경로에서 회로 브레이커 permission이 반환되지 않는다", "summary": "로컬 거부 경로에서 회로 브레이커 permission이 반환되지 않는다", - "disposition": "MERGE_INTO", - "target": "case:a-circuit-breaker-permit-that-leaks-on-local-rejection", - "reason": "기존 Root Tree Case가 이 finding의 동일 사건과 검증 단위를 명시적으로 포함한다. 관련 Topic/Reference가 아니라 사건 자체가 같은 경우에만 병합했다.", + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", "dispositionReview": "CONFIRMED" }, { @@ -26557,11 +7579,9 @@ ], "sourceHeading": "30. P3 — `BoundedDataBufferFlux`의 두 연산자가 이름만 있고 아무것도 하지 않는다", "summary": "`BoundedDataBufferFlux`의 두 연산자가 이름만 있고 아무것도 하지 않는다", - "disposition": "PROMOTE", - "target": "case:a11-f006-boundeddatabufferflux", - "reason": null, - "topic": "contract-domain-and-bounds", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A11-F007", @@ -26572,11 +7592,9 @@ ], "sourceHeading": "47. P3 — 동적 대상 DNS 핀 능력 검사가 블로킹 오버로드에만 있다", "summary": "동적 대상 DNS 핀 능력 검사가 블로킹 오버로드에만 있다", - "disposition": "PROMOTE", - "target": "case:a11-f007-dns", - "reason": null, - "topic": "transport-and-provider-semantics", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A11-F008", @@ -26601,11 +7619,9 @@ ], "sourceHeading": "3. P2 — `check`에 붙은 `verifyJsonSchemaRuntimeGraph`가 실행되면 실패한다", "summary": "`check`에 붙은 `verifyJsonSchemaRuntimeGraph`가 실행되면 실패한다", - "disposition": "PROMOTE", - "target": "case:a12-f001-check-verifyjsonschemaruntimegraph", - "reason": null, - "topic": "schema-and-data-contracts", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A12-F002", @@ -26616,11 +7632,9 @@ ], "sourceHeading": "4. P3 — README의 `jackson-databind` 부재 주장이 현재 상태와 어긋난다", "summary": "README의 `jackson-databind` 부재 주장이 현재 상태와 어긋난다", - "disposition": "PROMOTE", - "target": "case:a12-f002-jackson-databind", - "reason": null, - "topic": "declaration-and-document-drift", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A13-F001", @@ -26645,11 +7659,9 @@ ], "sourceHeading": "P2 — `AUTHENTICATION_FAILED`를 지우지 않는다는 `resumeHealthy`의 보장이, 관리자 평면에 노출된 2단계 시퀀스로 우회된다", "summary": "`AUTHENTICATION_FAILED`를 지우지 않는다는 `resumeHealthy`의 보장이, 관리자 평면에 노출된 2단계 시퀀스로 우회된다", - "disposition": "PROMOTE", - "target": "case:a13-f002-authentication-failed-resumehealthy", - "reason": null, - "topic": "runtime-contract-correctness", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A13-F003", @@ -26660,11 +7672,9 @@ ], "sourceHeading": "17.1 P2 — \"모든 reveal은 감사된다\"고 선언한 `AccessContext`를 읽는 코드가 저장소에 하나도 없다", "summary": "\"모든 reveal은 감사된다\"고 선언한 `AccessContext`를 읽는 코드가 저장소에 하나도 없다", - "disposition": "PROMOTE", - "target": "case:a13-f003-accesscontext", - "reason": null, - "topic": "security-policy-enforcement", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A13-F004", @@ -26675,11 +7685,9 @@ ], "sourceHeading": "17.2 P2 — Thymeleaf 예외 메시지 삭제 가드가 프로덕션이 타지 않는 오버로드에만 있다", "summary": "Thymeleaf 예외 메시지 삭제 가드가 프로덕션이 타지 않는 오버로드에만 있다", - "disposition": "PROMOTE", - "target": "case:a13-f004-thymeleaf", - "reason": null, - "topic": "runtime-contract-correctness", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A13-F005", @@ -26690,11 +7698,9 @@ ], "sourceHeading": "21.1 P3 — 음수 `Retry-After` 헤더가 throttle 결과 대신 `IllegalArgumentException`을 만든다", "summary": "음수 `Retry-After` 헤더가 throttle 결과 대신 `IllegalArgumentException`을 만든다", - "disposition": "PROMOTE", - "target": "case:a13-f005-retry-after-illegalargumentexception", - "reason": null, - "topic": "schema-and-data-contracts", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A13-F006", @@ -26719,11 +7725,9 @@ ], "sourceHeading": "25.2 P2 — \"상한을 두고 읽는다\"는 본문 핸들러가 전부 읽은 뒤에 자른다", "summary": "\"상한을 두고 읽는다\"는 본문 핸들러가 전부 읽은 뒤에 자른다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a13-f007", - "reason": null, - "topic": "contract-domain-and-bounds", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A13-F008", @@ -26734,11 +7738,9 @@ ], "sourceHeading": "25.3 P3 — SigV4가 서명한 `host`에 포트가 없어, 기본 포트가 아닌 엔드포인트에서 서명이 어긋난다", "summary": "SigV4가 서명한 `host`에 포트가 없어, 기본 포트가 아닌 엔드포인트에서 서명이 어긋난다", - "disposition": "PROMOTE", - "target": "case:a13-f008-host", - "reason": null, - "topic": "transport-and-provider-semantics", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A13-F009", @@ -26749,11 +7751,9 @@ ], "sourceHeading": "25.4 P3 — SigV4 서명 키 파생이 비밀을 지울 수 없는 `String`으로 승격시킨다", "summary": "SigV4 서명 키 파생이 비밀을 지울 수 없는 `String`으로 승격시킨다", - "disposition": "PROMOTE", - "target": "case:a13-f009-sigv4-string", - "reason": null, - "topic": "transport-and-provider-semantics", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A13-F010", @@ -26764,11 +7764,9 @@ ], "sourceHeading": "29.1 P2 — FCM만 \"커밋 후 응답 손실 = ambiguous\" 규칙 밖에 있고, 그 FCM이 두 계약 집합 어디에도 없다", "summary": "FCM만 \"커밋 후 응답 손실 = ambiguous\" 규칙 밖에 있고, 그 FCM이 두 계약 집합 어디에도 없다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a13-f010", - "reason": null, - "topic": "transport-and-provider-semantics", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A13-F011", @@ -26779,11 +7777,9 @@ ], "sourceHeading": "29.2 P3 — 공유 provider 계약이 8종 중 3종에서만 상속되고, 강제 장치가 없다", "summary": "공유 provider 계약이 8종 중 3종에서만 상속되고, 강제 장치가 없다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a13-f011", - "reason": null, - "topic": "transport-and-provider-semantics", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A14-F001", @@ -26808,11 +7804,9 @@ ], "sourceHeading": "8.2 P3 — `WebProblemSanitizer.alreadySafe`가 죽은 메서드이고 그 안의 조건도 죽어 있다", "summary": "`WebProblemSanitizer.alreadySafe`가 죽은 메서드이고 그 안의 조건도 죽어 있다", - "disposition": "PROMOTE", - "target": "case:a14-f002-webproblemsanitizer-alreadysafe", - "reason": null, - "topic": "runtime-contract-correctness", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A14-F003", @@ -26823,11 +7817,9 @@ ], "sourceHeading": "12.1 P1 — 플랫폼 요청 컨텍스트가 서블릿에는 생산자가 없고, 리액티브에는 익명 액터로 고정되어 있다", "summary": "플랫폼 요청 컨텍스트가 서블릿에는 생산자가 없고, 리액티브에는 익명 액터로 고정되어 있다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a14-f003", - "reason": null, - "topic": "runtime-contract-correctness", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A14-F004", @@ -26838,11 +7830,9 @@ ], "sourceHeading": "12.2 P2 — 프레임워크 자유 신원 모델과 교차 테넌트 가드가 프로덕션에서 한 번도 참조되지 않는다", "summary": "프레임워크 자유 신원 모델과 교차 테넌트 가드가 프로덕션에서 한 번도 참조되지 않는다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a14-f004", - "reason": null, - "topic": "runtime-contract-correctness", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A14-F005", @@ -26853,11 +7843,9 @@ ], "sourceHeading": "12.3 P3 — `publicPaths`가 `RestrictedPathRule`보다 먼저 등록되어, 넓은 공개 경로 하나가 관리 평면 규칙을 조용히 덮는다", "summary": "`publicPaths`가 `RestrictedPathRule`보다 먼저 등록되어, 넓은 공개 경로 하나가 관리 평면 규칙을 조용히 덮는다", - "disposition": "PROMOTE", - "target": "case:a14-f005-publicpaths-restrictedpathrule", - "reason": null, - "topic": "security-policy-enforcement", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A14-F006", @@ -26868,11 +7856,9 @@ ], "sourceHeading": "16.1 P1 — 용량 보호 계층 전체(41 main files)가 자기 테스트 픽스처 안에서만 실행된다", "summary": "용량 보호 계층 전체(41 main files)가 자기 테스트 픽스처 안에서만 실행된다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a14-f006", - "reason": null, - "topic": "verification-path-coverage", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A14-F007", @@ -26883,11 +7869,9 @@ ], "sourceHeading": "16.2 P2 — 리액티브 전송에는 속도 제한 경로가 하나도 없다", "summary": "리액티브 전송에는 속도 제한 경로가 하나도 없다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a14-f007", - "reason": null, - "topic": "runtime-contract-correctness", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A14-F008", @@ -26898,11 +7882,9 @@ ], "sourceHeading": "20.1 P1 — 멱등 실행 계층과 durable-operation 표면이 픽스처에서만 조립된다", "summary": "멱등 실행 계층과 durable-operation 표면이 픽스처에서만 조립된다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a14-f008", - "reason": null, - "topic": "runtime-reachability-and-composition", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A14-F009", @@ -26913,11 +7895,9 @@ ], "sourceHeading": "20.3 P3 — 의미 지문이 길이 프레이밍 없이 구분자로 만들어진다", "summary": "의미 지문이 길이 프레이밍 없이 구분자로 만들어진다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a14-f009", - "reason": null, - "topic": "contract-domain-and-bounds", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A14-F010", @@ -26928,11 +7908,9 @@ ], "sourceHeading": "24.1 P2 — 배선된 캐시 필터의 `no-store`가 배선된 조건부 읽기 경로를 무력화하고, 둘을 조정하려고 만든 패키지는 참조 0이다", "summary": "배선된 캐시 필터의 `no-store`가 배선된 조건부 읽기 경로를 무력화하고, 둘을 조정하려고 만든 패키지는 참조 0이다", - "disposition": "PROMOTE", - "target": "case:a14-f010-no-store", - "reason": null, - "topic": "runtime-reachability-and-composition", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A14-F011", @@ -26943,11 +7921,9 @@ ], "sourceHeading": "28.1 P2 — `maxArrayElements`가 선언만 되고 강제되지 않으며, 바이트 예산 백스톱도 없다", "summary": "`maxArrayElements`가 선언만 되고 강제되지 않으며, 바이트 예산 백스톱도 없다", - "disposition": "PROMOTE", - "target": "case:a14-f011-maxarrayelements", - "reason": null, - "topic": "contract-domain-and-bounds", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A14-F012", @@ -26986,11 +7962,9 @@ ], "sourceHeading": "36.1 P2 — 선언된 Advanced 능력 11개 중 9개는 켜는 방법이 없다", "summary": "선언된 Advanced 능력 11개 중 9개는 켜는 방법이 없다", - "disposition": "PROMOTE", - "target": "case:a14-f014-advanced", - "reason": null, - "topic": "runtime-contract-correctness", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A14-F015", @@ -27001,11 +7975,9 @@ ], "sourceHeading": "36.2 P3 — `VirtualThreadProfile.propertyName()`이 아무것도 게이트하지 않는 이름을 반환한다", "summary": "`VirtualThreadProfile.propertyName()`이 아무것도 게이트하지 않는 이름을 반환한다", - "disposition": "PROMOTE", - "target": "case:a14-f015-virtualthreadprofile-propertyname", - "reason": null, - "topic": "runtime-reachability-and-composition", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A14-F016", @@ -27016,11 +7988,9 @@ ], "sourceHeading": "40.1 P1 — 이 leaf의 리액티브 절반 29개 파일은 어떤 출하 배포에서도 활성화될 수 없다", "summary": "이 leaf의 리액티브 절반 29개 파일은 어떤 출하 배포에서도 활성화될 수 없다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a14-f016", - "reason": null, - "topic": "runtime-reachability-and-composition", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A14-F017", @@ -27031,11 +8001,9 @@ ], "sourceHeading": "44.1 P3 — `SpringMvcRouteInventoryCollector` 138줄에 참조가 하나도 없다", "summary": "`SpringMvcRouteInventoryCollector` 138줄에 참조가 하나도 없다", - "disposition": "PROMOTE", - "target": "case:a14-f017-springmvcrouteinventorycollector", - "reason": null, - "topic": "runtime-reachability-and-composition", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A14-F018", @@ -27046,11 +8014,9 @@ ], "sourceHeading": "44.2 P3 — `WebPlatformStartupValidator`가 시작 시 실행되지 않는다", "summary": "`WebPlatformStartupValidator`가 시작 시 실행되지 않는다", - "disposition": "PROMOTE", - "target": "case:a14-f018-webplatformstartupvalidator", - "reason": null, - "topic": "runtime-reachability-and-composition", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A14-F019", @@ -27061,11 +8027,9 @@ ], "sourceHeading": "48.1 P1 — 크로스 스택 게이트가 검증하는 조립은 픽스처의 조립이고, 플랫폼의 조립이 아니다", "summary": "크로스 스택 게이트가 검증하는 조립은 픽스처의 조립이고, 플랫폼의 조립이 아니다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a14-f019", - "reason": null, - "topic": "verification-path-coverage", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A15-F001", @@ -27076,11 +8040,9 @@ ], "sourceHeading": "4.1 P2 — 원인 사슬 순회가 2-순환에서 무한 루프에 빠지고, 저장소는 이미 그 사례를 이름으로 적어 두었다", "summary": "원인 사슬 순회가 2-순환에서 무한 루프에 빠지고, 저장소는 이미 그 사례를 이름으로 적어 두었다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a15-f001", - "reason": null, - "topic": "runtime-contract-correctness", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A15-F002", @@ -27091,11 +8053,9 @@ ], "sourceHeading": "4.2 P3 — 설정 바인딩이 마스터 스위치 밖에서 일어난다. 컴포지션 루트의 자기 규칙과 어긋난다", "summary": "설정 바인딩이 마스터 스위치 밖에서 일어난다. 컴포지션 루트의 자기 규칙과 어긋난다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a15-f002", - "reason": null, - "topic": "runtime-reachability-and-composition", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A16-F001", @@ -27106,11 +8066,9 @@ ], "sourceHeading": "8.1 P2 — 스키마 조립·계약 정체성·해시 사슬이 통째로 미배선이고, 그것을 발행할 액추에이터 엔드포인트도 등록되지 않는다", "summary": "스키마 조립·계약 정체성·해시 사슬이 통째로 미배선이고, 그것을 발행할 액추에이터 엔드포인트도 등록되지 않는다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a16-f001", - "reason": null, - "topic": "runtime-reachability-and-composition", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A16-F002", @@ -27121,11 +8079,9 @@ ], "sourceHeading": "8.2 P3 — `@oneOf` 게이트와 런타임 검증기가 미배선이고, \"플랫폼이 강제한다\"는 서술이 그것을 넘어선다", "summary": "`@oneOf` 게이트와 런타임 검증기가 미배선이고, \"플랫폼이 강제한다\"는 서술이 그것을 넘어선다", - "disposition": "PROMOTE", - "target": "case:a16-f002-oneof", - "reason": null, - "topic": "verification-path-coverage", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A16-F003", @@ -27136,11 +8092,9 @@ ], "sourceHeading": "12.1 P2 — 5계층 예산 모델에서 요청 계층만 강제되고, 나머지 파생이 전부 미배선이다", "summary": "5계층 예산 모델에서 요청 계층만 강제되고, 나머지 파생이 전부 미배선이다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a16-f003", - "reason": null, - "topic": "runtime-reachability-and-composition", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A16-F004", @@ -27151,11 +8105,9 @@ ], "sourceHeading": "12.2 P3 — 연산 이름 정책의 두 구현 중 하나만 배선되고, 미배선 쪽만 `GraphQlOperationNamePolicy`를 쓴다", "summary": "연산 이름 정책의 두 구현 중 하나만 배선되고, 미배선 쪽만 `GraphQlOperationNamePolicy`를 쓴다", - "disposition": "PROMOTE", - "target": "case:a16-f004-graphqloperationnamepolicy", - "reason": null, - "topic": "runtime-reachability-and-composition", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A16-F005", @@ -27166,11 +8118,9 @@ ], "sourceHeading": "16.1 P2 — 설정으로 정한 파서 한계가 graphql-java에 설치되지 않는다", "summary": "설정으로 정한 파서 한계가 graphql-java에 설치되지 않는다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a16-f005", - "reason": null, - "topic": "runtime-reachability-and-composition", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A16-F006", @@ -27181,11 +8131,9 @@ ], "sourceHeading": "16.2 P2 — 프로파일별 정책 매니페스트가 미배선이라, 자격에서 해석된 프로파일이 아무 예산도 선택하지 않는다", "summary": "프로파일별 정책 매니페스트가 미배선이라, 자격에서 해석된 프로파일이 아무 예산도 선택하지 않는다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a16-f006", - "reason": null, - "topic": "runtime-reachability-and-composition", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A16-F007", @@ -27210,11 +8158,9 @@ ], "sourceHeading": "20.2 P3 — 파싱·검증 실패에 플랫폼 매퍼가 없다", "summary": "파싱·검증 실패에 플랫폼 매퍼가 없다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a16-f008", - "reason": null, - "topic": "verification-path-coverage", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A16-F009", @@ -27253,11 +8199,9 @@ ], "sourceHeading": "29.2 P3 — 기계가 읽는 능력 매니페스트와 사람이 읽는 등급표가 커서 서명에 대해 다르게 답한다", "summary": "기계가 읽는 능력 매니페스트와 사람이 읽는 등급표가 커서 서명에 대해 다르게 답한다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a16-f011", - "reason": null, - "topic": "contract-domain-and-bounds", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A16-F012", @@ -27268,11 +8212,9 @@ ], "sourceHeading": "39.1 P3 — \"기본 비활성\"은 존재하지 않는 스위치의 기본값을 서술한다", "summary": "\"기본 비활성\"은 존재하지 않는 스위치의 기본값을 서술한다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a16-f012", - "reason": null, - "topic": "runtime-reachability-and-composition", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A17-F001", @@ -27297,11 +8239,9 @@ ], "sourceHeading": "13.1 P2 — 연결 티켓·origin 정책·메시지 권한·연결 예산이 요청 경로 밖이고, 그중 일부는 STOMP 어댑터가 다른 방식으로 대체한다", "summary": "연결 티켓·origin 정책·메시지 권한·연결 예산이 요청 경로 밖이고, 그중 일부는 STOMP 어댑터가 다른 방식으로 대체한다", - "disposition": "PROMOTE", - "target": "case:a17-f002-stomp", - "reason": null, - "topic": "security-policy-enforcement", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A17-F003", @@ -27312,11 +8252,9 @@ ], "sourceHeading": "22.1 P3 — 능력 프로퍼티 이름을 만드는 코드와 실제 게이트가 다른 접두사를 쓴다", "summary": "능력 프로퍼티 이름을 만드는 코드와 실제 게이트가 다른 접두사를 쓴다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a17-f003", - "reason": null, - "topic": "declaration-and-document-drift", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A18-F001", @@ -27327,11 +8265,9 @@ ], "sourceHeading": "4.1b P3 — 출하되는 web 어댑터의 스위치가 활성화 모델 밖에 있다", "summary": "출하되는 web 어댑터의 스위치가 활성화 모델 밖에 있다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a18-f001", - "reason": null, - "topic": "runtime-reachability-and-composition", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A18-F002", @@ -27342,11 +8278,9 @@ ], "sourceHeading": "26.1 P3 — 실패는 환경 원인이며, 그 테스트의 도구 가드가 불완전하다", "summary": "실패는 환경 원인이며, 그 테스트의 도구 가드가 불완전하다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a18-f002", - "reason": null, - "topic": "verification-path-coverage", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A19-F001", @@ -27357,11 +8291,9 @@ ], "sourceHeading": "3.4 P2 — capability 12개 중 main 코드가 읽는 것은 3개, 거부하는 것은 1개", "summary": "capability 12개 중 main 코드가 읽는 것은 3개, 거부하는 것은 1개", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a19-f001", - "reason": null, - "topic": "runtime-contract-correctness", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A19-F002", @@ -27372,11 +8304,9 @@ ], "sourceHeading": "3.5 P2 — 8개 profile validator 중 조립에서 실행되는 것은 3개", "summary": "8개 profile validator 중 조립에서 실행되는 것은 3개", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a19-f002", - "reason": null, - "topic": "runtime-reachability-and-composition", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A19-F003", @@ -27387,11 +8317,9 @@ ], "sourceHeading": "3.6 P3 — `messaging-reliability-api`는 main 13파일 · 817 LOC에 테스트가 0개다", "summary": "`messaging-reliability-api`는 main 13파일 · 817 LOC에 테스트가 0개다", - "disposition": "PROMOTE", - "target": "case:a19-f003-messaging-reliability-api", - "reason": null, - "topic": "verification-path-coverage", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A19-F004", @@ -27402,11 +8330,9 @@ ], "sourceHeading": "4.3 P2 — 스키마 호환성 검증기는 출하 leaf에 있고, main 코드에서 호출되지 않는다", "summary": "스키마 호환성 검증기는 출하 leaf에 있고, main 코드에서 호출되지 않는다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a19-f004", - "reason": null, - "topic": "verification-path-coverage", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A19-F005", @@ -27417,11 +8343,9 @@ ], "sourceHeading": "4.4 P2 — 호환성 게이트를 가진 두 포맷은 build-only이고, 출하되는 유일한 코덱에는 게이트가 없다", "summary": "호환성 게이트를 가진 두 포맷은 build-only이고, 출하되는 유일한 코덱에는 게이트가 없다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a19-f005", - "reason": null, - "topic": "runtime-contract-correctness", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A19-F006", @@ -27432,11 +8356,9 @@ ], "sourceHeading": "4.5 P2 — `messaging-cloudevents`는 출하 leaf이고 starter의 의존이며 소비자가 없다", "summary": "`messaging-cloudevents`는 출하 leaf이고 starter의 의존이며 소비자가 없다", - "disposition": "PROMOTE", - "target": "case:a19-f006-messaging-cloudevents", - "reason": null, - "topic": "runtime-reachability-and-composition", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A19-F007", @@ -27461,11 +8383,9 @@ ], "sourceHeading": "5.2 P2 — 브로커 ACL 매니페스트의 자기 점검이 존재하지 않는다", "summary": "브로커 ACL 매니페스트의 자기 점검이 존재하지 않는다", - "disposition": "PROMOTE", - "target": "case:a19-f008-acl", - "reason": null, - "topic": "runtime-contract-correctness", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A19-F009", @@ -27476,11 +8396,9 @@ ], "sourceHeading": "5.3 P3 — 접근 검사가 두 갈래로 존재하고, 조립된 쪽이 진단이 약한 쪽이다 (§8.3)", "summary": "접근 검사가 두 갈래로 존재하고, 조립된 쪽이 진단이 약한 쪽이다 (§8.3)", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a19-f009", - "reason": null, - "topic": "runtime-reachability-and-composition", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A19-F010", @@ -27533,11 +8451,9 @@ ], "sourceHeading": "6.4 P2 — 지원 매트릭스가 \"모든 messaging leaf는 build-only\"라고 적고, 가족 권위 문서는 그 문장이 틀렸다고 이미 기록했다", "summary": "지원 매트릭스가 \"모든 messaging leaf는 build-only\"라고 적고, 가족 권위 문서는 그 문장이 틀렸다고 이미 기록했다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a19-f013", - "reason": null, - "topic": "declaration-and-document-drift", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A19-F014", @@ -27548,11 +8464,9 @@ ], "sourceHeading": "6.5 P2 — 한 아티팩트 안의 서로 모르는 Kafka 스택 두 개 (MSG-015, 가족 문서가 미해결로 표시)", "summary": "한 아티팩트 안의 서로 모르는 Kafka 스택 두 개 (MSG-015, 가족 문서가 미해결로 표시)", - "disposition": "PROMOTE", - "target": "case:a19-f014-kafka-msg", - "reason": null, - "topic": "declaration-and-document-drift", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A19-F015", @@ -27563,11 +8477,9 @@ ], "sourceHeading": "6.7 P3 — `CompatibilityMatrix`에 `EXTENSION` 등급이 있고 항목이 없으며, bridge leaf가 표 밖에 있다", "summary": "`CompatibilityMatrix`에 `EXTENSION` 등급이 있고 항목이 없으며, bridge leaf가 표 밖에 있다", - "disposition": "PROMOTE", - "target": "case:a19-f015-compatibilitymatrix-extension", - "reason": null, - "topic": "declaration-and-document-drift", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A19-F016", @@ -27606,11 +8518,9 @@ ], "sourceHeading": "7.4 P3 — claim-check는 starter에 배선 코드가 한 줄도 없다", "summary": "claim-check는 starter에 배선 코드가 한 줄도 없다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a19-f018", - "reason": null, - "topic": "runtime-reachability-and-composition", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A19-F019", @@ -27621,11 +8531,9 @@ ], "sourceHeading": "8.2 P2 — admin 스위치가 가드를 켜고 서비스는 켜지 않는다", "summary": "admin 스위치가 가드를 켜고 서비스는 켜지 않는다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a19-f019", - "reason": null, - "topic": "runtime-reachability-and-composition", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A19-F020", @@ -27636,11 +8544,9 @@ ], "sourceHeading": "8.3 P3 — `messaging-admin-api`는 main 25파일 · 1,613 LOC에 테스트 파일이 1개다", "summary": "`messaging-admin-api`는 main 25파일 · 1,613 LOC에 테스트 파일이 1개다", - "disposition": "PROMOTE", - "target": "case:a19-f020-messaging-admin-api", - "reason": null, - "topic": "verification-path-coverage", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A19-F021", @@ -27665,11 +8571,9 @@ ], "sourceHeading": "9.5 P3 — `MessagingPublicSurfaceContractTest`가 가족 밖(app-bootstrap)에 있다", "summary": "`MessagingPublicSurfaceContractTest`가 가족 밖(app-bootstrap)에 있다", - "disposition": "PROMOTE", - "target": "case:a19-f022-messagingpublicsurfacecontracttest", - "reason": null, - "topic": "verification-path-coverage", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A20-F001", @@ -27708,11 +8612,9 @@ ], "sourceHeading": "3.3 P2 — 증거 등급 모델 전체가 자동 실행 경로 밖에 있고, CLAUDE.md는 현재 시제로 서술한다", "summary": "증거 등급 모델 전체가 자동 실행 경로 밖에 있고, CLAUDE.md는 현재 시제로 서술한다", - "disposition": "PROMOTE", - "target": "case:a20-f003-claude", - "reason": null, - "topic": "declaration-and-document-drift", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A20-F004", @@ -27723,11 +8625,9 @@ ], "sourceHeading": "3.4 P2 — 조립 경계가 정책 객체 9개를 만들고 서버를 만들지 않는다", "summary": "조립 경계가 정책 객체 9개를 만들고 서버를 만들지 않는다", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a20-f004", - "reason": null, - "topic": "runtime-reachability-and-composition", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A20-F005", @@ -27738,11 +8638,9 @@ ], "sourceHeading": "3.5 P3 — 저장소 어디에도 참조가 없는 타입 3개", "summary": "저장소 어디에도 참조가 없는 타입 3개", - "disposition": "PROMOTE", - "target": "case:analysis-finding-a20-f005", - "reason": null, - "topic": "runtime-contract-correctness", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A20-F006", @@ -27753,11 +8651,9 @@ ], "sourceHeading": "7.1 P2 — `GrpcAdmissionController.tryAdmit()`의 동시성 경계가 동시성 아래에서 성립하지 않는다", "summary": "`GrpcAdmissionController.tryAdmit()`의 동시성 경계가 동시성 아래에서 성립하지 않는다", - "disposition": "PROMOTE", - "target": "case:a20-f006-grpcadmissioncontroller-tryadmit", - "reason": null, - "topic": "state-ownership-and-concurrency", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A20-F007", @@ -27768,11 +8664,9 @@ ], "sourceHeading": "7.2 P2 — `GrpcStreamAdmission`도 같은 형태이고, per-caller 맵이 줄지 않는다", "summary": "`GrpcStreamAdmission`도 같은 형태이고, per-caller 맵이 줄지 않는다", - "disposition": "PROMOTE", - "target": "case:a20-f007-grpcstreamadmission", - "reason": null, - "topic": "runtime-contract-correctness", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A20-F008", @@ -27783,11 +8677,9 @@ ], "sourceHeading": "7.3 P2 — `GrpcSerializedStreamWriter`의 `DROP_OLDEST`가 잘못된 메시지의 바이트를 뺀다", "summary": "`GrpcSerializedStreamWriter`의 `DROP_OLDEST`가 잘못된 메시지의 바이트를 뺀다", - "disposition": "PROMOTE", - "target": "case:a20-f008-grpcserializedstreamwriter-drop-oldest", - "reason": null, - "topic": "runtime-contract-correctness", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A20-F009", @@ -27812,11 +8704,9 @@ ], "sourceHeading": "7.5 P2 — `GrpcOutcomeReplay`가 제거 경로 없는 인메모리 저장소다", "summary": "`GrpcOutcomeReplay`가 제거 경로 없는 인메모리 저장소다", - "disposition": "PROMOTE", - "target": "case:a20-f010-grpcoutcomereplay", - "reason": null, - "topic": "state-ownership-and-concurrency", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A20-F011", @@ -27827,11 +8717,9 @@ ], "sourceHeading": "7.6 P2 — `GrpcCompletionReconciler`가 요청 경로에서 동기화 없는 `ArrayList`를 변경한다", "summary": "`GrpcCompletionReconciler`가 요청 경로에서 동기화 없는 `ArrayList`를 변경한다", - "disposition": "PROMOTE", - "target": "case:a20-f011-grpccompletionreconciler-arraylist", - "reason": null, - "topic": "state-ownership-and-concurrency", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-case-a-catalog-nine-entries-short", @@ -27842,10 +8730,9 @@ ], "summary": "패키지 카탈로그가 트리보다 아홉 개 적어서 사이클이 통과했다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "case:a-catalog-nine-entries-short", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-case-a-certifying-lane-that-compared-nothing", @@ -27856,10 +8743,9 @@ ], "summary": "\"certified\"라 불리던 레인이 threshold를 하나도 비교하지 않고 있었다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "case:a-certifying-lane-that-compared-nothing", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-case-a-cleanup-claim-without-fencing", @@ -27870,10 +8756,9 @@ ], "summary": "claim이 소유자·토큰·만료를 기록하지 않아 죽은 worker의 항목이 영영 남았다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "case:a-cleanup-claim-without-fencing", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-case-a-customizer-that-discarded-the-bound-property", @@ -27884,10 +8769,9 @@ ], "summary": "Flyway location customizer가 운영자가 바인딩한 값을 덮어썼다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "case:a-customizer-that-discarded-the-bound-property", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-case-a-digest-that-covered-who-but-not-what", @@ -27898,10 +8782,9 @@ ], "summary": "transition digest가 \"누가·언제\"만 덮고 \"무엇\"을 덮지 않아 다른 전이를 같다고 보고했다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "case:a-digest-that-covered-who-but-not-what", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-case-a-five-second-string-that-broke-every-prod-deploy", @@ -27912,10 +8795,9 @@ ], "summary": "`connection-timeout: 5s`가 모든 prod 배포를 시작 실패시켰고 local만 통과했다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "case:a-five-second-string-that-broke-every-prod-deploy", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-case-a-lease-without-an-owner", @@ -27926,10 +8808,9 @@ ], "summary": "lease가 만료 시각만 기록하고 소유자를 기록하지 않아 terminal state가 되돌려졌다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "case:a-lease-without-an-owner", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-case-a-read-then-delete-race-on-the-upload-lease", @@ -27940,10 +8821,9 @@ ], "summary": "cleanup이 읽은 lease와 삭제 사이에 writer가 그 lease를 얻을 수 있었다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "case:a-read-then-delete-race-on-the-upload-lease", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-case-a-report-that-cannot-carry-a-datasource", @@ -27954,10 +8834,9 @@ ], "summary": "진단 리포트가 살아 있는 리소스를 담지 않도록 값 타입을 좁혔다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "case:a-report-that-cannot-carry-a-datasource", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-case-a-retry-implementation-nobody-calls", @@ -27968,10 +8847,9 @@ ], "summary": "재시도 구현이 둘이고 정교한 쪽을 아무도 호출하지 않는다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "case:a-retry-implementation-nobody-calls", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-case-a-validator-checking-the-wrong-datasource", @@ -27982,10 +8860,9 @@ ], "summary": "validator가 요청을 서비스하지 않는 datasource를 검증하고 있었다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "case:a-validator-checking-the-wrong-datasource", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-case-an-active-transaction-check-that-asked-the-wrong-question", @@ -27996,10 +8873,9 @@ ], "summary": "활성 트랜잭션 검사가 data source를 묻지 않아 다른 커넥션에서 커밋됐다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "case:an-active-transaction-check-that-asked-the-wrong-question", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-case-autoconfiguration-in-name-only", @@ -28010,10 +8886,9 @@ ], "summary": "이름만 AutoConfiguration이던 세 클래스가 capability 리포트에 Stable로 올라 있었다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "case:autoconfiguration-in-name-only", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-case-commit-ambiguity-is-not-only-sqlstate-08", @@ -28024,9 +8899,8 @@ ], "summary": "`pg_terminate_backend`가 `57P01`로 도착하고 커밋 레코드는 이미 WAL에 있었다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "case:commit-ambiguity-is-not-only-sqlstate-08", - "reason": "제1부 §4.1 이 인용문까지 채택했고 컨테이너 레인이 재현·진단·결론을 닫는다", + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", "dispositionReview": "CONFIRMED" }, { @@ -28038,10 +8912,9 @@ ], "summary": "`@ConditionalOnBean(DataSource.class)`가 클래스 파싱 시점에 평가되어 여덟 빈이 사라졌다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "case:conditionalonbean-evaluated-at-parse-time", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-case-cursor-verification-order", @@ -28052,10 +8925,9 @@ ], "summary": "커서 서명 검증이 길이·상수시간·순서를 전부 지켜야 했던 이유", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "case:cursor-verification-order", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-case-expired-claim-versus-expired-execution", @@ -28066,10 +8938,9 @@ ], "summary": "만료된 CLAIMED는 takeover하고 만료된 EXECUTING은 조정을 요구하도록 갈랐다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "case:expired-claim-versus-expired-execution", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-case-five-documents-say-nineteen-leaves", @@ -28080,10 +8951,9 @@ ], "summary": "다섯 문서가 \"exactly 19 leaf\"라고 적고 레지스트리는 62다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "case:five-documents-say-nineteen-leaves", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-case-h2-hid-a-column-type-mismatch", @@ -28094,10 +8964,9 @@ ], "summary": "`char(64)`와 `varchar(64)` 불일치를 H2가 가리고 있었다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "case:h2-hid-a-column-type-mismatch", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-case-jpa-platform-capabilities-have-no-consumer", @@ -28108,10 +8977,9 @@ ], "summary": "JPA 플랫폼 capability 대부분에 production 소비자가 없다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "case:jpa-platform-capabilities-have-no-consumer", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-case-narrowing-the-scan-orphaned-eight-components", @@ -28122,10 +8990,9 @@ ], "summary": "넓은 스캔을 좁히자 여덟 컴포넌트에 아무것도 도달하지 않았다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "case:narrowing-the-scan-orphaned-eight-components", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-case-native-claim-did-not-bump-the-version", @@ -28136,10 +9003,9 @@ ], "summary": "native claim이 `@Version`을 올리지 않아 충돌을 보고하지 않는 낙관적 잠금이 됐다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "case:native-claim-did-not-bump-the-version", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-case-registry-column-too-short-for-its-own-path", @@ -28150,10 +9016,9 @@ ], "summary": "레지스트리 컬럼이 38자 경로에서 짧아 \"더 짧은 경로를 적는\" 우회를 유혹했다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "case:registry-column-too-short-for-its-own-path", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-case-requires-new-pins-the-outer-connection", @@ -28164,10 +9029,9 @@ ], "summary": "`REQUIRES_NEW`가 바깥 커넥션을 핀한 채 새 커넥션을 딴다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "case:requires-new-pins-the-outer-connection", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-case-search-path-survived-the-return-to-the-pool", @@ -28178,10 +9042,9 @@ ], "summary": "`search_path`가 풀로 돌아간 커넥션에 남아 다음 tenant가 상속한다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "case:search-path-survived-the-return-to-the-pool", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-case-tenant-pools-summed-past-the-server-ceiling", @@ -28192,10 +9055,9 @@ ], "summary": "tenant별 풀이 개별적으로 합리적이고 그 합이 서버 상한을 넘긴다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "case:tenant-pools-summed-past-the-server-ceiling", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-case-the-second-platform-carried-the-design-not-the-wiring", @@ -28206,10 +9068,9 @@ ], "summary": "두 번째 플랫폼이 첫 번째의 bridge 부재는 막고 게이트 배선은 옮기지 않았다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "case:the-second-platform-carried-the-design-not-the-wiring", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-case-three-versions-declared-one-executed", @@ -28220,10 +9081,9 @@ ], "summary": "릴리스 레인이 매트릭스 세 버전 중 첫 번째만 돌리고 세 개를 커버로 기록했다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "case:three-versions-declared-one-executed", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-case-three-ways-rls-does-nothing", @@ -28234,10 +9094,9 @@ ], "summary": "RLS가 아무것도 하지 않는 세 가지 방법", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "case:three-ways-rls-does-nothing", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-case-two-owners-popped-the-evidence-frame", @@ -28249,8 +9108,7 @@ "summary": "커밋 증거 프레임을 두 주인이 pop해서 바깥 트랜잭션의 실패가 익명이 됐다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", "disposition": "KEEP_IN_SSOT", - "target": null, - "reason": "근거가 제2부 §A05 §3.4 에만 있다. 제1부 §4.1 의 확정 결함 목록에도 없다", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", "dispositionReview": "CONFIRMED" }, { @@ -28262,10 +9120,9 @@ ], "summary": "두 트리가 다 V1부터 번호를 매겨 공유 history가 하나를 건너뛸 수 있었다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "case:two-trees-both-numbered-from-v1", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-case-untranslated-contention-bypassed-the-retry-catch", @@ -28277,8 +9134,7 @@ "summary": "번역되지 않은 경합 예외가 재시도 코디네이터의 catch를 통째로 비껴갔다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", "disposition": "KEEP_IN_SSOT", - "target": null, - "reason": "근거가 제2부 §A05 §3.6 에만 있다. 제1부가 채택하지 않았으므로 후보 범위 밖이다", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", "dispositionReview": "CONFIRMED" }, { @@ -28290,10 +9146,9 @@ ], "summary": "capability_schema_registry — 스키마 적용과 사용 승인의 분리", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "concept:capability-schema-registry", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-concept-cardinality-bounds-as-types", @@ -28304,10 +9159,9 @@ ], "summary": "카디널리티 경계를 타입으로 표현하기", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "concept:cardinality-bounds-as-types", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-concept-cas-tuple-and-update-count", @@ -28318,10 +9172,9 @@ ], "summary": "CAS 튜플과 update count가 답이 되는 구조", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "concept:cas-tuple-and-update-count", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-concept-commit-evidence-phases", @@ -28346,10 +9199,9 @@ ], "summary": "호출 예산에서 DB 로컬 타임아웃까지의 데드라인 전파", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "concept:deadline-propagation", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-concept-evidence-grades-and-provenance", @@ -28360,10 +9212,9 @@ ], "summary": "증거 등급과 provenance — R1과 R2를 가르는 것", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "concept:evidence-grades-and-provenance", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-concept-fenced-lease", @@ -28374,10 +9225,9 @@ ], "summary": "fenced lease — 만료 시각만으로는 부족한 이유", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "concept:fenced-lease", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-concept-file-state-machine-and-ready", @@ -28388,10 +9238,9 @@ ], "summary": "파일 상태 기계와 READY가 뜻하는 것", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "concept:file-state-machine-and-ready", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-concept-four-grade-disclosure", @@ -28402,10 +9251,9 @@ ], "summary": "네 단계 공시 등급 — modelled에서 production-verified까지", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "concept:four-grade-disclosure", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-concept-four-multitenancy-strategies", @@ -28416,10 +9264,9 @@ ], "summary": "네 가지 멀티테넌시 전략과 각각의 격리 경계", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "concept:four-multitenancy-strategies", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-concept-independent-flyway-streams", @@ -28430,10 +9277,9 @@ ], "summary": "독립 Flyway 스트림과 baseline version 0", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "concept:independent-flyway-streams", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-concept-redis-admission-stages", @@ -28444,10 +9290,9 @@ ], "summary": "명령 카탈로그와 admission 아홉 단계", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "concept:redis-admission-stages", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-concept-requires-new-connection-cost", @@ -28458,10 +9303,9 @@ ], "summary": "`REQUIRES_NEW`의 커넥션 비용과 풀 사이징 제약", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "concept:requires-new-connection-cost", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-concept-rls-three-preconditions", @@ -28472,10 +9316,9 @@ ], "summary": "RLS가 성립하기 위한 세 전제", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "concept:rls-three-preconditions", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-concept-signed-cursor-structure", @@ -28486,10 +9329,9 @@ ], "summary": "서명된 커서의 구조와 검증 순서", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "concept:signed-cursor-structure", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-concept-staged-lifecycle-and-signed-grants", @@ -28500,10 +9342,9 @@ ], "summary": "staged lifecycle과 서명된 grant", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "concept:staged-lifecycle-and-signed-grants", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-concept-strict-test-lane", @@ -28514,10 +9355,9 @@ ], "summary": "strict test lane — 발견하지 못하면 실패하는 레인", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "concept:strict-test-lane", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-concept-three-assembly-paths", @@ -28528,10 +9368,9 @@ ], "summary": "Spring 조립의 세 경로와 각각이 결정하는 것", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "concept:three-assembly-paths", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-concept-transaction-result-algebra", @@ -28542,9 +9381,8 @@ ], "summary": "트랜잭션 결과 대수 — 다섯 변형이 각각 답하는 질문", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "concept:transaction-result-algebra", - "reason": "Indeterminate 를 모르면 Case 의 결론을 읽을 수 없다. Case 를 고른 뒤 거꾸로 더했다", + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", "dispositionReview": "CONFIRMED" }, { @@ -28556,10 +9394,9 @@ ], "summary": "전송 실패의 단계와 범주 — `AttemptStage`와 `FailureCategory`", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "concept:transport-failure-stage-and-category", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-concept-when-conditions-are-evaluated", @@ -28570,10 +9407,9 @@ ], "summary": "조건부 빈의 평가 시점 — 파싱 시점과 등록 시점", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "concept:when-conditions-are-evaluated", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-decision-candidate-evidence-stays-at-r1", @@ -28584,10 +9420,9 @@ ], "summary": "후보 증거는 통과해도 R1에 머무르고 R2는 별도 게이트가 판정한다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "decision:candidate-evidence-stays-at-r1", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-decision-capability-grade-is-declared-not-inferred", @@ -28598,10 +9433,9 @@ ], "summary": "지원 등급은 추론이 아니라 선언이고 증거 없이는 올라가지 않는다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "decision:capability-grade-is-declared-not-inferred", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-decision-capability-separates-installation-from-activation", @@ -28612,10 +9446,9 @@ ], "summary": "capability는 스키마 적용과 사용 승인을 분리한다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "decision:capability-separates-installation-from-activation", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-decision-classpath-presence-is-not-consent", @@ -28626,10 +9459,9 @@ ], "summary": "클래스패스에 있는 것은 실행 동의가 아니다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "decision:classpath-presence-is-not-consent", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-decision-completion-unknown-is-never-retried", @@ -28640,9 +9472,8 @@ ], "summary": "completion-unknown은 자동으로도 수동으로도 재시도하지 않는다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "decision:completion-unknown-is-never-retried", - "reason": "제1부 §10.2 가 근거 문장과 함께 채택했고 근거로 걸 Case 가 있다", + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", "dispositionReview": "CONFIRMED" }, { @@ -28654,10 +9485,9 @@ ], "summary": "커서에 서명하는 이유는 기밀성이 아니라 무결성이다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "decision:cursors-are-signed-for-integrity", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-decision-flyway-owns-the-schema", @@ -28668,10 +9498,9 @@ ], "summary": "Flyway가 스키마를 소유하고 런타임 롤은 DDL 권한을 갖지 않는다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "decision:flyway-owns-the-schema", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-decision-grades-derive-from-executed-evidence", @@ -28682,10 +9511,9 @@ ], "summary": "능력 등급은 코드가 아니라 실행된 증거에서 파생한다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "decision:grades-derive-from-executed-evidence", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-decision-grpc-stays-build-only-until-the-bridge-is-decided", @@ -28696,10 +9524,9 @@ ], "summary": "gRPC 플랫폼은 build-only로 두고 애플리케이션 도달 경로를 먼저 정한다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "decision:grpc-stays-build-only-until-the-bridge-is-decided", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-decision-in-root-write-fails-fast", @@ -28710,10 +9537,9 @@ ], "summary": "`inRootWrite`는 suspend하지 않고 fail-fast한다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "decision:in-root-write-fails-fast", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-decision-legacy-adoption-requires-two-approvers", @@ -28724,10 +9550,9 @@ ], "summary": "legacy 채택은 서로 다른 두 승인자의 서명을 요구한다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "decision:legacy-adoption-requires-two-approvers", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-decision-no-physical-paths-in-metadata", @@ -28738,10 +9563,9 @@ ], "summary": "물리 경로와 원본 파일명을 저장하지 않는다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "decision:no-physical-paths-in-metadata", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-decision-one-audit-mechanism-per-entity", @@ -28752,10 +9576,9 @@ ], "summary": "감사 메커니즘은 엔티티당 정확히 하나여야 한다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "decision:one-audit-mechanism-per-entity", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-decision-one-root-owns-the-master-switch", @@ -28766,10 +9589,9 @@ ], "summary": "마스터 스위치는 루트 하나가 소유하고 자식 설정은 조건을 갖지 않는다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "decision:one-root-owns-the-master-switch", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-decision-only-the-certification-lane-carries-no-docker-guard", @@ -28780,10 +9602,9 @@ ], "summary": "인증 레인만 Docker 가드를 달지 않는다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "decision:only-the-certification-lane-carries-no-docker-guard", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-decision-performance-measurement-is-not-a-release-gate", @@ -28794,10 +9615,9 @@ ], "summary": "성능 측정은 릴리스 게이트에 넣지 않는다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "decision:performance-measurement-is-not-a-release-gate", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-decision-pool-need-is-a-capability-question", @@ -28808,10 +9628,9 @@ ], "summary": "풀이 필요한지는 \"JPA가 켜졌나\"가 아니라 \"커넥션이 필요한 capability가 있나\"로 묻는다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "decision:pool-need-is-a-capability-question", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-decision-repair-is-not-a-mode", @@ -28822,10 +9641,9 @@ ], "summary": "Repair는 모드가 아니라 운영자가 호출하는 작업이다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "decision:repair-is-not-a-mode", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-decision-retry-safety-is-decided-by-evidence", @@ -28836,10 +9654,9 @@ ], "summary": "재시도 안전성은 증거에 기반해 판정한다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "decision:retry-safety-is-decided-by-evidence", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-decision-retry-unit-is-the-use-case", @@ -28851,8 +9668,7 @@ "summary": "재시도 단위는 statement가 아니라 유스케이스 전체다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", "disposition": "KEEP_IN_SSOT", - "target": null, - "reason": "근거가 제2부 §A05 §3.6 에만 있다. 제1부 §10 의 결정 표에 없다", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", "dispositionReview": "CONFIRMED" }, { @@ -28864,10 +9680,9 @@ ], "summary": "상태 기계 구현은 Spring stereotype을 갖지 않는다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "decision:state-machines-carry-no-stereotype", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-decision-templates-are-built-once-per-mode", @@ -28878,10 +9693,9 @@ ], "summary": "트랜잭션 템플릿은 모드별로 미리 만들어 둔다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "decision:templates-are-built-once-per-mode", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-decision-tenant-id-is-never-a-metric-tag", @@ -28892,10 +9706,9 @@ ], "summary": "tenant id는 메트릭 태그가 되지 않는다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "decision:tenant-id-is-never-a-metric-tag", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-decision-three-axes-of-evidence", @@ -28906,10 +9719,9 @@ ], "summary": "전송·업무·스트림 증거는 세 축이고 서로를 함의하지 않는다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": null, - "reason": "증거 세 축은 발행 경로 주제다. 그 Topic 이 재선별될 때 판정한다", - "dispositionReview": "PENDING", + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED", "deferred": "이 Topic 밖으로 나가는 후보다. 소관 Topic 이 재선별될 때 처분한다" }, { @@ -28921,10 +9733,9 @@ ], "summary": "분류되지 않은 명령은 fail-closed로 거부한다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "decision:unclassified-commands-are-refused", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-decision-widen-doc-contract-assertions", @@ -28936,11 +9747,10 @@ ], "summary": "문서 계약 테스트의 단언 범위를 capability 표까지 넓힐 것인가", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "open-question:widen-doc-contract-assertions", - "reason": null, + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", "note": "DECISION 에서 OPEN QUESTION 으로 정정. 노드 자신이 decision-status NOT_DECIDED 이고 채택 근거가 없다 — 프로젝트가 선택한 방향이 아니라 아직 닫히지 않은 선택이다.", - "dispositionReview": "PENDING" + "dispositionReview": "CONFIRMED" }, { "id": "BASE-open-question-commit-ambiguity-lane-not-executed", @@ -28951,9 +9761,8 @@ ], "summary": "커밋 모호성 계약 레인이 이 리비전에서 통과하는지 실행으로 확인되지 않았다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "question:commit-ambiguity-lane-not-rerun-at-head", - "reason": "「실행되지 않았다」는 틀렸다 — EVD-115 가 a24ece9c 에서 통과를 기록한다. 열려 있는 것은 HEAD 재실행이라 질문을 그쪽으로 다시 세웠다", + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", "dispositionReview": "CONFIRMED" }, { @@ -28965,10 +9774,9 @@ ], "summary": "`@ConditionalOnBean` 사슬의 실제 평가 순서를 확인하지 않았다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "open-question:conditional-evaluation-order-unverified", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-open-question-container-lanes-not-executed", @@ -28979,10 +9787,9 @@ ], "summary": "컨테이너가 필요한 레인의 실제 결과를 실행으로 확인하지 않았다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "open-question:container-lanes-not-executed", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-open-question-one-boot-would-settle-two-findings", @@ -28993,10 +9800,9 @@ ], "summary": "부팅 한 번으로 확증 가능한 두 건이 아직 정적 추론으로만 남아 있다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "open-question:one-boot-would-settle-two-findings", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-open-question-pool-contract-lane-not-executed", @@ -29007,10 +9813,9 @@ ], "summary": "풀 계약 레인이 실행되지 않아 포화 동작이 확인되지 않았다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "open-question:pool-contract-lane-not-executed", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-open-question-redis-topology-lane-not-executed", @@ -29021,10 +9826,9 @@ ], "summary": "Redis 토폴로지 레인이 실행되지 않아 key spec 드리프트가 확인되지 않았다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "open-question:redis-topology-lane-not-executed", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-open-question-v2-state-machine-lanes-not-executed", @@ -29035,10 +9839,9 @@ ], "summary": "V2 상태 기계 넷의 컨테이너 레인이 실행되지 않았다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "open-question:v2-state-machine-lanes-not-executed", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-a-bean-is-not-composition-evidence", @@ -29049,10 +9852,9 @@ ], "summary": "`@Bean`이 있다는 것은 조립 증거가 아니다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:a-bean-is-not-composition-evidence", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-a-binary-approval-codec-must-round-trip", @@ -29063,10 +9865,9 @@ ], "summary": "이진 승인 코덱은 decode 후 재인코딩이 원본과 같아야 한다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:a-binary-approval-codec-must-round-trip", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-a-contract-test-must-run-the-adapters-statement", @@ -29077,10 +9878,9 @@ ], "summary": "계약 테스트는 어댑터가 실제로 돌리는 statement를 실행해야 한다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:a-contract-test-must-run-the-adapters-statement", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-a-gate-nobody-runs-reports-the-last-run", @@ -29091,10 +9891,9 @@ ], "summary": "아무도 돌리지 않는 레인의 게이트는 마지막으로 돌린 사람이 본 것을 보고한다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:a-gate-nobody-runs-reports-the-last-run", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-a-lane-that-discovers-nothing-must-fail", @@ -29105,10 +9904,9 @@ ], "summary": "아무것도 발견하지 못한 레인은 성공이 아니라 실패여야 한다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:a-lane-that-discovers-nothing-must-fail", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-a-publicly-readable-state-must-be-complete-by-constraint", @@ -29119,10 +9917,9 @@ ], "summary": "공개 읽기 가능한 상태는 완전한 identity를 DB 제약으로 요구한다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:a-publicly-readable-state-must-be-complete-by-constraint", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-a-single-admission-point-must-count-its-bypasses", @@ -29133,10 +9930,9 @@ ], "summary": "단일 admission point는 우회 경로를 세어야 성립한다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:a-single-admission-point-must-count-its-bypasses", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-agreement-between-documents-proves-nothing", @@ -29147,10 +9943,9 @@ ], "summary": "문서와 상수가 서로 일치하는 것으로는 아무것도 증명되지 않는다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:agreement-between-documents-proves-nothing", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-ambiguity-rule-hurts-both-ways", @@ -29175,10 +9970,9 @@ ], "summary": "적용된 마이그레이션의 checksum은 그것을 돌린 모든 배포에 대한 약속이다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:an-applied-checksum-is-a-promise", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-atomic-type-is-not-atomicity", @@ -29189,10 +9983,9 @@ ], "summary": "`Atomic*` 타입의 존재는 원자성의 증거가 아니다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:atomic-type-is-not-atomicity", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-cas-tuple-in-the-where-clause", @@ -29203,10 +9996,9 @@ ], "summary": "CAS 튜플을 where 절에 전부 반복하고 update count를 답으로 쓴다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:cas-tuple-in-the-where-clause", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-check-which-duplicate-is-wired", @@ -29217,10 +10009,9 @@ ], "summary": "중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:check-which-duplicate-is-wired", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-claim-with-a-conditional-update-not-a-read", @@ -29231,10 +10022,9 @@ ], "summary": "조건부 update로 행을 claim하고 읽은 값으로 판단하지 않는다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:claim-with-a-conditional-update-not-a-read", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-conditionalonbean-must-be-satisfiable", @@ -29245,10 +10035,9 @@ ], "summary": "`@ConditionalOnBean`은 조건이 만족될 수 있는지까지 확인해야 한다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:conditionalonbean-must-be-satisfiable", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-count-the-frameworks-own-autoconfigurations", @@ -29259,10 +10048,9 @@ ], "summary": "프레임워크가 기여하는 자동설정까지 세지 않으면 스위치가 아니다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:count-the-frameworks-own-autoconfigurations", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-deadline-narrows-in-three-stages", @@ -29273,10 +10061,9 @@ ], "summary": "데드라인은 호출 예산에서 시작해 세 단계로 좁힌다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:deadline-narrows-in-three-stages", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-digest-must-be-length-framed-and-versioned", @@ -29287,10 +10074,9 @@ ], "summary": "digest는 길이 프레이밍하고 버전을 붙인다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:digest-must-be-length-framed-and-versioned", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-each-stream-owns-its-history-table", @@ -29301,10 +10087,9 @@ ], "summary": "마이그레이션 스트림은 자기 history 테이블을 갖는다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:each-stream-owns-its-history-table", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-expired-claim-and-expired-execution-differ", @@ -29315,10 +10100,9 @@ ], "summary": "만료된 claim과 만료된 실행은 다르게 다뤄야 한다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:expired-claim-and-expired-execution-differ", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-fix-overstatement-before-understatement", @@ -29329,10 +10113,9 @@ ], "summary": "과대 진술 문서를 과소보다 먼저 고친다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:fix-overstatement-before-understatement", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-grades-may-understate-never-overstate", @@ -29343,10 +10126,9 @@ ], "summary": "등급은 네 단계로 나누고 관측보다 높게 적지 않는다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:grades-may-understate-never-overstate", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-hibernate-filter-is-not-a-security-boundary", @@ -29357,10 +10139,9 @@ ], "summary": "Hibernate filter는 보안 경계가 아니다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:hibernate-filter-is-not-a-security-boundary", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-isolation-settings-must-be-transaction-local", @@ -29371,10 +10152,9 @@ ], "summary": "격리 설정은 트랜잭션 로컬이어야 한다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:isolation-settings-must-be-transaction-local", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-local-with-a-different-db-is-a-different-system", @@ -29385,10 +10165,9 @@ ], "summary": "로컬이 다른 DB면 로컬 테스트는 다른 시스템에 대한 진술이다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:local-with-a-different-db-is-a-different-system", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-make-the-unsafe-state-unrepresentable", @@ -29413,10 +10192,9 @@ ], "summary": "이름은 값이 아니라 registry key다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:names-are-registry-keys-not-values", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-numbers-in-docs-should-be-derived", @@ -29427,10 +10205,9 @@ ], "summary": "문서의 수치는 세지 말고 파생하거나 게이트로 붙든다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:numbers-in-docs-should-be-derived", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-off-must-be-structural", @@ -29441,10 +10218,9 @@ ], "summary": "\"꺼짐\"은 조건의 반복이 아니라 구조여야 한다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:off-must-be-structural", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-omission-that-passes-is-not-a-gate", @@ -29455,10 +10231,9 @@ ], "summary": "빠뜨림이 통과가 되는 게이트는 게이트가 아니다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:omission-that-passes-is-not-a-gate", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-prefix-matching-fits-signatures-not-sniffing", @@ -29469,10 +10244,9 @@ ], "summary": "접두사 시작 매칭은 시그니처에는 맞고 스니핑 패턴에는 맞지 않는다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:prefix-matching-fits-signatures-not-sniffing", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-read-the-assembling-side-first", @@ -29483,10 +10257,9 @@ ], "summary": "조립 결함을 판정하려면 조립하는 쪽을 먼저 읽어야 한다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:read-the-assembling-side-first", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-read-the-clock-after-the-lock", @@ -29497,10 +10270,9 @@ ], "summary": "시간은 DB에서, 그리고 행을 잠근 다음에 읽는다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:read-the-clock-after-the-lock", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-register-paths-bind-values", @@ -29511,10 +10283,9 @@ ], "summary": "path·identifier는 등록하고 value는 바인딩한다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:register-paths-bind-values", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-registry-rules-transfer-ci-rules-do-not", @@ -29525,10 +10296,9 @@ ], "summary": "레지스트리로 표현된 규칙은 전이되고 CI로 표현된 규칙은 전이되지 않는다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:registry-rules-transfer-ci-rules-do-not", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-reject-rather-than-sanitize", @@ -29539,10 +10309,9 @@ ], "summary": "sanitize가 아니라 reject가 기본이다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:reject-rather-than-sanitize", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-retryability-needs-both-idempotency-and-category", @@ -29553,10 +10322,9 @@ ], "summary": "재시도 가능성은 멱등성과 실패 범주를 함께 봐야 정해진다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:retryability-needs-both-idempotency-and-category", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-runtime-membership-decides-severity", @@ -29567,10 +10335,9 @@ ], "summary": "`runtime_memberships`를 먼저 읽고 심각도를 정한다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:runtime-membership-decides-severity", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-server-metadata-defines-the-command", @@ -29581,10 +10348,9 @@ ], "summary": "서버 메타데이터가 명령의 정의이고 정책 파일은 허용 범위다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:server-metadata-defines-the-command", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-session-scoped-settings-outlive-the-transaction", @@ -29595,10 +10361,9 @@ ], "summary": "세션 스코프 설정은 풀로 돌아간 커넥션에 남는다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:session-scoped-settings-outlive-the-transaction", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-telemetry-can-be-more-dangerous-than-its-subject", @@ -29609,10 +10374,9 @@ ], "summary": "관측을 위해 수집한 데이터가 관측 대상보다 위험할 수 있다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:telemetry-can-be-more-dangerous-than-its-subject", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-tenant-column-belongs-in-every-unique-constraint", @@ -29623,10 +10387,9 @@ ], "summary": "tenant 컬럼이 있는 테이블의 모든 unique 제약에 그 컬럼이 들어가야 한다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:tenant-column-belongs-in-every-unique-constraint", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-the-powerful-half-must-not-be-one-setting-away", @@ -29637,10 +10400,9 @@ ], "summary": "권한이 센 절반이 설정 한 줄로 켜지면 안 된다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:the-powerful-half-must-not-be-one-setting-away", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-the-startup-validator-follows-the-autoconfiguration-root", @@ -29651,10 +10413,9 @@ ], "summary": "시작 검증기가 도는지는 그 능력에 자동설정 루트가 있는지와 일치한다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:the-startup-validator-follows-the-autoconfiguration-root", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-translation-chain-order-is-a-contract", @@ -29666,8 +10427,7 @@ "summary": "실패 번역 사슬의 순서는 계약이다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", "disposition": "KEEP_IN_SSOT", - "target": null, - "reason": "번역 사슬 순서는 이 Topic 의 독자 질문에 답하지 않고 근거도 제2부에만 있다", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", "dispositionReview": "CONFIRMED" }, { @@ -29679,10 +10439,9 @@ ], "summary": "같은 개념의 두 어휘가 공존하면 하나를 죽은 것으로 표시한다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:two-vocabularies-for-one-concept", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-unknown-is-a-third-result", @@ -29693,9 +10452,8 @@ ], "summary": "모르는 것은 성공도 실패도 아닌 세 번째 결과여야 한다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:unknown-is-a-third-result", - "reason": "제1부 §9 규칙 13 이고 적용 조건과 예외가 §3.3 에 있다. 다음 프로젝트에도 그대로 적용된다", + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", "dispositionReview": "CONFIRMED" }, { @@ -29707,10 +10465,9 @@ ], "summary": "원인 사슬은 가장 구체적인 분류가 이기도록 순회한다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:walk-the-cause-chain-most-specific-wins", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "BASE-reference-write-transactions-need-a-finite-timeout", @@ -29721,10 +10478,9 @@ ], "summary": "쓰기 트랜잭션에는 유한 타임아웃이 필수다", "origin": "PRE_COVERAGE_ROOT_TREE_BASELINE", - "disposition": "PROMOTE", - "target": "reference:write-transactions-need-a-finite-timeout", - "reason": null, - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "X99-Q001", @@ -29749,10 +10505,9 @@ ], "sourceHeading": "남은 질문 2 — sample-portfolio 내부", "summary": "sample-portfolio 내부와 두 번째 runtime membership에서 달라질 수 있는 판정", - "disposition": "BLOCKED", - "target": null, - "reason": "sample-portfolio는 사용자 지시로 분석 대상에서 명시적으로 제외됐다. 분석 없이 질문 record를 생성하면 근거 범위를 넘어가므로 scope가 다시 열리기 전까지 BLOCKED다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "X99-Q003", @@ -29791,11 +10546,9 @@ ], "sourceHeading": "남은 질문 5 — 성능·용량 주장", "summary": "실제 성능·용량 특성을 어떤 모듈에서도 측정하지 않았다", - "disposition": "PROMOTE", - "target": "open-question:performance-and-capacity-unmeasured", - "reason": null, - "topic": "verification-path-coverage", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C2-F001", @@ -29806,11 +10559,10 @@ ], "sourceHeading": "P2 — insert-first 주장이 Spring Data 의 save 계약과 어긋난다. 그리고 테스트 이중이 그 차이를 가린다", "summary": "insert-first 주장이 Spring Data 의 save 계약과 어긋난다. 그리고 테스트 이중이 그 차이를 가린다", - "disposition": "PROMOTE", - "target": "case:assigned-id-turns-claim-into-upsert", - "reason": "사이클 2 전수 통독에서 처음 나온 finding. 기존 노드가 같은 사건을 담지 않아 새 노드로 낸다.", + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", "cycle": 2, - "dispositionReview": "PENDING" + "dispositionReview": "CONFIRMED" }, { "id": "C2-F002", @@ -29821,11 +10573,10 @@ ], "sourceHeading": "P2 — 자격증명 회전이 비교 후 교체가 아니고, 배수 완료가 진행 중인 회전을 되돌릴 수 있다", "summary": "자격증명 회전이 비교 후 교체가 아니고, 배수 완료가 진행 중인 회전을 되돌릴 수 있다", - "disposition": "PROMOTE", - "target": "case:complete-drain-rolls-back-a-rotation", - "reason": "사이클 2 전수 통독에서 처음 나온 finding. 기존 노드가 같은 사건을 담지 않아 새 노드로 낸다.", + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", "cycle": 2, - "dispositionReview": "PENDING" + "dispositionReview": "CONFIRMED" }, { "id": "C2-F003", @@ -29836,11 +10587,10 @@ ], "sourceHeading": "P2 — 시작 검증기가 시작 시 실행되지 않는다", "summary": "시작 검증기가 시작 시 실행되지 않는다", - "disposition": "PROMOTE", - "target": "case:startup-validator-is-the-only-reader-of-four-keys", - "reason": "사이클 2 전수 통독에서 처음 나온 finding. 기존 노드가 같은 사건을 담지 않아 새 노드로 낸다.", + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", "cycle": 2, - "dispositionReview": "PENDING" + "dispositionReview": "CONFIRMED" }, { "id": "C2-F004", @@ -29851,11 +10601,10 @@ ], "sourceHeading": "P2 — 같은 자동 설정 안에서 검증기 하나만 감싸이지 않는다", "summary": "같은 자동 설정 안에서 검증기 하나만 감싸이지 않는다", - "disposition": "PROMOTE", - "target": "case:validator-declared-and-never-injected", - "reason": "사이클 2 전수 통독에서 처음 나온 finding. 기존 노드가 같은 사건을 담지 않아 새 노드로 낸다.", + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", "cycle": 2, - "dispositionReview": "PENDING" + "dispositionReview": "CONFIRMED" }, { "id": "C2-F005", @@ -29866,11 +10615,10 @@ ], "sourceHeading": "P2 — deduplicatedPublish 를 무조건 참으로 선언하는데 실제 중복 제거는 프로파일에 창이 있을 때만 일어난다", "summary": "deduplicatedPublish 를 무조건 참으로 선언하는데 실제 중복 제거는 프로파일에 창이 있을 때만 일어난다", - "disposition": "PROMOTE", - "target": "case:capability-constant-outlives-its-condition", - "reason": "사이클 2 전수 통독에서 처음 나온 finding. 기존 노드가 같은 사건을 담지 않아 새 노드로 낸다.", + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", "cycle": 2, - "dispositionReview": "PENDING" + "dispositionReview": "CONFIRMED" }, { "id": "C2-F006", @@ -29881,11 +10629,10 @@ ], "sourceHeading": "P2 — 마스킹이 IPv4 만 알고, 그 결과 마스킹되지 않은 주소 검사가 나머지 형태를 전부 통과시킨다", "summary": "마스킹이 IPv4 만 알고, 그 결과 마스킹되지 않은 주소 검사가 나머지 형태를 전부 통과시킨다", - "disposition": "PROMOTE", - "target": "case:ipv4-only-mask-passes-every-other-form", - "reason": "사이클 2 전수 통독에서 처음 나온 finding. 기존 노드가 같은 사건을 담지 않아 새 노드로 낸다.", + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", "cycle": 2, - "dispositionReview": "PENDING" + "dispositionReview": "CONFIRMED" }, { "id": "C2-F007", @@ -29896,11 +10643,10 @@ ], "sourceHeading": "규칙 — 검증기는 발행이 아니라 주입이 강제다", "summary": "검증기는 발행이 아니라 주입이 강제다", - "disposition": "PROMOTE", - "target": "reference:a-validator-is-enforced-by-injection", - "reason": "사이클 2 전수 통독에서 처음 나온 finding. 기존 노드가 같은 사건을 담지 않아 새 노드로 낸다.", + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", "cycle": 2, - "dispositionReview": "PENDING" + "dispositionReview": "CONFIRMED" }, { "id": "A24-F001", @@ -29911,10 +10657,9 @@ ], "sourceHeading": "운영 프로파일에 TLS·인증을 요구하고 그 둘이 없는 Kafka 생산자를 만든다", "summary": "운영 프로파일에 TLS·인증을 요구하고 그 둘이 없는 Kafka 생산자를 만든다", - "disposition": "PROMOTE", - "target": "case:a-validator-that-demands-tls-and-an-assembly-that-omits-it", - "reason": "23개 리프 재통독에서 처음 나온 finding. 기존 노드가 같은 사건을 담지 않아 새 노드로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A24-F002", @@ -29925,10 +10670,9 @@ ], "sourceHeading": "두 파일이 같은 검증기를 빌드 게이트라고 적고 어떤 빌드도 부르지 않는다", "summary": "두 파일이 같은 검증기를 빌드 게이트라고 적고 어떤 빌드도 부르지 않는다", - "disposition": "PROMOTE", - "target": "case:two-files-name-a-build-gate-that-no-build-runs", - "reason": "23개 리프 재통독에서 처음 나온 finding. 기존 노드가 같은 사건을 담지 않아 새 노드로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A24-F003", @@ -29939,10 +10683,9 @@ ], "sourceHeading": "산문이 선언한 게이트는 빌드에 있는 게이트가 아니다", "summary": "산문이 선언한 게이트는 빌드에 있는 게이트가 아니다", - "disposition": "PROMOTE", - "target": "reference:a-gate-declared-in-prose-is-not-in-the-build", - "reason": "23개 리프 재통독에서 처음 나온 finding. 기존 노드가 같은 사건을 담지 않아 새 노드로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A24-F004", @@ -29953,10 +10696,9 @@ ], "sourceHeading": "이름이 검사한다고 말하는 것을 본문이 검사하지 않는 테스트 다섯", "summary": "이름이 검사한다고 말하는 것을 본문이 검사하지 않는 테스트 다섯", - "disposition": "PROMOTE", - "target": "case:test-names-that-assert-what-their-bodies-do-not", - "reason": "23개 리프 재통독에서 처음 나온 finding. 기존 노드가 같은 사건을 담지 않아 새 노드로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A24-F005", @@ -29967,10 +10709,9 @@ ], "sourceHeading": "결정을 그 결정이 판정한 대상에 묶는 것 — 같은 저장소의 맞는 판본과 틀린 판본", "summary": "결정을 그 결정이 판정한 대상에 묶는 것 — 같은 저장소의 맞는 판본과 틀린 판본", - "disposition": "PROMOTE", - "target": "case:the-same-repository-bound-a-decision-once-and-not-the-other-time", - "reason": "23개 리프 재통독에서 처음 나온 finding. 기존 노드가 같은 사건을 담지 않아 새 노드로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A24-F006", @@ -29981,10 +10722,9 @@ ], "sourceHeading": "담금 임계값이 열거형에 없는 등급을 위해 쓰여 중간 등급이 상위 등급보다 어려워졌다", "summary": "담금 임계값이 열거형에 없는 등급을 위해 쓰여 중간 등급이 상위 등급보다 어려워졌다", - "disposition": "PROMOTE", - "target": "case:a-soak-threshold-written-for-a-grade-that-does-not-exist", - "reason": "23개 리프 재통독에서 처음 나온 finding. 기존 노드가 같은 사건을 담지 않아 새 노드로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A25-F001", @@ -29996,11 +10736,9 @@ ], "sourceHeading": "능력 선언의 세 출처와 그것이 파생되지 않을 때", "summary": "능력 선언의 세 출처와 그것이 파생되지 않을 때", - "disposition": "PROMOTE", - "target": "concept:three-sources-of-a-capability-answer", - "topic": "capability-declaration-vs-proof", - "reason": "43개 리프 SSOT 재분해에서 나온 노드. 기존 Topic 이 담지 않는 문제 공간이라 새 Topic 으로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A25-F002", @@ -30012,11 +10750,9 @@ ], "sourceHeading": "브로커 트랜잭션을 무조건 참으로 선언하고, 그 조건을 검사하는 검증기는 기동 시 돌지 않는다", "summary": "브로커 트랜잭션을 무조건 참으로 선언하고, 그 조건을 검사하는 검증기는 기동 시 돌지 않는다", - "disposition": "PROMOTE", - "target": "case:a-transaction-capability-true-and-its-validator-never-run", - "topic": "capability-declaration-vs-proof", - "reason": "43개 리프 SSOT 재분해에서 나온 노드. 기존 Topic 이 담지 않는 문제 공간이라 새 Topic 으로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A25-F003", @@ -30027,11 +10763,9 @@ ], "sourceHeading": "지연 배달을 참으로 선언하는데 그 지연을 제공할 토폴로지가 조립되지 않는다", "summary": "지연 배달을 참으로 선언하는데 그 지연을 제공할 토폴로지가 조립되지 않는다", - "disposition": "PROMOTE", - "target": "case:a-delayed-delivery-flag-without-the-topology-that-delivers-it", - "topic": "capability-declaration-vs-proof", - "reason": "43개 리프 SSOT 재분해에서 나온 노드. 기존 Topic 이 담지 않는 문제 공간이라 새 Topic 으로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A25-F004", @@ -30042,11 +10776,9 @@ ], "sourceHeading": "같은 어댑터의 능력을 전송과 검증기가 다르게 답하고, 런타임이 쓰는 쪽이 record 의 의미와 어긋난다", "summary": "같은 어댑터의 능력을 전송과 검증기가 다르게 답하고, 런타임이 쓰는 쪽이 record 의 의미와 어긋난다", - "disposition": "PROMOTE", - "target": "case:the-transport-and-the-validator-answer-differently", - "topic": "capability-declaration-vs-proof", - "reason": "43개 리프 SSOT 재분해에서 나온 노드. 기존 Topic 이 담지 않는 문제 공간이라 새 Topic 으로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A25-F005", @@ -30057,11 +10789,9 @@ ], "sourceHeading": "운영자용 지원 매트릭스가 런타임 편입을 반대로 적고, 틀린 쪽이 옳은 쪽을 권위로 지목한다", "summary": "운영자용 지원 매트릭스가 런타임 편입을 반대로 적고, 틀린 쪽이 옳은 쪽을 권위로 지목한다", - "disposition": "PROMOTE", - "target": "case:the-support-matrix-says-nothing-is-deployed-and-eighteen-are", - "topic": "capability-declaration-vs-proof", - "reason": "43개 리프 SSOT 재분해에서 나온 노드. 기존 Topic 이 담지 않는 문제 공간이라 새 Topic 으로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A25-F006", @@ -30073,11 +10803,9 @@ ], "sourceHeading": "능력 선언은 프로파일에서 파생되어야 하고 상수는 그것을 할 수 없다", "summary": "능력 선언은 프로파일에서 파생되어야 하고 상수는 그것을 할 수 없다", - "disposition": "PROMOTE", - "target": "reference:a-capability-constant-must-derive-from-the-profile", - "topic": "capability-declaration-vs-proof", - "reason": "43개 리프 SSOT 재분해에서 나온 노드. 기존 Topic 이 담지 않는 문제 공간이라 새 Topic 으로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A25-F007", @@ -30089,11 +10817,9 @@ ], "sourceHeading": "능력 플래그의 무게는 그것을 읽는 코드가 정한다", "summary": "능력 플래그의 무게는 그것을 읽는 코드가 정한다", - "disposition": "PROMOTE", - "target": "reference:the-weight-of-a-flag-is-set-by-the-code-that-reads-it", - "topic": "capability-declaration-vs-proof", - "reason": "43개 리프 SSOT 재분해에서 나온 노드. 기존 Topic 이 담지 않는 문제 공간이라 새 Topic 으로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A25-F008", @@ -30105,11 +10831,9 @@ ], "sourceHeading": "원자 타입 위의 검사 후 실행과 비교 후 교체 루프", "summary": "원자 타입 위의 검사 후 실행과 비교 후 교체 루프", - "disposition": "PROMOTE", - "target": "concept:check-then-act-on-atomic-types", - "topic": "non-atomic-check-then-act", - "reason": "43개 리프 SSOT 재분해에서 나온 노드. 기존 Topic 이 담지 않는 문제 공간이라 새 Topic 으로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A25-F009", @@ -30120,11 +10844,9 @@ ], "sourceHeading": "회전이 비교 후 교체가 아니라 덮어쓰기이고, 세대 계수기는 음수가 되면 회수되지 않는다", "summary": "회전이 비교 후 교체가 아니라 덮어쓰기이고, 세대 계수기는 음수가 되면 회수되지 않는다", - "disposition": "PROMOTE", - "target": "case:a-rotation-that-overwrites-and-a-generation-that-cannot-be-reclaimed", - "topic": "non-atomic-check-then-act", - "reason": "43개 리프 SSOT 재분해에서 나온 노드. 기존 Topic 이 담지 않는 문제 공간이라 새 Topic 으로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A25-F010", @@ -30136,11 +10858,9 @@ ], "sourceHeading": "승인 경계가 동시성 아래에서 새고, 큐 계수기를 되돌리는 경로가 없다", "summary": "승인 경계가 동시성 아래에서 새고, 큐 계수기를 되돌리는 경로가 없다", - "disposition": "PROMOTE", - "target": "case:an-admission-boundary-that-leaks-and-a-counter-that-cannot-return", - "topic": "non-atomic-check-then-act", - "reason": "43개 리프 SSOT 재분해에서 나온 노드. 기존 Topic 이 담지 않는 문제 공간이라 새 Topic 으로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A25-F011", @@ -30151,11 +10871,9 @@ ], "sourceHeading": "배수를 시작한 뒤에도 한 서비스가 다시 SERVING 이 될 수 있다", "summary": "배수를 시작한 뒤에도 한 서비스가 다시 SERVING 이 될 수 있다", - "disposition": "PROMOTE", - "target": "case:draining-began-and-a-service-came-back-serving", - "topic": "non-atomic-check-then-act", - "reason": "43개 리프 SSOT 재분해에서 나온 노드. 기존 Topic 이 담지 않는 문제 공간이라 새 Topic 으로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A25-F012", @@ -30167,11 +10885,9 @@ ], "sourceHeading": "전이를 시작하는 쓰기와 그 전이가 덮는 집합은 한 연산이어야 한다", "summary": "전이를 시작하는 쓰기와 그 전이가 덮는 집합은 한 연산이어야 한다", - "disposition": "PROMOTE", - "target": "reference:beginning-a-transition-and-the-set-it-covers-are-one-operation", - "topic": "non-atomic-check-then-act", - "reason": "43개 리프 SSOT 재분해에서 나온 노드. 기존 Topic 이 담지 않는 문제 공간이라 새 Topic 으로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A25-F013", @@ -30183,11 +10899,9 @@ ], "sourceHeading": "bounded 와 unbounded 오버로드를 나란히 둔 포트", "summary": "bounded 와 unbounded 오버로드를 나란히 둔 포트", - "disposition": "PROMOTE", - "target": "concept:bounded-and-unbounded-side-by-side", - "topic": "retention-and-unbounded-growth", - "reason": "43개 리프 SSOT 재분해에서 나온 노드. 기존 Topic 이 담지 않는 문제 공간이라 새 Topic 으로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A25-F014", @@ -30199,11 +10913,9 @@ ], "sourceHeading": "cleanup 이 스스로 막겠다고 적은 장애를 일으키는 형태로 호출된다", "summary": "cleanup 이 스스로 막겠다고 적은 장애를 일으키는 형태로 호출된다", - "disposition": "PROMOTE", - "target": "case:the-cleanup-that-causes-the-outage-it-prevents", - "topic": "retention-and-unbounded-growth", - "reason": "43개 리프 SSOT 재분해에서 나온 노드. 기존 Topic 이 담지 않는 문제 공간이라 새 Topic 으로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A25-F015", @@ -30215,11 +10927,9 @@ ], "sourceHeading": "재생 저장소와 중복 제거 맵에 제거 경로가 없다", "summary": "재생 저장소와 중복 제거 맵에 제거 경로가 없다", - "disposition": "PROMOTE", - "target": "case:a-replay-store-with-no-eviction-path", - "topic": "retention-and-unbounded-growth", - "reason": "43개 리프 SSOT 재분해에서 나온 노드. 기존 Topic 이 담지 않는 문제 공간이라 새 Topic 으로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A25-F016", @@ -30231,11 +10941,9 @@ ], "sourceHeading": "두 형태를 나란히 내놓는 포트는 이미 안전하지 않은 쪽을 고른 것이다", "summary": "두 형태를 나란히 내놓는 포트는 이미 안전하지 않은 쪽을 고른 것이다", - "disposition": "PROMOTE", - "target": "reference:a-port-that-offers-both-forms-has-chosen-the-unsafe-one", - "topic": "retention-and-unbounded-growth", - "reason": "43개 리프 SSOT 재분해에서 나온 노드. 기존 Topic 이 담지 않는 문제 공간이라 새 Topic 으로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A25-F017", @@ -30247,11 +10955,9 @@ ], "sourceHeading": "같은 안전 규칙은 하나의 공식과 하나의 강제 시점을 갖는다", "summary": "같은 안전 규칙은 하나의 공식과 하나의 강제 시점을 갖는다", - "disposition": "PROMOTE", - "target": "reference:one-formula-and-one-enforcement-point-per-safety-rule", - "topic": "retention-and-unbounded-growth", - "reason": "43개 리프 SSOT 재분해에서 나온 노드. 기존 Topic 이 담지 않는 문제 공간이라 새 Topic 으로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A25-F018", @@ -30262,11 +10968,9 @@ ], "sourceHeading": "8단계 종료 순서 계약과 실제 종료 경로", "summary": "8단계 종료 순서 계약과 실제 종료 경로", - "disposition": "PROMOTE", - "target": "concept:the-eight-phase-shutdown-contract", - "topic": "drain-and-shutdown-ordering", - "reason": "43개 리프 SSOT 재분해에서 나온 노드. 기존 Topic 이 담지 않는 문제 공간이라 새 Topic 으로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A25-F019", @@ -30277,11 +10981,9 @@ ], "sourceHeading": "순서 계약을 구현하는 것이 없고, 그것을 검증한다는 테스트는 enum 선언 순서만 본다", "summary": "순서 계약을 구현하는 것이 없고, 그것을 검증한다는 테스트는 enum 선언 순서만 본다", - "disposition": "PROMOTE", - "target": "case:an-order-contract-with-no-implementation", - "topic": "drain-and-shutdown-ordering", - "reason": "43개 리프 SSOT 재분해에서 나온 노드. 기존 Topic 이 담지 않는 문제 공간이라 새 Topic 으로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A25-F020", @@ -30292,11 +10994,9 @@ ], "sourceHeading": "새 승인을 거절한다는 메서드가 단계만 기록하고 아무것도 거절하지 않는다", "summary": "새 승인을 거절한다는 메서드가 단계만 기록하고 아무것도 거절하지 않는다", - "disposition": "PROMOTE", - "target": "case:reject-new-admission-that-rejects-nothing", - "topic": "drain-and-shutdown-ordering", - "reason": "43개 리프 SSOT 재분해에서 나온 노드. 기존 Topic 이 담지 않는 문제 공간이라 새 Topic 으로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A25-F021", @@ -30307,11 +11007,9 @@ ], "sourceHeading": "비밀을 힙에서 지우는 마지막 단계가 종료 경로에 연결되지 않았다", "summary": "비밀을 힙에서 지우는 마지막 단계가 종료 경로에 연결되지 않았다", - "disposition": "PROMOTE", - "target": "case:the-last-step-of-secret-erasure-is-not-wired", - "topic": "drain-and-shutdown-ordering", - "reason": "43개 리프 SSOT 재분해에서 나온 노드. 기존 Topic 이 담지 않는 문제 공간이라 새 Topic 으로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A25-F022", @@ -30322,11 +11020,9 @@ ], "sourceHeading": "선언 순서를 단언하는 테스트는 그 순서를 읽는 코드가 있을 때만 게이트다", "summary": "선언 순서를 단언하는 테스트는 그 순서를 읽는 코드가 있을 때만 게이트다", - "disposition": "PROMOTE", - "target": "reference:a-declaration-order-test-is-a-gate-only-if-something-reads-that-order", - "topic": "drain-and-shutdown-ordering", - "reason": "43개 리프 SSOT 재분해에서 나온 노드. 기존 Topic 이 담지 않는 문제 공간이라 새 Topic 으로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A25-F023", @@ -30338,11 +11034,9 @@ ], "sourceHeading": "그 속성을 보일 수 없는 대역 위에서 통과한 테스트는 증인이 아니다", "summary": "그 속성을 보일 수 없는 대역 위에서 통과한 테스트는 증인이 아니다", - "disposition": "PROMOTE", - "target": "reference:a-fake-that-cannot-show-the-property-is-not-a-witness", - "topic": "drain-and-shutdown-ordering", - "reason": "43개 리프 SSOT 재분해에서 나온 노드. 기존 Topic 이 담지 않는 문제 공간이라 새 Topic 으로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A25-F024", @@ -30353,11 +11047,9 @@ ], "sourceHeading": "승인·검증·실행의 분리와 그것을 타입으로 표현하기", "summary": "승인·검증·실행의 분리와 그것을 타입으로 표현하기", - "disposition": "PROMOTE", - "target": "concept:approval-verification-execution", - "topic": "operator-approval-and-destructive-operations", - "reason": "43개 리프 SSOT 재분해에서 나온 노드. 기존 Topic 이 담지 않는 문제 공간이라 새 Topic 으로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A25-F025", @@ -30368,11 +11060,9 @@ ], "sourceHeading": "재개된 리드라이브가 옮기지 못한 메시지를 영구히 건너뛴다", "summary": "재개된 리드라이브가 옮기지 못한 메시지를 영구히 건너뛴다", - "disposition": "PROMOTE", - "target": "case:a-resumed-redrive-skips-what-it-could-not-move", - "topic": "operator-approval-and-destructive-operations", - "reason": "43개 리프 SSOT 재분해에서 나온 노드. 기존 Topic 이 담지 않는 문제 공간이라 새 Topic 으로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A25-F026", @@ -30384,11 +11074,9 @@ ], "sourceHeading": "위조 가능한 승인이 하필 되돌릴 수 없는 작업 쪽에만 남았다", "summary": "위조 가능한 승인이 하필 되돌릴 수 없는 작업 쪽에만 남았다", - "disposition": "PROMOTE", - "target": "case:the-forgeable-approval-survived-on-the-irreversible-half", - "topic": "operator-approval-and-destructive-operations", - "reason": "43개 리프 SSOT 재분해에서 나온 노드. 기존 Topic 이 담지 않는 문제 공간이라 새 Topic 으로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A25-F027", @@ -30399,11 +11087,9 @@ ], "sourceHeading": "BLOCKING 이면 기동이 실패한다는 보장이 어떤 배선에서도 실행되지 않는다", "summary": "BLOCKING 이면 기동이 실패한다는 보장이 어떤 배선에서도 실행되지 않는다", - "disposition": "PROMOTE", - "target": "case:blocking-means-startup-fails-and-nothing-runs-it", - "topic": "operator-approval-and-destructive-operations", - "reason": "43개 리프 SSOT 재분해에서 나온 노드. 기존 Topic 이 담지 않는 문제 공간이라 새 Topic 으로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A25-F028", @@ -30414,11 +11100,9 @@ ], "sourceHeading": "서명 능력과 검증 능력은 같은 객체에 두지 않는다", "summary": "서명 능력과 검증 능력은 같은 객체에 두지 않는다", - "disposition": "PROMOTE", - "target": "reference:signing-and-verifying-do-not-share-an-object", - "topic": "operator-approval-and-destructive-operations", - "reason": "43개 리프 SSOT 재분해에서 나온 노드. 기존 Topic 이 담지 않는 문제 공간이라 새 Topic 으로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A25-F029", @@ -30429,11 +11113,9 @@ ], "sourceHeading": "재개는 인덱스가 아니라 신원으로 한다", "summary": "재개는 인덱스가 아니라 신원으로 한다", - "disposition": "PROMOTE", - "target": "reference:resume-by-identity-not-by-index", - "topic": "operator-approval-and-destructive-operations", - "reason": "43개 리프 SSOT 재분해에서 나온 노드. 기존 Topic 이 담지 않는 문제 공간이라 새 Topic 으로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A25-F030", @@ -30444,11 +11126,9 @@ ], "sourceHeading": "권한 거부가 보안이 아니라 구성 오류로 기록된다", "summary": "권한 거부가 보안이 아니라 구성 오류로 기록된다", - "disposition": "PROMOTE", - "target": "case:an-authorization-denial-recorded-as-a-configuration-error", - "topic": "failure-category-across-adapters", - "reason": "43개 리프 SSOT 재분해에서 나온 노드. 기존 Topic 이 담지 않는 문제 공간이라 새 Topic 으로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A25-F031", @@ -30460,11 +11140,9 @@ ], "sourceHeading": "종료 중이라는 사정이 업무의 영구 실패로 분류된다", "summary": "종료 중이라는 사정이 업무의 영구 실패로 분류된다", - "disposition": "PROMOTE", - "target": "case:a-closing-transport-reported-as-a-permanent-business-failure", - "topic": "failure-category-across-adapters", - "reason": "43개 리프 SSOT 재분해에서 나온 노드. 기존 Topic 이 담지 않는 문제 공간이라 새 Topic 으로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A25-F032", @@ -30476,11 +11154,9 @@ ], "sourceHeading": "실패 범주는 재시도·DLQ·대시보드 사이의 계약이다", "summary": "실패 범주는 재시도·DLQ·대시보드 사이의 계약이다", - "disposition": "PROMOTE", - "target": "reference:a-category-is-a-contract-between-retry-dlq-and-dashboard", - "topic": "failure-category-across-adapters", - "reason": "43개 리프 SSOT 재분해에서 나온 노드. 기존 Topic 이 담지 않는 문제 공간이라 새 Topic 으로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "A25-F033", @@ -30492,11 +11168,9 @@ ], "sourceHeading": "이 세대의 사정은 업무의 영구 실패가 아니다", "summary": "이 세대의 사정은 업무의 영구 실패가 아니다", - "disposition": "PROMOTE", - "target": "reference:this-generations-circumstance-is-not-a-permanent-failure", - "topic": "failure-category-across-adapters", - "reason": "43개 리프 SSOT 재분해에서 나온 노드. 기존 Topic 이 담지 않는 문제 공간이라 새 Topic 으로 냈다.", - "dispositionReview": "PENDING" + "disposition": "KEEP_IN_SSOT", + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_ADMIN-L131", @@ -30507,12 +11181,10 @@ ], "sourceHeading": "`rejectNewAdmission()` 이 단계만 기록하고 아무것도 거절하지 않는다", "summary": "`rejectNewAdmission()` 이 단계만 기록하고 아무것도 거절하지 않는다", - "disposition": "PROMOTE", - "target": "case:grpc-admin-f01", + "disposition": "KEEP_IN_SSOT", "module": "grpc-admin", - "topic": "verification-path-coverage", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_ADMIN-L162", @@ -30523,12 +11195,10 @@ ], "sourceHeading": "비밀 필드 검사가 스냅숏의 네 구획 중 하나에만 적용된다", "summary": "비밀 필드 검사가 스냅숏의 네 구획 중 하나에만 적용된다", - "disposition": "PROMOTE", - "target": "case:grpc-admin-f02", + "disposition": "KEEP_IN_SSOT", "module": "grpc-admin", - "topic": "security-policy-enforcement", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_ADMIN-L181", @@ -30539,12 +11209,10 @@ ], "sourceHeading": "배수 조정자가 가변이고 동기화가 없다", "summary": "배수 조정자가 가변이고 동기화가 없다", - "disposition": "PROMOTE", - "target": "case:grpc-admin-f03", + "disposition": "KEEP_IN_SSOT", "module": "grpc-admin", - "topic": "state-ownership-and-concurrency", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_ADVANCED_BOOTSTRAP-L153", @@ -30555,12 +11223,10 @@ ], "sourceHeading": "등급 재정의에 하한이 없어 \"켤 수 없다\" 는 등급이 켜질 수 있다", "summary": "등급 재정의에 하한이 없어 \"켤 수 없다\" 는 등급이 켜질 수 있다", - "disposition": "PROMOTE", - "target": "case:grpc-advanced-bootstrap-f01", + "disposition": "KEEP_IN_SSOT", "module": "grpc-advanced-bootstrap", - "topic": "verification-path-coverage", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_ADVANCED_BOOTSTRAP-L182", @@ -30571,12 +11237,10 @@ ], "sourceHeading": "승격 게이트가 하향 전이도 승격 규칙으로 판정하고, javadoc 이 약속한 거부는 없다", "summary": "승격 게이트가 하향 전이도 승격 규칙으로 판정하고, javadoc 이 약속한 거부는 없다", - "disposition": "PROMOTE", - "target": "case:grpc-advanced-bootstrap-f02", + "disposition": "KEEP_IN_SSOT", "module": "grpc-advanced-bootstrap", - "topic": "declaration-and-document-drift", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_ADVANCED_BOOTSTRAP-L209", @@ -30587,12 +11251,10 @@ ], "sourceHeading": "깃발 홀더가 가변이고 동기화가 없다", "summary": "깃발 홀더가 가변이고 동기화가 없다", - "disposition": "PROMOTE", - "target": "case:grpc-advanced-bootstrap-f03", + "disposition": "KEEP_IN_SSOT", "module": "grpc-advanced-bootstrap", - "topic": "state-ownership-and-concurrency", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_ADVANCED_BOOTSTRAP-L286", @@ -30603,12 +11265,10 @@ ], "sourceHeading": "`capabilitiesDraggedAlong` 은 독립성을 증명하지 않는다. 상수를 상수와 비교한다", "summary": "`capabilitiesDraggedAlong` 은 독립성을 증명하지 않는다. 상수를 상수와 비교한다", - "disposition": "PROMOTE", - "target": "case:grpc-advanced-bootstrap-f04", + "disposition": "KEEP_IN_SSOT", "module": "grpc-advanced-bootstrap", - "topic": "verification-path-coverage", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_ADVANCED_BOOTSTRAP-L312", @@ -30619,12 +11279,10 @@ ], "sourceHeading": "예외가 들고 있는 능력이 `transient` 라 역직렬화 뒤 사라진다", "summary": "예외가 들고 있는 능력이 `transient` 라 역직렬화 뒤 사라진다", - "disposition": "PROMOTE", - "target": "case:grpc-advanced-bootstrap-f05", + "disposition": "KEEP_IN_SSOT", "module": "grpc-advanced-bootstrap", - "topic": "schema-and-data-contracts", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_ADVANCED_COMPAT-L120", @@ -30635,12 +11293,10 @@ ], "sourceHeading": "통합 다리의 메타데이터 조립이 메타데이터 예산을 검사하지 않는다", "summary": "통합 다리의 메타데이터 조립이 메타데이터 예산을 검사하지 않는다", - "disposition": "PROMOTE", - "target": "case:grpc-advanced-compat-f01", + "disposition": "KEEP_IN_SSOT", "module": "grpc-advanced-compat", - "topic": "runtime-reachability-and-composition", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_ADVANCED_COMPAT-L149", @@ -30651,12 +11307,10 @@ ], "sourceHeading": "반응형 표면 두 타입은 테스트조차 없다", "summary": "반응형 표면 두 타입은 테스트조차 없다", - "disposition": "PROMOTE", - "target": "case:grpc-advanced-compat-f02", + "disposition": "KEEP_IN_SSOT", "module": "grpc-advanced-compat", - "topic": "verification-path-coverage", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_ADVANCED_COMPAT-L162", @@ -30667,12 +11321,10 @@ ], "sourceHeading": "저장소가 참조 프록시 설정을 갖고 있는데, 그것을 판정할 코드에 넣지 않는다", "summary": "저장소가 참조 프록시 설정을 갖고 있는데, 그것을 판정할 코드에 넣지 않는다", - "disposition": "PROMOTE", - "target": "case:grpc-advanced-compat-f03", + "disposition": "KEEP_IN_SSOT", "module": "grpc-advanced-compat", - "topic": "verification-path-coverage", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_ADVANCED_DIAGNOSTICS-L167", @@ -30683,12 +11335,10 @@ ], "sourceHeading": "마스킹이 IPv4 만 알고, 그 결과 \"마스킹되지 않은 주소\" 검사가 나머지 형태를 전부 통과시킨다", "summary": "마스킹이 IPv4 만 알고, 그 결과 \"마스킹되지 않은 주소\" 검사가 나머지 형태를 전부 통과시킨다", - "disposition": "PROMOTE", - "target": "case:grpc-advanced-diagnostics-f01", + "disposition": "KEEP_IN_SSOT", "module": "grpc-advanced-diagnostics", - "topic": "security-policy-enforcement", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_ADVANCED_DIAGNOSTICS-L206", @@ -30699,12 +11349,10 @@ ], "sourceHeading": "금지 필드 검사가 키에만 적용되고 값에는 적용되지 않는다", "summary": "금지 필드 검사가 키에만 적용되고 값에는 적용되지 않는다", - "disposition": "PROMOTE", - "target": "case:grpc-advanced-diagnostics-f02", + "disposition": "KEEP_IN_SSOT", "module": "grpc-advanced-diagnostics", - "topic": "security-policy-enforcement", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_ADVANCED_DIAGNOSTICS-L218", @@ -30715,12 +11363,10 @@ ], "sourceHeading": "\"실환경 증거\" 가 두 리프에 반씩 있고 서로 만나지 않는다", "summary": "\"실환경 증거\" 가 두 리프에 반씩 있고 서로 만나지 않는다", - "disposition": "PROMOTE", - "target": "case:grpc-advanced-diagnostics-f03", + "disposition": "KEEP_IN_SSOT", "module": "grpc-advanced-diagnostics", - "topic": "contract-domain-and-bounds", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_ADVANCED_EDITION-L180", @@ -30731,12 +11377,10 @@ ], "sourceHeading": "비교 픽스처에 비교 대상이 없다", "summary": "비교 픽스처에 비교 대상이 없다", - "disposition": "PROMOTE", - "target": "case:grpc-advanced-edition-f01", + "disposition": "KEEP_IN_SSOT", "module": "grpc-advanced-edition", - "topic": "schema-and-data-contracts", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_ADVANCED_EDITION-L206", @@ -30747,12 +11391,10 @@ ], "sourceHeading": "승격 차단 목록에 담금 기간과 실환경 항목이 없다", "summary": "승격 차단 목록에 담금 기간과 실환경 항목이 없다", - "disposition": "PROMOTE", - "target": "case:grpc-advanced-edition-f02", + "disposition": "KEEP_IN_SSOT", "module": "grpc-advanced-edition", - "topic": "declaration-and-document-drift", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_ADVANCED_EDITION-L216", @@ -30763,12 +11405,10 @@ ], "sourceHeading": "정책의 자바독이 하지 않는 거부를 한다고 적고, 승격 승인이 두 곳에 따로 있다", "summary": "정책의 자바독이 하지 않는 거부를 한다고 적고, 승격 승인이 두 곳에 따로 있다", - "disposition": "PROMOTE", - "target": "case:grpc-advanced-edition-f03", + "disposition": "KEEP_IN_SSOT", "module": "grpc-advanced-edition", - "topic": "security-policy-enforcement", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_ADVANCED_RESILIENCE-L136", @@ -30779,12 +11419,10 @@ ], "sourceHeading": "부트스트랩 대조가 문서 어디든의 부분 문자열을 본다", "summary": "부트스트랩 대조가 문서 어디든의 부분 문자열을 본다", - "disposition": "PROMOTE", - "target": "case:grpc-advanced-resilience-f01", + "disposition": "KEEP_IN_SSOT", "module": "grpc-advanced-resilience", - "topic": "declaration-and-document-drift", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_ADVANCED_RESILIENCE-L155", @@ -30795,12 +11433,10 @@ ], "sourceHeading": "대체 선택기는 사용자 정의 선택기가 받는 보호를 받지 않는다", "summary": "대체 선택기는 사용자 정의 선택기가 받는 보호를 받지 않는다", - "disposition": "PROMOTE", - "target": "case:grpc-advanced-resilience-f02", + "disposition": "KEEP_IN_SSOT", "module": "grpc-advanced-resilience", - "topic": "contract-domain-and-bounds", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_ADVANCED_RESILIENCE-L176", @@ -30811,12 +11447,10 @@ ], "sourceHeading": "리졸버의 개정 가드가 비교 후 교체가 아니다", "summary": "리졸버의 개정 가드가 비교 후 교체가 아니다", - "disposition": "PROMOTE", - "target": "case:grpc-advanced-resilience-f03", + "disposition": "KEEP_IN_SSOT", "module": "grpc-advanced-resilience", - "topic": "state-ownership-and-concurrency", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_ADVANCED_STREAMING-L125", @@ -30827,12 +11461,10 @@ ], "sourceHeading": "클래스가 비판한 무제한 증가를 형제 맵이 그대로 한다", "summary": "클래스가 비판한 무제한 증가를 형제 맵이 그대로 한다", - "disposition": "PROMOTE", - "target": "case:grpc-advanced-streaming-f01", + "disposition": "KEEP_IN_SSOT", "module": "grpc-advanced-streaming", - "topic": "contract-domain-and-bounds", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_ADVANCED_STREAMING-L159", @@ -30843,12 +11475,10 @@ ], "sourceHeading": "클라이언트 스트림 정책의 네 상한 중 둘은 읽는 코드가 없다", "summary": "클라이언트 스트림 정책의 네 상한 중 둘은 읽는 코드가 없다", - "disposition": "PROMOTE", - "target": "case:grpc-advanced-streaming-f02", + "disposition": "KEEP_IN_SSOT", "module": "grpc-advanced-streaming", - "topic": "contract-domain-and-bounds", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_ADVANCED_STREAMING-L180", @@ -30859,12 +11489,10 @@ ], "sourceHeading": "체크포인트 전진이 `ConcurrentMap` 위의 확인 후 쓰기다", "summary": "체크포인트 전진이 `ConcurrentMap` 위의 확인 후 쓰기다", - "disposition": "PROMOTE", - "target": "case:grpc-advanced-streaming-f03", + "disposition": "KEEP_IN_SSOT", "module": "grpc-advanced-streaming", - "topic": "state-ownership-and-concurrency", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_CLIENT-L120", @@ -30875,12 +11503,10 @@ ], "sourceHeading": "`rotate` 가 비교 후 교체가 아니라 덮어쓰기다", "summary": "`rotate` 가 비교 후 교체가 아니라 덮어쓰기다", - "disposition": "PROMOTE", - "target": "case:grpc-client-f01", + "disposition": "KEEP_IN_SSOT", "module": "grpc-client", - "topic": "state-ownership-and-concurrency", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_CLIENT-L149", @@ -30891,12 +11517,10 @@ ], "sourceHeading": "비원자적 감소가 세대를 영구히 회수 불가로 만든다", "summary": "비원자적 감소가 세대를 영구히 회수 불가로 만든다", - "disposition": "PROMOTE", - "target": "case:grpc-client-f02", + "disposition": "KEEP_IN_SSOT", "module": "grpc-client", - "topic": "state-ownership-and-concurrency", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_CLIENT-L176", @@ -30907,12 +11531,10 @@ ], "sourceHeading": "배수 목록의 순회가 동기화 밖에서 일어난다", "summary": "배수 목록의 순회가 동기화 밖에서 일어난다", - "disposition": "PROMOTE", - "target": "case:grpc-client-f03", + "disposition": "KEEP_IN_SSOT", "module": "grpc-client", - "topic": "state-ownership-and-concurrency", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_CLIENT-L199", @@ -30923,12 +11545,10 @@ ], "sourceHeading": "프로파일 검증기가 javadoc 이 든 두 실수 중 하나만 검사한다", "summary": "프로파일 검증기가 javadoc 이 든 두 실수 중 하나만 검사한다", - "disposition": "PROMOTE", - "target": "case:grpc-client-f04", + "disposition": "KEEP_IN_SSOT", "module": "grpc-client", - "topic": "declaration-and-document-drift", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_CODEGEN-L190", @@ -30939,12 +11559,10 @@ ], "sourceHeading": "Buf 수명주기 태스크 목록이 빌드와 대조되지 않는다. 테스트는 목록을 자기 자신과 비교한다", "summary": "Buf 수명주기 태스크 목록이 빌드와 대조되지 않는다. 테스트는 목록을 자기 자신과 비교한다", - "disposition": "PROMOTE", - "target": "case:grpc-codegen-f01", + "disposition": "KEEP_IN_SSOT", "module": "grpc-codegen", - "topic": "verification-path-coverage", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_CODEGEN-L220", @@ -30955,12 +11573,10 @@ ], "sourceHeading": "릴리스 버전 불변성이 프로세스 안에서만 성립한다", "summary": "릴리스 버전 불변성이 프로세스 안에서만 성립한다", - "disposition": "PROMOTE", - "target": "case:grpc-codegen-f02", + "disposition": "KEEP_IN_SSOT", "module": "grpc-codegen", - "topic": "state-ownership-and-concurrency", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_CODEGEN-L239", @@ -30971,12 +11587,10 @@ ], "sourceHeading": "픽스처의 메서드 경로가 서비스 × 메서드 교차곱이다", "summary": "픽스처의 메서드 경로가 서비스 × 메서드 교차곱이다", - "disposition": "PROMOTE", - "target": "case:grpc-codegen-f03", + "disposition": "KEEP_IN_SSOT", "module": "grpc-codegen", - "topic": "schema-and-data-contracts", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_CODEGEN-L287", @@ -30987,12 +11601,10 @@ ], "sourceHeading": "`sha256:` 검사가 길이 15자 이상만 요구한다. 저장소 자신의 테스트가 32자 해시를 통과시킨다", "summary": "`sha256:` 검사가 길이 15자 이상만 요구한다. 저장소 자신의 테스트가 32자 해시를 통과시킨다", - "disposition": "PROMOTE", - "target": "case:grpc-codegen-f04", + "disposition": "KEEP_IN_SSOT", "module": "grpc-codegen", - "topic": "verification-path-coverage", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_CORE_API-L140", @@ -31003,12 +11615,10 @@ ], "sourceHeading": "정책 목록의 가장 강한 성질을 이 저장소에서는 쓸 수 없다", "summary": "정책 목록의 가장 강한 성질을 이 저장소에서는 쓸 수 없다", - "disposition": "PROMOTE", - "target": "case:grpc-core-api-f01", + "disposition": "KEEP_IN_SSOT", "module": "grpc-core-api", - "topic": "runtime-reachability-and-composition", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_CORE_API-L157", @@ -31019,12 +11629,10 @@ ], "sourceHeading": "모듈 목록 테스트가 레지스트리와 목록을 붙들지 않는다", "summary": "모듈 목록 테스트가 레지스트리와 목록을 붙들지 않는다", - "disposition": "PROMOTE", - "target": "case:grpc-core-api-f02", + "disposition": "KEEP_IN_SSOT", "module": "grpc-core-api", - "topic": "verification-path-coverage", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_CORE_API-L180", @@ -31035,12 +11643,10 @@ ], "sourceHeading": "`RESOURCE_EXHAUSTED` 매핑이 그 상태의 두 출처 중 하나만 가정한다", "summary": "`RESOURCE_EXHAUSTED` 매핑이 그 상태의 두 출처 중 하나만 가정한다", - "disposition": "PROMOTE", - "target": "case:grpc-core-api-f03", + "disposition": "KEEP_IN_SSOT", "module": "grpc-core-api", - "topic": "security-policy-enforcement", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_CORE_API-L200", @@ -31051,12 +11657,10 @@ ], "sourceHeading": "하나의 상태 코드가 같은 메서드 안에서 두 답을 갖는다", "summary": "하나의 상태 코드가 같은 메서드 안에서 두 답을 갖는다", - "disposition": "PROMOTE", - "target": "case:grpc-core-api-f04", + "disposition": "KEEP_IN_SSOT", "module": "grpc-core-api", - "topic": "runtime-contract-correctness", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_CORE_API-L219", @@ -31067,12 +11671,10 @@ ], "sourceHeading": "메타데이터 예산의 두 성분 중 하나는 강제되지 않고, 나머지 하나는 바이트가 아니라 문자를 센다", "summary": "메타데이터 예산의 두 성분 중 하나는 강제되지 않고, 나머지 하나는 바이트가 아니라 문자를 센다", - "disposition": "PROMOTE", - "target": "case:grpc-core-api-f05", + "disposition": "KEEP_IN_SSOT", "module": "grpc-core-api", - "topic": "contract-domain-and-bounds", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_CORE_API-L241", @@ -31083,12 +11685,10 @@ ], "sourceHeading": "직렬화 가능하다고 선언한 예외가 자기 내용을 직렬화하지 않는다", "summary": "직렬화 가능하다고 선언한 예외가 자기 내용을 직렬화하지 않는다", - "disposition": "PROMOTE", - "target": "case:grpc-core-api-f06", + "disposition": "KEEP_IN_SSOT", "module": "grpc-core-api", - "topic": "schema-and-data-contracts", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_DISCOVERY-L152", @@ -31099,12 +11699,10 @@ ], "sourceHeading": "프로파일이 스트림 재접속 예산을 선언하는데 그것이 함의하는 DNS 갱신 주기를 정하지 않는다", "summary": "프로파일이 스트림 재접속 예산을 선언하는데 그것이 함의하는 DNS 갱신 주기를 정하지 않는다", - "disposition": "PROMOTE", - "target": "case:grpc-discovery-f01", + "disposition": "KEEP_IN_SSOT", "module": "grpc-discovery", - "topic": "contract-domain-and-bounds", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_DISCOVERY-L177", @@ -31115,12 +11713,10 @@ ], "sourceHeading": "리졸버 검증기의 규칙이 하나뿐인데 javadoc 은 복수형으로 서술한다", "summary": "리졸버 검증기의 규칙이 하나뿐인데 javadoc 은 복수형으로 서술한다", - "disposition": "PROMOTE", - "target": "case:grpc-discovery-f02", + "disposition": "KEEP_IN_SSOT", "module": "grpc-discovery", - "topic": "declaration-and-document-drift", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_DISCOVERY-L187", @@ -31131,12 +11727,10 @@ ], "sourceHeading": "목록으로 보고하는 검증기가 주소 수 0 에서 던진다", "summary": "목록으로 보고하는 검증기가 주소 수 0 에서 던진다", - "disposition": "PROMOTE", - "target": "case:grpc-discovery-f03", + "disposition": "KEEP_IN_SSOT", "module": "grpc-discovery", - "topic": "state-ownership-and-concurrency", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_OBSERVABILITY-L197", @@ -31147,12 +11741,10 @@ ], "sourceHeading": "`queueHighWatermark` 는 요구되고 검증되지만 아무도 읽지 않는다", "summary": "`queueHighWatermark` 는 요구되고 검증되지만 아무도 읽지 않는다", - "disposition": "PROMOTE", - "target": "case:grpc-observability-f01", + "disposition": "KEEP_IN_SSOT", "module": "grpc-observability", - "topic": "runtime-reachability-and-composition", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_OBSERVABILITY-L213", @@ -31163,12 +11755,10 @@ ], "sourceHeading": "`deadlineRemaining` 도 meter 가 없다. javadoc 은 그것이 기록된다고 말한다", "summary": "`deadlineRemaining` 도 meter 가 없다. javadoc 은 그것이 기록된다고 말한다", - "disposition": "PROMOTE", - "target": "case:grpc-observability-f02", + "disposition": "KEEP_IN_SSOT", "module": "grpc-observability", - "topic": "declaration-and-document-drift", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_OBSERVABILITY-L238", @@ -31179,12 +11769,10 @@ ], "sourceHeading": "허용 태그 8개 중 둘은 값이 자유 문자열이고, 그중 하나는 bounded 열거형이 이미 존재한다", "summary": "허용 태그 8개 중 둘은 값이 자유 문자열이고, 그중 하나는 bounded 열거형이 이미 존재한다", - "disposition": "PROMOTE", - "target": "case:grpc-observability-f03", + "disposition": "KEEP_IN_SSOT", "module": "grpc-observability", - "topic": "runtime-contract-correctness", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_OPERATION_LEDGER_JPA-L184", @@ -31195,12 +11783,10 @@ ], "sourceHeading": "낙관적 잠금 컬럼이 없어 전이 가드가 메모리 안에만 있다", "summary": "낙관적 잠금 컬럼이 없어 전이 가드가 메모리 안에만 있다", - "disposition": "PROMOTE", - "target": "case:grpc-operation-ledger-jpa-f01", + "disposition": "KEEP_IN_SSOT", "module": "grpc-operation-ledger-jpa", - "topic": "contract-domain-and-bounds", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_OPERATION_LEDGER_JPA-L192", @@ -31211,12 +11797,10 @@ ], "sourceHeading": "`markCommitted` 는 던지고 `markFailed` 는 조용히 넘어간다", "summary": "`markCommitted` 는 던지고 `markFailed` 는 조용히 넘어간다", - "disposition": "PROMOTE", - "target": "case:grpc-operation-ledger-jpa-f02", + "disposition": "KEEP_IN_SSOT", "module": "grpc-operation-ledger-jpa", - "topic": "state-ownership-and-concurrency", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_POLICY-L184", @@ -31227,12 +11811,10 @@ ], "sourceHeading": "스트림 승인의 경계가 동시성 아래에서 새고, caller별 맵이 줄지 않는다", "summary": "스트림 승인의 경계가 동시성 아래에서 새고, caller별 맵이 줄지 않는다", - "disposition": "PROMOTE", - "target": "case:grpc-policy-f01", + "disposition": "KEEP_IN_SSOT", "module": "grpc-policy", - "topic": "security-policy-enforcement", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_POLICY-L204", @@ -31243,12 +11825,10 @@ ], "sourceHeading": "자격증명 회전이 비교 후 교체가 아니고, 배수 완료가 진행 중인 회전을 되돌릴 수 있다", "summary": "자격증명 회전이 비교 후 교체가 아니고, 배수 완료가 진행 중인 회전을 되돌릴 수 있다", - "disposition": "PROMOTE", - "target": "case:grpc-policy-f02", + "disposition": "KEEP_IN_SSOT", "module": "grpc-policy", - "topic": "security-policy-enforcement", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_POLICY-L249", @@ -31259,12 +11839,10 @@ ], "sourceHeading": "직렬 스트림 기록기의 가장 오래된 것 버리기가 잘못된 메시지의 바이트를 뺀다", "summary": "직렬 스트림 기록기의 가장 오래된 것 버리기가 잘못된 메시지의 바이트를 뺀다", - "disposition": "PROMOTE", - "target": "case:grpc-policy-f03", + "disposition": "KEEP_IN_SSOT", "module": "grpc-policy", - "topic": "schema-and-data-contracts", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_POLICY-L271", @@ -31275,12 +11853,10 @@ ], "sourceHeading": "완료 조정자가 요청 경로에서 동기화 없는 가변 리스트를 변경한다", "summary": "완료 조정자가 요청 경로에서 동기화 없는 가변 리스트를 변경한다", - "disposition": "PROMOTE", - "target": "case:grpc-policy-f04", + "disposition": "KEEP_IN_SSOT", "module": "grpc-policy", - "topic": "state-ownership-and-concurrency", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_POLICY-L285", @@ -31291,12 +11867,10 @@ ], "sourceHeading": "스트림 수명 조정자의 배수 신호가 스레드를 건너면서 `volatile` 이 아니다", "summary": "스트림 수명 조정자의 배수 신호가 스레드를 건너면서 `volatile` 이 아니다", - "disposition": "PROMOTE", - "target": "case:grpc-policy-f05", + "disposition": "KEEP_IN_SSOT", "module": "grpc-policy", - "topic": "state-ownership-and-concurrency", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_POLICY-L305", @@ -31307,12 +11881,10 @@ ], "sourceHeading": "오류 노출 거부 목록의 \"호스트와 포트\" 규칙이 IPv4 점표기만 본다", "summary": "오류 노출 거부 목록의 \"호스트와 포트\" 규칙이 IPv4 점표기만 본다", - "disposition": "PROMOTE", - "target": "case:grpc-policy-f06", + "disposition": "KEEP_IN_SSOT", "module": "grpc-policy", - "topic": "declaration-and-document-drift", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_POLICY-L324", @@ -31323,12 +11895,10 @@ ], "sourceHeading": "`clearAfterTask` 는 합법 값이 하나뿐인 성분이고, 아무도 읽지 않는다", "summary": "`clearAfterTask` 는 합법 값이 하나뿐인 성분이고, 아무도 읽지 않는다", - "disposition": "PROMOTE", - "target": "case:grpc-policy-f07", + "disposition": "KEEP_IN_SSOT", "module": "grpc-policy", - "topic": "runtime-reachability-and-composition", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_PROTO_CONTRACT-L175", @@ -31339,12 +11909,10 @@ ], "sourceHeading": "`reserved 2 to 5;` 범위가 개별 숫자로만 수집되어 `RESERVED_HISTORY` 오탐이 된다", "summary": "`reserved 2 to 5;` 범위가 개별 숫자로만 수집되어 `RESERVED_HISTORY` 오탐이 된다", - "disposition": "PROMOTE", - "target": "case:grpc-proto-contract-f01", + "disposition": "KEEP_IN_SSOT", "module": "grpc-proto-contract", - "topic": "schema-and-data-contracts", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_PROTO_CONTRACT-L191", @@ -31355,12 +11923,10 @@ ], "sourceHeading": "반환 목록이 자바독이 약속한 source order 가 아니다", "summary": "반환 목록이 자바독이 약속한 source order 가 아니다", - "disposition": "PROMOTE", - "target": "case:grpc-proto-contract-f02", + "disposition": "KEEP_IN_SSOT", "module": "grpc-proto-contract", - "topic": "declaration-and-document-drift", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_PROTO_CONTRACT-L203", @@ -31371,12 +11937,10 @@ ], "sourceHeading": "커밋 스키마 게이트가 파일 목록을 하드코딩한다", "summary": "커밋 스키마 게이트가 파일 목록을 하드코딩한다", - "disposition": "PROMOTE", - "target": "case:grpc-proto-contract-f03", + "disposition": "KEEP_IN_SSOT", "module": "grpc-proto-contract", - "topic": "schema-and-data-contracts", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_PROTO_CONTRACT-L265", @@ -31387,12 +11951,10 @@ ], "sourceHeading": "열거형 안의 `reserved` 는 수집되지 않는다", "summary": "열거형 안의 `reserved` 는 수집되지 않는다", - "disposition": "PROMOTE", - "target": "case:grpc-proto-contract-f04", + "disposition": "KEEP_IN_SSOT", "module": "grpc-proto-contract", - "topic": "schema-and-data-contracts", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_SERVER-L140", @@ -31403,12 +11965,10 @@ ], "sourceHeading": "두 아키텍처 규칙이 저장소 소스에 적용되지 않는다", "summary": "두 아키텍처 규칙이 저장소 소스에 적용되지 않는다", - "disposition": "PROMOTE", - "target": "case:grpc-server-f01", + "disposition": "KEEP_IN_SSOT", "module": "grpc-server", - "topic": "verification-path-coverage", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_SERVER-L167", @@ -31419,12 +11979,10 @@ ], "sourceHeading": "원시 API 규칙이 import 문만 보므로 완전 수식 사용과 와일드카드를 놓친다", "summary": "원시 API 규칙이 import 문만 보므로 완전 수식 사용과 와일드카드를 놓친다", - "disposition": "PROMOTE", - "target": "case:grpc-server-f02", + "disposition": "KEEP_IN_SSOT", "module": "grpc-server", - "topic": "declaration-and-document-drift", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_SERVER-L196", @@ -31435,12 +11993,10 @@ ], "sourceHeading": "빌더 경로에서 순서 규칙 넷 중 셋이 발화할 수 없다", "summary": "빌더 경로에서 순서 규칙 넷 중 셋이 발화할 수 없다", - "disposition": "PROMOTE", - "target": "case:grpc-server-f03", + "disposition": "KEEP_IN_SSOT", "module": "grpc-server", - "topic": "runtime-reachability-and-composition", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_SERVER-L211", @@ -31451,12 +12007,10 @@ ], "sourceHeading": "승인 제어기의 세 메서드가 원자적이지 않고, 큐 계수기를 되돌리는 경로가 없다", "summary": "승인 제어기의 세 메서드가 원자적이지 않고, 큐 계수기를 되돌리는 경로가 없다", - "disposition": "PROMOTE", - "target": "case:grpc-server-f04", + "disposition": "KEEP_IN_SSOT", "module": "grpc-server", - "topic": "security-policy-enforcement", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_SPRING_BOOT_STARTER-L183", @@ -31467,12 +12021,10 @@ ], "sourceHeading": "시작 검증기가 시작 시 실행되지 않는다", "summary": "시작 검증기가 시작 시 실행되지 않는다", - "disposition": "PROMOTE", - "target": "case:grpc-spring-boot-starter-f01", + "disposition": "KEEP_IN_SSOT", "module": "grpc-spring-boot-starter", - "topic": "runtime-reachability-and-composition", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_SPRING_BOOT_STARTER-L221", @@ -31483,12 +12035,10 @@ ], "sourceHeading": "자동 설정이 `transport` 를 읽지 않고 전송을 하드코딩한다", "summary": "자동 설정이 `transport` 를 읽지 않고 전송을 하드코딩한다", - "disposition": "PROMOTE", - "target": "case:grpc-spring-boot-starter-f02", + "disposition": "KEEP_IN_SSOT", "module": "grpc-spring-boot-starter", - "topic": "transport-and-provider-semantics", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_SPRING_BOOT_STARTER-L236", @@ -31499,12 +12049,10 @@ ], "sourceHeading": "`default-unary-deadline` 은 읽는 코드가 저장소에 없다", "summary": "`default-unary-deadline` 은 읽는 코드가 저장소에 없다", - "disposition": "PROMOTE", - "target": "case:grpc-spring-boot-starter-f03", + "disposition": "KEEP_IN_SSOT", "module": "grpc-spring-boot-starter", - "topic": "runtime-reachability-and-composition", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_SPRING_BOOT_STARTER-L249", @@ -31515,12 +12063,10 @@ ], "sourceHeading": "반사 모드를 명시하면 서비스·역할 허용 목록이 조용히 하드코딩으로 바뀐다", "summary": "반사 모드를 명시하면 서비스·역할 허용 목록이 조용히 하드코딩으로 바뀐다", - "disposition": "PROMOTE", - "target": "case:grpc-spring-boot-starter-f04", + "disposition": "KEEP_IN_SSOT", "module": "grpc-spring-boot-starter", - "topic": "runtime-contract-correctness", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_TESTKIT-L153", @@ -31531,12 +12077,10 @@ ], "sourceHeading": "네 레인이 `check` 에 붙지 않고, 이 가족을 이름으로 부르는 워크플로가 없다", "summary": "네 레인이 `check` 에 붙지 않고, 이 가족을 이름으로 부르는 워크플로가 없다", - "disposition": "PROMOTE", - "target": "case:grpc-testkit-f01", + "disposition": "KEEP_IN_SSOT", "module": "grpc-testkit", - "topic": "verification-path-coverage", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_TESTKIT-L172", @@ -31547,12 +12091,10 @@ ], "sourceHeading": "릴리스 게이트의 입력이 전부 호출자가 손으로 만드는 값이다", "summary": "릴리스 게이트의 입력이 전부 호출자가 손으로 만드는 값이다", - "disposition": "PROMOTE", - "target": "case:grpc-testkit-f02", + "disposition": "KEEP_IN_SSOT", "module": "grpc-testkit", - "topic": "state-ownership-and-concurrency", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_TESTKIT-L187", @@ -31563,12 +12105,10 @@ ], "sourceHeading": "고장 레인의 유일한 실소켓 시험이 자기가 관측한 것을 버리고 리터럴로 증거를 만든다", "summary": "고장 레인의 유일한 실소켓 시험이 자기가 관측한 것을 버리고 리터럴로 증거를 만든다", - "disposition": "PROMOTE", - "target": "case:grpc-testkit-f03", + "disposition": "KEEP_IN_SSOT", "module": "grpc-testkit", - "topic": "verification-path-coverage", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_TESTKIT-L233", @@ -31579,12 +12119,10 @@ ], "sourceHeading": "호환성 표의 레인 이름과 빌드의 레인 이름이 서로 다른 집합이다", "summary": "호환성 표의 레인 이름과 빌드의 레인 이름이 서로 다른 집합이다", - "disposition": "PROMOTE", - "target": "case:grpc-testkit-f04", + "disposition": "KEEP_IN_SSOT", "module": "grpc-testkit", - "topic": "verification-path-coverage", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_TESTKIT-L245", @@ -31595,12 +12133,10 @@ ], "sourceHeading": "계약 스위트 둘이 결과를 만드는 코드를 갖지 않는다", "summary": "계약 스위트 둘이 결과를 만드는 코드를 갖지 않는다", - "disposition": "PROMOTE", - "target": "case:grpc-testkit-f05", + "disposition": "KEEP_IN_SSOT", "module": "grpc-testkit", - "topic": "runtime-reachability-and-composition", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-GRPC_TESTKIT-L262", @@ -31611,12 +12147,10 @@ ], "sourceHeading": "던져 버릴 비밀번호를 만들어 놓고 외부 프로세스의 명령줄에 싣는다", "summary": "던져 버릴 비밀번호를 만들어 놓고 외부 프로세스의 명령줄에 싣는다", - "disposition": "PROMOTE", - "target": "case:grpc-testkit-f06", + "disposition": "KEEP_IN_SSOT", "module": "grpc-testkit", - "topic": "security-policy-enforcement", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_ADMIN_API-L915", @@ -31627,12 +12161,10 @@ ], "sourceHeading": "`DestructiveOperationGuard` 의 두 분기가 문서에도 없고 테스트에도 없다", "summary": "`DestructiveOperationGuard` 의 두 분기가 문서에도 없고 테스트에도 없다", - "disposition": "PROMOTE", - "target": "case:messaging-admin-api-f01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-admin-api", - "topic": "verification-path-coverage", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_ADMIN_API-L944", @@ -31643,12 +12175,10 @@ ], "sourceHeading": "계획 다이제스트가 승인 정규 형식과 다른 인코딩을 쓴다", "summary": "계획 다이제스트가 승인 정규 형식과 다른 인코딩을 쓴다", - "disposition": "PROMOTE", - "target": "case:messaging-admin-api-f02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-admin-api", - "topic": "security-policy-enforcement", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_ADMIN_API-L952", @@ -31659,12 +12189,10 @@ ], "sourceHeading": "`TopologyManagementMode` 가 어디에도 연결되어 있지 않다", "summary": "`TopologyManagementMode` 가 어디에도 연결되어 있지 않다", - "disposition": "PROMOTE", - "target": "case:messaging-admin-api-f03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-admin-api", - "topic": "runtime-reachability-and-composition", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_ADMIN_API-L956", @@ -31675,12 +12203,10 @@ ], "sourceHeading": "운영자용 표면 전체에 프로덕션 소비자가 없다", "summary": "운영자용 표면 전체에 프로덕션 소비자가 없다", - "disposition": "PROMOTE", - "target": "case:messaging-admin-api-f04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-admin-api", - "topic": "runtime-reachability-and-composition", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_ADMIN_API-L962", @@ -31691,12 +12217,10 @@ ], "sourceHeading": "`VerifiedApproval` 의 위조 방지가 package-private 에만 의존한다", "summary": "`VerifiedApproval` 의 위조 방지가 package-private 에만 의존한다", - "disposition": "PROMOTE", - "target": "case:messaging-admin-api-f05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-admin-api", - "topic": "security-policy-enforcement", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_ADMIN_API-L968", @@ -31707,12 +12231,10 @@ ], "sourceHeading": "`messaging-policy` 의존이 import 0건이다", "summary": "`messaging-policy` 의존이 import 0건이다", - "disposition": "PROMOTE", - "target": "case:messaging-admin-api-f06", + "disposition": "KEEP_IN_SSOT", "module": "messaging-admin-api", - "topic": "runtime-reachability-and-composition", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_ADMIN_API-L972", @@ -31723,12 +12245,10 @@ ], "sourceHeading": "같은 인가 실패 코드가 세 파일에 문자열 리터럴로 흩어져 있다", "summary": "같은 인가 실패 코드가 세 파일에 문자열 리터럴로 흩어져 있다", - "disposition": "PROMOTE", - "target": "case:messaging-admin-api-f07", + "disposition": "KEEP_IN_SSOT", "module": "messaging-admin-api", - "topic": "security-policy-enforcement", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_ADMIN_RUNTIME-L883", @@ -31739,12 +12259,10 @@ ], "sourceHeading": "파괴적 작업의 승인만 위조 가능한 형태로 남아 있다", "summary": "파괴적 작업의 승인만 위조 가능한 형태로 남아 있다", - "disposition": "PROMOTE", - "target": "case:messaging-admin-runtime-f01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-admin-runtime", - "topic": "security-policy-enforcement", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_ADMIN_RUNTIME-L910", @@ -31755,12 +12273,10 @@ ], "sourceHeading": "토폴로지 검증 스택이 두 벌이고 판정이 어긋난다", "summary": "토폴로지 검증 스택이 두 벌이고 판정이 어긋난다", - "disposition": "PROMOTE", - "target": "case:messaging-admin-runtime-f02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-admin-runtime", - "topic": "transport-and-provider-semantics", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_ADMIN_RUNTIME-L918", @@ -31771,12 +12287,10 @@ ], "sourceHeading": "오케스트레이터가 어디에서도 실행되지 않는다", "summary": "오케스트레이터가 어디에서도 실행되지 않는다", - "disposition": "PROMOTE", - "target": "case:messaging-admin-runtime-f03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-admin-runtime", - "topic": "runtime-reachability-and-composition", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_ADMIN_RUNTIME-L924", @@ -31787,12 +12301,10 @@ ], "sourceHeading": "public 인터페이스를 패키지 밖에서 구현할 수 없다", "summary": "public 인터페이스를 패키지 밖에서 구현할 수 없다", - "disposition": "PROMOTE", - "target": "case:messaging-admin-runtime-f04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-admin-runtime", - "topic": "runtime-reachability-and-composition", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_ADMIN_RUNTIME-L930", @@ -31803,12 +12315,10 @@ ], "sourceHeading": "감사 싱크가 중복 선언되어 있고 레닥션 계약이 유실된다", "summary": "감사 싱크가 중복 선언되어 있고 레닥션 계약이 유실된다", - "disposition": "PROMOTE", - "target": "case:messaging-admin-runtime-f05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-admin-runtime", - "topic": "security-policy-enforcement", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_ADMIN_RUNTIME-L936", @@ -31819,12 +12329,10 @@ ], "sourceHeading": "저널의 `itemsCompleted` 단조성이 인터페이스 계약에 없다", "summary": "저널의 `itemsCompleted` 단조성이 인터페이스 계약에 없다", - "disposition": "PROMOTE", - "target": "case:messaging-admin-runtime-f06", + "disposition": "KEEP_IN_SSOT", "module": "messaging-admin-runtime", - "topic": "state-ownership-and-concurrency", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_ADMIN_RUNTIME-L942", @@ -31835,12 +12343,10 @@ ], "sourceHeading": "리플레이가 리스를 받지만 재개하지 않는다", "summary": "리플레이가 리스를 받지만 재개하지 않는다", - "disposition": "PROMOTE", - "target": "case:messaging-admin-runtime-f07", + "disposition": "KEEP_IN_SSOT", "module": "messaging-admin-runtime", - "topic": "state-ownership-and-concurrency", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_ADMIN_RUNTIME-L948", @@ -31851,12 +12357,10 @@ ], "sourceHeading": "격리 리플레이의 guard 우회가 `dryRun` 파라미터로 표현된다", "summary": "격리 리플레이의 guard 우회가 `dryRun` 파라미터로 표현된다", - "disposition": "PROMOTE", - "target": "case:messaging-admin-runtime-f08", + "disposition": "KEEP_IN_SSOT", "module": "messaging-admin-runtime", - "topic": "security-policy-enforcement", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_ADMIN_RUNTIME-L957", @@ -31867,12 +12371,10 @@ ], "sourceHeading": "선언된 의존 6개 중 3개가 import 0건", "summary": "선언된 의존 6개 중 3개가 import 0건", - "disposition": "PROMOTE", - "target": "case:messaging-admin-runtime-f09", + "disposition": "KEEP_IN_SSOT", "module": "messaging-admin-runtime", - "topic": "runtime-reachability-and-composition", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_ADMIN_RUNTIME-L961", @@ -31883,12 +12385,10 @@ ], "sourceHeading": "실패한 리드라이브 항목의 사유가 어디에도 남지 않는다", "summary": "실패한 리드라이브 항목의 사유가 어디에도 남지 않는다", - "disposition": "PROMOTE", - "target": "case:messaging-admin-runtime-f10", + "disposition": "KEEP_IN_SSOT", "module": "messaging-admin-runtime", - "topic": "transport-and-provider-semantics", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_CLAIM_CHECK-L507", @@ -31899,12 +12399,10 @@ ], "sourceHeading": "배포 아티팩트가 싣지만 아무도 부르지 않고, 다른 곳의 에러 메시지가 이 경로를 권한다", "summary": "배포 아티팩트가 싣지만 아무도 부르지 않고, 다른 곳의 에러 메시지가 이 경로를 권한다", - "disposition": "PROMOTE", - "target": "case:messaging-claim-check-f01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-claim-check", - "topic": "runtime-reachability-and-composition", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_CLAIM_CHECK-L516", @@ -31915,12 +12413,10 @@ ], "sourceHeading": "claim check 문턱이 두 곳에서 독립적으로 정해진다", "summary": "같은 튜닝 값이 두 계층에 있으면 어느 쪽이 이기는지 정한다", - "disposition": "PROMOTE", - "target": "reference:messaging-claim-check-f02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-claim-check", - "topic": "state-ownership-and-concurrency", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_CLAIM_CHECK-L525", @@ -31931,12 +12427,10 @@ ], "sourceHeading": "예외 승격이 에러 코드 문자열 접미사에 의존한다", "summary": "예외 승격이 에러 코드 문자열 접미사에 의존한다", - "disposition": "PROMOTE", - "target": "case:messaging-claim-check-f03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-claim-check", - "topic": "runtime-contract-correctness", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_CLAIM_CHECK-L534", @@ -31947,12 +12441,10 @@ ], "sourceHeading": "`ClaimCheckPublisher`가 이 leaf의 테스트에 등장하지 않는다", "summary": "leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다", - "disposition": "PROMOTE", - "target": "reference:messaging-claim-check-f04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-claim-check", - "topic": "runtime-reachability-and-composition", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_CLAIM_CHECK-L543", @@ -31963,12 +12455,10 @@ ], "sourceHeading": "보존 sweep이 없다", "summary": "보존 sweep이 없다", - "disposition": "PROMOTE", - "target": "open-question:messaging-claim-check-f05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-claim-check", - "topic": "contract-domain-and-bounds", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_CLOUDEVENTS-L518", @@ -31979,12 +12469,10 @@ ], "sourceHeading": "상호운용을 위한 매퍼가 명세 준수 이벤트를 분류되지 않은 예외로 거절한다", "summary": "상호운용을 위한 매퍼가 명세 준수 이벤트를 분류되지 않은 예외로 거절한다", - "disposition": "PROMOTE", - "target": "case:messaging-cloudevents-f01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-cloudevents", - "topic": "verification-path-coverage", - "reason": "SSOT 의 `다음 단계` 가 CASE 후보라고 명시한다. 같은 줄 뒤의 DECISION 언급은 선택지 (b) 에 대한 것이고 SSOT 스스로 `NEEDS_DECISION` 이라고 적었으므로 emit 대상이 아니다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_CLOUDEVENTS-L529", @@ -31995,12 +12483,10 @@ ], "sourceHeading": "배포 아티팩트가 싣지만 아무도 부르지 않는다", "summary": "배포 아티팩트가 싣지만 아무도 부르지 않는다", - "disposition": "PROMOTE", - "target": "case:messaging-cloudevents-f02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-cloudevents", - "topic": "runtime-reachability-and-composition", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_CLOUDEVENTS-L538", @@ -32011,12 +12497,10 @@ ], "sourceHeading": "왕복이 다섯 필드를 버리고, 테스트가 그 필드를 비교하지 않는다", "summary": "왕복이라 부르는 테스트는 무엇을 보존하지 않는지도 적는다", - "disposition": "PROMOTE", - "target": "reference:messaging-cloudevents-f03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-cloudevents", - "topic": "verification-path-coverage", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_CLOUDEVENTS-L547", @@ -32027,12 +12511,10 @@ ], "sourceHeading": "`dataschema`가 채워질 경로가 없다", "summary": "`dataschema`가 채워질 경로가 없다", - "disposition": "PROMOTE", - "target": "open-question:messaging-cloudevents-f04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-cloudevents", - "topic": "schema-and-data-contracts", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_CLOUDEVENTS-L556", @@ -32043,12 +12525,10 @@ ], "sourceHeading": "`CloudEventMapper` javadoc의 범위 제한이 강제되지 않는다", "summary": "문서가 범위를 제한하면 그 제한을 누가 강제하는지 같이 적는다", - "disposition": "PROMOTE", - "target": "reference:messaging-cloudevents-f05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-cloudevents", - "topic": "declaration-and-document-drift", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_CORE_API-L835", @@ -32059,12 +12539,10 @@ ], "sourceHeading": "선언된 핸들러 계약이 배선된 것과 다르다", "summary": "선언된 핸들러 계약이 배선된 것과 다르다", - "disposition": "PROMOTE", - "target": "open-question:messaging-core-api-f01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-core-api", - "topic": "runtime-reachability-and-composition", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_CORE_API-L844", @@ -32075,12 +12553,10 @@ ], "sourceHeading": "배치 metadata를 만들고 넘길 곳이 없다", "summary": "배치 metadata를 만들고 넘길 곳이 없다", - "disposition": "PROMOTE", - "target": "case:messaging-core-api-f02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-core-api", - "topic": "transport-and-provider-semantics", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_CORE_API-L862", @@ -32091,12 +12567,10 @@ ], "sourceHeading": "12개 예외가 선언만 되어 있다", "summary": "12개 예외가 선언만 되어 있다", - "disposition": "PROMOTE", - "target": "open-question:messaging-core-api-f03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-core-api", - "topic": "runtime-reachability-and-composition", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_CORE_API-L871", @@ -32107,12 +12581,10 @@ ], "sourceHeading": "`MessagingRedactor`가 상수 대신 문자열 리터럴을 쓴다", "summary": "`MessagingRedactor`가 상수 대신 문자열 리터럴을 쓴다", - "disposition": "PROMOTE", - "target": "case:messaging-core-api-f04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-core-api", - "topic": "runtime-contract-correctness", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_CORE_API-L880", @@ -32123,12 +12595,10 @@ ], "sourceHeading": "`WireSafeText`의 규칙이 leaf 경계에서 멈춘다", "summary": "`WireSafeText`의 규칙이 leaf 경계에서 멈춘다", - "disposition": "PROMOTE", - "target": "case:messaging-core-api-f05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-core-api", - "topic": "contract-domain-and-bounds", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_INBOX_JDBC_POSTGRESQL-L647", @@ -32139,12 +12609,10 @@ ], "sourceHeading": "bounded purge가 구현돼 있고 호출되지 않아, cleanup이 스스로 막겠다고 한 장애를 일으킨다", "summary": "bounded purge가 구현돼 있고 호출되지 않아, cleanup이 스스로 막겠다고 한 장애를 일으킨다", - "disposition": "PROMOTE", - "target": "case:messaging-inbox-jdbc-postgresql-f01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-inbox-jdbc-postgresql", - "topic": "runtime-reachability-and-composition", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_INBOX_JDBC_POSTGRESQL-L657", @@ -32155,12 +12623,10 @@ ], "sourceHeading": "속성을 이름으로 주장하는 테스트가 그 속성을 보일 수 없는 fake 위에서 통과한다", "summary": "속성을 이름으로 주장하는 테스트가 그 속성을 보일 수 없는 fake 위에서 통과한다", - "disposition": "PROMOTE", - "target": "case:messaging-inbox-jdbc-postgresql-f02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-inbox-jdbc-postgresql", - "topic": "verification-path-coverage", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_INBOX_JDBC_POSTGRESQL-L666", @@ -32171,12 +12637,10 @@ ], "sourceHeading": "SQL 실패가 재시도 불가로 분류된다", "summary": "SQL 실패가 재시도 불가로 분류된다", - "disposition": "PROMOTE", - "target": "case:messaging-inbox-jdbc-postgresql-f03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-inbox-jdbc-postgresql", - "topic": "transport-and-provider-semantics", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_INBOX_JDBC_POSTGRESQL-L675", @@ -32187,12 +12651,10 @@ ], "sourceHeading": "세 갈래 판정이 포트의 `boolean`에서 두 갈래로 접힌다", "summary": "세 갈래 판정이 포트의 `boolean`에서 두 갈래로 접힌다", - "disposition": "PROMOTE", - "target": "open-question:messaging-inbox-jdbc-postgresql-f04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-inbox-jdbc-postgresql", - "topic": "declaration-and-document-drift", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_INBOX_JDBC_POSTGRESQL-L684", @@ -32203,12 +12665,10 @@ ], "sourceHeading": "`consumer_id` 길이 제약이 애플리케이션 층에 없다", "summary": "컬럼 폭은 애플리케이션 검증과 짝을 이룬다", - "disposition": "PROMOTE", - "target": "reference:messaging-inbox-jdbc-postgresql-f05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-inbox-jdbc-postgresql", - "topic": "contract-domain-and-bounds", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_INBOX_JDBC_POSTGRESQL-L693", @@ -32219,12 +12679,10 @@ ], "sourceHeading": "보존 규칙이 세 곳에 있고 공식이 다르다", "summary": "같은 안전 규칙은 한 공식과 한 강제 시점을 갖는다", - "disposition": "PROMOTE", - "target": "reference:messaging-inbox-jdbc-postgresql-f06", + "disposition": "KEEP_IN_SSOT", "module": "messaging-inbox-jdbc-postgresql", - "topic": "contract-domain-and-bounds", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_KAFKA-L208", @@ -32235,12 +12693,10 @@ ], "sourceHeading": "지원 문서가 `deduplicatedPublish` 를 지원으로 적고, 코드는 거짓이며, 그 차이가 정확히 코드가 경고한 피해다", "summary": "지원 문서가 `deduplicatedPublish` 를 지원으로 적고, 코드는 거짓이며, 그 차이가 정확히 코드가 경고한 피해다", - "disposition": "PROMOTE", - "target": "case:messaging-kafka-f01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-kafka", - "topic": "declaration-and-document-drift", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_KAFKA-L265", @@ -32251,12 +12707,10 @@ ], "sourceHeading": "천장에 닿아 일시정지된 파티션을 재개하는 경로가 없다", "summary": "천장에 닿아 일시정지된 파티션을 재개하는 경로가 없다", - "disposition": "PROMOTE", - "target": "case:messaging-kafka-f02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-kafka", - "topic": "transport-and-provider-semantics", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_KAFKA-L301", @@ -32267,12 +12721,10 @@ ], "sourceHeading": "오염된 재시도 헤더가 격리되지 않고 무한 pause-and-seek 을 만든다", "summary": "오염된 재시도 헤더가 격리되지 않고 무한 pause-and-seek 을 만든다", - "disposition": "PROMOTE", - "target": "case:messaging-kafka-f03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-kafka", - "topic": "transport-and-provider-semantics", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_KAFKA-L342", @@ -32283,12 +12735,10 @@ ], "sourceHeading": "시계를 주입받는 클래스가 한 곳에서만 벽시계를 읽는다", "summary": "시계를 주입받는 클래스가 한 곳에서만 벽시계를 읽는다", - "disposition": "PROMOTE", - "target": "case:messaging-kafka-f04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-kafka", - "topic": "transport-and-provider-semantics", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_KAFKA-L362", @@ -32299,12 +12749,10 @@ ], "sourceHeading": "결함으로 판정된 메서드가 남아 있고, 실브로커 증명이 그것 위에서 돈다", "summary": "결함으로 판정된 메서드가 남아 있고, 실브로커 증명이 그것 위에서 돈다", - "disposition": "PROMOTE", - "target": "case:messaging-kafka-f05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-kafka", - "topic": "transport-and-provider-semantics", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_KAFKA_SHARE_EXPERIMENTAL-L467", @@ -32315,12 +12763,10 @@ ], "sourceHeading": "\"등록\"이 아무것도 등록하지 않고 성공을 반환한다", "summary": "\"등록\"이 아무것도 등록하지 않고 성공을 반환한다", - "disposition": "PROMOTE", - "target": "case:messaging-kafka-share-experimental-f01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-kafka-share-experimental", - "topic": "transport-and-provider-semantics", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_KAFKA_SHARE_EXPERIMENTAL-L476", @@ -32331,12 +12777,10 @@ ], "sourceHeading": "선언된 의존 셋이 사용되지 않는다", "summary": "허용 의존 목록은 상한이므로 미사용을 잡지 않는다 — 그것을 잡으려면 별도 검사가 필요하다", - "disposition": "PROMOTE", - "target": "reference:messaging-kafka-share-experimental-f02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-kafka-share-experimental", - "topic": "runtime-reachability-and-composition", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_KAFKA_SHARE_EXPERIMENTAL-L485", @@ -32347,12 +12791,10 @@ ], "sourceHeading": "형제 어댑터 넷이 구현하는 SPI를 이 leaf만 구현하지 않는다", "summary": "형제 어댑터 넷이 구현하는 SPI를 이 leaf만 구현하지 않는다", - "disposition": "PROMOTE", - "target": "open-question:messaging-kafka-share-experimental-f03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-kafka-share-experimental", - "topic": "runtime-reachability-and-composition", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_KAFKA_SHARE_EXPERIMENTAL-L494", @@ -32363,12 +12805,10 @@ ], "sourceHeading": "두 거절이 다른 예외 계층을 쓴다", "summary": "구성 오류는 한 예외 타입과 안정 코드로 보고한다", - "disposition": "PROMOTE", - "target": "reference:messaging-kafka-share-experimental-f04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-kafka-share-experimental", - "topic": "runtime-contract-correctness", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_KAFKA_SHARE_EXPERIMENTAL-L503", @@ -32379,12 +12819,10 @@ ], "sourceHeading": "네 타입 중 하나만 테스트된다", "summary": "leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다", - "disposition": "PROMOTE", - "target": "reference:messaging-kafka-share-experimental-f05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-kafka-share-experimental", - "topic": "verification-path-coverage", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_KAFKA_SHARE_EXPERIMENTAL-L512", @@ -32395,12 +12833,10 @@ ], "sourceHeading": "활성화 프로퍼티 키가 에러 메시지에만 존재한다", "summary": "에러 메시지가 지시하는 설정은 그 설정을 읽는 코드와 함께 존재해야 한다", - "disposition": "PROMOTE", - "target": "reference:messaging-kafka-share-experimental-f06", + "disposition": "KEEP_IN_SSOT", "module": "messaging-kafka-share-experimental", - "topic": "transport-and-provider-semantics", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_NATS_EXPERIMENTAL-L146", @@ -32411,12 +12847,10 @@ ], "sourceHeading": "`deduplicatedPublish` 를 무조건 참으로 선언하는데 실제 중복 제거는 프로파일에 창이 있을 때만 일어난다", "summary": "`deduplicatedPublish` 를 무조건 참으로 선언하는데 실제 중복 제거는 프로파일에 창이 있을 때만 일어난다", - "disposition": "PROMOTE", - "target": "case:messaging-nats-experimental-f01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-nats-experimental", - "topic": "verification-path-coverage", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_NATS_EXPERIMENTAL-L209", @@ -32427,12 +12861,10 @@ ], "sourceHeading": "닫힌 전송의 거절이 영구 업무 실패로 분류된다", "summary": "닫힌 전송의 거절이 영구 업무 실패로 분류된다", - "disposition": "PROMOTE", - "target": "case:messaging-nats-experimental-f02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-nats-experimental", - "topic": "transport-and-provider-semantics", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_NATS_EXPERIMENTAL-L217", @@ -32443,12 +12875,10 @@ ], "sourceHeading": "`NatsJetStreamProfileValidator` 를 호출하는 곳이 저장소에 없다. javadoc 링크 하나가 유일한 흔적이다", "summary": "`NatsJetStreamProfileValidator` 를 호출하는 곳이 저장소에 없다. javadoc 링크 하나가 유일한 흔적이다", - "disposition": "PROMOTE", - "target": "case:messaging-nats-experimental-f03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-nats-experimental", - "topic": "transport-and-provider-semantics", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_NATS_EXPERIMENTAL-L238", @@ -32459,12 +12889,10 @@ ], "sourceHeading": "경과 시간 회귀를 막으려는 어셈블이 항상 참이다", "summary": "경과 시간 회귀를 막으려는 어셈블이 항상 참이다", - "disposition": "PROMOTE", - "target": "case:messaging-nats-experimental-f04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-nats-experimental", - "topic": "verification-path-coverage", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_OBSERVABILITY-L673", @@ -32475,12 +12903,10 @@ ], "sourceHeading": "태그 어휘가 존재하고 유일한 호출부가 우회해, 실패 분류가 기록되지 않는다", "summary": "태그 어휘가 존재하고 유일한 호출부가 우회해, 실패 분류가 기록되지 않는다", - "disposition": "PROMOTE", - "target": "case:messaging-observability-f01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-observability", - "topic": "declaration-and-document-drift", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_OBSERVABILITY-L682", @@ -32491,12 +12917,10 @@ ], "sourceHeading": "관측 구현이 조립되지 않고, 그 재료 둘만 bean으로 존재한다", "summary": "관측 구현이 조립되지 않고, 그 재료 둘만 bean으로 존재한다", - "disposition": "PROMOTE", - "target": "case:messaging-observability-f02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-observability", - "topic": "runtime-reachability-and-composition", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_OBSERVABILITY-L690", @@ -32507,12 +12931,10 @@ ], "sourceHeading": "브로커 홉 추적기가 소비자를 갖지 않는다", "summary": "브로커 홉 추적기가 소비자를 갖지 않는다", - "disposition": "PROMOTE", - "target": "open-question:messaging-observability-f03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-observability", - "topic": "transport-and-provider-semantics", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_OBSERVABILITY-L699", @@ -32523,12 +12945,10 @@ ], "sourceHeading": "감사 sink 인터페이스가 사용처에서 다시 선언된다", "summary": "감사 sink 인터페이스가 사용처에서 다시 선언된다", - "disposition": "PROMOTE", - "target": "case:messaging-observability-f04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-observability", - "topic": "schema-and-data-contracts", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_OBSERVABILITY-L708", @@ -32539,12 +12959,10 @@ ], "sourceHeading": "자격증명 판정이 core-api보다 약하다", "summary": "자격증명 판정이 core-api보다 약하다", - "disposition": "PROMOTE", - "target": "case:messaging-observability-f05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-observability", - "topic": "security-policy-enforcement", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_OBSERVABILITY-L717", @@ -32555,12 +12973,10 @@ ], "sourceHeading": "감사 이벤트가 redaction을 강제하지 않는다", "summary": "타입이 문서화한 불변식은 타입이 강제한다", - "disposition": "PROMOTE", - "target": "reference:messaging-observability-f06", + "disposition": "KEEP_IN_SSOT", "module": "messaging-observability", - "topic": "declaration-and-document-drift", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_OBSERVABILITY-L726", @@ -32571,12 +12987,10 @@ ], "sourceHeading": "`extract`가 손상된 추적 헤더에 분류되지 않은 예외를 던진다", "summary": "`extract`가 손상된 추적 헤더에 분류되지 않은 예외를 던진다", - "disposition": "PROMOTE", - "target": "case:messaging-observability-f07", + "disposition": "KEEP_IN_SSOT", "module": "messaging-observability", - "topic": "contract-domain-and-bounds", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_OUTBOX_JDBC_POSTGRESQL-L876", @@ -32587,12 +13001,10 @@ ], "sourceHeading": "정리 작업이 무제한 DELETE 를 쏘고, 그것을 막는 오버로드는 호출되지 않는다", "summary": "정리 작업이 무제한 DELETE 를 쏘고, 그것을 막는 오버로드는 호출되지 않는다", - "disposition": "PROMOTE", - "target": "case:messaging-outbox-jdbc-postgresql-f01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-outbox-jdbc-postgresql", - "topic": "runtime-reachability-and-composition", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_OUTBOX_JDBC_POSTGRESQL-L888", @@ -32603,12 +13015,10 @@ ], "sourceHeading": "배포되는 Debezium 설정이 수정 이전 버전이다", "summary": "배포되는 Debezium 설정이 수정 이전 버전이다", - "disposition": "PROMOTE", - "target": "case:messaging-outbox-jdbc-postgresql-f02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-outbox-jdbc-postgresql", - "topic": "verification-path-coverage", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_OUTBOX_JDBC_POSTGRESQL-L899", @@ -32619,12 +13029,10 @@ ], "sourceHeading": "역슬래시로 끝나는 헤더 값이 헤더 맵을 깨뜨린다", "summary": "역슬래시로 끝나는 헤더 값이 헤더 맵을 깨뜨린다", - "disposition": "PROMOTE", - "target": "case:messaging-outbox-jdbc-postgresql-f03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-outbox-jdbc-postgresql", - "topic": "verification-path-coverage", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_OUTBOX_JDBC_POSTGRESQL-L909", @@ -32635,12 +13043,10 @@ ], "sourceHeading": "두 릴레이 상호배제가 기동에서 강제되지 않는다", "summary": "두 릴레이 상호배제가 기동에서 강제되지 않는다", - "disposition": "PROMOTE", - "target": "case:messaging-outbox-jdbc-postgresql-f04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-outbox-jdbc-postgresql", - "topic": "runtime-reachability-and-composition", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_OUTBOX_JDBC_POSTGRESQL-L917", @@ -32651,12 +13057,10 @@ ], "sourceHeading": "구세대 전이 메서드가 신세대와 다른 행 상태를 남긴다", "summary": "구세대 전이 메서드가 신세대와 다른 행 상태를 남긴다", - "disposition": "PROMOTE", - "target": "case:messaging-outbox-jdbc-postgresql-f05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-outbox-jdbc-postgresql", - "topic": "runtime-contract-correctness", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_OUTBOX_JDBC_POSTGRESQL-L923", @@ -32667,12 +13071,10 @@ ], "sourceHeading": "백오프 지터가 인스턴스를 분산시키지 못한다", "summary": "백오프 지터가 인스턴스를 분산시키지 못한다", - "disposition": "PROMOTE", - "target": "case:messaging-outbox-jdbc-postgresql-f06", + "disposition": "KEEP_IN_SSOT", "module": "messaging-outbox-jdbc-postgresql", - "topic": "declaration-and-document-drift", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_OUTBOX_JDBC_POSTGRESQL-L929", @@ -32683,12 +13085,10 @@ ], "sourceHeading": "커넥션 획득 방식이 리프 안에서 갈린다", "summary": "커넥션 획득 방식이 리프 안에서 갈린다", - "disposition": "PROMOTE", - "target": "case:messaging-outbox-jdbc-postgresql-f07", + "disposition": "KEEP_IN_SSOT", "module": "messaging-outbox-jdbc-postgresql", - "topic": "declaration-and-document-drift", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_OUTBOX_JDBC_POSTGRESQL-L935", @@ -32699,12 +13099,10 @@ ], "sourceHeading": "`maxBatches` 가 하드코딩이고 현재는 의미가 없다", "summary": "`maxBatches` 가 하드코딩이고 현재는 의미가 없다", - "disposition": "PROMOTE", - "target": "case:messaging-outbox-jdbc-postgresql-f08", + "disposition": "KEEP_IN_SSOT", "module": "messaging-outbox-jdbc-postgresql", - "topic": "runtime-contract-correctness", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_POLICY-L781", @@ -32715,12 +13113,10 @@ ], "sourceHeading": "재시도 엔진과 DLQ 조정자가 bean으로 만들어지고 주입되는 곳이 없다", "summary": "재시도 엔진과 DLQ 조정자가 bean으로 만들어지고 주입되는 곳이 없다", - "disposition": "PROMOTE", - "target": "case:messaging-policy-f01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-policy", - "topic": "transport-and-provider-semantics", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_POLICY-L790", @@ -32731,12 +13127,10 @@ ], "sourceHeading": "출하 컨텍스트가 발행은 하고 소비는 하지 못한다", "summary": "출하 컨텍스트가 발행은 하고 소비는 하지 못한다", - "disposition": "PROMOTE", - "target": "open-question:messaging-policy-f02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-policy", - "topic": "runtime-reachability-and-composition", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_POLICY-L799", @@ -32747,12 +13141,10 @@ ], "sourceHeading": "재시도와 DLQ 각각에 두 개의 구현이 있고 정본이 표시되지 않았다", "summary": "재시도와 DLQ 각각에 두 개의 구현이 있고 정본이 표시되지 않았다", - "disposition": "PROMOTE", - "target": "case:messaging-policy-f03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-policy", - "topic": "transport-and-provider-semantics", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_POLICY-L808", @@ -32763,12 +13155,10 @@ ], "sourceHeading": "DLQ 메타데이터의 두 시각이 항상 같다", "summary": "DLQ 메타데이터의 두 시각이 항상 같다", - "disposition": "PROMOTE", - "target": "case:messaging-policy-f04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-policy", - "topic": "transport-and-provider-semantics", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_POLICY-L817", @@ -32779,12 +13169,10 @@ ], "sourceHeading": "사이클 검사가 경로마다 집합을 복사한다", "summary": "부팅 경로의 알고리즘 복잡도는 문서화한다", - "disposition": "PROMOTE", - "target": "reference:messaging-policy-f05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-policy", - "topic": "runtime-contract-correctness", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_POLICY-L826", @@ -32795,12 +13183,10 @@ ], "sourceHeading": "프로파일 검증 실패가 플랫폼 예외 계층 밖이다", "summary": "구성 오류는 한 예외 타입과 안정 코드로 보고한다", - "disposition": "PROMOTE", - "target": "reference:messaging-policy-f06", + "disposition": "KEEP_IN_SSOT", "module": "messaging-policy", - "topic": "declaration-and-document-drift", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_POLICY-L835", @@ -32811,12 +13197,10 @@ ], "sourceHeading": "javadoc이 해소되지 않는 설계 문서를 인용한다", "summary": "저장소 밖 문서를 절 번호로 인용하지 않는다", - "disposition": "PROMOTE", - "target": "reference:messaging-policy-f07", + "disposition": "KEEP_IN_SSOT", "module": "messaging-policy", - "topic": "declaration-and-document-drift", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_PULSAR_EXPERIMENTAL-L196", @@ -32827,12 +13211,10 @@ ], "sourceHeading": "닫힌 전송의 거절이 영구 업무 실패로 분류된다", "summary": "닫힌 전송의 거절이 영구 업무 실패로 분류된다", - "disposition": "PROMOTE", - "target": "case:messaging-pulsar-experimental-f01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-pulsar-experimental", - "topic": "transport-and-provider-semantics", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_RABBIT-L210", @@ -32843,12 +13225,10 @@ ], "sourceHeading": "확인 등급이 요구에서 파생되고, 그 요구를 뒷받침하는 강제는 목적지 종류 하나에만 걸린다", "summary": "확인 등급이 요구에서 파생되고, 그 요구를 뒷받침하는 강제는 목적지 종류 하나에만 걸린다", - "disposition": "PROMOTE", - "target": "case:messaging-rabbit-f01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-rabbit", - "topic": "transport-and-provider-semantics", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_RABBIT-L238", @@ -32859,12 +13239,10 @@ ], "sourceHeading": "반환을 순번에 맞추는 조각이 production 에 없고, 시험이 그 자리를 스스로 메운다", "summary": "반환을 순번에 맞추는 조각이 production 에 없고, 시험이 그 자리를 스스로 메운다", - "disposition": "PROMOTE", - "target": "case:messaging-rabbit-f02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-rabbit", - "topic": "state-ownership-and-concurrency", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_RABBIT-L274", @@ -32875,12 +13253,10 @@ ], "sourceHeading": "SCRAM 자격을 RabbitMQ 의 데모 기구로 조용히 매핑한다", "summary": "SCRAM 자격을 RabbitMQ 의 데모 기구로 조용히 매핑한다", - "disposition": "PROMOTE", - "target": "case:messaging-rabbit-f03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-rabbit", - "topic": "transport-and-provider-semantics", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_RABBIT-L307", @@ -32891,12 +13267,10 @@ ], "sourceHeading": "능력 상수의 `delayedDelivery` 가 무조건 참이고, 그 지연을 제공할 토폴로지는 조립되지 않는다", "summary": "능력 상수의 `delayedDelivery` 가 무조건 참이고, 그 지연을 제공할 토폴로지는 조립되지 않는다", - "disposition": "PROMOTE", - "target": "case:messaging-rabbit-f04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-rabbit", - "topic": "runtime-reachability-and-composition", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_RABBIT-L335", @@ -32907,12 +13281,10 @@ ], "sourceHeading": "`pause` 의 의미가 SPI 하나 뒤에서 두 브로커에 다르게 구현된다", "summary": "`pause` 의 의미가 SPI 하나 뒤에서 두 브로커에 다르게 구현된다", - "disposition": "PROMOTE", - "target": "case:messaging-rabbit-f05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-rabbit", - "topic": "transport-and-provider-semantics", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_RELIABILITY_API-L689", @@ -32923,12 +13295,10 @@ ], "sourceHeading": "한 인터페이스가 같은 전이의 두 세대를 갖고, 안전하지 않은 쪽에 `@Deprecated`가 없다", "summary": "한 인터페이스가 같은 전이의 두 세대를 갖고, 안전하지 않은 쪽에 `@Deprecated`가 없다", - "disposition": "PROMOTE", - "target": "case:messaging-reliability-api-f01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-reliability-api", - "topic": "verification-path-coverage", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_RELIABILITY_API-L698", @@ -32939,12 +13309,10 @@ ], "sourceHeading": "fencing token 경로가 실제 데이터베이스에 대해 실행되지 않는다", "summary": "fencing token 경로가 실제 데이터베이스에 대해 실행되지 않는다", - "disposition": "PROMOTE", - "target": "case:messaging-reliability-api-f02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-reliability-api", - "topic": "runtime-reachability-and-composition", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_RELIABILITY_API-L707", @@ -32955,12 +13323,10 @@ ], "sourceHeading": "dual-write의 답이라고 선언한 진입점에 구현이 없다", "summary": "dual-write의 답이라고 선언한 진입점에 구현이 없다", - "disposition": "PROMOTE", - "target": "open-question:messaging-reliability-api-f03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-reliability-api", - "topic": "runtime-reachability-and-composition", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_RELIABILITY_API-L716", @@ -32971,12 +13337,10 @@ ], "sourceHeading": "이 leaf에 테스트가 없다", "summary": "계약만 담는 leaf도 계약의 거절 조건은 자기 레인에서 검증한다", - "disposition": "PROMOTE", - "target": "reference:messaging-reliability-api-f04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-reliability-api", - "topic": "verification-path-coverage", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_RELIABILITY_API-L725", @@ -32987,12 +13351,10 @@ ], "sourceHeading": "inbox 보존 규칙이 문서로만 있다", "summary": "inbox 보존 규칙이 문서로만 있다", - "disposition": "PROMOTE", - "target": "case:messaging-reliability-api-f05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-reliability-api", - "topic": "contract-domain-and-bounds", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_RELIABILITY_API-L734", @@ -33003,12 +13365,10 @@ ], "sourceHeading": "트랜잭션 계약 셋이 타입으로 강제되지 않는다", "summary": "호출 컨텍스트가 계약이면 그 컨텍스트를 검증할 수단을 함께 정한다", - "disposition": "PROMOTE", - "target": "reference:messaging-reliability-api-f06", + "disposition": "KEEP_IN_SSOT", "module": "messaging-reliability-api", - "topic": "declaration-and-document-drift", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_RELIABILITY_API-L743", @@ -33019,12 +13379,10 @@ ], "sourceHeading": "`OutboxRecord.equals`가 다섯 필드만 비교하고 이유가 없다", "summary": "record의 `equals`를 좁히면 이유를 적는다", - "disposition": "PROMOTE", - "target": "reference:messaging-reliability-api-f07", + "disposition": "KEEP_IN_SSOT", "module": "messaging-reliability-api", - "topic": "verification-path-coverage", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_RELIABILITY_API-L752", @@ -33035,12 +13393,10 @@ ], "sourceHeading": "포트가 bounded/unbounded purge 두 오버로드를 나란히 노출하고, 호출자가 무제한 쪽을 고른다", "summary": "포트가 bounded/unbounded purge 두 오버로드를 나란히 노출하고, 호출자가 무제한 쪽을 고른다", - "disposition": "PROMOTE", - "target": "case:messaging-reliability-api-f08", + "disposition": "KEEP_IN_SSOT", "module": "messaging-reliability-api", - "topic": "contract-domain-and-bounds", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_RUNTIME_CORE-L710", @@ -33051,12 +13407,10 @@ ], "sourceHeading": "관측이 구현·호출부·주입 자리를 모두 갖추고도 출하에서 no-op이다", "summary": "관측이 구현·호출부·주입 자리를 모두 갖추고도 출하에서 no-op이다", - "disposition": "PROMOTE", - "target": "case:messaging-runtime-core-f01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-runtime-core", - "topic": "runtime-reachability-and-composition", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_RUNTIME_CORE-L719", @@ -33067,12 +13421,10 @@ ], "sourceHeading": "소비 오케스트레이터가 조립되지 않는다", "summary": "소비 오케스트레이터가 조립되지 않는다", - "disposition": "PROMOTE", - "target": "case:messaging-runtime-core-f02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-runtime-core", - "topic": "runtime-reachability-and-composition", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_RUNTIME_CORE-L727", @@ -33083,12 +13435,10 @@ ], "sourceHeading": "선언된 content type과 실제 인코딩이 조용히 갈라질 수 있다", "summary": "선언된 content type과 실제 인코딩이 조용히 갈라질 수 있다", - "disposition": "PROMOTE", - "target": "case:messaging-runtime-core-f03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-runtime-core", - "topic": "schema-and-data-contracts", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_RUNTIME_CORE-L736", @@ -33099,12 +13449,10 @@ ], "sourceHeading": "같은 실패 코드가 두 completion에 쓰인다", "summary": "안정 코드는 운영자의 행동이 갈리는 지점마다 나눈다", - "disposition": "PROMOTE", - "target": "reference:messaging-runtime-core-f04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-runtime-core", - "topic": "transport-and-provider-semantics", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_RUNTIME_CORE-L745", @@ -33115,12 +13463,10 @@ ], "sourceHeading": "admission 실패만 예외로 전파된다", "summary": "`CompletionStage`를 반환하는 메서드는 동기적으로 던지지 않는다", - "disposition": "PROMOTE", - "target": "reference:messaging-runtime-core-f05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-runtime-core", - "topic": "runtime-contract-correctness", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_RUNTIME_CORE-L754", @@ -33131,12 +13477,10 @@ ], "sourceHeading": "`generation`이 항상 1이다", "summary": "증가한다고 문서화한 값이 리터럴이면 그 사실을 적는다", - "disposition": "PROMOTE", - "target": "reference:messaging-runtime-core-f06", + "disposition": "KEEP_IN_SSOT", "module": "messaging-runtime-core", - "topic": "verification-path-coverage", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_RUNTIME_CORE-L763", @@ -33147,12 +13491,10 @@ ], "sourceHeading": "`missingResult()`가 아무 데도 쓰이지 않는다", "summary": "만들어 두고 흘리지 않는 진단값은 진단이 아니다", - "disposition": "PROMOTE", - "target": "reference:messaging-runtime-core-f07", + "disposition": "KEEP_IN_SSOT", "module": "messaging-runtime-core", - "topic": "runtime-reachability-and-composition", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_SCHEMA_API-L494", @@ -33163,12 +13505,10 @@ ], "sourceHeading": "포맷 독립 진화 규칙이 호출되지 않고, 그것이 막으려던 중복이 실제로 생겼다", "summary": "포맷 독립 진화 규칙이 호출되지 않고, 그것이 막으려던 중복이 실제로 생겼다", - "disposition": "PROMOTE", - "target": "case:messaging-schema-api-f01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-api", - "topic": "runtime-reachability-and-composition", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_SCHEMA_API-L503", @@ -33179,12 +13519,10 @@ ], "sourceHeading": "port 구현의 스레드 안전성 요구가 문서화되어 있지 않다", "summary": "port 계약은 동시성 요구를 적는다", - "disposition": "PROMOTE", - "target": "reference:messaging-schema-api-f02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-api", - "topic": "declaration-and-document-drift", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_SCHEMA_API-L512", @@ -33195,12 +13533,10 @@ ], "sourceHeading": "`SchemaRegistry`라는 이름이 저장소에서 두 가지를 가리킨다", "summary": "도달성 판정은 단어가 아니라 import로 확인한다", - "disposition": "PROMOTE", - "target": "reference:messaging-schema-api-f03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-api", - "topic": "declaration-and-document-drift", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_SCHEMA_AVRO-L524", @@ -33211,12 +13547,10 @@ ], "sourceHeading": "CI에서 돈다고 선언한 게이트를 부르는 CI가 없다", "summary": "CI에서 돈다고 선언한 게이트를 부르는 CI가 없다", - "disposition": "PROMOTE", - "target": "case:messaging-schema-avro-f01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-avro", - "topic": "declaration-and-document-drift", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_SCHEMA_AVRO-L533", @@ -33227,12 +13561,10 @@ ], "sourceHeading": "진화 판단이 두 곳에 있고 형태가 반대다", "summary": "진화 판단이 두 곳에 있고 형태가 반대다", - "disposition": "PROMOTE", - "target": "case:messaging-schema-avro-f02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-avro", - "topic": "schema-and-data-contracts", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_SCHEMA_AVRO-L542", @@ -33243,12 +13575,10 @@ ], "sourceHeading": "`history` 순서 계약이 port와 게이트에서 반대다", "summary": "컬렉션 순서가 계약이면 양쪽에서 같은 방향으로 적는다", - "disposition": "PROMOTE", - "target": "reference:messaging-schema-avro-f03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-avro", - "topic": "schema-and-data-contracts", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_SCHEMA_AVRO-L551", @@ -33259,12 +13589,10 @@ ], "sourceHeading": "transitive 분기가 테스트되지 않는다", "summary": "모드 enum을 분기 조건으로 쓰면 각 분기에 테스트를 둔다", - "disposition": "PROMOTE", - "target": "reference:messaging-schema-avro-f04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-avro", - "topic": "verification-path-coverage", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_SCHEMA_AVRO-L560", @@ -33275,12 +13603,10 @@ ], "sourceHeading": "에러 코드 어휘가 형제 codec과 갈라진다", "summary": "안정 코드는 판단 단위로 정하고 구현 단위로 정하지 않는다", - "disposition": "PROMOTE", - "target": "reference:messaging-schema-avro-f05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-avro", - "topic": "schema-and-data-contracts", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_SCHEMA_JSON-L463", @@ -33291,12 +13617,10 @@ ], "sourceHeading": "포맷 중립 payload 정책이, 자기 상수를 두고 JSON codec의 상수를 참조한다", "summary": "포맷 중립 payload 정책이, 자기 상수를 두고 JSON codec의 상수를 참조한다", - "disposition": "PROMOTE", - "target": "case:messaging-schema-json-f01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-json", - "topic": "schema-and-data-contracts", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_SCHEMA_JSON-L472", @@ -33307,12 +13631,10 @@ ], "sourceHeading": "파서 방어 여섯 갈래가 하나의 실패 코드로 접힌다", "summary": "실패 코드는 운영자의 다음 행동이 갈리는 지점마다 나눈다", - "disposition": "PROMOTE", - "target": "reference:messaging-schema-json-f02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-json", - "topic": "transport-and-provider-semantics", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_SCHEMA_JSON-L481", @@ -33323,12 +13645,10 @@ ], "sourceHeading": "빈 registry로 조립되면 모든 메시지가 거절된다", "summary": "빈 registry로 조립되면 모든 메시지가 거절된다", - "disposition": "PROMOTE", - "target": "open-question:messaging-schema-json-f03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-json", - "topic": "runtime-reachability-and-composition", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_SCHEMA_PROTOBUF-L527", @@ -33339,12 +13659,10 @@ ], "sourceHeading": "`.proto` fixture와 테스트 descriptor의 일치를 아무도 강제하지 않는다", "summary": "검증되지 않는 스키마 파일은 문서임을 파일 안에 적는다", - "disposition": "PROMOTE", - "target": "reference:messaging-schema-protobuf-f01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-protobuf", - "topic": "verification-path-coverage", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_SCHEMA_PROTOBUF-L536", @@ -33355,12 +13673,10 @@ ], "sourceHeading": "디코딩 상한 분기가 테스트되지 않는다", "summary": "신뢰할 수 없는 입력 쪽 경계를 먼저 테스트한다", - "disposition": "PROMOTE", - "target": "reference:messaging-schema-protobuf-f02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-protobuf", - "topic": "verification-path-coverage", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_SCHEMA_PROTOBUF-L545", @@ -33371,12 +13687,10 @@ ], "sourceHeading": "protobuf-java 버전이 저장소에 셋이고 전역 정책이 없다", "summary": "protobuf-java 버전이 저장소에 셋이고 전역 정책이 없다", - "disposition": "PROMOTE", - "target": "open-question:messaging-schema-protobuf-f03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-protobuf", - "topic": "schema-and-data-contracts", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_SCHEMA_PROTOBUF-L554", @@ -33387,12 +13701,10 @@ ], "sourceHeading": "registry 조회 로직이 세 codec에 복제돼 있다", "summary": "registry 조회 로직이 세 codec에 복제돼 있다", - "disposition": "PROMOTE", - "target": "case:messaging-schema-protobuf-f04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-protobuf", - "topic": "schema-and-data-contracts", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_SECURITY-L634", @@ -33403,12 +13715,10 @@ ], "sourceHeading": "같은 TLS posture를 두 클래스가 다른 엄격도로 검사한다", "summary": "같은 TLS posture를 두 클래스가 다른 엄격도로 검사한다", - "disposition": "PROMOTE", - "target": "case:messaging-security-f01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-security", - "topic": "security-policy-enforcement", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_SECURITY-L643", @@ -33419,12 +13729,10 @@ ], "sourceHeading": "권한 거부가 `AUTHORIZATION`이 아니라 `CONFIGURATION`으로 기록된다", "summary": "권한 거부가 `AUTHORIZATION`이 아니라 `CONFIGURATION`으로 기록된다", - "disposition": "PROMOTE", - "target": "case:messaging-security-f02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-security", - "topic": "security-policy-enforcement", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_SECURITY-L652", @@ -33435,12 +13743,10 @@ ], "sourceHeading": "ACL 매니페스트 전체가 쓰이지 않는다", "summary": "ACL 매니페스트 전체가 쓰이지 않는다", - "disposition": "PROMOTE", - "target": "open-question:messaging-security-f03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-security", - "topic": "security-policy-enforcement", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_SECURITY-L661", @@ -33451,12 +13757,10 @@ ], "sourceHeading": "종료 시 자격증명 소거가 호출되지 않는다", "summary": "종료 시 자격증명 소거가 호출되지 않는다", - "disposition": "PROMOTE", - "target": "case:messaging-security-f04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-security", - "topic": "runtime-reachability-and-composition", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_SECURITY-L670", @@ -33467,12 +13771,10 @@ ], "sourceHeading": "회전 술어가 두 번 구현돼 있고, 쓰이지 않는 쪽이 테스트된다", "summary": "같은 술어가 두 타입에 있으면 하나가 다른 하나를 부른다", - "disposition": "PROMOTE", - "target": "reference:messaging-security-f05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-security", - "topic": "verification-path-coverage", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_SECURITY-L679", @@ -33483,12 +13785,10 @@ ], "sourceHeading": "자격증명 해석이 맵 bin 락 안에서 외부 I/O를 한다", "summary": "맵 갱신 함수 안에서 I/O를 하면 그 지연이 락 범위가 된다", - "disposition": "PROMOTE", - "target": "reference:messaging-security-f06", + "disposition": "KEEP_IN_SSOT", "module": "messaging-security", - "topic": "security-policy-enforcement", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_SECURITY-L688", @@ -33499,12 +13799,10 @@ ], "sourceHeading": "다섯 타입이 이 leaf의 테스트에 등장하지 않는다", "summary": "배선된 게이트는 자기 leaf 레인에서 검증한다", - "disposition": "PROMOTE", - "target": "reference:messaging-security-f07", + "disposition": "KEEP_IN_SSOT", "module": "messaging-security", - "topic": "runtime-reachability-and-composition", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_SECURITY-L697", @@ -33515,12 +13813,10 @@ ], "sourceHeading": "`CredentialRuntime.material`이 동기화되지 않는다", "summary": "가변 필드로 상태 전이를 표현하면 가시성을 함께 정한다", - "disposition": "PROMOTE", - "target": "reference:messaging-security-f08", + "disposition": "KEEP_IN_SSOT", "module": "messaging-security", - "topic": "state-ownership-and-concurrency", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_SPRING_BOOT_STARTER-L281", @@ -33531,12 +13827,10 @@ ], "sourceHeading": "같은 자동 설정 안에서 검증기 하나만 감싸이지 않는다", "summary": "같은 자동 설정 안에서 검증기 하나만 감싸이지 않는다", - "disposition": "PROMOTE", - "target": "case:messaging-spring-boot-starter-f01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-spring-boot-starter", - "topic": "runtime-reachability-and-composition", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_SPRING_BOOT_STARTER-L300", @@ -33547,12 +13841,10 @@ ], "sourceHeading": "출고되는 신뢰성 체인 전체가 아무도 공급하지 않는 빈 뒤에 있고, 그 사슬이 자기 클래스 안을 가리킨다", "summary": "출고되는 신뢰성 체인 전체가 아무도 공급하지 않는 빈 뒤에 있고, 그 사슬이 자기 클래스 안을 가리킨다", - "disposition": "PROMOTE", - "target": "case:messaging-spring-boot-starter-f02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-spring-boot-starter", - "topic": "declaration-and-document-drift", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_SPRING_BOOT_STARTER-L319", @@ -33563,12 +13855,10 @@ ], "sourceHeading": "죽은 매개변수 하나가 유일한 비기본값에서 NPE 를 낳는다", "summary": "죽은 매개변수 하나가 유일한 비기본값에서 NPE 를 낳는다", - "disposition": "PROMOTE", - "target": "case:messaging-spring-boot-starter-f03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-spring-boot-starter", - "topic": "runtime-contract-correctness", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_SPRING_BOOT_STARTER-L342", @@ -33579,12 +13869,10 @@ ], "sourceHeading": "설정 경로의 재시도가 예외 분류를 표현할 수 없다", "summary": "설정 경로의 재시도가 예외 분류를 표현할 수 없다", - "disposition": "PROMOTE", - "target": "case:messaging-spring-boot-starter-f04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-spring-boot-starter", - "topic": "declaration-and-document-drift", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_SPRING_BOOT_STARTER-L367", @@ -33595,12 +13883,10 @@ ], "sourceHeading": "배치 발행자가 `CompletionStage` 를 돌려주면서 동기 예외를 던진다", "summary": "배치 발행자가 `CompletionStage` 를 돌려주면서 동기 예외를 던진다", - "disposition": "PROMOTE", - "target": "case:messaging-spring-boot-starter-f05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-spring-boot-starter", - "topic": "verification-path-coverage", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_SPRING_CLOUD_STREAM_BRIDGE-L552", @@ -33611,12 +13897,10 @@ ], "sourceHeading": "선언된 의존 둘이 사용되지 않는다", "summary": "허용 의존 목록은 상한이므로 미사용을 잡지 않는다", - "disposition": "PROMOTE", - "target": "reference:messaging-spring-cloud-stream-bridge-f01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-spring-cloud-stream-bridge", - "topic": "runtime-reachability-and-composition", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_SPRING_CLOUD_STREAM_BRIDGE-L561", @@ -33627,12 +13911,10 @@ ], "sourceHeading": "브리지의 바인더 쪽 절반이 없다", "summary": "브리지의 바인더 쪽 절반이 없다", - "disposition": "PROMOTE", - "target": "open-question:messaging-spring-cloud-stream-bridge-f02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-spring-cloud-stream-bridge", - "topic": "transport-and-provider-semantics", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_SPRING_CLOUD_STREAM_BRIDGE-L570", @@ -33643,12 +13925,10 @@ ], "sourceHeading": "인터페이스를 publisher만 구현하고 두 클래스가 같은 바인딩에 각자 상태를 갖는다", "summary": "한 개념의 등록 상태를 두 객체가 나눠 갖지 않는다", - "disposition": "PROMOTE", - "target": "reference:messaging-spring-cloud-stream-bridge-f03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-spring-cloud-stream-bridge", - "topic": "declaration-and-document-drift", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_SPRING_CLOUD_STREAM_BRIDGE-L579", @@ -33659,12 +13939,10 @@ ], "sourceHeading": "두 맵 갱신이 원자적이지 않다", "summary": "함께 읽히는 두 맵은 한 값으로 묶는다", - "disposition": "PROMOTE", - "target": "reference:messaging-spring-cloud-stream-bridge-f04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-spring-cloud-stream-bridge", - "topic": "state-ownership-and-concurrency", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_SPRING_CLOUD_STREAM_BRIDGE-L588", @@ -33675,12 +13953,10 @@ ], "sourceHeading": "등록 해제 경로가 없다", "summary": "등록을 받는 컴포넌트는 해제도 제공한다", - "disposition": "PROMOTE", - "target": "reference:messaging-spring-cloud-stream-bridge-f05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-spring-cloud-stream-bridge", - "topic": "runtime-reachability-and-composition", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_SPRING_CLOUD_STREAM_BRIDGE-L597", @@ -33691,12 +13967,10 @@ ], "sourceHeading": "활성화 프로퍼티 키가 에러 메시지에만 존재한다", "summary": "에러 메시지가 지시하는 설정은 그 설정을 읽는 코드와 함께 존재해야 한다", - "disposition": "PROMOTE", - "target": "reference:messaging-spring-cloud-stream-bridge-f06", + "disposition": "KEEP_IN_SSOT", "module": "messaging-spring-cloud-stream-bridge", - "topic": "state-ownership-and-concurrency", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_TESTKIT-L940", @@ -33707,12 +13981,10 @@ ], "sourceHeading": "`FaultController` 의 5개 중 2개가 구현만 3벌 있고 호출부가 0건이다", "summary": "`FaultController` 의 5개 중 2개가 구현만 3벌 있고 호출부가 0건이다", - "disposition": "PROMOTE", - "target": "case:messaging-testkit-f01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-testkit", - "topic": "runtime-reachability-and-composition", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_TESTKIT-L950", @@ -33723,12 +13995,10 @@ ], "sourceHeading": "클래스 javadoc 이 강제되지 않는 규칙을 강제된다고 말한다", "summary": "클래스 javadoc 이 강제되지 않는 규칙을 강제된다고 말한다", - "disposition": "PROMOTE", - "target": "case:messaging-testkit-f02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-testkit", - "topic": "declaration-and-document-drift", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_TESTKIT-L960", @@ -33739,12 +14009,10 @@ ], "sourceHeading": "`Faults` 내부클래스 57줄이 3개 모듈에 바이트 단위로 복제되어 있다", "summary": "`Faults` 내부클래스 57줄이 3개 모듈에 바이트 단위로 복제되어 있다", - "disposition": "PROMOTE", - "target": "case:messaging-testkit-f03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-testkit", - "topic": "transport-and-provider-semantics", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_TESTKIT-L966", @@ -33755,12 +14023,10 @@ ], "sourceHeading": "1 MiB 한도가 `PayloadPolicy` 를 두고 리터럴로 재선언된다", "summary": "1 MiB 한도가 `PayloadPolicy` 를 두고 리터럴로 재선언된다", - "disposition": "PROMOTE", - "target": "case:messaging-testkit-f04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-testkit", - "topic": "runtime-reachability-and-composition", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_TESTKIT-L972", @@ -33771,12 +14037,10 @@ ], "sourceHeading": "`messaging-transport-spi` 의존이 import 0건이다", "summary": "`messaging-transport-spi` 의존이 import 0건이다", - "disposition": "PROMOTE", - "target": "case:messaging-testkit-f05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-testkit", - "topic": "runtime-reachability-and-composition", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_TESTKIT-L976", @@ -33787,12 +14051,10 @@ ], "sourceHeading": "`BrokerFailureMatrix.adapters()` 는 호출부가 0건이다", "summary": "`BrokerFailureMatrix.adapters()` 는 호출부가 0건이다", - "disposition": "PROMOTE", - "target": "case:messaging-testkit-f06", + "disposition": "KEEP_IN_SSOT", "module": "messaging-testkit", - "topic": "runtime-reachability-and-composition", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_TESTKIT-L980", @@ -33803,12 +14065,10 @@ ], "sourceHeading": "항등식을 단언하는 테스트가 하나 있다", "summary": "항등식을 단언하는 테스트가 하나 있다", - "disposition": "PROMOTE", - "target": "case:messaging-testkit-f07", + "disposition": "KEEP_IN_SSOT", "module": "messaging-testkit", - "topic": "verification-path-coverage", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_TESTKIT-L984", @@ -33819,12 +14079,10 @@ ], "sourceHeading": "`gitCommit` 은 기록되지만 읽혀 판정되지 않는다", "summary": "`gitCommit` 은 기록되지만 읽혀 판정되지 않는다", - "disposition": "PROMOTE", - "target": "case:messaging-testkit-f08", + "disposition": "KEEP_IN_SSOT", "module": "messaging-testkit", - "topic": "declaration-and-document-drift", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_TRANSPORT_SPI-L659", @@ -33835,12 +14093,10 @@ ], "sourceHeading": "드레인 마감 30초가 세 곳에서 독립적으로 결정된다", "summary": "드레인 마감 30초가 세 곳에서 독립적으로 결정된다", - "disposition": "PROMOTE", - "target": "case:messaging-transport-spi-f01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-transport-spi", - "topic": "verification-path-coverage", - "reason": "61개 canonical SSOT 전수 recall. 독립 causal/semantic/verification unit 이라 기존 노드와 병합하지 않았다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_TRANSPORT_SPI-L668", @@ -33851,12 +14107,10 @@ ], "sourceHeading": "종료 중 `install`이 닫히지 않는 창", "summary": "멱등 종료를 보장하는 컴포넌트는 종료 이후의 등록도 정의한다", - "disposition": "PROMOTE", - "target": "reference:messaging-transport-spi-f02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-transport-spi", - "topic": "transport-and-provider-semantics", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-MESSAGING_TRANSPORT_SPI-L677", @@ -33867,12 +14121,10 @@ ], "sourceHeading": "pause scope sentinel이 두 인터페이스에서 다르다", "summary": "같은 개념의 sentinel은 계층을 넘어 하나로 정한다", - "disposition": "PROMOTE", - "target": "reference:messaging-transport-spi-f03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-transport-spi", - "topic": "transport-and-provider-semantics", - "reason": "REFERENCE semantic audit — SSOT 가 괄호로 적은 재사용 규칙을 rule/scope/exceptions 형태로 다시 썼다. 구체 사건은 source finding 이 소유한다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-FAM19-L860", @@ -33883,12 +14135,10 @@ ], "sourceHeading": "messaging 마이그레이션 스트림을 적용하는 곳이 없고, 적용하려는 순간 버전이 충돌한다", "summary": "messaging 마이그레이션 스트림을 적용하는 곳이 없고, 적용하려는 순간 버전이 충돌한다", - "disposition": "PROMOTE", - "target": "case:messaging-migration-stream-has-no-applier-and-collides-on-adoption", + "disposition": "KEEP_IN_SSOT", "module": "integration/19-messaging-platform", - "topic": "cross-leaf-integration-facts", - "reason": "cross-leaf integration 에서만 성립하는 사실이라 leaf candidate 로 환원되지 않는다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-FAM19-L926", @@ -33899,12 +14149,10 @@ ], "sourceHeading": "claim-check는 starter에 배선 코드가 한 줄도 없다", "summary": "claim-check는 starter에 배선 코드가 한 줄도 없다", - "disposition": "PROMOTE", - "target": "case:claim-check-has-no-wiring-line-in-the-starter", + "disposition": "KEEP_IN_SSOT", "module": "integration/19-messaging-platform", - "topic": "cross-leaf-integration-facts", - "reason": "cross-leaf integration 에서만 성립하는 사실이라 leaf candidate 로 환원되지 않는다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-FAM19-L975", @@ -33915,12 +14163,10 @@ ], "sourceHeading": "admin 스위치가 가드를 켜고 서비스는 켜지 않는다", "summary": "admin 스위치가 가드를 켜고 서비스는 켜지 않는다", - "disposition": "PROMOTE", - "target": "case:the-admin-switch-turns-on-the-guard-and-not-the-service", + "disposition": "KEEP_IN_SSOT", "module": "integration/19-messaging-platform", - "topic": "cross-leaf-integration-facts", - "reason": "cross-leaf integration 에서만 성립하는 사실이라 leaf candidate 로 환원되지 않는다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-FAM19-L997", @@ -33931,12 +14177,10 @@ ], "sourceHeading": "`messaging-admin-api`는 main 25파일 · 1,613 LOC에 테스트 파일이 1개다", "summary": "`messaging-admin-api`는 main 25파일 · 1,613 LOC에 테스트 파일이 1개다", - "disposition": "PROMOTE", - "target": "case:twenty-five-main-files-and-one-test-file", + "disposition": "KEEP_IN_SSOT", "module": "integration/19-messaging-platform", - "topic": "cross-leaf-integration-facts", - "reason": "cross-leaf integration 에서만 성립하는 사실이라 leaf candidate 로 환원되지 않는다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "R3-FAM19-L1064", @@ -33963,12 +14207,10 @@ ], "sourceHeading": "`MessagingPublicSurfaceContractTest`가 가족 밖(app-bootstrap)에 있다", "summary": "`MessagingPublicSurfaceContractTest`가 가족 밖(app-bootstrap)에 있다", - "disposition": "PROMOTE", - "target": "case:the-public-surface-contract-test-lives-outside-the-family", + "disposition": "KEEP_IN_SSOT", "module": "integration/19-messaging-platform", - "topic": "cross-leaf-integration-facts", - "reason": "cross-leaf integration 에서만 성립하는 사실이라 leaf candidate 로 환원되지 않는다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C2-XS-CLASSIFIER", @@ -33980,12 +14222,10 @@ ], "sourceHeading": "분류기는 엔진이 남긴 것만 볼 수 있다", "summary": "분류기는 엔진이 남긴 것만 볼 수 있다", - "disposition": "PROMOTE", - "target": "reference:a-classifier-sees-only-what-the-engine-kept", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-httpclient", - "topic": "http-failure-classification", - "reason": "사이클 2 의 판정 번복(EVD-332)에서 추출한 재사용 기준. `final/document.md#a11` §51.3-51.4 와 cross-scope §5 가 근거다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C2-XS-RUNTIME-SHAPE", @@ -33996,12 +14236,10 @@ ], "sourceHeading": "런타임의 모양에 대한 주장은 런타임에서 확인한다", "summary": "런타임의 모양에 대한 주장은 런타임에서 확인한다", - "disposition": "PROMOTE", - "target": "reference:verify-runtime-shape-at-runtime", + "disposition": "KEEP_IN_SSOT", "module": "cross-scope", - "topic": "http-failure-classification", - "reason": "cross-scope §5 가 사이클 2 의 번복 1건과 자기 교정 3건에서 뽑은 측정 규칙이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_GRAPHQL-L130", @@ -34012,12 +14250,10 @@ ], "sourceHeading": "(8.2) 조건 형제 비교 — off 계약의 두 절반", "summary": "(8.2) 조건 형제 비교 — off 계약의 두 절반", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-graphql-c01", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-graphql", - "topic": "security-and-trust-boundaries", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_GRAPHQL-L221", @@ -34028,12 +14264,10 @@ ], "sourceHeading": "(8.2) 조건 형제 비교 — 스키마 해시의 생산자와 소비자", "summary": "(8.2) 조건 형제 비교 — 스키마 해시의 생산자와 소비자", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-graphql-c02", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-graphql", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_GRAPHQL-L336", @@ -34044,12 +14278,10 @@ ], "sourceHeading": "(8.2) 조건 형제 비교 — 연산 정체성을 정하는 두 구현", "summary": "(8.2) 조건 형제 비교 — 연산 정체성을 정하는 두 구현", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-graphql-c03", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-graphql", - "topic": "composition-and-lifecycle-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_GRAPHQL-L354", @@ -34060,12 +14292,10 @@ ], "sourceHeading": "(8.3) 중복 메커니즘 — 예산 계층", "summary": "(8.3) 중복 메커니즘 — 예산 계층", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-graphql-c04", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-graphql", - "topic": "admission-budget-and-backpressure", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_GRAPHQL-L376", @@ -34076,12 +14306,10 @@ ], "sourceHeading": "(8.4) 문서/구현 드리프트 — 취소 경로", "summary": "(8.4) 문서/구현 드리프트 — 취소 경로", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-graphql-c05", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-graphql", - "topic": "admission-budget-and-backpressure", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_GRAPHQL-L471", @@ -34092,12 +14320,10 @@ ], "sourceHeading": "(8.2) 조건 형제 비교 — 클라이언트 정책이 어떻게 정해지는가", "summary": "(8.2) 조건 형제 비교 — 클라이언트 정책이 어떻게 정해지는가", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-graphql-c06", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-graphql", - "topic": "composition-and-lifecycle-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_GRAPHQL-L604", @@ -34108,12 +14334,10 @@ ], "sourceHeading": "(8.3) 중복 메커니즘 — 실행 전 실패의 매퍼", "summary": "(8.3) 중복 메커니즘 — 실행 전 실패의 매퍼", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-graphql-c07", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-graphql", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_GRAPHQL-L612", @@ -34124,12 +14348,10 @@ ], "sourceHeading": "(8.4) 문서/구현 드리프트 — 보고되는 HTTP 프로파일", "summary": "(8.4) 문서/구현 드리프트 — 보고되는 HTTP 프로파일", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-graphql-c08", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-graphql", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_GRAPHQL-L660", @@ -34140,12 +14362,10 @@ ], "sourceHeading": "b 그 결과 — HTTP 전송 계약 계층이 미배선이고 실제 전송은 프레임워크가 정한다", "summary": "b 그 결과 — HTTP 전송 계약 계층이 미배선이고 실제 전송은 프레임워크가 정한다", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-graphql-c09", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-graphql", - "topic": "composition-and-lifecycle-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_GRAPHQL-L719", @@ -34156,12 +14376,10 @@ ], "sourceHeading": "(8.1) 도달성 — 네 패키지의 배선 상태", "summary": "(8.1) 도달성 — 네 패키지의 배선 상태", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-graphql-c10", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-graphql", - "topic": "query-and-pagination-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_GRAPHQL-L725", @@ -34172,12 +14390,10 @@ ], "sourceHeading": "(8.2) 조건 형제 비교 — 커서 서명 키의 두 소비처", "summary": "(8.2) 조건 형제 비교 — 커서 서명 키의 두 소비처", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-graphql-c11", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-graphql", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_GRAPHQL-L867", @@ -34188,12 +14404,10 @@ ], "sourceHeading": "(8.2) 조건 형제 비교 — 두 능력 목록이 커서에 대해 다르게 답한다", "summary": "(8.2) 조건 형제 비교 — 두 능력 목록이 커서에 대해 다르게 답한다", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-graphql-c12", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-graphql", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_GRAPHQL-L873", @@ -34204,12 +14418,10 @@ ], "sourceHeading": "(8.1) 도달성 — 릴리스 게이트 자체", "summary": "(8.1) 도달성 — 릴리스 게이트 자체", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-graphql-c13", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-graphql", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_GRPC-L140", @@ -34220,12 +14432,10 @@ ], "sourceHeading": "(8.3) 중복 메커니즘 — 인증과 예외 처리의 인터셉터 순서", "summary": "(8.3) 중복 메커니즘 — 인증과 예외 처리의 인터셉터 순서", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-grpc-c01", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-grpc", - "topic": "result-and-failure-algebra", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_WEB-L116", @@ -34236,12 +14446,10 @@ ], "sourceHeading": "(8.2) 조건 형제 비교 — 두 자동설정의 게이트", "summary": "(8.2) 조건 형제 비교 — 두 자동설정의 게이트", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-web-c01", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-web", - "topic": "capability-and-disclosure-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_WEB-L140", @@ -34252,12 +14460,10 @@ ], "sourceHeading": "(8.4) 문서/구현 드리프트 — 모듈 경계 선언과 실제 트리", "summary": "(8.4) 문서/구현 드리프트 — 모듈 경계 선언과 실제 트리", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-web-c02", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-web", - "topic": "composition-and-lifecycle-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_WEB-L426", @@ -34268,12 +14474,10 @@ ], "sourceHeading": "(8.1) 도달성 — 신원 모델의 프로덕션 참조 수", "summary": "(8.1) 도달성 — 신원 모델의 프로덕션 참조 수", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-web-c03", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-web", - "topic": "security-and-trust-boundaries", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_WEB-L473", @@ -34284,12 +14488,10 @@ ], "sourceHeading": "(8.3) 필터 체인 순서 — `publicPaths` 대 `RestrictedPathRule`", "summary": "(8.3) 필터 체인 순서 — `publicPaths` 대 `RestrictedPathRule`", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-web-c04", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-web", - "topic": "security-and-trust-boundaries", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_WEB-L666", @@ -34300,12 +14502,10 @@ ], "sourceHeading": "(8.4) 게이트 프로퍼티가 존재하는가", "summary": "(8.4) 게이트 프로퍼티가 존재하는가", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-web-c05", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-web", - "topic": "admission-budget-and-backpressure", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_WEB-L762", @@ -34316,12 +14516,10 @@ ], "sourceHeading": "(8.2) durable-operation HTTP 표면의 두 게이트", "summary": "(8.2) durable-operation HTTP 표면의 두 게이트", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-web-c06", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-web", - "topic": "query-and-pagination-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_WEB-L785", @@ -34332,12 +14530,10 @@ ], "sourceHeading": "(8.4) 지문 정규화가 길이 프레이밍인가", "summary": "(8.4) 지문 정규화가 길이 프레이밍인가", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-web-c07", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-web", - "topic": "security-and-trust-boundaries", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_WEB-L843", @@ -34348,12 +14544,10 @@ ], "sourceHeading": "(8.1) 도달성 — 라이브러리 타입의 소비자", "summary": "(8.1) 도달성 — 라이브러리 타입의 소비자", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-web-c08", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-web", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_WEB-L864", @@ -34364,12 +14558,10 @@ ], "sourceHeading": "(8.2) 조건 형제 비교 — 캐시 정책이 두 벌이다", "summary": "(8.2) 조건 형제 비교 — 캐시 정책이 두 벌이다", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-web-c09", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-web", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_WEB-L889", @@ -34380,12 +14572,10 @@ ], "sourceHeading": "(8.3) 중복 메커니즘 — 커서 코덱도 두 벌", "summary": "(8.3) 중복 메커니즘 — 커서 코덱도 두 벌", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-web-c10", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-web", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_WEB-L978", @@ -34396,12 +14586,10 @@ ], "sourceHeading": "(8.2) 조건 형제 비교 — `OpenApiCustomizer` 가 두 개다", "summary": "(8.2) 조건 형제 비교 — `OpenApiCustomizer` 가 두 개다", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-web-c11", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-web", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_WEB-L986", @@ -34412,12 +14600,10 @@ ], "sourceHeading": "(8.3) XML/CBOR 표현의 런타임 배선", "summary": "(8.3) XML/CBOR 표현의 런타임 배선", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-web-c12", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-web", - "topic": "composition-and-lifecycle-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_WEB-L1071", @@ -34428,12 +14614,10 @@ ], "sourceHeading": "(8.2) 조건 형제 비교 — `X-Request-Id`에 대해 배선된 두 필터가 반대 정책을 쓴다", "summary": "(8.2) 조건 형제 비교 — `X-Request-Id`에 대해 배선된 두 필터가 반대 정책을 쓴다", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-web-c13", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-web", - "topic": "composition-and-lifecycle-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_WEB-L1098", @@ -34444,12 +14628,10 @@ ], "sourceHeading": "(8.1) 도달성 — forwarded 헤더 신뢰 정책", "summary": "(8.1) 도달성 — forwarded 헤더 신뢰 정책", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-web-c14", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-web", - "topic": "admission-budget-and-backpressure", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_WEB-L1108", @@ -34460,12 +14642,10 @@ ], "sourceHeading": "(8.3) 중복 메커니즘 — 상관 식별자가 세 벌이다", "summary": "(8.3) 중복 메커니즘 — 상관 식별자가 세 벌이다", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-web-c15", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-web", - "topic": "observability-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_WEB-L1217", @@ -34476,12 +14656,10 @@ ], "sourceHeading": "(8.4) 카운트 드리프트 — 선언된 능력 11개, 활성화 게이트 2개", "summary": "(8.4) 카운트 드리프트 — 선언된 능력 11개, 활성화 게이트 2개", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-web-c16", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-web", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_WEB-L1255", @@ -34492,12 +14670,10 @@ ], "sourceHeading": "(8.3) 중복 메커니즘 — 하나의 스위치가 두 능력을 켠다", "summary": "(8.3) 중복 메커니즘 — 하나의 스위치가 두 능력을 켠다", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-web-c17", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-web", - "topic": "capability-and-disclosure-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_WEB-L1336", @@ -34508,12 +14684,10 @@ ], "sourceHeading": "(8.1) 도달성 — 시작 검증과 조립", "summary": "(8.1) 도달성 — 시작 검증과 조립", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-web-c18", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-web", - "topic": "admission-budget-and-backpressure", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_WEB-L1555", @@ -34524,12 +14698,10 @@ ], "sourceHeading": "(8.2) 조건 형제 비교 — 두 개의 계약 강제 형태", "summary": "(8.2) 조건 형제 비교 — 두 개의 계약 강제 형태", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-web-c19", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-web", - "topic": "identity-and-value-contracts", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_WEBSOCKET-L24", @@ -34540,12 +14712,10 @@ ], "sourceHeading": "이 모듈의 형태 — 하나의 leaf, 세 개의 설정 네임스페이스", "summary": "이 모듈의 형태 — 하나의 leaf, 세 개의 설정 네임스페이스", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-websocket-c01", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-websocket", - "topic": "composition-and-lifecycle-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_WEBSOCKET-L251", @@ -34556,12 +14726,10 @@ ], "sourceHeading": "(8.1) 도달성 — 정책의 실제 적용 지점", "summary": "(8.1) 도달성 — 정책의 실제 적용 지점", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-websocket-c02", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-websocket", - "topic": "transaction-and-consistency-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_WEBSOCKET-L267", @@ -34572,12 +14740,10 @@ ], "sourceHeading": "(8.4) 카운트 — `WebSocketFailureCategory`", "summary": "(8.4) 카운트 — `WebSocketFailureCategory`", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-websocket-c03", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-websocket", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_WEBSOCKET-L381", @@ -34588,12 +14754,10 @@ ], "sourceHeading": "(8.2) 조건 형제 비교 — 재개 토큰 서명", "summary": "(8.2) 조건 형제 비교 — 재개 토큰 서명", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-websocket-c04", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-websocket", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_INBOUND_WEBSOCKET-L420", @@ -34604,12 +14768,10 @@ ], "sourceHeading": "(8.3) 중복 메커니즘 — 승격 게이트", "summary": "(8.3) 중복 메커니즘 — 승격 게이트", - "disposition": "PROMOTE", - "target": "concept:adapter-inbound-websocket-c05", + "disposition": "KEEP_IN_SSOT", "module": "adapter-inbound-websocket", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_CACHE_REDIS-L85", @@ -34620,12 +14782,10 @@ ], "sourceHeading": "조립의 순서가 클래스 하나에 고정돼 있다", "summary": "조립의 순서가 클래스 하나에 고정돼 있다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-cache-redis-c01", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-cache-redis", - "topic": "composition-and-lifecycle-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_CACHE_REDIS-L107", @@ -34636,12 +14796,10 @@ ], "sourceHeading": "Confirmed — raw allowlist 기본값은 없는 리소스를 가리키고, 그것이 의도다", "summary": "Confirmed — raw allowlist 기본값은 없는 리소스를 가리키고, 그것이 의도다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-cache-redis-c02", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-cache-redis", - "topic": "composition-and-lifecycle-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_CACHE_REDIS-L235", @@ -34652,12 +14810,10 @@ ], "sourceHeading": "Confirmed — \"설계상 부재\" 주장 6건이 구현·정책 계층까지 일치한다", "summary": "Confirmed — \"설계상 부재\" 주장 6건이 구현·정책 계층까지 일치한다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-cache-redis-c03", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-cache-redis", - "topic": "identity-and-value-contracts", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_CACHE_REDIS-L245", @@ -34668,12 +14824,10 @@ ], "sourceHeading": "Confirmed — 두 프로그래밍 모델의 대칭이 기계 검사되고, 검사기 자신도 검사된다", "summary": "Confirmed — 두 프로그래밍 모델의 대칭이 기계 검사되고, 검사기 자신도 검사된다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-cache-redis-c04", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-cache-redis", - "topic": "admission-budget-and-backpressure", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_CACHE_REDIS-L324", @@ -34684,12 +14838,10 @@ ], "sourceHeading": "키: 렌더된 문자열을 받는 API가 존재하지 않는다", "summary": "키: 렌더된 문자열을 받는 API가 존재하지 않는다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-cache-redis-c05", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-cache-redis", - "topic": "admission-budget-and-backpressure", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_CACHE_REDIS-L332", @@ -34700,12 +14852,10 @@ ], "sourceHeading": "실패: 재시도 가능성과 모호성이 배타로 강제된다", "summary": "실패: 재시도 가능성과 모호성이 배타로 강제된다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-cache-redis-c06", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-cache-redis", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_CACHE_REDIS-L350", @@ -34716,12 +14866,10 @@ ], "sourceHeading": "명령 기술: 정책 파일과 서버 메타데이터의 접합점", "summary": "명령 기술: 정책 파일과 서버 메타데이터의 접합점", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-cache-redis-c07", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-cache-redis", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_CACHE_REDIS-L456", @@ -34732,12 +14880,10 @@ ], "sourceHeading": "Confirmed — 두 프로그래밍 모델이 같은 request builder를 공유한다", "summary": "Confirmed — 두 프로그래밍 모델이 같은 request builder를 공유한다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-cache-redis-c08", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-cache-redis", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_CACHE_REDIS-L477", @@ -34748,12 +14894,10 @@ ], "sourceHeading": "Confirmed — guard를 지나지 않는 경로가 하나 있고, 그것이 선언돼 있다", "summary": "Confirmed — guard를 지나지 않는 경로가 하나 있고, 그것이 선언돼 있다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-cache-redis-c09", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-cache-redis", - "topic": "result-and-failure-algebra", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_CACHE_REDIS-L560", @@ -34764,12 +14908,10 @@ ], "sourceHeading": "`CommandPolicyGuard` — 순서가 고정된 단일 입장 지점", "summary": "`CommandPolicyGuard` — 순서가 고정된 단일 입장 지점", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-cache-redis-c10", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-cache-redis", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_CACHE_REDIS-L579", @@ -34780,12 +14922,10 @@ ], "sourceHeading": "정책 문서를 일반 YAML 파서로 읽지 않는다", "summary": "정책 문서를 일반 YAML 파서로 읽지 않는다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-cache-redis-c11", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-cache-redis", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_CACHE_REDIS-L684", @@ -34796,12 +14936,10 @@ ], "sourceHeading": "raw gateway — \"escape hatch\"가 두 겹의 사전 승인으로 닫혀 있다", "summary": "raw gateway — \"escape hatch\"가 두 겹의 사전 승인으로 닫혀 있다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-cache-redis-c12", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-cache-redis", - "topic": "admission-budget-and-backpressure", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_CACHE_REDIS-L701", @@ -34812,12 +14950,10 @@ ], "sourceHeading": "스크립트와 트랜잭션 — 등록이 배포 단계이고, 창(window)은 노드에 고정된다", "summary": "스크립트와 트랜잭션 — 등록이 배포 단계이고, 창(window)은 노드에 고정된다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-cache-redis-c13", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-cache-redis", - "topic": "transaction-and-consistency-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_FILESERVER-L127", @@ -34828,12 +14964,10 @@ ], "sourceHeading": "Confirmed — 적재 경로는 auto-configuration이 아니라 명시적 component scan이다", "summary": "Confirmed — 적재 경로는 auto-configuration이 아니라 명시적 component scan이다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-fileserver-c01", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-fileserver", - "topic": "composition-and-lifecycle-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_FILESERVER-L168", @@ -34844,12 +14978,10 @@ ], "sourceHeading": "Confirmed — codec이 \"canonical\"을 왕복으로 강제한다", "summary": "Confirmed — codec이 \"canonical\"을 왕복으로 강제한다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-fileserver-c02", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-fileserver", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_FILESERVER-L184", @@ -34860,12 +14992,10 @@ ], "sourceHeading": "Confirmed — 상태 전이가 인접 행렬이고 terminal이 진짜 terminal이다", "summary": "Confirmed — 상태 전이가 인접 행렬이고 terminal이 진짜 terminal이다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-fileserver-c03", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-fileserver", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_FILESERVER-L192", @@ -34876,12 +15006,10 @@ ], "sourceHeading": "Confirmed — 두 개의 락 형태가 각자의 쓰기 원시연산에 맞춰져 있다", "summary": "Confirmed — 두 개의 락 형태가 각자의 쓰기 원시연산에 맞춰져 있다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-fileserver-c04", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-fileserver", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_FILESERVER-L300", @@ -34892,12 +15020,10 @@ ], "sourceHeading": "Confirmed — canonical digest가 길이 프레이밍이고, route token 충돌을 명시적으로 검사한다", "summary": "Confirmed — canonical digest가 길이 프레이밍이고, route token 충돌을 명시적으로 검사한다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-fileserver-c05", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-fileserver", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_FILESERVER-L507", @@ -34908,12 +15034,10 @@ ], "sourceHeading": "Confirmed — 인가와 감사가 정보를 흘리지 않는다", "summary": "Confirmed — 인가와 감사가 정보를 흘리지 않는다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-fileserver-c06", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-fileserver", - "topic": "admission-budget-and-backpressure", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_FILESERVER-L517", @@ -34924,12 +15048,10 @@ ], "sourceHeading": "Confirmed — 실패를 \"재시도 안전한가\"로 분류한다", "summary": "Confirmed — 실패를 \"재시도 안전한가\"로 분류한다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-fileserver-c07", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-fileserver", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_FILESERVER-L558", @@ -34940,12 +15062,10 @@ ], "sourceHeading": "Confirmed — payload 계층이 자신의 잔여 위험을 먼저 선언한다", "summary": "Confirmed — payload 계층이 자신의 잔여 위험을 먼저 선언한다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-fileserver-c08", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-fileserver", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_HTTPCLIENT-L90", @@ -34956,12 +15076,10 @@ ], "sourceHeading": "`ClientProfileValidator` — 34개 위반 코드가 각각 과거 사고를 적는다", "summary": "`ClientProfileValidator` — 34개 위반 코드가 각각 과거 사고를 적는다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-httpclient-c01", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-httpclient", - "topic": "admission-budget-and-backpressure", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_HTTPCLIENT-L112", @@ -34972,12 +15090,10 @@ ], "sourceHeading": "`ClientRuntimeRegistry` — 세대 교체가 틈으로 관측되지 않는다", "summary": "`ClientRuntimeRegistry` — 세대 교체가 틈으로 관측되지 않는다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-httpclient-c02", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-httpclient", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_HTTPCLIENT-L237", @@ -34988,12 +15104,10 @@ ], "sourceHeading": "`ObjectBody`의 재생 가능성 판정 — 값의 성질이지 코덱의 성질이 아니다", "summary": "`ObjectBody`의 재생 가능성 판정 — 값의 성질이지 코덱의 성질이 아니다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-httpclient-c03", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-httpclient", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_HTTPCLIENT-L305", @@ -35004,12 +15118,10 @@ ], "sourceHeading": "재시도 결정표가 순서로 표현돼 있다", "summary": "재시도 결정표가 순서로 표현돼 있다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-httpclient-c04", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-httpclient", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_HTTPCLIENT-L323", @@ -35020,12 +15132,10 @@ ], "sourceHeading": "가드 순서와 그 근거", "summary": "가드 순서와 그 근거", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-httpclient-c05", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-httpclient", - "topic": "admission-budget-and-backpressure", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_HTTPCLIENT-L403", @@ -35036,12 +15146,10 @@ ], "sourceHeading": "두 예산, 두 계층, 그리고 읽는 도중의 강제", "summary": "두 예산, 두 계층, 그리고 읽는 도중의 강제", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-httpclient-c06", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-httpclient", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_HTTPCLIENT-L474", @@ -35052,12 +15160,10 @@ ], "sourceHeading": "목적지 정책 — 절대 URI를 정화하지 않고 거부한다", "summary": "목적지 정책 — 절대 URI를 정화하지 않고 거부한다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-httpclient-c07", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-httpclient", - "topic": "security-and-trust-boundaries", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_HTTPCLIENT-L573", @@ -35068,12 +15174,10 @@ ], "sourceHeading": "전송은 능력을 선언하고, 프로파일보다 약하면 startup이 실패한다", "summary": "전송은 능력을 선언하고, 프로파일보다 약하면 startup이 실패한다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-httpclient-c08", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-httpclient", - "topic": "admission-budget-and-backpressure", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_HTTPCLIENT-L627", @@ -35084,12 +15188,10 @@ ], "sourceHeading": "교정 — 영구 TLS 실패의 `CONNECT` 분류는 분류기 결함이 아니라 픽스처의 듀얼스택 호스트명이다", "summary": "교정 — 영구 TLS 실패의 `CONNECT` 분류는 분류기 결함이 아니라 픽스처의 듀얼스택 호스트명이다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-httpclient-c09", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-httpclient", - "topic": "security-and-trust-boundaries", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_MESSAGING-L174", @@ -35100,12 +15202,10 @@ ], "sourceHeading": "레지스트리가 \"닫혀 있다\"는 것의 의미", "summary": "레지스트리가 \"닫혀 있다\"는 것의 의미", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-messaging-c01", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-messaging", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_MESSAGING-L189", @@ -35116,12 +15216,10 @@ ], "sourceHeading": "봉투 작성이 파서를 거치지 않는다", "summary": "봉투 작성이 파서를 거치지 않는다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-messaging-c02", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-messaging", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_MESSAGING-L237", @@ -35132,12 +15230,10 @@ ], "sourceHeading": "계약이 컴파일되어 닫힌다", "summary": "계약이 컴파일되어 닫힌다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-messaging-c03", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-messaging", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_MESSAGING-L284", @@ -35148,12 +15244,10 @@ ], "sourceHeading": "두 발행 경로의 실패 정책이 정반대이고 그 이유가 적혀 있다", "summary": "두 발행 경로의 실패 정책이 정반대이고 그 이유가 적혀 있다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-messaging-c04", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-messaging", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_NOTIFICATION-L84", @@ -35164,12 +15258,10 @@ ], "sourceHeading": "\"이름 없는 상태\"를 없애는 것이 이 sub-scope의 주제다", "summary": "\"이름 없는 상태\"를 없애는 것이 이 sub-scope의 주제다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-notification-c01", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-notification", - "topic": "composition-and-lifecycle-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_NOTIFICATION-L306", @@ -35180,12 +15272,10 @@ ], "sourceHeading": "(8.3) 중복 메커니즘 — 종료 경로", "summary": "(8.3) 중복 메커니즘 — 종료 경로", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-notification-c02", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-notification", - "topic": "composition-and-lifecycle-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_NOTIFICATION-L582", @@ -35196,12 +15286,10 @@ ], "sourceHeading": "(8.1) 도달성 — provider가 준 `Retry-After`는 실제로 쓰이는가", "summary": "(8.1) 도달성 — provider가 준 `Retry-After`는 실제로 쓰이는가", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-notification-c03", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-notification", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_NOTIFICATION-L619", @@ -35212,12 +15300,10 @@ ], "sourceHeading": "(8.4) 문서/카운트 드리프트 — 어떤 상태가 unhealthy인가", "summary": "(8.4) 문서/카운트 드리프트 — 어떤 상태가 unhealthy인가", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-notification-c04", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-notification", - "topic": "security-and-trust-boundaries", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_NOTIFICATION-L703", @@ -35228,12 +15314,10 @@ ], "sourceHeading": "(8.1) 도달성 — SSRF 가드가 도달하는 호출처 전수", "summary": "(8.1) 도달성 — SSRF 가드가 도달하는 호출처 전수", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-notification-c05", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-notification", - "topic": "identity-and-value-contracts", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_NOTIFICATION-L913", @@ -35244,12 +15328,10 @@ ], "sourceHeading": "(8.1) 도달성 — 공유 계약을 실제로 상속하는 어댑터", "summary": "(8.1) 도달성 — 공유 계약을 실제로 상속하는 어댑터", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-notification-c06", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-notification", - "topic": "identity-and-value-contracts", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_OBJECTSTORAGE-L63", @@ -35260,12 +15342,10 @@ ], "sourceHeading": "Confirmed — \"컴파일이 먼저, 생성은 나중\"이 실제 순서다", "summary": "Confirmed — \"컴파일이 먼저, 생성은 나중\"이 실제 순서다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-objectstorage-c01", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-objectstorage", - "topic": "capability-and-disclosure-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_OBJECTSTORAGE-L92", @@ -35276,12 +15356,10 @@ ], "sourceHeading": "Confirmed — legacy가 세 겹으로 격리돼 있다", "summary": "Confirmed — legacy가 세 겹으로 격리돼 있다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-objectstorage-c02", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-objectstorage", - "topic": "admission-budget-and-backpressure", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_OBJECTSTORAGE-L135", @@ -35292,12 +15370,10 @@ ], "sourceHeading": "Confirmed — 후보로 본 unguarded split은 값 타입이 막고 있다", "summary": "Confirmed — 후보로 본 unguarded split은 값 타입이 막고 있다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-objectstorage-c03", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-objectstorage", - "topic": "security-and-trust-boundaries", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_OBJECTSTORAGE-L173", @@ -35308,12 +15384,10 @@ ], "sourceHeading": "Confirmed — 계열이 닫혀 있고 스키마가 fail-closed다", "summary": "Confirmed — 계열이 닫혀 있고 스키마가 fail-closed다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-objectstorage-c04", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-objectstorage", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_OBJECTSTORAGE-L244", @@ -35324,12 +15398,10 @@ ], "sourceHeading": "Confirmed — 다섯 개의 닫힌 전이표가 있고 terminal이 진짜 terminal이다", "summary": "Confirmed — 다섯 개의 닫힌 전이표가 있고 terminal이 진짜 terminal이다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-objectstorage-c05", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-objectstorage", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_OBJECTSTORAGE-L273", @@ -35340,12 +15412,10 @@ ], "sourceHeading": "Confirmed — 모든 키가 단일 인코더에서 나오고 route를 벗어날 수 없다", "summary": "Confirmed — 모든 키가 단일 인코더에서 나오고 route를 벗어날 수 없다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-objectstorage-c06", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-objectstorage", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_OBJECTSTORAGE-L420", @@ -35356,12 +15426,10 @@ ], "sourceHeading": "이 sub-scope의 설계 — 비밀은 durable하지 않고, 승인은 명시적으로 닫힌다", "summary": "이 sub-scope의 설계 — 비밀은 durable하지 않고, 승인은 명시적으로 닫힌다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-objectstorage-c07", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-objectstorage", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_OBJECTSTORAGE-L560", @@ -35372,12 +15440,10 @@ ], "sourceHeading": "§41 보강 — 레지스트리는 문서 주장을 얼어붙히지만 런타임 설정 경로는 덮지 않는다", "summary": "§41 보강 — 레지스트리는 문서 주장을 얼어붙히지만 런타임 설정 경로는 덮지 않는다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-objectstorage-c08", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-objectstorage", - "topic": "capability-and-disclosure-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_OBJECTSTORAGE-L601", @@ -35388,12 +15454,10 @@ ], "sourceHeading": "Confirmed — local-dev provider의 경로 방어와 publication", "summary": "Confirmed — local-dev provider의 경로 방어와 publication", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-objectstorage-c09", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-objectstorage", - "topic": "identity-and-value-contracts", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L70", @@ -35404,12 +15468,10 @@ ], "sourceHeading": "Sub-scope 02 — API contracts (`api/**`)", "summary": "Sub-scope 02 — API contracts (`api/**`)", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c01", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L126", @@ -35420,12 +15482,10 @@ ], "sourceHeading": "Capability API — 실행 기능과 지원 등급을 reportable contract로 분리", "summary": "Capability API — 실행 기능과 지원 등급을 reportable contract로 분리", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c02", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "transaction-and-consistency-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L208", @@ -35436,12 +15496,10 @@ ], "sourceHeading": "Error API — provider exception을 stable failure algebra로 변환", "summary": "Error API — provider exception을 stable failure algebra로 변환", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c03", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L302", @@ -35452,12 +15510,10 @@ ], "sourceHeading": "Query API — pagination 비용과 trust boundary를 type shape로 제한", "summary": "Query API — pagination 비용과 trust boundary를 type shape로 제한", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c04", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L358", @@ -35468,12 +15524,10 @@ ], "sourceHeading": "`SignedJsonCursorCodec`: 좋은 trust-boundary 설계와 경계값 결함이 동시에 존재", "summary": "`SignedJsonCursorCodec`: 좋은 trust-boundary 설계와 경계값 결함이 동시에 존재", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c05", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L462", @@ -35484,12 +15538,10 @@ ], "sourceHeading": "Transaction API — 실행체보다 먼저 retry 가능 상태를 제한한다", "summary": "Transaction API — 실행체보다 먼저 retry 가능 상태를 제한한다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c06", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L639", @@ -35500,12 +15552,10 @@ ], "sourceHeading": "API surface verification", "summary": "API surface verification", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c07", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L721", @@ -35516,12 +15566,10 @@ ], "sourceHeading": "Sub-scope 03 — transaction + persistence failure", "summary": "Sub-scope 03 — transaction + persistence failure", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c08", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L737", @@ -35532,12 +15580,10 @@ ], "sourceHeading": "같은 leaf 안에 두 개의 transaction model이 존재한다", "summary": "같은 leaf 안에 두 개의 transaction model이 존재한다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c09", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L763", @@ -35548,12 +15594,10 @@ ], "sourceHeading": "B. persistence-jpa public API boundary", "summary": "B. persistence-jpa public API boundary", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c10", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "transaction-and-consistency-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L788", @@ -35564,12 +15608,10 @@ ], "sourceHeading": "`SpringTransactionPort`: application-core의 실제 Spring 구현", "summary": "`SpringTransactionPort`: application-core의 실제 Spring 구현", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c11", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "transaction-and-consistency-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L832", @@ -35580,12 +15622,10 @@ ], "sourceHeading": "`SpringPolicyTransactionPort`: transaction result를 boolean 성공/실패보다 세밀하게 표현", "summary": "`SpringPolicyTransactionPort`: transaction result를 boolean 성공/실패보다 세밀하게 표현", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c12", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L879", @@ -35596,12 +15636,10 @@ ], "sourceHeading": "CallBudget를 transaction timeout보다 먼저 적용한다", "summary": "CallBudget를 transaction timeout보다 먼저 적용한다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c13", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L938", @@ -35612,12 +15650,10 @@ ], "sourceHeading": "retry classification은 structured state로 제한한다", "summary": "retry classification은 structured state로 제한한다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c14", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "transaction-and-consistency-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L953", @@ -35628,12 +15664,10 @@ ], "sourceHeading": "public JPA path: `SpringJpaTransactionExecutor`", "summary": "public JPA path: `SpringJpaTransactionExecutor`", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c15", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "transaction-and-consistency-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L974", @@ -35644,12 +15678,10 @@ ], "sourceHeading": "`FullTransactionRetryCoordinator`: whole-use-case retry 의도", "summary": "`FullTransactionRetryCoordinator`: whole-use-case retry 의도", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c16", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "transaction-and-consistency-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L1074", @@ -35660,12 +15692,10 @@ ], "sourceHeading": "`CommitFailureClassifier`", "summary": "`CommitFailureClassifier`", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c17", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L1184", @@ -35676,12 +15706,10 @@ ], "sourceHeading": "reconciliation record production path = 0", "summary": "reconciliation record production path = 0", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c18", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "transaction-and-consistency-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L1210", @@ -35692,12 +15720,10 @@ ], "sourceHeading": "completion-unknown metric도 현재 transaction path에서 호출되지 않는다", "summary": "completion-unknown metric도 현재 transaction path에서 호출되지 않는다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c19", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L1334", @@ -35708,12 +15734,10 @@ ], "sourceHeading": "zero-reference지만 dead가 아닌 `JpaTransactionConfig`", "summary": "zero-reference지만 dead가 아닌 `JpaTransactionConfig`", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c20", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "transaction-and-consistency-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L1358", @@ -35724,12 +15748,10 @@ ], "sourceHeading": "두 failure translator 계열은 현재 역할이 다르다", "summary": "두 failure translator 계열은 현재 역할이 다르다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c21", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "transaction-and-consistency-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L1384", @@ -35740,12 +15762,10 @@ ], "sourceHeading": "`failure.PersistenceExceptionTranslator`", "summary": "`failure.PersistenceExceptionTranslator`", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c22", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "transaction-and-consistency-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L1416", @@ -35756,12 +15776,10 @@ ], "sourceHeading": "runtime bean-factory-owned", "summary": "runtime bean-factory-owned", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c23", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "transaction-and-consistency-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L1610", @@ -35772,12 +15790,10 @@ ], "sourceHeading": "이 sub-scope는 하나의 query framework가 아니라 세 단계의 정책층이다", "summary": "이 sub-scope는 하나의 query framework가 아니라 세 단계의 정책층이다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c24", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "capability-and-disclosure-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L1643", @@ -35788,12 +15804,10 @@ ], "sourceHeading": "Hibernate provider policy는 declared baseline과 실제 runtime을 분리한다", "summary": "Hibernate provider policy는 declared baseline과 실제 runtime을 분리한다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c25", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L1819", @@ -35804,12 +15818,10 @@ ], "sourceHeading": "bulk DML과 StatelessSession은 일반 repository path와 다른 비용 모델을 명시한다", "summary": "bulk DML과 StatelessSession은 일반 repository path와 다른 비용 모델을 명시한다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c26", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "transaction-and-consistency-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L1860", @@ -35820,12 +15832,10 @@ ], "sourceHeading": "Spring Data repository support는 generic CRUD보다 query execution policy에 가깝다", "summary": "Spring Data repository support는 generic CRUD보다 query execution policy에 가깝다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c27", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "query-and-pagination-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L1933", @@ -35836,12 +15846,10 @@ ], "sourceHeading": "keyset predicate는 mixed type / mixed direction을 표현하도록 진화했다", "summary": "keyset predicate는 mixed type / mixed direction을 표현하도록 진화했다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c28", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "query-and-pagination-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L1973", @@ -35852,12 +15860,10 @@ ], "sourceHeading": "keyset execution은 `size + 1`로 hasNext를 판정하고 count query를 제거한다", "summary": "keyset execution은 `size + 1`로 hasNext를 판정하고 count query를 제거한다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c29", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "query-and-pagination-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L2087", @@ -35868,12 +15874,10 @@ ], "sourceHeading": "Querydsl integration은 production runtime classpath를 강제로 오염시키지 않는다", "summary": "Querydsl integration은 production runtime classpath를 강제로 오염시키지 않는다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c30", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L2151", @@ -35884,12 +15888,10 @@ ], "sourceHeading": "대부분의 optimization helper가 production에서 직접 소비되지 않는다는 사실은 이미 repository가 알고 있다", "summary": "대부분의 optimization helper가 production에서 직접 소비되지 않는다는 사실은 이미 repository가 알고 있다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c31", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L2217", @@ -35900,12 +15902,10 @@ ], "sourceHeading": "실제 app-bootstrap consumer rule은 별도 allowlist를 다시 가진다", "summary": "실제 app-bootstrap consumer rule은 별도 allowlist를 다시 가진다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c32", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L2230", @@ -35916,12 +15916,10 @@ ], "sourceHeading": "leaf list 자체는 outside consumer를 검사하지 않는다", "summary": "leaf list 자체는 outside consumer를 검사하지 않는다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c33", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L2523", @@ -35932,12 +15930,10 @@ ], "sourceHeading": "release-task existence validator", "summary": "release-task existence validator", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c34", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L2632", @@ -35948,12 +15944,10 @@ ], "sourceHeading": "PostgreSQL failure translation: SQLSTATE 분류는 맞지만 `40003` 의미가 translator에서 소실된다", "summary": "PostgreSQL failure translation: SQLSTATE 분류는 맞지만 `40003` 의미가 translator에서 소실된다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c35", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L2669", @@ -35964,12 +15958,10 @@ ], "sourceHeading": "PostgreSQL Idempotency V2: owner/CAS 구조는 강하지만 replay 경계가 두 군데 어긋난다", "summary": "PostgreSQL Idempotency V2: owner/CAS 구조는 강하지만 replay 경계가 두 군데 어긋난다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c36", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L2734", @@ -35980,12 +15972,10 @@ ], "sourceHeading": "Same-store inbox / polling outbox: 구현 계약은 강하지만 현재 미조립 candidate에 replay holes가 있다", "summary": "Same-store inbox / polling outbox: 구현 계약은 강하지만 현재 미조립 candidate에 replay holes가 있다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c37", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "transaction-and-consistency-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L2780", @@ -35996,12 +15986,10 @@ ], "sourceHeading": "확인된 안전 경계", "summary": "확인된 안전 경계", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c38", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L2805", @@ -36012,12 +16000,10 @@ ], "sourceHeading": "Vendor migrations", "summary": "Vendor migrations", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c39", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L2883", @@ -36028,12 +16014,10 @@ ], "sourceHeading": "Idempotency real-PostgreSQL TTL boundaries", "summary": "Idempotency real-PostgreSQL TTL boundaries", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c40", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L2913", @@ -36044,12 +16028,10 @@ ], "sourceHeading": "이번 scope에서 finding으로 승격하지 않은 항목", "summary": "이번 scope에서 finding으로 승격하지 않은 항목", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c41", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L2971", @@ -36060,12 +16042,10 @@ ], "sourceHeading": "Baseline composition을 먼저 분리해야 하는 이유", "summary": "Baseline composition을 먼저 분리해야 하는 이유", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c42", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "transaction-and-consistency-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L3119", @@ -36076,12 +16056,10 @@ ], "sourceHeading": "H2 idempotency와 V2 owner 필드", "summary": "H2 idempotency와 V2 owner 필드", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c43", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L3123", @@ -36092,12 +16070,10 @@ ], "sourceHeading": "`audit`와 `auditing` 두 경로", "summary": "`audit`와 `auditing` 두 경로", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c44", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "observability-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L3166", @@ -36108,12 +16084,10 @@ ], "sourceHeading": "Fileserver composition과 schema lifecycle", "summary": "Fileserver composition과 schema lifecycle", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c45", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L3350", @@ -36124,12 +16098,10 @@ ], "sourceHeading": "quota FIFO settlement 자체", "summary": "quota FIFO settlement 자체", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c46", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L3354", @@ -36140,12 +16112,10 @@ ], "sourceHeading": "cleanup fenced lease의 expiry-after / takeover-before window", "summary": "cleanup fenced lease의 expiry-after / takeover-before window", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c47", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L3401", @@ -36156,12 +16126,10 @@ ], "sourceHeading": "Notification composition과 schema lifecycle", "summary": "Notification composition과 schema lifecycle", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c48", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L3561", @@ -36172,12 +16140,10 @@ ], "sourceHeading": "crypto envelope와 contact-point secret protection", "summary": "crypto envelope와 contact-point secret protection", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c49", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L3565", @@ -36188,12 +16154,10 @@ ], "sourceHeading": "tenant-bound repository guard", "summary": "tenant-bound repository guard", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c50", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L3609", @@ -36204,12 +16168,10 @@ ], "sourceHeading": "현재 production composition은 Experimental을 실행하지 않지만 opt-in 경계는 완전히 구조적이지 않다", "summary": "현재 production composition은 Experimental을 실행하지 않지만 opt-in 경계는 완전히 구조적이지 않다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c51", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "transaction-and-consistency-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L3780", @@ -36220,12 +16182,10 @@ ], "sourceHeading": "schema identifier selection/reset", "summary": "schema identifier selection/reset", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c52", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L3784", @@ -36236,12 +16196,10 @@ ], "sourceHeading": "tenant repository/listener guard가 곧 production isolation이라는 주장", "summary": "tenant repository/listener guard가 곧 production isolation이라는 주장", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c53", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "security-and-trust-boundaries", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L4017", @@ -36252,12 +16210,10 @@ ], "sourceHeading": "`CommitAmbiguityProxy` / `PostgreSqlContractExtension`", "summary": "`CommitAmbiguityProxy` / `PostgreSqlContractExtension`", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c54", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L4021", @@ -36268,12 +16224,10 @@ ], "sourceHeading": "`JpaReleaseManifest`의 regex parser", "summary": "`JpaReleaseManifest`의 regex parser", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c55", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L4143", @@ -36284,12 +16238,10 @@ ], "sourceHeading": "always-install scan과 opt-in scan의 경계는 실제로 지켜지고 있다", "summary": "always-install scan과 opt-in scan의 경계는 실제로 지켜지고 있다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c56", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "composition-and-lifecycle-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L4306", @@ -36300,12 +16252,10 @@ ], "sourceHeading": "release gate 소속은 양방향으로 검증되지 않는다", "summary": "release gate 소속은 양방향으로 검증되지 않는다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c57", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_JPA-L4541", @@ -36316,12 +16266,10 @@ ], "sourceHeading": "`JpaPlatformContractSupport`의 컨테이너 수명 서술은 실제와 다르다", "summary": "`JpaPlatformContractSupport`의 컨테이너 수명 서술은 실제와 다르다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-jpa-c58", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-jpa", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_MONGO-L98", @@ -36332,12 +16280,10 @@ ], "sourceHeading": "opt-in은 네 겹이고, 각 겹이 서로 다른 실패를 막는다", "summary": "opt-in은 네 겹이고, 각 겹이 서로 다른 실패를 막는다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-mongo-c01", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-mongo", - "topic": "composition-and-lifecycle-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_MONGO-L493", @@ -36348,12 +16294,10 @@ ], "sourceHeading": "mapping의 나머지는 manifest를 실제로 강제한다", "summary": "mapping의 나머지는 manifest를 실제로 강제한다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-mongo-c02", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-mongo", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_MONGO-L586", @@ -36364,12 +16308,10 @@ ], "sourceHeading": "실행 scope의 고정된 순서가 이 sub-scope의 중심이다", "summary": "실행 scope의 고정된 순서가 이 sub-scope의 중심이다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-mongo-c03", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-mongo", - "topic": "transaction-and-consistency-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_MONGO-L648", @@ -36380,12 +16322,10 @@ ], "sourceHeading": "reactive 경로가 명시적으로 배치한 세 가지", "summary": "reactive 경로가 명시적으로 배치한 세 가지", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-mongo-c04", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-mongo", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_MONGO-L709", @@ -36396,12 +16336,10 @@ ], "sourceHeading": "Confirmed — 이 sub-scope는 정책과 값 객체이고, 배선된 것은 하나뿐이다", "summary": "Confirmed — 이 sub-scope는 정책과 값 객체이고, 배선된 것은 하나뿐이다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-mongo-c05", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-mongo", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_MONGO-L868", @@ -36412,12 +16350,10 @@ ], "sourceHeading": "migration은 fencing을 정면으로 다룬다", "summary": "migration은 fencing을 정면으로 다룬다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-mongo-c06", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-mongo", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_MONGO-L1015", @@ -36428,12 +16364,10 @@ ], "sourceHeading": "이 sub-scope는 이 leaf에서 유일하게 \"조립까지 된\" 대형 서브시스템이다", "summary": "이 sub-scope는 이 leaf에서 유일하게 \"조립까지 된\" 대형 서브시스템이다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-mongo-c07", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-mongo", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_MONGO-L1142", @@ -36444,12 +16378,10 @@ ], "sourceHeading": "`failure`는 이 leaf에서 가장 잘 배선되고 가장 잘 논증된 부분이다", "summary": "`failure`는 이 leaf에서 가장 잘 배선되고 가장 잘 논증된 부분이다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-mongo-c08", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-mongo", - "topic": "composition-and-lifecycle-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_PERSISTENCE_MONGO-L1269", @@ -36460,12 +16392,10 @@ ], "sourceHeading": "Confirmed — 분류 불변식이 실제로 성립한다", "summary": "Confirmed — 분류 불변식이 실제로 성립한다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-persistence-mongo-c09", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-persistence-mongo", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_SUPPORT-L52", @@ -36476,12 +16406,10 @@ ], "sourceHeading": "모듈의 정체와 경계", "summary": "모듈의 정체와 경계", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-support-c01", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-support", - "topic": "composition-and-lifecycle-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_SUPPORT-L119", @@ -36492,12 +16420,10 @@ ], "sourceHeading": "`FailOpenDependencyLogger`: 진단을 business outcome과 분리하려는 계약", "summary": "`FailOpenDependencyLogger`: 진단을 business outcome과 분리하려는 계약", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-support-c02", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-support", - "topic": "observability-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_SUPPORT-L207", @@ -36508,12 +16434,10 @@ ], "sourceHeading": "global masking도 이 보장을 복구하지 않는다", "summary": "global masking도 이 보장을 복구하지 않는다", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-support-c03", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-support", - "topic": "security-and-trust-boundaries", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_SUPPORT-L318", @@ -36524,12 +16448,10 @@ ], "sourceHeading": "`OutboundSupportConfig`: unconditional shared bean seam과 실제 runtime wiring", "summary": "`OutboundSupportConfig`: unconditional shared bean seam과 실제 runtime wiring", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-support-c04", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-support", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_SUPPORT-L368", @@ -36540,12 +16462,10 @@ ], "sourceHeading": "outbound peer isolation", "summary": "outbound peer isolation", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-support-c05", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-support", - "topic": "security-and-trust-boundaries", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-ADAPTER_OUTBOUND_SUPPORT-L558", @@ -36556,12 +16476,10 @@ ], "sourceHeading": "architecture suite / dependency registry", "summary": "architecture suite / dependency registry", - "disposition": "PROMOTE", - "target": "concept:adapter-outbound-support-c06", + "disposition": "KEEP_IN_SSOT", "module": "adapter-outbound-support", - "topic": "observability-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-APP_BOOTSTRAP-L179", @@ -36572,12 +16490,10 @@ ], "sourceHeading": "(8.3) 중복 메커니즘 — 세 개의 환경 검증기", "summary": "(8.3) 중복 메커니즘 — 세 개의 환경 검증기", - "disposition": "PROMOTE", - "target": "concept:app-bootstrap-c01", + "disposition": "KEEP_IN_SSOT", "module": "app-bootstrap", - "topic": "capability-and-disclosure-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-APP_BOOTSTRAP-L185", @@ -36588,12 +16504,10 @@ ], "sourceHeading": "— 다섯 어댑터 범위는 런타임 멤버십 레지스트리와 일치한다 (결함 아님)", "summary": "— 다섯 어댑터 범위는 런타임 멤버십 레지스트리와 일치한다 (결함 아님)", - "disposition": "PROMOTE", - "target": "concept:app-bootstrap-c02", + "disposition": "KEEP_IN_SSOT", "module": "app-bootstrap", - "topic": "capability-and-disclosure-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-APP_BOOTSTRAP-L264", @@ -36604,12 +16518,10 @@ ], "sourceHeading": "(8.4) 카운트 — `.imports` 여섯 줄과 다섯 능력", "summary": "(8.4) 카운트 — `.imports` 여섯 줄과 다섯 능력", - "disposition": "PROMOTE", - "target": "concept:app-bootstrap-c03", + "disposition": "KEEP_IN_SSOT", "module": "app-bootstrap", - "topic": "capability-and-disclosure-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-APP_BOOTSTRAP-L386", @@ -36620,12 +16532,10 @@ ], "sourceHeading": "(8.1)·(8.2) 도달성과 게이트", "summary": "(8.1)·(8.2) 도달성과 게이트", - "disposition": "PROMOTE", - "target": "concept:app-bootstrap-c04", + "disposition": "KEEP_IN_SSOT", "module": "app-bootstrap", - "topic": "identity-and-value-contracts", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-APP_BOOTSTRAP-L460", @@ -36636,12 +16546,10 @@ ], "sourceHeading": "(8.1) 도달성 — 레지스트리 계약이 실제 레지스트리 파일을 읽는가", "summary": "(8.1) 도달성 — 레지스트리 계약이 실제 레지스트리 파일을 읽는가", - "disposition": "PROMOTE", - "target": "concept:app-bootstrap-c05", + "disposition": "KEEP_IN_SSOT", "module": "app-bootstrap", - "topic": "security-and-trust-boundaries", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-APPLICATION_CORE-L58", @@ -36652,12 +16560,10 @@ ], "sourceHeading": "모듈 경계와 빌드 의존성", "summary": "모듈 경계와 빌드 의존성", - "disposition": "PROMOTE", - "target": "concept:application-core-c01", + "disposition": "KEEP_IN_SSOT", "module": "application-core", - "topic": "query-and-pagination-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-APPLICATION_CORE-L78", @@ -36668,12 +16574,10 @@ ], "sourceHeading": "authorization: permission과 object access를 분리한다", "summary": "authorization: permission과 object access를 분리한다", - "disposition": "PROMOTE", - "target": "concept:application-core-c02", + "disposition": "KEEP_IN_SSOT", "module": "application-core", - "topic": "security-and-trust-boundaries", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-APPLICATION_CORE-L88", @@ -36684,12 +16588,10 @@ ], "sourceHeading": "transaction: framework vocabulary 대신 application semantic policy", "summary": "transaction: framework vocabulary 대신 application semantic policy", - "disposition": "PROMOTE", - "target": "concept:application-core-c03", + "disposition": "KEEP_IN_SSOT", "module": "application-core", - "topic": "transaction-and-consistency-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-APPLICATION_CORE-L118", @@ -36700,12 +16602,10 @@ ], "sourceHeading": "idempotency, inbox, outbox: uncertainty를 상태로 보존한다", "summary": "idempotency, inbox, outbox: uncertainty를 상태로 보존한다", - "disposition": "PROMOTE", - "target": "concept:application-core-c04", + "disposition": "KEEP_IN_SSOT", "module": "application-core", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-APPLICATION_CORE-L152", @@ -36716,12 +16616,10 @@ ], "sourceHeading": "cache, lease, lock: 동시성 완화와 correctness authority를 구분한다", "summary": "cache, lease, lock: 동시성 완화와 correctness authority를 구분한다", - "disposition": "PROMOTE", - "target": "concept:application-core-c05", + "disposition": "KEEP_IN_SSOT", "module": "application-core", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-APPLICATION_CORE-L174", @@ -36732,12 +16630,10 @@ ], "sourceHeading": "messaging과 realtime은 provider/transport vocabulary를 밖으로 밀어낸다", "summary": "messaging과 realtime은 provider/transport vocabulary를 밖으로 밀어낸다", - "disposition": "PROMOTE", - "target": "concept:application-core-c06", + "disposition": "KEEP_IN_SSOT", "module": "application-core", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-APPLICATION_CORE-L182", @@ -36748,12 +16644,10 @@ ], "sourceHeading": "storage/file publication: legacy 경로와 semantic 경로가 공존한다", "summary": "storage/file publication: legacy 경로와 semantic 경로가 공존한다", - "disposition": "PROMOTE", - "target": "concept:application-core-c07", + "disposition": "KEEP_IN_SSOT", "module": "application-core", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-APPLICATION_CORE-L190", @@ -36764,12 +16658,10 @@ ], "sourceHeading": "objectstorage: staged lifecycle, opaque identity, privilege separation", "summary": "objectstorage: staged lifecycle, opaque identity, privilege separation", - "disposition": "PROMOTE", - "target": "concept:application-core-c08", + "disposition": "KEEP_IN_SSOT", "module": "application-core", - "topic": "admission-budget-and-backpressure", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-APPLICATION_CORE-L200", @@ -36780,12 +16672,10 @@ ], "sourceHeading": "fileserver: DB metadata와 physical content 사이의 실패 seam을 명시한다", "summary": "fileserver: DB metadata와 physical content 사이의 실패 seam을 명시한다", - "disposition": "PROMOTE", - "target": "concept:application-core-c09", + "disposition": "KEEP_IN_SSOT", "module": "application-core", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-APPLICATION_CORE-L230", @@ -36796,12 +16686,10 @@ ], "sourceHeading": "public API와 secret boundary", "summary": "public API와 secret boundary", - "disposition": "PROMOTE", - "target": "concept:application-core-c10", + "disposition": "KEEP_IN_SSOT", "module": "application-core", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-APPLICATION_CORE-L270", @@ -36812,12 +16700,10 @@ ], "sourceHeading": "실제 production reachability와 legacy/dead-path 판정", "summary": "실제 production reachability와 legacy/dead-path 판정", - "disposition": "PROMOTE", - "target": "concept:application-core-c11", + "disposition": "KEEP_IN_SSOT", "module": "application-core", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-DOMAIN_CORE-L68", @@ -36828,12 +16714,10 @@ ], "sourceHeading": "관찰: 재사용 가능한 도메인 “내용”보다 도메인 모델링 계약을 소유한다", "summary": "관찰: 재사용 가능한 도메인 “내용”보다 도메인 모델링 계약을 소유한다", - "disposition": "PROMOTE", - "target": "concept:domain-core-c01", + "disposition": "KEEP_IN_SSOT", "module": "domain-core", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-DOMAIN_CORE-L146", @@ -36844,12 +16728,10 @@ ], "sourceHeading": "Runtime reachability / wiring", "summary": "Runtime reachability / wiring", - "disposition": "PROMOTE", - "target": "concept:domain-core-c02", + "disposition": "KEEP_IN_SSOT", "module": "domain-core", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-DOMAIN_CORE-L158", @@ -36860,12 +16742,10 @@ ], "sourceHeading": "Success / failure mechanics", "summary": "Success / failure mechanics", - "disposition": "PROMOTE", - "target": "concept:domain-core-c03", + "disposition": "KEEP_IN_SSOT", "module": "domain-core", - "topic": "result-and-failure-algebra", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-GRPC_ADMIN-L50", @@ -36876,12 +16756,10 @@ ], "sourceHeading": "건강 레지스트리 — 낙관에서 시작하지 않는다", "summary": "건강 레지스트리 — 낙관에서 시작하지 않는다", - "disposition": "PROMOTE", - "target": "concept:grpc-admin-c01", + "disposition": "KEEP_IN_SSOT", "module": "grpc-admin", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-GRPC_ADMIN-L65", @@ -36892,12 +16770,10 @@ ], "sourceHeading": "배수 순서", "summary": "배수 순서", - "disposition": "PROMOTE", - "target": "concept:grpc-admin-c02", + "disposition": "KEEP_IN_SSOT", "module": "grpc-admin", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-GRPC_ADVANCED_BOOTSTRAP-L53", @@ -36908,12 +16784,10 @@ ], "sourceHeading": "능력 15종과 등급 4종", "summary": "능력 15종과 등급 4종", - "disposition": "PROMOTE", - "target": "concept:grpc-advanced-bootstrap-c01", + "disposition": "KEEP_IN_SSOT", "module": "grpc-advanced-bootstrap", - "topic": "admission-budget-and-backpressure", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-GRPC_ADVANCED_BOOTSTRAP-L76", @@ -36924,12 +16798,10 @@ ], "sourceHeading": "게이트가 세 조건을 순서대로 본다", "summary": "게이트가 세 조건을 순서대로 본다", - "disposition": "PROMOTE", - "target": "concept:grpc-advanced-bootstrap-c02", + "disposition": "KEEP_IN_SSOT", "module": "grpc-advanced-bootstrap", - "topic": "composition-and-lifecycle-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-GRPC_ADVANCED_BOOTSTRAP-L89", @@ -36940,12 +16812,10 @@ ], "sourceHeading": "승격 게이트", "summary": "승격 게이트", - "disposition": "PROMOTE", - "target": "concept:grpc-advanced-bootstrap-c03", + "disposition": "KEEP_IN_SSOT", "module": "grpc-advanced-bootstrap", - "topic": "security-and-trust-boundaries", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-GRPC_ADVANCED_DIAGNOSTICS-L41", @@ -36956,12 +16826,10 @@ ], "sourceHeading": "모듈의 정체", "summary": "모듈의 정체", - "disposition": "PROMOTE", - "target": "concept:grpc-advanced-diagnostics-c01", + "disposition": "KEEP_IN_SSOT", "module": "grpc-advanced-diagnostics", - "topic": "security-and-trust-boundaries", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-GRPC_ADVANCED_DIAGNOSTICS-L51", @@ -36972,12 +16840,10 @@ ], "sourceHeading": "두 겹의 게이트", "summary": "두 겹의 게이트", - "disposition": "PROMOTE", - "target": "concept:grpc-advanced-diagnostics-c02", + "disposition": "KEEP_IN_SSOT", "module": "grpc-advanced-diagnostics", - "topic": "capability-and-disclosure-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-GRPC_ADVANCED_RESILIENCE-L58", @@ -36988,12 +16854,10 @@ ], "sourceHeading": "헤징 예산", "summary": "헤징 예산", - "disposition": "PROMOTE", - "target": "concept:grpc-advanced-resilience-c01", + "disposition": "KEEP_IN_SSOT", "module": "grpc-advanced-resilience", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-GRPC_ADVANCED_RESILIENCE-L75", @@ -37004,12 +16868,10 @@ ], "sourceHeading": "xDS 시작 가드", "summary": "xDS 시작 가드", - "disposition": "PROMOTE", - "target": "concept:grpc-advanced-resilience-c02", + "disposition": "KEEP_IN_SSOT", "module": "grpc-advanced-resilience", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-GRPC_ADVANCED_STREAMING-L86", @@ -37020,12 +16882,10 @@ ], "sourceHeading": "수동 흐름 제어", "summary": "수동 흐름 제어", - "disposition": "PROMOTE", - "target": "concept:grpc-advanced-streaming-c01", + "disposition": "KEEP_IN_SSOT", "module": "grpc-advanced-streaming", - "topic": "admission-budget-and-backpressure", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-GRPC_CODEGEN-L104", @@ -37036,12 +16896,10 @@ ], "sourceHeading": "소비자 컴파일 게이트", "summary": "소비자 컴파일 게이트", - "disposition": "PROMOTE", - "target": "concept:grpc-codegen-c01", + "disposition": "KEEP_IN_SSOT", "module": "grpc-codegen", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-GRPC_CORE_API-L105", @@ -37052,12 +16910,10 @@ ], "sourceHeading": "Stable 모듈 목록과 불변식", "summary": "Stable 모듈 목록과 불변식", - "disposition": "PROMOTE", - "target": "concept:grpc-core-api-c01", + "disposition": "KEEP_IN_SSOT", "module": "grpc-core-api", - "topic": "identity-and-value-contracts", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-GRPC_DISCOVERY-L85", @@ -37068,12 +16924,10 @@ ], "sourceHeading": "생성자가 거부하는 것과 검증기가 보고하는 것", "summary": "생성자가 거부하는 것과 검증기가 보고하는 것", - "disposition": "PROMOTE", - "target": "concept:grpc-discovery-c01", + "disposition": "KEEP_IN_SSOT", "module": "grpc-discovery", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-GRPC_OBSERVABILITY-L46", @@ -37084,12 +16938,10 @@ ], "sourceHeading": "모듈의 정체와 경계", "summary": "모듈의 정체와 경계", - "disposition": "PROMOTE", - "target": "concept:grpc-observability-c01", + "disposition": "KEEP_IN_SSOT", "module": "grpc-observability", - "topic": "security-and-trust-boundaries", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-GRPC_OBSERVABILITY-L59", @@ -37100,12 +16952,10 @@ ], "sourceHeading": "의존성과 런타임 배선", "summary": "의존성과 런타임 배선", - "disposition": "PROMOTE", - "target": "concept:grpc-observability-c02", + "disposition": "KEEP_IN_SSOT", "module": "grpc-observability", - "topic": "composition-and-lifecycle-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-GRPC_OBSERVABILITY-L74", @@ -37116,12 +16966,10 @@ ], "sourceHeading": "계약·불변식", "summary": "계약·불변식", - "disposition": "PROMOTE", - "target": "concept:grpc-observability-c03", + "disposition": "KEEP_IN_SSOT", "module": "grpc-observability", - "topic": "observability-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-GRPC_OPERATION_LEDGER_JPA-L53", @@ -37132,12 +16980,10 @@ ], "sourceHeading": "스키마가 계약이다", "summary": "스키마가 계약이다", - "disposition": "PROMOTE", - "target": "concept:grpc-operation-ledger-jpa-c01", + "disposition": "KEEP_IN_SSOT", "module": "grpc-operation-ledger-jpa", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-GRPC_OPERATION_LEDGER_JPA-L76", @@ -37148,12 +16994,10 @@ ], "sourceHeading": "저장 키와 유니크 제약이 같은 행을 가리킨다", "summary": "저장 키와 유니크 제약이 같은 행을 가리킨다", - "disposition": "PROMOTE", - "target": "concept:grpc-operation-ledger-jpa-c02", + "disposition": "KEEP_IN_SSOT", "module": "grpc-operation-ledger-jpa", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-GRPC_OPERATION_LEDGER_JPA-L108", @@ -37164,12 +17008,10 @@ ], "sourceHeading": "상태 전이", "summary": "상태 전이", - "disposition": "PROMOTE", - "target": "concept:grpc-operation-ledger-jpa-c03", + "disposition": "KEEP_IN_SSOT", "module": "grpc-operation-ledger-jpa", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-GRPC_POLICY-L73", @@ -37180,12 +17022,10 @@ ], "sourceHeading": "재개 토큰 — 서명하고, 구분자를 봉인한다", "summary": "재개 토큰 — 서명하고, 구분자를 봉인한다", - "disposition": "PROMOTE", - "target": "concept:grpc-policy-c01", + "disposition": "KEEP_IN_SSOT", "module": "grpc-policy", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-GRPC_SERVER-L53", @@ -37196,12 +17036,10 @@ ], "sourceHeading": "인터셉터 순서 계약", "summary": "인터셉터 순서 계약", - "disposition": "PROMOTE", - "target": "concept:grpc-server-c01", + "disposition": "KEEP_IN_SSOT", "module": "grpc-server", - "topic": "transaction-and-consistency-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-GRPC_SPRING_BOOT_STARTER-L42", @@ -37212,12 +17050,10 @@ ], "sourceHeading": "모듈의 정체와 격리 규칙", "summary": "모듈의 정체와 격리 규칙", - "disposition": "PROMOTE", - "target": "concept:grpc-spring-boot-starter-c01", + "disposition": "KEEP_IN_SSOT", "module": "grpc-spring-boot-starter", - "topic": "security-and-trust-boundaries", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-GRPC_SPRING_BOOT_STARTER-L87", @@ -37228,12 +17064,10 @@ ], "sourceHeading": "검증기가 담은 규칙", "summary": "검증기가 담은 규칙", - "disposition": "PROMOTE", - "target": "concept:grpc-spring-boot-starter-c02", + "disposition": "KEEP_IN_SSOT", "module": "grpc-spring-boot-starter", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-GRPC_TESTKIT-L102", @@ -37244,12 +17078,10 @@ ], "sourceHeading": "릴리스 게이트 — 문서가 후속이 아니라 차단 사유다", "summary": "릴리스 게이트 — 문서가 후속이 아니라 차단 사유다", - "disposition": "PROMOTE", - "target": "concept:grpc-testkit-c01", + "disposition": "KEEP_IN_SSOT", "module": "grpc-testkit", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_ADMIN_API-L57", @@ -37260,12 +17092,10 @@ ], "sourceHeading": "모듈의 정체와 경계", "summary": "모듈의 정체와 경계", - "disposition": "PROMOTE", - "target": "concept:messaging-admin-api-c01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-admin-api", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_ADMIN_API-L98", @@ -37276,12 +17106,10 @@ ], "sourceHeading": "의존성과 런타임 배선", "summary": "의존성과 런타임 배선", - "disposition": "PROMOTE", - "target": "concept:messaging-admin-api-c02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-admin-api", - "topic": "security-and-trust-boundaries", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_ADMIN_API-L185", @@ -37292,12 +17120,10 @@ ], "sourceHeading": "계약·불변식·상태 모델", "summary": "계약·불변식·상태 모델", - "disposition": "PROMOTE", - "target": "concept:messaging-admin-api-c03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-admin-api", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_ADMIN_API-L493", @@ -37308,12 +17134,10 @@ ], "sourceHeading": "주요 실행 경로", "summary": "주요 실행 경로", - "disposition": "PROMOTE", - "target": "concept:messaging-admin-api-c04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-admin-api", - "topic": "admission-budget-and-backpressure", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_ADMIN_API-L543", @@ -37324,12 +17148,10 @@ ], "sourceHeading": "실패 경로와 복구/번역", "summary": "실패 경로와 복구/번역", - "disposition": "PROMOTE", - "target": "concept:messaging-admin-api-c05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-admin-api", - "topic": "security-and-trust-boundaries", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_ADMIN_API-L588", @@ -37340,12 +17162,10 @@ ], "sourceHeading": "트랜잭션·동시성·수명주기", "summary": "트랜잭션·동시성·수명주기", - "disposition": "PROMOTE", - "target": "concept:messaging-admin-api-c06", + "disposition": "KEEP_IN_SSOT", "module": "messaging-admin-api", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_ADMIN_API-L802", @@ -37356,12 +17176,10 @@ ], "sourceHeading": "Git/설계 문서에서 확인한 변화와 실패 기록", "summary": "Git/설계 문서에서 확인한 변화와 실패 기록", - "disposition": "PROMOTE", - "target": "concept:messaging-admin-api-c07", + "disposition": "KEEP_IN_SSOT", "module": "messaging-admin-api", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_ADMIN_RUNTIME-L62", @@ -37372,12 +17190,10 @@ ], "sourceHeading": "모듈의 정체와 경계", "summary": "모듈의 정체와 경계", - "disposition": "PROMOTE", - "target": "concept:messaging-admin-runtime-c01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-admin-runtime", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_ADMIN_RUNTIME-L78", @@ -37388,12 +17204,10 @@ ], "sourceHeading": "의존성과 런타임 배선", "summary": "의존성과 런타임 배선", - "disposition": "PROMOTE", - "target": "concept:messaging-admin-runtime-c02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-admin-runtime", - "topic": "composition-and-lifecycle-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_ADMIN_RUNTIME-L163", @@ -37404,12 +17218,10 @@ ], "sourceHeading": "계약·불변식·상태 모델", "summary": "계약·불변식·상태 모델", - "disposition": "PROMOTE", - "target": "concept:messaging-admin-runtime-c03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-admin-runtime", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_ADMIN_RUNTIME-L440", @@ -37420,12 +17232,10 @@ ], "sourceHeading": "주요 실행 경로", "summary": "주요 실행 경로", - "disposition": "PROMOTE", - "target": "concept:messaging-admin-runtime-c04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-admin-runtime", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_ADMIN_RUNTIME-L472", @@ -37436,12 +17246,10 @@ ], "sourceHeading": "실패 경로와 복구/번역", "summary": "실패 경로와 복구/번역", - "disposition": "PROMOTE", - "target": "concept:messaging-admin-runtime-c05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-admin-runtime", - "topic": "observability-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_ADMIN_RUNTIME-L493", @@ -37452,12 +17260,10 @@ ], "sourceHeading": "트랜잭션·동시성·수명주기", "summary": "트랜잭션·동시성·수명주기", - "disposition": "PROMOTE", - "target": "concept:messaging-admin-runtime-c06", + "disposition": "KEEP_IN_SSOT", "module": "messaging-admin-runtime", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_ADMIN_RUNTIME-L772", @@ -37468,12 +17274,10 @@ ], "sourceHeading": "Git/설계 문서에서 확인한 변화와 실패 기록", "summary": "Git/설계 문서에서 확인한 변화와 실패 기록", - "disposition": "PROMOTE", - "target": "concept:messaging-admin-runtime-c07", + "disposition": "KEEP_IN_SSOT", "module": "messaging-admin-runtime", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_CLAIM_CHECK-L57", @@ -37484,12 +17288,10 @@ ], "sourceHeading": "모듈의 정체와 경계", "summary": "모듈의 정체와 경계", - "disposition": "PROMOTE", - "target": "concept:messaging-claim-check-c01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-claim-check", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_CLAIM_CHECK-L85", @@ -37500,12 +17302,10 @@ ], "sourceHeading": "의존성과 런타임 배선", "summary": "의존성과 런타임 배선", - "disposition": "PROMOTE", - "target": "concept:messaging-claim-check-c02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-claim-check", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_CLAIM_CHECK-L124", @@ -37516,12 +17316,10 @@ ], "sourceHeading": "계약·불변식·상태 모델", "summary": "계약·불변식·상태 모델", - "disposition": "PROMOTE", - "target": "concept:messaging-claim-check-c03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-claim-check", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_CLAIM_CHECK-L260", @@ -37532,12 +17330,10 @@ ], "sourceHeading": "주요 실행 경로", "summary": "주요 실행 경로", - "disposition": "PROMOTE", - "target": "concept:messaging-claim-check-c04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-claim-check", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_CLAIM_CHECK-L270", @@ -37548,12 +17344,10 @@ ], "sourceHeading": "실패 경로와 복구/번역", "summary": "실패 경로와 복구/번역", - "disposition": "PROMOTE", - "target": "concept:messaging-claim-check-c05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-claim-check", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_CLAIM_CHECK-L286", @@ -37564,12 +17358,10 @@ ], "sourceHeading": "트랜잭션·동시성·수명주기", "summary": "트랜잭션·동시성·수명주기", - "disposition": "PROMOTE", - "target": "concept:messaging-claim-check-c06", + "disposition": "KEEP_IN_SSOT", "module": "messaging-claim-check", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_CLAIM_CHECK-L436", @@ -37580,12 +17372,10 @@ ], "sourceHeading": "Git/설계 문서에서 확인한 변화와 실패 기록", "summary": "Git/설계 문서에서 확인한 변화와 실패 기록", - "disposition": "PROMOTE", - "target": "concept:messaging-claim-check-c07", + "disposition": "KEEP_IN_SSOT", "module": "messaging-claim-check", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_CLOUDEVENTS-L48", @@ -37596,12 +17386,10 @@ ], "sourceHeading": "모듈의 정체와 경계", "summary": "모듈의 정체와 경계", - "disposition": "PROMOTE", - "target": "concept:messaging-cloudevents-c01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-cloudevents", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_CLOUDEVENTS-L80", @@ -37612,12 +17400,10 @@ ], "sourceHeading": "의존성과 런타임 배선", "summary": "의존성과 런타임 배선", - "disposition": "PROMOTE", - "target": "concept:messaging-cloudevents-c02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-cloudevents", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_CLOUDEVENTS-L92", @@ -37628,12 +17414,10 @@ ], "sourceHeading": "패키지/컴포넌트 지도", "summary": "패키지/컴포넌트 지도", - "disposition": "PROMOTE", - "target": "concept:messaging-cloudevents-c03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-cloudevents", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_CLOUDEVENTS-L113", @@ -37644,12 +17428,10 @@ ], "sourceHeading": "계약·불변식·상태 모델", "summary": "계약·불변식·상태 모델", - "disposition": "PROMOTE", - "target": "concept:messaging-cloudevents-c04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-cloudevents", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_CLOUDEVENTS-L290", @@ -37660,12 +17442,10 @@ ], "sourceHeading": "주요 실행 경로", "summary": "주요 실행 경로", - "disposition": "PROMOTE", - "target": "concept:messaging-cloudevents-c05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-cloudevents", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_CLOUDEVENTS-L298", @@ -37676,12 +17456,10 @@ ], "sourceHeading": "실패 경로와 복구/번역", "summary": "실패 경로와 복구/번역", - "disposition": "PROMOTE", - "target": "concept:messaging-cloudevents-c06", + "disposition": "KEEP_IN_SSOT", "module": "messaging-cloudevents", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_CLOUDEVENTS-L321", @@ -37692,12 +17470,10 @@ ], "sourceHeading": "트랜잭션·동시성·수명주기", "summary": "트랜잭션·동시성·수명주기", - "disposition": "PROMOTE", - "target": "concept:messaging-cloudevents-c07", + "disposition": "KEEP_IN_SSOT", "module": "messaging-cloudevents", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_CLOUDEVENTS-L451", @@ -37708,12 +17484,10 @@ ], "sourceHeading": "Git/설계 문서에서 확인한 변화와 실패 기록", "summary": "Git/설계 문서에서 확인한 변화와 실패 기록", - "disposition": "PROMOTE", - "target": "concept:messaging-cloudevents-c08", + "disposition": "KEEP_IN_SSOT", "module": "messaging-cloudevents", - "topic": "result-and-failure-algebra", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_CORE_API-L70", @@ -37724,12 +17498,10 @@ ], "sourceHeading": "모듈의 정체와 경계", "summary": "모듈의 정체와 경계", - "disposition": "PROMOTE", - "target": "concept:messaging-core-api-c01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-core-api", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_CORE_API-L101", @@ -37740,12 +17512,10 @@ ], "sourceHeading": "의존성과 런타임 배선", "summary": "의존성과 런타임 배선", - "disposition": "PROMOTE", - "target": "concept:messaging-core-api-c02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-core-api", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_CORE_API-L123", @@ -37756,12 +17526,10 @@ ], "sourceHeading": "패키지/컴포넌트 지도", "summary": "패키지/컴포넌트 지도", - "disposition": "PROMOTE", - "target": "concept:messaging-core-api-c03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-core-api", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_CORE_API-L180", @@ -37772,12 +17540,10 @@ ], "sourceHeading": "계약·불변식·상태 모델", "summary": "계약·불변식·상태 모델", - "disposition": "PROMOTE", - "target": "concept:messaging-core-api-c04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-core-api", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_CORE_API-L406", @@ -37788,12 +17554,10 @@ ], "sourceHeading": "주요 실행 경로", "summary": "주요 실행 경로", - "disposition": "PROMOTE", - "target": "concept:messaging-core-api-c05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-core-api", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_CORE_API-L419", @@ -37804,12 +17568,10 @@ ], "sourceHeading": "실패 경로와 복구/번역", "summary": "실패 경로와 복구/번역", - "disposition": "PROMOTE", - "target": "concept:messaging-core-api-c06", + "disposition": "KEEP_IN_SSOT", "module": "messaging-core-api", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_CORE_API-L463", @@ -37820,12 +17582,10 @@ ], "sourceHeading": "트랜잭션·동시성·수명주기", "summary": "트랜잭션·동시성·수명주기", - "disposition": "PROMOTE", - "target": "concept:messaging-core-api-c07", + "disposition": "KEEP_IN_SSOT", "module": "messaging-core-api", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_CORE_API-L734", @@ -37836,12 +17596,10 @@ ], "sourceHeading": "Git/설계 문서에서 확인한 변화와 실패 기록", "summary": "Git/설계 문서에서 확인한 변화와 실패 기록", - "disposition": "PROMOTE", - "target": "concept:messaging-core-api-c08", + "disposition": "KEEP_IN_SSOT", "module": "messaging-core-api", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_INBOX_JDBC_POSTGRESQL-L57", @@ -37852,12 +17610,10 @@ ], "sourceHeading": "모듈의 정체와 경계", "summary": "모듈의 정체와 경계", - "disposition": "PROMOTE", - "target": "concept:messaging-inbox-jdbc-postgresql-c01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-inbox-jdbc-postgresql", - "topic": "transaction-and-consistency-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_INBOX_JDBC_POSTGRESQL-L98", @@ -37868,12 +17624,10 @@ ], "sourceHeading": "의존성과 런타임 배선", "summary": "의존성과 런타임 배선", - "disposition": "PROMOTE", - "target": "concept:messaging-inbox-jdbc-postgresql-c02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-inbox-jdbc-postgresql", - "topic": "transaction-and-consistency-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_INBOX_JDBC_POSTGRESQL-L146", @@ -37884,12 +17638,10 @@ ], "sourceHeading": "계약·불변식·상태 모델", "summary": "계약·불변식·상태 모델", - "disposition": "PROMOTE", - "target": "concept:messaging-inbox-jdbc-postgresql-c03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-inbox-jdbc-postgresql", - "topic": "transaction-and-consistency-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_INBOX_JDBC_POSTGRESQL-L319", @@ -37900,12 +17652,10 @@ ], "sourceHeading": "주요 실행 경로", "summary": "주요 실행 경로", - "disposition": "PROMOTE", - "target": "concept:messaging-inbox-jdbc-postgresql-c04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-inbox-jdbc-postgresql", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_INBOX_JDBC_POSTGRESQL-L329", @@ -37916,12 +17666,10 @@ ], "sourceHeading": "실패 경로와 복구/번역", "summary": "실패 경로와 복구/번역", - "disposition": "PROMOTE", - "target": "concept:messaging-inbox-jdbc-postgresql-c05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-inbox-jdbc-postgresql", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_INBOX_JDBC_POSTGRESQL-L346", @@ -37932,12 +17680,10 @@ ], "sourceHeading": "트랜잭션·동시성·수명주기", "summary": "트랜잭션·동시성·수명주기", - "disposition": "PROMOTE", - "target": "concept:messaging-inbox-jdbc-postgresql-c06", + "disposition": "KEEP_IN_SSOT", "module": "messaging-inbox-jdbc-postgresql", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_INBOX_JDBC_POSTGRESQL-L581", @@ -37948,12 +17694,10 @@ ], "sourceHeading": "Git/설계 문서에서 확인한 변화와 실패 기록", "summary": "Git/설계 문서에서 확인한 변화와 실패 기록", - "disposition": "PROMOTE", - "target": "concept:messaging-inbox-jdbc-postgresql-c07", + "disposition": "KEEP_IN_SSOT", "module": "messaging-inbox-jdbc-postgresql", - "topic": "result-and-failure-algebra", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_KAFKA-L69", @@ -37964,12 +17708,10 @@ ], "sourceHeading": "소비자 런타임 — 스레드 규율이 설계다", "summary": "소비자 런타임 — 스레드 규율이 설계다", - "disposition": "PROMOTE", - "target": "concept:messaging-kafka-c01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-kafka", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_KAFKA-L87", @@ -37980,12 +17722,10 @@ ], "sourceHeading": "커밋은 연속 워터마크로만 전진한다", "summary": "커밋은 연속 워터마크로만 전진한다", - "disposition": "PROMOTE", - "target": "concept:messaging-kafka-c02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-kafka", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_KAFKA_SHARE_EXPERIMENTAL-L54", @@ -37996,12 +17736,10 @@ ], "sourceHeading": "모듈의 정체와 경계", "summary": "모듈의 정체와 경계", - "disposition": "PROMOTE", - "target": "concept:messaging-kafka-share-experimental-c01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-kafka-share-experimental", - "topic": "composition-and-lifecycle-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_KAFKA_SHARE_EXPERIMENTAL-L84", @@ -38012,12 +17750,10 @@ ], "sourceHeading": "의존성과 런타임 배선", "summary": "의존성과 런타임 배선", - "disposition": "PROMOTE", - "target": "concept:messaging-kafka-share-experimental-c02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-kafka-share-experimental", - "topic": "composition-and-lifecycle-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_KAFKA_SHARE_EXPERIMENTAL-L131", @@ -38028,12 +17764,10 @@ ], "sourceHeading": "계약·불변식·상태 모델", "summary": "계약·불변식·상태 모델", - "disposition": "PROMOTE", - "target": "concept:messaging-kafka-share-experimental-c03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-kafka-share-experimental", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_KAFKA_SHARE_EXPERIMENTAL-L234", @@ -38044,12 +17778,10 @@ ], "sourceHeading": "주요 실행 경로", "summary": "주요 실행 경로", - "disposition": "PROMOTE", - "target": "concept:messaging-kafka-share-experimental-c04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-kafka-share-experimental", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_KAFKA_SHARE_EXPERIMENTAL-L244", @@ -38060,12 +17792,10 @@ ], "sourceHeading": "실패 경로와 복구/번역", "summary": "실패 경로와 복구/번역", - "disposition": "PROMOTE", - "target": "concept:messaging-kafka-share-experimental-c05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-kafka-share-experimental", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_KAFKA_SHARE_EXPERIMENTAL-L258", @@ -38076,12 +17806,10 @@ ], "sourceHeading": "트랜잭션·동시성·수명주기", "summary": "트랜잭션·동시성·수명주기", - "disposition": "PROMOTE", - "target": "concept:messaging-kafka-share-experimental-c06", + "disposition": "KEEP_IN_SSOT", "module": "messaging-kafka-share-experimental", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_KAFKA_SHARE_EXPERIMENTAL-L399", @@ -38092,12 +17820,10 @@ ], "sourceHeading": "Git/설계 문서에서 확인한 변화와 실패 기록", "summary": "Git/설계 문서에서 확인한 변화와 실패 기록", - "disposition": "PROMOTE", - "target": "concept:messaging-kafka-share-experimental-c07", + "disposition": "KEEP_IN_SSOT", "module": "messaging-kafka-share-experimental", - "topic": "capability-and-disclosure-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_NATS_EXPERIMENTAL-L82", @@ -38108,12 +17834,10 @@ ], "sourceHeading": "능력 선언", "summary": "능력 선언", - "disposition": "PROMOTE", - "target": "concept:messaging-nats-experimental-c01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-nats-experimental", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_NATS_EXPERIMENTAL-L94", @@ -38124,12 +17848,10 @@ ], "sourceHeading": "프로파일이 스스로 거부하는 것", "summary": "프로파일이 스스로 거부하는 것", - "disposition": "PROMOTE", - "target": "concept:messaging-nats-experimental-c02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-nats-experimental", - "topic": "composition-and-lifecycle-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_OBSERVABILITY-L52", @@ -38140,12 +17862,10 @@ ], "sourceHeading": "모듈의 정체와 경계", "summary": "모듈의 정체와 경계", - "disposition": "PROMOTE", - "target": "concept:messaging-observability-c01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-observability", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_OBSERVABILITY-L70", @@ -38156,12 +17876,10 @@ ], "sourceHeading": "의존성과 런타임 배선", "summary": "의존성과 런타임 배선", - "disposition": "PROMOTE", - "target": "concept:messaging-observability-c02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-observability", - "topic": "observability-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_OBSERVABILITY-L113", @@ -38172,12 +17890,10 @@ ], "sourceHeading": "계약·불변식·상태 모델", "summary": "계약·불변식·상태 모델", - "disposition": "PROMOTE", - "target": "concept:messaging-observability-c03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-observability", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_OBSERVABILITY-L334", @@ -38188,12 +17904,10 @@ ], "sourceHeading": "주요 실행 경로", "summary": "주요 실행 경로", - "disposition": "PROMOTE", - "target": "concept:messaging-observability-c04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-observability", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_OBSERVABILITY-L346", @@ -38204,12 +17918,10 @@ ], "sourceHeading": "실패 경로와 복구/번역", "summary": "실패 경로와 복구/번역", - "disposition": "PROMOTE", - "target": "concept:messaging-observability-c05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-observability", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_OBSERVABILITY-L363", @@ -38220,12 +17932,10 @@ ], "sourceHeading": "트랜잭션·동시성·수명주기", "summary": "트랜잭션·동시성·수명주기", - "disposition": "PROMOTE", - "target": "concept:messaging-observability-c06", + "disposition": "KEEP_IN_SSOT", "module": "messaging-observability", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_OBSERVABILITY-L597", @@ -38236,12 +17946,10 @@ ], "sourceHeading": "Git/설계 문서에서 확인한 변화와 실패 기록", "summary": "Git/설계 문서에서 확인한 변화와 실패 기록", - "disposition": "PROMOTE", - "target": "concept:messaging-observability-c07", + "disposition": "KEEP_IN_SSOT", "module": "messaging-observability", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_OUTBOX_JDBC_POSTGRESQL-L66", @@ -38252,12 +17960,10 @@ ], "sourceHeading": "모듈의 정체와 경계", "summary": "모듈의 정체와 경계", - "disposition": "PROMOTE", - "target": "concept:messaging-outbox-jdbc-postgresql-c01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-outbox-jdbc-postgresql", - "topic": "transaction-and-consistency-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_OUTBOX_JDBC_POSTGRESQL-L102", @@ -38268,12 +17974,10 @@ ], "sourceHeading": "의존성과 런타임 배선", "summary": "의존성과 런타임 배선", - "disposition": "PROMOTE", - "target": "concept:messaging-outbox-jdbc-postgresql-c02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-outbox-jdbc-postgresql", - "topic": "transaction-and-consistency-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_OUTBOX_JDBC_POSTGRESQL-L177", @@ -38284,12 +17988,10 @@ ], "sourceHeading": "계약·불변식·상태 모델", "summary": "계약·불변식·상태 모델", - "disposition": "PROMOTE", - "target": "concept:messaging-outbox-jdbc-postgresql-c03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-outbox-jdbc-postgresql", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_OUTBOX_JDBC_POSTGRESQL-L430", @@ -38300,12 +18002,10 @@ ], "sourceHeading": "주요 실행 경로", "summary": "주요 실행 경로", - "disposition": "PROMOTE", - "target": "concept:messaging-outbox-jdbc-postgresql-c04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-outbox-jdbc-postgresql", - "topic": "transaction-and-consistency-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_OUTBOX_JDBC_POSTGRESQL-L486", @@ -38316,12 +18016,10 @@ ], "sourceHeading": "트랜잭션·동시성·수명주기", "summary": "트랜잭션·동시성·수명주기", - "disposition": "PROMOTE", - "target": "concept:messaging-outbox-jdbc-postgresql-c05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-outbox-jdbc-postgresql", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_OUTBOX_JDBC_POSTGRESQL-L767", @@ -38332,12 +18030,10 @@ ], "sourceHeading": "Git/설계 문서에서 확인한 변화와 실패 기록", "summary": "Git/설계 문서에서 확인한 변화와 실패 기록", - "disposition": "PROMOTE", - "target": "concept:messaging-outbox-jdbc-postgresql-c06", + "disposition": "KEEP_IN_SSOT", "module": "messaging-outbox-jdbc-postgresql", - "topic": "transaction-and-consistency-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_POLICY-L56", @@ -38348,12 +18044,10 @@ ], "sourceHeading": "모듈의 정체와 경계", "summary": "모듈의 정체와 경계", - "disposition": "PROMOTE", - "target": "concept:messaging-policy-c01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-policy", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_POLICY-L84", @@ -38364,12 +18058,10 @@ ], "sourceHeading": "의존성과 런타임 배선", "summary": "의존성과 런타임 배선", - "disposition": "PROMOTE", - "target": "concept:messaging-policy-c02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-policy", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_POLICY-L135", @@ -38380,12 +18072,10 @@ ], "sourceHeading": "계약·불변식·상태 모델", "summary": "계약·불변식·상태 모델", - "disposition": "PROMOTE", - "target": "concept:messaging-policy-c03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-policy", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_POLICY-L411", @@ -38396,12 +18086,10 @@ ], "sourceHeading": "주요 실행 경로", "summary": "주요 실행 경로", - "disposition": "PROMOTE", - "target": "concept:messaging-policy-c04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-policy", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_POLICY-L423", @@ -38412,12 +18100,10 @@ ], "sourceHeading": "실패 경로와 복구/번역", "summary": "실패 경로와 복구/번역", - "disposition": "PROMOTE", - "target": "concept:messaging-policy-c05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-policy", - "topic": "admission-budget-and-backpressure", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_POLICY-L451", @@ -38428,12 +18114,10 @@ ], "sourceHeading": "트랜잭션·동시성·수명주기", "summary": "트랜잭션·동시성·수명주기", - "disposition": "PROMOTE", - "target": "concept:messaging-policy-c06", + "disposition": "KEEP_IN_SSOT", "module": "messaging-policy", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_POLICY-L603", @@ -38444,12 +18128,10 @@ ], "sourceHeading": "E. control: the publish path IS constructed in production", "summary": "E. control: the publish path IS constructed in production", - "disposition": "PROMOTE", - "target": "concept:messaging-policy-c07", + "disposition": "KEEP_IN_SSOT", "module": "messaging-policy", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_POLICY-L617", @@ -38460,12 +18142,10 @@ ], "sourceHeading": "G. every file that acts on a RetryDecision variant", "summary": "G. every file that acts on a RetryDecision variant", - "disposition": "PROMOTE", - "target": "concept:messaging-policy-c08", + "disposition": "KEEP_IN_SSOT", "module": "messaging-policy", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_POLICY-L700", @@ -38476,12 +18156,10 @@ ], "sourceHeading": "Git/설계 문서에서 확인한 변화와 실패 기록", "summary": "Git/설계 문서에서 확인한 변화와 실패 기록", - "disposition": "PROMOTE", - "target": "concept:messaging-policy-c09", + "disposition": "KEEP_IN_SSOT", "module": "messaging-policy", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_PULSAR_EXPERIMENTAL-L50", @@ -38492,12 +18170,10 @@ ], "sourceHeading": "실패 분류 — 타입 있는 신호만 본다", "summary": "실패 분류 — 타입 있는 신호만 본다", - "disposition": "PROMOTE", - "target": "concept:messaging-pulsar-experimental-c01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-pulsar-experimental", - "topic": "result-and-failure-algebra", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_RABBIT-L91", @@ -38508,12 +18184,10 @@ ], "sourceHeading": "소비·정착·죽은 편지의 세 규율", "summary": "소비·정착·죽은 편지의 세 규율", - "disposition": "PROMOTE", - "target": "concept:messaging-rabbit-c01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-rabbit", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_RABBIT-L106", @@ -38524,12 +18198,10 @@ ], "sourceHeading": "자격증명은 연결 시도마다 해석된다", "summary": "자격증명은 연결 시도마다 해석된다", - "disposition": "PROMOTE", - "target": "concept:messaging-rabbit-c02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-rabbit", - "topic": "security-and-trust-boundaries", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_RELIABILITY_API-L51", @@ -38540,12 +18212,10 @@ ], "sourceHeading": "모듈의 정체와 경계", "summary": "모듈의 정체와 경계", - "disposition": "PROMOTE", - "target": "concept:messaging-reliability-api-c01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-reliability-api", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_RELIABILITY_API-L92", @@ -38556,12 +18226,10 @@ ], "sourceHeading": "의존성과 런타임 배선", "summary": "의존성과 런타임 배선", - "disposition": "PROMOTE", - "target": "concept:messaging-reliability-api-c02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-reliability-api", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_RELIABILITY_API-L143", @@ -38572,12 +18240,10 @@ ], "sourceHeading": "계약·불변식·상태 모델", "summary": "계약·불변식·상태 모델", - "disposition": "PROMOTE", - "target": "concept:messaging-reliability-api-c03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-reliability-api", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_RELIABILITY_API-L338", @@ -38588,12 +18254,10 @@ ], "sourceHeading": "주요 실행 경로", "summary": "주요 실행 경로", - "disposition": "PROMOTE", - "target": "concept:messaging-reliability-api-c04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-reliability-api", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_RELIABILITY_API-L348", @@ -38604,12 +18268,10 @@ ], "sourceHeading": "실패 경로와 복구/번역", "summary": "실패 경로와 복구/번역", - "disposition": "PROMOTE", - "target": "concept:messaging-reliability-api-c05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-reliability-api", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_RELIABILITY_API-L366", @@ -38620,12 +18282,10 @@ ], "sourceHeading": "트랜잭션·동시성·수명주기", "summary": "트랜잭션·동시성·수명주기", - "disposition": "PROMOTE", - "target": "concept:messaging-reliability-api-c06", + "disposition": "KEEP_IN_SSOT", "module": "messaging-reliability-api", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_RELIABILITY_API-L610", @@ -38636,12 +18296,10 @@ ], "sourceHeading": "Git/설계 문서에서 확인한 변화와 실패 기록", "summary": "Git/설계 문서에서 확인한 변화와 실패 기록", - "disposition": "PROMOTE", - "target": "concept:messaging-reliability-api-c07", + "disposition": "KEEP_IN_SSOT", "module": "messaging-reliability-api", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_RUNTIME_CORE-L55", @@ -38652,12 +18310,10 @@ ], "sourceHeading": "모듈의 정체와 경계", "summary": "모듈의 정체와 경계", - "disposition": "PROMOTE", - "target": "concept:messaging-runtime-core-c01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-runtime-core", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_RUNTIME_CORE-L86", @@ -38668,12 +18324,10 @@ ], "sourceHeading": "의존성과 런타임 배선", "summary": "의존성과 런타임 배선", - "disposition": "PROMOTE", - "target": "concept:messaging-runtime-core-c02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-runtime-core", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_RUNTIME_CORE-L128", @@ -38684,12 +18338,10 @@ ], "sourceHeading": "계약·불변식·상태 모델", "summary": "계약·불변식·상태 모델", - "disposition": "PROMOTE", - "target": "concept:messaging-runtime-core-c03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-runtime-core", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_RUNTIME_CORE-L387", @@ -38700,12 +18352,10 @@ ], "sourceHeading": "주요 실행 경로", "summary": "주요 실행 경로", - "disposition": "PROMOTE", - "target": "concept:messaging-runtime-core-c04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-runtime-core", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_RUNTIME_CORE-L397", @@ -38716,12 +18366,10 @@ ], "sourceHeading": "실패 경로와 복구/번역", "summary": "실패 경로와 복구/번역", - "disposition": "PROMOTE", - "target": "concept:messaging-runtime-core-c05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-runtime-core", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_RUNTIME_CORE-L429", @@ -38732,12 +18380,10 @@ ], "sourceHeading": "트랜잭션·동시성·수명주기", "summary": "트랜잭션·동시성·수명주기", - "disposition": "PROMOTE", - "target": "concept:messaging-runtime-core-c06", + "disposition": "KEEP_IN_SSOT", "module": "messaging-runtime-core", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_RUNTIME_CORE-L631", @@ -38748,12 +18394,10 @@ ], "sourceHeading": "Git/설계 문서에서 확인한 변화와 실패 기록", "summary": "Git/설계 문서에서 확인한 변화와 실패 기록", - "disposition": "PROMOTE", - "target": "concept:messaging-runtime-core-c07", + "disposition": "KEEP_IN_SSOT", "module": "messaging-runtime-core", - "topic": "composition-and-lifecycle-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SCHEMA_API-L62", @@ -38764,12 +18408,10 @@ ], "sourceHeading": "모듈의 정체와 경계", "summary": "모듈의 정체와 경계", - "disposition": "PROMOTE", - "target": "concept:messaging-schema-api-c01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-api", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SCHEMA_API-L79", @@ -38780,12 +18422,10 @@ ], "sourceHeading": "의존성과 런타임 배선", "summary": "의존성과 런타임 배선", - "disposition": "PROMOTE", - "target": "concept:messaging-schema-api-c02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-api", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SCHEMA_API-L113", @@ -38796,12 +18436,10 @@ ], "sourceHeading": "계약·불변식·상태 모델", "summary": "계약·불변식·상태 모델", - "disposition": "PROMOTE", - "target": "concept:messaging-schema-api-c03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-api", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SCHEMA_API-L255", @@ -38812,12 +18450,10 @@ ], "sourceHeading": "주요 실행 경로", "summary": "주요 실행 경로", - "disposition": "PROMOTE", - "target": "concept:messaging-schema-api-c04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-api", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SCHEMA_API-L267", @@ -38828,12 +18464,10 @@ ], "sourceHeading": "실패 경로와 복구/번역", "summary": "실패 경로와 복구/번역", - "disposition": "PROMOTE", - "target": "concept:messaging-schema-api-c05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-api", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SCHEMA_API-L282", @@ -38844,12 +18478,10 @@ ], "sourceHeading": "트랜잭션·동시성·수명주기", "summary": "트랜잭션·동시성·수명주기", - "disposition": "PROMOTE", - "target": "concept:messaging-schema-api-c06", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-api", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SCHEMA_API-L432", @@ -38860,12 +18492,10 @@ ], "sourceHeading": "Git/설계 문서에서 확인한 변화와 실패 기록", "summary": "Git/설계 문서에서 확인한 변화와 실패 기록", - "disposition": "PROMOTE", - "target": "concept:messaging-schema-api-c07", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-api", - "topic": "result-and-failure-algebra", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SCHEMA_AVRO-L49", @@ -38876,12 +18506,10 @@ ], "sourceHeading": "모듈의 정체와 경계", "summary": "모듈의 정체와 경계", - "disposition": "PROMOTE", - "target": "concept:messaging-schema-avro-c01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-avro", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SCHEMA_AVRO-L75", @@ -38892,12 +18520,10 @@ ], "sourceHeading": "의존성과 런타임 배선", "summary": "의존성과 런타임 배선", - "disposition": "PROMOTE", - "target": "concept:messaging-schema-avro-c02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-avro", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SCHEMA_AVRO-L106", @@ -38908,12 +18534,10 @@ ], "sourceHeading": "계약·불변식·상태 모델", "summary": "계약·불변식·상태 모델", - "disposition": "PROMOTE", - "target": "concept:messaging-schema-avro-c03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-avro", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SCHEMA_AVRO-L254", @@ -38924,12 +18548,10 @@ ], "sourceHeading": "주요 실행 경로", "summary": "주요 실행 경로", - "disposition": "PROMOTE", - "target": "concept:messaging-schema-avro-c04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-avro", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SCHEMA_AVRO-L266", @@ -38940,12 +18562,10 @@ ], "sourceHeading": "실패 경로와 복구/번역", "summary": "실패 경로와 복구/번역", - "disposition": "PROMOTE", - "target": "concept:messaging-schema-avro-c05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-avro", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SCHEMA_AVRO-L298", @@ -38956,12 +18576,10 @@ ], "sourceHeading": "트랜잭션·동시성·수명주기", "summary": "트랜잭션·동시성·수명주기", - "disposition": "PROMOTE", - "target": "concept:messaging-schema-avro-c06", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-avro", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SCHEMA_AVRO-L457", @@ -38972,12 +18590,10 @@ ], "sourceHeading": "Git/설계 문서에서 확인한 변화와 실패 기록", "summary": "Git/설계 문서에서 확인한 변화와 실패 기록", - "disposition": "PROMOTE", - "target": "concept:messaging-schema-avro-c07", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-avro", - "topic": "admission-budget-and-backpressure", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SCHEMA_JSON-L46", @@ -38988,12 +18604,10 @@ ], "sourceHeading": "모듈의 정체와 경계", "summary": "모듈의 정체와 경계", - "disposition": "PROMOTE", - "target": "concept:messaging-schema-json-c01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-json", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SCHEMA_JSON-L71", @@ -39004,12 +18618,10 @@ ], "sourceHeading": "의존성과 런타임 배선", "summary": "의존성과 런타임 배선", - "disposition": "PROMOTE", - "target": "concept:messaging-schema-json-c02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-json", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SCHEMA_JSON-L124", @@ -39020,12 +18632,10 @@ ], "sourceHeading": "계약·불변식·상태 모델", "summary": "계약·불변식·상태 모델", - "disposition": "PROMOTE", - "target": "concept:messaging-schema-json-c03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-json", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SCHEMA_JSON-L238", @@ -39036,12 +18646,10 @@ ], "sourceHeading": "주요 실행 경로", "summary": "주요 실행 경로", - "disposition": "PROMOTE", - "target": "concept:messaging-schema-json-c04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-json", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SCHEMA_JSON-L246", @@ -39052,12 +18660,10 @@ ], "sourceHeading": "실패 경로와 복구/번역", "summary": "실패 경로와 복구/번역", - "disposition": "PROMOTE", - "target": "concept:messaging-schema-json-c05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-json", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SCHEMA_JSON-L263", @@ -39068,12 +18674,10 @@ ], "sourceHeading": "트랜잭션·동시성·수명주기", "summary": "트랜잭션·동시성·수명주기", - "disposition": "PROMOTE", - "target": "concept:messaging-schema-json-c06", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-json", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SCHEMA_JSON-L396", @@ -39084,12 +18688,10 @@ ], "sourceHeading": "Git/설계 문서에서 확인한 변화와 실패 기록", "summary": "Git/설계 문서에서 확인한 변화와 실패 기록", - "disposition": "PROMOTE", - "target": "concept:messaging-schema-json-c07", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-json", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SCHEMA_PROTOBUF-L49", @@ -39100,12 +18702,10 @@ ], "sourceHeading": "모듈의 정체와 경계", "summary": "모듈의 정체와 경계", - "disposition": "PROMOTE", - "target": "concept:messaging-schema-protobuf-c01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-protobuf", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SCHEMA_PROTOBUF-L76", @@ -39116,12 +18716,10 @@ ], "sourceHeading": "의존성과 런타임 배선", "summary": "의존성과 런타임 배선", - "disposition": "PROMOTE", - "target": "concept:messaging-schema-protobuf-c02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-protobuf", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SCHEMA_PROTOBUF-L112", @@ -39132,12 +18730,10 @@ ], "sourceHeading": "계약·불변식·상태 모델", "summary": "계약·불변식·상태 모델", - "disposition": "PROMOTE", - "target": "concept:messaging-schema-protobuf-c03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-protobuf", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SCHEMA_PROTOBUF-L229", @@ -39148,12 +18744,10 @@ ], "sourceHeading": "주요 실행 경로", "summary": "주요 실행 경로", - "disposition": "PROMOTE", - "target": "concept:messaging-schema-protobuf-c04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-protobuf", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SCHEMA_PROTOBUF-L239", @@ -39164,12 +18758,10 @@ ], "sourceHeading": "실패 경로와 복구/번역", "summary": "실패 경로와 복구/번역", - "disposition": "PROMOTE", - "target": "concept:messaging-schema-protobuf-c05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-protobuf", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SCHEMA_PROTOBUF-L258", @@ -39180,12 +18772,10 @@ ], "sourceHeading": "트랜잭션·동시성·수명주기", "summary": "트랜잭션·동시성·수명주기", - "disposition": "PROMOTE", - "target": "concept:messaging-schema-protobuf-c06", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-protobuf", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SCHEMA_PROTOBUF-L458", @@ -39196,12 +18786,10 @@ ], "sourceHeading": "Git/설계 문서에서 확인한 변화와 실패 기록", "summary": "Git/설계 문서에서 확인한 변화와 실패 기록", - "disposition": "PROMOTE", - "target": "concept:messaging-schema-protobuf-c07", + "disposition": "KEEP_IN_SSOT", "module": "messaging-schema-protobuf", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SECURITY-L52", @@ -39212,12 +18800,10 @@ ], "sourceHeading": "모듈의 정체와 경계", "summary": "모듈의 정체와 경계", - "disposition": "PROMOTE", - "target": "concept:messaging-security-c01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-security", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SECURITY-L91", @@ -39228,12 +18814,10 @@ ], "sourceHeading": "의존성과 런타임 배선", "summary": "의존성과 런타임 배선", - "disposition": "PROMOTE", - "target": "concept:messaging-security-c02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-security", - "topic": "composition-and-lifecycle-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SECURITY-L141", @@ -39244,12 +18828,10 @@ ], "sourceHeading": "계약·불변식·상태 모델", "summary": "계약·불변식·상태 모델", - "disposition": "PROMOTE", - "target": "concept:messaging-security-c03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-security", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SECURITY-L327", @@ -39260,12 +18842,10 @@ ], "sourceHeading": "주요 실행 경로", "summary": "주요 실행 경로", - "disposition": "PROMOTE", - "target": "concept:messaging-security-c04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-security", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SECURITY-L339", @@ -39276,12 +18856,10 @@ ], "sourceHeading": "실패 경로와 복구/번역", "summary": "실패 경로와 복구/번역", - "disposition": "PROMOTE", - "target": "concept:messaging-security-c05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-security", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SECURITY-L359", @@ -39292,12 +18870,10 @@ ], "sourceHeading": "트랜잭션·동시성·수명주기", "summary": "트랜잭션·동시성·수명주기", - "disposition": "PROMOTE", - "target": "concept:messaging-security-c06", + "disposition": "KEEP_IN_SSOT", "module": "messaging-security", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SECURITY-L562", @@ -39308,12 +18884,10 @@ ], "sourceHeading": "Git/설계 문서에서 확인한 변화와 실패 기록", "summary": "Git/설계 문서에서 확인한 변화와 실패 기록", - "disposition": "PROMOTE", - "target": "concept:messaging-security-c07", + "disposition": "KEEP_IN_SSOT", "module": "messaging-security", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SPRING_BOOT_STARTER-L94", @@ -39324,12 +18898,10 @@ ], "sourceHeading": "선택은 닫힌 레지스트리이고, 등록과 조립은 다르다", "summary": "선택은 닫힌 레지스트리이고, 등록과 조립은 다르다", - "disposition": "PROMOTE", - "target": "concept:messaging-spring-boot-starter-c01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-spring-boot-starter", - "topic": "composition-and-lifecycle-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SPRING_BOOT_STARTER-L111", @@ -39340,12 +18912,10 @@ ], "sourceHeading": "설정이 프로파일이 된다", "summary": "설정이 프로파일이 된다", - "disposition": "PROMOTE", - "target": "concept:messaging-spring-boot-starter-c02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-spring-boot-starter", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SPRING_BOOT_STARTER-L155", @@ -39356,12 +18926,10 @@ ], "sourceHeading": "종료 순서가 두 수명 주기의 phase 로 표현된다", "summary": "종료 순서가 두 수명 주기의 phase 로 표현된다", - "disposition": "PROMOTE", - "target": "concept:messaging-spring-boot-starter-c03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-spring-boot-starter", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SPRING_CLOUD_STREAM_BRIDGE-L56", @@ -39372,12 +18940,10 @@ ], "sourceHeading": "모듈의 정체와 경계", "summary": "모듈의 정체와 경계", - "disposition": "PROMOTE", - "target": "concept:messaging-spring-cloud-stream-bridge-c01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-spring-cloud-stream-bridge", - "topic": "composition-and-lifecycle-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SPRING_CLOUD_STREAM_BRIDGE-L86", @@ -39388,12 +18954,10 @@ ], "sourceHeading": "의존성과 런타임 배선", "summary": "의존성과 런타임 배선", - "disposition": "PROMOTE", - "target": "concept:messaging-spring-cloud-stream-bridge-c02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-spring-cloud-stream-bridge", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SPRING_CLOUD_STREAM_BRIDGE-L146", @@ -39404,12 +18968,10 @@ ], "sourceHeading": "계약·불변식·상태 모델", "summary": "계약·불변식·상태 모델", - "disposition": "PROMOTE", - "target": "concept:messaging-spring-cloud-stream-bridge-c03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-spring-cloud-stream-bridge", - "topic": "admission-budget-and-backpressure", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SPRING_CLOUD_STREAM_BRIDGE-L309", @@ -39420,12 +18982,10 @@ ], "sourceHeading": "주요 실행 경로", "summary": "주요 실행 경로", - "disposition": "PROMOTE", - "target": "concept:messaging-spring-cloud-stream-bridge-c04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-spring-cloud-stream-bridge", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SPRING_CLOUD_STREAM_BRIDGE-L319", @@ -39436,12 +18996,10 @@ ], "sourceHeading": "실패 경로와 복구/번역", "summary": "실패 경로와 복구/번역", - "disposition": "PROMOTE", - "target": "concept:messaging-spring-cloud-stream-bridge-c05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-spring-cloud-stream-bridge", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_SPRING_CLOUD_STREAM_BRIDGE-L341", @@ -39452,12 +19010,10 @@ ], "sourceHeading": "트랜잭션·동시성·수명주기", "summary": "트랜잭션·동시성·수명주기", - "disposition": "PROMOTE", - "target": "concept:messaging-spring-cloud-stream-bridge-c06", + "disposition": "KEEP_IN_SSOT", "module": "messaging-spring-cloud-stream-bridge", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_TESTKIT-L68", @@ -39468,12 +19024,10 @@ ], "sourceHeading": "모듈의 정체와 경계", "summary": "모듈의 정체와 경계", - "disposition": "PROMOTE", - "target": "concept:messaging-testkit-c01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-testkit", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_TESTKIT-L99", @@ -39484,12 +19038,10 @@ ], "sourceHeading": "의존성과 런타임 배선", "summary": "의존성과 런타임 배선", - "disposition": "PROMOTE", - "target": "concept:messaging-testkit-c02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-testkit", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_TESTKIT-L140", @@ -39500,12 +19052,10 @@ ], "sourceHeading": "패키지/컴포넌트 지도", "summary": "패키지/컴포넌트 지도", - "disposition": "PROMOTE", - "target": "concept:messaging-testkit-c03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-testkit", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_TESTKIT-L169", @@ -39516,12 +19066,10 @@ ], "sourceHeading": "계약·불변식·상태 모델", "summary": "계약·불변식·상태 모델", - "disposition": "PROMOTE", - "target": "concept:messaging-testkit-c04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-testkit", - "topic": "identity-and-value-contracts", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_TESTKIT-L452", @@ -39532,12 +19080,10 @@ ], "sourceHeading": "주요 실행 경로", "summary": "주요 실행 경로", - "disposition": "PROMOTE", - "target": "concept:messaging-testkit-c05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-testkit", - "topic": "capability-and-disclosure-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_TESTKIT-L492", @@ -39548,12 +19094,10 @@ ], "sourceHeading": "실패 경로와 복구/번역", "summary": "실패 경로와 복구/번역", - "disposition": "PROMOTE", - "target": "concept:messaging-testkit-c06", + "disposition": "KEEP_IN_SSOT", "module": "messaging-testkit", - "topic": "result-and-failure-algebra", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_TESTKIT-L539", @@ -39564,12 +19108,10 @@ ], "sourceHeading": "트랜잭션·동시성·수명주기", "summary": "트랜잭션·동시성·수명주기", - "disposition": "PROMOTE", - "target": "concept:messaging-testkit-c07", + "disposition": "KEEP_IN_SSOT", "module": "messaging-testkit", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_TESTKIT-L848", @@ -39580,12 +19122,10 @@ ], "sourceHeading": "Git/설계 문서에서 확인한 변화와 실패 기록", "summary": "Git/설계 문서에서 확인한 변화와 실패 기록", - "disposition": "PROMOTE", - "target": "concept:messaging-testkit-c08", + "disposition": "KEEP_IN_SSOT", "module": "messaging-testkit", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_TRANSPORT_SPI-L62", @@ -39596,12 +19136,10 @@ ], "sourceHeading": "모듈의 정체와 경계", "summary": "모듈의 정체와 경계", - "disposition": "PROMOTE", - "target": "concept:messaging-transport-spi-c01", + "disposition": "KEEP_IN_SSOT", "module": "messaging-transport-spi", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_TRANSPORT_SPI-L90", @@ -39612,12 +19150,10 @@ ], "sourceHeading": "의존성과 런타임 배선", "summary": "의존성과 런타임 배선", - "disposition": "PROMOTE", - "target": "concept:messaging-transport-spi-c02", + "disposition": "KEEP_IN_SSOT", "module": "messaging-transport-spi", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_TRANSPORT_SPI-L124", @@ -39628,12 +19164,10 @@ ], "sourceHeading": "계약·불변식·상태 모델", "summary": "계약·불변식·상태 모델", - "disposition": "PROMOTE", - "target": "concept:messaging-transport-spi-c03", + "disposition": "KEEP_IN_SSOT", "module": "messaging-transport-spi", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_TRANSPORT_SPI-L326", @@ -39644,12 +19178,10 @@ ], "sourceHeading": "주요 실행 경로", "summary": "주요 실행 경로", - "disposition": "PROMOTE", - "target": "concept:messaging-transport-spi-c04", + "disposition": "KEEP_IN_SSOT", "module": "messaging-transport-spi", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_TRANSPORT_SPI-L338", @@ -39660,12 +19192,10 @@ ], "sourceHeading": "실패 경로와 복구/번역", "summary": "실패 경로와 복구/번역", - "disposition": "PROMOTE", - "target": "concept:messaging-transport-spi-c05", + "disposition": "KEEP_IN_SSOT", "module": "messaging-transport-spi", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_TRANSPORT_SPI-L352", @@ -39676,12 +19206,10 @@ ], "sourceHeading": "트랜잭션·동시성·수명주기", "summary": "트랜잭션·동시성·수명주기", - "disposition": "PROMOTE", - "target": "concept:messaging-transport-spi-c06", + "disposition": "KEEP_IN_SSOT", "module": "messaging-transport-spi", - "topic": "state-machines-and-ownership", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-MESSAGING_TRANSPORT_SPI-L579", @@ -39692,12 +19220,10 @@ ], "sourceHeading": "Git/설계 문서에서 확인한 변화와 실패 기록", "summary": "Git/설계 문서에서 확인한 변화와 실패 기록", - "disposition": "PROMOTE", - "target": "concept:messaging-transport-spi-c07", + "disposition": "KEEP_IN_SSOT", "module": "messaging-transport-spi", - "topic": "result-and-failure-algebra", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-SHARED_CONTRACT-L51", @@ -39708,12 +19234,10 @@ ], "sourceHeading": "주요 계약과 불변식", "summary": "주요 계약과 불변식", - "disposition": "PROMOTE", - "target": "concept:shared-contract-c01", + "disposition": "KEEP_IN_SSOT", "module": "shared-contract", - "topic": "delivery-and-settlement-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-SHARED_CONTRACT-L69", @@ -39724,12 +19248,10 @@ ], "sourceHeading": "Permission", "summary": "Permission", - "disposition": "PROMOTE", - "target": "concept:shared-contract-c02", + "disposition": "KEEP_IN_SSOT", "module": "shared-contract", - "topic": "admission-budget-and-backpressure", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-SHARED_CONTRACT-L108", @@ -39740,12 +19262,10 @@ ], "sourceHeading": "Activation and health snapshot", "summary": "Activation and health snapshot", - "disposition": "PROMOTE", - "target": "concept:shared-contract-c03", + "disposition": "KEEP_IN_SSOT", "module": "shared-contract", - "topic": "result-and-failure-algebra", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" }, { "id": "C-SHARED_CONTRACT-L114", @@ -39756,12 +19276,10 @@ ], "sourceHeading": "Messaging envelope schema", "summary": "Messaging envelope schema", - "disposition": "PROMOTE", - "target": "concept:shared-contract-c04", + "disposition": "KEEP_IN_SSOT", "module": "shared-contract", - "topic": "schema-and-wire-models", - "reason": "CONCEPT recall pass — SSOT 본문이 설명하는 구현된 메커니즘. §17 finding 이 아니라 정상 동작 모델이다.", - "dispositionReview": "PENDING" + "reason": "제2부 모듈 분석의 절에서 나온 후보다. 제1부가 이 주장을 글감으로 채택하지 않았고, 채택한 것들은 이미 같은 사건을 담은 글감이 있다. 분석에는 그대로 남는다", + "dispositionReview": "CONFIRMED" } ], "history": { @@ -58520,109 +38038,5 @@ } } }, - "unlisted": [ - "transport-and-provider-semantics/case/case-messaging-nats-experimental-f02.md", - "composition-and-lifecycle-models/concept/concept-adapter-outbound-support-c01.md", - "composition-and-lifecycle-models/concept/concept-grpc-observability-c02.md", - "delivery-and-settlement-models/concept/concept-messaging-admin-api-c01.md", - "schema-and-wire-models/concept/concept-messaging-admin-api-c03.md", - "state-machines-and-ownership/concept/concept-messaging-admin-api-c06.md", - "state-machines-and-ownership/concept/concept-messaging-admin-api-c07.md", - "delivery-and-settlement-models/concept/concept-messaging-admin-runtime-c01.md", - "composition-and-lifecycle-models/concept/concept-messaging-admin-runtime-c02.md", - "state-machines-and-ownership/concept/concept-messaging-admin-runtime-c03.md", - "delivery-and-settlement-models/concept/concept-messaging-admin-runtime-c04.md", - "state-machines-and-ownership/concept/concept-messaging-admin-runtime-c06.md", - "state-machines-and-ownership/concept/concept-messaging-admin-runtime-c07.md", - "state-machines-and-ownership/concept/concept-messaging-claim-check-c01.md", - "state-machines-and-ownership/concept/concept-messaging-claim-check-c02.md", - "state-machines-and-ownership/concept/concept-messaging-claim-check-c03.md", - "state-machines-and-ownership/concept/concept-messaging-claim-check-c04.md", - "state-machines-and-ownership/concept/concept-messaging-claim-check-c05.md", - "state-machines-and-ownership/concept/concept-messaging-claim-check-c06.md", - "state-machines-and-ownership/concept/concept-messaging-claim-check-c07.md", - "schema-and-wire-models/concept/concept-messaging-cloudevents-c01.md", - "schema-and-wire-models/concept/concept-messaging-cloudevents-c02.md", - "schema-and-wire-models/concept/concept-messaging-cloudevents-c03.md", - "delivery-and-settlement-models/concept/concept-messaging-cloudevents-c04.md", - "schema-and-wire-models/concept/concept-messaging-cloudevents-c05.md", - "schema-and-wire-models/concept/concept-messaging-cloudevents-c06.md", - "state-machines-and-ownership/concept/concept-messaging-cloudevents-c07.md", - "result-and-failure-algebra/concept/concept-messaging-cloudevents-c08.md", - "state-machines-and-ownership/concept/concept-messaging-core-api-c01.md", - "schema-and-wire-models/concept/concept-messaging-core-api-c02.md", - "delivery-and-settlement-models/concept/concept-messaging-core-api-c04.md", - "schema-and-wire-models/concept/concept-messaging-core-api-c05.md", - "delivery-and-settlement-models/concept/concept-messaging-core-api-c06.md", - "state-machines-and-ownership/concept/concept-messaging-core-api-c07.md", - "state-machines-and-ownership/concept/concept-messaging-core-api-c08.md", - "transaction-and-consistency-models/concept/concept-messaging-inbox-jdbc-postgresql-c01.md", - "transaction-and-consistency-models/concept/concept-messaging-inbox-jdbc-postgresql-c02.md", - "state-machines-and-ownership/concept/concept-messaging-inbox-jdbc-postgresql-c04.md", - "state-machines-and-ownership/concept/concept-messaging-inbox-jdbc-postgresql-c05.md", - "state-machines-and-ownership/concept/concept-messaging-inbox-jdbc-postgresql-c06.md", - "result-and-failure-algebra/concept/concept-messaging-inbox-jdbc-postgresql-c07.md", - "composition-and-lifecycle-models/concept/concept-messaging-kafka-share-experimental-c01.md", - "composition-and-lifecycle-models/concept/concept-messaging-kafka-share-experimental-c02.md", - "state-machines-and-ownership/concept/concept-messaging-kafka-share-experimental-c03.md", - "delivery-and-settlement-models/concept/concept-messaging-kafka-share-experimental-c04.md", - "delivery-and-settlement-models/concept/concept-messaging-kafka-share-experimental-c05.md", - "state-machines-and-ownership/concept/concept-messaging-kafka-share-experimental-c06.md", - "state-machines-and-ownership/concept/concept-messaging-observability-c01.md", - "delivery-and-settlement-models/concept/concept-messaging-observability-c03.md", - "delivery-and-settlement-models/concept/concept-messaging-observability-c04.md", - "delivery-and-settlement-models/concept/concept-messaging-observability-c05.md", - "state-machines-and-ownership/concept/concept-messaging-observability-c06.md", - "state-machines-and-ownership/concept/concept-messaging-observability-c07.md", - "delivery-and-settlement-models/concept/concept-messaging-outbox-jdbc-postgresql-c03.md", - "state-machines-and-ownership/concept/concept-messaging-outbox-jdbc-postgresql-c05.md", - "state-machines-and-ownership/concept/concept-messaging-policy-c01.md", - "delivery-and-settlement-models/concept/concept-messaging-policy-c02.md", - "state-machines-and-ownership/concept/concept-messaging-policy-c03.md", - "delivery-and-settlement-models/concept/concept-messaging-policy-c04.md", - "state-machines-and-ownership/concept/concept-messaging-policy-c06.md", - "state-machines-and-ownership/concept/concept-messaging-policy-c09.md", - "state-machines-and-ownership/concept/concept-messaging-reliability-api-c01.md", - "state-machines-and-ownership/concept/concept-messaging-reliability-api-c02.md", - "state-machines-and-ownership/concept/concept-messaging-reliability-api-c03.md", - "state-machines-and-ownership/concept/concept-messaging-reliability-api-c04.md", - "state-machines-and-ownership/concept/concept-messaging-reliability-api-c05.md", - "state-machines-and-ownership/concept/concept-messaging-reliability-api-c06.md", - "state-machines-and-ownership/concept/concept-messaging-reliability-api-c07.md", - "delivery-and-settlement-models/concept/concept-messaging-runtime-core-c01.md", - "delivery-and-settlement-models/concept/concept-messaging-runtime-core-c02.md", - "delivery-and-settlement-models/concept/concept-messaging-runtime-core-c03.md", - "state-machines-and-ownership/concept/concept-messaging-runtime-core-c04.md", - "delivery-and-settlement-models/concept/concept-messaging-runtime-core-c05.md", - "state-machines-and-ownership/concept/concept-messaging-runtime-core-c06.md", - "state-machines-and-ownership/concept/concept-messaging-schema-api-c01.md", - "schema-and-wire-models/concept/concept-messaging-schema-api-c02.md", - "schema-and-wire-models/concept/concept-messaging-schema-api-c03.md", - "schema-and-wire-models/concept/concept-messaging-schema-api-c04.md", - "state-machines-and-ownership/concept/concept-messaging-schema-api-c05.md", - "state-machines-and-ownership/concept/concept-messaging-schema-api-c06.md", - "result-and-failure-algebra/concept/concept-messaging-schema-api-c07.md", - "schema-and-wire-models/concept/concept-messaging-schema-avro-c01.md", - "schema-and-wire-models/concept/concept-messaging-schema-avro-c02.md", - "schema-and-wire-models/concept/concept-messaging-schema-avro-c03.md", - "schema-and-wire-models/concept/concept-messaging-schema-avro-c04.md", - "schema-and-wire-models/concept/concept-messaging-schema-avro-c05.md", - "state-machines-and-ownership/concept/concept-messaging-schema-avro-c06.md", - "delivery-and-settlement-models/concept/concept-messaging-schema-json-c01.md", - "schema-and-wire-models/concept/concept-messaging-schema-json-c04.md", - "delivery-and-settlement-models/concept/concept-messaging-schema-json-c05.md", - "state-machines-and-ownership/concept/concept-messaging-schema-json-c06.md", - "state-machines-and-ownership/concept/concept-messaging-schema-json-c07.md", - "state-machines-and-ownership/concept/concept-messaging-schema-protobuf-c06.md", - "state-machines-and-ownership/concept/concept-messaging-security-c01.md", - "state-machines-and-ownership/concept/concept-messaging-security-c03.md", - "state-machines-and-ownership/concept/concept-messaging-security-c04.md", - "delivery-and-settlement-models/concept/concept-messaging-security-c05.md", - "state-machines-and-ownership/concept/concept-messaging-security-c06.md", - "state-machines-and-ownership/concept/concept-messaging-security-c07.md", - "delivery-and-settlement-models/concept/concept-messaging-spring-cloud-stream-bridge-c02.md", - "state-machines-and-ownership/concept/concept-messaging-spring-cloud-stream-bridge-c06.md", - "delivery-and-settlement-models/concept/concept-messaging-testkit-c02.md", - "state-machines-and-ownership/concept/concept-messaging-testkit-c07.md" - ] + "unlisted": [] } diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-inbound-websocket-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-inbound-websocket-c02.md deleted file mode 100644 index c3c5d98..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-inbound-websocket-c02.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-inbound-websocket-c02 -title: 요청 경로에 있는 것은 stomp 패키지가 참조하는 것뿐이다 -topic: transaction-and-consistency-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-inbound-websocket-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-inbound-websocket-c02 - file: ../../../final/evidence/rendered/adapter-inbound-websocket-c02.svg -evidence: - - ../../../final/evidence/raw/adapter-inbound-websocket-c02.txt -source: - - 원본 분석 절은 final/document.md#a17#L251 이다. -module: adapter-inbound-websocket ---- - -# 요청 경로에 있는 것은 stomp 패키지가 참조하는 것뿐이다 - -`stomp/WebSocketInboundAuthorizationInterceptor`(53)와 `stomp/AuthenticatedHandshakeInterceptor`(34)만 `WebSocketConfig`에 등록된다. 플랫폼 정책 타입은 `stomp`가 참조하지 않는다. - -## 본문 - - - -이 sub-scope에서 실제로 요청 경로에 있는 것은 **`stomp` 패키지가 참조하는 것뿐**이다. `stomp/WebSocketInboundAuthorizationInterceptor`(53)와 `stomp/AuthenticatedHandshakeInterceptor`(34)가 `WebSocketConfig`에 등록되고, 그 둘은 `stomp/WebSocketProperties`를 쓴다. - -## WebSocketConfig 참조 위치 - -:::evidence key="adapter-inbound-websocket-c02" alt="코드베이스에서 WebSocketConfig 를 검색한 출력 10줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="WebSocketConfig 코드베이스 검색 — 10줄 · exit 0" zoom="true" -::: - -## 요청 경로 밖에 남는 정책 타입 - -`security`(4) · `authz`(1) · `idempotency`(4) · `budget`(1)의 플랫폼 정책 타입은 `stomp`가 참조하지 않는다. 즉 **인증 프로파일 · 티켓 · origin 정책 · 메시지 권한 · 연결 예산 · 명령 멱등성이 모두 요청 경로 밖이다.** - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-cache-redis-c13.md b/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-cache-redis-c13.md deleted file mode 100644 index 1423813..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-cache-redis-c13.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-cache-redis-c13 -title: 등록되지 않은 스크립트는 서버에 닿을 방법이 없다 -topic: transaction-and-consistency-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-cache-redis-c13 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-cache-redis-c13 - file: ../../../final/evidence/rendered/adapter-outbound-cache-redis-c13.svg - - key: adapter-outbound-cache-redis-c13-diagram - file: ../../../final/assets/diagrams/adapter-outbound-cache-redis-c13.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-cache-redis-c13.txt -source: - - 원본 분석 절은 final/document.md#a10#L701 이다. -module: adapter-outbound-cache-redis ---- - -# 등록되지 않은 스크립트는 서버에 닿을 방법이 없다 - -`RedisScriptRegistry`가 등록을 배포 단계로 두고, 트랜잭션 창은 감시 키에서 정한 노드에 고정된다. - -## 본문 - - - -`RedisScriptRegistry`의 규칙 — "Registration is a deployment step, not a request-time one. A script that was never registered has no digest and therefore **no way to reach the server**, which is what makes 'only reviewed scripts run' a structural property rather than a convention." 같은 identity에 다른 body를 등록하면 거부하고, 실행 시점에도 body가 등록본과 같은지 다시 본다. - -## 등록과 실행의 분리 - -:::evidence key="adapter-outbound-cache-redis-c13-diagram" alt="배포 시 스크립트 등록과 digest 대조와 EVALSHA 실행이 왼쪽에서 오른쪽으로 이어지는 구조" caption="등록과 실행의 분리" zoom="false" -::: - -README가 주장하는 복구 사슬 `EVALSHA → NOSCRIPT → SCRIPT LOAD → digest verify → EVALSHA`는 **실재한다** — `LettuceRedisScriptOperations:26` javadoc이 "NOSCRIPT is the one failure retried automatically"라고 적고, `:129-133`이 `RedisNoScriptException` 또는 메시지 접두 `NOSCRIPT`를 잡아 `:100`에서 `registry.forget(script.id())`를 호출한다. 다음 호출이 `digest(...)`에서 다시 `SCRIPT LOAD`한다. - -## RedisScriptRegistry 참조 위치 - -:::evidence key="adapter-outbound-cache-redis-c13" alt="코드베이스에서 RedisScriptRegistry 를 검색한 출력 7줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RedisScriptRegistry 코드베이스 검색 — 7줄 · exit 0" zoom="true" -::: - -## Cluster에서 MULTI 창을 노드에 고정하는 방법 - -`RedisTransactionRunner`는 Cluster에서의 `MULTI` 문제를 정면으로 다룬다. javadoc이 문제와 해법을 적는다 — 다른 레인은 명령마다 슬롯 소유 노드로 라우팅하는데 "that is exactly what a `MULTI` window must not do: the queued commands would be spread across nodes and none of them would be part of the same window." 해법은 연결이 아니라 **라우팅 결정**이었다 — 감시 키(또는 명시적 슬롯 태그)에서 노드를 정해 레인을 고정한다. - -## 감시 키가 없는 Cluster 트랜잭션을 거부하는 이유 - -"the keys the callback will queue are not known until the callback runs, which is after the window is open." - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c02.md deleted file mode 100644 index 8209133..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c02.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c02 -title: 능력 어휘와 지원 등급과 실제 보고를 나눈다 -topic: transaction-and-consistency-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c02 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c02.svg - - key: adapter-outbound-persistence-jpa-c02-diagram - file: ../../../final/assets/diagrams/adapter-outbound-persistence-jpa-c02.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c02.txt -source: - - 원본 분석 절은 final/document.md#a05#L126 이다. -module: adapter-outbound-persistence-jpa ---- - -# 능력 어휘와 지원 등급과 실제 보고를 나눈다 - -`JpaCapability` enum은 16개 capability id를 갖고 app-bootstrap의 `capabilities()`도 16개를 선언한다. 능력 이름은 어휘일 뿐이고 `CapabilitySupport`가 등급을 결합하며 composition이 실제 보고를 구성한다. - -## 본문 - - - -현재 enum은 16개 capability id를 갖는다. app-bootstrap의 `JpaPlatformAutoConfiguration.capabilities()` 역시 16개를 선언하므로 **enum catalog와 current composition count는 일치**한다. - -Stable composition은 대표적으로 transaction retry, completion evidence, keyset pagination, batch, schema gate, runtime-role verification, observability를 기본 지원으로 보고한다. Advanced capability는 PostgreSQL native write/work claim/JSONB/array-range, bulk DML, stateless session, COPY, L2 cache, Envers 등을 constraints와 함께 보고한다. - -## 보고 가능한 계약이 만들어지는 층 - -:::evidence key="adapter-outbound-persistence-jpa-c02-diagram" alt="능력 어휘와 지원 등급 결합과 실제 보고 구성이 위에서 아래로 쌓이고 보고 구성 화살표가 아래로 그려진 구조" caption="보고 가능한 계약이 만들어지는 층" zoom="false" -::: - -이 분리는 "classpath에 코드가 있다"와 "현재 composition이 기본 지원한다고 약속한다"를 동일시하지 않는다. capability enum은 vocabulary이고, `CapabilitySupport`가 support level을 결합하며, app-bootstrap composition이 실제 현재 report를 구성한다. - -## CapabilitySupport 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c02" alt="코드베이스에서 CapabilitySupport 를 검색한 출력 11줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="CapabilitySupport 코드베이스 검색 — 11줄 · exit 0" zoom="true" -::: - -## 생성자가 보장하는 것 - -capability non-null, level non-null, constraints list defensive copy, 각 constraint non-null / non-blank. `usableByDefault()`는 STABLE만 true다 — Advanced/Experimental이 "존재하므로 기본 사용 가능"으로 오해되지 않게 support level을 코드에 남긴다. - -## 보고서가 담지 않는 것 - -`JpaPlatformReport`는 JDBC URL/user/password/SQL/entity catalog를 필드로 갖지 않도록 설계되어 있고, privilege detail도 boolean으로 축약한다. management endpoint의 reconnaissance surface를 좁히는 방향이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c10.md b/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c10.md deleted file mode 100644 index 0bb917e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c10.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c10 -title: 두 트랜잭션 경계가 공존하고 문서만 어긋난다 -topic: transaction-and-consistency-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c10 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c10 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c10.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c10.txt -source: - - 원본 분석 절은 final/document.md#a05#L763 이다. -module: adapter-outbound-persistence-jpa ---- - -# 두 트랜잭션 경계가 공존하고 문서만 어긋난다 - -`FullTransactionRetryCoordinator`는 현재 runtime bean graph에 포함되는 구현이다. 그러나 그것을 실제 business/application code가 호출하는 경로는 확인되지 않았고, application-core transaction port 쪽이 canonical contract로 쓰인다. - -## 본문 - - - -공개 어휘는 `PersistenceOperationName`, `TransactionProfile`, `RetryProfile`, `JpaPersistenceException`, `TransactionCompletionEvidence`다. `JpaPlatformRuntimeAutoConfiguration`은 `PlatformTransactionManager`가 있으면 `SpringJpaTransactionExecutor` bean을 만들고, 그 executor가 있으면 `FullTransactionRetryCoordinator` bean도 만든다. 따라서 source tree 수준에서는 단순 historical class가 아니라 **현재 runtime bean graph에도 포함되는 구현**이다. - -## 그런데 호출하는 곳이 없다 - -repository production call search에서는 `FullTransactionRetryCoordinator.execute(...)`를 실제 business/application code가 호출하는 경로가 확인되지 않았다. 반대로 application-core transaction port는 sample/use-case/composition에서 canonical contract로 사용된다. - -## 분석 원문의 경계 서술 - -:::evidence key="adapter-outbound-persistence-jpa-c10" alt="분석 문서 final/document.md#a05 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a05 발췌 — 15줄" zoom="true" -::: - -## 공존이 결함이 아닌 이유와 실제 문제 - -이 공존 자체는 곧바로 defect가 아니다. `api/**`는 intended external surface이므로 fork/application이 이 경로를 programmatically 사용할 수 있다. 문제는 문서가 두 boundary의 관계를 일관되게 설명하지 못하고, 일부 composition helper는 실제 type relationship과 다른 설명을 한다는 점이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c11.md b/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c11.md deleted file mode 100644 index 9dc3b5c..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c11.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c11 -title: Spring stereotype이 있다고 runtime에 도달하지 않는다 -topic: transaction-and-consistency-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c11 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c11 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c11.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c11.txt -source: - - 원본 분석 절은 final/document.md#a05#L788 이다. -module: adapter-outbound-persistence-jpa ---- - -# Spring stereotype이 있다고 runtime에 도달하지 않는다 - -root `CaSkeletonApplication`이 persistence package를 broad scan에서 의도적으로 제외하므로, adapter leaf 내부의 `@Component`가 자동으로 등록되지 않는다. `SpringTransactionPort`는 `JpaAdapterComponentsConfig`의 narrow component scan으로 등록된다. - -## 본문 - - - -`SpringTransactionPort`는 `PolicyTransactionPort`를 구현하며 `JpaAdapterComponentsConfig`의 narrow component scan으로 등록된다. 이 wiring은 중요하다 — root `CaSkeletonApplication`은 persistence package를 broad scan에서 의도적으로 제외한다. 그래서 adapter leaf 내부의 `@Component`를 "annotation이 있으니 알아서 등록될 것"이라고 볼 수 없다. - -## SpringTransactionPort 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c11" alt="코드베이스에서 SpringTransactionPort 를 검색한 출력 11줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="SpringTransactionPort 코드베이스 검색 — 11줄 · exit 0" zoom="true" -::: - -## 이 배치를 만든 과거 회귀 - -`JpaAdapterComponentsConfig` source에 기록돼 있다. persistence package를 broad scan에서 제외했고, `SpringTransactionPort` 같은 component를 별도 scan하지 않았다. 처음 transaction port가 필요한 capability가 조립될 때 unsatisfied dependency로 드러났고, 해결은 JPA master switch 아래에서만 persistence adapter package를 narrow scan하는 것이었다. current root는 `PersistenceJpaRootAutoConfiguration -> JpaAdapterComponentsConfig -> component scan` 체인을 통해 이를 해결한다. - -## inRootWrite가 inWrite와 다른 지점 - -`inWrite`와 `inRootWrite` 둘 다 REQUIRED · READ_COMMITTED · read-only false로 매핑된다. 특히 vendor default isolation에 맡기지 않고 READ_COMMITTED를 명시한다. 그런데 `inRootWrite`는 시작 전에 `TransactionSynchronizationManager.isActualTransactionActive()`를 확인해 ambient physical transaction이 있으면 manager/action 호출 전에 거부한다. "root boundary"를 REQUIRED의 join semantics로 조용히 바꾸지 않는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c14.md b/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c14.md deleted file mode 100644 index cba3ecd..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c14.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c14 -title: SQLSTATE가 retryable이라고 해서 use case를 다시 돌리지 않는다 -topic: transaction-and-consistency-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c14 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c14 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c14.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c14.txt -source: - - 원본 분석 절은 final/document.md#a05#L938 이다. -module: adapter-outbound-persistence-jpa ---- - -# SQLSTATE가 retryable이라고 해서 use case를 다시 돌리지 않는다 - -`TransactionRetryClassifier`의 automatic replay candidate는 `40001`과 `40P01` 둘뿐이고, 그것만으로는 재시도하지 않는다. 업무 side-effect가 replay-safe하다고 application policy가 선언해야 한다. - -## 본문 - - - -`TransactionRetryClassifier`는 cause chain에서 SQLSTATE를 찾지만 automatic replay candidate는 `40001`과 `40P01` 둘뿐이다. `08007` 같은 transaction-resolution-unknown은 candidate가 아니다. - -## TransactionRetryClassifier 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c14" alt="코드베이스에서 TransactionRetryClassifier 를 검색한 출력 11줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="TransactionRetryClassifier 코드베이스 검색 — 11줄 · exit 0" zoom="true" -::: - -## 분류만으로는 재시도하지 않는다 - -`SpringPolicyTransactionPort`는 ordinary command에서 40001이 나더라도 `COMMAND_SERIALIZABLE_REPLAY_SAFE`가 아니면 retry하지 않는다. failure 종류뿐 아니라 **업무 side-effect가 replay-safe하다고 application policy가 선언했는가**가 함께 필요하다. 이것은 "DB가 retryable이라고 말하니 use case를 다시 실행"하는 구조와 다르다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c15.md b/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c15.md deleted file mode 100644 index 200972d..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c15.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c15 -title: 한 번의 물리 시도만 담당하고 재시도는 하지 않는다 -topic: transaction-and-consistency-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c15 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c15 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c15.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c15.txt -source: - - 원본 분석 절은 final/document.md#a05#L953 이다. -module: adapter-outbound-persistence-jpa ---- - -# 한 번의 물리 시도만 담당하고 재시도는 하지 않는다 - -`SpringJpaTransactionExecutor`는 attempt boundary를 소유하고 raw provider exception을 `JpaPersistenceException`으로 바꾸는 위치다. 자체 retry는 하지 않는다. - -## 본문 - - - -이 executor는 한 번의 physical attempt만 담당한다. 자체 retry는 하지 않는다. - -```text -TransactionEvidenceContext.begin(...) - -> TransactionTemplate.execute(work) - -> success return -or - -> attempt boundary에서 failure translation - -> translated runtime exception rethrow -finally - -> TransactionEvidenceScope close -``` - -## SpringJpaTransactionExecutor 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c15" alt="코드베이스에서 SpringJpaTransactionExecutor 를 검색한 출력 26줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="SpringJpaTransactionExecutor 코드베이스 검색 — 26줄 · exit 0" zoom="true" -::: - -## 이 자리가 번역 지점인 이유 - -attempt boundary에서 operation, attempt number, elapsed time, reconciliation key를 알고 있으므로 raw provider exception을 `JpaPersistenceException`으로 변환하는 위치로 사용된다. vendor translator가 조립되면 PostgreSQL 40001/40P01 같은 structured SQLSTATE가 stable exception으로 바뀌어 coordinator가 처리할 수 있다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c16.md b/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c16.md deleted file mode 100644 index 0d72a95..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c16.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c16 -title: 완료 불명과 되돌릴 수 없는 부작용에서는 재시도하지 않는다 -topic: transaction-and-consistency-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c16 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c16 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c16.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c16.txt -source: - - 원본 분석 절은 final/document.md#a05#L974 이다. -module: adapter-outbound-persistence-jpa ---- - -# 완료 불명과 되돌릴 수 없는 부작용에서는 재시도하지 않는다 - -coordinator는 `JpaPersistenceException`만 catch하고 retry decision에 따라 새 transaction과 새 persistence context에서 전체 work를 다시 호출한다. 다섯 가지 guard가 그 재호출을 막는다. - -## 본문 - - - -coordinator는 `JpaPersistenceException`만 catch하고, retry decision에 따라 **새 transaction / 새 persistence context에서 전체 work를 다시 호출**한다. 설계상 중요한 guard가 다섯이다. - -- completion unknown → no retry -- irreversible side effect context → no retry -- retry budget elapsed → stop -- max attempts → stop -- backoff interrupt → stop - -retry listener는 observation only다. - -## FullTransactionRetryCoordinator 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c16" alt="코드베이스에서 FullTransactionRetryCoordinator 를 검색한 출력 15줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="FullTransactionRetryCoordinator 코드베이스 검색 — 15줄 · exit 0" zoom="true" -::: - -## 모델 자체의 테스트는 강하고 조립에는 결함이 있다 - -이 모델 자체의 unit tests는 강하다 — serialization/deadlock retry, exhaustion, completion unknown no-retry, interrupted sleep, irreversible side effect 등을 검증한다. 하지만 current implementation에는 public composition contract와 맞지 않는 별도 defect가 있다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c18.md b/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c18.md deleted file mode 100644 index 2fa3ab2..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c18.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c18 -title: 문서가 지목하는 조정 레코드를 쓰는 코드가 없다 -topic: transaction-and-consistency-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c18 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c18 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c18.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c18.txt -source: - - 원본 분석 절은 final/document.md#a05#L1184 이다. -module: adapter-outbound-persistence-jpa ---- - -# 문서가 지목하는 조정 레코드를 쓰는 코드가 없다 - -`CompletionUnknownRecord`와 `CompletionUnknownRecorder`는 current production에서 자신들의 정의 외 consumer/implementation이 없다. 그런데 문서는 훨씬 강한 계약을 선언한다. - -## 본문 - - - -`CompletionUnknownRecord`와 `CompletionUnknownRecorder`는 current production에서 자신들의 정의 외 consumer/implementation이 없다. - -## CompletionUnknownRecord 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c18" alt="코드베이스에서 CompletionUnknownRecord 를 검색한 출력 10줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="CompletionUnknownRecord 코드베이스 검색 — 10줄 · exit 0" zoom="true" -::: - -## 문서는 훨씬 강한 계약을 선언한다 - -support matrix는 이렇게 적는다. - -```text -Commit completion evidence = Stable -Automatic reconciliation unsupported. -The platform records; the domain resolves. -``` - -runbook은 신호로 `jpa.transaction.completion.unknown` 증가와 조정 채널의 `CompletionUnknownRecord`를 들고, 그 record의 `transactionKey`를 사용하라고 한다. 현재 이 record를 실제로 쓰는 production channel은 확인되지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c20.md b/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c20.md deleted file mode 100644 index 10eabd3..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c20.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c20 -title: 참조 수가 0이어도 스캔으로 도달한다 -topic: transaction-and-consistency-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c20 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c20 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c20.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c20.txt -source: - - 원본 분석 절은 final/document.md#a05#L1334 이다. -module: adapter-outbound-persistence-jpa ---- - -# 참조 수가 0이어도 스캔으로 도달한다 - -`JpaTransactionConfig`는 direct production reference가 거의 없지만 `@Configuration` + `@EnableConfigurationProperties`이고 `JpaAdapterComponentsConfig`가 transaction package를 component scan한다. - -## 본문 - - - -`JpaTransactionConfig`도 direct production reference는 거의 없다. 하지만 이 class는 `@Configuration` + `@EnableConfigurationProperties(JpaTransactionSettings.class)`이고, `JpaAdapterComponentsConfig`가 transaction package를 component scan한다. 따라서 direct Java call/import가 0이어도 runtime reachability가 있다. - -## JpaTransactionConfig 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c20" alt="코드베이스에서 JpaTransactionConfig 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="JpaTransactionConfig 코드베이스 검색 — 2줄 · exit 0" zoom="true" -::: - -## 이 클래스가 기록한 이력 - -- root `@ConfigurationPropertiesScan`에서 optional persistence tree 제외 -- JPA on 상태에서도 settings가 아무도 bind하지 않던 문제 발생 -- transaction port construction 실패 -- package-local configuration으로 JPA master switch 안에서만 settings enable - -## 참조 수만으로 dead code를 찾으면 안 되는 이유 - -이 사례는 mandatory public-reachability probe가 필요한 이유를 잘 보여준다. static reference count만으로 dead code를 찾으면 Spring discovery path를 오탐한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c21.md b/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c21.md deleted file mode 100644 index cf0c6c4..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c21.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c21 -title: 이름이 비슷한 두 번역 계열의 출력 계약이 다르다 -topic: transaction-and-consistency-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c21 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c21 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c21.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c21.txt -source: - - 원본 분석 절은 final/document.md#a05#L1358 이다. -module: adapter-outbound-persistence-jpa ---- - -# 이름이 비슷한 두 번역 계열의 출력 계약이 다르다 - -이 scope에는 이름이 비슷한 두 translation mechanism이 있다. 같은 이름 영역을 다루지만 current evidence로는 competing duplicate implementation이 아니다. - -## 본문 - - - -이 scope에는 이름이 비슷한 두 translation mechanism이 있다. - -`PersistenceFailureTranslatorChain`은 raw persistence/provider failure를 받아 `JpaPersistenceException` 계층을 내놓고, `SpringJpaTransactionExecutor`와 retry semantics가 소비한다. 목적은 SQLSTATE/optimistic conflict를 retry/completion semantics에 필요한 stable persistence failure로 바꾸는 것이다. - -## 분석 원문의 두 계열 비교 - -:::evidence key="adapter-outbound-persistence-jpa-c21" alt="분석 문서 final/document.md#a05 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a05 발췌 — 15줄" zoom="true" -::: - -## 중복 구현이 아니라 출력 계약이 다르다 - -다른 하나의 consumer는 adapter/application error boundary 쪽이다. 따라서 동일 이름 영역을 다루지만 current evidence로는 competing duplicate implementation이 아니다. **transaction retry algebra와 platform operational error mapping이라는 서로 다른 output contract**를 가진다. PostgreSQL vendor translator와 exact SQLSTATE catalog correctness는 vendor sub-scope에서 계속 검증한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c22.md b/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c22.md deleted file mode 100644 index 5e9e04a..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c22.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c22 -title: adapter 오류 경계 쪽 번역기는 다른 출력을 낸다 -topic: transaction-and-consistency-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c22 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c22 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c22.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c22.txt -source: - - 원본 분석 절은 final/document.md#a05#L1384 이다. -module: adapter-outbound-persistence-jpa ---- - -# adapter 오류 경계 쪽 번역기는 다른 출력을 낸다 - -`failure.PersistenceExceptionTranslator`의 consumer는 adapter/application error boundary 쪽이다. retry algebra 쪽 번역기와 출력 계약이 다르다. - -## 본문 - - - -`failure.PersistenceExceptionTranslator`의 consumer는 adapter/application error boundary 쪽이다. 따라서 retry algebra 쪽 번역기와 동일 이름 영역을 다루지만 current evidence로는 competing duplicate implementation이 아니다. - -## 분석 원문의 소비자 서술 - -:::evidence key="adapter-outbound-persistence-jpa-c22" alt="분석 문서 final/document.md#a05 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a05 발췌 — 15줄" zoom="true" -::: - -## 두 번역기가 나누는 출력 계약 - -**transaction retry algebra와 platform operational error mapping이라는 서로 다른 output contract**를 가진다. PostgreSQL vendor translator와 exact SQLSTATE catalog correctness는 vendor sub-scope에서 계속 검증한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c23.md b/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c23.md deleted file mode 100644 index 90cb58e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c23.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c23 -title: 설치되는 bean 셋과 설치되지 않는 구현 셋 -topic: transaction-and-consistency-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c23 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c23 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c23.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c23.txt -source: - - 원본 분석 절은 final/document.md#a05#L1416 이다. -module: adapter-outbound-persistence-jpa ---- - -# 설치되는 bean 셋과 설치되지 않는 구현 셋 - -`JpaPlatformRuntimeAutoConfiguration`이 만드는 bean은 셋이고, 문서상 completion-evidence/operability의 핵심인 세 구현은 current root assembly에서 provider가 없다. - -## 본문 - - - -`JpaPlatformRuntimeAutoConfiguration`이 만드는 것은 셋이다. - -- `SpringJpaTransactionExecutor` — `PlatformTransactionManager`가 있을 때 -- `FullTransactionRetryCoordinator` — executor가 있을 때 -- default empty `RetryEventListener` - -## JpaPlatformRuntimeAutoConfiguration 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c23" alt="코드베이스에서 JpaPlatformRuntimeAutoConfiguration 를 검색한 출력 12줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="JpaPlatformRuntimeAutoConfiguration 코드베이스 검색 — 12줄 · exit 0" zoom="true" -::: - -## 현재 설치되지 않는 특화 구현 셋 - -`EvidenceAwareJpaTransactionManager`, `CompletionUnknownRecorder`, `JpaTransactionObservation` call path. 이 셋은 문서상 completion-evidence/operability의 핵심이지만 current root assembly에서 provider가 없다. 이 sibling comparison으로 "class가 있으니 feature가 있다"는 판단을 피했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c26.md b/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c26.md deleted file mode 100644 index 9ddf91c..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c26.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c26 -title: 쓰기 경로마다 트랜잭션 소유가 다르다 -topic: transaction-and-consistency-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c26 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c26 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c26.svg - - key: adapter-outbound-persistence-jpa-c26-diagram - file: ../../../final/assets/diagrams/adapter-outbound-persistence-jpa-c26.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c26.txt -source: - - 원본 분석 절은 final/document.md#a05#L1819 이다. -module: adapter-outbound-persistence-jpa ---- - -# 쓰기 경로마다 트랜잭션 소유가 다르다 - -`HibernateBulkDmlExecutor`는 arbitrary JPQL을 아무 데서나 실행하는 helper가 아니고, `HibernateStatelessSessionRunner`는 이 platform에서 transaction ownership 예외를 명시적으로 드러낸다. - -## 본문 - - - -`HibernateBulkDmlExecutor`는 arbitrary JPQL string을 아무 데서나 실행하는 helper가 아니다. operation name 등록, affected-row expectation, persistence-context cleanup, transaction requirement를 contract로 둔다. bulk DML은 managed entity lifecycle을 우회하므로 ordinary entity save와 같은 audit/lifecycle guarantee를 기대하면 안 된다. - -## 쓰기 경로마다 다른 소유 모델 - -:::evidence key="adapter-outbound-persistence-jpa-c26-diagram" alt="쓰기 경로에서 일반 repository adapter 와 bulk DML 과 StatelessSession runner 로 화살표가 나가고 화살표마다 트랜잭션 소유 방식이 붙은 구조" caption="쓰기 경로마다 다른 소유 모델" zoom="false" -::: - -## HibernateBulkDmlExecutor 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c26" alt="코드베이스에서 HibernateBulkDmlExecutor 를 검색한 출력 7줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="HibernateBulkDmlExecutor 코드베이스 검색 — 7줄 · exit 0" zoom="true" -::: - -## 사용 중이라고 주장하지 않는 이유 - -support matrix도 이를 Advanced capability로 분리한다. 현재 production business consumer는 확인되지 않았고 PostgreSQL integration fixture에서 실제 behavior를 qualification한다. 따라서 "runtime에서 사용 중"이라고 주장하지 않는다. - -## 이름만 row cap 이던 문제는 고쳐져 있다 - -`HibernateStatelessSessionRunner`는 오히려 이 platform에서 transaction ownership 예외를 명시적으로 드러낸다 — 일반 repository adapter는 application transaction boundary에 참여하고, StatelessSession runner는 그렇지 않다. 과거 review에서는 caller가 선언한 maxRows가 실제 affected rows와 연결되지 않는 문제가 있었다. 현재는 `StatelessWorkResult(value, affectedRows)`를 요구하고 cap 초과 시 commit 전에 rollback한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c37.md b/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c37.md deleted file mode 100644 index 5467685..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c37.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c37 -title: 중복 재생이 owner 검증보다 먼저 저장된 owner를 돌려준다 -topic: transaction-and-consistency-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c37 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c37 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c37.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c37.txt -source: - - 원본 분석 절은 final/document.md#a05#L2734 이다. -module: adapter-outbound-persistence-jpa ---- - -# 중복 재생이 owner 검증보다 먼저 저장된 owner를 돌려준다 - -`PostgreSqlSameStoreInboxAdapter`와 `PostgreSqlPollingDeliveryAdapter`는 owner-safe transition contract를 구현하지만 현재 production composition에서 bean construction이나 stereotype이 확인되지 않았다. 따라서 아래는 채택 시 활성화되는 latent defect다. - -## 본문 - - - -`PostgreSqlSameStoreInboxAdapter`와 `PostgreSqlPollingDeliveryAdapter`는 `application-core`의 owner-safe transition contract를 구현하지만, 현재 production composition에서 bean construction이나 stereotype은 확인되지 않았다. 따라서 아래 finding은 **현재 배포 기본 경로의 즉시 장애가 아니라, 이 candidate adapter를 채택할 때 활성화되는 latent defect**로 분리한다. - -## markProcessing이 검증보다 먼저 owner를 돌려준다 - -`markProcessing()`은 같은 `START + operationId`를 발견하면 `classifyMismatch()`보다 먼저 `owner(row)`를 반환한다. 이 때문에 scope/operation id만 맞춘 forged owner로 replay하면 DB에 저장된 실제 owner token을 돌려받을 수 있다. 실제 PostgreSQL probe는 이렇다. - -```text -inboxForgedReplay.outcome=PROCESSING_STARTED -inboxForgedReplay.returnedActualToken=true -inboxForgedReplay.returnedForgedToken=false -inboxForgedReplay.completeWithReturnedOwner=COMPLETED -``` - -즉 duplicate handling이 owner capability recovery oracle처럼 동작한다. 채택 전에는 duplicate replay에서도 persisted owner tuple/revision과 supplied owner를 먼저 검증하도록 고쳐야 한다. - -## PostgreSqlSameStoreInboxAdapter 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c37" alt="코드베이스에서 PostgreSqlSameStoreInboxAdapter 를 검색한 출력 8줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="PostgreSqlSameStoreInboxAdapter 코드베이스 검색 — 8줄 · exit 0" zoom="true" -::: - -## digest가 담지 않는 두 인자 - -`markRetryable`/`markDead`의 `retention`은 실제 SQL update에는 들어가지만 transition digest에는 들어가지 않는다. 동일 operation id로 retention만 바꾼 replay가 same-operation으로 흡수된다. retention은 terminal row 보존 기간을 결정하는 semantic argument이므로 digest에 canonical millis를 포함해야 한다. - -`markRetryable()`은 `nextAttemptAt`을 DB에 기록하지만 transition digest는 kind + operation + owner + errorCode만 포함한다. 재시도 시각은 delivery scheduling 자체를 바꾸는 semantic argument다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c42.md b/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c42.md deleted file mode 100644 index cd30ec1..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c42.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c42 -title: 여섯 package만 스캔하므로 같은 리프 안에서도 도달성이 다르다 -topic: transaction-and-consistency-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c42 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c42 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c42.svg - - key: adapter-outbound-persistence-jpa-c42-diagram - file: ../../../final/assets/diagrams/adapter-outbound-persistence-jpa-c42.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c42.txt -source: - - 원본 분석 절은 final/document.md#a05#L2971 이다. -module: adapter-outbound-persistence-jpa ---- - -# 여섯 package만 스캔하므로 같은 리프 안에서도 도달성이 다르다 - -`JpaAdapterComponentsConfig`는 adapter 전체를 넓게 scan하지 않고 `audit`·`failure`·`idempotency`·`lock`·`outbox`·`transaction` 여섯만 명시적으로 component scan한다. - -## 본문 - - - -`JpaAdapterComponentsConfig`는 adapter 전체를 넓게 scan하지 않고 `audit`·`failure`·`idempotency`·`lock`·`outbox`·`transaction` 여섯 package만 명시적으로 component scan한다. - -## narrow scan이 닿는 범위 - -:::evidence key="adapter-outbound-persistence-jpa-c42-diagram" alt="스캔 경계 안에 여섯 package 가 두 상자로 들어 있고 두 어댑터가 경계 밖 빗금 상자로 놓인 구조" caption="narrow scan 이 닿는 범위" zoom="false" -::: - -`OutboxStoreAdapter`는 baseline scan에 들어가고 `app-bootstrap`의 `OutboxConfig`가 `OutboxStorePort`로 사용한다. `DurableOperationStoreAdapter`, `JpaLiveEventReplayAdapter`는 현재 baseline component scan에 들어가지 않고 별도 production constructor/reference도 확인되지 않았다. - -## JpaAdapterComponentsConfig 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c42" alt="코드베이스에서 JpaAdapterComponentsConfig 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="JpaAdapterComponentsConfig 코드베이스 검색 — 5줄 · exit 0" zoom="true" -::: - -## default composition에 들어가지 않는 것 - -`HibernateCacheGuard`, `HibernateEnversHistoryReader`와 Spring Data auditing candidate도 default composition에 들어가지 않는다. runtime-role verifier 자체는 app-bootstrap bean으로 구성되지만, policy를 적용하는 `requireSafe()` caller가 없다. - -## 판정을 세 등급으로 나누는 이유 - -이 차이 때문에 아래 finding은 `production`, `conditional-production`, `latent`를 분리해 판정한다. 정적 composition snapshot은 `evidence/raw/072-baseline-capability-reachability.txt`에 남겼다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c51.md b/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c51.md deleted file mode 100644 index 032087e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-jpa-c51.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-jpa-c51 -title: Stable 스캔 목록에 experimental package가 이미 들어 있다 -topic: transaction-and-consistency-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-jpa-c51 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-jpa-c51 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c51.svg - - key: adapter-outbound-persistence-jpa-c51-diagram - file: ../../../final/assets/diagrams/adapter-outbound-persistence-jpa-c51.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c51.txt -source: - - 원본 분석 절은 final/document.md#a05#L3609 이다. -module: adapter-outbound-persistence-jpa ---- - -# Stable 스캔 목록에 experimental package가 이미 들어 있다 - -현재 production call graph에서 experimental 타입을 조립하는 경로는 없다. 그러나 structural opt-in은 완전히 닫혀 있지 않다. - -## 본문 - - - -현재 repository 내부 production call graph에서는 `TenantDataSourceRegistry`, `TenantEntityManagerFactoryRegistry`, `SchemaMultiTenantConnectionProvider`, `ConsistencyAwareDataSourceRouter`, `RlsTenantSessionBinder`, `SchemaTenantMigrationOrchestrator` 등을 app-bootstrap이나 다른 production leaf가 조립하는 경로를 찾지 못했다. `backend.jpa.experimental.*` property도 production configuration에서 읽어 bean을 만드는 경로가 없고, 실제 문자열은 `ExperimentalFeature` enum의 property vocabulary에만 존재한다. - -## TenantDataSourceRegistry 참조 위치 - -:::evidence key="adapter-outbound-persistence-jpa-c51" alt="코드베이스에서 TenantDataSourceRegistry 를 검색한 출력 8줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="TenantDataSourceRegistry 코드베이스 검색 — 8줄 · exit 0" zoom="true" -::: - -## latent로 분류한 이유 - -따라서 아래 semantic finding은 **현재 app-bootstrap runtime에서 즉시 활성화된 production defect가 아니라 latent experimental defect**로 분류한다. 이 구분은 중요하다 — public API surface에 올라 있고 같은 artifact에 포함된 library code가 잘못된 것과, 현재 기본 애플리케이션이 그 code를 실제 실행하는 것은 다른 주장이다. - -## Stable 스캔 목록에 들어 있는 것 - -:::evidence key="adapter-outbound-persistence-jpa-c51-diagram" alt="Stable EntityScan 목록에서 stable package 목록과 experimental package 로 화살표가 나가고 experimental 쪽만 빗금 상자로 표시된 구조" caption="Stable 스캔 목록에 들어 있는 것" zoom="false" -::: - -반면 structural opt-in은 완전히 닫혀 있지 않다. `PersistenceJpaConfig`의 Stable `@EntityScan`과 `@EnableJpaRepositories` 문자열 목록에는 이미 `dev.caskeleton.adapter.outbound.persistence.experimental`이 들어 있다. 현재 experimental package에는 `@Entity`, `@Repository`, `JpaRepository`, `@MappedSuperclass`가 없어서 당장 persistence unit에 들어오는 concrete JPA type은 없지만, 이후 experimental entity/repository 하나가 추가되면 별도 feature condition 없이 Stable persistence unit이 스캔한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-mongo-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-mongo-c03.md deleted file mode 100644 index 2c14f14..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-adapter-outbound-persistence-mongo-c03.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -kind: CONCEPT -slug: adapter-outbound-persistence-mongo-c03 -title: 아직 쓰이지 않은 연산에도 같은 불변식이 성립하게 만든 순서 -topic: transaction-and-consistency-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:adapter-outbound-persistence-mongo-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: adapter-outbound-persistence-mongo-c03 - file: ../../../final/evidence/rendered/adapter-outbound-persistence-mongo-c03.svg - - key: adapter-outbound-persistence-mongo-c03-diagram - file: ../../../final/assets/diagrams/adapter-outbound-persistence-mongo-c03.svg -evidence: - - ../../../final/evidence/raw/adapter-outbound-persistence-mongo-c03.txt -source: - - 원본 분석 절은 final/document.md#a06#L586 이다. -module: adapter-outbound-persistence-mongo ---- - -# 아직 쓰이지 않은 연산에도 같은 불변식이 성립하게 만든 순서 - -`DefaultMongoImperativeExecutor.executeInternal(...)`이 collection profile 해석 → observation 개시 → consistency 바인딩 → callback 실행 → 실패 번역(최대 한 번) → observation 종료 순서를 고정한다. - -## 본문 - - - -`DefaultMongoImperativeExecutor.executeInternal(...)`은 순서를 고정한다 — collection profile 해석 → observation 개시 → consistency 바인딩 → callback 실행 → 실패 번역(최대 한 번) → observation 종료. javadoc이 이유를 적는다: "Fixing it here is what makes the invariants hold for operations nobody has written yet." - -## 실행 scope의 고정된 순서 - -:::evidence key="adapter-outbound-persistence-mongo-c03-diagram" alt="profile 해석과 observation 개시와 consistency 바인딩과 callback 실행이 왼쪽에서 오른쪽으로 이어지는 구조" caption="실행 scope 의 고정된 순서" zoom="false" -::: - -## 세 가지 방어 - -- 이미 번역된 `MongoPersistenceException`은 그대로 통과시킨다. 재번역하면 bulk partial failure나 guardrail 거절처럼 **그것을 던진 계층이 더 잘 아는** category를, driver 코드에서 유도한 일반 category로 덮어쓰게 된다. -- Spring이 감싼 driver 예외를 `unwrap(...)`으로 되꺼낸다. Spring의 번역은 error label을 잃는데, label이야말로 replayable transaction과 unknown commit을 가르는 값이다. -- `MongoCompletion.successOutcomeFor(operationType)`가 read와 write의 성공 outcome을 나눈다. - -## MongoPersistenceException 참조 위치 - -:::evidence key="adapter-outbound-persistence-mongo-c03" alt="코드베이스에서 MongoPersistenceException 를 검색한 출력 28줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MongoPersistenceException 코드베이스 검색 — 28줄 · exit 0" zoom="true" -::: - -## 성공 outcome을 read와 write로 나눈 이유 - -과거에는 두 executor 모두 성공을 `WRITE_CONFIRMED`로 기록해, "write가 acknowledge되고 있는가"를 답하는 지표가 read 트래픽의 함수가 됐다. `default` 분기가 `READ_CONFIRMED`로 떨어지는 것도 의도적이다 — "the honest answer is the one that claims least". - -## 동적 collection 이름을 우회할 방법이 없는 이유 - -`MongoCollectionProfileRegistry`가 "동적 collection 이름 금지"를 강제 가능하게 만드는 지점이다. 애플리케이션은 profile을 부르고 물리 이름은 이 registry만 안다. `ScopedAccess.collection(String)`은 요청된 collection이 scope의 것과 다르면 거부하고, `ScopedMongoOperations`의 어떤 메서드도 collection 인자를 받지 않는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-application-core-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-application-core-c03.md deleted file mode 100644 index 678bb77..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-application-core-c03.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -kind: CONCEPT -slug: application-core-c03 -title: 예외가 발생했다는 것을 rollback으로 단순화하지 않는다 -topic: transaction-and-consistency-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:application-core-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: application-core-c03 - file: ../../../final/evidence/rendered/application-core-c03.svg - - key: application-core-c03-diagram - file: ../../../final/assets/diagrams/application-core-c03.svg -evidence: - - ../../../final/evidence/raw/application-core-c03.txt -source: - - 원본 분석 절은 final/document.md#a03#L88 이다. -module: application-core ---- - -# 예외가 발생했다는 것을 rollback으로 단순화하지 않는다 - -`TransactionPolicyId`가 Spring propagation 숫자 대신 application semantic ID를 노출하고, `TransactionResult`가 commit 결과를 다섯 상태로 분리한다. - -## 관계 - -- **legacy storage/notification compatibility surface의 제거 조건 추적** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -`TransactionPort`는 `inWrite`, `inRootWrite`, `inRead`, `inNew` 네 개의 framework-neutral boundary를 노출한다. `PolicyTransactionPort`는 기존 surface를 깨지 않고 `TransactionRequest -> TransactionResult` 정책 기반 API를 추가한다. - -## 노출하는 어휘와 감추는 어휘 - -:::evidence key="application-core-c03-diagram" alt="application-core 경계 안에 정책 식별자와 결과 대수가 들어 있고 Spring 전파 숫자가 경계 밖 빗금 상자로 놓인 구조" caption="노출하는 어휘와 감추는 어휘" zoom="false" -::: - -`TransactionPolicyId`는 Spring propagation 숫자가 아니라 `COMMAND_DEFAULT`, `COMMAND_SERIALIZABLE_REPLAY_SAFE`, `QUERY_PRIMARY`, `QUERY_REPLICA_ELIGIBLE`, `OUTBOX_APPEND`, `INBOX_AND_HANDLER`, `MAINTENANCE_NEW`처럼 application semantic ID를 노출한다. `TransactionRequest` constructor는 read policy의 consistency allowlist, non-read의 readConsistency 금지, operationId-required policy의 stable id 존재를 fail-fast한다. - -## 이 기록이 다루는 파일 범위 - -:::evidence key="application-core-c03" alt="코드베이스에서 파일 목록을 만든 출력 25줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 25줄 · exit 0" zoom="true" -::: - -## commit 결과 다섯 상태 - -- `Committed`: physical commit을 확인한 결과. -- `Participating`: outer transaction에 참여했지만 아직 commit을 주장할 수 없는 결과. -- `DeterminateRollback`: rollback이 확정된 실패. -- `Indeterminate`: commit 여부를 확정할 수 없는 결과. -- `CommittedWithPostCommitFailure`: commit은 됐지만 이후 operational cleanup이 실패한 결과. - -## Indeterminate를 정식 상태로 두는 이유 - -이 algebra의 핵심은 "exception이 발생했다 = rollback"으로 단순화하지 않는 것이다. `Indeterminate`는 last observed transaction phase와 optional reconciliation reference를 보존하며, `CompletionResolution`은 `STILL_UNKNOWN`을 정식 상태로 둔다. 불확실한 commit을 임의로 NOT_COMMITTED로 가정해 use case를 재실행하는 것을 피한다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-grpc-server-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-grpc-server-c01.md deleted file mode 100644 index 9960b03..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-grpc-server-c01.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -kind: CONCEPT -slug: grpc-server-c01 -title: 승인이 마감보다 먼저이고 멱등이 검증보다 먼저다 -topic: transaction-and-consistency-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:grpc-server-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-server-c01 - file: ../../../final/evidence/rendered/grpc-server-c01.svg - - key: grpc-server-c01-diagram - file: ../../../final/assets/diagrams/grpc-server-c01.svg -evidence: - - ../../../final/evidence/raw/grpc-server-c01.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-server#L53 이다. -module: grpc-server ---- - -# 승인이 마감보다 먼저이고 멱등이 검증보다 먼저다 - -인터셉터가 열 단계이고 선언 순서가 계약이다. 각 위치의 이유가 열거형 javadoc에 있다. - -## 본문 - - - -열 단계이고 선언 순서가 계약이다. 각 위치의 이유가 열거형 javadoc 에 있다. - -```text -EXCEPTION_BOUNDARY → TRACE → AUTHENTICATION → ACTOR_TENANT → AUTHORIZATION -→ ADMISSION → DEADLINE_CANCELLATION → IDEMPOTENCY → VALIDATION → SERVICE_ADAPTER -``` - -## 열 단계 중 안쪽 다섯 - -:::evidence key="grpc-server-c01-diagram" alt="승인과 마감 취소와 멱등과 검증과 서비스 어댑터가 왼쪽에서 오른쪽으로 이어지고 화살표마다 넘어가는 값이 붙은 구조" caption="열 단계 중 안쪽 다섯" zoom="false" -::: - -- 예외 경계가 가장 바깥 — 이후 단계의 실패가 매핑되지 않은 상태로 새지 않는다 -- 인증 → 행위자·소속 → 인가 — 각 단계가 앞 단계의 답을 필요로 한다 -- 승인이 마감보다 먼저 — 부하 중 서버가 일을 쓰기 전에 흘려보낸다 -- 멱등이 검증보다 먼저 — 재생된 요청이 이미 받아들인 본문을 다시 검증하지 않고 저장된 결과를 돌려준다 -- 검증이 어댑터 직전 — 사용 사례는 믿을 수 있는 메시지를 받는다 - -## 이 기록이 다루는 파일 범위 - -:::evidence key="grpc-server-c01" alt="코드베이스에서 파일 목록을 만든 출력 17줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 17줄 · exit 0" zoom="true" -::: - -## 선택적인 단계는 하나뿐이다 - -필수가 아닌 단계는 멱등 하나다 — 상태 변경 키 메서드가 없는 서버에는 할 일이 없기 때문이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-messaging-inbox-jdbc-postgresql-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-messaging-inbox-jdbc-postgresql-c01.md deleted file mode 100644 index 32e4d25..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-messaging-inbox-jdbc-postgresql-c01.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-inbox-jdbc-postgresql-c01 -title: 메커니즘 전체가 하나의 SQL 문장에 있다 -topic: transaction-and-consistency-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-inbox-jdbc-postgresql-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-inbox-jdbc-postgresql-c01 - file: ../../../final/evidence/rendered/messaging-inbox-jdbc-postgresql-c01.svg -evidence: - - ../../../final/evidence/raw/messaging-inbox-jdbc-postgresql-c01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-inbox-jdbc-postgresql#L57 이다. -module: messaging-inbox-jdbc-postgresql ---- - -# 메커니즘 전체가 하나의 SQL 문장에 있다 - -`messaging-reliability-api`의 `InboxRepository`·`IdempotentMessageHandler` 포트를 PostgreSQL로 구현한다. 중복 제거 메커니즘 전체가 복합 기본키 하나에 있다. - -## 관계 - -- **컬럼 폭은 애플리케이션 검증과 짝을 이룬다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 안전 규칙은 한 공식과 한 강제 시점을 갖는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -`messaging-reliability-api`의 `InboxRepository`·`IdempotentMessageHandler` 포트를 PostgreSQL로 구현한다. 이름이 기술을 드러낸다 — `docs/messaging/support-matrix.md`가 그 개명 이유를 적는다(MSG-023). - -**메커니즘 전체가 하나의 SQL 문장에 있다.** - -```sql -INSERT INTO messaging_inbox (message_id, consumer_id, processed_at) -VALUES (?, ?, ?) -ON CONFLICT (message_id, consumer_id) DO NOTHING -``` - -> "Reservation is an `INSERT ... ON CONFLICT DO NOTHING` whose affected-row count is the answer: one means first delivery, zero means already processed. The composite primary key does the work, so there is no read-then-write race — two concurrent deliveries of the same message cannot both see 'not processed' and both proceed." - -## InboxRepository 참조 위치 - -:::evidence key="messaging-inbox-jdbc-postgresql-c01" alt="코드베이스에서 InboxRepository 를 검색한 출력 20줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="InboxRepository 코드베이스 검색 — 20줄 · exit 0" zoom="true" -::: - -## 실제로 실행되는 레인 - -migration이 같은 사실을 반대편에서 적고, `build.gradle` 주석이 테스트 전략을 명시한다. **그리고 실제로 실행된다** — `InboxPostgresIT` 6개가 기본 `test` 태스크에서 통과한다(§10). - -## migration이 같은 사실을 적는다 - -복합 기본키가 중복 제거 메커니즘이고, 예약은 핸들러의 부작용과 같은 트랜잭션 안에서 성공하거나 키를 위반하는 INSERT다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-messaging-inbox-jdbc-postgresql-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-messaging-inbox-jdbc-postgresql-c02.md deleted file mode 100644 index 93ae23c..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-messaging-inbox-jdbc-postgresql-c02.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-inbox-jdbc-postgresql-c02 -title: starter가 셋을 만들고 InboxRepository는 그 목록에 없다 -topic: transaction-and-consistency-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-inbox-jdbc-postgresql-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-inbox-jdbc-postgresql-c02 - file: ../../../final/evidence/rendered/messaging-inbox-jdbc-postgresql-c02.svg -evidence: - - ../../../final/evidence/raw/messaging-inbox-jdbc-postgresql-c02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-inbox-jdbc-postgresql#L98 이다. -module: messaging-inbox-jdbc-postgresql ---- - -# starter가 셋을 만들고 InboxRepository는 그 목록에 없다 - -starter의 `MessagingReliabilityAutoConfiguration`이 세 bean을 만드는데 `JdbcInboxRepository`는 그 목록에 없다. - -## 관계 - -- **컬럼 폭은 애플리케이션 검증과 짝을 이룬다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 안전 규칙은 한 공식과 한 강제 시점을 갖는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -들어오는 것: `messaging-core-api`(api), `messaging-reliability-api`(api), `spring-jdbc`·`spring-tx`(implementation). 나가는 것: `messaging-spring-boot-starter`. - -## MessagingReliabilityAutoConfiguration 참조 위치 - -:::evidence key="messaging-inbox-jdbc-postgresql-c02" alt="코드베이스에서 MessagingReliabilityAutoConfiguration 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingReliabilityAutoConfiguration 코드베이스 검색 — 3줄 · exit 0" zoom="true" -::: - -## 만들어지는 것과 만들어지지 않는 것 - -**배선됨.** starter의 `MessagingReliabilityAutoConfiguration`이 셋을 만든다. `JdbcInboxRepository`는 그 목록에 없다 — `InboxRepository` bean을 누가 만드는지는 starter leaf가 답한다. - -## Spring 타입을 쓰는 두 곳 - -`DataSourceUtils`와 `TransactionSynchronizationManager`. 둘 다 `implementation` scope이고 public 시그니처에 나오지 않으므로 vendor `api` 규칙에 맞는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-messaging-inbox-jdbc-postgresql-c03.md b/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-messaging-inbox-jdbc-postgresql-c03.md deleted file mode 100644 index 9a16c99..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-messaging-inbox-jdbc-postgresql-c03.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-inbox-jdbc-postgresql-c03 -title: 두 오버로드가 있었고 호출되는 쪽이 틀린 쪽이었다 -topic: transaction-and-consistency-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-inbox-jdbc-postgresql-c03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-inbox-jdbc-postgresql-c03 - file: ../../../final/evidence/rendered/messaging-inbox-jdbc-postgresql-c03.svg - - key: messaging-inbox-jdbc-postgresql-c03-diagram - file: ../../../final/assets/diagrams/messaging-inbox-jdbc-postgresql-c03.svg -evidence: - - ../../../final/evidence/raw/messaging-inbox-jdbc-postgresql-c03.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-inbox-jdbc-postgresql#L146 이다. -module: messaging-inbox-jdbc-postgresql ---- - -# 두 오버로드가 있었고 호출되는 쪽이 틀린 쪽이었다 - -`requireActiveTransaction`이 이 leaf에서 가장 중요한 안전 장치이고, 이전 결함이 javadoc에 있다. 현재는 interface 메서드가 세 가지를 확인한다. - -## 관계 - -- **컬럼 폭은 애플리케이션 검증과 짝을 이룬다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 안전 규칙은 한 공식과 한 강제 시점을 갖는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 본문 - - - -이 leaf에서 가장 중요한 안전 장치이고 이전 결함이 javadoc에 있다. - -> "Package-private. It used to be public and was the only path that actually joined the caller's transaction, while the interface method — the one `IdempotentConsumer` calls — opened a raw connection that auto-commits. A reservation that commits on its own while the business side effect rolls back is a message that will never be redelivered and whose work never happened." - -**두 개의 오버로드가 있었고 호출되는 쪽이 틀린 쪽이었다.** - -## 예약 전에 확인하는 세 가지 - -:::evidence key="messaging-inbox-jdbc-postgresql-c03-diagram" alt="requireActiveTransaction 경계 안에 활성 트랜잭션과 읽기 전용 아님과 같은 DataSource 세 상자가 나란히 들어 있는 구조" caption="예약 전에 확인하는 세 가지" zoom="false" -::: - -| 검사 | 실패 시 메시지의 핵심 | -|---|---| -| `isActualTransactionActive()` | "a reservation that commits alone marks a message processed whose work may still roll back" | -| `!isCurrentTransactionReadOnly()` | "the current one is read-only" | -| `hasResource(dataSource)` | "it is bound to another, so the reservation and the side effect would commit independently" | - -세 번째가 특히 정교하다 — **트랜잭션이 활성이어도 다른 DataSource에 묶여 있으면 거절한다.** 멀티 데이터소스 배포에서 실제로 발생하는 형태이고, 그 경우 예약과 부작용이 서로 다른 트랜잭션에 들어간다. 세 검사 전부 같은 코드 `INBOX_TRANSACTION_REQUIRED`를 쓴다 — 메시지만 다르다. - -## InboxPostgresIT 참조 위치 - -:::evidence key="messaging-inbox-jdbc-postgresql-c03" alt="코드베이스에서 InboxPostgresIT 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="InboxPostgresIT 코드베이스 검색 — 2줄 · exit 0" zoom="true" -::: - -**"the path nobody exercises before production"**가 이 leaf의 테스트 전략을 설명한다 — `InboxPostgresIT.aRolledBackTransactionLeavesNoReservationAndNoSideEffect`가 정확히 그 경로를 실 DB에서 돈다. - -## 위임과 검증이 짝을 이룬다 - -`TransactionRunner`가 함수형 인터페이스이고 ` T inTransaction(Supplier work)` 하나다. 즉 이 leaf는 Spring `@Transactional`에 의존하지 않고 **경계 제공을 호출자에게 위임**한다. `JdbcInboxRepository.requireActiveTransaction`이 그 위임이 지켜졌는지를 런타임에 확인한다. - -## 중복이 정상 결과라는 명시 - -"A duplicate is not an error. It is the expected consequence of at-least-once delivery, so the skip path is a normal outcome rather than an exception." 세 금지가 `messaging-reliability-api`의 `TransactionalMessageAction` javadoc이 구현자에게 요구한 것과 대칭이다 — 그쪽은 action에게, 이쪽은 handler에게. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-messaging-outbox-jdbc-postgresql-c01.md b/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-messaging-outbox-jdbc-postgresql-c01.md deleted file mode 100644 index 3f86ddf..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-messaging-outbox-jdbc-postgresql-c01.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-outbox-jdbc-postgresql-c01 -title: 모르는 것을 실패로 취급하지 않는다 -topic: transaction-and-consistency-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-outbox-jdbc-postgresql-c01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-outbox-jdbc-postgresql-c01 - file: ../../../final/evidence/rendered/messaging-outbox-jdbc-postgresql-c01.svg - - key: messaging-outbox-jdbc-postgresql-c01-diagram - file: ../../../final/assets/diagrams/messaging-outbox-jdbc-postgresql-c01.svg -evidence: - - ../../../final/evidence/raw/messaging-outbox-jdbc-postgresql-c01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-outbox-jdbc-postgresql#L66 이다. -module: messaging-outbox-jdbc-postgresql ---- - -# 모르는 것을 실패로 취급하지 않는다 - -트랜잭셔널 아웃박스의 PostgreSQL 구현이다. 이 리프의 축은 하나다 — 모호한 발행을 같은 메시지 id로 재시도한다. - -## 본문 - - - -**트랜잭셔널 아웃박스의 PostgreSQL 구현**이다. 비즈니스 트랜잭션이 쓰고 릴레이가 배출한다. 이 리프의 축은 하나다: **"모르는 것을 실패로 취급하지 않는다."** - -> "The relay's correctness rests on one rule: an ambiguous publish is retried **under the same message id**. Minting a new id would turn a possibly-delivered message into a definitely-second message, and no downstream deduplication could recover from it. Marking it failed instead would lose a message the broker may already hold." - -마지막 문장이 중요하다 — 릴레이는 at-least-once publication만 보장한다고 자기 상한을 스스로 명시한다. - -## 이 리프가 아는 것과 모르는 것 - -:::evidence key="messaging-outbox-jdbc-postgresql-c01-diagram" alt="리프 경계 안에 발행 포트와 관계형 데이터베이스가 들어 있고 브로커와 스프링 컨텍스트가 경계 밖 점선 상자로 놓인 구조" caption="이 리프가 아는 것과 모르는 것" zoom="false" -::: - -경계: 브로커를 모른다(`MessagePublisher` 포트만 안다). 스프링 컨텍스트를 모른다(`spring-jdbc`/`spring-tx` 는 `implementation` 이며 트랜잭션 동기화 조회에만 쓴다). 배선은 starter 몫이다. - -## MessagePublisher 참조 위치 - -:::evidence key="messaging-outbox-jdbc-postgresql-c01" alt="코드베이스에서 MessagePublisher 를 검색한 출력 14줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagePublisher 코드베이스 검색 — 14줄 · exit 0" zoom="true" -::: - -## 파괴적 작업 저널이 같이 사는 이유 - -`messaging-admin-api` 의 파괴적 작업 저널 구현도 이 리프에 산다. build.gradle 이 그 이유를 적는다 — "The destructive-operation journal lives here because it needs exactly what the outbox needs: one relational database every replica can see, and a migration lane that already exists. The contract it implements belongs to the admin API." - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-messaging-outbox-jdbc-postgresql-c02.md b/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-messaging-outbox-jdbc-postgresql-c02.md deleted file mode 100644 index ef1425a..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-messaging-outbox-jdbc-postgresql-c02.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-outbox-jdbc-postgresql-c02 -title: starter가 만들지 않는 셋은 애플리케이션이 직접 등록해야 한다 -topic: transaction-and-consistency-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-outbox-jdbc-postgresql-c02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-outbox-jdbc-postgresql-c02 - file: ../../../final/evidence/rendered/messaging-outbox-jdbc-postgresql-c02.svg -evidence: - - ../../../final/evidence/raw/messaging-outbox-jdbc-postgresql-c02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-outbox-jdbc-postgresql#L102 이다. -module: messaging-outbox-jdbc-postgresql ---- - -# starter가 만들지 않는 셋은 애플리케이션이 직접 등록해야 한다 - -`JdbcOutboxRepository`, `OutboxEnvelopeFactory`, `JdbcAdminOperationJournal` 셋은 starter가 만들지 않는다. 애플리케이션이 `DataSource`/`ProducerId`를 알고 직접 등록해야 한다. - -## 본문 - - - -testcontainers 주석이 이 리프의 성격을 요약한다 — "신뢰성 패턴은 트랜잭션 경계와 유일성 제약에 대한 주장이고, 그것을 결판낼 수 있는 것은 실제 데이터베이스뿐이다." 그리고 그 레인이 **실제로 돈다**(§10). - -## JdbcOutboxRepository 참조 위치 - -:::evidence key="messaging-outbox-jdbc-postgresql-c02" alt="코드베이스에서 JdbcOutboxRepository 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="JdbcOutboxRepository 코드베이스 검색 — 5줄 · exit 0" zoom="true" -::: - -## starter가 만들지 않는 셋 - -`JdbcOutboxRepository`, `OutboxEnvelopeFactory`, `JdbcAdminOperationJournal`(`EVD-312`). 셋 다 애플리케이션이 `DataSource`/`ProducerId` 를 알고 직접 등록해야 한다. - -## 기본 저널이 프로덕션에서 거부되는 이유 - -`AdminOperationJournal` 의 기본값은 `InMemoryAdminOperationJournal` 이며, 프로덕션 프로파일에서는 `MessagingAdminDurabilityValidator` 가 그것을 거부한다(`final/document.md#a19-messaging-admin-runtime` §4.4 참조). - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-messaging-outbox-jdbc-postgresql-c04.md b/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-messaging-outbox-jdbc-postgresql-c04.md deleted file mode 100644 index b581399..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-messaging-outbox-jdbc-postgresql-c04.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-outbox-jdbc-postgresql-c04 -title: 쓰기와 배출과 정리와 저널이 각각 다른 경로다 -topic: transaction-and-consistency-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-outbox-jdbc-postgresql-c04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-outbox-jdbc-postgresql-c04 - file: ../../../final/evidence/rendered/messaging-outbox-jdbc-postgresql-c04.svg -evidence: - - ../../../final/evidence/raw/messaging-outbox-jdbc-postgresql-c04.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-outbox-jdbc-postgresql#L430 이다. -module: messaging-outbox-jdbc-postgresql ---- - -# 쓰기와 배출과 정리와 저널이 각각 다른 경로다 - -네 실행 경로가 있고 각각 진입점과 종료 조건이 다르다. - -## 본문 - - - -네 실행 경로가 있고 각각 진입점과 종료 조건이 다르다. - -**쓰기** — 비즈니스 트랜잭션 → `append(record)` → 트랜잭션 3중 검사 → `DataSourceUtils.getConnection` → INSERT(22컬럼). - -**배출** — `MessagingOutboxRelayLifecycle` → `worker.start()` → `runPass()` → `relay.runOnce(now)` → 청구/발행/종결 → `scheduler.backoff(unproductive)` → 다음 패스 자기 스케줄링. - -## DataSourceUtils 참조 위치 - -:::evidence key="messaging-outbox-jdbc-postgresql-c04" alt="코드베이스에서 DataSourceUtils 를 검색한 출력 15줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DataSourceUtils 코드베이스 검색 — 15줄 · exit 0" zoom="true" -::: - -## 정리와 admin 저널 - -**정리** — `OutboxCleanupJob.runOnce(now)` → `cutoff = now - retention` → `purgePublishedBefore(cutoff)` **무제한 오버로드** ×(최대 `maxBatches`, 실제로는 2회) → §12.1(a). - -**admin 저널** — `begin` → INSERT ON CONFLICT / TAKE_OVER → `checkpoint` × N → `complete` 또는 `fail`. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-messaging-outbox-jdbc-postgresql-c06.md b/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-messaging-outbox-jdbc-postgresql-c06.md deleted file mode 100644 index ed25222..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transaction-and-consistency-models/concept/concept-messaging-outbox-jdbc-postgresql-c06.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -kind: CONCEPT -slug: messaging-outbox-jdbc-postgresql-c06 -title: 만들어졌지만 아무도 부르지 않는다를 이름 붙인 유일한 자리 -topic: transaction-and-consistency-models -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: concept:messaging-outbox-jdbc-postgresql-c06 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-outbox-jdbc-postgresql-c06 - file: ../../../final/evidence/rendered/messaging-outbox-jdbc-postgresql-c06.svg -evidence: - - ../../../final/evidence/raw/messaging-outbox-jdbc-postgresql-c06.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-outbox-jdbc-postgresql#L767 이다. -module: messaging-outbox-jdbc-postgresql ---- - -# 만들어졌지만 아무도 부르지 않는다를 이름 붙인 유일한 자리 - -SQL 마이그레이션과 javadoc이 함께 이력을 이룬다. 여덟 개의 "이전에는 이랬다" 중 마지막 둘이 이 저장소에서 반복되는 결함 계열을 명시적으로 이름 붙인다. - -## 본문 - - - -SQL 마이그레이션과 javadoc 이 함께 이력을 이룬다. 여덟 개의 "이전에는 이랬다". - -| 위치 | 기록된 과거 결함 | -|---|---| -| `V2:3-14` | 리스만으로는 stale relay 가 PUBLISHED 위에 AMBIGUOUS 를 덮어썼다 | -| `V2:25-27` | "V1's CHECK listed five states, so writing the sixth failed at the constraint rather than at review" | -| `V4:6-9` | 정경 필드가 갈 곳이 없어 유실되거나 `msg.*` 로 밀반입되었다 | -| `V4:56-61` | Debezium 키가 `destination` 이라 한 토픽의 모든 메시지가 한 파티션에 몰렸다 | -| `JdbcOutboxRepository:205-209` | `append` 가 풀에서 raw 커넥션을 열어 자동 커밋했다 — "a business transaction that rolled back afterwards left the event behind" | -| `JdbcOutboxRepository:630-636` | 이스케이프가 역슬래시와 따옴표만 처리해 제어문자가 JSONB 를 깨뜨렸다 | -| `OutboxRelay:117-123` | "The scheduler was built by the auto-configuration and handed to nobody" | -| `OutboxRelayWorker:18-21` | "The relay, its retry scheduler and the attempt budget all existed and nothing ever called `runOnce`" | - -## 이 기록이 다루는 파일 범위 - -:::evidence key="messaging-outbox-jdbc-postgresql-c06" alt="코드베이스에서 파일 목록을 만든 출력 13줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 13줄 · exit 0" zoom="true" -::: - -## 결함 계열에 이름이 붙은 유일한 자리 - -마지막 두 개(`OutboxRelay:117-123`, `OutboxRelayWorker:18-21`)가 이 저장소 전체에서 반복되는 결함 계열 — **"만들어졌지만 아무도 부르지 않는다"** — 을 명시적으로 이름 붙인 유일한 자리다. 그리고 이 리프에서는 그 둘이 실제로 고쳐졌다. §12.1(c)의 `requireExactlyOneRelay` 만 같은 상태로 남았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transaction-deadline-and-pool/case/case-a-five-second-string-that-broke-every-prod-deploy.md b/docs/clean-architecture-backend-template/tech-log-studio/transaction-deadline-and-pool/case/case-a-five-second-string-that-broke-every-prod-deploy.md index a7b9448..abe8f9b 100644 --- a/docs/clean-architecture-backend-template/tech-log-studio/transaction-deadline-and-pool/case/case-a-five-second-string-that-broke-every-prod-deploy.md +++ b/docs/clean-architecture-backend-template/tech-log-studio/transaction-deadline-and-pool/case/case-a-five-second-string-that-broke-every-prod-deploy.md @@ -1,7 +1,7 @@ --- kind: CASE slug: a-five-second-string-that-broke-every-prod-deploy -title: 'connection-timeout: 5s가 모든 prod 배포를 시작 실패시켰고 local만 통과했다' +title: connection-timeout: 5s가 모든 prod 배포를 시작 실패시켰고 local만 통과했다 topic: transaction-deadline-and-pool project: clean-architecture-backend-template status: 게시 전 diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transaction-deadline-and-pool/case/case-a-validator-checking-the-wrong-datasource.md b/docs/clean-architecture-backend-template/tech-log-studio/transaction-deadline-and-pool/case/case-a-validator-checking-the-wrong-datasource.md deleted file mode 100644 index e21dd3b..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transaction-deadline-and-pool/case/case-a-validator-checking-the-wrong-datasource.md +++ /dev/null @@ -1,167 +0,0 @@ ---- -kind: CASE -slug: a-validator-checking-the-wrong-datasource -title: validator가 요청을 서비스하지 않는 datasource를 검증하고 있었다 -topic: transaction-deadline-and-pool -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a-validator-checking-the-wrong-datasource -evidenceCapturedOn: 2026-09-03 -assets: - - key: a-validator-checking-the-wrong-datasource - file: ../../../final/evidence/rendered/a-validator-checking-the-wrong-datasource.svg - - key: a-validator-checking-the-wrong-datasource-run - file: ../../../final/evidence/rendered/a-validator-checking-the-wrong-datasource-run.svg -evidence: - - ../../../final/evidence/raw/a-validator-checking-the-wrong-datasource.txt - - ../../../final/evidence/raw/a-validator-checking-the-wrong-datasource-run.txt -source: - - 이 사건의 1차 기록은 두 곳이다. 하나는 `JpaDataSourceProfileValidator` 와 `JpaResolvedDataSourceValidationTest` 두 클래스의 자바독이고, 다른 하나는 저장소 자신의 설계 문서 `docs/superpowers/specs/2026-08-15-five-adapter-runtime-remediation-review-design.md:307` 의 `JPA-INT-002` 항목이다. - - root-tree 노드가 가리키는 `final/document.md#a05 §14.5` 는 지금 그 파일에 없다. `final/document.md#a05` 에서 `14.5` 는 0 건이고, 그 문서에 이 검증기와 병렬 네임스페이스가 나오지 않는다. - - 원본 상류에 없는 것이 둘이다. `validateResolved` 를 부르는 프로덕션 코드가 지금은 하나 있다는 것과, 그 호출자가 발행된 빈을 쓰지 않고 자기 인스턴스를 만든다는 것이다. ---- - -# validator가 요청을 서비스하지 않는 datasource를 검증하고 있었다 - -`JpaDataSourceProfileValidator` 의 자바독에 두 결함이 적혀 있다. 이 검증기가 `app.jpa-platform.datasource.*` 를 읽었는데 요청을 받는 풀은 `spring.datasource.hikari.*` 에서 만들어졌다는 것, 그리고 그 병렬 네임스페이스가 어떤 출하 설정에도 없어 두 필드가 늘 널이었다는 것이다. 널에서 던지는 `requirePoolBounds` 를 부르는 코드가 없어서 애플리케이션은 그대로 기동했다. - -## 관계 - -- **connection-timeout: 5s가 모든 prod 배포를 시작 실패시켰고 local만 통과했다** - 둘 다 `spring.datasource.hikari.*` 를 두고 벌어진 일이다. 저쪽은 그 값을 바인더가 거부했고, 여기는 다른 네임스페이스를 읽느라 그 값을 보지 못했다. -- **중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다** - 풀을 만드는 네임스페이스와 검증기가 읽는 네임스페이스가 다르면, 검증기는 배포가 쓰지 않는 값을 보고 통과한다. 어느 쪽이 풀을 만드는지 먼저 확인해야 그 통과가 무엇에 대한 것인지 알 수 있다. -- **@Bean이 있다는 것은 조립 증거가 아니다** - 이 타입의 빈 정의가 하나 있는데 그것을 주입받는 프로덕션 코드가 없고, 실제 검사는 호출자가 직접 만든 객체가 한다. - -## 문제 - -검증기는 배포가 실제로 향하는 데이터소스를 봐야 한다. - -그런데 상한 값의 출처는 JpaDataSourceSettings 였다. 그것이 바인딩하는 접두는 app.jpa-platform.datasource 이고, 풀을 만드는 접두는 spring.datasource.hikari 다. - -## 결론 - -자바독은 두 결함이 서로를 상쇄했다고 적는다. - -첫째, JpaDataSourceProfileValidator 가 배포가 쓰지 않는 네임스페이스를 읽었다. 둘째, 그 네임스페이스는 출하 YAML 과 환경 키 레지스트리 어디에도 없어 두 필드가 늘 널이었다. requirePoolBounds 가 불렸다면 모든 배포가 기동에 실패했겠지만, 그것을 부르는 코드가 없었다. - -지금은 JpaDataSourceSettings 클래스가 없고 설정 파일에서도 0 건이다. src 에 남은 일곱 자리 중 여섯은 자바독이고 하나는 회귀 테스트가 쓰는 검색 리터럴이다. 옛 메서드 이름 셋도 전부 자바독뿐이다. - -다만 src 밖이 남았다. infra/jpa/postgres/README.md:18 이 운영자에게 max_connections 를 이제 없는 프로퍼티에 맞춰 잡으라고 지시한다. - -validateResolved 는 해석된 데이터소스를 받아 널을 검사하고, 벤더 선택이 PostgreSQL 일 때만 커넥션을 열어 제품과 버전을 본다. 열지 못하면 던지므로 도달할 수 없는 데이터베이스가 첫 질의가 아니라 기동에서 실패한다. - -여기서 원본 상류에 없는 것이 나온다. - -지금은 그 검사를 부르는 프로덕션 코드가 있다. PersistenceJpaRootAutoConfiguration:113 한 자리이고, 그 자동설정은 ca-skeleton.persistence-jpa.enabled 뒤에 있으며 출하 기본값은 거짓이다. 그 메서드는 발행된 빈을 주입받지 않고 :109 에서 자기 인스턴스를 만드는데, 이유로 적힌 조건은 이미 제거됐다. 이 타입의 빈 정의는 JpaPlatformRuntimeAutoConfiguration:112 하나이고 그것이 JpaPlatformAutoConfiguration:49 에 위임한다. 그 빈은 어디에도 주입되지 않는다. - -풀 상한 검사는 의도적으로 여기서 하지 않는다. 다만 HikariPoolConstraintValidator 가 읽는 다섯 키는 타임아웃과 수명과 누수 임계이고, maximum-pool-size 하한은 RuntimeNumericBoundsValidator:22 가 본다. 자바독이 함께 적은 이득 하나는 지금 성립하지 않는다 — HikariCP 는 선언만 testImplementation 이고 runtimeClasspath 에는 전이로 올라온다. - -회귀 테스트가 그 부재를 고정한다. 훑을 목록이 비어 있지 않다는 것을 먼저 단언하고 주석을 걷어낸 뒤 검사하되, 훑는 범위는 두 모듈이다. 세 건을 돌려 전부 통과하는 것을 확인했다. - -상류 어디에도 등급이 없다. root-tree 노드에도 candidate ledger 에도 설계 문서의 JPA-INT-002 항목에도 없어서, 이 기록도 새로 매기지 않는다. 두 결함은 닫혀 있고 남은 것은 관찰이다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Spring Boot : 4.0.8 -확인 방식 : 자바독의 사후 기록 확인, app.jpa-platform.datasource 와 requirePoolBounds 의 잔존 자리 전수, validateResolved 본문과 그 프로덕션 호출자·조건 확인, 이 타입의 빈 정의와 주입처 계수, runtimeClasspath 전개, 회귀 테스트 실행 -소스 수정 : x - -## 재현 조건 - -1. JpaDataSourceProfileValidator 의 클래스 자바독을 읽는다. 두 결함과 그 상쇄가 적혀 있다. -2. app.jpa-platform.datasource 를 src 안팎에서 각각 찾고, 설정 파일과 바인딩 클래스 쪽을 따로 센다. -3. 남은 히트가 주석인지 코드인지 소스 세트별로 가른다. -4. requirePoolBounds 가 남은 자리를 센다. -5. validateResolved 가 무엇을 읽고 어떤 조건에서 무엇을 던지는지 본문에서 읽는다. -6. 그것을 부르는 자리를 main 과 test 로 갈라 세고, 그 자동설정이 붙는 프로퍼티와 출하 기본값을 확인한다. -7. 그 호출자가 검증기를 주입받는지 직접 만드는지 보고, 직접 만드는 이유로 적힌 조건이 지금도 있는지 확인한다. -8. 이 타입의 @Bean 정의를 모두 찾고 그것이 무엇에 위임하는지 확인한 뒤, 그 빈을 받는 코드를 센다. -9. HikariPoolConstraintValidator 가 읽는 키를 나열하고, maximum-pool-size 를 검사하는 코드를 따로 찾는다. -10. 이 모듈의 HikariCP 선언 구성과 runtimeClasspath 전개 결과를 대조한다. -11. 회귀 테스트의 단언과 가드와 스캔 범위를 읽고, 그 테스트를 실행해 결과를 읽는다. - -## 본문 - - - -`JpaDataSourceProfileValidator` 의 클래스 자바독에는 이 검증기가 예전에 다른 데이터소스를 검증했다는 내용이 먼저 적혀 있다. - -## 자바독이 적은 두 결함과 그 상쇄 - -:::evidence key="a-validator-checking-the-wrong-datasource" alt="저장소 루트에서 돌린 정적 검색 출력 158줄. JpaDataSourceProfileValidator 11행부터 19행까지의 자바독이 두 결함과 상쇄를 원문 그대로 적는다. 이어서 app.jpa-platform.datasource 가 src 안에 남아 있는 일곱 자리가 main 다섯과 test 둘로 나오고, src 밖에서 그 문자열을 쓰는 파일 다섯과 줄 수, 그중 infra 의 운영자용 표에 살아 있는 줄이 이어진다. 설정 파일에서는 0 건이고 주석이 아닌 줄은 main 0 test 1 이며 JpaDataSourceSettings 클래스도 0 건이다. requirePoolBounds 가 남은 세 자리는 전부 자바독이다. 그다음 validateResolved 의 본문 50행부터 66행, 그 호출자가 붙는 프로퍼티 조건과 출하 기본값 false, 직접 생성의 이유로 적힌 조건과 그 조건이 지금 제거됐다는 사슬, 호출자가 자기 인스턴스를 만드는 106행부터 114행이 나온다. 이 타입의 빈 정의는 하나이고 JpaPlatformAutoConfiguration 의 @Bean 개수는 0 이며 그 빈을 받는 프로덕션 코드는 0 건이다. 끝으로 HikariPoolConstraintValidator 가 읽는 다섯 키와 그 빈, maximum-pool-size 를 실제로 보는 다른 검증기, HikariCP 가 선언은 testImplementation 인데 runtimeClasspath 에는 올라온다는 출력, 회귀 테스트 세 건의 이름과 그 스캔 범위와 공허한 통과를 막는 단언이 보인다." caption="자바독의 두 결함 · 네임스페이스 잔존 위치와 설정 0 · validateResolved 의 본문과 조건 · 빈 정의 하나와 주입 0 · 풀 키의 실제 소유자 · HikariCP 의 선언과 런타임 · 회귀 테스트 셋 — 158줄 · exit 0" zoom="true" -::: - -첫 번째 결함으로 자바독이 적는 것은 읽는 대상의 어긋남이다. 상한 값은 `JpaDataSourceSettings` 가 `app.jpa-platform.datasource.*` 에 바인딩한 설정에서 왔고, 요청을 처리하는 풀은 `spring.datasource.hikari.*` 에서 만들어졌다. 자바독은 이것을 풀 하나에 설명이 둘인 상태로 적고, 그러면 검증기가 실제로 쓰이지 않는 쪽 설명을 보고 통과할 수 있다고 적는다. - -두 번째가 더 나쁘다고 자바독은 적는다. 그 병렬 네임스페이스는 어떤 출하 YAML 에도, 환경 키 레지스트리의 어떤 행에도 없었다. 두 필드가 항상 널이었고 `requirePoolBounds` 는 널에서 던진다. 무언가 그것을 불렀다면 모든 배포가 기동에 실패했을 것이다. - -아무것도 그것을 부르지 않았고, 그래서 아무것도 실패하지 않았다. 자바독은 여기까지를 두 결함이 서로를 상쇄한 것으로 적고, 애플리케이션이 시작된 이유를 두 번째 결함이 첫 번째를 가린 것으로 적는다. - -## app.jpa-platform.datasource 가 지금 남아 있는 자리 - -`src` 안에서 이 문자열을 찾으면 일곱 자리가 나온다. 설정 파일에서는 0 건이고, 그것을 바인딩하던 `JpaDataSourceSettings` 클래스도 없다. - -main 다섯은 전부 자바독이다. 그중 넷은 이 검증기가 아니라 다른 리프의 것으로, `NotificationSmtpProviderConfig:48` 과 `NotificationSmtpSettings:14` 와 `SmtpProviderRuntimeAssembler:40` 이 자기 결정의 이유를 대면서 이 사건을 선례로 인용하고, `KafkaMessagingAutoConfiguration:119` 도 브로커 주소를 두 번 기술하지 않는 이유로 같은 것을 든다. 나머지 하나가 검증기 자신의 `:12` 다. - -test 둘 중 하나는 자바독이고, 나머지 하나는 `JpaResolvedDataSourceValidationTest:57` 의 `.contains("app.jpa-platform.datasource")` — 회귀 테스트가 이 문자열을 찾을 때 쓰는 검색 리터럴이다. - -`src` 밖에도 다섯 파일이 이 문자열을 쓴다. 넷은 설계 문서와 계획 문서지만 하나는 다르다. `infra/jpa/postgres/README.md:18` 이 운영자용 표에서 `max_connections` 를 `app.jpa-platform.datasource.maximum-pool-size` 에 맞춰 잡으라고 지금도 지시한다. 그 프로퍼티는 더 이상 없다. - -옛 상한 검사 메서드 이름도 셋 남았는데 역시 전부 자바독이다. 검증기 자신의 `:17` 과 회귀 테스트의 `:26`·`:29` 다. 부를 수 있는 메서드로는 남아 있지 않다. - -## validateResolved 가 읽는 값과 던지는 조건 - -`validateResolved(DataSource, boolean)` 는 `DataSource` 널 검사를 먼저 하고, 벤더 선택이 PostgreSQL 이 아니면 그대로 돌아온다. 그 자리 주석이 이유를 적는다 — 로컬 개발은 설계상 H2 로 돌고 프로덕션에서 그것을 막는 것은 `PersistenceVendorProdSafetyValidator` 이며, 여기서도 PostgreSQL 을 요구하면 모든 노트북을 거절하게 된다. - -PostgreSQL 이 선택된 배포에서만 커넥션을 열어 `versionPolicy.requireStable` 에 메타데이터를 넘긴다. 열지 못하면 `IllegalStateException` 을 던지고, 메시지는 `"A context that starts without this check reports healthy and fails on whoever sends the first request."` 다. - -## validateResolved 의 프로덕션 호출자 하나와, 발행된 빈 - -`validateResolved` 를 부르는 프로덕션 코드는 `PersistenceJpaRootAutoConfiguration:113` 하나다. `@Bean InitializingBean jpaResolvedDataSourceCheck` 가 해석된 `DataSource` 와 `Environment` 를 받아, 벤더 프로퍼티를 읽고 그 값이 `postgresql` 인지를 두 번째 인자로 넘기는 람다를 돌려준다. - -이 자동설정은 `:57`\~`:60` 의 `@ConditionalOnProperty(prefix = "ca-skeleton.persistence-jpa", name = "enabled", havingValue = "true")` 뒤에 있고 `application.yml:364` 의 출하 기본값은 `false` 다. JPA 마스터 스위치를 켠 배포에서만 이 검사가 돈다. - -그 메서드는 검증기를 주입받지 않는다. `:109`\~`:110` 에서 `new JpaDataSourceProfileValidator(new PostgreSqlVersionPolicy())` 로 자기 인스턴스를 만든다. 이유도 `:95`\~`:99` 에 적혀 있다. 빈을 내놓는 클래스가 `@ConditionalOnBean(DataSource.class)` 를 달고 있어 실제 애플리케이션에서 조용히 빠지므로, 그 빈에 의존하면 이 검사도 같은 이유로 사라진다는 것이다. - -그 이유는 지금 성립하지 않는다. `JpaPlatformRuntimeAutoConfiguration` 의 자바독 `:57`\~`:58` 은 이 클래스가 마스터 스위치를 이미 든 루트 아래에 있어 데이터소스 존재 여부가 파싱 시점에 이미 답해져 있다고 적고, 그 조건은 제거됐다. - -이 타입을 빈으로 정의하는 자리는 하나다. `JpaPlatformRuntimeAutoConfiguration:112` 의 `@Bean` 이고, 그 메서드가 `JpaPlatformAutoConfiguration:49` 의 `dataSourceProfileValidator()` 에 위임한다. 뒤쪽 클래스에는 `@Bean` 이 하나도 없다 — 자바독 `:19` 가 `app-bootstrap` 이 조립을 소유한다는 규칙 때문에 평범한 생성으로 둔다고 적는다. - -그 빈을 파라미터나 필드로 받는 프로덕션 코드는 0 건이다. 검사는 도는데, 도는 것은 발행된 빈이 아니라 호출자가 직접 만든 객체다. - -## 풀 크기 상한을 읽는 것은 또 다른 검증기다 - -자바독은 풀 상한을 여기서 보지 않는 결정을 굵게 적으면서 `HikariPoolConstraintValidator` 를 지목한다. 그 검증기가 실제로 읽는 다섯 키는 `connection-timeout`, `validation-timeout`, `keepalive-time`, `max-lifetime`, `leak-detection-threshold` 이고, `RuntimeSafetyConfig:49` 의 `@Bean` 이 그것을 내놓는다. - -옛 `requirePoolBounds` 가 널에서 던지던 `maximum-pool-size` 는 그쪽이 아니라 `RuntimeNumericBoundsValidator:22` 가 하한 1 로 본다. `minimum-idle` 은 `:26` 이다. - -자바독은 이 클래스가 풀 타입 이름을 대지 않게 되어 HikariCP 가 `app-bootstrap` 의 프로덕션 클래스패스에서 빠진다고도 적는다. 선언은 그렇다 — `app-bootstrap/build.gradle:140` 이 `testImplementation` 하나뿐이다. 그런데 `runtimeClasspath` 를 풀어 보면 `com.zaxxer:HikariCP:7.0.2` 가 올라와 있다. `implementation project(':adapter:outbound:persistence-jpa')` 를 타고 `spring-boot-starter-data-jpa` 가 끌어온 것이라, 자바독이 적은 그 이득은 지금 성립하지 않는다. - -## 회귀 테스트가 공허한 통과까지 막는다 - -:::evidence key="a-validator-checking-the-wrong-datasource-run" alt="gradle 실행 출력 6줄. PIPESTATUS 로 읽은 gradle 종료 코드 0 이 먼저 찍히고, JpaResolvedDataSourceValidationTest 스위트의 JUnit 결과가 tests 3, failures 0, errors 0, skipped 0 으로 나온다. 이어서 세 테스트가 각각 통과로 표시되는데 도달할 수 없는 데이터베이스가 기동에서 실패하는지, 제품 검사가 벤더 선택을 따르는지, 프로덕션 코드가 병렬 네임스페이스를 바인딩하지 않는지 셋이다." caption="회귀 테스트 세 건 실행 결과 — 6줄 · exit 0" zoom="true" -::: - -`theParallelNamespaceIsGone`(\:46)이 소스를 훑어 병렬 바인딩이 없다고 단언한다. 그 앞에 단언이 하나 더 있다. 훑은 목록이 비어 있지 않다는 것(\:49\~\:51)이고, 그 단언의 문구가 이유를 적는다 — 아무 소스에도 닿지 못한 스캔은 모든 바인딩을 없다고 보고한다. - -`:53`\~`:54` 의 주석이 왜 주석을 걷어내고 검사하는지도 적는다. 네임스페이스를 퇴역시켰다고 기록한 문장은 결함의 반대이기 때문이다. - -다만 이 테스트가 잡을 수 있는 범위는 두 모듈이다. `productionSources()`(\:115\~\:119)가 `app-bootstrap` 과 `adapter/outbound/persistence-jpa` 만 훑는다. 위에서 센 다른 리프 넷은 애초에 스캔 대상 밖이라, 그것들이 이 테스트를 깨뜨리지 않는 이유는 주석 제거만이 아니다. - -나머지 둘은 부재가 아니라 지금 동작을 고정한다. `anUnreachableDatabaseFailsAtStartup`(\:71)이 죽은 포트를 가리키는 `HikariDataSource` 에 대해 커넥션을 열지 못한다는 메시지를 요구하고, `theProductCheckFollowsTheVendorSelector`(\:92)가 H2 를 벤더 선택이 거짓일 때는 통과시키고 참일 때는 거절한다고 단언한다. - -돌리면 세 건이 통과한다. - -## 확인하지 못한 것 - -결함이 있던 시점의 코드를 직접 열어 보지 않았다. 두 결함을 적어 둔 곳은 자바독과 위 설계 문서이고, 지금의 상태만 계수와 실행으로 확인했다. - -주입처가 0 건이라는 판정의 근거는 이름 기반 정적 검색이다. 빈 이름으로 찾아 쓰거나 리플렉션으로 꺼내는 경로까지 배제하지는 못했다. - -결함이 있던 시점의 코드를 직접 열어 보지 않았다. 두 결함을 적어 둔 곳은 자바독과 위 설계 문서이고, 지금의 상태만 계수와 실행으로 확인했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transaction-deadline-and-pool/reference/reference-deadline-narrows-in-three-stages.md b/docs/clean-architecture-backend-template/tech-log-studio/transaction-deadline-and-pool/reference/reference-deadline-narrows-in-three-stages.md deleted file mode 100644 index c6ed5f6..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transaction-deadline-and-pool/reference/reference-deadline-narrows-in-three-stages.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -kind: REFERENCE -slug: deadline-narrows-in-three-stages -title: 데드라인은 호출 예산에서 시작해 세 단계로 좁힌다 -topic: transaction-deadline-and-pool -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:deadline-narrows-in-three-stages -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# 데드라인은 호출 예산에서 시작해 세 단계로 좁힌다 - -## 목적 - -상위 호출자가 포기한 뒤에도 하위 작업이 계속 돌아 자원을 점유하는 것을 막는다. - -## 규칙 - -1. 출발점은 호출 예산이다 - 상위 호출자가 기다릴 수 있는 시간에서 시작한다. 하위가 스스로 정한 값에서 시작하지 않는다. - -2. 획득 시간을 뺀다 - 커넥션이나 슬롯을 얻는 데 든 시간은 이미 예산에서 소비된 것이다. 그것을 빼지 않으면 남은 시간이 없는 채로 작업을 시작한다. - -3. 각 단계는 앞 단계보다 작다 - 호출 예산보다 트랜잭션 타임아웃이 작고, 그보다 데이터베이스 로컬 타임아웃이 작다. - -4. 가장 안쪽이 먼저 끊는다 - 데이터베이스가 먼저 끊어야 애플리케이션이 그 실패를 분류할 기회를 갖는다. 반대면 쿼리가 결과를 받을 사람 없이 계속 돈다. - -5. 계산을 자기 타입에 둔다 - 값이 여러 곳에서 계산되면 그중 하나가 획득 시간을 빼는 것을 잊는다. - -## 적용 조건 - -트랜잭션과 원격 호출처럼 시간이 걸리는 모든 하위 작업 - -풀에서 자원을 얻어 쓰는 경로 - -## 예외 - -배치나 백그라운드 작업처럼 상위 호출자가 없는 경우는 예산의 출발점이 다르다. 그때는 그 작업 자체의 상한이 출발점이다. - -## 예시 - -데드라인 계산기가 획득 봉투를 포함하는 형태와 포함하지 않는 형태를 나눠 갖는다. - -로컬 타임아웃을 거는 방법은 데이터베이스마다 달라서 설정기가 따로 있다. - -## 관계 - -- **호출 예산에서 DB 로컬 타임아웃까지의 데드라인 전파** - 이 규칙이 나온 개념이다. -- **쓰기 트랜잭션에는 유한 타임아웃이 필수다** - 마지막 단계가 비어 있을 때의 규칙이다. -- **재시도 가능성은 멱등성과 실패 범주를 함께 봐야 정해진다** - 데드라인이 재시도 판정의 입력이 되는 지점이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transaction-deadline-and-pool/reference/reference-session-scoped-settings-outlive-the-transaction.md b/docs/clean-architecture-backend-template/tech-log-studio/transaction-deadline-and-pool/reference/reference-session-scoped-settings-outlive-the-transaction.md deleted file mode 100644 index 8405dfd..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transaction-deadline-and-pool/reference/reference-session-scoped-settings-outlive-the-transaction.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -kind: REFERENCE -slug: session-scoped-settings-outlive-the-transaction -title: 세션 스코프 설정은 풀로 돌아간 커넥션에 남는다 -topic: transaction-deadline-and-pool -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:session-scoped-settings-outlive-the-transaction -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# 세션 스코프 설정은 풀로 돌아간 커넥션에 남는다 - -## 목적 - -한 트랜잭션을 위해 건 세션 설정이 커넥션과 함께 풀로 돌아가, 무관한 다음 작업에 적용되는 것을 막는다. - -## 규칙 - -1. 세션 스코프와 트랜잭션 스코프를 구별한다 - 세션 스코프로 설정하면 커넥션이 살아 있는 동안 유지된다. 트랜잭션이 끝나도 사라지지 않는다. - -2. 가능하면 로컬 스코프를 쓴다 - 트랜잭션 로컬로 설정하면 커밋이나 롤백과 함께 사라진다. - -3. 로컬이 불가능하면 반납 전에 되돌린다 - 설정을 건 쪽이 그것을 지우는 책임을 갖는다. 다음 사용자가 지울 것이라고 가정하지 않는다. - -4. 데이터베이스마다 방법이 다르다 - 같은 개념의 설정이라도 문법과 스코프가 다르므로 설정기를 데이터베이스별로 둔다. - -5. 테넌트나 사용자 컨텍스트를 세션에 남기지 않는다 - 커넥션이 풀에서 재사용되면 다른 테넌트의 요청이 앞 요청의 컨텍스트를 물려받는다. - -## 적용 조건 - -커넥션 풀을 쓰는 모든 데이터베이스 접근 - -로컬 타임아웃 검색 경로 역할 테넌트 컨텍스트 같은 세션 설정 - -## 예외 - -풀을 쓰지 않고 요청마다 새 커넥션을 여는 구성은 이 규칙의 대상이 아니다. 그 경우 다른 비용이 든다. - -## 예시 - -로컬 타임아웃 설정기가 PostgreSQL 과 H2 로 나뉘어 있다. 설정 문법이 다르고 세션에 남는 방식도 다르기 때문이다. - -멀티테넌시에서 테넌트 컨텍스트를 세션에 남기면 풀 재사용이 곧 테넌트 경계 위반이 된다. - -## 관계 - -- **호출 예산에서 DB 로컬 타임아웃까지의 데드라인 전파** - 로컬 타임아웃을 거는 지점이다. -- **connection-timeout이 5s 문자열로 출하되어 prod와 dev 배포가 전부 시작에 실패했다** - 같은 설정 계층의 다른 함정이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transaction-deadline-and-pool/reference/reference-write-transactions-need-a-finite-timeout.md b/docs/clean-architecture-backend-template/tech-log-studio/transaction-deadline-and-pool/reference/reference-write-transactions-need-a-finite-timeout.md deleted file mode 100644 index d41a117..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transaction-deadline-and-pool/reference/reference-write-transactions-need-a-finite-timeout.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -kind: REFERENCE -slug: write-transactions-need-a-finite-timeout -title: 쓰기 트랜잭션에는 유한 타임아웃이 필수다 -topic: transaction-deadline-and-pool -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:write-transactions-need-a-finite-timeout -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 ---- - -# 쓰기 트랜잭션에는 유한 타임아웃이 필수다 - -## 목적 - -타임아웃 없는 쓰기 트랜잭션이 잠금을 무한히 들고 있어, 다른 쓰기 전부를 막는 것을 방지한다. - -## 규칙 - -1. 쓰기 경로에 무한 대기를 두지 않는다 - 타임아웃이 없으면 잠금 대기가 끝나지 않는다. - -2. 기본값을 무한으로 두지 않는다 - 설정하지 않았을 때의 동작이 무한 대기면, 설정을 잊은 배포가 가장 위험한 배포가 된다. - -3. 읽기와 쓰기의 상한을 따로 둔다 - 읽기가 길어지는 것과 쓰기가 길어지는 것은 영향 범위가 다르다. - -4. 타임아웃 값을 타입으로 강제한다 - 설정 값이 비어 있을 수 있는 형태면 그 경로가 언젠가 무한이 된다. - -## 적용 조건 - -잠금을 잡는 모든 쓰기 트랜잭션 - -여러 인스턴스가 같은 행을 경합하는 구조 - -## 예외 - -관리자가 명시적으로 실행하는 일회성 마이그레이션이나 백필은 상한이 다를 수 있다. 그 경우 실행 절차에 그 사실이 있어야 한다. - -## 예시 - -트랜잭션 템플릿이 모드마다 미리 만들어져 있고 전부 같은 격리 수준에 고정되어 있다. 데드라인은 실행 시점에 계산되어 전달된다. - -풀 커넥션 타임아웃은 기본 30 초 대신 짧은 값으로 고정한다. 풀이 고갈된 스레드를 오래 붙잡지 않고 빠르게 거절하기 위해서다. - -## 관계 - -- **데드라인은 호출 예산에서 시작해 세 단계로 좁힌다** - 이 규칙이 속한 전파 구조다. -- **커밋 모호성 판정은 넓혀도 좁혀도 해롭다** - 타임아웃이 만드는 실패를 어떻게 분류할지 다룬 규칙이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-capability-constant-outlives-its-condition.md b/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-capability-constant-outlives-its-condition.md deleted file mode 100644 index 56e55a9..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-capability-constant-outlives-its-condition.md +++ /dev/null @@ -1,108 +0,0 @@ ---- -kind: CASE -slug: capability-constant-outlives-its-condition -title: 능력 상수가 프로파일 조건보다 오래 살아서 중복 제거 없는 목적지가 가드를 통과한다 -topic: transport-and-provider-semantics -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:capability-constant-outlives-its-condition -evidenceCapturedOn: 2026-09-01 -body: case-capability-constant-outlives-its-condition.body.md -assets: - - key: capability-constant-outlives-its-condition - file: ../../../final/evidence/rendered/capability-constant-outlives-its-condition.svg -evidence: - - ../../../final/evidence/raw/capability-constant-outlives-its-condition.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-nats-experimental#L111 이다. ---- - -# 능력 상수가 프로파일 조건보다 오래 살아서 중복 제거 없는 목적지가 가드를 통과한다 - -어댑터가 중복 제거 지원을 상수로 참이라 답한다. 실제 중복 제거 식별자는 프로파일에 창이 있을 때만 실린다. 창은 선택 사항이고 아무도 요구하지 않는다. 그리고 이 플래그는 부재가 예외를 만드는 유일한 능력이다. - -## 관계 - -- **같은 자동 설정 안에서 검증기 하나만 감싸이지 않아 트랜잭션 조건이 검사되지 않는다** - 같은 통독에서 나온 짝이다. -- **지원 매트릭스가 코드와 반대를 말한다** - 같은 능력 표의 반대 방향 사례다. -- **조용히 약해진 보증은 사고 전까지 동작하는 것과 구분되지 않는다** - 능력 레코드의 자바독이 적은 근거다. - -## 문제 - -능력 레코드의 자바독이 계약을 선언한다. - -프로파일이 여기 없는 것을 요구하면 플랫폼이 크게 실패한다는 것이다. 조용히 저하되지 않는다는 것이고, 그 이유는 조용히 약해진 보증이 사고가 나기 전까지 동작하는 보증과 구분되지 않기 때문이라는 것이다. - -이 어댑터가 그 계약을 어떻게 답하는지 확인했다. - -## 결론 - -상수로 답한다. - -능력 값이 정적 최종 필드이고 프로파일을 보지 않는다. 그중 중복 제거 발행이 참이다. - -그런데 중복 제거 식별자는 프로파일에 창이 있을 때만 만들어진다. 창이 비어 있으면 식별자가 없고, 헤더가 실리지 않고, 서버는 중복을 제거하지 않는다. - -창은 선택 사항이다. 프로파일 생성자도 시작 검증기도 창을 요구하지 않는다. - -이 플래그가 특별한 이유가 있다. - -능력 열둘 중 부재가 예외를 만드는 유일한 것이다. 나머지는 읽히지 않거나 분기에만 쓰인다. - -그러므로 창 없는 목적지가 그 가드를 통과한다. 가드는 어댑터가 참이라 답했으니 통과시킨다. - -그리고 어댑터 자신이 그 조건을 알고 있다. - -클래스 자바독이 시간 초과 발행을 모호로 다루는 이유를 적으면서, 그 모호를 재시도해도 안전하게 만드는 것은 중복 제거 창이며 프로파일이 그것을 켰을 때라고 적는다. - -프로파일이 그것을 켰을 때라는 조건이 정확히 능력이 담지 않은 것이다. - -창이 없는 목적지에서 모호를 재시도하면 스트림에 같은 메시지가 두 번 들어간다. - -## 검증 환경 - -확인 방식 : 능력 상수와 식별자 생성 경로 대조, 프로파일 생성자와 검증기의 요구 확인 -소스 수정 : x - -## 재현 조건 - -원문은 document-detail 의 final/document.md#a19-messaging-nats-experimental 에 있다. - -1. 어댑터의 능력 상수를 읽고 각 성분의 의미를 레코드 문서에서 확인한다. -2. 중복 제거 식별자를 만드는 메서드를 읽는다. -3. 프로파일의 창 필드가 선택 사항인지 확인한다. -4. 생성자와 검증기가 창을 요구하는지 확인한다. -5. 그 플래그의 부재가 어디서 예외를 만드는지 확인한다. - -## 본문 - - - -어댑터가 `deduplicatedPublish=true` 를 상수로 답한다. 그런데 중복 제거 식별자는 프로파일에 창이 있을 때만 만들어지고, 창은 선택 사항이며 생성자도 검증기도 요구하지 않는다. - -## 중복 제거 식별자가 만들어지는 조건 - -:::evidence key="capability-constant-outlives-its-condition" alt="분석 문서 final/document.md#a19-messaging-nats-experimental 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a19-messaging-nats-experimental 발췌 — 15줄" zoom="true" -::: - -## 창이 없으면 서버가 중복을 제거하지 않는다 - -`Nats-Msg-Id` 가 실리지 않기 때문이다. - -## 하필 그 플래그다 - -이 플래그는 능력 열둘 중 부재가 예외를 만드는 유일한 것이라, 창 없는 목적지가 그 가드를 통과한 뒤 모호 재발행에서 스트림에 같은 메시지를 두 번 넣는다. - -## 어댑터 자신의 javadoc 이 조건을 안다 - -재시도를 안전하게 만드는 것은 창이며 "프로파일이 그것을 켰을 때" 라고 적는다. - -## 확인하지 못한 것 - -창 없는 프로파일로 모호 재발행을 실행해 중복 저장을 재현하지 않았다. 이 리프는 실험 등급이고 배선 경로가 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-grpc-spring-boot-starter-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-grpc-spring-boot-starter-f02.md deleted file mode 100644 index af04355..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-grpc-spring-boot-starter-f02.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -kind: CASE -slug: grpc-spring-boot-starter-f02 -title: 자동 설정이 transport 를 읽지 않고 전송을 하드코딩한다 -topic: transport-and-provider-semantics -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-spring-boot-starter-f02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-spring-boot-starter-f02 - file: ../../../final/evidence/rendered/grpc-spring-boot-starter-f02.svg -evidence: - - ../../../final/evidence/raw/grpc-spring-boot-starter-f02.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-spring-boot-starter#L221 이다. -module: grpc-spring-boot-starter -priority: P3 ---- - -# 자동 설정이 transport 를 읽지 않고 전송을 하드코딩한다 - -GrpcPlatformProperties.transport 는 GrpcServerTransport 열거형이고 기본값이 NETTY_SHADED 다. 그 값을 자동 설정이 보지 않으므로 다른 값을 설정해도 만들어지는 프로파일은 같다. - -## 문제 - -GrpcPlatformProperties.transport 는 GrpcServerTransport 열거형이고 기본값이 NETTY_SHADED 다. - -그 값을 자동 설정이 보지 않으므로 다른 값을 설정해도 만들어지는 프로파일은 같다. - -## 결론 - -지금은 무해에 가깝다 — 기본값이 하드코딩된 것과 같고, 다른 값은 §17.1 때문에 거부되지도 않지만 반영되지도 않는다. - -그러나 설정 키가 존재하고 문서화되어 있으므로 운영자는 그것이 전송을 고른다고 읽는다. - -수정은 프로파일 팩토리를 transport 로 분기시키거나, 그 키를 검증 전용임을 자바독에 명시하는 것이다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : GrpcPlatformProperties 참조 19건 검색과 자동 설정이 transport 값을 읽는지 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-spring-boot-starter#L221 에 있다. - -## 본문 - - - -`GrpcPlatformProperties.transport` 는 `GrpcServerTransport` 열거형이고 기본값이 `NETTY_SHADED` 다. 그 값을 자동 설정이 보지 않으므로 다른 값을 설정해도 만들어지는 프로파일은 같다. - -## GrpcPlatformProperties 참조 위치 - -:::evidence key="grpc-spring-boot-starter-f02" alt="코드베이스에서 GrpcPlatformProperties 를 검색한 출력 19줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcPlatformProperties 코드베이스 검색 — 19줄 · exit 0" zoom="true" -::: - -## 지금은 무해에 가깝다 - -기본값이 하드코딩된 것과 같고, 다른 값은 §17.1 때문에 거부되지도 않지만 반영되지도 않는다. - -## 그래도 운영자는 그것이 전송을 고른다고 읽는다 - -설정 키가 존재하고 문서화되어 있다. 수정은 프로파일 팩토리를 `transport` 로 분기시키거나, 그 키를 검증 전용임을 자바독에 명시하는 것이다. - -## 확인하지 못한 것 - -다른 transport 값을 설정해 만들어지는 프로파일이 같은지 실행으로 확인하지 않았다. 자동 설정이 그 값을 읽지 않는다는 것으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-admin-runtime-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-admin-runtime-f02.md deleted file mode 100644 index cd6e62e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-admin-runtime-f02.md +++ /dev/null @@ -1,83 +0,0 @@ ---- -kind: CASE -slug: messaging-admin-runtime-f02 -title: 토폴로지 검증 스택이 두 벌이고 판정이 어긋난다 -topic: transport-and-provider-semantics -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-admin-runtime-f02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-admin-runtime-f02 - file: ../../../final/evidence/rendered/messaging-admin-runtime-f02.svg - - key: messaging-admin-runtime-f02-diagram - file: ../../../final/assets/diagrams/messaging-admin-runtime-f02.svg -evidence: - - ../../../final/evidence/raw/messaging-admin-runtime-f02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-admin-runtime#L910 이다. -module: messaging-admin-runtime -priority: P2 ---- - -# 토폴로지 검증 스택이 두 벌이고 판정이 어긋난다 - -Stack A 는 파티션 스케일업을 ADVISORY 로 두어 기동을 허용하고 그 근거를 명시한다. Stack B 는 같은 상황을 차이로 보고 기동을 거부한다. - -## 문제 - -Stack A 는 파티션 스케일업을 ADVISORY 로 두어 기동을 허용하고 그 근거를 명시한다. - -Stack B 는 같은 상황을 차이로 보고 기동을 거부한다. - -## 결론 - -둘 다 프로덕션 호출부가 0건이라 지금은 충돌하지 않지만, final/document.md#a19-messaging-admin-api §17 첫 항목대로 토폴로지 검증을 기동에 배선하는 순간 어느 스택을 배선하느냐가 스케일업한 배포의 기동 여부를 가른다. - -Stack A 가 남아야 할 것으로 보인다 — severity 구분, physicalName 검사, 근거 주석이 있고 테스트도 13건으로 더 두껍다. - -Stack B(TopologyValidationRuntime, TopologyReader, ObservedTopology, 그리고 그것만 쓰는 TopologyManifest.differencesFrom)를 제거하는 편이 낫다. - -같은 코드 문자열 TOPOLOGY_MISMATCH 를 두 예외 타입이 쓰는 것도 정리 대상이다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : TopologyValidationRuntime 참조 13건 검색과 두 스택이 같은 상황에 내리는 판정 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-admin-runtime#L910 에 있다. - -## 본문 - - - -Stack A 는 파티션 스케일업을 ADVISORY 로 두어 기동을 허용하고 그 근거를 명시한다. Stack B 는 같은 상황을 차이로 보고 기동을 거부한다. - -## 두 스택이 갈리는 지점 - -:::evidence key="messaging-admin-runtime-f02-diagram" alt="스택 A 쪽에 파티션 스케일업과 ADVISORY 기동 허용이 놓이고 스택 B 쪽에 파티션 스케일업과 차이 기동 거부가 놓인다" caption="두 스택이 갈리는 지점" zoom="false" -::: - -## TopologyValidationRuntime 참조 위치 - -:::evidence key="messaging-admin-runtime-f02" alt="코드베이스에서 TopologyValidationRuntime 를 검색한 출력 13줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="TopologyValidationRuntime 코드베이스 검색 — 13줄 · exit 0" zoom="true" -::: - -## 배선하는 순간 기동 여부가 갈린다 - -둘 다 프로덕션 호출부가 0건이라 지금은 충돌하지 않지만, `final/document.md#a19-messaging-admin-api` §17 첫 항목대로 토폴로지 검증을 기동에 배선하는 순간 **어느 스택을 배선하느냐가 스케일업한 배포의 기동 여부를 가른다**. - -## 남아야 할 쪽 - -Stack A 로 보인다 — severity 구분, `physicalName` 검사, 근거 주석이 있고 테스트도 13건으로 더 두껍다. Stack B(`TopologyValidationRuntime`, `TopologyReader`, `ObservedTopology`, 그리고 그것만 쓰는 `TopologyManifest.differencesFrom`)를 제거하는 편이 낫다. 같은 코드 문자열 `TOPOLOGY_MISMATCH` 를 두 예외 타입이 쓰는 것도 정리 대상이다. - -## 확인하지 못한 것 - -두 스택을 실제 브로커 토폴로지에 걸어 판정 차이를 관측하지 않았다. BrokerTopologyInspector 의 구현이 저장소에 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-admin-runtime-f10.md b/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-admin-runtime-f10.md deleted file mode 100644 index 8ba7e24..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-admin-runtime-f10.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -kind: CASE -slug: messaging-admin-runtime-f10 -title: 실패한 리드라이브 항목의 사유가 어디에도 남지 않는다 -topic: transport-and-provider-semantics -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-admin-runtime-f10 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-admin-runtime-f10 - file: ../../../final/evidence/rendered/messaging-admin-runtime-f10.svg -evidence: - - ../../../final/evidence/raw/messaging-admin-runtime-f10.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-admin-runtime#L961 이다. -module: messaging-admin-runtime -priority: P3 ---- - -# 실패한 리드라이브 항목의 사유가 어디에도 남지 않는다 - -attempt(...) 는 예외와 미확인을 모두 false 로 접는다(RedriveService:142-156). 감사 이벤트는 failed 개수만 담는다(:135). - -## 문제 - -attempt(...) 는 예외와 미확인을 모두 false 로 접는다(RedriveService:142-156). - -감사 이벤트는 failed 개수만 담는다(:135). - -## 결론 - -사건 복구 중에 "왜 이 메시지들이 안 갔는가" 를 물을 수 있어야 하는데 답이 없다. - -RedriveReport 에 실패 사유별 집계(코드 → 개수) 정도만 추가해도 크게 달라진다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : RedriveReport 참조 10건 검색과 실패 항목이 접히는 지점 및 감사 이벤트가 담는 값 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-admin-runtime#L961 에 있다. - -## 본문 - - - -`attempt(...)` 는 예외와 미확인을 모두 `false` 로 접는다(`RedriveService:142-156`). 감사 이벤트는 `failed` 개수만 담는다(`:135`). - -## RedriveReport 참조 위치 - -:::evidence key="messaging-admin-runtime-f10" alt="코드베이스에서 RedriveReport 를 검색한 출력 10줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RedriveReport 코드베이스 검색 — 10줄 · exit 0" zoom="true" -::: - -## 사건 복구 중에 물을 수 있어야 하는 질문 - -"왜 이 메시지들이 안 갔는가" 인데 답이 없다. `RedriveReport` 에 실패 사유별 집계(코드 → 개수) 정도만 추가해도 크게 달라진다. - -## 확인하지 못한 것 - -사건 복구 중에 실패 사유를 되묻는 상황을 재현하지 않았다. attempt 가 예외와 미확인을 같은 값으로 접는다는 코드로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-core-api-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-core-api-f02.md deleted file mode 100644 index 876279a..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-core-api-f02.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -kind: CASE -slug: messaging-core-api-f02 -title: 배치 metadata를 만들고 넘길 곳이 없다 -topic: transport-and-provider-semantics -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-core-api-f02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-core-api-f02 - file: ../../../final/evidence/rendered/messaging-core-api-f02.svg - - key: messaging-core-api-f02-diagram - file: ../../../final/assets/diagrams/messaging-core-api-f02.svg -evidence: - - ../../../final/evidence/raw/messaging-core-api-f02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-core-api#L844 이다. -module: messaging-core-api -priority: P2 ---- - -# 배치 metadata를 만들고 넘길 곳이 없다 - -KafkaBatchConsumerRegistrar:104와 RabbitBatchConsumerRegistrar:139가 BatchDeliveryMetadata를 만들고, 두 javadoc 다 "the batch-wide metadata handed to the handler"라고 적는다. new BatchMessageDelivery는 저장소 전체에서 0건이고 BatchMessageHandler 참조도 0건이다. - -## 문제 - -KafkaBatchConsumerRegistrar:104와 RabbitBatchConsumerRegistrar:139가 BatchDeliveryMetadata를 만들고, 두 javadoc 다 "the batch-wide metadata handed to the handler"라고 적는다. - -new BatchMessageDelivery는 저장소 전체에서 0건이고 BatchMessageHandler 참조도 0건이다. - -## 결론 - -두 registrar는 브로커별로 다른 정확한 계산을 한다 — Kafka는 파티션 단위 커밋이라 settlableAsBatch=true, Rabbit은 multiple-ack이 in-flight까지 정산하므로 false. - -이 판단이 계산되어 어디에도 전달되지 않는다. - -javadoc은 존재하지 않는 수신자를 가리킨다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : BatchDeliveryMetadata 참조 10건 검색과 그것을 받을 타입의 생성 지점 수 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-core-api#L844 에 있다. - -## 본문 - - - -`KafkaBatchConsumerRegistrar:104`와 `RabbitBatchConsumerRegistrar:139`가 `BatchDeliveryMetadata`를 만들고, 두 javadoc 다 "the batch-wide metadata handed to the handler"라고 적는다. - -## 만들고 넘길 곳이 없는 값 - -:::evidence key="messaging-core-api-f02-diagram" alt="registrar 의 계산만 이 값이 도달하는 범위 안에 놓이고 BatchMessageHandler 가 바깥에 빗금으로 놓인다" caption="만들고 넘길 곳이 없는 값" zoom="false" -::: - -`new BatchMessageDelivery`는 저장소 전체에서 0건이고 `BatchMessageHandler` 참조도 0건이다. - -## BatchDeliveryMetadata 참조 위치 - -:::evidence key="messaging-core-api-f02" alt="코드베이스에서 BatchDeliveryMetadata 를 검색한 출력 10줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="BatchDeliveryMetadata 코드베이스 검색 — 10줄 · exit 0" zoom="true" -::: - -## 브로커별로 다른 정확한 계산이 버려진다 - -Kafka는 파티션 단위 커밋이라 `settlableAsBatch=true`, Rabbit은 multiple-ack이 in-flight까지 정산하므로 `false`. 이 판단이 계산되어 어디에도 전달되지 않고, javadoc은 존재하지 않는 수신자를 가리킨다. - -## 확인하지 못한 것 - -저장소 밖 소비자가 이 타입을 구현하는지 확인할 방법이 이 저장소 안에 없다. 이 판단이 그 미지수에 걸려 있다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-inbox-jdbc-postgresql-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-inbox-jdbc-postgresql-f03.md deleted file mode 100644 index d515755..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-inbox-jdbc-postgresql-f03.md +++ /dev/null @@ -1,84 +0,0 @@ ---- -kind: CASE -slug: messaging-inbox-jdbc-postgresql-f03 -title: SQL 실패가 재시도 불가로 분류된다 -topic: transport-and-provider-semantics -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-inbox-jdbc-postgresql-f03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-inbox-jdbc-postgresql-f03 - file: ../../../final/evidence/rendered/messaging-inbox-jdbc-postgresql-f03.svg - - key: messaging-inbox-jdbc-postgresql-f03-diagram - file: ../../../final/assets/diagrams/messaging-inbox-jdbc-postgresql-f03.svg -evidence: - - ../../../final/evidence/raw/messaging-inbox-jdbc-postgresql-f03.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-inbox-jdbc-postgresql#L666 이다. -module: messaging-inbox-jdbc-postgresql -priority: P2 ---- - -# SQL 실패가 재시도 불가로 분류된다 - -INBOX_RESERVE_FAILED·INBOX_QUERY_FAILED·INBOX_PURGE_FAILED 셋 다 MessagingConfigurationException이고, 그 예외의 카테고리는 CONFIGURATION, retryable = false다. SQLException의 원인 대부분은 구성 오류가 아니라 일시적 인프라다 — 연결 끊김, 데드락, 락 타임아웃, 커넥션 풀 고갈. - -## 관계 - -- **컬럼 폭은 애플리케이션 검증과 짝을 이룬다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 안전 규칙은 한 공식과 한 강제 시점을 갖는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 문제 - -INBOX_RESERVE_FAILED·INBOX_QUERY_FAILED·INBOX_PURGE_FAILED 셋 다 MessagingConfigurationException이고, 그 예외의 카테고리는 CONFIGURATION, retryable = false다. - -SQLException의 원인 대부분은 구성 오류가 아니라 일시적 인프라다 — 연결 끊김, 데드락, 락 타임아웃, 커넥션 풀 고갈. - -## 결론 - -FailureCategory는 "the stable classification a retry engine, DLQ router, and dashboard all agree on"이고 retryable = false는 재시도 엔진이 즉시 파킹한다는 뜻이다. - -같은 leaf의 INBOX_ACTION_FAILED는 TRANSIENT_INFRASTRUCTURE/retryable = true로 정확히 분류된다 — 같은 파일 안에서 기준이 갈린다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : 세 코드가 쓰는 예외 타입과 그 예외의 카테고리·재시도 가능 여부 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-inbox-jdbc-postgresql#L666 에 있다. - -## 본문 - - - -`INBOX_RESERVE_FAILED`·`INBOX_QUERY_FAILED`·`INBOX_PURGE_FAILED` 셋 다 `MessagingConfigurationException`이고, 그 예외의 카테고리는 `CONFIGURATION`, `retryable = false`다. - -## 같은 파일 안에서 갈리는 분류 - -:::evidence key="messaging-inbox-jdbc-postgresql-f03-diagram" alt="SQLException 에서 세 코드는 CONFIGURATION 이고 INBOX_ACTION_FAILED 는 TRANSIENT 인 두 갈래가 나온다" caption="같은 파일 안에서 갈리는 분류" zoom="false" -::: - -`SQLException`의 원인 대부분은 구성 오류가 아니라 **일시적 인프라**다 — 연결 끊김, 데드락, 락 타임아웃, 커넥션 풀 고갈. - -## 세 코드가 공유하는 예외 타입 - -:::evidence key="messaging-inbox-jdbc-postgresql-f03" alt="분석 문서 final/document.md#a19-messaging-inbox-jdbc-postgresql 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a19-messaging-inbox-jdbc-postgresql 발췌 — 15줄" zoom="true" -::: - -## 세 소비자의 합의가 깨진다 - -`FailureCategory`는 "the stable classification a retry engine, DLQ router, and dashboard all agree on"이고 `retryable = false`는 재시도 엔진이 즉시 파킹한다는 뜻이다. 같은 leaf의 `INBOX_ACTION_FAILED`는 `TRANSIENT_INFRASTRUCTURE`/`retryable = true`로 정확히 분류된다 — 같은 파일 안에서 기준이 갈린다. - -## 확인하지 못한 것 - -일시적 인프라 오류를 실제로 일으켜 메시지가 파킹되는 것을 관측하지 않았다. 예외 타입과 카테고리의 대조로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-kafka-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-kafka-f02.md deleted file mode 100644 index f969792..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-kafka-f02.md +++ /dev/null @@ -1,97 +0,0 @@ ---- -kind: CASE -slug: messaging-kafka-f02 -title: 천장에 닿아 일시정지된 파티션을 재개하는 경로가 없다 -topic: transport-and-provider-semantics -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-kafka-f02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-kafka-f02 - file: ../../../final/evidence/rendered/messaging-kafka-f02.svg - - key: messaging-kafka-f02-diagram - file: ../../../final/assets/diagrams/messaging-kafka-f02.svg -evidence: - - ../../../final/evidence/raw/messaging-kafka-f02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-kafka#L265 이다. -module: messaging-kafka -priority: P2 ---- - -# 천장에 닿아 일시정지된 파티션을 재개하는 경로가 없다 - -pollOnce 의 파티션 루프는 세 경우에 그 파티션을 멈춘다. 이 세 경로 중 어느 것도 retries.pauseUntil(...) 을 부르지 않는다. - -## 문제 - -pollOnce 의 파티션 루프는 세 경우에 그 파티션을 멈춘다. - -이 세 경로 중 어느 것도 retries.pauseUntil(...) 을 부르지 않는다. - -## 결론 - -그런데 폴 루프가 파티션을 재개하는 곳은 하나뿐이다. - -retries 에 항목을 넣는 곳은 QueuedSettlement.enqueueRequeue 하나이고, 그것은 핸들러 실패·타임아웃·명시적 requeue 경로다. - -천장·배수·풀 거부 경로는 등록하지 않는다. - -따라서 천장 때문에 멈춘 파티션은 폴 루프가 스스로 재개하지 않는다. - -재개할 수 있는 것은 외부에서 부른 resume(scope) 이나 재조정뿐이다. - -maxInFlightPerOrderingUnit 의 기본값은 1 이다(DestinationSettings.Consumer). - -한 폴이 같은 파티션의 레코드를 둘 이상 돌려주는 순간 두 번째에서 tryAcquire 가 거짓이 되고, 그 파티션이 멈춘다. - -그 뒤 작업자가 끝나 coordinator.release 로 슬롯이 비어도 consumer 는 여전히 일시정지 상태다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : QueuedSettlement 참조 3건 검색과 파티션을 멈추는 세 경로의 재개 등록 여부 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-kafka#L265 에 있다. - -## 본문 - - - -`pollOnce` 의 파티션 루프는 세 경우에 그 파티션을 멈춘다 — 천장(`tryAcquire` 실패), 배수 시작(`tryBeginWork` 실패), 풀 거부(`dispatch` 실패). 이 세 경로 중 어느 것도 `retries.pauseUntil(...)` 을 부르지 않는다. - -## 재개가 끊긴 자리 - -:::evidence key="messaging-kafka-f02-diagram" alt="핸들러 실패와 타임아웃과 requeue 만 재개 목록에 등록되는 경로 안에 놓이고 천장 도달과 배수 및 풀 거부가 바깥에 빗금으로 놓인다" caption="재개가 끊긴 자리" zoom="false" -::: - -폴 루프가 파티션을 재개하는 곳은 `applyDueResumes` 하나이고 그것은 `retries.dueForResume(now)` 만 본다. `retries` 에 항목을 넣는 곳은 `QueuedSettlement.enqueueRequeue` 하나이고, 그것은 핸들러 실패·타임아웃·명시적 requeue 경로다. - -## QueuedSettlement 참조 위치 - -:::evidence key="messaging-kafka-f02" alt="코드베이스에서 QueuedSettlement 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="QueuedSettlement 코드베이스 검색 — 3줄 · exit 0" zoom="true" -::: - -## 기본값이 이 경로를 흔하게 만든다 - -`maxInFlightPerOrderingUnit` 의 기본값은 1 이다(`DestinationSettings.Consumer`). 한 폴이 같은 파티션의 레코드를 둘 이상 돌려주는 순간 두 번째에서 `tryAcquire` 가 거짓이 되고 그 파티션이 멈춘다. 그 뒤 작업자가 끝나 `coordinator.release` 로 슬롯이 비어도 `consumer` 는 여전히 일시정지 상태다. - -## 두 pause 가 구분되어 쓰인다 - -`applySettlements` 의 `PAUSE_AND_SEEK` 는 `coordinator.pause(...)` 와 `consumer.pause(...)` 를 둘 다 부르고, 천장 경로는 `consumer` 쪽만 부른다. 그래서 조정자는 그 파티션을 멈춘 것으로 알지 못하고, `tryAcquire` 는 계속 참을 답하는데 브로커에서 레코드가 오지 않는다. - -## 수정 - -천장 경로가 `retries.pauseUntil(partition, seekBackTo, Duration.ZERO, now)` 를 등록하면 다음 주기의 `applyDueResumes` 가 즉시 재개한다 — 지연이 0 이므로 `dueForResume` 이 곧바로 돌려준다. 배수 경로는 재개하지 않는 것이 맞고, 풀 거부 경로는 천장과 같다. 소비 경로가 조립되지 않으므로(§12.1) P2. - -## 확인하지 못한 것 - -실행으로 재현하지 않았다. resume 호출처가 둘뿐이고 천장 경로가 재개 목록에 아무것도 등록하지 않는다는 것으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-kafka-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-kafka-f03.md deleted file mode 100644 index b64419a..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-kafka-f03.md +++ /dev/null @@ -1,97 +0,0 @@ ---- -kind: CASE -slug: messaging-kafka-f03 -title: 오염된 재시도 헤더가 격리되지 않고 무한 pause-and-seek 을 만든다 -topic: transport-and-provider-semantics -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-kafka-f03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-kafka-f03 - file: ../../../final/evidence/rendered/messaging-kafka-f03.svg - - key: messaging-kafka-f03-diagram - file: ../../../final/assets/diagrams/messaging-kafka-f03.svg -evidence: - - ../../../final/evidence/raw/messaging-kafka-f03.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-kafka#L301 이다. -module: messaging-kafka -priority: P2 ---- - -# 오염된 재시도 헤더가 격리되지 않고 무한 pause-and-seek 을 만든다 - -KafkaRetryMetadataMapper.attemptOf 는 읽을 수 없는 msg.retry.attempt 에 대해 fail-closed 를 택하고, 그 이유를 정확하게 적는다. 메시지가 "quarantined" 라고 말한다. - -## 문제 - -KafkaRetryMetadataMapper.attemptOf 는 읽을 수 없는 msg.retry.attempt 에 대해 fail-closed 를 택하고, 그 이유를 정확하게 적는다. - -메시지가 "quarantined" 라고 말한다. - -## 결론 - -소비자는 그렇게 하지 않는다. - -격리 경로는 디코딩 실패에만 걸려 있다. - -attemptOf 는 디코딩이 끝난 뒤 두 번째 블록에서 던지고, MessagingConfigurationException 은 MessagingException 을 통해 RuntimeException 이므로 두 번째 catch 가 잡는다. - -결과는 requeueAfterFailure() → PAUSE_AND_SEEK → 같은 오프셋 재읽기 → 같은 헤더 → 같은 예외다. - -즉 fail-closed 가 막으려던 것(재시도 예산 무력화)보다 나쁜 것을 만든다 — 그 파티션이 영구히 그 레코드에서 멈춘다. - -그리고 javadoc 이 지적한 대로 이 헤더는 호출자가 쓸 수 있는 값이므로, 숫자가 아닌 값 하나로 파티션 하나를 정지시킬 수 있다. - -ReservedHeaderForgeryTest.aMalformedRetryAttemptIsQuarantined 는 attemptOf 가 던지는 것만 단언한다. - -이름은 "quarantined" 인데 격리를 확인하지 않는다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : KafkaRetryMetadataMapper 참조 9건 검색과 던지는 지점이 놓인 try·catch 범위 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-kafka#L301 에 있다. - -## 본문 - - - -`KafkaRetryMetadataMapper.attemptOf` 는 읽을 수 없는 `msg.retry.attempt` 에 대해 fail-closed 를 택하고, 메시지가 "quarantined" 라고 말한다. 소비자는 그렇게 하지 않는다 — 격리 경로는 **디코딩 실패에만** 걸려 있다. - -## 격리 대신 도는 루프 - -:::evidence key="messaging-kafka-f03-diagram" alt="숫자 아닌 헤더가 attemptOf 던짐과 두 번째 catch 를 지나 같은 오프셋 재읽기로 이어진다" caption="격리 대신 도는 루프" zoom="false" -::: - -`attemptOf` 는 디코딩이 끝난 뒤 두 번째 블록에서 던지고, `MessagingConfigurationException` 은 `MessagingException` 을 통해 `RuntimeException` 이므로 두 번째 `catch` 가 잡는다. 결과는 `requeueAfterFailure()` → `PAUSE_AND_SEEK` → 같은 오프셋 재읽기 → 같은 헤더 → 같은 예외다. - -## KafkaRetryMetadataMapper 참조 위치 - -:::evidence key="messaging-kafka-f03" alt="코드베이스에서 KafkaRetryMetadataMapper 를 검색한 출력 9줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="KafkaRetryMetadataMapper 코드베이스 검색 — 9줄 · exit 0" zoom="true" -::: - -## fail-closed 가 막으려던 것보다 나쁜 것을 만든다 - -그 파티션이 영구히 그 레코드에서 멈춘다. 그리고 javadoc 이 지적한 대로 이 헤더는 호출자가 쓸 수 있는 값이므로, 숫자가 아닌 값 하나로 파티션 하나를 정지시킬 수 있다. - -## 테스트가 이름과 다른 것을 본다 - -`ReservedHeaderForgeryTest.aMalformedRetryAttemptIsQuarantined` 는 `attemptOf` 가 던지는 것만 단언한다. 이름은 "quarantined" 인데 격리를 확인하지 않는다. - -## 수정 - -`attemptOf` 호출을 디코딩과 같은 블록으로 옮겨 격리 경로에 태우거나, 두 번째 `catch` 가 예외 종류를 나누게 한다 — `MessagingConfigurationException` 은 재시도로 회복되지 않는 종류이므로 격리 대상이고, 핸들러 실패는 재시도 대상이다. - -## 확인하지 못한 것 - -실행으로 재현하지 않았다. attemptOf 가 dispatch 의 두 번째 try 안에 있고 그 catch 가 재요청이라는 것, 그리고 그 예외가 RuntimeException 을 상속한다는 것으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-kafka-f04.md b/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-kafka-f04.md deleted file mode 100644 index 00732c2..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-kafka-f04.md +++ /dev/null @@ -1,91 +0,0 @@ ---- -kind: CASE -slug: messaging-kafka-f04 -title: 시계를 주입받는 클래스가 한 곳에서만 벽시계를 읽는다 -topic: transport-and-provider-semantics -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-kafka-f04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-kafka-f04 - file: ../../../final/evidence/rendered/messaging-kafka-f04.svg - - key: messaging-kafka-f04-diagram - file: ../../../final/assets/diagrams/messaging-kafka-f04.svg -evidence: - - ../../../final/evidence/raw/messaging-kafka-f04.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-kafka#L342 이다. -module: messaging-kafka -priority: P3 ---- - -# 시계를 주입받는 클래스가 한 곳에서만 벽시계를 읽는다 - -KafkaConsumerRegistrar 의 설계 성질이 javadoc 에 적혀 있다. 주기마다 Instant now 를 받아 applyDueResumes(now) 로 넘긴다. - -## 문제 - -KafkaConsumerRegistrar 의 설계 성질이 javadoc 에 적혀 있다. - -주기마다 Instant now 를 받아 applyDueResumes(now) 로 넘긴다. - -## 결론 - -그런데 그 짝인 등록 쪽은 이렇다. - -이 리프에서 Instant.now() 를 읽는 유일한 자리다. - -그리고 그 호출은 작업자 스레드에서 일어나므로 폴 스레드의 now 와 다른 순간이다. - -결과는 두 가지다. - -지연 재시도(requeue(Duration))의 재개 시점을 고정 시계로 시험할 수 없고, 시험이 Duration.ZERO 밖의 지연을 다루지 못한다 — 실제로 어떤 시험도 다루지 않는다. - -수정은 생성자에 Supplier 를 하나 더 받는 것이다. - -같은 저장소의 MessagingShutdownLifecycle 이 정확히 그 형태로 두 생성자를 둔다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : KafkaConsumerRegistrar 참조 5건 검색과 주입된 시계가 덮는 범위 대비 벽시계 호출 지점 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-kafka#L342 에 있다. - -## 본문 - - - -`KafkaConsumerRegistrar` 의 설계 성질이 javadoc 에 적혀 있다 — 주기마다 `Instant now` 를 받아 `applyDueResumes(now)` 로 넘긴다. - -## 시계가 닿지 않는 한 곳 - -:::evidence key="messaging-kafka-f04-diagram" alt="폴 주기의 now 만 주입된 시계가 덮는 범위 안에 놓이고 등록 쪽 Instant.now 가 바깥에 빗금으로 놓인다" caption="시계가 닿지 않는 한 곳" zoom="false" -::: - -그 짝인 등록 쪽이 이 리프에서 `Instant.now()` 를 읽는 유일한 자리다. 그리고 그 호출은 작업자 스레드에서 일어나므로 폴 스레드의 `now` 와 다른 순간이다. - -## KafkaConsumerRegistrar 참조 위치 - -:::evidence key="messaging-kafka-f04" alt="코드베이스에서 KafkaConsumerRegistrar 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="KafkaConsumerRegistrar 코드베이스 검색 — 5줄 · exit 0" zoom="true" -::: - -## 결과 둘 - -지연 재시도(`requeue(Duration)`)의 재개 시점을 고정 시계로 시험할 수 없고, 시험이 `Duration.ZERO` 밖의 지연을 다루지 못한다 — 실제로 어떤 시험도 다루지 않는다. - -## 수정 - -생성자에 `Supplier` 를 하나 더 받는 것이다. 같은 저장소의 `MessagingShutdownLifecycle` 이 정확히 그 형태로 두 생성자를 둔다. - -## 확인하지 못한 것 - -시계를 고정해 두 경로의 시각 차이를 관측하지 않았다. 호출 지점의 코드 대조로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-kafka-f05.md b/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-kafka-f05.md deleted file mode 100644 index 40e7e86..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-kafka-f05.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -kind: CASE -slug: messaging-kafka-f05 -title: 결함으로 판정된 메서드가 남아 있고, 실브로커 증명이 그것 위에서 돈다 -topic: transport-and-provider-semantics -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-kafka-f05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-kafka-f05 - file: ../../../final/evidence/rendered/messaging-kafka-f05.svg -evidence: - - ../../../final/evidence/raw/messaging-kafka-f05.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-kafka#L362 이다. -module: messaging-kafka -priority: P3 ---- - -# 결함으로 판정된 메서드가 남아 있고, 실브로커 증명이 그것 위에서 돈다 - -KafkaTransactionalPublisher 에 같은 일을 하는 메서드가 둘 있다. inTransaction 의 javadoc 이 둘째를 결함으로 지목한다. - -## 문제 - -KafkaTransactionalPublisher 에 같은 일을 하는 메서드가 둘 있다. - -inTransaction 의 javadoc 이 둘째를 결함으로 지목한다. - -## 결론 - -sendInTransaction 은 public 이고 production 호출자가 없다. - -호출하는 것은 시험 다섯 자리뿐이다 — 그리고 그 다섯이 실브로커 트랜잭션 증명 전부다(KafkaTransactionIT·KafkaTransactionFencingIT·KafkaReadCommittedIT). - -고쳐진 inTransaction 을 시험하는 것은 KafkaTransactionOrderingTest 하나이고 MockProducer 다. - -즉 실브로커에서 커밋·중단·펜싱이 증명된 것은 옛 모양이고, 새 모양은 목 위에서만 증명됐다. - -기능적 차이는 크지 않다(body 가 비어 있으면 두 메서드는 같은 호출열을 만든다). - -그래도 두 가지가 남는다 — 결함으로 판정된 순서를 만드는 public 진입점이 여전히 열려 있다는 것, 그리고 실브로커 증거가 production 경로가 아닌 것 위에 있다는 것. - -수정은 ITs 를 inTransaction(..., () -> null) 로 옮기고 sendInTransaction 을 지우는 것이다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : KafkaTransactionalPublisher 참조 26건 검색과 두 메서드 중 실브로커 증명이 쓰는 쪽 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-kafka#L362 에 있다. - -## 본문 - - - -`KafkaTransactionalPublisher` 에 같은 일을 하는 메서드가 둘 있고, `inTransaction` 의 javadoc 이 둘째를 결함으로 지목한다. `sendInTransaction` 은 public 이고 production 호출자가 없다. - -## KafkaTransactionalPublisher 참조 위치 - -:::evidence key="messaging-kafka-f05" alt="코드베이스에서 KafkaTransactionalPublisher 를 검색한 출력 26줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="KafkaTransactionalPublisher 코드베이스 검색 — 26줄 · exit 0" zoom="true" -::: - -## 실브로커 증명이 옛 모양 위에서 돈다 - -호출하는 것은 시험 다섯 자리뿐이고, 그 다섯이 실브로커 트랜잭션 증명 전부다(`KafkaTransactionIT`·`KafkaTransactionFencingIT`·`KafkaReadCommittedIT`). 고쳐진 `inTransaction` 을 시험하는 것은 `KafkaTransactionOrderingTest` 하나이고 `MockProducer` 다. - -## 남는 두 가지 - -기능적 차이는 크지 않다 — `body` 가 비어 있으면 두 메서드는 같은 호출열을 만든다. 그래도 결함으로 판정된 순서를 만드는 public 진입점이 여전히 열려 있고, 실브로커 증거가 production 경로가 아닌 것 위에 있다. 수정은 ITs 를 `inTransaction(..., () -> null)` 로 옮기고 `sendInTransaction` 을 지우는 것이다. - -## 확인하지 못한 것 - -Toxiproxy 인증 레인을 직접 돌리지 않았다. 코드와 그 레인이 기록하는 증거 형식만 읽었다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-kafka-share-experimental-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-kafka-share-experimental-f01.md deleted file mode 100644 index 3209671..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-kafka-share-experimental-f01.md +++ /dev/null @@ -1,98 +0,0 @@ ---- -kind: CASE -slug: messaging-kafka-share-experimental-f01 -title: "등록"이 아무것도 등록하지 않고 성공을 반환한다 -topic: transport-and-provider-semantics -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-kafka-share-experimental-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-kafka-share-experimental-f01 - file: ../../../final/evidence/rendered/messaging-kafka-share-experimental-f01.svg - - key: messaging-kafka-share-experimental-f01-diagram - file: ../../../final/assets/diagrams/messaging-kafka-share-experimental-f01.svg -evidence: - - ../../../final/evidence/raw/messaging-kafka-share-experimental-f01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-kafka-share-experimental#L467 이다. -module: messaging-kafka-share-experimental -priority: P2 ---- - -# "등록"이 아무것도 등록하지 않고 성공을 반환한다 - -KafkaShareGroupRegistrar.register(profile, spec)이 spec을 Objects.requireNonNull로만 처리하고 버린다. ShareRegistration은 profile과 AtomicBoolean 둘만 갖는다. - -## 관계 - -- **허용 의존 목록은 상한이므로 미사용을 잡지 않는다 — 그것을 잡으려면 별도 검사가 필요하다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **에러 메시지가 지시하는 설정은 그 설정을 읽는 코드와 함께 존재해야 한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **구성 오류는 한 예외 타입과 안정 코드로 보고한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 문제 - -KafkaShareGroupRegistrar.register(profile, spec)이 spec을 Objects.requireNonNull로만 처리하고 버린다. - -ShareRegistration은 profile과 AtomicBoolean 둘만 갖는다. - -## 결론 - -Kafka 소비자가 만들어지지 않고(import org.apache.kafka 0건), spec.sink가 저장되지 않으므로 어떤 전달도 일어나지 않는다. - -반환된 registration은 isActive() == true를 보고한다. - -같은 클래스의 javadoc이 pause를 조용히 무시하는 것을 거절한 이유로 "would let a retry policy that depends on pausing appear to work while doing nothing"을 든다. - -register 자체가 정확히 그 형태다 — 성공을 반환하고 아무것도 하지 않으며 isActive()가 true다. - -오늘 호출자가 없으므로 사고는 아니지만, 이 leaf를 배선하는 사람이 가장 먼저 부를 메서드다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : register 본문에서 spec 이 보관되는 필드 유무와 sink 호출 지점 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-kafka-share-experimental#L467 에 있다. - -## 본문 - - - -`KafkaShareGroupRegistrar.register(profile, spec)`이 `spec`을 `Objects.requireNonNull`로만 처리하고 버린다. - -## 등록이 버리는 것 - -:::evidence key="messaging-kafka-share-experimental-f01-diagram" alt="profile 과 AtomicBoolean 만 register 가 보관하는 것 안에 놓이고 spec 과 sink 가 바깥에 빗금으로 놓인다" caption="등록이 버리는 것" zoom="false" -::: - -`ShareRegistration`은 `profile`과 `AtomicBoolean` 둘만 갖는다. Kafka 소비자가 만들어지지 않고(`import org.apache.kafka` 0건), `spec.sink`가 저장되지 않으므로 어떤 전달도 일어나지 않는다. - -## register 가 spec 에 하는 일 - -:::evidence key="messaging-kafka-share-experimental-f01" alt="분석 문서 final/document.md#a19-messaging-kafka-share-experimental 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a19-messaging-kafka-share-experimental 발췌 — 15줄" zoom="true" -::: - -## 같은 클래스가 이 형태를 거절한 적이 있다 - -반환된 registration은 `isActive() == true`를 보고한다. 같은 클래스의 javadoc이 pause를 조용히 무시하는 것을 거절한 이유로 "would let a retry policy that depends on pausing appear to work while doing nothing"을 든다. `register` 자체가 정확히 그 형태다 — 성공을 반환하고 아무것도 하지 않으며 `isActive()`가 true다. - -## 오늘 호출자는 없다 - -사고는 아니지만, 이 leaf를 배선하는 사람이 가장 먼저 부를 메서드다. - -## 확인하지 못한 것 - -이 리프를 완성할 계획이 있는지 확인할 수 없었다. 커밋이 대량 커밋뿐이고 기록이 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-nats-experimental-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-nats-experimental-f02.md deleted file mode 100644 index d217f76..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-nats-experimental-f02.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -kind: CASE -slug: messaging-nats-experimental-f02 -title: 닫힌 전송의 거절이 영구 업무 실패로 분류된다 -topic: transport-and-provider-semantics -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-nats-experimental-f02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-nats-experimental-f02 - file: ../../../final/evidence/rendered/messaging-nats-experimental-f02.svg -evidence: - - ../../../final/evidence/raw/messaging-nats-experimental-f02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-nats-experimental#L209 이다. -module: messaging-nats-experimental -priority: P3 ---- - -# 닫힌 전송의 거절이 영구 업무 실패로 분류된다 - -rejectedLocally 가 FailureCategory.PERMANENT_BUSINESS 를 고정으로 쓰고, 두 호출자 중 하나가 NATS_TRANSPORT_CLOSED 다. 자매 어댑터(Pulsar)와 같은 형태이고 같은 판단이다 — 종료 중이라는 것은 이 세대의 사정이지 업무의 영구 실패가 아니다. - -## 문제 - -rejectedLocally 가 FailureCategory.PERMANENT_BUSINESS 를 고정으로 쓰고, 두 호출자 중 하나가 NATS_TRANSPORT_CLOSED 다. - -자매 어댑터(Pulsar)와 같은 형태이고 같은 판단이다 — 종료 중이라는 것은 이 세대의 사정이지 업무의 영구 실패가 아니다. - -## 결론 - -같은 파일의 classify 는 범주를 신중히 나눈다. - -두 리프가 같은 형태를 공유하므로 수정도 함께 하는 편이 낫다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : rejectedLocally 가 붙이는 범주와 두 호출자의 실패 성격 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-nats-experimental#L209 에 있다. - -## 본문 - - - -`rejectedLocally` 가 `FailureCategory.PERMANENT_BUSINESS` 를 고정으로 쓰고, 두 호출자 중 하나가 `NATS_TRANSPORT_CLOSED` 다. - -## rejectedLocally 가 붙이는 범주 - -:::evidence key="messaging-nats-experimental-f02" alt="분석 문서 final/document.md#a19-messaging-nats-experimental 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a19-messaging-nats-experimental 발췌 — 15줄" zoom="true" -::: - -## 자매 어댑터와 같은 형태이고 같은 판단이다 - -Pulsar 쪽도 그렇다 — 종료 중이라는 것은 이 세대의 사정이지 업무의 영구 실패가 아니다. - -## 같은 파일의 classify 는 범주를 신중히 나눈다 - -두 리프가 같은 형태를 공유하므로 수정도 함께 하는 편이 낫다. - -## 확인하지 못한 것 - -종료 중 거절을 실제로 발생시켜 재시도 정책의 차이를 관측하지 않았다. 같은 파일의 분류기가 범주를 나누는 것과의 대조로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-nats-experimental-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-nats-experimental-f03.md deleted file mode 100644 index 1373c49..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-nats-experimental-f03.md +++ /dev/null @@ -1,97 +0,0 @@ ---- -kind: CASE -slug: messaging-nats-experimental-f03 -title: NatsJetStreamProfileValidator 를 부르는 곳이 javadoc 링크 하나뿐이다 -topic: transport-and-provider-semantics -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-nats-experimental-f03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-nats-experimental-f03 - file: ../../../final/evidence/rendered/messaging-nats-experimental-f03.svg - - key: messaging-nats-experimental-f03-diagram - file: ../../../final/assets/diagrams/messaging-nats-experimental-f03.svg -evidence: - - ../../../final/evidence/raw/messaging-nats-experimental-f03.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-nats-experimental#L217 이다. -module: messaging-nats-experimental -priority: P2 ---- - -# NatsJetStreamProfileValidator 를 부르는 곳이 javadoc 링크 하나뿐이다 - -75줄짜리 검증기가 이 어댑터의 시작 시점 판단 넷을 들고 있다 — 실험 스위치가 꺼져 있으면 거부, 최소 한 번 배달에 코어 NATS 거부, 순서 있는 소비자와 경쟁 작업자 동시 사용 거부, 키 순서 목적지 거부. 저장소 전역에서 이 클래스 이름이 나오는 곳은 두 줄뿐이다. - -## 문제 - -75줄짜리 검증기가 이 어댑터의 시작 시점 판단 넷을 들고 있다 — 실험 스위치가 꺼져 있으면 거부, 최소 한 번 배달에 코어 NATS 거부, 순서 있는 소비자와 경쟁 작업자 동시 사용 거부, 키 순서 목적지 거부. - -저장소 전역에서 이 클래스 이름이 나오는 곳은 두 줄뿐이다. - -## 결론 - -하나는 선언이고 하나는 javadoc 링크다. - -코드 호출자 0, 테스트 0. - -validate 는 jetStreamEnabled · orderedConsumer · competingWorkers · enabled 를 전부 인자로 받는다. - -즉 스스로 아무것도 관찰하지 않고, 호출자가 이미 알고 있는 사실을 넘겨 줘야만 판단한다. - -그런 호출자가 없으니 이 판단들은 한 번도 실행된 적이 없다. - -전송의 클래스 javadoc 이 "그래서 검증기가 시작 시 그 조합을 거부한다"고 단언한다. - -읽는 사람에게 이 어댑터는 코어 NATS 오설정으로부터 보호되는 것처럼 보이는데, 실제로는 아무 게이트도 걸려 있지 않다. - -실험 등급이라 지금 당장 사고가 나지는 않지만, 이 어댑터를 실전에 붙이는 사람이 가장 먼저 신뢰할 문장이 지금 사실이 아니다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : NatsJetStreamProfileValidator 참조 2건 검색으로 선언과 javadoc 링크 외의 호출 유무 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-nats-experimental#L217 에 있다. - -## 본문 - - - -75줄짜리 검증기가 이 어댑터의 시작 시점 판단 넷을 들고 있다 — 실험 스위치가 꺼져 있으면 거부, 최소 한 번 배달에 코어 NATS 거부, 순서 있는 소비자와 경쟁 작업자 동시 사용 거부, 키 순서 목적지 거부. - -## 검증기를 가리키는 두 줄 - -:::evidence key="messaging-nats-experimental-f03-diagram" alt="선언과 javadoc 링크만 검증기를 가리키는 것 안에 놓이고 코드 호출자와 테스트가 바깥에 빗금으로 놓인다" caption="검증기를 가리키는 두 줄" zoom="false" -::: - -저장소 전역에서 이 클래스 이름이 나오는 곳은 두 줄뿐이다 — 하나는 선언이고 하나는 **javadoc 링크**다. 코드 호출자 0, 테스트 0. - -## NatsJetStreamProfileValidator 참조 위치 - -:::evidence key="messaging-nats-experimental-f03" alt="코드베이스에서 NatsJetStreamProfileValidator 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="NatsJetStreamProfileValidator 코드베이스 검색 — 2줄 · exit 0" zoom="true" -::: - -## 스스로 아무것도 관찰하지 않는다 - -`validate` 는 `jetStreamEnabled` · `orderedConsumer` · `competingWorkers` · `enabled` 를 전부 인자로 받는다 — 호출자가 이미 알고 있는 사실을 넘겨 줘야만 판단한다. 그런 호출자가 없으니 이 판단들은 한 번도 실행된 적이 없다. - -## 읽는 사람에게는 보호되는 것처럼 보인다 - -전송의 클래스 javadoc 이 "그래서 검증기가 시작 시 그 조합을 거부한다"고 단언한다. 실험 등급이라 지금 당장 사고가 나지는 않지만, 이 어댑터를 실전에 붙이는 사람이 가장 먼저 신뢰할 문장이 지금 사실이 아니다. - -## 같은 형태를 여러 번 봤고 이쪽이 더 나쁘다 - -`GrpcRawApiImportRule` · `GrpcApplicationBoundaryRules` · `GrpcNettyParityContract` 등은 최소한 테스트가 리터럴을 먹여 판단 자체는 실행해 본다. 여기서는 그것조차 없다. 수정은 어댑터 조립 지점에서 `validate` 를 부르거나, 그럴 지점이 아직 없다면 프로파일 생성 시점에 걸리도록 옮기는 것이다. 어느 쪽도 못 하겠다면 전송 javadoc 의 "refuses ... at startup" 을 사실에 맞게 고친다. - -## 확인하지 못한 것 - -리플렉션이나 문자열 기반 조립으로 이 검증기를 부르는 형태는 배제하지 못했다. 클래스 이름과 패키지 이름 두 가지 grep 으로만 확인했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-policy-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-policy-f01.md deleted file mode 100644 index 6bcc494..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-policy-f01.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -kind: CASE -slug: messaging-policy-f01 -title: 재시도 엔진과 DLQ 조정자가 bean으로 만들어지고 주입되는 곳이 없다 -topic: transport-and-provider-semantics -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-policy-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-policy-f01 - file: ../../../final/evidence/rendered/messaging-policy-f01.svg - - key: messaging-policy-f01-diagram - file: ../../../final/assets/diagrams/messaging-policy-f01.svg -evidence: - - ../../../final/evidence/raw/messaging-policy-f01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-policy#L781 이다. -module: messaging-policy -priority: P2 ---- - -# 재시도 엔진과 DLQ 조정자가 bean으로 만들어지고 주입되는 곳이 없다 - -MessagingCoreAutoConfiguration이 RetryDecisionEngine(:167)과 DeadLetterOrchestrator(:179)를 @Bean @ConditionalOnMissingBean으로 만든다. 두 타입을 받는 production 코드는 각각 KafkaRetryExecutor와 KafkaDeadLetterPublisher/RabbitDeadLetterPublisher뿐이고, 셋 다 저장소 어디에서도 생성되지 않는다. - -## 관계 - -- **구성 오류는 한 예외 타입과 안정 코드로 보고한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **저장소 밖 문서를 절 번호로 인용하지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **부팅 경로의 알고리즘 복잡도는 문서화한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 문제 - -MessagingCoreAutoConfiguration이 RetryDecisionEngine(:167)과 DeadLetterOrchestrator(:179)를 @Bean @ConditionalOnMissingBean으로 만든다. - -두 타입을 받는 production 코드는 각각 KafkaRetryExecutor와 KafkaDeadLetterPublisher/RabbitDeadLetterPublisher뿐이고, 셋 다 저장소 어디에서도 생성되지 않는다. - -## 결론 - -같은 설정의 51개 bean 중 두 타입을 인자로 받는 @Bean 메서드가 없다. - -컨텍스트에 두 bean이 앉아 있고 MessagingAutoConfigurationTest류의 hasSingleBean 검사는 통과한다 — 즉 bean 존재 검사가 배선을 증명하지 않는다. - -그리고 이 leaf가 가장 공들인 두 축(6개 재시도 모드·8단 판단 순서·full jitter·capability 인식, DLQ 발행-후-정산 불변식·예약 헤더 6개)이 실행되지 않는다. - -42개 테스트 중 16개가 이 두 축을 검증한다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : MessagingCoreAutoConfiguration 참조 8건 검색과 두 빈을 인자로 받는 Bean 메서드 유무 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-policy#L781 에 있다. - -## 본문 - - - -`MessagingCoreAutoConfiguration`이 `RetryDecisionEngine`(\:167)과 `DeadLetterOrchestrator`(\:179)를 `@Bean @ConditionalOnMissingBean`으로 만든다. - -## 만들어지고 쓰이지 않는 빈 - -:::evidence key="messaging-policy-f01-diagram" alt="RetryDecisionEngine 과 DeadLetterOrchestrator 가 컨텍스트에 있는 것 안에 놓이고 두 빈을 받는 Bean 메서드가 바깥에 빗금으로 놓인다" caption="만들어지고 쓰이지 않는 빈" zoom="false" -::: - -두 타입을 받는 production 코드는 각각 `KafkaRetryExecutor`와 `KafkaDeadLetterPublisher`/`RabbitDeadLetterPublisher`뿐이고, **셋 다 저장소 어디에서도 생성되지 않는다.** 같은 설정의 51개 bean 중 두 타입을 인자로 받는 `@Bean` 메서드가 없다. - -## MessagingCoreAutoConfiguration 참조 위치 - -:::evidence key="messaging-policy-f01" alt="코드베이스에서 MessagingCoreAutoConfiguration 를 검색한 출력 8줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingCoreAutoConfiguration 코드베이스 검색 — 8줄 · exit 0" zoom="true" -::: - -## bean 존재 검사가 배선을 증명하지 않는다 - -컨텍스트에 두 bean이 앉아 있고 `MessagingAutoConfigurationTest`류의 `hasSingleBean` 검사는 통과한다. 그리고 이 leaf가 가장 공들인 두 축(6개 재시도 모드·8단 판단 순서·full jitter·capability 인식, DLQ 발행-후-정산 불변식·예약 헤더 6개)이 실행되지 않는다. 42개 테스트 중 16개가 이 두 축을 검증한다. - -## 확인하지 못한 것 - -소비 경로를 배선할 계획이 있는지 확인할 수 없었다. 저장소 안에 답이 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-policy-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-policy-f03.md deleted file mode 100644 index bdf8b5e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-policy-f03.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -kind: CASE -slug: messaging-policy-f03 -title: 재시도와 DLQ 각각에 두 개의 구현이 있고 정본이 표시되지 않았다 -topic: transport-and-provider-semantics -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-policy-f03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-policy-f03 - file: ../../../final/evidence/rendered/messaging-policy-f03.svg -evidence: - - ../../../final/evidence/raw/messaging-policy-f03.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-policy#L799 이다. -module: messaging-policy -priority: P3 ---- - -# 재시도와 DLQ 각각에 두 개의 구현이 있고 정본이 표시되지 않았다 - -재시도: DefaultRetryDecisionEngine(6모드·백오프·순서 인식) vs DefaultDeliveryProcessor(고정 지연·시도 횟수 미확인). DLQ: DeadLetterOrchestrator(예약 헤더 6개 부착) vs DefaultDeliveryProcessor.DeadLetterPublisher(헤더 없음). - -## 관계 - -- **구성 오류는 한 예외 타입과 안정 코드로 보고한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **저장소 밖 문서를 절 번호로 인용하지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **부팅 경로의 알고리즘 복잡도는 문서화한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 문제 - -재시도: DefaultRetryDecisionEngine(6모드·백오프·순서 인식) vs DefaultDeliveryProcessor(고정 지연·시도 횟수 미확인). - -DLQ: DeadLetterOrchestrator(예약 헤더 6개 부착) vs DefaultDeliveryProcessor.DeadLetterPublisher(헤더 없음). - -## 결론 - -둘 다 조립되지 않았다. - -오늘 경쟁하지 않지만, 소비 경로를 배선하는 사람이 둘 중 하나를 고르게 되고 코드가 어느 쪽이 정본인지 말하지 않는다. - -두 javadoc이 각각 자기가 플랫폼 규칙의 구현이라고 서술한다. - -그리고 선택 결과가 다르다 — DefaultDeliveryProcessor 경로로 DLQ된 메시지에는 실패 카테고리·코드·원본 목적지·시도 횟수가 붙지 않는다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : DefaultRetryDecisionEngine 참조 6건 검색과 두 구현의 javadoc·분기 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-policy#L799 에 있다. - -## 본문 - - - -재시도와 DLQ 각각에 구현이 둘이다. - -| 축 | 한쪽 | 다른쪽 | -|---|---|---| -| 재시도 | `DefaultRetryDecisionEngine`(6모드·백오프·순서 인식) | `DefaultDeliveryProcessor`(고정 지연·시도 횟수 미확인) | -| DLQ | `DeadLetterOrchestrator`(예약 헤더 6개 부착) | `DefaultDeliveryProcessor.DeadLetterPublisher`(헤더 없음) | - -## DefaultRetryDecisionEngine 참조 위치 - -:::evidence key="messaging-policy-f03" alt="코드베이스에서 DefaultRetryDecisionEngine 를 검색한 출력 6줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DefaultRetryDecisionEngine 코드베이스 검색 — 6줄 · exit 0" zoom="true" -::: - -## 둘 다 조립되지 않았다 - -오늘 경쟁하지 않지만, 소비 경로를 배선하는 사람이 둘 중 하나를 고르게 되고 코드가 어느 쪽이 정본인지 말하지 않는다. 두 javadoc이 각각 자기가 플랫폼 규칙의 구현이라고 서술한다. - -## 선택 결과가 다르다 - -`DefaultDeliveryProcessor` 경로로 DLQ된 메시지에는 실패 카테고리·코드·원본 목적지·시도 횟수가 붙지 않는다. - -## 확인하지 못한 것 - -두 재시도 구현 중 어느 쪽이 정본인지 확인할 수 없었다. 소비 경로 배선 계획이라는 같은 미지수에 걸린다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-policy-f04.md b/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-policy-f04.md deleted file mode 100644 index fbfdcd2..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-policy-f04.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -kind: CASE -slug: messaging-policy-f04 -title: DLQ 메타데이터의 두 시각이 항상 같다 -topic: transport-and-provider-semantics -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-policy-f04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-policy-f04 - file: ../../../final/evidence/rendered/messaging-policy-f04.svg -evidence: - - ../../../final/evidence/raw/messaging-policy-f04.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-policy#L808 이다. -module: messaging-policy -priority: P3 ---- - -# DLQ 메타데이터의 두 시각이 항상 같다 - -DeadLetterMetadata가 firstFailureAt과 lastFailureAt을 별도 필드로 선언하는데, 유일한 생산 지점인 DeadLetterOrchestrator:89-97이 둘 다 delivery.metadata().receivedAt()으로 채운다. 두 헤더(msg.first-failure-at, msg.last-failure-at)가 DLQ 메시지에 붙는데 항상 같은 값이다. - -## 관계 - -- **구성 오류는 한 예외 타입과 안정 코드로 보고한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **저장소 밖 문서를 절 번호로 인용하지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **부팅 경로의 알고리즘 복잡도는 문서화한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 문제 - -DeadLetterMetadata가 firstFailureAt과 lastFailureAt을 별도 필드로 선언하는데, 유일한 생산 지점인 DeadLetterOrchestrator:89-97이 둘 다 delivery.metadata().receivedAt()으로 채운다. - -두 헤더(msg.first-failure-at, msg.last-failure-at)가 DLQ 메시지에 붙는데 항상 같은 값이다. - -## 결론 - -운영자가 "이 메시지가 얼마나 오래 실패해 왔는가"를 헤더에서 알 수 없다. - -ReservedHeaders가 두 이름을 따로 정의한 목적이 실현되지 않는다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : DeadLetterMetadata 참조 5건 검색과 유일한 생산 지점이 두 필드에 넣는 값 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-policy#L808 에 있다. - -## 본문 - - - -`DeadLetterMetadata`가 `firstFailureAt`과 `lastFailureAt`을 별도 필드로 선언하는데, 유일한 생산 지점인 `DeadLetterOrchestrator:89-97`이 둘 다 `delivery.metadata().receivedAt()`으로 채운다. - -## DeadLetterMetadata 참조 위치 - -:::evidence key="messaging-policy-f04" alt="코드베이스에서 DeadLetterMetadata 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DeadLetterMetadata 코드베이스 검색 — 5줄 · exit 0" zoom="true" -::: - -## 두 헤더가 항상 같은 값이다 - -`msg.first-failure-at`, `msg.last-failure-at` 가 DLQ 메시지에 붙는다. 운영자가 "이 메시지가 얼마나 오래 실패해 왔는가"를 헤더에서 알 수 없다 — `ReservedHeaders`가 두 이름을 따로 정의한 목적이 실현되지 않는다. - -## 확인하지 못한 것 - -DLQ 메시지의 두 헤더가 실제 배포에서 같은 값을 갖는 것을 관측하지 않았다. 생산 지점 한 곳의 코드로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-pulsar-experimental-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-pulsar-experimental-f01.md deleted file mode 100644 index d4e9c9d..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-pulsar-experimental-f01.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -kind: CASE -slug: messaging-pulsar-experimental-f01 -title: 닫힌 전송의 거절이 영구 업무 실패로 분류된다 -topic: transport-and-provider-semantics -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-pulsar-experimental-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-pulsar-experimental-f01 - file: ../../../final/evidence/rendered/messaging-pulsar-experimental-f01.svg -evidence: - - ../../../final/evidence/raw/messaging-pulsar-experimental-f01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-pulsar-experimental#L196 이다. -module: messaging-pulsar-experimental -priority: P3 ---- - -# 닫힌 전송의 거절이 영구 업무 실패로 분류된다 - -두 호출자가 이 메서드를 쓴다. 첫째는 영구 업무 실패가 맞다. - -## 문제 - -두 호출자가 이 메서드를 쓴다. - -첫째는 영구 업무 실패가 맞다. - -## 결론 - -둘째는 아니다. - -종료 중이라는 것은 이 세대의 사정이고, 다음 세대나 다른 인스턴스에서는 같은 메시지가 발행된다. - -같은 파일의 classify 가 분류를 신중히 나눈다 — 사전 거절은 CONFIGURATION, 모호는 TRANSIENT_INFRASTRUCTURE. - -닫힘만 그 규율 밖에 있다. - -전송되지 않았다는 증거(notTransmitted)는 옳다. - -어긋난 것은 범주뿐이다. - -수정은 닫힘에 TRANSIENT_INFRASTRUCTURE 를 주거나, 두 호출자가 범주를 인자로 받게 하는 것이다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : 로컬 거절 헬퍼가 붙이는 범주와 두 호출자의 실패 성격 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-pulsar-experimental#L196 에 있다. - -## 본문 - - - -두 호출자가 로컬 거절 헬퍼를 쓴다. 첫째(적재물 상한 초과)는 영구 업무 실패가 맞다. 둘째(전송 종료 중)는 아니다. - -## 헬퍼를 부르는 두 호출자 - -:::evidence key="messaging-pulsar-experimental-f01" alt="분석 문서 final/document.md#a19-messaging-pulsar-experimental 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a19-messaging-pulsar-experimental 발췌 — 15줄" zoom="true" -::: - -## 종료 중은 이 세대의 사정이다 - -다음 세대나 다른 인스턴스에서는 같은 메시지가 발행된다. - -## 같은 파일의 classify 는 분류를 신중히 나눈다 - -사전 거절은 `CONFIGURATION`, 모호는 `TRANSIENT_INFRASTRUCTURE`. 닫힘만 그 규율 밖에 있다. - -## 증거는 옳고 범주만 어긋났다 - -전송되지 않았다는 증거(`notTransmitted`)는 옳다. 수정은 닫힘에 `TRANSIENT_INFRASTRUCTURE` 를 주거나, 두 호출자가 범주를 인자로 받게 하는 것이다. - -## 확인하지 못한 것 - -실제 Pulsar 브로커를 띄우지 않았다. 이 리프가 클라이언트 브리지를 싣지 않으므로 그럴 대상도 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-rabbit-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-rabbit-f01.md deleted file mode 100644 index c483eec..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-rabbit-f01.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -kind: CASE -slug: messaging-rabbit-f01 -title: 확인 등급이 요구에서 파생되고, 그 요구를 뒷받침하는 강제는 목적지 종류 하나에만 걸린다 -topic: transport-and-provider-semantics -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-rabbit-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-rabbit-f01 - file: ../../../final/evidence/rendered/messaging-rabbit-f01.svg -evidence: - - ../../../final/evidence/raw/messaging-rabbit-f01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-rabbit#L210 이다. -module: messaging-rabbit -priority: P3 ---- - -# 확인 등급이 요구에서 파생되고, 그 요구를 뒷받침하는 강제는 목적지 종류 하나에만 걸린다 - -증거의 등급이 브로커가 무엇을 했는지가 아니라 프로파일이 무엇을 요구했는지 에서 나온다. 대부분의 경우 이 파생은 성립한다. - -## 문제 - -증거의 등급이 브로커가 무엇을 했는지가 아니라 프로파일이 무엇을 요구했는지 에서 나온다. - -대부분의 경우 이 파생은 성립한다. - -## 결론 - -두 강제가 그것을 받쳐 준다. - -RabbitHeaderMapper.toProperties 가 배달 모드를 무조건 PERSISTENT 로 둔다. - -RabbitMQ 는 지속 메시지를 디스크에 쓴 뒤에 확인한다. - -RabbitProfileValidator.validateDestination 이 내구 작업 큐에 쿼럼 큐를 요구한다. - -쿼럼 큐의 확인은 다수 복제 뒤에 온다. - -빈틈은 둘째 강제의 범위다. - -작업 큐가 아닌 목적지에는 쿼럼 요구가 없다. - -교환기로 발행하는 목적지가 REPLICATION_OR_PERSISTENCE_ACK 를 요구하면, 그 교환기에 바인딩된 큐가 고전 큐여도 어댑터는 그 등급을 보고한다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : RabbitHeaderMapper 참조 13건 검색과 확인 등급이 파생되는 입력 및 강제가 걸린 목적지 종류 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-rabbit#L210 에 있다. - -## 본문 - - - -증거의 등급이 브로커가 무엇을 했는지가 아니라 프로파일이 무엇을 **요구했는지** 에서 나온다. - -## RabbitHeaderMapper 참조 위치 - -:::evidence key="messaging-rabbit-f01" alt="코드베이스에서 RabbitHeaderMapper 를 검색한 출력 13줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RabbitHeaderMapper 코드베이스 검색 — 13줄 · exit 0" zoom="true" -::: - -## 두 강제가 그 파생을 받쳐 준다 - -`RabbitHeaderMapper.toProperties` 가 배달 모드를 무조건 `PERSISTENT` 로 둔다 — RabbitMQ 는 지속 메시지를 디스크에 쓴 뒤에 확인한다. 그리고 `RabbitProfileValidator.validateDestination` 이 내구 작업 큐에 쿼럼 큐를 요구한다 — 쿼럼 큐의 확인은 다수 복제 뒤에 온다. - -## 빈틈은 둘째 강제의 범위다 - -작업 큐가 아닌 목적지에는 쿼럼 요구가 없다. 교환기로 발행하는 목적지가 `REPLICATION_OR_PERSISTENCE_ACK` 를 요구하면, 그 교환기에 바인딩된 큐가 고전 큐여도 어댑터는 그 등급을 보고한다. 지속 모드 덕분에 디스크 기록은 보장되지만 복제는 보장되지 않는다. - -## 이 저장소의 규율과 어긋난다 - -증거가 관측에서 나와야 한다는 것이다 — `MessagingCapabilities` 의 javadoc 이 "a silently weakened guarantee is indistinguishable from a working one until the incident" 라고 적는다. 수정은 쿼럼 요구를 목적지 종류가 아니라 **요구된 확인 등급** 에 걸거나, 작업 큐가 아닌 목적지에서는 등급을 `BROKER_ACK` 로 낮추는 것이다. - -## 확인하지 못한 것 - -실제 브로커로 반환-먼저-확인 순서를 재현하지 않았다. 그 레인은 컨테이너가 필요하다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-rabbit-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-rabbit-f03.md deleted file mode 100644 index 2b076c9..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-rabbit-f03.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -kind: CASE -slug: messaging-rabbit-f03 -title: SCRAM 자격을 RabbitMQ 의 데모 기구로 조용히 매핑한다 -topic: transport-and-provider-semantics -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-rabbit-f03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-rabbit-f03 - file: ../../../final/evidence/rendered/messaging-rabbit-f03.svg -evidence: - - ../../../final/evidence/raw/messaging-rabbit-f03.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-rabbit#L274 이다. -module: messaging-rabbit -priority: P3 ---- - -# SCRAM 자격을 RabbitMQ 의 데모 기구로 조용히 매핑한다 - -RABBIT-CR-DEMO 는 RabbitMQ 의 시연용 challenge-response 인증 기구(rabbit_auth_mechanism_cr_demo)의 이름이고 기본 활성이 아니다. RabbitMQ 는 SCRAM-SHA 를 구현하지 않으므로 SaslScram 에 대응하는 AMQP 기구가 없다는 것 자체는 사실이다. - -## 문제 - -RABBIT-CR-DEMO 는 RabbitMQ 의 시연용 challenge-response 인증 기구(rabbit_auth_mechanism_cr_demo)의 이름이고 기본 활성이 아니다. - -RabbitMQ 는 SCRAM-SHA 를 구현하지 않으므로 SaslScram 에 대응하는 AMQP 기구가 없다는 것 자체는 사실이다. - -## 결론 - -문제는 그 사실을 다루는 방식이 같은 파일 안에서 일관되지 않다는 것이다. - -그리고 이웃 어댑터의 같은 클래스가 정확히 이 상황에 대한 규범을 적어 두었다. - -SaslScram 에도 그 규범이 적용되어야 한다. - -플러그인이 없는 브로커에서는 handshake 가 알아보기 어려운 오류로 실패하고, 있는 브로커에서는 시연용 기구로 인증한다. - -수정은 Nkey 와 같이 거부하거나, PLAIN 으로 매핑하고 그 이유를 주석으로 남기는 것이다. - -어느 쪽이든 지금처럼 말없이 데모 기구를 고르는 것보다 낫다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : SaslScram 참조 18건 검색과 매핑되는 기구 이름의 RabbitMQ 기본 활성 여부 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-rabbit#L274 에 있다. - -## 본문 - - - -`RABBIT-CR-DEMO` 는 RabbitMQ 의 시연용 challenge-response 인증 기구(`rabbit_auth_mechanism_cr_demo`)의 이름이고 기본 활성이 아니다. - -## SaslScram 참조 위치 - -:::evidence key="messaging-rabbit-f03" alt="코드베이스에서 SaslScram 를 검색한 출력 18줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="SaslScram 코드베이스 검색 — 18줄 · exit 0" zoom="true" -::: - -## 대응 기구가 없다는 것 자체는 사실이다 - -RabbitMQ 는 SCRAM-SHA 를 구현하지 않는다. 문제는 그 사실을 다루는 방식이 같은 파일 안에서 일관되지 않다는 것이다 — 이웃 어댑터의 같은 클래스가 정확히 이 상황에 대한 규범을 적어 두었고, `SaslScram` 에도 그 규범이 적용되어야 한다. - -## 두 브로커 형상에서 결과가 다르다 - -플러그인이 없는 브로커에서는 handshake 가 알아보기 어려운 오류로 실패하고, 있는 브로커에서는 시연용 기구로 인증한다. 수정은 `Nkey` 와 같이 거부하거나, `PLAIN` 으로 매핑하고 그 이유를 주석으로 남기는 것이다. - -## 확인하지 못한 것 - -이 기구를 실제 브로커에 붙여 보지 않았다. 이름과 RabbitMQ 의 기본 활성 상태로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-rabbit-f05.md b/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-rabbit-f05.md deleted file mode 100644 index fc03f68..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-rabbit-f05.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -kind: CASE -slug: messaging-rabbit-f05 -title: pause 의 의미가 SPI 하나 뒤에서 두 브로커에 다르게 구현된다 -topic: transport-and-provider-semantics -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-rabbit-f05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-rabbit-f05 - file: ../../../final/evidence/rendered/messaging-rabbit-f05.svg -evidence: - - ../../../final/evidence/raw/messaging-rabbit-f05.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-rabbit#L335 이다. -module: messaging-rabbit -priority: P3 ---- - -# pause 의 의미가 SPI 하나 뒤에서 두 브로커에 다르게 구현된다 - -호출자가 이 단계를 기다리고 나면 "일시정지되었다" 고 읽는다. 실제로 일어난 것은 onMessage 가 이후 배달에 false 를 답하기 시작한 것뿐이고, 리스너 컨테이너는 계속 배달을 밀며 그 배달들은 미확인 상태로 재배달된다. - -## 문제 - -호출자가 이 단계를 기다리고 나면 "일시정지되었다" 고 읽는다. - -실제로 일어난 것은 onMessage 가 이후 배달에 false 를 답하기 시작한 것뿐이고, 리스너 컨테이너는 계속 배달을 밀며 그 배달들은 미확인 상태로 재배달된다. - -## 결론 - -즉 정지가 아니라 거부-재배달 루프다. - -Kafka 쪽은 같은 SPI 를 정반대로 구현하고 그 이유를 적는다. - -AMQP 에는 대응하는 수단이 있다 — basicCancel 로 소비자를 취소하거나 컨테이너를 멈추는 것. - -지금 구현이 그것을 하지 않는 이유는 어디에도 없다. - -전용 시험(aPausedQueueRefusesDeliveriesSoTheBrokerRedeliversThem)의 이름이 이미 실제 동작을 정확히 말한다. - -그러므로 수정은 둘 중 하나다 — 컨테이너를 실제로 멈추거나, SPI 의 javadoc 에 "브로커에 따라 정지가 거부-재배달일 수 있다" 를 명시하는 것. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : 두 브로커의 pause 구현 본문 대조와 리스너 컨테이너의 배달 지속 여부 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-rabbit#L335 에 있다. - -## 본문 - - - -호출자가 pause 단계를 기다리고 나면 "일시정지되었다" 고 읽는다. 실제로 일어난 것은 `onMessage` 가 이후 배달에 `false` 를 답하기 시작한 것뿐이고, 리스너 컨테이너는 계속 배달을 밀며 그 배달들은 미확인 상태로 재배달된다 — 정지가 아니라 거부-재배달 루프다. - -## pause 이후 실제로 일어나는 것 - -:::evidence key="messaging-rabbit-f05" alt="분석 문서 final/document.md#a19-messaging-rabbit 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a19-messaging-rabbit 발췌 — 15줄" zoom="true" -::: - -## Kafka 쪽은 같은 SPI 를 정반대로 구현한다 - -그리고 그 이유를 적는다. AMQP 에는 대응하는 수단이 있다 — `basicCancel` 로 소비자를 취소하거나 컨테이너를 멈추는 것. 지금 구현이 그것을 하지 않는 이유는 어디에도 없다. - -## 시험 이름이 이미 실제 동작을 말한다 - -`aPausedQueueRefusesDeliveriesSoTheBrokerRedeliversThem`. 수정은 둘 중 하나다 — 컨테이너를 실제로 멈추거나, SPI 의 javadoc 에 "브로커에 따라 정지가 거부-재배달일 수 있다" 를 명시하는 것. - -## 확인하지 못한 것 - -실제 브로커로 일시정지 이후의 재배달을 관측하지 않았다. 두 구현의 코드 대조로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-testkit-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-testkit-f03.md deleted file mode 100644 index e003288..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/case/case-messaging-testkit-f03.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -kind: CASE -slug: messaging-testkit-f03 -title: Faults 내부클래스 57줄이 3개 모듈에 바이트 단위로 복제되어 있다 -topic: transport-and-provider-semantics -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-testkit-f03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-testkit-f03 - file: ../../../final/evidence/rendered/messaging-testkit-f03.svg -evidence: - - ../../../final/evidence/raw/messaging-testkit-f03.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-testkit#L960 이다. -module: messaging-testkit -priority: P3 ---- - -# Faults 내부클래스 57줄이 3개 모듈에 바이트 단위로 복제되어 있다 - -sha256 이 세 곳 모두 3028b459… 로 동일하다(EVD-299). 총 171줄. - -## 문제 - -sha256 이 세 곳 모두 3028b459… 로 동일하다(EVD-299). - -총 171줄. - -## 결론 - -messaging-testkit/src/main 에 DefaultFaultController (또는 RecordingFaultController) 하나를 두고 세 하니스가 그것을 필드로 갖게 하면 된다. - -MessagingAdapterHarness.faults() 의 반환 타입은 FaultController 그대로이므로 외부 API 변경이 없다. - -이 복제가 위험한 이유는 P2 와 겹친다: rejectPublish 의 전송 증거를 고치려면 지금은 세 파일을 고쳐야 하고, 세 파일이 어긋나면 어댑터마다 다른 결함 의미를 갖게 된다 — 이 리프가 존재하는 이유 자체를 무너뜨린다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : Faults 참조 12건 검색과 세 복제본의 sha256 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-testkit#L960 에 있다. - -## 본문 - - - -`sha256` 이 세 곳 모두 `3028b459…` 로 동일하다(`EVD-299`). 총 171줄. - -## Faults 참조 위치 - -:::evidence key="messaging-testkit-f03" alt="코드베이스에서 Faults 를 검색한 출력 12줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="Faults 코드베이스 검색 — 12줄 · exit 0" zoom="true" -::: - -## 통합 방법 - -`messaging-testkit/src/main` 에 `DefaultFaultController`(또는 `RecordingFaultController`) 하나를 두고 세 하니스가 그것을 필드로 갖게 하면 된다. `MessagingAdapterHarness.faults()` 의 반환 타입은 `FaultController` 그대로이므로 외부 API 변경이 없다. - -## 이 복제가 위험한 이유는 P2 와 겹친다 - -`rejectPublish` 의 전송 증거를 고치려면 지금은 세 파일을 고쳐야 하고, 세 파일이 어긋나면 어댑터마다 다른 결함 의미를 갖게 된다 — 이 리프가 존재하는 이유 자체를 무너뜨린다. - -## 확인하지 못한 것 - -없음 — 세 사본의 해시가 같다는 것을 직접 확인했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/question/openquestion-messaging-observability-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/question/openquestion-messaging-observability-f03.md deleted file mode 100644 index b5aaf25..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/question/openquestion-messaging-observability-f03.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -kind: QUESTION -slug: messaging-observability-f03 -title: 브로커 홉 추적기가 소비자를 갖지 않는다 -topic: transport-and-provider-semantics -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: open-question:messaging-observability-f03 -questionStatus: OPEN -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-observability#L690 ---- - -# 브로커 홉 추적기가 소비자를 갖지 않는다 - -브로커를 사이에 둔 두 span 을 잇는 명시된 수단이 아무 데서도 호출되지 않는다. 어댑터가 각자 하고 있는지, 아무도 하지 않는지가 아직 확인되지 않았다. - -## 관계 - -- **타입이 문서화한 불변식은 타입이 강제한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 사실 - -MessagingTracer 의 leaf 밖 참조가 0 이다. git grep -n -w MessagingTracer -- src 가 이 leaf 만 돌려준다. - -이 클래스가 존재하는 이유는 "the only way the two spans meet is if the context travels in the message headers" 다. - -messaging-core-api 의 TraceContext 가 봉투 필드로 있고(그쪽 §4.11) 어댑터가 헤더를 매핑한다. 그런데 traceparent · tracestate · baggage 를 헤더로 옮기는 명시된 수단을 아무도 쓰지 않는다. - -## 미지수 - -어댑터들이 세 헤더 이름을 각자 다루고 있는가. 그렇다면 MessageHeaders.platform 사용 여부와 빈 추적 처리가 어댑터마다 다를 수 있다. - -## 선택지 - -어댑터가 MessagingTracer 를 쓰게 한다 - 세 이름의 처리와 빈 추적 규칙이 한 곳에 모인다. - -어댑터가 대신 하고 있음을 확인하고 이 클래스를 정리한다 - 확인이 먼저다. 중복이 아니라 부재일 수 있다. - -## 다음 검증 - -messaging-kafka 와 messaging-rabbit 의 헤더 매퍼가 세 이름을 어떻게 다루는지 확인한다. 판정이 그 두 leaf 의 사실에 걸린다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/question/openquestion-messaging-spring-cloud-stream-bridge-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/question/openquestion-messaging-spring-cloud-stream-bridge-f02.md deleted file mode 100644 index d14edaf..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/question/openquestion-messaging-spring-cloud-stream-bridge-f02.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -kind: QUESTION -slug: messaging-spring-cloud-stream-bridge-f02 -title: 브리지의 바인더 쪽 절반이 없다 -topic: transport-and-provider-semantics -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: open-question:messaging-spring-cloud-stream-bridge-f02 -questionStatus: OPEN -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-spring-cloud-stream-bridge#L561 ---- - -# 브리지의 바인더 쪽 절반이 없다 - -정책 · 검증 · 정직성 세 층이 완성돼 있고 그것을 실제 바인딩에 연결하는 코드가 없다. leaf 이름이 약속하는 것의 절반만 존재한다. - -## 관계 - -- **허용 의존 목록은 상한이므로 미사용을 잡지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **등록을 받는 컴포넌트는 해제도 제공한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **함께 읽히는 두 맵은 한 값으로 묶는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **에러 메시지가 지시하는 설정은 그 설정을 읽는 코드와 함께 존재해야 한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **한 개념의 등록 상태를 두 객체가 나눠 갖지 않는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 사실 - -ChannelSend 와 BridgedHandler 두 함수형 인터페이스가 바인더 접촉면이고, 그 구현이 저장소에 없다. git grep -n 'ChannelSend\|BridgedHandler' -- src 가 이 leaf 와 그 테스트만 돌려준다. - -MessagingBindingBridge javadoc 은 존재 이유로 "a service already has Stream bindings and needs to reach the same destinations without a rewrite" 를 든다. - -runtime_memberships: [] 와 정합하므로 오늘의 결함은 아니다. - -## 미지수 - -이 leaf 를 완성할 것인가. messaging-kafka-share-experimental §17 첫 항목과 같은 질문이다. - -## 선택지 - -바인더 어댑터를 만든다 - 두 인터페이스에 구현이 생기고 leaf 이름이 약속하는 경로가 닫힌다. - -파생 프로젝트의 구현점임을 명시한다 - 구현 부재가 미완이 아니라 설계임을 javadoc 이 말한다. - -## 다음 검증 - -evidence/raw/296 §A · §B 로 구현 부재는 확정된다. 완성 여부의 결정은 저장소 안의 사실로 닫히지 않는다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/reference/reference-messaging-kafka-share-experimental-f06.md b/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/reference/reference-messaging-kafka-share-experimental-f06.md deleted file mode 100644 index 17fe3fe..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/reference/reference-messaging-kafka-share-experimental-f06.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-kafka-share-experimental-f06 -title: 에러 메시지가 지시하는 설정은 그 설정을 읽는 코드와 함께 존재해야 한다 -topic: transport-and-provider-semantics -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-kafka-share-experimental-f06 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-kafka-share-experimental#L512 ---- - -# 에러 메시지가 지시하는 설정은 그 설정을 읽는 코드와 함께 존재해야 한다 - -## 관계 - -- **"등록"이 아무것도 등록하지 않고 성공을 반환한다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. - -## 목적 - -운영자는 에러 메시지가 시키는 대로 프로퍼티를 설정하고, 아무 일도 일어나지 않는 것을 본다. 그때 의심할 대상이 자기 오타인지 코드인지 판단할 근거가 없다. - -## 규칙 - -1. 에러 메시지에 등장하는 프로퍼티 키를 저장소에서 검색한다 - KAFKA_SHARE_DISABLED 메시지가 backend.messaging.experimental.kafka-share=true 를 지시한다. git grep -n 'kafka-share' -- src 는 이 leaf 의 문자열 하나만 돌려준다. - -2. 그 키를 읽는 바인딩이 있는지 확인한다 - enabled 는 KafkaShareProfile 생성자 인자이고, 그 profile 을 만드는 production 코드가 없다. - -3. 바인딩이 없으면 메시지에서 키를 빼거나 바인딩을 함께 만든다 - 지시가 실행 가능해야 지시다. - -## 적용 조건 - -실패 메시지가 복구 방법을 문장으로 제시하는 모든 자리. - -## 예외 - -SSOT 가 이 규칙의 반례를 적지 않았다. 배선 계획이 확정된 키를 미리 안내하는 경우가 있다면 그 사실이 메시지 자체에 있어야 한다. - -## 예시 - -git grep -n 'kafka-share' -- src 의 결과가 문자열 하나라는 것과, KafkaShareProfile 을 만드는 production 코드가 없다는 것. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/reference/reference-messaging-runtime-core-f04.md b/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/reference/reference-messaging-runtime-core-f04.md deleted file mode 100644 index 238d4dc..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/reference/reference-messaging-runtime-core-f04.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-runtime-core-f04 -title: 안정 코드는 운영자의 행동이 갈리는 지점마다 나눈다 -topic: transport-and-provider-semantics -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-runtime-core-f04 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-runtime-core#L736 ---- - -# 안정 코드는 운영자의 행동이 갈리는 지점마다 나눈다 - -## 관계 - -- **관측이 구현·호출부·주입 자리를 모두 갖추고도 출하에서 no-op이다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **소비 오케스트레이터가 조립되지 않는다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **선언된 content type과 실제 인코딩이 조용히 갈라질 수 있다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. - -## 목적 - -PUBLISH_DEADLINE_EXCEEDED 하나가 두 completion 에 붙는데, 두 경우의 운영자 행동은 정반대다. 전송 전이면 버려도 안전하고, 전송 후면 같은 messageId 로만 재발행해야 한다. 대시보드가 코드로 집계하는 순간 그 구분이 사라진다. - -## 규칙 - -1. 코드 하나가 몇 개의 completion 에 붙는지 센다 - 전송 전이면 REJECTED(:16-21), 전송 후면 AMBIGUOUS(:42-47) 다. - -2. 각 completion 에서 운영자가 할 일이 같은지 묻는다 - 다르면 코드가 갈라져야 한다. - -3. FailureDescriptor.code 의 계약을 기준으로 판정한다 - 그 필드는 "stable, machine-readable code" 이고, completion 을 함께 읽어야만 뜻이 정해지는 코드는 그 계약을 지키지 못한다. - -## 적용 조건 - -실패를 안정 코드로 분류하고 그 코드가 대시보드·알람의 집계 키가 되는 모든 자리. - -## 예외 - -SSOT 가 이 규칙의 반례를 적지 않았다. 코드를 합치고 completion 을 항상 함께 노출하는 설계가 있다면 그 규약이 어딘가에 선언돼야 한다. - -## 예시 - -PUBLISH_DEADLINE_EXCEEDED 가 붙는 두 위치. 확인 방법은 git grep -n 'PUBLISH_DEADLINE_EXCEEDED' -- src/messaging/messaging-runtime-core 다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/reference/reference-messaging-schema-json-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/reference/reference-messaging-schema-json-f02.md deleted file mode 100644 index e3f438d..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/reference/reference-messaging-schema-json-f02.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-schema-json-f02 -title: 실패 코드는 운영자의 다음 행동이 갈리는 지점마다 나눈다 -topic: transport-and-provider-semantics -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-schema-json-f02 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-schema-json#L472 ---- - -# 실패 코드는 운영자의 다음 행동이 갈리는 지점마다 나눈다 - -## 관계 - -- **포맷 중립 payload 정책이, 자기 상수를 두고 JSON codec의 상수를 참조한다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. - -## 목적 - -여섯 갈래 중 셋(중복 키 · trailing token · 깊이 초과)은 적대적 입력의 신호이고 나머지 셋은 계약 불일치다. 하나의 코드로 접히면 DLQ 를 보는 운영자가 그 둘을 구분할 수 없다. FailureDescriptor.exceptionType 마저 비어 있어 원인 클래스도 남지 않는다. - -## 규칙 - -1. 한 catch 로 모이는 실패의 종류를 센다 - decode 의 catch (JacksonException) 단일 분기가 깊이 초과 · 중복 키 · trailing token · 미지 필드 · 문서 길이 · 토큰 길이를 전부 JSON_DECODE_FAILED 로 만든다. - -2. 각각에 대해 운영자가 할 일이 같은지 묻는다 - 공격 신호는 차단으로, 계약 불일치는 스키마 수정으로 이어진다. - -3. 코드를 나누거나 최소한 원인 타입을 채운다 - JacksonException 하위 타입별로 코드를 나누거나, exceptionType 에 원인 클래스 단순명을 넣는다. - -## 적용 조건 - -라이브러리 예외 하나를 잡아 자체 실패 코드로 옮기는 모든 디코더·파서 경계. - -## 예외 - -SSOT 가 이 규칙의 반례를 적지 않았다. 여섯 갈래의 대응이 실제로 동일하다면 하나의 코드가 맞지만, 여기서는 셋이 보안 신호다. - -## 예시 - -JacksonMessageCodec.java:155-158 의 단일 catch. 확인 방법은 JacksonMessageCodecTest 의 네 케이스가 전부 같은 예외 타입을 기대한다는 것이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/reference/reference-messaging-transport-spi-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/reference/reference-messaging-transport-spi-f02.md deleted file mode 100644 index 2e086cb..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/reference/reference-messaging-transport-spi-f02.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-transport-spi-f02 -title: 멱등 종료를 보장하는 컴포넌트는 종료 이후의 등록도 정의한다 -topic: transport-and-provider-semantics -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-transport-spi-f02 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-transport-spi#L668 ---- - -# 멱등 종료를 보장하는 컴포넌트는 종료 이후의 등록도 정의한다 - -## 관계 - -- **드레인 마감 30초가 세 곳에서 독립적으로 결정된다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. - -## 목적 - -이 클래스의 다른 모든 경로가 "정확히 한 번 close" 를 CAS 로 보장한다. 이 한 창만 그 보장 밖이고, 남는 것은 닫히지 않은 브로커 연결이다 — §13 의 세 번째 결함과 같은 결과다. - -## 규칙 - -1. 종료 메서드가 상태를 비우는 순서를 본다 - close() 가 draining 은 synchronized 로 비우고, current 는 List.copyOf(current.keySet()) 후 개별 remove 한다. - -2. 그 사이에 등록이 들어올 수 있는지 본다 - install 이 새 세대를 넣으면 그 세대는 닫히지 않는다. - -3. 종료 이후의 등록 동작을 정의한다 - close() 에 종료 플래그를 두고, 그 이후의 install 은 즉시 runtime.close() 하게 한다. - -## 적용 조건 - -세대 교체나 참조 계수로 자원 종료를 보장하면서 등록 API 를 함께 노출하는 registry. - -## 예외 - -종료 중 등록이 타입으로 불가능하면 대상이 아니다. 여기서는 install 이 종료 여부를 보지 않는다. - -## 예시 - -DefaultMessagingRuntimeRegistry.java:141-156. 확인 방법은 코드 검토이고, 테스트로 재현하려면 close() 중 install 을 끼워 넣어야 한다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/reference/reference-messaging-transport-spi-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/reference/reference-messaging-transport-spi-f03.md deleted file mode 100644 index f6f6d3b..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/transport-and-provider-semantics/reference/reference-messaging-transport-spi-f03.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-transport-spi-f03 -title: 같은 개념의 sentinel은 계층을 넘어 하나로 정한다 -topic: transport-and-provider-semantics -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-transport-spi-f03 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-transport-spi#L677 ---- - -# 같은 개념의 sentinel은 계층을 넘어 하나로 정한다 - -## 관계 - -- **드레인 마감 30초가 세 곳에서 독립적으로 결정된다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. - -## 목적 - -PauseResumeController 가 소비자 0 이라 오늘 충돌하지 않는다. 그것을 배선하는 사람이 변환을 넣어야 하고, 빠뜨리면 "" 가 이름이 "" 인 파티션을 가리킨다 — 실패하지 않고 아무것도 일시정지하지 않는다. - -## 규칙 - -1. 같은 개념의 전체 지정자가 계층마다 무엇인지 모은다 - TransportConsumerRegistration.pause 는 빈 문자열이 전체, PauseResumeController.pause 는 "*" 가 전체다. - -2. 변환 지점이 코드에 있는지 확인한다 - 지금은 두 계층을 잇는 코드 자체가 없다. - -3. sentinel 을 통일하거나 타입으로 바꾼다 - Optional 이면 sentinel 자체가 사라진다. - -## 적용 조건 - -"전체" · "없음" · "기본" 같은 특수 의미를 문자열 값에 실어 계층을 넘기는 모든 API. - -## 예외 - -두 계층이 절대 만나지 않는다면 대상이 아니다. 이 둘은 만나도록 설계된 상하 계층이다. - -## 예시 - -두 javadoc 의 sentinel 정의. 확인 방법은 그 둘을 대조하는 것이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-admin-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-admin-f01.md deleted file mode 100644 index 7952eb5..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-admin-f01.md +++ /dev/null @@ -1,97 +0,0 @@ ---- -kind: CASE -slug: grpc-admin-f01 -title: rejectNewAdmission() 이 단계만 기록하고 아무것도 거절하지 않는다 -topic: verification-path-coverage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-admin-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-admin-f01 - file: ../../../final/evidence/rendered/grpc-admin-f01.svg - - key: grpc-admin-f01-diagram - file: ../../../final/assets/diagrams/grpc-admin-f01.svg -evidence: - - ../../../final/evidence/raw/grpc-admin-f01.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-admin#L131 이다. -module: grpc-admin -priority: P2 ---- - -# rejectNewAdmission() 이 단계만 기록하고 아무것도 거절하지 않는다 - -javadoc 은 "Starts refusing new calls" 라고 적는다. 실제로 하는 일은 단계 목록에 표식을 넣는 것뿐이다. - -## 문제 - -javadoc 은 "Starts refusing new calls" 라고 적는다. - -실제로 하는 일은 단계 목록에 표식을 넣는 것뿐이다. - -## 결론 - -조정자는 GrpcAdmissionController 를 협력자로 들고 있는데, 그것을 쓰는 곳은 inFlightAdmitted() 의 조회 하나다. - -그리고 승인 제어기의 공개 표면에 승인을 멈추는 메서드가 없다. - -close·drain·refuseNew 에 해당하는 것이 없다. - -그러므로 배수가 시작된 뒤에도 tryAdmit() 은 용량이 남아 있는 한 계속 승인한다. - -admittingNewCalls() 은 그 사실과 무관하게 거짓을 돌려준다 — 표식을 읽기 때문이다. - -운영자나 상위 코드가 이 값을 보고 "더 이상 받지 않는다" 고 읽으면 틀린 답을 얻는다. - -배수 테스트가 단언하는 것은 admittingNewCalls() 의 값이고, 단계 이후에 tryAdmit() 이 거절되는지는 어느 테스트도 묻지 않는다. - -승인 제어기에 승인 중단 상태를 두고(stopAdmitting() 과 그것을 보는 tryAdmit), 조정자의 rejectNewAdmission 이 그것을 부르게 한다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : GrpcAdmissionController 참조 16건 전수 검색과 두 클래스의 공개 표면 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-admin#L131 에 있다. - -## 본문 - - - -javadoc 은 "Starts refusing new calls" 라고 적는다. 실제로 하는 일은 단계 목록에 표식을 넣는 것뿐이다. - -## 승인 제어기에 없는 것 - -:::evidence key="grpc-admin-f01-diagram" alt="tryAdmit 과 inFlightAdmitted 가 승인 제어기 공개 표면 안에 놓이고 승인 중단 메서드가 바깥에 빗금으로 놓인다" caption="승인 제어기에 없는 것" zoom="false" -::: - -조정자는 `GrpcAdmissionController` 를 협력자로 들고 있는데, 그것을 쓰는 곳은 `inFlightAdmitted()` 의 조회 하나다. 그리고 승인 제어기의 공개 표면에 승인을 멈추는 메서드가 없다 — `close`·`drain`·`refuseNew` 에 해당하는 것이 없다. - -## GrpcAdmissionController 참조 위치 - -:::evidence key="grpc-admin-f01" alt="코드베이스에서 GrpcAdmissionController 를 검색한 출력 16줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcAdmissionController 코드베이스 검색 — 16줄 · exit 0" zoom="true" -::: - -## 배수 이후에도 tryAdmit 은 계속 승인한다 - -용량이 남아 있는 한 그렇다. `admittingNewCalls()` 은 그 사실과 무관하게 거짓을 돌려준다 — 표식을 읽기 때문이다. 운영자나 상위 코드가 이 값을 보고 "더 이상 받지 않는다" 고 읽으면 틀린 답을 얻는다. - -## 테스트가 묻지 않는 것 - -배수 테스트가 단언하는 것은 `admittingNewCalls()` 의 값이고, 단계 이후에 `tryAdmit()` 이 거절되는지는 어느 테스트도 묻지 않는다. - -## 수정 - -승인 제어기에 승인 중단 상태를 두고(`stopAdmitting()` 과 그것을 보는 `tryAdmit`), 조정자의 `rejectNewAdmission` 이 그것을 부르게 한다. 지금 형태에서는 배수 순서를 지키는 장치가 순서 표식만 갖고 있다. - -## 확인하지 못한 것 - -배수 중에 GrpcAdmissionController 를 호출해 이 결함을 재현하지 않았다. 조정자와 제어기의 공개 표면에 승인을 멈추는 메서드가 없다는 것으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-advanced-bootstrap-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-advanced-bootstrap-f01.md deleted file mode 100644 index 2170c5d..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-advanced-bootstrap-f01.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -kind: CASE -slug: grpc-advanced-bootstrap-f01 -title: 등급 재정의에 하한이 없어 "켤 수 없다" 는 등급이 켜질 수 있다 -topic: verification-path-coverage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-advanced-bootstrap-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-advanced-bootstrap-f01 - file: ../../../final/evidence/rendered/grpc-advanced-bootstrap-f01.svg -evidence: - - ../../../final/evidence/raw/grpc-advanced-bootstrap-f01.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-advanced-bootstrap#L153 이다. -module: grpc-advanced-bootstrap -priority: P3 ---- - -# 등급 재정의에 하한이 없어 "켤 수 없다" 는 등급이 켜질 수 있다 - -GrpcCapabilityGrade 의 javadoc 이 두 등급을 단정한다. 그런데 등급은 런타임에 갈아끼울 수 있다. - -## 문제 - -GrpcCapabilityGrade 의 javadoc 이 두 등급을 단정한다. - -그런데 등급은 런타임에 갈아끼울 수 있다. - -## 결론 - -withGrade(EDITION_2026, ADVANCED_STABLE).enable(EDITION_2026) 이면 가드의 두 번째 조건이 통과한다. - -등급 올리기 자체는 의도된 기능이다 — 테스트 a deployment may raise a capability's grade on its own evidence 가 HEDGING(EXPERIMENTAL)을 ADVANCED_STABLE 로 올린다. - -문제는 그 재정의에 하한이 없다는 것이다. - -EXPERIMENTAL 을 올리는 것은 "실패 양식이 충분히 규명되지 않은 것을 감수한다" 는 판단이고 배포가 자기 증거로 내릴 수 있다. - -WATCH 를 올리는 것은 다르다. - -그 등급의 뜻이 "추적할 뿐 구현되지 않았다" 이므로 배포가 가질 자기 증거가 없다. - -그리고 승격 게이트는 WATCH 가 EXPERIMENTAL 을 먼저 거쳐야 한다는 규칙을 갖는데, 런타임 재정의는 그 게이트를 지나지 않는다. - -같은 리프 안에 문이 둘이고 증거 규칙은 한쪽에만 있다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : GrpcCapabilityGrade 참조 9건 검색과 등급 재정의 경로에 하한 가드가 있는지 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-advanced-bootstrap#L153 에 있다. - -## 본문 - - - -`GrpcCapabilityGrade` 의 javadoc 이 두 등급을 단정한다. 그런데 등급은 런타임에 갈아끼울 수 있다 — `withGrade(EDITION_2026, ADVANCED_STABLE).enable(EDITION_2026)` 이면 가드의 두 번째 조건이 통과한다. - -## GrpcCapabilityGrade 참조 위치 - -:::evidence key="grpc-advanced-bootstrap-f01" alt="코드베이스에서 GrpcCapabilityGrade 를 검색한 출력 9줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcCapabilityGrade 코드베이스 검색 — 9줄 · exit 0" zoom="true" -::: - -## 등급 올리기 자체는 의도된 기능이다 - -테스트 `a deployment may raise a capability's grade on its own evidence` 가 `HEDGING`(EXPERIMENTAL)을 `ADVANCED_STABLE` 로 올린다. - -## 문제는 그 재정의에 하한이 없다는 것이다 - -`EXPERIMENTAL` 을 올리는 것은 "실패 양식이 충분히 규명되지 않은 것을 감수한다" 는 판단이고 배포가 자기 증거로 내릴 수 있다. `WATCH` 를 올리는 것은 다르다 — 그 등급의 뜻이 "추적할 뿐 구현되지 않았다" 이므로 배포가 가질 자기 증거가 없다. - -## 문이 둘이고 증거 규칙은 한쪽에만 있다 - -승격 게이트는 `WATCH` 가 `EXPERIMENTAL` 을 먼저 거쳐야 한다는 규칙을 갖는데, 런타임 재정의는 그 게이트를 지나지 않는다. 수정은 `withGrade` 가 현재 등급이 `startable()` 인 능력에만 적용되게 하거나, `WATCH`·`DISABLED` 에서 올리는 재정의를 거부하는 것이다. - -## 확인하지 못한 것 - -등급 재정의로 그 능력을 켜는 것을 실행으로 재현하지 않았다. 코드 경로로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-advanced-bootstrap-f04.md b/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-advanced-bootstrap-f04.md deleted file mode 100644 index bbade2e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-advanced-bootstrap-f04.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -kind: CASE -slug: grpc-advanced-bootstrap-f04 -title: capabilitiesDraggedAlong 은 독립성을 증명하지 않는다. 상수를 상수와 비교한다 -topic: verification-path-coverage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-advanced-bootstrap-f04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-advanced-bootstrap-f04 - file: ../../../final/evidence/rendered/grpc-advanced-bootstrap-f04.svg -evidence: - - ../../../final/evidence/raw/grpc-advanced-bootstrap-f04.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-advanced-bootstrap#L286 이다. -module: grpc-advanced-bootstrap -priority: P3 ---- - -# capabilitiesDraggedAlong 은 독립성을 증명하지 않는다. 상수를 상수와 비교한다 - -javadoc 이 스스로 밝히듯 본문은 무조건 빈 목록이다. 그것을 단언하는 테스트는 리터럴이 리터럴임을 확인한다 — 증거를 능력마다 따로 기록했다는 §4 의 설계 속성과는 아무 연결이 없다. - -## 문제 - -javadoc 이 스스로 밝히듯 본문은 무조건 빈 목록이다. - -그것을 단언하는 테스트는 리터럴이 리터럴임을 확인한다 — 증거를 능력마다 따로 기록했다는 §4 의 설계 속성과는 아무 연결이 없다. - -## 결론 - -설계가 무너져 apply 가 다른 능력의 등급을 바꾸게 되어도 이 메서드는 여전히 빈 목록을 돌려준다. - -진짜 증거는 같은 테스트의 다른 줄에 있다. - -승격을 실제로 적용하고 다른 능력의 등급이 그대로임을 확인한다. - -이쪽은 설계가 무너지면 깨진다. - -앞선 판에서 이 메서드를 "주석이 주장하는 대신 테스트가 붙든다"는 확인된 설계로 분류했다. - -다시 읽으니 붙드는 것은 옆줄이고, 이 메서드는 그 옆줄이 있다는 사실을 가린다. - -메서드를 지우고 단언을 매트릭스 비교 쪽으로 남긴다. - -남겨 둔다면 실제로 매트릭스를 훑어 등급이 바뀐 다른 능력을 돌려주게 만든다 — 그때 비로소 이름이 하는 말과 본문이 맞는다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : capabilitiesDraggedAlong 본문이 돌려주는 값과 그것을 단언하는 테스트의 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-advanced-bootstrap#L286 에 있다. - -## 본문 - - - -javadoc 이 스스로 밝히듯 `capabilitiesDraggedAlong` 의 본문은 무조건 빈 목록이다. 그것을 단언하는 테스트는 리터럴이 리터럴임을 확인한다. - -## 본문이 무조건 빈 목록이다 - -:::evidence key="grpc-advanced-bootstrap-f04" alt="분석 문서 final/document.md#a20-grpc-advanced-bootstrap 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-advanced-bootstrap 발췌 — 15줄" zoom="true" -::: - -## 설계 속성과 연결이 없다 - -증거를 능력마다 따로 기록했다는 §4 의 설계 속성과 아무 연결이 없다. 설계가 무너져 `apply` 가 다른 능력의 등급을 바꾸게 되어도 이 메서드는 여전히 빈 목록을 돌려준다. - -## 진짜 증거는 같은 테스트의 다른 줄에 있다 - -승격을 실제로 적용하고 다른 능력의 등급이 그대로임을 확인한다. 이쪽은 설계가 무너지면 깨진다. - -## 앞선 판의 분류를 정정한다 - -앞선 판에서 이 메서드를 "주석이 주장하는 대신 테스트가 붙든다"는 확인된 설계로 분류했다. 다시 읽으니 붙드는 것은 옆줄이고, 이 메서드는 그 옆줄이 있다는 사실을 가린다. 메서드를 지우고 단언을 매트릭스 비교 쪽으로 남긴다. 남겨 둔다면 실제로 매트릭스를 훑어 등급이 바뀐 다른 능력을 돌려주게 만든다. - -## 확인하지 못한 것 - -테스트를 실행하지 않았다. 본문이 무조건 빈 목록을 돌려주고 단언이 그 리터럴을 비교한다는 것으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-advanced-compat-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-advanced-compat-f02.md deleted file mode 100644 index e2eb2a8..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-advanced-compat-f02.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -kind: CASE -slug: grpc-advanced-compat-f02 -title: 반응형 표면 두 타입은 테스트조차 없다 -topic: verification-path-coverage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-advanced-compat-f02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-advanced-compat-f02 - file: ../../../final/evidence/rendered/grpc-advanced-compat-f02.svg -evidence: - - ../../../final/evidence/raw/grpc-advanced-compat-f02.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-advanced-compat#L149 이다. -module: grpc-advanced-compat -priority: P3 ---- - -# 반응형 표면 두 타입은 테스트조차 없다 - -이 가족의 다른 미참조 Advanced 타입은 전부 테스트가 하나씩 있다 — 같은 패키지의 GrpcReactorCancellationBridge 는 2개 파일, GrpcReactorContextBridge 는 4개 파일에 등장한다. 두 타입은 채택자가 부를 표면이므로 production 참조 0 이 설계와 모순되지는 않는다. - -## 문제 - -이 가족의 다른 미참조 Advanced 타입은 전부 테스트가 하나씩 있다 — 같은 패키지의 GrpcReactorCancellationBridge 는 2개 파일, GrpcReactorContextBridge 는 4개 파일에 등장한다. - -두 타입은 채택자가 부를 표면이므로 production 참조 0 이 설계와 모순되지는 않는다. - -## 결론 - -어긋나는 것은 검증이다. - -채택자용 표면이면 그 계약이 무엇인지를 테스트가 붙들어야 하고, 이 가족은 다른 곳에서 정확히 그렇게 한다. - -ReactiveGrpcClient 의 javadoc 이 "Exposes a unary call as a Mono and a server stream as a Flux" 라고 적는데, 그 사상이 취소와 배압에서 어떻게 동작하는지는 어디에서도 확인되지 않는다. - -같은 리프의 GrpcReactorCancellationBridge 가 취소 전파를 다루므로 둘을 함께 검증할 자리가 이미 있다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : GrpcReactorCancellationBridge 참조 6건 검색과 같은 패키지 형제 타입의 테스트 등장 수 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-advanced-compat#L149 에 있다. - -## 본문 - - - -이 가족의 다른 미참조 Advanced 타입은 전부 테스트가 하나씩 있다 — 같은 패키지의 `GrpcReactorCancellationBridge` 는 2개 파일, `GrpcReactorContextBridge` 는 4개 파일에 등장한다. - -## GrpcReactorCancellationBridge 참조 위치 - -:::evidence key="grpc-advanced-compat-f02" alt="코드베이스에서 GrpcReactorCancellationBridge 를 검색한 출력 6줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcReactorCancellationBridge 코드베이스 검색 — 6줄 · exit 0" zoom="true" -::: - -## production 참조 0 은 설계와 모순되지 않는다 - -두 타입은 채택자가 부를 표면이다. - -## 어긋나는 것은 검증이다 - -채택자용 표면이면 그 계약이 무엇인지를 테스트가 붙들어야 하고, 이 가족은 다른 곳에서 정확히 그렇게 한다. `ReactiveGrpcClient` 의 javadoc 이 "Exposes a unary call as a `Mono` and a server stream as a `Flux`" 라고 적는데, 그 사상이 취소와 배압에서 어떻게 동작하는지는 어디에서도 확인되지 않는다. 같은 리프의 `GrpcReactorCancellationBridge` 가 취소 전파를 다루므로 둘을 함께 검증할 자리가 이미 있다. - -## 확인하지 못한 것 - -두 타입이 채택자 쪽에서 실제로 동작하는지 확인하지 않았다. 코틀린 툴체인이 없어 코틀린 계약 넷은 실행으로 확인할 수 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-advanced-compat-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-advanced-compat-f03.md deleted file mode 100644 index a285f8e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-advanced-compat-f03.md +++ /dev/null @@ -1,84 +0,0 @@ ---- -kind: CASE -slug: grpc-advanced-compat-f03 -title: 저장소가 참조 프록시 설정을 갖고 있는데, 그것을 판정할 코드에 넣지 않는다 -topic: verification-path-coverage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-advanced-compat-f03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-advanced-compat-f03 - file: ../../../final/evidence/rendered/grpc-advanced-compat-f03.svg -evidence: - - ../../../final/evidence/raw/grpc-advanced-compat-f03.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-advanced-compat#L162 이다. -module: grpc-advanced-compat -priority: P3 ---- - -# 저장소가 참조 프록시 설정을 갖고 있는데, 그것을 판정할 코드에 넣지 않는다 - -이 리프에는 두 가지가 함께 있다. GrpcWebProxyContract.violations(profile, exposedHeaders, allowedOrigins) — 프록시 설정이 브라우저 클라이언트에게 통할지 판정하는 코드. - -## 문제 - -이 리프에는 두 가지가 함께 있다. - -GrpcWebProxyContract.violations(profile, exposedHeaders, allowedOrigins) — 프록시 설정이 브라우저 클라이언트에게 통할지 판정하는 코드. - -## 결론 - -src/main/resources/envoy/envoy.yaml — 그 설정의 참조 구현. - -그리고 설정 파일 자신이 그 관계를 주장한다. - -GrpcWebProxyContract 는 이 파일에 대해 아무것도 단언하지 않는다. - -판정기는 시험에서 리터럴 집합을 받고, 참조 설정은 시험에서 문자열 포함으로만 확인된다. - -그래서 참조 설정이 grpc-status 를 노출하는지는 문자열이 확인하고, 그 노출이 충분한지 는 requiredExposedHeaders() 가 정의하는데, 둘을 잇는 코드가 없다. - -필수 트레일러 목록이 늘어나면 판정기는 새 항목을 요구하고 참조 설정은 옛 문자열로 계속 통과한다. - -이 리프의 다른 판정기들과 다른 점은 재료가 이미 저장소에 있다는 것이다 — grpc-testkit §17.5·grpc-server §17.1 은 스캔할 대상 자체를 만들어야 하지만, 여기서는 파일 하나를 파싱하면 된다. - -수정은 시험이 envoy.yaml 의 expose_headers 와 allow_origin(exact:)을 뽑아 GrpcWebProxyContract.violations 에 넣고 비어 있음을 단언하는 것이다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : GrpcWebProxyContract 참조 10건 검색과 저장소의 참조 프록시 설정 위치 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-advanced-compat#L162 에 있다. - -## 본문 - - - -이 리프에는 두 가지가 함께 있다 — `GrpcWebProxyContract.violations(profile, exposedHeaders, allowedOrigins)`(프록시 설정이 브라우저 클라이언트에게 통할지 판정하는 코드)와 `src/main/resources/envoy/envoy.yaml`(그 설정의 참조 구현). 설정 파일 자신이 그 관계를 주장한다. - -## GrpcWebProxyContract 참조 위치 - -:::evidence key="grpc-advanced-compat-f03" alt="코드베이스에서 GrpcWebProxyContract 를 검색한 출력 10줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcWebProxyContract 코드베이스 검색 — 10줄 · exit 0" zoom="true" -::: - -## 둘을 잇는 코드가 없다 - -판정기는 시험에서 리터럴 집합을 받고, 참조 설정은 시험에서 문자열 포함으로만 확인된다. 참조 설정이 `grpc-status` 를 노출하는지는 문자열이 확인하고, 그 노출이 **충분한지** 는 `requiredExposedHeaders()` 가 정의하는데, 둘을 잇는 코드가 없다. 필수 트레일러 목록이 늘어나면 판정기는 새 항목을 요구하고 참조 설정은 옛 문자열로 계속 통과한다. - -## 다른 판정기들과 다른 점 - -재료가 이미 저장소에 있다 — grpc-testkit §17.5·grpc-server §17.1 은 스캔할 대상 자체를 만들어야 하지만, 여기서는 파일 하나를 파싱하면 된다. 수정은 시험이 `envoy.yaml` 의 `expose_headers` 와 `allow_origin`(`exact:`)을 뽑아 `GrpcWebProxyContract.violations` 에 넣고 비어 있음을 단언하는 것이다. - -## 확인하지 못한 것 - -참조 프록시 설정을 실제 판정 코드에 넣어 결과를 보지 않았다. 실제 브라우저·프록시로 어떤 다리도 돌리지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-codegen-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-codegen-f01.md deleted file mode 100644 index 6f770fd..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-codegen-f01.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -kind: CASE -slug: grpc-codegen-f01 -title: Buf 수명주기 태스크 목록이 빌드와 대조되지 않는다. 테스트는 목록을 자기 자신과 비교한다 -topic: verification-path-coverage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-codegen-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-codegen-f01 - file: ../../../final/evidence/rendered/grpc-codegen-f01.svg -evidence: - - ../../../final/evidence/raw/grpc-codegen-f01.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-codegen#L190 이다. -module: grpc-codegen -priority: P3 ---- - -# Buf 수명주기 태스크 목록이 빌드와 대조되지 않는다. 테스트는 목록을 자기 자신과 비교한다 - -정책이 네 태스크 이름을 담고, javadoc 이 그 이유를 적는다. 그런데 그 네 이름은 저장소의 어떤 빌드 파일에도 없다. - -## 문제 - -정책이 네 태스크 이름을 담고, javadoc 이 그 이유를 적는다. - -그런데 그 네 이름은 저장소의 어떤 빌드 파일에도 없다. - -## 결론 - -그리고 테스트가 비교하는 대상이 실제 등록 태스크 집합이 아니다. - -첫 단언은 목록을 리터럴과, 셋째는 목록을 자기 자신과 비교한다. - -어느 것도 빌드가 그 단계를 등록했는지 묻지 않는다. - -Buf CLI 가 이 툴체인에 없다는 것은 build.gradle 이 이미 밝힌 사실이므로 태스크가 없는 것 자체는 놀랍지 않다. - -어긋난 것은 javadoc 의 주장이다 — 지금 형태에서 단계가 사라져도 테스트는 초록이다. - -수정은 missingTasks 에 Gradle 이 실제로 등록한 태스크 이름 집합을 넣는 검사를 만들거나(다른 가족의 레인 등록 검사와 같은 형태), CLI 가 없는 동안에는 그 문장을 "CI 환경이 채울 계약" 으로 낮추는 것이다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : 정책이 담은 네 태스크 이름을 빌드 파일 전수에서 검색하고, 테스트가 비교하는 대상 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-codegen#L190 에 있다. - -## 본문 - - - -정책이 네 태스크 이름을 담고, javadoc 이 그 이유를 적는다. 그런데 그 네 이름은 저장소의 어떤 빌드 파일에도 없다. - -## 정책이 담은 네 태스크 이름 - -:::evidence key="grpc-codegen-f01" alt="분석 문서 final/document.md#a20-grpc-codegen 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-codegen 발췌 — 15줄" zoom="true" -::: - -## 테스트가 비교하는 대상이 등록 태스크 집합이 아니다 - -첫 단언은 목록을 리터럴과, 셋째는 목록을 자기 자신과 비교한다. 어느 것도 빌드가 그 단계를 등록했는지 묻지 않는다. - -## 태스크가 없는 것 자체는 놀랍지 않다 - -Buf CLI 가 이 툴체인에 없다는 것은 build.gradle 이 이미 밝힌 사실이다. 어긋난 것은 javadoc 의 주장이다 — 지금 형태에서 단계가 사라져도 테스트는 초록이다. 수정은 `missingTasks` 에 Gradle 이 실제로 등록한 태스크 이름 집합을 넣는 검사를 만들거나, CLI 가 없는 동안에는 그 문장을 "CI 환경이 채울 계약" 으로 낮추는 것이다. - -## 확인하지 못한 것 - -실제 protoc 이나 Buf CLI 를 돌리지 않았다. 저장소에 둘 다 없다. 리플렉션이나 서비스 로더로 부르는 형태는 배제하지 못했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-codegen-f04.md b/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-codegen-f04.md deleted file mode 100644 index 4eb116f..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-codegen-f04.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -kind: CASE -slug: grpc-codegen-f04 -title: sha256: 검사가 길이 15자 이상만 요구한다. 저장소 자신의 테스트가 32자 해시를 통과시킨다 -topic: verification-path-coverage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-codegen-f04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-codegen-f04 - file: ../../../final/evidence/rendered/grpc-codegen-f04.svg -evidence: - - ../../../final/evidence/raw/grpc-codegen-f04.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-codegen#L287 이다. -module: grpc-codegen -priority: P3 ---- - -# sha256: 검사가 길이 15자 이상만 요구한다. 저장소 자신의 테스트가 32자 해시를 통과시킨다 - -같은 검사가 두 곳에 손으로 복사돼 있다. "sha256:" 이 7자이므로 뒤에 8자만 있으면 통과한다. - -## 문제 - -같은 검사가 두 곳에 손으로 복사돼 있다. - -"sha256:" 이 7자이므로 뒤에 8자만 있으면 통과한다. - -## 결론 - -sha256 digest 는 hex 64자다. - -그리고 이 헐거움이 테스트에 이미 드러나 있다. - -32자 — sha256 이 아니다. - -여기서는 "다른 해시" 역할이라 결과가 바뀌지 않지만, 형식 검사가 이런 값을 유효한 해시로 받는다는 사실 자체가 이 값 객체의 주장("the hashes that prove which bytes it was built from")을 약하게 만든다. - -sha256: 뒤 64자 hex 를 정규식으로 요구하고, 검사를 한 곳에 둔다 — 두 record 가 같은 규칙을 각자 적고 있는 지금 형태에서는 한쪽만 조여도 다른 쪽이 남는다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : 두 곳에 복사된 sha256 검사의 길이 조건과 저장소 테스트의 해시 리터럴 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-codegen#L287 에 있다. - -## 본문 - - - -같은 검사가 두 곳에 손으로 복사돼 있다. `"sha256:"` 이 7자이므로 뒤에 8자만 있으면 통과한다. sha256 digest 는 hex 64자다. - -## 길이 검사가 요구하는 최소 - -:::evidence key="grpc-codegen-f04" alt="분석 문서 final/document.md#a20-grpc-codegen 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-codegen 발췌 — 15줄" zoom="true" -::: - -## 헐거움이 테스트에 이미 드러나 있다 - -테스트가 쓰는 값이 32자 — sha256 이 아니다. 여기서는 "다른 해시" 역할이라 결과가 바뀌지 않지만, 형식 검사가 이런 값을 유효한 해시로 받는다는 사실 자체가 이 값 객체의 주장("the hashes that prove which bytes it was built from")을 약하게 만든다. - -## 수정 - -`sha256:` 뒤 64자 hex 를 정규식으로 요구하고, 검사를 한 곳에 둔다 — 두 record 가 같은 규칙을 각자 적고 있는 지금 형태에서는 한쪽만 조여도 다른 쪽이 남는다. - -## 확인하지 못한 것 - -짧은 해시를 실제 발행 경로에 넣어 통과를 관측하지 않았다. 길이 조건과 테스트 리터럴의 대조로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-core-api-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-core-api-f02.md deleted file mode 100644 index cea2fae..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-core-api-f02.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -kind: CASE -slug: grpc-core-api-f02 -title: 모듈 목록 테스트가 레지스트리와 목록을 붙들지 않는다 -topic: verification-path-coverage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-core-api-f02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-core-api-f02 - file: ../../../final/evidence/rendered/grpc-core-api-f02.svg -evidence: - - ../../../final/evidence/raw/grpc-core-api-f02.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-core-api#L157 이다. -module: grpc-core-api -priority: P3 ---- - -# 모듈 목록 테스트가 레지스트리와 목록을 붙들지 않는다 - -클래스 javadoc 이 두 SSOT 의 관계를 적는다. 그 테스트는 레지스트리를 읽지 않는다. - -## 문제 - -클래스 javadoc 이 두 SSOT 의 관계를 적는다. - -그 테스트는 레지스트리를 읽지 않는다. - -## 결론 - -다섯 테스트가 하는 일은 목록을 리터럴과 대조하고, 두 집합의 서로소를 확인하고, 누출 판정을 확인하는 것이다. - -modules.json 을 읽는 줄도, 파일 경로도 없다. - -두 목록은 오늘 일치한다 — 레지스트리의 grpc 계열 리프가 18 개이고 목록이 12 + 6 이다. - -어긋난 것은 그 일치를 무엇이 지키는가다. - -같은 저장소가 이 형태를 messaging 가족에서 이미 기록했다 — 정확한 목록은 레지스트리가 소유하므로 산문에서 세지 않는다, 세는 순간 다시 표류한다. - -수정은 테스트가 modules.json 을 읽어 grpc 계열 리프 집합과 두 상수 집합의 합집합을 대조하는 것이다. - -그 테스트가 있으면 새 리프가 어느 쪽에도 들어가지 않은 채 추가되는 것을 잡는다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : 테스트가 읽는 입력과 클래스 javadoc 이 선언한 두 SSOT 의 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-core-api#L157 에 있다. - -## 본문 - - - -클래스 javadoc 이 두 SSOT 의 관계를 적는다 — 레지스트리(`src/config/architecture/modules.json`)가 어떤 Gradle 프로젝트가 존재하는지의 SSOT 이고, 이 목록은 그중 Stable 계약이 덮는 것의 SSOT 다. - -## javadoc 이 적은 두 SSOT 의 관계 - -:::evidence key="grpc-core-api-f02" alt="분석 문서 final/document.md#a20-grpc-core-api 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-core-api 발췌 — 15줄" zoom="true" -::: - -## 그 테스트는 레지스트리를 읽지 않는다 - -다섯 테스트가 하는 일은 목록을 리터럴과 대조하고, 두 집합의 서로소를 확인하고, 누출 판정을 확인하는 것이다. `modules.json` 을 읽는 줄도, 파일 경로도 없다. - -## 두 목록은 오늘 일치한다 - -레지스트리의 grpc 계열 리프가 18 개이고 목록이 12 + 6 이다. 어긋난 것은 그 일치를 무엇이 지키는가다. - -## 같은 저장소가 이 형태를 messaging 가족에서 이미 기록했다 - -정확한 목록은 레지스트리가 소유하므로 산문에서 세지 않는다 — 세는 순간 다시 표류한다. 수정은 테스트가 `modules.json` 을 읽어 grpc 계열 리프 집합과 두 상수 집합의 합집합을 대조하는 것이다. 그 테스트가 있으면 새 리프가 어느 쪽에도 들어가지 않은 채 추가되는 것을 잡는다. - -## 확인하지 못한 것 - -레지스트리와 목록을 어긋나게 만들어 테스트가 통과하는지 실행으로 확인하지 않았다. 테스트가 레지스트리를 읽지 않는다는 것으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-server-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-server-f01.md deleted file mode 100644 index e2f27da..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-server-f01.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -kind: CASE -slug: grpc-server-f01 -title: 두 아키텍처 규칙이 저장소 소스에 적용되지 않는다 -topic: verification-path-coverage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-server-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-server-f01 - file: ../../../final/evidence/rendered/grpc-server-f01.svg - - key: grpc-server-f01-diagram - file: ../../../final/assets/diagrams/grpc-server-f01.svg -evidence: - - ../../../final/evidence/raw/grpc-server-f01.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-server#L140 이다. -module: grpc-server -priority: P2 ---- - -# 두 아키텍처 규칙이 저장소 소스에 적용되지 않는다 - -GrpcApplicationBoundaryRules javadoc: 세 적용처 중 저장소에 존재하는 것이 없다. 그리고 이 리프의 테스트는 저장소 파일을 훑지 않는다. - -## 문제 - -GrpcApplicationBoundaryRules javadoc: 세 적용처 중 저장소에 존재하는 것이 없다. - -그리고 이 리프의 테스트는 저장소 파일을 훑지 않는다. - -## 결론 - -인라인 소스 문자열을 넣는다. - -즉 규칙의 판정 로직은 검증되지만, 저장소의 어떤 파일도 그 판정을 받지 않는다. - -GrpcServiceAdapterMarker 는 규칙이 어댑터를 런타임에 열거할 수 있도록 만든 애너테이션인데, 그것을 붙인 타입도 그것을 읽는 코드도 없다. - -이 리프의 테스트에 저장소 소스를 훑는 검사를 추가한다 — src/**/*.java 를 읽어 GrpcRawApiImportRule.violations 를 돌리고 비어 있음을 단언하는 형태다. - -규칙이 이미 파일 이름과 소스 텍스트를 받는 서명이므로 재료는 갖춰져 있다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : GrpcApplicationBoundaryRules 참조 8건 검색과 규칙이 판정하는 입력의 출처 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-server#L140 에 있다. - -## 본문 - - - -`GrpcApplicationBoundaryRules` javadoc 이 드는 세 적용처 중 저장소에 존재하는 것이 없다. - -## 규칙이 닿지 않는 대상 - -:::evidence key="grpc-server-f01-diagram" alt="인라인 소스 문자열만 규칙이 판정하는 입력 안에 놓이고 저장소의 자바 파일이 바깥에 빗금으로 놓인다" caption="규칙이 닿지 않는 대상" zoom="false" -::: - -그리고 이 리프의 테스트는 저장소 파일을 훑지 않는다 — 인라인 소스 문자열을 넣는다. 즉 규칙의 판정 로직은 검증되지만, 저장소의 어떤 파일도 그 판정을 받지 않는다. - -## GrpcApplicationBoundaryRules 참조 위치 - -:::evidence key="grpc-server-f01" alt="코드베이스에서 GrpcApplicationBoundaryRules 를 검색한 출력 8줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcApplicationBoundaryRules 코드베이스 검색 — 8줄 · exit 0" zoom="true" -::: - -## 애너테이션도 붙은 곳이 없다 - -`GrpcServiceAdapterMarker` 는 규칙이 어댑터를 런타임에 열거할 수 있도록 만든 애너테이션인데, 그것을 붙인 타입도 그것을 읽는 코드도 없다. - -## 재료는 갖춰져 있다 - -이 리프의 테스트에 저장소 소스를 훑는 검사를 추가한다 — `src/**/*.java` 를 읽어 `GrpcRawApiImportRule.violations` 를 돌리고 비어 있음을 단언하는 형태다. 규칙이 이미 파일 이름과 소스 텍스트를 받는 서명이다. - -## 확인하지 못한 것 - -실제 서버를 세워 인터셉터 사슬을 돌리지 않았다. 이 리프에 서버를 만드는 코드가 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-testkit-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-testkit-f01.md deleted file mode 100644 index 1d9d646..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-testkit-f01.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -kind: CASE -slug: grpc-testkit-f01 -title: 네 레인이 check 에 붙지 않고, 이 가족을 이름으로 부르는 워크플로가 없다 -topic: verification-path-coverage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-testkit-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-testkit-f01 - file: ../../../final/evidence/rendered/grpc-testkit-f01.svg - - key: grpc-testkit-f01-diagram - file: ../../../final/assets/diagrams/grpc-testkit-f01.svg -evidence: - - ../../../final/evidence/raw/grpc-testkit-f01.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-testkit#L153 이다. -module: grpc-testkit -priority: P2 ---- - -# 네 레인이 check 에 붙지 않고, 이 가족을 이름으로 부르는 워크플로가 없다 - -ca.strict-test-lane.gradle 은 레인을 verification 그룹의 Test 태스크로 등록만 한다. check 에 연결하는 줄이 없다. - -## 문제 - -ca.strict-test-lane.gradle 은 레인을 verification 그룹의 Test 태스크로 등록만 한다. - -check 에 연결하는 줄이 없다. - -## 결론 - -그리고 CI 워크플로에서 이 가족을 이름으로 부르는 것이 없다. - -ci-quality-gates.yml 이 ./gradlew check 를 돌리므로 각 리프의 기본 test 는 돈다(이번에 확인: classes=71 tests=579 failures=0). - -네 증거 레인은 그 밖에 있다. - -결과적으로 이 플랫폼의 CONTRACT·TRANSPORT·FAULT 등급을 뒷받침하는 것은 25개 테스트이고, 그 25개는 누군가 명령을 직접 입력할 때만 돈다. - -build.gradle 자신이 그 위험을 적는다 — "A lane that discovers nothing fails, and none of them can serve an up-to-date result". - -첫 성질은 레인 규약이 지킨다. - -둘째 성질은 아무도 돌리지 않으면 무의미하다. - -같은 저장소가 이 형태를 두 번 기록했다 — 모듈 18 의 "붉은 게이트는 마지막으로 돌린 사람이 본 것을 보고한다" 와 mongo 가족의 릴리스 게이트 지형. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : 레인 등록 코드와 check 연결 여부 확인, 이 가족을 부르는 워크플로 검색 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-testkit#L153 에 있다. - -## 본문 - - - -`ca.strict-test-lane.gradle` 은 레인을 `verification` 그룹의 `Test` 태스크로 **등록만** 한다. `check` 에 연결하는 줄이 없다. - -## 레인을 부르는 경로 - -:::evidence key="grpc-testkit-f01-diagram" alt="직접 입력한 명령만 레인을 부르는 경로 안에 놓이고 check 연결과 CI 워크플로가 바깥에 빗금으로 놓인다" caption="레인을 부르는 경로" zoom="false" -::: - -그리고 CI 워크플로에서 이 가족을 이름으로 부르는 것이 없다. `ci-quality-gates.yml` 이 `./gradlew check` 를 돌리므로 각 리프의 기본 `test` 는 돈다(이번에 확인: classes=71 tests=579 failures=0). 네 증거 레인은 그 밖에 있다. - -## 레인이 등록되는 방식 - -:::evidence key="grpc-testkit-f01" alt="분석 문서 final/document.md#a20-grpc-testkit 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a20-grpc-testkit 발췌 — 15줄" zoom="true" -::: - -## 25개 테스트가 누군가 명령을 입력할 때만 돈다 - -이 플랫폼의 CONTRACT·TRANSPORT·FAULT 등급을 뒷받침하는 것이 그 25개다. build.gradle 자신이 위험을 적는다 — "A lane that discovers nothing fails, and **none of them can serve an up-to-date result**". 첫 성질은 레인 규약이 지킨다. 둘째 성질은 아무도 돌리지 않으면 무의미하다. - -## 같은 저장소가 이 형태를 두 번 기록했다 - -모듈 18 의 "붉은 게이트는 마지막으로 돌린 사람이 본 것을 보고한다" 와 mongo 가족의 릴리스 게이트 지형. 차이는 이쪽 레인이 오늘 초록이라는 것이고, 그것을 확인한 방법이 이번 분석에서 직접 돌린 것이라는 점이다. 수정은 세 레인(성능 제외)을 `check` 에 붙이거나, messaging 가족처럼 전용 워크플로를 두는 것이다. - -## 확인하지 못한 것 - -성능 레인을 돌리지 않았다. 세 레인은 이전 분석에서 통과를 확인했고 이번 회차에는 다시 돌리지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-testkit-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-testkit-f03.md deleted file mode 100644 index 17439f8..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-testkit-f03.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -kind: CASE -slug: grpc-testkit-f03 -title: 고장 레인의 유일한 실소켓 시험이 자기가 관측한 것을 버리고 리터럴로 증거를 만든다 -topic: verification-path-coverage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-testkit-f03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-testkit-f03 - file: ../../../final/evidence/rendered/grpc-testkit-f03.svg - - key: grpc-testkit-f03-diagram - file: ../../../final/assets/diagrams/grpc-testkit-f03.svg -evidence: - - ../../../final/evidence/raw/grpc-testkit-f03.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-testkit#L187 이다. -module: grpc-testkit -priority: P2 ---- - -# 고장 레인의 유일한 실소켓 시험이 자기가 관측한 것을 버리고 리터럴로 증거를 만든다 - -GrpcTransportEvidenceClassifierTest.aRealConnectionLossAfterAppStartIsCompletionUnknown 은 이 리프에서 유일하게 실제 연결을 작업 중에 끊는다. 서버 핸들러가 래치로 멈춰 있는 동안 server.close() 를 부른다. - -## 문제 - -GrpcTransportEvidenceClassifierTest.aRealConnectionLossAfterAppStartIsCompletionUnknown 은 이 리프에서 유일하게 실제 연결을 작업 중에 끊는다. - -서버 핸들러가 래치로 멈춰 있는 동안 server.close() 를 부른다. - -## 결론 - -거기까지는 진짜 고장이다. - -그런데 그 고장이 만들어 낸 관측이 어디에도 남지 않는다. - -주석이 "the exception is the observation" 이라고 말하는데 그 예외는 catch 안에서 사라지고, 변수는 null 로 고정되고, 단언은 자기 대입을 확인한다. - -그리고 GrpcFaultResult 에 들어가는 증거는 방금 일어난 호출에서 오지 않는다. - -즉 소켓은 실제로 죽었고, 그 죽음에서 읽어 낸 값은 하나도 쓰이지 않는다. - -이 시험이 실제로 증명하는 것은 applicationStarted == true 하나다. - -나머지는 분류기의 산술이고, 그것은 같은 파일의 다른 일곱 시험이 이미 소켓 없이 증명한다. - -이 형태를 이 리프 자신이 이름 붙여 두었다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : GrpcTransportEvidenceClassifierTest 참조 검색과 관측값·단언값의 대입 경로 추적 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-testkit#L187 에 있다. - -## 본문 - - - -`GrpcTransportEvidenceClassifierTest.aRealConnectionLossAfterAppStartIsCompletionUnknown` 은 이 리프에서 유일하게 실제 연결을 작업 중에 끊는다. 서버 핸들러가 래치로 멈춰 있는 동안 `server.close()` 를 부른다. 거기까지는 진짜 고장이다. - -## 증거가 오는 곳 - -:::evidence key="grpc-testkit-f03-diagram" alt="리터럴로 쓴 값만 증거가 오는 곳 안에 놓이고 실제 소켓 관측이 바깥에 빗금으로 놓인다" caption="증거가 오는 곳" zoom="false" -::: - -그 고장이 만들어 낸 관측이 어디에도 남지 않는다. 주석이 "the exception is the observation" 이라고 말하는데 그 예외는 `catch` 안에서 사라지고, 변수는 `null` 로 고정되고, 단언은 자기 대입을 확인한다. 그리고 `GrpcFaultResult` 에 들어가는 증거는 방금 일어난 호출에서 오지 않는다. - -## GrpcTransportEvidenceClassifierTest 참조 위치 - -:::evidence key="grpc-testkit-f03" alt="코드베이스에서 GrpcTransportEvidenceClassifierTest 를 검색한 출력 1줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcTransportEvidenceClassifierTest 코드베이스 검색 — 1줄 · exit 0" zoom="true" -::: - -## 이 시험이 실제로 증명하는 것 - -`applicationStarted == true` 하나다. 나머지는 분류기의 산술이고, 그것은 같은 파일의 다른 일곱 시험이 이미 소켓 없이 증명한다. - -## 한 단계 아래의 같은 치환이다 - -이 형태를 이 리프 자신이 이름 붙여 두었다. 여기서는 소켓이 열렸는데도 등급을 뒷받침해야 할 증거가 여전히 손으로 쓴 값이다. 수정은 `callUnary` 를 부른 스레드가 잡은 예외와 그 시점의 진행 상태를 밖으로 넘겨(`AtomicReference`) 그것으로 `ClientObservation` 을 구성하는 것이다 — 그러면 `sendCompleted`·`responseHeadersReceived` 가 관측값이 되고 이 시험이 FAULT 등급을 실제로 뒷받침한다. - -## 확인하지 못한 것 - -observedFailure 경로를 실행으로 확인하지 않았다. 대입과 단언이 같은 메서드 안에 있고 그 사이에 재대입이 없다는 것으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-testkit-f04.md b/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-testkit-f04.md deleted file mode 100644 index e800272..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-grpc-testkit-f04.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -kind: CASE -slug: grpc-testkit-f04 -title: 호환성 표의 레인 이름과 빌드의 레인 이름이 서로 다른 집합이다 -topic: verification-path-coverage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:grpc-testkit-f04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: grpc-testkit-f04 - file: ../../../final/evidence/rendered/grpc-testkit-f04.svg -evidence: - - ../../../final/evidence/raw/grpc-testkit-f04.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-testkit#L233 이다. -module: grpc-testkit -priority: P3 ---- - -# 호환성 표의 레인 이름과 빌드의 레인 이름이 서로 다른 집합이다 - -GrpcStableReleaseGate.evaluate 의 둘째 인자는 Map laneResults 이고, GrpcCompatibilityMatrix.missingResults 가 그 키를 자기 목록과 대조한다. 그 목록은 배포 조합의 이름이다 — boot-managed-platform, netty-shaded, upstream-grpc-java-override, protobuf-edition-2024 … 빌드가 등록하는 레인의 이름은 증거 종류다 — grpcInProcessContractTest, grpcNettyContractTest, grpcFaultTest, grpcPerformanceTest. - -## 문제 - -GrpcStableReleaseGate.evaluate 의 둘째 인자는 Map laneResults 이고, GrpcCompatibilityMatrix.missingResults 가 그 키를 자기 목록과 대조한다. - -그 목록은 배포 조합의 이름이다 — boot-managed-platform, netty-shaded, upstream-grpc-java-override, protobuf-edition-2024 … 빌드가 등록하는 레인의 이름은 증거 종류다 — grpcInProcessContractTest, grpcNettyContractTest, grpcFaultTest, grpcPerformanceTest. - -## 결론 - -두 집합의 교집합이 비어 있다. - -그래서 §17.1 을 고쳐 네 Gradle 레인을 check 에 붙이더라도, 그 결과가 이 게이트의 laneResults 를 채우지는 못한다 — 이름이 다른 축을 가리키기 때문이다. - -게이트가 요구하는 것은 "Boot 관리 플랫폼 조합에서 돌았는가" 이고, 레인이 답할 수 있는 것은 "전송 증거를 냈는가" 다. - -두 축이 다 필요하다는 것 자체는 옳다. - -기록하는 이유는 §17.1·§17.2 의 수정이 이것까지 함께 다루지 않으면 게이트가 여전히 손으로 만든 값을 먹는다는 점이다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : GrpcStableReleaseGate 참조 7건 검색과 두 레인 이름 집합의 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a20-grpc-testkit#L233 에 있다. - -## 본문 - - - -`GrpcStableReleaseGate.evaluate` 의 둘째 인자는 `Map laneResults` 이고, `GrpcCompatibilityMatrix.missingResults` 가 그 키를 자기 목록과 대조한다. - -## GrpcStableReleaseGate 참조 위치 - -:::evidence key="grpc-testkit-f04" alt="코드베이스에서 GrpcStableReleaseGate 를 검색한 출력 7줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcStableReleaseGate 코드베이스 검색 — 7줄 · exit 0" zoom="true" -::: - -## 두 집합의 교집합이 비어 있다 - -목록은 배포 조합의 이름이다 — `boot-managed-platform`, `netty-shaded`, `upstream-grpc-java-override`, `protobuf-edition-2024` …. 빌드가 등록하는 레인의 이름은 증거 종류다 — `grpcInProcessContractTest`, `grpcNettyContractTest`, `grpcFaultTest`, `grpcPerformanceTest`. - -## 그래서 §17.1 을 고쳐도 채워지지 않는다 - -네 Gradle 레인을 `check` 에 붙이더라도 그 결과가 이 게이트의 `laneResults` 를 채우지는 못한다 — 이름이 다른 축을 가리키기 때문이다. 게이트가 요구하는 것은 "Boot 관리 플랫폼 조합에서 돌았는가" 이고, 레인이 답할 수 있는 것은 "전송 증거를 냈는가" 다. - -## 두 축이 다 필요하다는 것 자체는 옳다 - -기록하는 이유는 §17.1·§17.2 의 수정이 이것까지 함께 다루지 않으면 게이트가 여전히 손으로 만든 값을 먹는다는 점이다. - -## 확인하지 못한 것 - -두 집합을 실제로 맞물려 게이트를 실행하지 않았다. 이름 목록의 대조로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-admin-api-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-admin-api-f01.md deleted file mode 100644 index 86d048d..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-admin-api-f01.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -kind: CASE -slug: messaging-admin-api-f01 -title: DestructiveOperationGuard 의 두 분기가 문서에도 없고 테스트에도 없다 -topic: verification-path-coverage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-admin-api-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-admin-api-f01 - file: ../../../final/evidence/rendered/messaging-admin-api-f01.svg - - key: messaging-admin-api-f01-diagram - file: ../../../final/assets/diagrams/messaging-admin-api-f01.svg -evidence: - - ../../../final/evidence/raw/messaging-admin-api-f01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-admin-api#L915 이다. -module: messaging-admin-api -priority: P2 ---- - -# DestructiveOperationGuard 의 두 분기가 문서에도 없고 테스트에도 없다 - -operation 불일치(:72-83)와 source 불일치(:84-93)는 클래스 javadoc 의 "Four conditions" 에 포함되지 않고, 두 에러 코드를 단언하는 테스트도 저장소 전체에 없다(EVD-303). 이 둘은 사소한 검사가 아니다 — 5번 분기의 인라인 주석이 정확히 말한다: "A guard that only checks presence and window lets a verified redrive approval authorise a destination deletion." 즉 검증된 승인으로 목적지 삭제를 인가하는 것을 막는 검사다. - -## 문제 - -operation 불일치(:72-83)와 source 불일치(:84-93)는 클래스 javadoc 의 "Four conditions" 에 포함되지 않고, 두 에러 코드를 단언하는 테스트도 저장소 전체에 없다(EVD-303). - -이 둘은 사소한 검사가 아니다 — 5번 분기의 인라인 주석이 정확히 말한다: "A guard that only checks presence and window lets a verified redrive approval authorise a destination deletion." 즉 검증된 승인으로 목적지 삭제를 인가하는 것을 막는 검사다. - -## 결론 - -혼동을 키우는 정황이 하나 더 있다. - -anApplicationRuntimeCannotRedrive 는 REDRIVE 요청에 REPLAY 승인을 넘기지만 adminCredentialPresent=false 라 분기 2에서 먼저 걸린다. - -불일치 조합이 테스트에 등장하지만 그 분기는 실행되지 않는다. - -수정: javadoc 을 여섯으로 고치고, new DestructiveOperationGuard(true) 위에서 operation 불일치·source 불일치 각각 1건씩 테스트를 추가한다. - -이 리프에는 이미 verified(operation, source, from, until) 헬퍼가 있어 두 줄이면 된다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : DestructiveOperationGuard 참조 19건 검색과 javadoc 이 든 조건 대비 본문 분기 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-admin-api#L915 에 있다. - -## 본문 - - - -operation 불일치(`:72-83`)와 source 불일치(`:84-93`)는 클래스 javadoc 의 "Four conditions" 에 포함되지 않고, 두 에러 코드를 단언하는 테스트도 저장소 전체에 없다(`EVD-303`). - -## 목록에서 빠진 검사 - -:::evidence key="messaging-admin-api-f01-diagram" alt="javadoc 이 든 조건 쪽에 승인 존재와 유효 기간과 빠진 둘이 놓이고 본문이 보는 조건 쪽에 작업 종류와 출처가 놓인다" caption="목록에서 빠진 검사" zoom="false" -::: - -## 사소한 검사가 아니다 - -5번 분기의 인라인 주석이 정확히 말한다 — "A guard that only checks presence and window lets a verified redrive approval authorise a destination deletion." 즉 **검증된 승인으로 목적지 삭제를 인가하는 것**을 막는 검사다. - -## DestructiveOperationGuard 참조 위치 - -:::evidence key="messaging-admin-api-f01" alt="코드베이스에서 DestructiveOperationGuard 를 검색한 출력 19줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DestructiveOperationGuard 코드베이스 검색 — 19줄 · exit 0" zoom="true" -::: - -## 혼동을 키우는 정황 - -`anApplicationRuntimeCannotRedrive` 는 `REDRIVE` 요청에 `REPLAY` 승인을 넘기지만 `adminCredentialPresent=false` 라 분기 2에서 먼저 걸린다. 불일치 조합이 테스트에 등장하지만 그 분기는 실행되지 않는다. - -## 수정 - -javadoc 을 여섯으로 고치고, `new DestructiveOperationGuard(true)` 위에서 operation 불일치·source 불일치 각각 1건씩 테스트를 추가한다. 이 리프에는 이미 `verified(operation, source, from, until)` 헬퍼가 있어 두 줄이면 된다. - -## 확인하지 못한 것 - -guard 를 실제로 호출해 두 분기에 도달시키지 않았다. 두 에러 코드를 단언하는 테스트가 저장소 전체에 없다는 것으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-cloudevents-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-cloudevents-f01.md deleted file mode 100644 index cab6d56..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-cloudevents-f01.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -kind: CASE -slug: messaging-cloudevents-f01 -title: 상호운용을 위한 매퍼가 명세 준수 이벤트를 분류되지 않은 예외로 거절한다 -topic: verification-path-coverage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-cloudevents-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-cloudevents-f01 - file: ../../../final/evidence/rendered/messaging-cloudevents-f01.svg - - key: messaging-cloudevents-f01-diagram - file: ../../../final/assets/diagrams/messaging-cloudevents-f01.svg -evidence: - - ../../../final/evidence/raw/messaging-cloudevents-f01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-cloudevents#L518 이다. -module: messaging-cloudevents -priority: P2 ---- - -# 상호운용을 위한 매퍼가 명세 준수 이벤트를 분류되지 않은 예외로 거절한다 - -fromCloudEvent가 new MessageId(UUID.fromString(event.getId()))로 id를 파싱한다. CloudEvents 1.0.2는 id를 비어 있지 않은 문자열로만 제약한다. - -## 관계 - -- **왕복이라 부르는 테스트는 무엇을 보존하지 않는지도 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **문서가 범위를 제한하면 그 제한을 누가 강제하는지 같이 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 문제 - -fromCloudEvent가 new MessageId(UUID.fromString(event.getId()))로 id를 파싱한다. - -CloudEvents 1.0.2는 id를 비어 있지 않은 문자열로만 제약한다. - -## 결론 - -런타임 probe 결과: 명세 예시 id A234-1234-1234 → java.lang.IllegalArgumentException: Invalid UUID string, UUIDv4 → java.lang.IllegalArgumentException: a message identity is UUIDv7. - -둘 다 MessagingException이 아니다. - -(1) 범위. - -v4 거절은 의도이고 테스트 주석이 그렇게 적는다. - -그러나 비UUID 거절은 어디에도 언급되지 않았고 그것은 다른 판단이다 — v4 거절은 "우리 정책", 비UUID 거절은 "CloudEvents 상호운용 포기"다. - -이 leaf의 존재 이유가 상호운용인데 명세 예시조차 받지 못한다. - -(2) 실패 어휘. - -같은 메서드의 다른 검증 실패 넷은 전부 MessageValidationException이고 안정 코드(CLOUDEVENT_TIME_REQUIRED 등)를 갖는다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : MessagingException 참조 36건 검색과 id 파싱 경로의 예외 분류 확인. 런타임 probe 로 거절을 실행 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-cloudevents#L518 에 있다. - -## 본문 - - - -`fromCloudEvent`가 `new MessageId(UUID.fromString(event.getId()))`로 id를 파싱한다. CloudEvents 1.0.2는 `id`를 비어 있지 않은 문자열로만 제약한다. - -## 구현이 받는 id 범위 - -:::evidence key="messaging-cloudevents-f01-diagram" alt="UUIDv7 id 만 구현이 받는 범위 안에 놓이고 명세 예시 id 와 UUIDv4 id 가 바깥에 빗금으로 놓인다" caption="구현이 받는 id 범위" zoom="false" -::: - -런타임 probe 결과 — 명세 예시 id `A234-1234-1234` → `java.lang.IllegalArgumentException: Invalid UUID string`, UUIDv4 → `java.lang.IllegalArgumentException: a message identity is UUIDv7`. **둘 다 `MessagingException`이 아니다.** - -## MessagingException 참조 위치 - -:::evidence key="messaging-cloudevents-f01" alt="코드베이스에서 MessagingException 를 검색한 출력 36줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingException 코드베이스 검색 — 36줄 · exit 0" zoom="true" -::: - -## 범위 — 두 거절의 성격이 다르다 - -v4 거절은 의도이고 테스트 주석이 그렇게 적는다. 그러나 **비UUID 거절은 어디에도 언급되지 않았고** 그것은 다른 판단이다 — v4 거절은 "우리 정책", 비UUID 거절은 "CloudEvents 상호운용 포기"다. 이 leaf의 존재 이유가 상호운용인데 명세 예시조차 받지 못한다. - -## 실패 어휘 — id 실패만 분류되지 않는다 - -같은 메서드의 다른 검증 실패 넷은 전부 `MessageValidationException`이고 안정 코드(`CLOUDEVENT_TIME_REQUIRED` 등)를 갖는다. id 실패만 raw `IllegalArgumentException`이라 `FailureDescriptor`가 없다 — 카테고리도, 코드도, retryable 판정도 없다. DLQ 라우팅과 대시보드가 이 실패를 분류하지 못한다. 같은 문제가 `causationid`·`type`·`correlationid`·`tenantcontext`·`producer`·`datacontenttype`·`schemaversion` 값 범위에도 있다(§6의 "코드 없음" 여덟 행). - -## 확인하지 못한 것 - -실제 외부 CloudEvents producer 의 id 형식 분포를 측정하지 않았다. 명세가 형식을 제약하지 않으므로 UUID 가 아닐 가능성이 높다는 것까지만 말할 수 있다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-inbox-jdbc-postgresql-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-inbox-jdbc-postgresql-f02.md deleted file mode 100644 index f6aeaed..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-inbox-jdbc-postgresql-f02.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -kind: CASE -slug: messaging-inbox-jdbc-postgresql-f02 -title: 속성을 이름으로 주장하는 테스트가 그 속성을 보일 수 없는 fake 위에서 통과한다 -topic: verification-path-coverage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-inbox-jdbc-postgresql-f02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-inbox-jdbc-postgresql-f02 - file: ../../../final/evidence/rendered/messaging-inbox-jdbc-postgresql-f02.svg - - key: messaging-inbox-jdbc-postgresql-f02-diagram - file: ../../../final/assets/diagrams/messaging-inbox-jdbc-postgresql-f02.svg -evidence: - - ../../../final/evidence/raw/messaging-inbox-jdbc-postgresql-f02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-inbox-jdbc-postgresql#L657 이다. -module: messaging-inbox-jdbc-postgresql -priority: P2 ---- - -# 속성을 이름으로 주장하는 테스트가 그 속성을 보일 수 없는 fake 위에서 통과한다 - -InboxOperationsTest.cleanupDeletesInBoundedBatches가 InMemoryInbox(List.of(1000, 500))에 대해 removed == 1500과 cutoffs.hasSize(3)을 단언한다. 그 fake의 무제한 메서드는 미리 준 목록을 순서대로 반환하는 대본이고 아무것도 삭제하거나 제한하지 않는다. - -## 관계 - -- **컬럼 폭은 애플리케이션 검증과 짝을 이룬다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 안전 규칙은 한 공식과 한 강제 시점을 갖는다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 문제 - -InboxOperationsTest.cleanupDeletesInBoundedBatches가 InMemoryInbox(List.of(1000, 500))에 대해 removed == 1500과 cutoffs.hasSize(3)을 단언한다. - -그 fake의 무제한 메서드는 미리 준 목록을 순서대로 반환하는 대본이고 아무것도 삭제하거나 제한하지 않는다. - -## 결론 - -bounded 오버로드는 fake에도 있지만 job이 부르지 않아 실행되지 않는다. - -이 테스트가 통과로 증명하는 것은 "0을 받을 때까지 루프를 돈다"이고 이름이 주장하는 "배치로 제한된다"가 아니다. - -1000·500은 배치처럼 보이는 숫자다. - -P1이 이 테스트를 통과한 채로 존재할 수 있었던 이유다. - -그리고 컨테이너 레인(InboxPostgresIT.retentionRemovesOldRows)도 무제한 오버로드를 한 행에 대해 부르므로 실 DB에서도 드러나지 않는다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : InboxOperationsTest 참조 검색과 테스트가 쓰는 fake 의 무제한 메서드 구현 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-inbox-jdbc-postgresql#L657 에 있다. - -## 본문 - - - -`InboxOperationsTest.cleanupDeletesInBoundedBatches`가 `InMemoryInbox(List.of(1000, 500))`에 대해 `removed == 1500`과 `cutoffs.hasSize(3)`을 단언한다. - -## 대역이 재현하지 못하는 것 - -:::evidence key="messaging-inbox-jdbc-postgresql-f02-diagram" alt="실제 저장소 쪽에 LIMIT 로 제한과 삭제가 일어남이 놓이고 테스트 대역 쪽에 대본 목록 반환과 삭제 없음이 빗금으로 놓인다" caption="대역이 재현하지 못하는 것" zoom="false" -::: - -그 fake의 무제한 메서드는 미리 준 목록을 순서대로 반환하는 **대본**이고 아무것도 삭제하거나 제한하지 않는다. bounded 오버로드는 fake에도 있지만 job이 부르지 않아 실행되지 않는다. - -## InboxOperationsTest 참조 위치 - -:::evidence key="messaging-inbox-jdbc-postgresql-f02" alt="코드베이스에서 InboxOperationsTest 를 검색한 출력 1줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="InboxOperationsTest 코드베이스 검색 — 1줄 · exit 0" zoom="true" -::: - -## 통과가 증명하는 것과 이름이 주장하는 것 - -증명하는 것은 "0을 받을 때까지 루프를 돈다"이고 이름이 주장하는 "배치로 제한된다"가 아니다. 1000·500은 배치처럼 보이는 숫자다. **P1이 이 테스트를 통과한 채로 존재할 수 있었던 이유**다. - -## 컨테이너 레인도 드러내지 못한다 - -`InboxPostgresIT.retentionRemovesOldRows`도 무제한 오버로드를 한 행에 대해 부른다. 후보는 fake의 무제한 메서드가 실제로 컬렉션에서 삭제하게 하고 bounded 메서드가 `limit`를 존중하게 하는 것 — 그러면 테스트가 P1을 잡는다. - -## 확인하지 못한 것 - -테스트를 고쳐 결함 있는 구현과 올바른 구현을 가르는지 확인하지 않았다. 대역이 삭제도 제한도 하지 않는다는 코드로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-nats-experimental-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-nats-experimental-f01.md deleted file mode 100644 index 24b8db7..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-nats-experimental-f01.md +++ /dev/null @@ -1,97 +0,0 @@ ---- -kind: CASE -slug: messaging-nats-experimental-f01 -title: deduplicatedPublish 를 무조건 참으로 선언하는데 실제 중복 제거는 프로파일에 창이 있을 때만 일어난다 -topic: verification-path-coverage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-nats-experimental-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-nats-experimental-f01 - file: ../../../final/evidence/rendered/messaging-nats-experimental-f01.svg - - key: messaging-nats-experimental-f01-diagram - file: ../../../final/assets/diagrams/messaging-nats-experimental-f01.svg -evidence: - - ../../../final/evidence/raw/messaging-nats-experimental-f01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-nats-experimental#L146 이다. -module: messaging-nats-experimental -priority: P2 ---- - -# deduplicatedPublish 를 무조건 참으로 선언하는데 실제 중복 제거는 프로파일에 창이 있을 때만 일어난다 - -검증기의 capabilities() 도 같은 값을 돌려준다. 그런데 중복 제거 식별자는 프로파일에 창이 있을 때만 만들어진다. - -## 문제 - -검증기의 capabilities() 도 같은 값을 돌려준다. - -그런데 중복 제거 식별자는 프로파일에 창이 있을 때만 만들어진다. - -## 결론 - -NatsJetStreamProfile.deduplicationWindow 는 Optional 이고, 비어 있는 것이 정상 상태다 — 프로파일 생성자도 검증기도 창을 요구하지 않는다. - -창이 없으면 Nats-Msg-Id 가 실리지 않고 서버는 중복을 제거하지 않는다. - -즉 능력 선언이 프로파일과 무관하게 참이다. - -이 저장소에서 능력 열두 개 중 부재가 예외를 만드는 유일한 것이 deduplicatedPublish 다(DefaultMessagePublisher:250). - -나머지는 읽히지 않거나 분기에 쓰인다. - -그러므로 이 플래그의 과대 선언은 다른 어느 플래그의 과대 선언보다 직접적이다 — 창 없는 목적지가 그 가드를 통과한다. - -클래스 javadoc: "when the profile enables one" 이 정확히 능력이 담지 않은 조건이다. - -창이 없는 목적지에서 모호를 재시도하면 스트림에 같은 메시지가 두 번 들어간다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : NatsJetStreamProfile 참조 17건 검색과 능력 상수 대비 중복 제거 식별자 생성 조건 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-nats-experimental#L146 에 있다. - -## 본문 - - - -능력은 상수이고 검증기의 `capabilities()` 도 같은 값을 돌려준다. 그런데 중복 제거 식별자는 프로파일에 창이 있을 때만 만들어진다. - -## 선언과 실제가 갈리는 조건 - -:::evidence key="messaging-nats-experimental-f01-diagram" alt="창 없는 프로파일이 능력은 참과 가드 통과를 지나 중복 저장으로 이어진다" caption="선언과 실제가 갈리는 조건" zoom="false" -::: - -`NatsJetStreamProfile.deduplicationWindow` 는 `Optional` 이고, 비어 있는 것이 정상 상태다 — 프로파일 생성자도 검증기도 창을 요구하지 않는다. 창이 없으면 `Nats-Msg-Id` 가 실리지 않고 서버는 중복을 제거하지 않는다. - -## NatsJetStreamProfile 참조 위치 - -:::evidence key="messaging-nats-experimental-f01" alt="코드베이스에서 NatsJetStreamProfile 를 검색한 출력 17줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="NatsJetStreamProfile 코드베이스 검색 — 17줄 · exit 0" zoom="true" -::: - -## 이 플래그의 과대 선언이 가장 직접적이다 - -능력 열두 개 중 부재가 예외를 만드는 유일한 것이 `deduplicatedPublish` 다(`DefaultMessagePublisher:250`). 나머지는 읽히지 않거나 분기에 쓰인다. 그러므로 창 없는 목적지가 그 가드를 통과한다. 클래스 javadoc 의 "when the profile enables one" 이 정확히 능력이 담지 않은 조건이고, 창이 없는 목적지에서 모호를 재시도하면 스트림에 같은 메시지가 두 번 들어간다. - -## 모순이 테스트로 고정되어 있다 - -`NatsAdapterContractTest` 안에서 같은 빈 창 프로파일(`confirming(Optional.empty())`)에 대해 둘 다 통과한다. 우연히 남은 것이 아니라는 뜻이고, 수정할 때 함께 고쳐야 할 지점이 어디인지도 이 두 개가 알려 준다. - -## 수정 - -능력을 프로파일에서 파생시킨다. `NatsJetStreamProfile.durable(...)` 는 창을 2분으로 채워 주지만 호출자가 없고, 정규 생성자는 빈 창을 정상값으로 받는다(§4). - -## 확인하지 못한 것 - -중복 제거 창이 없는 프로파일로 모호 재발행을 재현하지 않았다. 실제 JetStream 서버를 띄우지 않았다 — 클라이언트 브리지를 싣지 않는 리프다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-nats-experimental-f04.md b/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-nats-experimental-f04.md deleted file mode 100644 index f7cb84a..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-nats-experimental-f04.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -kind: CASE -slug: messaging-nats-experimental-f04 -title: 경과 시간 회귀를 막으려는 어셈블이 항상 참이다 -topic: verification-path-coverage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-nats-experimental-f04 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-nats-experimental-f04 - file: ../../../final/evidence/rendered/messaging-nats-experimental-f04.svg -evidence: - - ../../../final/evidence/raw/messaging-nats-experimental-f04.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-nats-experimental#L238 이다. -module: messaging-nats-experimental -priority: P3 ---- - -# 경과 시간 회귀를 막으려는 어셈블이 항상 참이다 - -as(...) 가 막으려는 회귀는 "모든 결과가 Duration.ZERO 를 보고하던 것"이다. 그런데 어셈블은 >= Duration.ZERO 다. - -## 문제 - -as(...) 가 막으려는 회귀는 "모든 결과가 Duration.ZERO 를 보고하던 것"이다. - -그런데 어셈블은 >= Duration.ZERO 다. - -## 결론 - -Duration.ZERO 는 이 조건을 통과한다. - -경과 시간은 시작 시점에서 잰 값이라 음수가 될 수 없으므로, 이 어셈블은 구현이 무엇을 하든 통과한다. - -이름과 as 메시지가 정확히 짚은 회귀를, 어셈블만 못 잡는다. - -그래서 이 테스트는 회귀 방지가 아니라 회귀 방지의 표시다. - -isGreaterThan(Duration.ZERO) 로 바꾼다. - -시간 분해능이 불안하면 전송 람다에 관측 가능한 지연을 넣고 그 하한과 비교한다 — 같은 클래스의 aPublishThatNeverCompletesIsBoundedByTheCallersTimeout 가 이미 50밀리초 마감으로 그 방식을 쓴다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : as 어셈블의 비교 연산자와 그것이 막겠다고 한 회귀 값의 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-nats-experimental#L238 에 있다. - -## 본문 - - - -`as(...)` 가 막으려는 회귀는 "모든 결과가 `Duration.ZERO` 를 보고하던 것"이다. 그런데 어셈블은 `>= Duration.ZERO` 다 — `Duration.ZERO` 는 이 조건을 통과한다. - -## as() 가 막으려던 회귀 - -:::evidence key="messaging-nats-experimental-f04" alt="분석 문서 final/document.md#a19-messaging-nats-experimental 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a19-messaging-nats-experimental 발췌 — 15줄" zoom="true" -::: - -## 구현이 무엇을 하든 통과한다 - -경과 시간은 시작 시점에서 잰 값이라 음수가 될 수 없다. 이름과 `as` 메시지가 정확히 짚은 회귀를 어셈블만 못 잡는다. 그래서 이 테스트는 회귀 방지가 아니라 회귀 방지의 표시다. - -## 수정 - -`isGreaterThan(Duration.ZERO)` 로 바꾼다. 시간 분해능이 불안하면 전송 람다에 관측 가능한 지연을 넣고 그 하한과 비교한다 — 같은 클래스의 `aPublishThatNeverCompletesIsBoundedByTheCallersTimeout` 가 이미 50밀리초 마감으로 그 방식을 쓴다. - -## 확인하지 못한 것 - -테스트를 실행하지 않았다. 항상 통과한다는 것은 어셈블 의미론으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-outbox-jdbc-postgresql-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-outbox-jdbc-postgresql-f02.md deleted file mode 100644 index f66d766..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-outbox-jdbc-postgresql-f02.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -kind: CASE -slug: messaging-outbox-jdbc-postgresql-f02 -title: 배포되는 Debezium 설정이 수정 이전 버전이다 -topic: verification-path-coverage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-outbox-jdbc-postgresql-f02 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-outbox-jdbc-postgresql-f02 - file: ../../../final/evidence/rendered/messaging-outbox-jdbc-postgresql-f02.svg - - key: messaging-outbox-jdbc-postgresql-f02-diagram - file: ../../../final/assets/diagrams/messaging-outbox-jdbc-postgresql-f02.svg -evidence: - - ../../../final/evidence/raw/messaging-outbox-jdbc-postgresql-f02.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-outbox-jdbc-postgresql#L888 이다. -module: messaging-outbox-jdbc-postgresql -priority: P2 ---- - -# 배포되는 Debezium 설정이 수정 이전 버전이다 - -src/main/resources/debezium/outbox-event-router.properties 가 event.key=destination 을 유지하고 있다. 같은 저장소의 Java(DebeziumOutboxEventRouter), V4 마이그레이션 주석, 그리고 전용 테스트(theRoutedKeyIsNotTheTopicName)가 모두 그것이 결함이라고 말한다 — "keying by destination puts every message on a topic onto one partition". - -## 문제 - -src/main/resources/debezium/outbox-event-router.properties 가 event.key=destination 을 유지하고 있다. - -같은 저장소의 Java(DebeziumOutboxEventRouter), V4 마이그레이션 주석, 그리고 전용 테스트(theRoutedKeyIsNotTheTopicName)가 모두 그것이 결함이라고 말한다 — "keying by destination puts every message on a topic onto one partition". - -## 결론 - -추가로 헤더 매핑이 15개 중 4개뿐이라, 이 파일로 배포한 CDC 는 tenant·correlation·causation·producer·trace·partition/ordering key 를 전부 잃는다. - -V4 가 존재하는 이유가 그 유실을 막는 것이다. - -1. - -properties 를 Java 설정에서 생성하거나, 최소한 둘을 대조하는 테스트를 둔다. - -DebeziumOutboxEventRouter.connectorConfiguration("") 의 항목이 파일에 모두 있는지 확인하는 테스트면 충분하다. - -지금은 두 표현을 잇는 코드가 한 줄도 없다. - -2. - -aggregateIdAsPartitionKey 를 connectorConfiguration 에 전달하거나, 전달할 수 없다면 DebeziumOutboxRecordMapper 에서 그 분기를 제거한다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : DebeziumOutboxEventRouter 참조 5건 검색과 배포 properties·Java 설정의 항목별 텍스트 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-outbox-jdbc-postgresql#L888 에 있다. - -## 본문 - - - -`src/main/resources/debezium/outbox-event-router.properties` 가 `event.key=destination` 을 유지하고 있다. - -## 두 판본의 차이 - -:::evidence key="messaging-outbox-jdbc-postgresql-f02-diagram" alt="Java 설정 쪽에 aggregate id 키와 헤더 매핑 열다섯이 놓이고 배포 properties 쪽에 destination 키와 헤더 매핑 넷이 빗금으로 놓인다" caption="두 판본의 차이" zoom="false" -::: - -같은 저장소의 Java(`DebeziumOutboxEventRouter`), V4 마이그레이션 주석, 그리고 전용 테스트(`theRoutedKeyIsNotTheTopicName`)가 모두 그것이 결함이라고 말한다 — "keying by destination puts every message on a topic onto one partition". - -## DebeziumOutboxEventRouter 참조 위치 - -:::evidence key="messaging-outbox-jdbc-postgresql-f02" alt="코드베이스에서 DebeziumOutboxEventRouter 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DebeziumOutboxEventRouter 코드베이스 검색 — 5줄 · exit 0" zoom="true" -::: - -## 헤더 매핑도 4개뿐이다 - -이 파일로 배포한 CDC 는 tenant·correlation·causation·producer·trace·partition/ordering key 를 **전부 잃는다**. V4 가 존재하는 이유가 그 유실을 막는 것이다. - -## 수정 둘 - -properties 를 Java 설정에서 생성하거나, 최소한 둘을 대조하는 테스트를 둔다 — `DebeziumOutboxEventRouter.connectorConfiguration("")` 의 항목이 파일에 모두 있는지 확인하는 테스트면 충분하다. 그리고 `aggregateIdAsPartitionKey` 를 `connectorConfiguration` 에 전달하거나, 전달할 수 없다면 `DebeziumOutboxRecordMapper` 에서 그 분기를 제거한다 — 지금은 모델이 커넥터가 하지 않을 일을 예측한다. - -## 확인하지 못한 것 - -실제 Debezium 커넥터를 띄워 properties 의 동작을 확인하지 않았다. 두 설정의 차이는 텍스트 대조로 확인했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-outbox-jdbc-postgresql-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-outbox-jdbc-postgresql-f03.md deleted file mode 100644 index 64601c6..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-outbox-jdbc-postgresql-f03.md +++ /dev/null @@ -1,91 +0,0 @@ ---- -kind: CASE -slug: messaging-outbox-jdbc-postgresql-f03 -title: 역슬래시로 끝나는 헤더 값이 헤더 맵을 깨뜨린다 -topic: verification-path-coverage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-outbox-jdbc-postgresql-f03 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-outbox-jdbc-postgresql-f03 - file: ../../../final/evidence/rendered/messaging-outbox-jdbc-postgresql-f03.svg - - key: messaging-outbox-jdbc-postgresql-f03-diagram - file: ../../../final/assets/diagrams/messaging-outbox-jdbc-postgresql-f03.svg -evidence: - - ../../../final/evidence/raw/messaging-outbox-jdbc-postgresql-f03.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-outbox-jdbc-postgresql#L899 이다. -module: messaging-outbox-jdbc-postgresql -priority: P2 ---- - -# 역슬래시로 끝나는 헤더 값이 헤더 맵을 깨뜨린다 - -findClosingQuote(:657-664)가 이스케이프된 역슬래시를 고려하지 않는다. 값이 역슬래시로 끝나면 파서가 종료 지점을 놓치고, 뒤에 헤더가 더 있으면 맵 전체가 붕괴한다(EVD-314, 런타임 재현). - -## 문제 - -findClosingQuote(:657-664)가 이스케이프된 역슬래시를 고려하지 않는다. - -값이 역슬래시로 끝나면 파서가 종료 지점을 놓치고, 뒤에 헤더가 더 있으면 맵 전체가 붕괴한다(EVD-314, 런타임 재현). - -## 결론 - -HeaderValue 는 제어문자만 금지하므로 이 입력은 플랫폼 검증을 통과한다. - -헤더 주입으로 이어지지는 않는다 — 예약 이름은 키가 아니라 값이 되고, 쓰기 경로가 예약 이름을 이미 거절한다. - -수정: 종료 판정을 "앞의 연속된 역슬래시 개수가 짝수" 로 바꾸거나, 인덱스를 앞에서부터 스캔하며 이스케이프 상태를 추적한다. - -후자가 unescape 와 대칭이라 낫다. - -테스트는 OutboxPostgresIT.aHeaderValueWithControlCharactersRoundTrips 옆에 역슬래시 종결 케이스를 추가하면 된다 — 실 DB 왕복까지 확인할 수 있다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : HeaderValue 참조 26건 검색과 파서의 종료 지점 탐색 코드 확인. jshell 리플렉션으로 왕복 손상을 런타임 재현 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-outbox-jdbc-postgresql#L899 에 있다. - -## 본문 - - - -`findClosingQuote`(`:657-664`)가 이스케이프된 역슬래시를 고려하지 않는다. - -## 구분자가 삼켜지는 지점 - -:::evidence key="messaging-outbox-jdbc-postgresql-f03-diagram" alt="역슬래시로 끝난 값이 종료 인용부 놓침과 뒤 헤더 흡수를 지나 헤더 맵 붕괴로 이어진다" caption="구분자가 삼켜지는 지점" zoom="false" -::: - -값이 역슬래시로 끝나면 파서가 종료 지점을 놓치고, 뒤에 헤더가 더 있으면 맵 전체가 붕괴한다(`EVD-314`, 런타임 재현). - -## HeaderValue 참조 위치 - -:::evidence key="messaging-outbox-jdbc-postgresql-f03" alt="코드베이스에서 HeaderValue 를 검색한 출력 26줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="HeaderValue 코드베이스 검색 — 26줄 · exit 0" zoom="true" -::: - -## 플랫폼 검증을 통과한다 - -`HeaderValue` 는 제어문자만 금지한다. - -## 헤더 주입으로 이어지지는 않는다 - -예약 이름은 키가 아니라 값이 되고, 쓰기 경로가 예약 이름을 이미 거절한다. - -## 수정 - -종료 판정을 "앞의 연속된 역슬래시 개수가 짝수" 로 바꾸거나, 인덱스를 앞에서부터 스캔하며 이스케이프 상태를 추적한다 — 후자가 `unescape` 와 대칭이라 낫다. 테스트는 `OutboxPostgresIT.aHeaderValueWithControlCharactersRoundTrips` 옆에 역슬래시 종결 케이스를 추가하면 실 DB 왕복까지 확인할 수 있다. - -## 확인하지 못한 것 - -없음 — 역슬래시 종결 값의 헤더 맵 붕괴를 런타임으로 재현했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-reliability-api-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-reliability-api-f01.md deleted file mode 100644 index be87cec..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-reliability-api-f01.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -kind: CASE -slug: messaging-reliability-api-f01 -title: 한 인터페이스가 같은 전이의 두 세대를 갖고, 안전하지 않은 쪽에 @Deprecated가 없다 -topic: verification-path-coverage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-reliability-api-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-reliability-api-f01 - file: ../../../final/evidence/rendered/messaging-reliability-api-f01.svg - - key: messaging-reliability-api-f01-diagram - file: ../../../final/assets/diagrams/messaging-reliability-api-f01.svg -evidence: - - ../../../final/evidence/raw/messaging-reliability-api-f01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-reliability-api#L689 이다. -module: messaging-reliability-api -priority: P2 ---- - -# 한 인터페이스가 같은 전이의 두 세대를 갖고, 안전하지 않은 쪽에 @Deprecated가 없다 - -OutboxRepository가 다섯 전이 각각에 대해 MessageId 기반(반환 void)과 OutboxLease 기반(반환 OutboxTransitionResult) 두 형태를 선언한다. javadoc이 전자를 "deprecated for the relay's use"라고 부르지만 @Deprecated 애노테이션이 이 leaf 전체에 0건이다. - -## 관계 - -- **계약만 담는 leaf도 계약의 거절 조건은 자기 레인에서 검증한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **record의 `equals`를 좁히면 이유를 적는다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **호출 컨텍스트가 계약이면 그 컨텍스트를 검증할 수단을 함께 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 문제 - -OutboxRepository가 다섯 전이 각각에 대해 MessageId 기반(반환 void)과 OutboxLease 기반(반환 OutboxTransitionResult) 두 형태를 선언한다. - -javadoc이 전자를 "deprecated for the relay's use"라고 부르지만 @Deprecated 애노테이션이 이 leaf 전체에 0건이다. - -## 결론 - -전자에는 fencing이 없다 — OutboxLease javadoc이 그 부재가 만든 이중 발행 사고를 기록한다. - -컴파일러가 경고하지 않으므로 새 호출자가 그것을 고를 수 있고, 실제로 PostgreSQL 컨테이너 테스트가 그렇게 했다(§12.1c). - -그리고 새 구현자는 17개 메서드를 전부 구현해야 하며 그중 다섯은 안전하지 않은 형태다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : OutboxRepository 참조 35건 검색과 다섯 전이의 두 세대 시그니처 및 @Deprecated 수 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-reliability-api#L689 에 있다. - -## 본문 - - - -`OutboxRepository`가 다섯 전이 각각에 대해 `MessageId` 기반(반환 `void`)과 `OutboxLease` 기반(반환 `OutboxTransitionResult`) 두 형태를 선언한다. - -## 표시가 없는 두 형태 - -:::evidence key="messaging-reliability-api-f01-diagram" alt="lease 기반 쪽에 결과 반환과 fencing 있음이 놓이고 MessageId 기반 쪽에 void 반환과 fencing 없음이 빗금으로 놓인다" caption="표시가 없는 두 형태" zoom="false" -::: - -javadoc이 전자를 "deprecated for the relay's use"라고 부르지만 `@Deprecated` 애노테이션이 이 leaf 전체에 **0건**이다. - -## OutboxRepository 참조 위치 - -:::evidence key="messaging-reliability-api-f01" alt="코드베이스에서 OutboxRepository 를 검색한 출력 35줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="OutboxRepository 코드베이스 검색 — 35줄 · exit 0" zoom="true" -::: - -## 컴파일러가 경고하지 않는다 - -전자에는 fencing이 없다 — `OutboxLease` javadoc이 그 부재가 만든 이중 발행 사고를 기록한다. 새 호출자가 그것을 고를 수 있고, **실제로 PostgreSQL 컨테이너 테스트가 그렇게 했다**(§12.1c). 그리고 새 구현자는 17개 메서드를 전부 구현해야 하며 그중 다섯은 안전하지 않은 형태다. - -## 확인하지 못한 것 - -구세대를 쓰는 배포를 만들어 fencing 부재의 결과를 관측하지 않았다. 이 저장소에는 그런 배포가 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-spring-boot-starter-f05.md b/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-spring-boot-starter-f05.md deleted file mode 100644 index 589ed19..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-spring-boot-starter-f05.md +++ /dev/null @@ -1,83 +0,0 @@ ---- -kind: CASE -slug: messaging-spring-boot-starter-f05 -title: 배치 발행자가 CompletionStage 를 돌려주면서 동기 예외를 던진다 -topic: verification-path-coverage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-spring-boot-starter-f05 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-spring-boot-starter-f05 - file: ../../../final/evidence/rendered/messaging-spring-boot-starter-f05.svg -evidence: - - ../../../final/evidence/raw/messaging-spring-boot-starter-f05.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-spring-boot-starter#L367 이다. -module: messaging-spring-boot-starter -priority: P3 ---- - -# 배치 발행자가 CompletionStage 를 돌려주면서 동기 예외를 던진다 - -같은 클래스가 자기 의존 대상에 대해서는 정확히 이 형태를 방어한다. 즉 "게으르게 검증하고 실패한 스테이지를 돌려준다" 가 이 클래스가 아는 계약인데, 자기 호출자에게는 그것을 지키지 않는다. - -## 관계 - -- **검증기는 발행이 아니라 주입이 강제다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 문제 - -같은 클래스가 자기 의존 대상에 대해서는 정확히 이 형태를 방어한다. - -즉 "게으르게 검증하고 실패한 스테이지를 돌려준다" 가 이 클래스가 아는 계약인데, 자기 호출자에게는 그것을 지키지 않는다. - -## 결론 - -비동기 파이프라인으로 배치를 부르는 코드는 .exceptionally(...) 로 잡히지 않는 예외를 만난다. - -등급이 P3 인 이유는 이것이 프로그래밍 오류(배치 크기 초과)이고 결과가 손실이 아니라 예외 형태의 불일치이기 때문이다. - -전용 테스트(aBatchLargerThanItsLimitIsRefusedBeforeAnythingIsPublished)가 assertThatThrownBy 로 현재 동작을 고정하고 있으므로, 고치려면 그 테스트도 함께 바꾼다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : 반환 타입과 예외 전달 경로 대조, 같은 클래스가 의존 대상에 적용한 방어와의 비교 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-spring-boot-starter#L367 에 있다. - -## 본문 - - - -배치 발행자가 `CompletionStage` 를 돌려주면서 동기 예외를 던진다. - -## 반환 타입과 예외 전달 방식 - -:::evidence key="messaging-spring-boot-starter-f05" alt="분석 문서 final/document.md#a19-messaging-spring-boot-starter 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a19-messaging-spring-boot-starter 발췌 — 15줄" zoom="true" -::: - -## 같은 클래스가 자기 의존 대상에는 이 형태를 방어한다 - -"게으르게 검증하고 실패한 스테이지를 돌려준다" 가 이 클래스가 아는 계약인데, 자기 호출자에게는 그것을 지키지 않는다. - -## 비동기 파이프라인이 잡지 못한다 - -`.exceptionally(...)` 로 잡히지 않는 예외를 만난다. 등급이 P3 인 이유는 이것이 프로그래밍 오류(배치 크기 초과)이고 결과가 손실이 아니라 예외 형태의 불일치이기 때문이다. - -## 테스트가 현재 동작을 고정하고 있다 - -`aBatchLargerThanItsLimitIsRefusedBeforeAnythingIsPublished` 가 `assertThatThrownBy` 를 쓰므로, 고치려면 그 테스트도 함께 바꾼다. - -## 확인하지 못한 것 - -@ConditionalOnBean 의 평가 순서를 실제 컨텍스트로 재현하지 않았다. 스프링의 문서화된 제약으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-testkit-f07.md b/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-testkit-f07.md deleted file mode 100644 index 166da25..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-testkit-f07.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -kind: CASE -slug: messaging-testkit-f07 -title: 항등식을 단언하는 테스트가 하나 있다 -topic: verification-path-coverage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-testkit-f07 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-testkit-f07 - file: ../../../final/evidence/rendered/messaging-testkit-f07.svg -evidence: - - ../../../final/evidence/raw/messaging-testkit-f07.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-testkit#L980 이다. -module: messaging-testkit -priority: P3 ---- - -# 항등식을 단언하는 테스트가 하나 있다 - -CompatibilityMatrixTest.aCertificationClaimCannotBeMadeWithoutEvidence(:48-60)의 좌변과 우변이 같은 식이다(§12.3(c)). 이름이 약속하는 것을 검사하지 않는다. - -## 문제 - -CompatibilityMatrixTest.aCertificationClaimCannotBeMadeWithoutEvidence(:48-60)의 좌변과 우변이 같은 식이다(§12.3(c)). - -이름이 약속하는 것을 검사하지 않는다. - -## 결론 - -실질 검사는 같은 파일의 다른 두 테스트가 하고 있으므로 커버리지 손실은 없다. - -이 테스트를 지우거나, "증거를 비우면 Stable 주장이 무너진다" 를 실제로 검사하도록 바꾼다 — 후자가 이름에 맞는다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : CompatibilityMatrixTest 참조 검색과 해당 단언의 좌변·우변 식 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-testkit#L980 에 있다. - -## 본문 - - - -`CompatibilityMatrixTest.aCertificationClaimCannotBeMadeWithoutEvidence`(`:48-60`)의 좌변과 우변이 같은 식이다(§12.3(c)). 이름이 약속하는 것을 검사하지 않는다. - -## CompatibilityMatrixTest 참조 위치 - -:::evidence key="messaging-testkit-f07" alt="코드베이스에서 CompatibilityMatrixTest 를 검색한 출력 1줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="CompatibilityMatrixTest 코드베이스 검색 — 1줄 · exit 0" zoom="true" -::: - -## 커버리지 손실은 없다 - -실질 검사는 같은 파일의 다른 두 테스트가 하고 있다. 이 테스트를 지우거나, "증거를 비우면 Stable 주장이 무너진다" 를 실제로 검사하도록 바꾼다 — 후자가 이름에 맞는다. - -## 확인하지 못한 것 - -테스트를 실행하지 않았다. 좌변과 우변이 같은 식이라는 것을 본문으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-transport-spi-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-transport-spi-f01.md deleted file mode 100644 index 43d284b..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/case/case-messaging-transport-spi-f01.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -kind: CASE -slug: messaging-transport-spi-f01 -title: 드레인 마감 30초가 세 곳에서 독립적으로 결정된다 -topic: verification-path-coverage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:messaging-transport-spi-f01 -evidenceCapturedOn: 2026-09-01 -assets: - - key: messaging-transport-spi-f01 - file: ../../../final/evidence/rendered/messaging-transport-spi-f01.svg -evidence: - - ../../../final/evidence/raw/messaging-transport-spi-f01.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-transport-spi#L659 이다. -module: messaging-transport-spi -priority: P3 ---- - -# 드레인 마감 30초가 세 곳에서 독립적으로 결정된다 - -MessagingLifecycle.DEFAULT_DRAIN_DEADLINE(public), DefaultMessagingRuntimeRegistry.DEFAULT_DRAIN_DEADLINE(private), MessagingShutdownLifecycle의 생성자 인자. public 상수가 같은 leaf 안에 있는데 다른 클래스가 자기 private 복사본을 쓴다. - -## 관계 - -- **멱등 종료를 보장하는 컴포넌트는 종료 이후의 등록도 정의한다** - 같은 분석 리프에서 끌어낸 규칙이다. -- **같은 개념의 sentinel은 계층을 넘어 하나로 정한다** - 같은 분석 리프에서 끌어낸 규칙이다. - -## 문제 - -MessagingLifecycle.DEFAULT_DRAIN_DEADLINE(public), DefaultMessagingRuntimeRegistry.DEFAULT_DRAIN_DEADLINE(private), MessagingShutdownLifecycle의 생성자 인자. - -public 상수가 같은 leaf 안에 있는데 다른 클래스가 자기 private 복사본을 쓴다. - -## 결론 - -MessagingLifecycleTest.theDefaultDrainDeadlineMatchesTheDesign이 public 쪽만 고정하므로 private 쪽이 바뀌어도 통과한다. - -그리고 §17 첫 항목대로 public 상수가 있는 인터페이스는 구현체가 없다 — 즉 살아 있는 값(private)이 죽은 인터페이스의 값(public)을 참조하지 않는다. - -## 검증 환경 - -OpenJDK : 21.0.12 java -version 으로 확인 -Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인 -확인 방식 : MessagingShutdownLifecycle 참조 11건 검색과 같은 값이 독립적으로 선언된 세 지점 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/document.md#a19-messaging-transport-spi#L659 에 있다. - -## 본문 - - - -`MessagingLifecycle.DEFAULT_DRAIN_DEADLINE`(public), `DefaultMessagingRuntimeRegistry.DEFAULT_DRAIN_DEADLINE`(private), `MessagingShutdownLifecycle`의 생성자 인자 — 30초가 세 곳에서 독립적으로 결정된다. - -## MessagingShutdownLifecycle 참조 위치 - -:::evidence key="messaging-transport-spi-f01" alt="코드베이스에서 MessagingShutdownLifecycle 를 검색한 출력 11줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingShutdownLifecycle 코드베이스 검색 — 11줄 · exit 0" zoom="true" -::: - -## 테스트가 한쪽만 고정한다 - -`MessagingLifecycleTest.theDefaultDrainDeadlineMatchesTheDesign`이 public 쪽만 고정하므로 private 쪽이 바뀌어도 통과한다. - -## 살아 있는 값이 죽은 인터페이스를 참조하지 않는다 - -§17 첫 항목대로 public 상수가 있는 인터페이스는 구현체가 없다. - -## 확인하지 못한 것 - -세 값이 실제 배포에서 어긋난 적이 있는지 확인하지 않았다. 오늘은 같은 값이고, 잇는 코드가 없다는 것으로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/reference/reference-messaging-cloudevents-f03.md b/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/reference/reference-messaging-cloudevents-f03.md deleted file mode 100644 index c539fa9..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/reference/reference-messaging-cloudevents-f03.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-cloudevents-f03 -title: 왕복이라 부르는 테스트는 무엇을 보존하지 않는지도 적는다 -topic: verification-path-coverage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-cloudevents-f03 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-cloudevents#L538 ---- - -# 왕복이라 부르는 테스트는 무엇을 보존하지 않는지도 적는다 - -## 관계 - -- **배포 아티팩트가 싣지만 아무도 부르지 않는다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **상호운용을 위한 매퍼가 명세 준수 이벤트를 분류되지 않은 예외로 거절한다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. - -## 목적 - -이름이 왕복이라고 말하는 테스트가 실제로는 부분 보존만 확인할 때, 읽는 사람은 확인되지 않은 필드를 확인된 것으로 읽는다. 그 착각이 가장 비싸게 끝나는 필드가 trace 다. - -## 규칙 - -1. 비교하는 필드와 왕복하는 필드를 각각 센다 - fromCloudEvent 가 partitionKey 와 orderingKey 를 empty 로, traceContext 를 none() 으로, headers 를 empty() 로 두고 producedAt 을 occurredAt 값으로 덮는다. 테스트는 여섯 필드만 비교하고 이 다섯을 비교하지 않는다. - -2. 비교하지 않는 필드가 실제로 어긋나는지 확인한다 - fixture 의 producedAt 은 09:15:01Z, occurredAt 은 09:15:00Z 다. 비교했다면 실패했을 값이다. - -3. 소실을 계약에 적거나 테스트 이름을 실제 보장에 맞춘다 - messaging-core-api 의 TraceContext javadoc 이 그 필드를 봉투에 둔 이유를 "a trace survives an Outbox round trip through the database, where broker headers do not exist yet" 라고 적는다. CloudEvents 왕복이 그 보존을 깨뜨리면서, CloudEvents 자신이 정의하는 distributed-tracing extension 도 쓰지 않는다. - -## 적용 조건 - -매핑이 양방향이고 한쪽 방향에서 필드가 줄어드는 모든 코덱·어댑터 경계. - -## 예외 - -SSOT 가 이 규칙의 반례를 적지 않았다. 의도적으로 버리는 필드가 있다면 그 목록이 javadoc 에 있어야 한다는 것이 여기서 제시된 후보 중 하나다. - -## 예시 - -DefaultCloudEventMapper.java:115-131 의 매핑과 CloudEventMappingTest.java:72-84, 143-161 의 비교 대상. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/reference/reference-messaging-kafka-share-experimental-f05.md b/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/reference/reference-messaging-kafka-share-experimental-f05.md deleted file mode 100644 index cb96707..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/reference/reference-messaging-kafka-share-experimental-f05.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-kafka-share-experimental-f05 -title: leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다 -topic: verification-path-coverage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-kafka-share-experimental-f05 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-kafka-share-experimental#L503 ---- - -# leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다 - -## 관계 - -- **"등록"이 아무것도 등록하지 않고 성공을 반환한다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. - -## 목적 - -네 타입 중 KafkaShareProfileValidator 만 테스트를 갖는다. registrar 에 테스트가 있었다면 register 가 spec 을 버리는 것이 sink 호출 확인에서 바로 드러났을 것이고, capability 의 boolean 12개는 지금 순서 값을 true 로 바꿔도 아무것도 깨지지 않는다. - -## 규칙 - -1. 테스트 클래스 이름 목록과 public 타입 목록을 맞춘다 - find src/test -name '*Test.java' 가 하나를 돌려준다. - -2. 짝이 없는 타입이 무엇을 혼자 결정하는지 센다 - registrar 는 register 의 spec 처리를, capability 는 12개 boolean 선언을 혼자 갖는다. - -3. 선언만 있는 상수도 테스트 대상이다 - 읽는 코드가 없더라도 값이 바뀌면 안 되는 것이면 그 사실을 테스트가 붙든다. - -## 적용 조건 - -타입 수가 적어 테스트 하나로 충분해 보이는 leaf. 특히 미배선 상태라 실행 경로가 없는 leaf. - -## 예외 - -테스트가 소비자 leaf 에 있는 경우는 예외로 볼 수 있다. 이 leaf 는 소비자가 0 이라 그 예외에 해당하지 않는다. - -## 예시 - -테스트 클래스 목록이 하나라는 것과, 그 하나가 validator 를 겨냥한다는 것. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/reference/reference-messaging-reliability-api-f04.md b/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/reference/reference-messaging-reliability-api-f04.md deleted file mode 100644 index ad8aa6c..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/reference/reference-messaging-reliability-api-f04.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-reliability-api-f04 -title: 계약만 담는 leaf도 계약의 거절 조건은 자기 레인에서 검증한다 -topic: verification-path-coverage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-reliability-api-f04 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-reliability-api#L716 ---- - -# 계약만 담는 leaf도 계약의 거절 조건은 자기 레인에서 검증한다 - -## 관계 - -- **inbox 보존 규칙이 문서로만 있다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **포트가 bounded/unbounded purge 두 오버로드를 나란히 노출하고, 호출자가 무제한 쪽을 고른다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **fencing token 경로가 실제 데이터베이스에 대해 실행되지 않는다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **한 인터페이스가 같은 전이의 두 세대를 갖고, 안전하지 않은 쪽에 `@Deprecated`가 없다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. - -## 목적 - -계약 불변식 중 일부는 구현이 우연히 그 조합을 만들지 않으면 한 번도 실행되지 않는다. OutboxCanonicalMetadata 가 schemaUri 있고 schemaSubject 없는 조합을 거절하는 것, OutboxLease 가 token < 1 을 거절하는 것, InboxResult.isSafeToSettle 의 세 값이 그런 것들이다. - -## 규칙 - -1. leaf 에 src/test 가 있는지부터 본다 - messaging-reliability-api 에는 그 디렉터리가 없다. 13개 타입의 record 생성자 검증 여섯과 술어 셋이 이 leaf 의 레인에서 실행되지 않는다. - -2. 형제 leaf 의 기준선을 확인한다 - messaging-core-api 79개, messaging-policy 42개다. 계약 leaf 라서 테스트가 없는 것이 아니다. - -3. 거절 조건과 술어를 겨냥한 단위 테스트를 둔다 - 구현이 지나가는 경로가 아니라 계약이 금지하는 조합을 겨냥한다. - -## 적용 조건 - -타입 선언과 javadoc 만 담는 계약 leaf. 특히 record 생성자가 검증을 갖는 leaf. - -## 예외 - -SSOT 가 이 규칙의 반례를 적지 않았다. 검증이 전혀 없는 순수 인터페이스 leaf 라면 대상이 아니지만, 이 leaf 는 생성자 검증 여섯을 갖는다. - -## 예시 - -ls src/messaging/messaging-reliability-api/src 가 main 만 돌려준다는 것. 원문 근거는 evidence/raw/289 §A 이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/reference/reference-messaging-reliability-api-f07.md b/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/reference/reference-messaging-reliability-api-f07.md deleted file mode 100644 index c51e5df..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/reference/reference-messaging-reliability-api-f07.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-reliability-api-f07 -title: record의 equals를 좁히면 이유를 적는다 -topic: verification-path-coverage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-reliability-api-f07 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-reliability-api#L743 ---- - -# record의 equals를 좁히면 이유를 적는다 - -## 관계 - -- **inbox 보존 규칙이 문서로만 있다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **포트가 bounded/unbounded purge 두 오버로드를 나란히 노출하고, 호출자가 무제한 쪽을 고른다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **fencing token 경로가 실제 데이터베이스에 대해 실행되지 않는다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **한 인터페이스가 같은 전이의 두 세대를 갖고, 안전하지 않은 쪽에 `@Deprecated`가 없다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. - -## 목적 - -같은 messageId · status · attempts · payload 를 가진 두 행이 다른 목적지, 다른 provenance 를 가져도 같다고 판정된다. 컬렉션 연산과 테스트 단언에서 의미가 달라지는데, 그 좁힘의 이유가 어디에도 없다. - -## 규칙 - -1. 어떤 필드를 비교에서 뺐는지 센다 - OutboxRecord.equals 와 hashCode 가 넷만 본다. destination · metadata · createdAt · leaseExpiresAt · lastFailureCode 는 무시한다. - -2. 재정의가 필요했던 이유와 좁힌 이유를 구분한다 - 배열 필드 때문에 재정의가 필요한 것까지는 명확하다. messaging-schema-api 의 EncodedMessage 도 같은 이유로 재정의한다. - -3. 같은 이유에서 출발한 형제와 대조한다 - EncodedMessage 는 모든 필드를 비교한다. 여기서 갈라진 지점이 기록돼야 할 자리다. - -## 적용 조건 - -배열 필드나 파생 필드 때문에 record 의 기본 equals 를 재정의하는 모든 자리. - -## 예외 - -SSOT 가 이 규칙의 반례를 적지 않았다. 신원 필드만으로 동등성을 정의하는 설계가 의도라면 그 문장이 javadoc 에 있어야 한다. - -## 예시 - -OutboxRecord.java:114-126 의 비교 대상. 확인 방법은 그것과 EncodedMessage 의 equals 를 대조하는 것이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/reference/reference-messaging-runtime-core-f06.md b/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/reference/reference-messaging-runtime-core-f06.md deleted file mode 100644 index 2299fc5..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/reference/reference-messaging-runtime-core-f06.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-runtime-core-f06 -title: 증가한다고 문서화한 값이 리터럴이면 그 사실을 적는다 -topic: verification-path-coverage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-runtime-core-f06 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-runtime-core#L754 ---- - -# 증가한다고 문서화한 값이 리터럴이면 그 사실을 적는다 - -## 관계 - -- **관측이 구현·호출부·주입 자리를 모두 갖추고도 출하에서 no-op이다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **소비 오케스트레이터가 조립되지 않는다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **선언된 content type과 실제 인코딩이 조용히 갈라질 수 있다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. - -## 목적 - -진단에서 generation 을 읽는 사람은 언제나 1 을 본다. 회전 코드가 없는 지금은 무해하지만, DefaultMessagingRuntimeRegistry 의 세대 드레인 로직은 세대 구분을 전제한다. 회전을 붙일 때 이 리터럴이 잊히면 두 세대가 같은 번호를 갖는다. - -## 규칙 - -1. 증가한다고 적힌 값의 설치 지점을 찾는다 - 유일한 지점인 MessagingCoreAutoConfiguration:476 이 리터럴 1L 을 넘긴다. - -2. 문서가 무엇을 약속하는지 확인한다 - MessagingRuntime.generation() javadoc 은 "increasing with each replacement", TransportMessagingRuntime javadoc 은 "the credential generation a rotation increments" 라고 적는다. - -3. 값을 실제 카운터에 연결하거나 리터럴임을 주석으로 남긴다 - 둘 중 하나가 없으면 문서와 값이 조용히 갈라진 상태로 남는다. - -## 적용 조건 - -javadoc 이 값의 변화를 약속하고 그 값의 생성 지점이 조립 코드에 하나뿐인 자리. - -## 예외 - -SSOT 가 이 규칙의 반례를 적지 않았다. 값이 영원히 고정이라면 그것이 문서의 표현이어야 하고, 지금은 문서 쪽이 증가를 말한다. - -## 예시 - -MessagingCoreAutoConfiguration:476 의 리터럴과 두 javadoc. 확인 방법은 git grep -n 'TransportMessagingRuntime(' -- src/main 이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/reference/reference-messaging-schema-avro-f04.md b/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/reference/reference-messaging-schema-avro-f04.md deleted file mode 100644 index a7125c6..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/reference/reference-messaging-schema-avro-f04.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-schema-avro-f04 -title: 모드 enum을 분기 조건으로 쓰면 각 분기에 테스트를 둔다 -topic: verification-path-coverage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-schema-avro-f04 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-schema-avro#L551 ---- - -# 모드 enum을 분기 조건으로 쓰면 각 분기에 테스트를 둔다 - -## 관계 - -- **진화 판단이 두 곳에 있고 형태가 반대다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **CI에서 돈다고 선언한 게이트를 부르는 CI가 없다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. - -## 목적 - -transitive 모드는 "여러 릴리스 뒤처진 consumer" 를 위한 것이고 그것이 이 게이트가 존재하는 이유의 절반이다. 그 절반이 한 번도 실행되지 않는다. 그리고 그 분기가 §12.3 의 중복 구현이 갈라질 지점이기도 하다. - -## 규칙 - -1. 분기 조건이 되는 enum 값과 테스트가 넘긴 값을 맞춰 본다 - AvroCompatibilityTest 의 게이트 호출 2건이 모두 SchemaCompatibility.BACKWARD 다. isTransitive 가 true 인 경로가 실행되지 않는다. - -2. 실행되지 않는 분기가 무엇을 결정하는지 센다 - 여기서는 비교 대상 스키마의 개수와 범위가 통째로 달라진다. - -3. 분기마다 입력을 만든다 - v1 · v2 · v3 세 스키마로 BACKWARD_TRANSITIVE 케이스를 추가한다. - -## 적용 조건 - -enum 이 알고리즘을 가르는 모든 게이트·검증기·전략 선택 지점. - -## 예외 - -SSOT 가 이 규칙의 반례를 적지 않았다. 분기가 같은 코드로 수렴하는 것이 증명돼 있다면 대상이 아니지만, 여기서는 두 분기가 다른 비교를 수행한다. - -## 예시 - -AvroCompatibilityTest.java:134-150 의 모드 인자 두 개. 확인 방법은 두 테스트의 인자를 보는 것이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/reference/reference-messaging-schema-protobuf-f01.md b/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/reference/reference-messaging-schema-protobuf-f01.md deleted file mode 100644 index 399a181..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/reference/reference-messaging-schema-protobuf-f01.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-schema-protobuf-f01 -title: 검증되지 않는 스키마 파일은 문서임을 파일 안에 적는다 -topic: verification-path-coverage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-schema-protobuf-f01 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-schema-protobuf#L527 ---- - -# 검증되지 않는 스키마 파일은 문서임을 파일 안에 적는다 - -## 관계 - -- **registry 조회 로직이 세 codec에 복제돼 있다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. - -## 목적 - -테스트 javadoc 이 .proto 를 "the fixture documents" 라고 부르므로 읽는 사람은 그 파일이 테스트의 근거라고 믿는다. 실제로는 테스트가 그 파일을 읽지 않고 DescriptorProto 로 같은 스키마를 손수 만든다. 한쪽만 수정되면 조용히 갈라진다. - -## 규칙 - -1. 스키마 파일이 빌드나 테스트에 입력으로 들어가는지 확인한다 - src/test/proto/order_created_v1.proto 는 어디에도 입력되지 않는다. 오늘 두 정의는 일치한다 — 필드 4개, 태그 1–4, 타입까지 전수 대조했다. - -2. 검증되지 않는다면 그 사실을 파일 안에 적는다 - "이 파일은 문서이며 테스트는 descriptor 를 손수 만든다" 가 그 문장이다. - -3. 자동 대조가 가능한지 먼저 따져 본다 - protoc 없이 protobuf-java 파서만으로는 .proto 를 읽어 descriptor 를 만들 수 없다. protoc 를 뺀 것은 이유가 적힌 설계 결정이다. - -## 적용 조건 - -빌드 입력이 아닌 스키마·설정 예제 파일이 저장소에 남아 있는 자리. - -## 예외 - -빌드가 그 파일을 실제로 소비하면 이 규칙의 대상이 아니다. 여기서는 소비 지점이 없다. - -## 예시 - -.proto 전문과 ProtobufCompatibilityTest.java:41-66 의 손수 만든 descriptor. 확인 방법은 두 정의의 필드·태그·타입 대조와 find src/messaging/messaging-schema-protobuf -name '*.proto' 다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/reference/reference-messaging-schema-protobuf-f02.md b/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/reference/reference-messaging-schema-protobuf-f02.md deleted file mode 100644 index 90766fe..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/reference/reference-messaging-schema-protobuf-f02.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-schema-protobuf-f02 -title: 신뢰할 수 없는 입력 쪽 경계를 먼저 테스트한다 -topic: verification-path-coverage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-schema-protobuf-f02 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-schema-protobuf#L536 ---- - -# 신뢰할 수 없는 입력 쪽 경계를 먼저 테스트한다 - -## 관계 - -- **registry 조회 로직이 세 codec에 복제돼 있다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. - -## 목적 - -디코딩은 브로커에서 온 바이트를 받는 쪽이다. 지금은 자기 코드가 만든 바이트에 대한 방어가 남이 만든 바이트에 대한 방어보다 잘 검증돼 있다. 형제 leaf 는 반대로 되어 있다 — AvroRegistryBoundsTest.theEvolutionDecodeAppliesTheSameBound 가 정확히 이 각도를 덮는다. - -## 규칙 - -1. 상한 검사가 인코딩 쪽과 디코딩 쪽에 각각 있는지 본다 - ProtobufMessageCodec.java:104-108 의 if (encoded.length > maxBytes) 가 디코딩 쪽 검사다. - -2. 각 검사에 테스트가 붙었는지 센다 - 인코딩 상한은 두 테스트가 덮고, 디코딩 분기를 겨냥한 테스트는 ProtobufCompatibilityTest 12개 중 없다. - -3. 입력의 출처로 우선순위를 정한다 - maxBytes 보다 큰 byte[] 로 decode 를 부르는 테스트를 먼저 추가한다. - -## 적용 조건 - -같은 상한이 인코딩과 디코딩 양쪽에 걸려 있고 한쪽 입력만 외부에서 오는 모든 codec. - -## 예외 - -디코딩 입력이 같은 프로세스에서 만들어진다고 타입이 보장하면 대상이 아니다. 이 codec 의 디코딩 입력은 브로커에서 온다. - -## 예시 - -ProtobufMessageCodec.java:104-108 의 분기와 ProtobufCompatibilityTest 12개 전수. 확인 방법은 그 12개 중 decode 에 큰 입력을 주는 것이 없음을 보는 것이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/reference/reference-messaging-security-f05.md b/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/reference/reference-messaging-security-f05.md deleted file mode 100644 index ceb2b03..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/verification-path-coverage/reference/reference-messaging-security-f05.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -kind: REFERENCE -slug: messaging-security-f05 -title: 같은 술어가 두 타입에 있으면 하나가 다른 하나를 부른다 -topic: verification-path-coverage -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: reference:messaging-security-f05 -verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다 -source: - - final/document.md#a19-messaging-security#L670 ---- - -# 같은 술어가 두 타입에 있으면 하나가 다른 하나를 부른다 - -## 관계 - -- **종료 시 자격증명 소거가 호출되지 않는다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **같은 TLS posture를 두 클래스가 다른 엄격도로 검사한다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. -- **권한 거부가 `AUTHORIZATION`이 아니라 `CONFIGURATION`으로 기록된다** - 이 규칙의 근거는 같은 분석 리프의 판정이 소유한다. - -## 목적 - -테스트가 고정하는 것과 실행되는 것이 다른 객체다. 한쪽만 고치면 다른 쪽은 조용히 다른 시점에 회전한다. - -## 규칙 - -1. 같은 판단을 하는 메서드가 둘인지 센다 - CredentialRotationPlan.isDue 와 isExpired, CredentialRuntime.isDueForRotation 과 isExpired 가 글자까지 같다. - -2. 실행되는 쪽과 테스트되는 쪽이 같은지 본다 - 전자는 소비자가 0 이고 전용 테스트 CredentialRotationContractTest 4개를 갖는다. 후자가 실행되는 쪽이다. - -3. 위임으로 하나를 만든다 - CredentialRuntime 이 CredentialRotationPlan 을 필드로 갖고 위임하거나, 계획 record 를 제거한다. - -## 적용 조건 - -같은 시간·상태 판단이 값 객체와 런타임 객체에 각각 구현되는 자리. - -## 예외 - -SSOT 가 이 규칙의 반례를 적지 않았다. 두 술어가 의도적으로 다른 기준을 갖는다면 그 차이가 이름이나 javadoc 에 있어야 하는데, 지금은 본문이 같다. - -## 예시 - -두 메서드의 본문. 원문 근거는 evidence/raw/287 §E 이고, 확인 방법은 두 본문을 대조하는 것이다. - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-a14-f002-webproblemsanitizer-alreadysafe.md b/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-a14-f002-webproblemsanitizer-alreadysafe.md deleted file mode 100644 index 305d00e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-a14-f002-webproblemsanitizer-alreadysafe.md +++ /dev/null @@ -1,136 +0,0 @@ ---- -kind: CASE -slug: a14-f002-webproblemsanitizer-alreadysafe -title: 호출자 없이 들어온 메서드이고, 도달 불가라던 가지는 켜지면 반대로 답한다 -topic: web-inbound-and-http-surface -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a14-f002-webproblemsanitizer-alreadysafe -evidenceCapturedOn: 2026-09-02 -body: case-a14-f002-webproblemsanitizer-alreadysafe.body.md -assets: - - key: a14-f002-webproblemsanitizer-alreadysafe - file: ../../../final/evidence/rendered/a14-f002-webproblemsanitizer-alreadysafe.svg - - key: a14-f002-webproblemsanitizer-alreadysafe-branch - file: ../../../final/evidence/rendered/a14-f002-webproblemsanitizer-alreadysafe-branch.svg -evidence: - - ../../../final/evidence/raw/a14-f002-webproblemsanitizer-alreadysafe.txt - - ../../../final/evidence/raw/a14-f002-webproblemsanitizer-alreadysafe-branch.txt -source: - - 원본 분석 절은 final/document.md#a14#L380 이다. ---- - -# 호출자 없이 들어온 메서드이고, 도달 불가라던 가지는 켜지면 반대로 답한다 - -메시지가 이미 안전한지 단언하는 메서드다. 이름이 저장소에 딱 한 번, 자기 선언에만 나온다. 원문은 그 안의 삼항 한 가지도 도달 불가라고 적었는데, 실제로는 코드 점 스물셋이 그 가지를 타고 스물셋 다 안전하다는 답을 받는다. - -## 관계 - -- **adapter까지 있고 호출자가 없는 교정 경로** - 만들어 두고 아무도 부르지 않는다는 점이 같다. -- **소비자가 없는 fixture 셋** - 이쪽도 호출자 수를 세어 프로덕션에서 쓰이지 않는다는 것을 확인했다. -- **runtime_memberships를 먼저 읽고 심각도를 정한다** - 이 사례의 완화가 조립 부재가 아니라는 것을 그 규칙으로 판정했다. - -## 문제 - -문제 응답 정제기에 안전 확인 메서드가 있고 자바독이 용도를 적는다. 그 용도가 수행되는지, 그리고 안쪽 조건이 정말 죽었는지 확인했다. - -## 결론 - -호출자가 없다. 저장소가 추적하는 모든 파일에서 이 이름이 잡히는 자리가 선언 하나다. - -쓰이다 끊긴 것도 아니다. 모든 참조를 뒤져도 이 이름을 건드린 커밋이 하나이고, 그 커밋이 이 파일을 새로 더한다. - -안쪽 가지는 다르다. 원문은 앞선 검사가 공백만인 입력을 걸렀으니 다듬은 문자열이 빌 수 없고 따라서 삼항의 편집됨 가지는 도달하지 않는다고 적었다. - -그 논리가 두 판정을 하나로 본다. 공백이냐를 묻는 쪽과 앞뒤를 떼어 내는 쪽이 서로 다른 집합을 쓰고, 그 차집합이 비어 있지 않다. - -그 조각을 세어 봤다. 기본 다국어 평면 전체에서 스물셋이고, U+0000 부터 U+0008 까지와 U+000E 부터 U+001B 까지 두 구간이다. 널 문자와 백스페이스와 탈출 문자는 그중 셋일 뿐이다. - -스물셋 모두 참을 받는다. 정제기와 삼항이 같은 편집 문구로 수렴하기 때문이다. 제어 문자 하나짜리 텍스트를 이미 안전하다고 단언하는 셈이다. - -반대 방향도 나왔다. 삭제 문자는 다듬기가 떼지 않으므로 거짓이 되고, 붙임 공백 한 글자는 어느 규칙에도 걸리지 않아 참이 된다. 다만 그 글자를 상한보다 길게 늘이면 거짓으로 뒤집힌다. 정제기가 상한에서 잘라 낸 앞부분과 삼항이 고른 원문이 달라지기 때문이다. - -그래서 원문이 적은 것과 다른 결함이 된다. 이 분기는 도달할 수 없는 코드가 아니라 실제로 실행되는 코드이고, 실행되면 원문이 예상한 것과 반대 값을 돌려준다. - -판정은 P3 다. 호출자가 없으므로 오늘 그 답을 소비하는 코드가 없다. - -다만 완화의 성격이 배선 부재는 아니다. 모듈 레지스트리에서 이 리프의 런타임 소속을 읽으면 둘이다. 남는 완화는 호출자가 없다는 것 하나뿐이고, 그것은 한 줄이면 사라진다. 그 상태를 붙들어 두는 장치도 없다. 이 리프가 선언한 구조 규칙은 여덟이고 두 목록에 담겨 실제로 도는 것은 일곱이다. 그것들이 보는 것은 컨트롤러와 트랜잭션과 와이어 모델과 엔벌로프와 의존 방향이고, 미호출 메서드를 보는 것은 없다. 목록에 담기지 않은 여덟 번째 자신도 호출자가 없다. 다만 그 여덟 번째 규칙의 자바독은 임포트가 놀지 않게 두는 표식이라고 스스로 밝힌다. 이 사례의 메서드는 하지 않는 일을 하겠다고 적어 두었다는 점이 다르다. 공개 표면 기준선도 이 리프에는 적용되지 않는다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 추적 파일 전수 검색과 모든 참조의 이력 확인, 기본 다국어 평면 전체 훑기, 구조 규칙 목록 확인 -소스 수정 : x - -## 재현 조건 - -이 사례의 프로브는 새로 만든 것이고 원문에는 없다. 원문 근거는 분석 문서의 #L380 절이다. - -1. 메서드와 그 자바독을 읽는다. -2. 그 이름을 추적 파일 전부에서 대소문자 없이 찾는다. -3. 모든 참조를 대상으로 그 이름을 건드린 커밋을 찾고, 그 커밋에서 파일 상태를 본다. -4. 앞선 검사와 다듬기가 각각 어떤 판정을 쓰는지 읽는다. -5. 두 판정이 갈리는 코드 점을 기본 다국어 평면 전체에서 센다. -6. 그중 메서드가 참을 돌려주는 것이 몇 개인지 함께 센다. -7. 모듈 레지스트리에서 이 리프의 런타임 소속을 읽고, 이 리프의 구조 규칙이 무엇을 보는지 나열한다. - -## 본문 - - - -자바독이 용도를 한 줄로 적는다. - -```java -// WebProblemSanitizer.java:78-84 - /** Whether text would survive sanitising unchanged, for asserting a message is already safe. */ - public boolean alreadySafe(String input, int maxLength) { - return input != null - && !input.isBlank() - && sanitize(input, maxLength) - .equals(input.trim().toLowerCase(Locale.ROOT).isEmpty() ? REDACTED : input.trim()); - } -``` - -## 부르는 곳이 없고, 있었던 적도 없다 - -:::evidence key="a14-f002-webproblemsanitizer-alreadysafe" alt="코드베이스 정적 검색 출력 51줄. 메서드와 자바독, 추적 파일 전부에서 그 이름을 찾은 결과 한 줄, 모든 참조에서 그 이름을 건드린 커밋과 그 커밋에서의 파일 상태, 앞선 검사와 다듬기가 쓰는 판정 줄, 이 리프의 런타임 소속, 선언된 구조 규칙 여덟과 그중 두 목록에 담겨 실제로 도는 일곱, 공개 표면 기준선 적용 여부가 차례로 보인다." caption="이름이 나오는 자리와 그 이력, 리프의 소속과 구조 규칙 — 51줄 · exit 0" zoom="true" -::: - -`git grep -i` 는 확장자를 가리지 않는다. 그런데도 잡히는 줄이 `WebProblemSanitizer:79` 하나다. `git log --all -S` 도 커밋을 하나만 내고, 그 커밋에서 이 파일의 상태가 `A` 다. 호출자를 잃은 것이 아니라 없이 들어왔다. - -## 도달 불가라는 근거가 성립하지 않는다 - -원문은 `!input.isBlank()` 가 통과했으니 `input.trim()` 이 빌 수 없고, 따라서 삼항의 `REDACTED` 가지는 도달 불가라고 적는다. 그 추론에는 두 판정이 같은 것으로 놓여 있다. - -`String.isBlank()` 는 `Character.isWhitespace` 를 쓴다. `String.trim()` 이 떼어 내는 것은 문자열 양끝의 U+0020 이하다. 한쪽이 공백으로 세지 않는 문자를 다른 쪽이 지우면 앞선 검사를 통과한 입력의 다듬은 값이 빌 수 있다. - -## 갈리는 코드 점을 전부 셌다 - -:::evidence key="a14-f002-webproblemsanitizer-alreadysafe-branch" alt="JVM 프로브 출력 18줄. 열 가지 입력마다 isBlank 결과와 다듬은 문자열이 비는지와 삼항이 타는 가지와 alreadySafe 의 반환을 한 줄에 적고, 이어서 기본 다국어 평면 전체에서 두 판정이 갈리는 코드 점의 수와 구간과 그중 참을 받는 수를 적었다." caption="열 입력의 가지와 갈리는 코드 점 전수 — 18줄 · exit 0" zoom="true" -::: - -앞 표가 예시 열 개다. 널 문자 하나, 널 문자 셋, 백스페이스, 탈출 문자가 그 가지를 타고 넷 다 참을 받는다. `sanitize` 도 그 입력에서는 `REDACTED` 를 돌려주므로 등호가 성립한다. - -예시로는 범위를 말할 수 없어 뒤에서 기본 다국어 평면 전체를 훑었다. 갈리는 코드 점이 스물셋이고 `U+0000..U+0008` 과 `U+000E..U+001B` 두 구간이다. `U+0020` 이하는 서른셋인데 그중 열이 빠진다. `U+0009..U+000D` 와 `U+001C..U+0020` 이 자바에서는 공백이라 앞선 검사에 걸리기 때문이다. 가운데가 끊기는 이유도, 두 번째 구간이 `U+001F` 가 아니라 `U+001B` 에서 멈추는 이유도 그것이다. 정보 구분자 넷이 공백으로 세어진다는 것이 위쪽 경계를 정한다. - -스물셋 가운데 참을 받는 것이 스물셋이다. 예시 넷은 대표값이고 방향이 갈리는 코드 점은 없다. - -## 경계 두 개가 더 있다 - -`U+007F` 는 U+0020 보다 크므로 다듬기가 떼지 않는다. 삼항은 원문 가지를 고르고 `sanitize` 는 `CONTROL` 규칙으로 그것을 지우므로 두 값이 갈려 거짓이 된다. - -`U+00A0` 은 `Character.isWhitespace` 가 거짓이고 다듬기도 떼지 않는다. `REMOVALS` 의 여섯과 `CONTROL`, `WHITESPACE` 어느 것에도 걸리지 않아 입력이 그대로 살아남고 참이 된다. 눈에 보이지 않는 한 글자가 이미 안전한 메시지로 통과한다. - -길이는 별개다. 같은 글자를 `maxLength` 보다 길게 늘이면 `:70-74` 의 잘림이 개입해 거짓이 된다. - -정제기에서 값을 바꾸는 것은 여덟 규칙만이 아니다. 규칙을 적용한 뒤 `:66` 이 한 번 더 다듬고, 빈 값은 `:58-60`·`:67-69`·`:75` 에서 편집 문구로 바뀌며, 상한을 넘으면 `:70-74` 가 잘라 낸다. 제어 문자 스물셋에는 앞의 둘이 이어서 걸린다. `CONTROL` 이 그 글자를 공백 하나로 바꾸고, `:66` 이 그것을 다듬어 값을 비우고, 그제서야 `:67-69` 가 편집 문구를 돌려준다. `:66` 을 빼면 값이 공백 하나로 남아 스물셋이 전부 거짓이 된다. 앞뒤 공백이 참을 받는 것은 첫째 장치 하나 때문이고, 길이가 걸리는 것은 마지막 장치다. - -## 확인하지 못한 것 - -보충 평면은 훑지 않았다. 자바의 문자 하나가 담지 못하는 값이라 이 판정에 걸릴 수 없다고 보았을 뿐 세어 보지는 않았다. 정제기의 다른 메서드에도 같은 판정 불일치가 있는지도 세지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-a14-f005-publicpaths-restrictedpathrule.md b/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-a14-f005-publicpaths-restrictedpathrule.md deleted file mode 100644 index 6814f05..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-a14-f005-publicpaths-restrictedpathrule.md +++ /dev/null @@ -1,191 +0,0 @@ ---- -kind: CASE -slug: a14-f005-publicpaths-restrictedpathrule -title: 공개 경로가 관리 경로를 덮으면 제한 규칙이 건너뛰어지고, 그 겹침을 보는 것이 없다 -topic: web-inbound-and-http-surface -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -evidenceCapturedOn: 2026-09-02 -rootTreeNode: case:a14-f005-publicpaths-restrictedpathrule -body: case-a14-f005-publicpaths-restrictedpathrule.body.md -assets: - - key: a14-f005-publicpaths-restrictedpathrule - file: ../../../final/evidence/rendered/a14-f005-publicpaths-restrictedpathrule.svg - - key: a14-f005-publicpaths-restrictedpathrule-chain - file: ../../../final/evidence/rendered/a14-f005-publicpaths-restrictedpathrule-chain.svg - - key: a14-f005-publicpaths-restrictedpathrule-mgmtport - file: ../../../final/evidence/rendered/a14-f005-publicpaths-restrictedpathrule-mgmtport.svg -evidence: - - ../../../final/evidence/raw/a14-f005-publicpaths-restrictedpathrule.txt - - ../../../final/evidence/raw/a14-f005-publicpaths-restrictedpathrule-chain.txt - - ../../../final/evidence/raw/a14-f005-publicpaths-restrictedpathrule-mgmtport.txt -source: - - 원본 분석 절은 final/document.md#a14#L582 이다. ---- - -# 공개 경로가 관리 경로를 덮으면 제한 규칙이 건너뛰어지고, 그 겹침을 보는 것이 없다 - -보안 사슬이 설정에서 온 공개 경로를 먼저 등록하고 관리 평면 규칙을 그다음에 등록한다. 앞의 등록이 뒤의 패턴을 삼키면 뒤는 아무 일도 하지 못한다. 실제 패턴과 권한으로 사슬을 세워 열한 번 왕복해 보니, 권한이 없는 인증 호출자가 403 을 받아야 할 자리에서 사슬 끝까지 갔다. - -## 관계 - -- **publicPaths가 먼저 등록되어 제한 경로 규칙을 덮는다** - 이 사례에서 확인한 등록 순서를 개념으로 정리한 문서다. -- **단일 admission point는 우회 경로를 세어야 성립한다** - 우회 경로를 세지 않으면 단일 지점이 성립하지 않는다는 점이 같다. -- **production 판정이 두 개의 리터럴 프로파일 이름에 걸려 있다** - 두 사례 모두 설정값 때문에 보안 규칙보다 먼저 요청이 통과할 수 있다. -- **runtime_memberships를 먼저 읽고 심각도를 정한다** - 이 리프에 그 완화가 적용되지 않는다는 것을 확인할 때 쓴 규칙이다. - -## 문제 - -보안 사슬에 두 종류의 등록이 있다. 설정으로 받은 공개 경로를 모두 허용하는 등록과, 관리 평면을 지키는 제한 경로 규칙이다. 등록 순서가 어떤 결과를 만드는지 확인했다. - -## 결론 - -공개 경로가 먼저 등록된다. - -제한 경로 규칙의 자바독은 이 규칙이 존재하는 이유를 적는다. 나중에 참조되는 응용 수준 정책은 이미 요청을 통과시킨 전송을 되돌릴 수 없다는 것이다. 그런데 모두 허용 등록이 그보다 먼저 와서 정확히 그 일을 한다. - -순서만 읽고 결론을 내지 않았다. 프로덕션 패턴과 권한으로 사슬을 짓고, 호출자 셋과 공개 경로 값 다섯을 바꿔 가며 열한 번 왕복했다. - -인증 없는 요청만 보아서는 판정이 갈리지 않는다. 캐치올도 같은 401 을 내기 때문이다. 그래서 권한만 없는 호출자를 함께 넣었다. - -권한만 없는 호출자를 넣으면 결과가 갈린다. 덮이지 않은 요청은 403 을 받는다. 규칙이 요구하는 권한이 이 호출자에게 없어서다. 덮이는 순간 같은 호출자가 사슬 끝까지 간다. 규칙은 등록되어 있는데 아무 일도 하지 않는다. - -두 대조군이 그 403 의 출처를 못박는다. 규칙 빈을 빼면 같은 요청이 200 이 되고, 권한을 가진 호출자를 넣어도 200 이 된다. 403 을 만든 것은 규칙이고 캐치올이나 진입점이 아니다. - -원문이 예로 든 값은 이 일을 일으키지 않는다. 관리 경로가 /internal/ 아래에 있어서 /api/** 를 열어도 닿지 않고, 같은 호출자가 그 값에서는 403 을 받는다. 겹치게 하려면 운영자가 /internal/** 이나 /** 같은 값을 직접 넣어야 한다. 원문도 그 조건을 달아 두었다. - -여기에 배포 형태라는 조건이 하나 더 있다. 관리 평면 라우트는 관리 컨텍스트에 실려 나가고, 출하 설정은 그 서버에 별도 포트를 준다. 포트가 갈려 있으면 그 경로를 받을 핸들러가 애플리케이션 쪽에 없다. - -이 대목은 소스 주석이 먼저 말해 둔 것이라 받아 적지 않고 프레임워크에 질의했다. 출하되는 한 쌍만 갈린 쪽으로 나오고, 관리 포트를 애플리케이션과 맞추거나 비우거나 지운 세 경우는 모두 한 컨텍스트로 묶인다. 같다는 판정에서 켜지는 설정이 고르는 목록에 이 저장소의 관리 컨텍스트 설정이 있고, 그 설정의 등록 타입은 두 배치 모두를 받는다. 그 배포에서는 관리 라우트가 애플리케이션 쪽으로 넘어온다. 프로브는 그때 요청을 판정하는 사슬의 생성 메서드를 그대로 불렀다. - -부팅에는 그 겹침을 검사하는 자리가 없다. 규칙 타입을 언급하는 파일이 셋이고 전부 main 인데, 두 집합을 함께 쥐는 자리는 등록하는 곳 하나뿐이고 거기서 하는 일은 비교가 아니다. 공개 경로 쪽은 표기 세 형태로 다시 훑었다. 그 키를 쥔 파일이 열아홉이고 그중 둘은 추적되지 않는데, 어느 쪽에도 제한 패턴이 함께 나오지 않는다. - -가까운 통제가 하나 있기는 하다. 공개 경로에는 스냅숏 게이트가 걸려 있다. 다만 그것이 재는 것은 운영자 기계에 있는 파일 하나와 기준선의 차이다. 그 파일은 .gitignore 가 막아 저장소에 없고, 없으면 게이트는 비교에 들어가기 전에 던진다. 그 태스크를 부르는 자리가 워크플로 셋에 네 곳인데, 어느 곳도 그 파일을 만들지 않는다. 배포 시점 환경 변수는 애초에 이 경로 어디에도 나타나지 않는다. - -규칙 생성자는 자기가 물러지는 것만 막는다. 빈 권한 목록을 거부하고 그 이유를 예외 문구에 적어 둔다. 그 이유가 규칙 전체가 건너뛰어지는 경우에도 그대로 적용된다는 것이 이 사례다. - -권고는 둘 중 하나다. 제한 규칙을 공개 경로보다 먼저 등록하거나, 두 패턴 집합이 겹치면 부팅에서 거부하는 것이다. - -판정은 P3 다. 이 결함이 오늘 도는 배포에 닿으려면 네 가지가 동시에 참이어야 한다. 파일서버 플랫폼 스위치가 켜져야 하고, 그 아래 관리 평면 스위치도 켜져야 하고, 운영자가 관리 경로를 덮는 공개 경로 값을 넣어야 하고, 관리 서버 포트를 애플리케이션 포트와 같게 두거나 비워야 한다. 넷 다 출하값이 반대쪽이다. 두 스위치는 거짓으로 나가고, 출하되는 공개 경로 목록에는 헬스체크만 있고, 두 포트는 9001 과 8080 으로 갈라져 있다. - -심각도 규칙은 리프의 런타임 멤버십을 먼저 읽으라고 적는다. 이 리프의 멤버십은 비어 있지 않으므로 리프 단위 완화를 쓸 수 없고, 그 규칙이 등급을 낮춰 주지도 않는다. P3 인 이유는 위 네 조건이지 그 규칙이 아니다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 프로덕션 패턴과 권한으로 세운 필터 사슬에 열한 번 왕복, 관리 포트 판정과 컨텍스트 선택을 프레임워크에 직접 질의, 등록 순서와 출하 스위치 확인 -소스 수정 : x - -## 재현 조건 - -이 사례의 프로브는 새로 만든 것이고 원문에는 없다. 원문 근거는 분석 문서의 #L582 절이다. - -1. 사슬 등록 블록을 읽어 세 등록의 순서를 확인한다. -2. 제한 경로 규칙의 자바독과 생성자 검증을 읽는다. -3. 규칙 빈을 내놓는 설정과 그것을 켜는 두 스위치를 찾는다. -4. 관리 라우트를 등록하는 관리 컨텍스트 설정과 출하 포트를 읽고, 두 포트가 같을 때 그 설정이 어디로 가는지 프레임워크에 묻는다. -5. 공개 경로 기본값과 규칙 타입을 언급하는 파일을 전수로 찾는다. -6. 프로덕션 코드가 쓰는 생성 메서드로 필터 사슬을 세운다. -7. 무인증 호출자와 권한 없는 인증 호출자로 각각 공개 경로를 바꿔 가며 상태를 읽는다. -8. 권한을 가진 호출자와 규칙 빈을 뺀 사슬로 403 의 출처를 가른다. - -## 본문 - - - -등록 세 개가 한 블록 안에 순서대로 있다. - -```java -// SecurityConfig.java:83-94 - .authorizeHttpRequests( - auth -> { - if (publicPaths.length > 0) { - auth.requestMatchers(publicPaths).permitAll(); - } - // Ordered before the authenticated catch-all: a management path must be refused at - // the transport, not by an application policy the request has already passed. - for (RestrictedPathRule rule : restricted) { - auth.requestMatchers(rule.pathPattern()).hasAnyAuthority(rule.authorities()); - } - auth.anyRequest().authenticated(); - }) -``` - -주석은 제한 규칙을 인증 캐치올보다 앞에 둔 것을 설명한다. 그보다 앞선 등록은 다루지 않는다. - -## 규칙은 어디서 오고 무엇이 켜야 하는가 - -:::evidence key="a14-f005-publicpaths-restrictedpathrule" alt="코드베이스 정적 검색 출력 138줄. 사슬 등록 블록, 규칙 생성자의 빈 권한 거부, 규칙을 내놓는 자리, 프로덕션 패턴과 관리 평면 스위치, 그 설정 클래스를 부르는 자리 전부와 그것을 들이는 자동설정의 조건, 컴포넌트 스캔의 제외 정규식 전문, 관리 컨텍스트 등록과 두 포트, 설정 기본값, 공개 경로를 접근자로 읽는 자리, 그 키를 쥔 파일 열아홉과 그중 추적되지 않는 둘과 제한 패턴을 함께 언급하는 파일 수, 규칙 타입을 언급하는 파일 셋, 스냅숏 게이트의 두 입력과 값 쪽 입력의 무시 규칙과 없을 때의 중단과 그것을 부르는 네 자리와 워크플로의 .env 언급 전부, 마지막으로 게이트가 제한 패턴을 언급하는 횟수가 차례로 보인다." caption="등록 순서와 프로덕션이 등록하는 값 — 138줄 · exit 0" zoom="true" -::: - -규칙을 내놓는 자리는 `FileserverAdminPlaneConfiguration:36` 하나이고, 패턴은 `/internal/fileserver/**`, 권한 기본값은 `ROLE_FILE_ADMIN` 이다. - -그 설정 클래스를 이름으로 부르는 자리는 셋인데, 그중 등록에 해당하는 것은 `FileserverPlatformAutoConfiguration:71` 의 `@Import` 하나다. 나머지 둘은 클래스 선언과 자바독의 링크다. 그 자동설정에도 자기 스위치가 있고, 컴포넌트 스캔은 자동설정 패키지를 정규식으로 제외한다. 스위치를 우회해 이 설정에 닿는 길이 없다는 뜻이다. 두 스위치는 `false` 로 출하된다. - -공개 경로 기본값은 헬스체크 하나다. `publicPaths()` 를 읽는 프로덕션 코드는 `SecurityConfig:75` 하나뿐이다. - -## 열한 번 왕복해서 받은 상태 - -:::evidence key="a14-f005-publicpaths-restrictedpathrule-chain" alt="JVM 프로브 출력 22줄. 관리 라우트 하나에 요청을 보내며 호출자 세 종류와 공개 경로 값 다섯을 바꿔 가며 받은 상태 코드를 네 묶음으로 적었다. 마지막 묶음은 규칙 빈을 뺀 사슬이다." caption="호출자와 공개 경로 조합별 상태 — 22줄 · exit 0" zoom="true" -::: - -`SecurityConfig` 의 사슬 생성 메서드를 그대로 불러 필터 사슬을 만들고 `/internal/fileserver/storage-health` 에 요청을 보냈다. 컨트롤러가 실제로 매핑하는 여덟 라우트 중 하나다. 사슬 끝에 200 을 적는 종단을 달았으므로 200 은 인가를 통과했다는 뜻이다. - -무인증 호출자만으로는 규칙이 일했는지 알 수 없다. yml 기본값에서도 401 이고 `/api/**` 를 열어도 401 인데, 그 401 은 `anyRequest().authenticated()` 도 낼 수 있는 값이다. - -권한 없는 인증 호출자를 넣으면 갈린다. 겹치지 않는 세 값에서는 403 인데 그중 하나가 `/api/healthcheck` 다. `local` 프로파일이 그 값을 리터럴로 박아 두고, 샘플 애플리케이션도 같은 값을 기본으로 쓴다. `/internal/**` 이나 `/**` 로 덮으면 같은 호출자가 200 을 받는다. - -그 403 이 규칙에서 왔다는 것은 두 줄이 더 말한다. 권한을 `ROLE_FILE_ADMIN` 으로 바꾸면 같은 요청이 200 이고, 규칙 빈이 없는 사슬에 보내도 200 이다. 규칙이 있고 권한이 없을 때만 403 이다. - -## 어느 배포에서 성립하는가 - -관리 라우트를 등록하는 것은 `@ManagementContextConfiguration` 이고, 그 컨트롤러 패키지는 컴포넌트 스캔의 제외 정규식에 이름이 올라 있다. 출하 설정은 관리 서버에 9001, 애플리케이션에 8080 을 준다. - -포트가 갈린 배포에서는 관리 경로가 애플리케이션 커넥터에 오르지 않는다. 넓은 공개 경로가 덮는 것은 핸들러가 없는 경로다. - -:::evidence key="a14-f005-publicpaths-restrictedpathrule-mgmtport" alt="JVM 프로브 출력 17줄. 포트 조합 넷에 대한 판정, 같다고 판정될 때 켜지는 설정과 그것이 고르는 타입, 그 설정이 고르는 클래스 넷이 차례로 보이고 그중 하나가 이 저장소의 관리 컨텍스트 설정이다." caption="관리 포트 판정과 컨텍스트 선택 — 17줄 · exit 0" zoom="true" -::: - -이 대목은 관리 컨텍스트 설정의 자바독이 먼저 적어 둔 것이라, 옮겨 적는 대신 프레임워크에 물었다. `ManagementPortType` 은 출하값 한 쌍을 `DIFFERENT` 로, 두 포트가 같은 값과 관리 포트가 빈 값과 아예 없는 값을 `SAME` 으로 답한다. `SAME` 일 때 켜지는 설정 안의 중첩 설정이 `@EnableManagementContext(SAME)` 를 달고 있고, 그 선택자가 고르는 넷 중 하나가 `FileserverAdminManagementContextConfiguration` 이다. 그 설정의 등록 타입은 `ANY` 라 두 배치 어디에도 들어간다. - -두 포트가 같으면 관리 라우트는 애플리케이션 컨텍스트로 들어온다. 그 요청을 받는 것은 `SecurityConfig` 가 만드는 필터 사슬이고, 이 프로브가 부른 것이 그것을 만드는 메서드다. - -## 두 집합을 마주 놓는 코드 - -규칙 타입을 언급하는 파일은 셋이고 전부 main 이다. 두 집합을 함께 쥐는 자리는 `SecurityConfig` 하나인데 거기서 하는 일은 비교가 아니라 순서대로 등록하는 것이다. - -공개 경로 쪽에서도 훑었다. 점 표기와 환경 변수와 중첩 YAML 세 형태로 빌드 산출물을 뺀 작업 트리를 뒤지면 그 키를 쥔 파일이 열아홉인데, 그중 둘은 저장소가 추적하지 않는다. `.env` 와 SDD 작업 폴더에 남은 리뷰 diff 다. 추적되는 열일곱은 스냅숏 기준선과 게이트 스크립트, 문서 셋, yml 다섯, 자바 일곱이고 자바는 전부 시험이다. 열아홉 중 제한 패턴을 함께 언급하는 파일은 없다. - -스냅숏 게이트는 있다. `src/gradle/public-path-snapshot.gradle` 이 승인 없는 변경을 빌드에서 막는다고 되어 있다. 그런데 그것이 읽는 두 파일의 성격이 다르다. 기준선 `docs/security/public-paths-snapshot.txt` 는 저장소에 있지만, 값을 가져오는 `.env` 는 `.gitignore:7` 이 막는 운영자 입력이다. - -그 파일이 없으면 게이트는 비교하지 않고 `missing public-path environment file` 로 던진다. 이 태스크를 부르는 자리는 워크플로 셋에 네 곳이다. `ci-quality-gates.yml` 이 서로 다른 두 잡에서 한 번씩, 나머지 둘이 한 번씩이다. 워크플로 전체를 훑어도 `.env` 가 나오는 곳은 트리거 경로 목록 둘뿐이고, 그 파일을 만드는 단계는 어느 워크플로에도 없다. 그러니 실제로 맞춰 보는 것은 운영자 기계의 값이다. 배포 시점 환경 변수는 그 경로에도 들어오지 않는다. 게이트가 제한 패턴을 언급하는 횟수도 0 이다. - -## 생성자가 막는 것과 막지 못하는 것 - -```java -// RestrictedPathRule.java:30-34 - if (requiredAuthorities.isEmpty()) { - throw new IllegalArgumentException( - "requiredAuthorities must not be empty: a rule that requires nothing is weaker than the " - + "authenticated default it replaces"); - } -``` - -빈 권한 목록은 거부하고 그 이유를 문구에 적는다. 아무것도 요구하지 않는 규칙은 그것이 대체하는 인증된 기본값보다 약하다는 것이다. - -같은 논리를 규칙 전체에 적용하면 이 사례가 된다. 규칙 전체가 건너뛰어지면 그것도 기본값보다 약하다. 그 경우는 아무 데서도 걸리지 않는다. - -## 확인하지 못한 것 - -세션 모드에서는 돌려 보지 않았다. 프로브가 세운 것은 JWT 모드의 사슬이고, 등록 블록은 두 모드가 갈리기 전에 있으므로 순서는 같다. - -프로브는 애플리케이션 사슬 하나만 감쌌다. 프로덕션에는 액추에이터 사슬이 더 있고, 스위치가 켜지면 콜백 사슬도 붙는다. 둘 다 경로 매처가 관리 경로와 겹치지 않아 판정이 달라지지 않는다고 읽었을 뿐 돌려 보지는 않았다. 관리 컨텍스트 쪽은 포트 판정과 설정 선택까지만 물었고, 애플리케이션을 두 포트가 같은 설정으로 실제로 띄운 것은 아니다. - -200 은 프로브가 사슬 끝에 단 종단이 적는 값이지 컨트롤러의 응답이 아니다. 인증된 호출자도 토큰을 제시해 만든 것이 아니라 사슬이 읽는 자리에 직접 넣은 것이라, 이 왕복은 디코더와 변환기를 지나지 않는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-a14-f010-no-store.md b/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-a14-f010-no-store.md deleted file mode 100644 index 8217b08..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-a14-f010-no-store.md +++ /dev/null @@ -1,199 +0,0 @@ ---- -kind: CASE -slug: a14-f010-no-store -title: 저장 금지와 조건부 읽기가 같은 응답에 실리고, 둘을 이으라고 만든 이름은 아무도 읽지 않는다 -topic: web-inbound-and-http-surface -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -evidenceCapturedOn: 2026-09-03 -rootTreeNode: case:a14-f010-no-store -body: case-a14-f010-no-store.body.md -assets: - - key: a14-f010-no-store - file: ../../../final/evidence/rendered/a14-f010-no-store.svg - - key: a14-f010-no-store-conditional - file: ../../../final/evidence/rendered/a14-f010-no-store-conditional.svg -evidence: - - ../../../final/evidence/raw/a14-f010-no-store.txt - - ../../../final/evidence/raw/a14-f010-no-store-conditional.txt -source: - - 원본 분석 절은 final/document.md#a14#L899 이다. ---- - -# 저장 금지와 조건부 읽기가 같은 응답에 실리고, 둘을 이으라고 만든 이름은 아무도 읽지 않는다 - -이 저장소에는 조건부 읽기를 구현한 자리가 둘이다. 둘 다 같은 응답에 저장 금지를 함께 싣는다. 한쪽은 캐시 필터의 기본값이 그대로 남아서, 다른 한쪽은 자기 정책의 출하 기본값이 그래서다. 둘을 조정하라고 만든 어휘가 있지만 부르는 코드가 없다. - -## 관계 - -- **캐시 헤더를 소유하는 것은 24줄 상수 두 개다** - 이 사례에서 확인한 캐시 헤더 기본값을 개념으로 정리한 문서다. -- **maxArrayElements가 선언만 되고 강제되지 않으며, 바이트 예산 백스톱도 없다** - 같은 리프의 다른 P2 다. -- **중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다** - 같은 일을 하는 어휘가 여럿일 때 쓴 규칙이다. -- **내부 소비자 부재와 public 계약 필요성을 따로 묻는다** - 참조 0 을 어떻게 읽을지 정할 때 쓴 규칙이다. -- **runtime_memberships를 먼저 읽고 심각도를 정한다** - 이 리프가 픽스처 배포에도 실리는지를 가릴 때 쓴 규칙이다. - -## 문제 - -응답 하나에 개체 태그와 저장 금지가 함께 실리는지, 실린다면 그 저장 금지를 누가 쓰는지 확인했다. - -## 결론 - -필터의 기본값은 예외를 허용하도록 설계돼 있다. 헤더가 사슬 앞에서 얹히므로 뒤에서 자기 값을 쓰는 쪽이 이긴다. 대조용 엔드포인트로 확인했다. 원문은 필터가 모든 응답에 저장 금지를 붙인다고 적었는데, 붙는 것은 덮어쓸 수 있는 기본값이다. - -응답에 개체 태그를 쓰는 파일은 다섯이고, 그중 둘은 서로를 가리킬 뿐 밖에서 오는 호출이 없어 어느 응답에도 태그를 얹지 못한다. 남는 셋이 만드는 경로는 둘이다. 파일서버 내려받기와 픽스처 모듈의 단건 조회다. - -두 자리가 저장 금지를 받는 경로는 서로 다르다. - -내려받기 쪽은 지시자까지 스스로 정해서 필터의 값을 덮는다. 그러니 거기 실린 저장 금지는 그 경로의 출하 기본값이다. 태그와 304 결정을 만든 컴포넌트가 같은 응답에서 캐시를 무력화한다. - -픽스처 모듈 쪽은 아무것도 쓰지 않아서 필터의 값이 그대로 남는다. 이 경로는 프로브로 왕복해 봤다. 세 요청 모두 태그와 저장 금지를 함께 받았고 304 도 예외가 아니었다. - -Cache-Control 값을 정하는 컴포넌트는 두 배포에서 다르다. 플랫폼 배포에서는 보안 설정이 프레임워크 캐시 기록기를 꺼서 기본값을 얹는 것이 필터 하나만 남는다. 픽스처 모듈은 그 설정을 스캔에서 빼고 자기 체인으로 대신하는데, 거기에는 그 끄기가 없다. 그래서 픽스처 쪽 사슬에는 Spring Security 의 캐시 헤더 기록기가 그대로 있다. 그런데도 헤더가 같은 것은 그 기록기가 값이 이미 있으면 손대지 않기 때문이다. 필터 없이 돌린 배치가 그것을 보여 준다. - -무엇이 무효가 되는지는 좁게 잡아야 한다. RFC 9111 의 저장 금지는 캐시에 거는 지시라, 끊기는 것은 재검증 경로다. 응답을 캐시에 맡기지 않고 태그만 자기 상태에 저장해 두는 소비자는 영향을 받지 않는다. 쓰기 쪽 일치 조건도 무관하다. 거기서는 클라이언트가 아무것도 쌓아 둘 이유가 없다. - -이 어긋남을 없애라고 만들어 둔 것이 있다. 캐시 패키지가 프로파일과 지시자와 Vary 규칙을 네 파일 삼백열 줄로 담고 있는데, 그 타입을 부르는 자리는 전부 자기 패키지 안이거나 자기 시험이다. 연산 프로파일이 캐시 정책을 이름으로 들고 다니는데, 그 이름을 읽는 코드도 0 이다. 조건부 읽기 쪽에도 같은 자리가 있다. 원문에는 없는 것이다. 304 판정을 전담하라고 만든 클래스를 프로덕션에서 부르는 코드가 없다. 같은 패키지의 ETags 가 같은 비교를 따로 구현해 두었고, 조회 컨트롤러는 그 클래스를 부른다. - -목록이 발행하는 프로파일 하나는 내려받기 경로가 손으로 쓴 것과 같은 문자열을 이미 갖고 있다. 이으려면 이름부터 맞춰야 한다. 연산 쪽 기본 이름이 그 목록에 없는 이름이기 때문이다. - -모듈 경계가 이 패키지를 Spring 에서 떼어 둔 이유를 적어 두었다. 세 자리가 프로파일을 나눠 쓰게 하려는 것이었는데, 셋 중 어느 것도 그렇게 하고 있지 않다. 원문은 그 셋이 존재하지 않는다고 적었는데, 서블릿 기록기와 반응형 기록기는 파일서버 내려받기 경로에 있다. 이 패키지를 참조하지 않을 뿐이다. - -지시자를 문자열로 쥔 main 자바 파일이 여덟인데 여섯은 이 패키지 바깥이다. 자바 밖에도 셋이 더 있다. 플랫폼 설정 파일의 프로퍼티 기본값과, 환경 파일 둘이 같은 문자열을 각각 대입으로 다시 쓴다. - -권고는 필터가 상수 대신 프로파일 목록을 읽고, 연산에 맞는 프로파일이 없을 때만 저장 금지로 떨어지게 하는 것이다. 저장 금지 기본값 자체는 민감한 API 에 옳다. 바꿀 것은 예외가 없다는 점이다. - -판정은 P2 다. 다만 두 경로의 도달성이 다르다. - -내려받기는 파일서버 플랫폼 마스터 스위치가 켜져야 존재하고, 그 출하값은 세 곳 모두 거짓이다. 픽스처 모듈의 단건 조회는 다르다. config/architecture/modules.json 이 선언한 런타임 컴포지션은 app-bootstrap 과 sample-portfolio 둘이고, 픽스처 모듈이 그중 하나다. 그 모듈의 조회 컨트롤러에는 스위치가 없다. 프로브가 받은 세 응답이 그 배포에서 오늘 나오는 값이다. - -기본 플랫폼 배포에서 무조건 도는 것은 필터뿐이고, 거기에는 아직 낭비할 조건부 읽기가 없다. 스위치를 켜면 내려받기 경로에서도 같은 일이 일어난다. 서버는 캐시를 거치는 소비자에게 매번 전부를 보내고, 태그 계산과 조건부 평가는 매 요청 돌면서 아무것도 아끼지 못한다. 어느 쪽도 오류를 내지 않으므로 조용하다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 필터와 컨트롤러 조합을 바꿔 세 사슬에서 여섯 번 왕복, 태그를 쓰는 자리와 그 조건, 리프의 런타임 소속과 캐시 어휘 계수 -소스 수정 : x - -## 재현 조건 - -원문 근거는 분석 문서의 #L899 절이다. - -1. 필터가 붙이는 두 헤더와 그것을 사슬 앞에 두는 이유를 읽는다. -2. 보안 설정이 프레임워크 기록기를 끄는 자리와, 그 설정을 스캔에서 빼는 모듈을 읽는다. -3. 개체 태그를 응답에 쓰는 파일을 전수로 찾고 각자를 켜는 조건을 읽는다. -4. 내려받기 기록기가 태그와 지시자를 스스로 쓰는 두 줄과 그 출하 기본값을 읽는다. -5. 픽스처 모듈의 조회 경로와 그 모듈이 캐시 지시자를 정하는지 확인한다. -6. 필터와 컨트롤러를 한 사슬에 올려 태그 없이, 맞는 태그로, 틀린 태그로 요청한다. -7. 대조용 엔드포인트 하나를 같은 사슬에 올려 덮어쓰기가 되는지 본다. -8. 프레임워크 기록기를 사슬에 넣은 배치와 그것만 남긴 배치를 각각 돌린다. -9. config/architecture/modules.json 에서 선언된 런타임 컴포지션과 이 리프의 runtime_memberships 를 읽는다. -10. 캐시 패키지의 타입을 부르는 자리, 연산 프로파일의 캐시 이름을 읽는 자리, 지시자 문자열을 들고 있는 자리를 센다. - -## 본문 - - - -필터는 사슬에 들어가기 전에 두 헤더를 얹는다. - -```java -// CacheControlFilter.java:24-34 - static final String DEFAULT_CACHE_CONTROL = "no-store"; - static final String DEFAULT_VARY = "Accept, Accept-Encoding, Authorization"; - - @Override - protected void doFilterInternal( - HttpServletRequest request, HttpServletResponse response, FilterChain chain) - throws ServletException, IOException { - response.setHeader(ApiHeaders.CACHE_CONTROL, DEFAULT_CACHE_CONTROL); - response.setHeader(ApiHeaders.VARY, DEFAULT_VARY); - chain.doFilter(request, response); - } -``` - -기본값을 먼저 얹고 사슬을 부르므로 뒤에서 자기 헤더를 쓰는 엔드포인트가 이긴다. 클래스 자바독이 그것을 옵트인이라고 부른다. - -## 소유자는 배포마다 다르다 - -:::evidence key="a14-f010-no-store" alt="코드베이스 정적 검색 출력 158줄. 필터의 자바독과 두 상수와 본문, 보안 설정이 프레임워크 캐시 기록기를 끄는 네 줄과 그 설정을 빼는 픽스처 모듈과 그 모듈이 적어 둔 제외 이유와 대신 두는 체인, 응답에 개체 태그를 쓰는 파일 다섯과 그중 서로를 가리킬 뿐 밖에서 호출이 없는 둘, 시험 밖에서 부르는 자리가 없는 304 판정 클래스와 같은 패키지에서 실제로 호출되는 ETags, 레지스트리가 선언한 런타임 컴포지션과 이 리프의 소속, 그 304 판정 클래스의 자바독 첫 줄, 내려받기 컨트롤러를 켜는 조건과 그 스위치의 출하값, 내려받기 기록기가 쓰는 태그와 지시자와 설정 파일의 기본값, 픽스처 모듈의 조회 경로, 캐시 패키지 네 파일의 줄 수와 그 타입을 부르는 파일별 횟수, 모듈 경계가 적은 근거, 캐시 헤더를 쓰는 자리, 연산 프로파일의 캐시 이름과 그것을 읽는 자리 수, 지시자 문자열을 들고 있는 자리, 목록이 발행하는 프로파일 이름이 차례로 보인다." caption="캐시 정책을 정하는 자리와 그것을 쓰는 자리 — 158줄 · exit 0" zoom="true" -::: - -플랫폼 쪽 보안 설정은 프레임워크의 캐시 헤더 기록기를 끄고 주석에 이유를 적는다. 캐시 헤더 정책은 이 필터가 갖는다는 것이다. 기본값을 얹는 자리가 하나라는 뜻이고, 그 뒤에 자기 값을 쓰는 기록기는 별개다. - -픽스처 모듈은 그 보안 설정을 컴포넌트 스캔에서 뺀다. 공개 데모라 IdP 없이 돌아야 한다는 이유다. 대신 `/**` 를 잡는 자기 체인을 두는데, 그 체인은 캐시 헤더 기록기를 끄지 않는다. 그래서 그 배포에는 프레임워크 기록기가 남는다. - -## 태그를 싣는 경로는 내려받기와 픽스처 조회 둘이다 - -응답에 개체 태그를 쓰는 main 파일은 다섯이다. 아웃바운드 S3 클라이언트는 뺐다. 그중 `WebSuccessResponse` 는 `WebResponseContract` 를 부르고 `WebResponseContract` 는 자바독으로 그것을 되가리키는데, 둘 다 밖에서 오는 호출이 없다. 남는 것은 내려받기 기록기 둘과 픽스처 모듈의 조회 컨트롤러이고, 내려받기의 서블릿 쪽과 반응형 쪽은 같은 결정을 두 전송으로 그린 것이므로 태그를 싣는 경로는 둘이다. - -같은 자리에 하나 더 있다. `ConditionalReadEvaluator` 가 GET 과 HEAD 를 304 로 답해도 되는지 판정하라고 만들어져 있는데, 자기 시험 말고는 부르는 코드가 없다. 같은 `conditional` 패키지에 `ETags` 가 따로 있고, 그 클래스가 같은 비교를 한다. 픽스처 컨트롤러는 그 비교를 `ETags.matches` 로 부른다. 한 패키지에 판정기가 둘 있고, 프로덕션이 부르는 쪽은 전담 클래스가 아니다. - -내려받기 기록기는 태그를 쓴 뒤 지시자도 자기가 쓴다. - -```java -// MvcDownloadResponseWriter.java:37,43 - response.setHeader(HttpHeaders.ETAG, descriptor.representation().strongEtag()); - ... - response.setHeader(HttpHeaders.CACHE_CONTROL, descriptor.cacheControl()); -``` - -`setHeader` 이므로 필터가 얹어 둔 값은 덮인다. 거기 실리는 저장 금지는 필터의 것이 아니라 내려받기 정책 자신의 출하 기본값이다. `DownloadPolicy:25` 가 설계 기본값으로 정하고, `application.yml:880` 이 프로퍼티 기본값으로, `.env` 와 `.env.example` 이 각각 대입으로 같은 문자열을 다시 쓴다. 조건부 요청은 별도로 평가되어 304 결정까지 간다. - -한 컴포넌트가 강한 태그와 304 결정을 만들어 놓고, 같은 응답에 캐시더러 아무것도 남기지 말라고 적는 셈이다. - -다만 이 경로는 켜야 존재한다. 컨트롤러가 `app.fileserver-platform.enabled` 에 걸려 있고 그 스위치의 출하값은 세 곳 모두 거짓이다. - -## 사슬에 올려 받은 헤더 - -:::evidence key="a14-f010-no-store-conditional" alt="JVM 프로브 출력 14줄. 조건부 읽기 경로에 태그 없이, 맞는 태그로, 틀린 태그로 보낸 세 요청의 상태와 캐시 지시자와 개체 태그, 스스로 지시자를 붙이는 엔드포인트 한 줄, 프레임워크 기록기를 함께 넣은 배치와 그것만 남긴 배치 두 줄이 보인다." caption="세 사슬에서 받은 상태와 헤더 — 14줄 · exit 0" zoom="true" -::: - -픽스처 모듈의 단건 조회를 필터와 함께 한 사슬에 올리고 같은 자원에 세 번 요청했다. 저장소는 버전 3 인 표현 하나만 돌려준다. - -세 번 다 저장 금지와 `W/"3"` 이 함께 나온다. 맞는 태그를 되돌려 보내면 304 가 오는데 그 304 도 저장 금지를 달고 있다. - -넷째 줄이 대조군이다. 자기 지시자를 붙이는 엔드포인트에서는 `max-age=60` 이 그대로 나온다. 덮어쓰기는 실제로 동작하고, 조회 경로가 그것을 쓰지 않을 뿐이다. - -마지막 두 줄은 프레임워크 기록기를 넣은 배치다. 둘이 함께 있으면 헤더는 그대로다. 그 기록기가 이미 값이 있으면 쓰지 않기 때문이다. 필터를 빼고 기록기만 남기면 네 지시자짜리 다른 문자열이 나온다. - -## 무엇이 무효가 되는가 - -```java -// WorkLogController.java:143-147 - String etag = etagOf(workLog); - if (ETags.matches(ifNoneMatch, etag)) { - return ResponseEntity.status(HttpStatus.NOT_MODIFIED).eTag(etag).build(); - } - return ResponseEntity.ok().eTag(etag).body(WorkLogWebMapper.toResponse(workLog)); -``` - -RFC 9111 의 저장 금지는 캐시에 거는 지시다. 규격을 지키는 캐시는 표현도 태그도 남기지 않으므로, 캐시를 통해 재검증하는 클라이언트는 다음 요청에 붙일 것이 없다. 태그를 애플리케이션 상태로 따로 들고 있는 소비자는 여전히 조건부 요청을 보낼 수 있다. 무효가 되는 것은 캐시 재검증 쪽이다. - -쓰기 쪽 일치 조건은 다르다. 방금 받은 태그를 같은 세션에서 되돌려 보내는 낙관적 동시성이라 보관이 필요 없다. - -## 프로파일 이름과 그 이름을 읽는 코드 - -캐시 패키지는 네 파일이다. 헤더 값 28줄, 정책 레코드 72줄, 프로파일 목록 120줄, Vary 규칙 90줄이다. 그 타입들을 이름으로 부르는 자리는 전부 그 패키지 안이거나 그 패키지의 시험이고, 밖에서 부르는 파일은 없다. - -연산 프로파일 쪽도 같다. `WebOperationProfile` 이 `CachePolicyName` 을 필드로 들고 다니지만 `cachePolicy()` 를 읽는 자리가 0 이다. - -목록이 발행하는 프로파일은 `sensitive` `immutable-asset` `revalidated` `browser-private` 넷이고, `sensitive` 의 지시자가 `private, no-store` 다. 파일서버가 손으로 다시 쓴 그 문자열이다. 다만 연산 쪽 기본 이름은 `no-store` 라 목록에 그런 이름이 없다. 이름부터 맞지 않는다. - -모듈 경계는 이 패키지를 프레임워크에서 떼어 두면 기록기 둘과 문서 생성기가 같은 프로파일을 나눠 쓸 수 있다고 적는다. 그 셋 중 이 패키지의 프로파일을 적용하는 것은 없다. 저장소에는 서블릿 기록기와 반응형 기록기라 부를 만한 쌍이 둘 있지만 어느 쪽도 이 패키지를 참조하지 않는다. - -지시자를 문자열로 들고 있는 main 자바 파일은 여덟이고, 그중 여섯이 이 패키지 밖이다. 자바가 아닌 자리도 셋이다. 설정 파일의 프로퍼티 기본값과 환경 파일 둘의 대입이다. - -## 확인하지 못한 것 - -실제 클라이언트나 중간 캐시로 두 요청을 보내 재검증이 일어나지 않는 것을 재현하지는 않았다. 실제 클라이언트나 중간 캐시의 동작은 재현하지 않았고, 서버가 보내는 헤더까지만 확인했다. - -프로브는 사슬 셋을 세웠다. 필터와 컨트롤러, 필터와 프레임워크 기록기, 기록기 단독이다. 기록기가 든 두 사슬은 200 응답으로만 돌렸다. 경로 접두사를 붙이는 설정이 없어 요청 경로도 배포와 다르다. 헤더에는 영향이 없다고 읽었다. - -파일서버 내려받기 경로는 읽기만 했고 돌려 보지 않았다. 내려받기 응답에 태그와 지시자가 함께 실린다는 것은 기록기의 두 줄과 출하 기본값에서 읽은 것이고, 마스터 스위치를 켠 배포를 띄워 본 것도 아니다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-a14-f011-maxarrayelements.md b/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-a14-f011-maxarrayelements.md deleted file mode 100644 index 95648a8..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-a14-f011-maxarrayelements.md +++ /dev/null @@ -1,206 +0,0 @@ ---- -kind: CASE -slug: a14-f011-maxarrayelements -title: 배열 원소 상한은 선언만 되고, 파서가 그것을 대신할 수단은 이 팩토리에서만 비어 있다 -topic: web-inbound-and-http-surface -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -evidenceCapturedOn: 2026-09-03 -rootTreeNode: case:a14-f011-maxarrayelements -body: case-a14-f011-maxarrayelements.body.md -assets: - - key: a14-f011-maxarrayelements - file: ../../../final/evidence/rendered/a14-f011-maxarrayelements.svg - - key: a14-f011-maxarrayelements-converter - file: ../../../final/evidence/rendered/a14-f011-maxarrayelements-converter.svg - - key: a14-f011-maxarrayelements-parser - file: ../../../final/evidence/rendered/a14-f011-maxarrayelements-parser.svg -evidence: - - ../../../final/evidence/raw/a14-f011-maxarrayelements.txt - - ../../../final/evidence/raw/a14-f011-maxarrayelements-converter.txt - - ../../../final/evidence/raw/a14-f011-maxarrayelements-parser.txt -source: - - 원본 분석 절은 final/document.md#a14#L1006 이다. ---- - -# 배열 원소 상한은 선언만 되고, 파서가 그것을 대신할 수단은 이 팩토리에서만 비어 있다 - -JSON 프로파일이 한 배열에 받아들일 원소 수를 100,000 으로 선언한다. 그 값을 읽는 코드가 없다. 파서는 이 실패를 막을 상한을 두 가지 제공하고 이 저장소도 그중 하나를 여섯 자리에서 쓰는데, 요청 본문을 읽는 팩토리에서만 둘 다 비어 있다. - -## 관계 - -- **저장 금지와 조건부 읽기가 같은 응답에 실리고, 둘을 이으라고 만든 이름은 아무도 읽지 않는다** - 같은 리프의 다른 P2 다. -- **타입이 문서화한 불변식은 타입이 강제한다** - 이 프로파일이 어기고 있는 규칙이다. -- **문서가 범위를 제한하면 그 제한을 누가 강제하는지 같이 적는다** - 이 프로파일도 javadoc 으로 한도를 적어 두고 그것을 강제하는 자리를 두지 않았다. -- **"상한을 두고 읽는다"는 본문 핸들러가 전부 읽은 뒤에 자른다** - 두 사례 모두 상한이 선언돼 있지만 힙에 올라가는 것을 막지는 못한다. - -## 문제 - -JSON 프로파일이 중첩 깊이와 배열 원소 수와 문자열 길이 셋을 선언한다. 셋 다 실제로 걸리는지, 걸리지 않는다면 걸 수단이 있는지 확인했다. - -## 결론 - -배열 원소 상한만 걸리지 않는다. - -WebJsonProfile.maxArrayElements() 를 부르는 코드가 저장소 전체에 없다. 유계 팩토리는 깊이와 문자열 길이만 파서에 넘긴다. - -먼저 이 팩토리가 실제 요청 경로에 있는지 확인했다. 매퍼 빈은 타입으로 주입될 뿐 이름으로 지목되는 자리가 없어서, 컨텍스트를 띄워 요청 본문을 읽는 컨버터가 무엇을 들고 있는지 직접 물었다. 그 컨버터가 든 매퍼의 제약이 유계 팩토리가 세우는 값과 일치했고, 이 저장소 자동설정만 뺀 대조군에서는 Jackson 기본값이 왔다. 이 팩토리는 요청 경로에 있다. - -같은 이름의 필드가 요청 예산 타입에도 있다. 다만 값이 다르다. 프로덕션이 등록하는 예산의 배열 원소 수는 1,000 이다. 100,000 은 그 타입에서 플랫폼 절대 상한 상수로만 쓰이고, 그것을 읽는 자리는 두 곳이다. 깊이도 어긋난다. 예산은 32 를 담고 프로파일은 64 를 담는다. - -예산에 담긴 값도 요청을 판정하는 데는 쓰이지 않는다. 생성자가 그 값을 검사하고 예산끼리 비교하는 술어도 이 필드를 보지만, 그 술어에 이르는 재정의 등록을 프로덕션에서 부르는 코드가 없다. 검증은 생성 시점에 한 번 돌고, 비교는 시험에서만 돈다. - -예산을 강제하는 두 필터도 이 필드를 읽지 않는다. 둘이 재는 축의 수는 같다. 반응형 쪽은 흐르는 본문과 응답을 요청과 응답을 감싼 교환에서 잰다. 두 필터를 등록하는 자리는 각각 시험용 픽스처 하나씩이다. - -여기서 원문과 갈린다. - -원문은 파서에 해당 제약이 없으므로 파서 수준에서는 걸 수 없다고 적었다. 배열 원소 수만 놓고 보면 맞다. 그러나 Jackson 3 의 읽기 제약은 여섯 축을 받고, 그중 문서 길이와 토큰 수가 이 실패를 막는다. 이 팩토리는 넷을 쓰고 있다. 깊이와 문자열 길이는 이 코드가 정했고 이름 길이와 숫자 길이는 라이브러리 기본값이 남았다. 비어 있는 것은 문서 길이와 토큰 수 둘이다. - -문서 길이는 이 저장소가 이미 쓰는 축이다. 여섯 자리에서 쓴다. 그러나 배포에서 실제로 만들어지는 것은 메시징 코덱 하나뿐이고, 나머지 다섯은 생성하는 자리가 시험뿐이다. 같은 어댑터의 두 자리는 시험에서만 불리고 백엔드 jar 도 runtimeClasspath 에 없다. 웹소켓 어댑터의 두 자리와 메시징 쪽 스키마 레지스트리도 main 클래스이지만 생성은 시험뿐이다. 토큰 수만 저장소 어디에서도 쓰이지 않는다. - -프로브로 확인했다. 선언된 상한의 스무 배짜리 평평한 배열이 이 저장소의 팩토리에서는 끝까지 읽히고, 토큰 수 상한을 건 팩토리에서는 거부된다. - -유계 팩토리의 자바독이 이 원칙을 먼저 적어 두었다. 중첩 폭탄은 보내기는 싸고 들고 있기는 비싸므로 도움이 되는 유일한 한계는 흐름 파서가 넘기기를 거부하는 한계라는 것이다. 깊이와 문자열 길이에는 그 원칙을 적용했다. 원소 수에는 쓸 수 있는 축이 둘 남아 있는데도 값만 선언했다. - -권고도 원문과 다르다. 문서 길이 상한을 이 팩토리에도 걸면 된다. 다만 그대로 옮겨 붙일 수는 없다. 지금 이 팩토리에는 예산이 전달되지 않는다. 중간에 매퍼 팩토리가 한 겹 더 있어서, 넓혀야 할 서명이 둘이고 고쳐야 할 자동설정 메서드가 둘이다. 등록되는 예산이 하나뿐이라 지금은 고를 것도 없다. 토큰 수 상한을 함께 걸 수도 있지만 그 값은 배열 하나가 아니라 문서 전체에 걸리므로, 선언된 100,000 을 그대로 옮길 수는 없다. - -파서를 손대지 않는다면 원문이 든 두 선택지가 남는다. 계약 규칙 쪽으로 옮기거나, 걸리지 않는 필드를 지워 걸리는 것처럼 읽히지 않게 하는 것이다. - -판정은 P2 다. 이 리프는 두 런타임 컴포지션에 모두 올라 있고, 위에서 확인한 대로 JSON 본문을 읽는 컨버터가 이 팩토리의 매퍼를 든다. 크기 제약이 붙지 않은 컬렉션 필드는 상한 없이 실체화된다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Jackson : 3.1.5 -Spring Boot : 4.0.8 -확인 방식 : 선언과 참조 전수 검색, 파서가 제공하는 축 전수 확인, 유계 팩토리에 넓은 배열 통과, 컨텍스트를 띄워 컨버터가 든 매퍼 확인 -소스 수정 : x - -## 재현 조건 - -원문 근거는 분석 문서의 #L1006 절이다. - -1. JSON 프로파일의 세 상한 선언과 엄격 프로파일의 값을 읽는다. -2. maxArrayElements() 를 부르는 자리를 전수로 센다. -3. 요청 예산 타입의 같은 이름 필드와 그 검증·비교 자리, 그리고 프로덕션이 등록하는 값을 읽는다. -4. 재정의 등록과 절대 상한 술어를 부르는 자리를 전수로 센다. -5. 두 예산 필터가 읽는 필드와 그 필터를 등록하는 자리를 확인한다. -6. 유계 팩토리가 파서에 넘기는 값과 그 자바독을 읽는다. -7. 캐시에 있는 jackson-core jar 에서 읽기 제약 빌더가 받는 축을 전수로 뽑고, 그중 이 저장소가 이미 쓰는 축과 그 호출자를 센다. -8. 그 팩토리에 넓은 배열을 넣어 어디까지 읽는지 보고, 토큰 수 상한만 건 팩토리와 비교한다. -9. 자동설정을 올려 핸들러 어댑터의 JSON 컨버터가 든 매퍼의 제약을 읽고, 이 저장소 자동설정을 뺀 대조군과 비교한다. - -## 본문 - - - -프로파일이 세 상한을 선언하고 각각을 문서화한다. 엄격 프로파일이 그 값을 준다. - -```java -// WebJsonProfile.java:38-40 - public static WebJsonProfile strict() { - return new WebJsonProfile(true, true, true, true, true, 64, 100_000, 1_048_576); - } -``` - -가운데 `100_000` 이 `maxArrayElements` 다. - -## 선언한 자리와 읽는 자리 - -:::evidence key="a14-f011-maxarrayelements" alt="코드베이스 정적 검색 출력 145줄. 프로파일의 세 상한 선언과 엄격 프로파일 값, maxArrayElements 접근자를 부르는 자리 수, 예산 타입의 같은 이름 필드와 절대 상한 상수와 필드 순서와 프로덕션이 등록하는 값, 재정의 등록을 부르는 자리, 예산 목록을 만드는 자동설정 두 곳과 등록되는 예산 하나와 공개 팩토리 수, 두 예산 필터가 읽는 필드와 반응형이 감싼 교환에 넘기는 둘, 그 필터를 등록하는 두 자리, 유계 팩토리의 자바독과 파서에 거는 두 값, 유계 팩토리를 부르는 자리와 그것을 부르는 자동설정, Jackson 읽기 제약 빌더가 받는 여섯 축과 그중 이 저장소가 이미 쓰는 여섯 자리, 같은 어댑터의 두 자리가 받는 예산 타입, 그 여섯 자리를 실제로 만드는 곳 전부와 백엔드 잠금 구성, 표본 애플리케이션의 크기 제약이 차례로 보인다." caption="상한을 선언하는 자리와 그것을 읽는 자리 — 145줄 · exit 0" zoom="true" -::: - -`WebJsonProfile.maxArrayElements()` 를 부르는 자리는 0 이다. - -같은 이름의 필드가 `WebRequestBudget` 에도 있다. 다만 값이 다르다. 필드 순서로 보면 `standard()` 가 담는 배열 원소 수는 `1_000` 이고(`:92`), `100_000` 은 절대 상한 상수 `ABSOLUTE_ARRAY_ELEMENTS_MAX`(`:51`)로만 나타나며 그 상수를 쓰는 자리는 생성자의 상한 검사(`:85`)와 `withinPlatformMaximum()`(`:110`) 둘이다. 깊이도 갈린다. 예산은 `maxJsonDepth = 32`, 프로파일은 `maxDepth = 64` 다. - -예산 쪽 값도 요청을 판정하지 않는다. 생성자가 양수인지(`:59`)와 절대 상한 이하인지(`:85`) 검사하고, 예산끼리 비교하는 술어가 이 필드를 보지만(`:123`), 그 술어에 이르는 `registerOverride` 를 부르는 자리는 시험 두 줄뿐이다. 프로덕션 자동설정이 부르는 것은 비교하지 않는 `register` 다. - -예산을 강제하는 필터는 서블릿과 반응형 둘이고, 어느 쪽도 이 필드를 읽지 않는다. 두 필터가 재는 축은 다섯으로 같다. 서블릿은 다섯을 자기 파일에서 읽는다. 반응형은 넷을 자기 파일에서 읽는데, 그중 본문 바이트는 `:103` 이 선언된 Content-Length 만 먼저 보는 것이고 실제로 흐르는 바이트는 `:64` 가 만드는 `BoundedServerWebExchange` 가 잰다. 다섯째인 응답 바이트는 그 교환에서만 잰다. 그리고 두 필터를 등록하는 자리는 `src/testkit` 과 `src/webfluxContractTest` 의 픽스처 애플리케이션이다. - -## 이 팩토리가 요청 경로에 있는가 - -자동설정은 이 팩토리로 `webStrictObjectMapper` 라는 `JsonMapper` 빈을 만든다. 그 빈을 이름이나 한정자로 받는 코드는 저장소에 없으므로, 컨텍스트를 올려 직접 물었다. - -:::evidence key="a14-f011-maxarrayelements-converter" alt="JVM 프로브 출력 19줄. 컨텍스트에 있는 JsonMapper 빈과 그 읽기 제약, 핸들러 어댑터가 든 세 Jackson 컨버터와 각각의 읽기 제약, 엄격 프로파일이 세우는 제약, 그리고 이 저장소 자동설정만 뺀 대조군의 매퍼와 컨버터가 차례로 보인다." caption="핸들러 어댑터의 컨버터가 든 매퍼 — 19줄 · exit 0" zoom="true" -::: - -컨텍스트에 `JsonMapper` 빈은 하나이고, 핸들러 어댑터의 `JacksonJsonHttpMessageConverter` 가 든 매퍼의 제약은 깊이 64 · 문자열 길이 1,048,576 이다. 유계 팩토리가 세우는 값과 같다. - -마지막 묶음이 대조군이다. 이 저장소 자동설정만 빼고 같은 컨텍스트를 올리면 `jacksonJsonMapper` 가 오고, 컨버터가 드는 제약도 Jackson 기본값인 500 · 100,000,000 이 된다. 그 차이를 만드는 것이 이 자동설정이다. - -이 지문은 엄격 프로파일에만 붙는 것은 아니다. 관대 프로파일도 같은 두 값을 갖는다. 여기서 확인되는 것은 컨버터가 유계 팩토리로 만든 매퍼를 든다는 것이고, 그 프로파일이 엄격인 이유는 `WebMvcPlatformSettings` 의 `strictJson` 기본값이 참이기 때문이다. - -## 파서가 주는 축은 여섯이고 이 코드가 고르는 것은 둘이다 - -```java -// BoundedJsonFactory.java:33-39 - JsonFactoryBuilder builder = - JsonFactory.builder() - .streamReadConstraints( - StreamReadConstraints.builder() - .maxNestingDepth(profile.maxDepth()) - .maxStringLength(profile.maxStringBytes()) - .build()); -``` - -잠금 파일이 고정한 `jackson-core-3.1.5` 의 읽기 제약 빌더는 여섯을 받는다. 위의 둘 말고 `maxNameLength`, `maxNumberLength`, `maxDocumentLength`, `maxTokenCount` 가 더 있다. 앞의 둘은 라이브러리 기본값이 남으므로 실제로 비어 있는 축은 뒤의 둘이다. - -그중 `maxDocumentLength` 는 이 저장소가 이미 여섯 자리에서 쓴다. 그중 main 자동설정이 만드는 것은 `JacksonMessageCodec:213` 하나로, `MessagingCoreAutoConfiguration:364` 가 그 팩토리를 부른다. 나머지 다섯은 이 검색에서 시험에서만 생성된다. - -같은 어댑터에도 두 자리가 있다. `WebCborMapperFactory:55` 와 `WebXmlMapperFactory:74` 인데, 둘이 받는 것은 `CodecBudget` 이다. 요청 예산과 이름만 같은 별개 레코드이고, 이 팩토리들을 부르는 자리는 `WebCodecMapperTest` 뿐이며, 두 형식의 백엔드 잠금 항목에는 `runtimeClasspath` 가 없다. 웹소켓 어댑터의 `StrictWebSocketJsonCodec:64` 와 `WebSocketCborCodec:76` 도 main 클래스이지만 생성하는 자리는 시험뿐이다. 메시징 어댑터의 `LocalJsonSchemaRegistry:623` 도 마찬가지로 `new` 하는 자리가 시험뿐이고, 그것을 받는 `JsonSchemaIntegrationEventEncoder` 도 빈으로 등록되지 않는다. 선례로 삼기에는 다섯 다 배포에서 돌지 않는다. - -저장소 어디에서도 쓰이지 않는 축은 `maxTokenCount` 하나다. - -## 넓은 배열을 넣어 보면 - -:::evidence key="a14-f011-maxarrayelements-parser" alt="JVM 프로브 출력 22줄. 엄격 프로파일이 선언한 세 값, 유계 팩토리가 세운 파서의 실효 읽기 제약 여섯 항목과 그중 비어 있는 둘, 원소 이백만 개짜리 배열의 본문 바이트와 읽어 낸 토큰 수와 결과, 같은 본문을 토큰 수 상한만 건 파서에 넣었을 때의 거부 메시지가 보인다." caption="유계 팩토리가 넓은 배열을 어디까지 읽는가 — 22줄 · exit 0" zoom="true" -::: - -원소 200 만 개짜리 평평한 배열을 그 팩토리에 넣었다. 본문은 4,000,001 바이트이고, 파서는 토큰 2,000,002 개를 끝까지 읽는다. 선언된 상한 100,000 의 스무 배다. - -같은 본문을 토큰 수 상한만 1,000,000 으로 건 팩토리에 넣으면 상한을 넘었다는 메시지와 함께 거부된다. - -## 자바독이 먼저 적어 둔 원칙 - -``` - * A depth limit applied to a parsed tree has already paid for - * the tree; a nesting bomb is cheap to send and expensive to hold, so the only limit that helps is - * one the streaming parser refuses to exceed. -``` - -깊이와 문자열 길이는 이 원칙대로 걸었다. 원소 수는 형제 팩토리가 이미 쓰는 축을 놔둔 채 값만 선언했다. - -## 옮길 자리 - -문서 길이 상한을 걸려면 유계 팩토리가 예산을 받아야 한다. 지금 서명은 프로파일만 받고(`BoundedJsonFactory:31`), 그것을 부르는 것은 `WebObjectMapperFactory:59` 하나이며, 그 클래스를 부르는 자동설정 메서드가 `WebMvcPlatformAutoConfiguration:135` 와 `WebFluxPlatformAutoConfiguration:129` 둘이다. 서명 둘과 호출부 둘을 함께 넓혀야 한다. 다만 같은 클래스의 `standard()`·`standardJsonMapper()`·`create()` 도 같은 `jsonMapper` 로 위임하므로, 서명을 넓히면 그 셋과 그 호출부도 함께 손대야 한다. - -어느 예산을 쓸지는 오늘 문제가 아니다. 두 자동설정이 등록하는 것은 `standard` 하나뿐이고 `WebRequestBudget` 의 공개 팩토리도 하나다. 다만 목록이 프로파일 이름으로 키를 잡고 매퍼는 컨텍스트당 하나이므로, 배포가 프로파일을 더 등록하면 그때 정해야 한다. - -토큰 수 상한도 쓸 수 있지만 배열 원소 수와 일대일은 아니다. 평평한 스칼라 배열에서만 원소 하나가 토큰 하나이고, `List` 처럼 객체가 들어가면 원소당 토큰 수가 그 모양에 따라 달라진다. 같은 상한이 문서 안의 다른 토큰도 함께 세므로 선언된 100,000 을 그대로 옮길 수 없다. - -파서 쪽을 손대지 않는다면 컬렉션 필드에 크기 제약을 요구하는 쪽이 남는다. 표본 애플리케이션이 이미 그렇게 한다. - -```java -// WorkLogController.java:206-208 - public record BatchCreateRequest( - @Valid - @Size(max = MAX_BATCH_SIZE, message = "batch may not exceed " + MAX_BATCH_SIZE + " items") -``` - -## 확인하지 못한 것 - -실제 HTTP 요청으로 힙 증가를 측정하지 않았다. 프로브가 확인한 것은 파서가 그 배열을 끝까지 읽는다는 것까지다. - -프로브가 넣은 배열은 원소 200 만 개다. 원문이 든 1 억 개를 넣어 본 것은 아니다. - -컨버터를 확인한 컨텍스트는 자동설정 넷만 올린 것이고 실제 애플리케이션 부팅이 아니다. 그 컨텍스트에는 JSON 매퍼 빈이 하나뿐이었다. 같은 컨텍스트에 CBOR·XML 컨버터가 함께 있는 것은 프로브가 시험 클래스패스로 돌기 때문이고, 그 두 형식의 jar 는 배포 클래스패스에 없다. - -문서 길이나 토큰 수 상한을 이 저장소의 유계 팩토리에 실제로 걸어 회귀가 없는지 확인하지는 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-a14-f014-advanced.md b/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-a14-f014-advanced.md deleted file mode 100644 index eceadbb..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-a14-f014-advanced.md +++ /dev/null @@ -1,212 +0,0 @@ ---- -kind: CASE -slug: a14-f014-advanced -title: 선언된 Advanced 능력 열둘 중 속성 이름을 읽는 코드가 있는 것은 둘이다 -topic: web-inbound-and-http-surface -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -evidenceCapturedOn: 2026-09-03 -rootTreeNode: case:a14-f014-advanced -body: case-a14-f014-advanced.body.md -assets: - - key: a14-f014-advanced - file: ../../../final/evidence/rendered/a14-f014-advanced.svg - - key: a14-f014-advanced-switches - file: ../../../final/evidence/rendered/a14-f014-advanced-switches.svg -evidence: - - ../../../final/evidence/raw/a14-f014-advanced.txt - - ../../../final/evidence/raw/a14-f014-advanced-switches.txt -source: - - 원본 분석 절은 final/document.md#a14#L1267 이다. ---- - -# 선언된 Advanced 능력 열둘 중 속성 이름을 읽는 코드가 있는 것은 둘이다 - -고급 능력 열거형이 상수를 열둘 선언하고 각 상수가 `backend.web.advanced.<이름>.enabled` 를 계산해 준다. 그 이름을 게이트나 바인딩으로 읽는 것은 두 접두사뿐이다. 나머지 열은 속성을 참으로 설정해도 그 값을 보는 코드가 없고, 그 열을 속성으로 구동하는 시험도 없다. - -## 관계 - -- **그러나 R0 경계가 문서에만 있고 compile 경로에서 닫히지 않는다** - 두 사례 모두 선언된 능력에 소비자가 없다. a09-f004 는 만들어진 제공자를 꺼내 쓰는 코드가 없고, 여기는 발행된 속성 이름을 읽는 코드가 없다. -- **플랫폼 요청 컨텍스트가 서블릿에는 생산자가 없고, 리액티브에는 익명 액터로 고정되어 있다** - 같은 리프의 P1 이고, 둘 다 시험이 손으로 만든 값 위에서 통과해 배포 경로의 공백이 드러나지 않았다. -- **그 속성을 보일 수 없는 대역 위에서 통과한 테스트는 증인이 아니다** - 레인이 나머지 열 능력을 돌리기는 하지만 그 시험들은 대상 클래스를 곧바로 만들어 부른다. 레인이 녹색이어도 배포가 그 능력을 켤 수 있다는 증거는 되지 않는다. -- **선언 순서를 단언하는 테스트는 그 순서를 읽는 코드가 있을 때만 게이트다** - 릴리스 시험이 단언하는 속성 이름을 읽는 코드가 없다. - -## 문제 - -야간 레인과 릴리스 레인이 이 능력들을 돌린다. 배포가 그 능력들을 실제로 켤 수 있는지, 그리고 레인이 무엇을 확인하는지 봤다. - -## 결론 - -열거형 자바독에 규칙이 적혀 있다. 능력 하나에 플래그 하나이고, advanced 라는 이름의 스위치 하나로 묶지 않는다. 하나로 묶으면 가상 스레드를 원한 배포가 XML 파서까지 받게 되기 때문이다. - -세어 보면 상수가 열둘인데 원문은 열한 개로 적었다. 빠진 것은 OPENAPI_32 다. - -각 상수가 propertyName() 으로 이름을 계산한다. 열거형을 실제로 불러 상수마다 그 이름을 만들고 main 소스 4,626 개를 훑으니, 걸리는 상수는 둘이고 나머지 열은 하나도 걸리지 않는다. - -그 접두사가 main 에 나오는 여섯 줄은 성격이 다르다. 게이트나 바인딩이 셋, 이름을 만들기만 하는 것이 둘, 자바독이 하나다. 이름을 만들기만 하는 둘 중 하나는 열거형 자신이고, 다른 하나인 가상 스레드 프로파일은 virtual-threads 를 돌려주는데 열거형이 만드는 이름도 실제 게이트도 mvc-virtual-threads 다. 원문은 이 이름 불일치를 다른 절에서 P3 으로 따로 적었다. - -읽히는 둘도 결과가 같지 않다. - -ndjson 을 켜면 기록기 빈이 둘 생기고, JSON_SEQUENCE 도 자기 이름 없이 이때 함께 켜진다. 다만 그 기록기가 내는 두 미디어 타입을 선언한 라우트가 main 에 하나도 없고, 기록기를 부르는 코드도 자기 패키지 밖에는 없다. - -mvc-virtual-threads 를 켜면 Boot 의 MVC 비동기 지원이 찾는 이름으로 실행기가 등록되는데, 출하 조립 경로에는 그 이름을 쓰는 빈이 이미 하나 더 있다. 이 스위치를 켜고 출하 조립을 띄운 시험이 없어서, 요청이 실제로 가상 스레드에서 처리되는지는 확인하지 못했다. 본문에 두 선언과 그 결과가 갈리는 조건을 적었다. - -그래서 세는 기준을 밝혀야 한다. 게이트가 자기 이름을 읽는 상수는 둘, 스위치를 켜면 빈이 생기는 상수는 셋이다. 요청 동작까지 달라지는 상수는 이 검증 범위에서 확인하지 못했다. - -원문이 적은 아홉은 첫째 기준을 열한 개 분모에 적용한 수다. 분모를 열둘로 고치면 같은 기준에서 열이 된다. 둘째 기준으로 세도 아홉이 나오기는 하는데 그 아홉은 다른 집합이다. 원문이 이름으로 적은 아홉에는 JSON_SEQUENCE 가 들어 있고 OPENAPI_32 가 빠져 있으며, 둘째 기준의 아홉은 그 반대다. - -레인이 무엇을 확인하는지도 봤다. - -webAdvancedTest 는 시험 클래스를 이름으로 적지 않고 web-advanced 태그로 고른다. 그 태그가 붙은 클래스가 스물둘이고, 원문이 지목한 롤백 시험은 그중 하나다. 그 안의 일곱 중 속성을 참으로 두는 것은 없다. 속성 상태를 다루는 둘 가운데 하나는 값을 아예 두지 않은 경우를, 하나는 두 실제 이름을 거짓으로 둔 경우를 확인한다. 나머지 다섯은 픽스처와 승격 게이트 기본값을 쓴다. - -같은 레인의 다른 시험은 두 실제 속성을 참으로 두고 만들어지는 빈을 확인한다. 나머지 열 능력에도 레인이 도는 단위 시험이 있다. 원문이 레인은 이 능력들을 전부 돌린다고 적은 것은 그대로 맞다. 다만 그 시험들은 대상 클래스를 곧바로 만들거나 빌드 정의를 읽어 단언하지, 속성을 두고 컨텍스트를 띄우지 않는다. 저장소 전체 시험에서 backend.web.advanced 로 시작하는 속성을 두는 자리는 mvc-virtual-threads 와 ndjson 뿐이다. - -원문이 롤백 시험의 대상을 테스트 전용 값이라고 적은 대목도 다르다. 그 값을 쓰는 시험은 일곱 중 하나뿐이고, 그 시험은 빈 집합에 대해 참을 단언한 뒤 상수마다 한 원소 집합으로 같은 메서드를 부른다. 뒤쪽 비교는 그 메서드가 부르는 술어를 상대로 하므로 구현을 자기 자신과 맞춘다. - -같은 리프의 §40.1 도 배포가 켤 수 없는 코드를 P1 로 적었다. 원문이 P1 근거를 따로 문장으로 적지는 않았지만, 그 절이 든 29 파일 안에는 이 리프의 유일한 프로덕션 요청 컨텍스트 생산자가 들어 있다고 원문이 굵게 표시해 두었다. 그러면 Stable 경로가 함께 빈다. 여기서는 열 능력이 모두 부가 기능이고 Stable 경로가 그중 무엇에도 기대지 않는다. 그래서 P2 다. - -권고는 원문과 같다. 열거형이 이미 속성 이름을 계산하므로, 능력마다 그 이름으로 게이트된 설정을 두거나 아직 배선할 수 없는 상수를 열거형에서 빼면 된다. 릴리스 시험이 JSON_MERGE_PATCH.propertyName() 을 단언하고 있으니, 그 이름이 의미를 갖는다는 전제는 시험에 이미 들어가 있다. - -## 검증 환경 - -OpenJDK : 21.0.12 -확인 방식 : 열거형 상수를 실행으로 세고 각 속성 이름을 main 자바·yml·yaml·properties 4,626 개에서 검색, 레인의 선택 방식과 켜는 쪽·끄는 쪽 시험 본문 확인, 두 스위치가 만드는 빈과 그 이름의 경쟁 확인 -소스 수정 : x - -## 재현 조건 - -원문 근거는 분석 문서의 #L1267 절이다. - -1. 열거형의 자바독과 상수를 읽고 수를 센다. -2. 열거형을 실제로 불러 상수마다 속성 이름을 만들고, main 소스에서 그 이름을 쓰는 파일을 찾는다. -3. backend.web.advanced 가 main 에 나오는 줄을 전부 뽑아 게이트·바인딩과 이름 생성과 자바독으로 가른다. -4. 두 스위치가 각각 어떤 빈을 만드는지 읽는다. -5. ndjson 이 만든 기록기가 내는 미디어 타입을 내는 라우트와, 그 기록기를 부르는 코드를 센다. -6. 가상 스레드 스위치가 등록하는 빈 이름을 app-bootstrap 에서 다시 찾고, 그 패키지가 컴포넌트 스캔 제외에 있는지 본다. -7. 레인 태스크가 무엇을 고르는지와 그 태그를 단 시험 클래스 수를 읽는다. -8. 롤백 시험 일곱의 본문과, 같은 레인에서 켜는 쪽을 단언하는 시험을 읽는다. -9. 플래그 값을 쓰는 시험의 두 단언과 그 메서드의 구현을 대조한다. - -## 본문 - - - -열거형이 자기 규칙을 자바독에 적어 두었다. - -```java -// WebAdvancedFeature.java:8-12 - *

One flag per capability, not one for "advanced". They have nothing in common operationally: - * virtual threads change how every request is scheduled, streaming changes how long a response - * holds a connection, XML adds a parser with a decades-long history of entity-expansion attacks. A - * single switch would make those one decision, and a deployment that wanted the first would be - * given the third. -``` - -능력 하나에 플래그 하나이고, 그 이름은 상수가 직접 계산한다. - -## 선언된 상수와 그 이름이 나오는 곳 - -:::evidence key="a14-f014-advanced" alt="코드베이스 정적 검색 출력 172줄. 열거형 자바독의 규칙과 선언된 상수 수와 목록, propertyName 이 이름을 만드는 방식, main 에 그 접두사가 나오는 여섯 줄과 그중 게이트·바인딩 셋과 이름을 만들기만 하는 둘, 가상 스레드 설정이 등록하는 실행기 이름과 app-bootstrap 이 같은 이름을 선언하는 자리와 스캔 제외 여부, ndjson 설정이 만드는 두 기록기와 그 미디어 타입을 내는 라우트 수, 레인이 태그로 고른다는 빌드 정의와 그 태그를 단 클래스 수, 켜는 쪽을 단언하는 시험이 쓰는 속성과 확인하는 빈, 롤백 시험 일곱의 이름과 속성 상태를 다루는 둘과 플래그 시험 전문, 롤백 값 타입을 부르는 자리 다섯, 레인 둘과 속성 이름을 단언하는 시험이 차례로 보인다." caption="선언된 능력과 그 이름을 다루는 자리 — 172줄 · exit 0" zoom="true" -::: - -상수는 열두 개다. 원문은 열한 개로 적었다. - -`backend.web.advanced` 가 main 에 나오는 줄은 여섯이다. 게이트나 바인딩이 셋인데 `mvc-virtual-threads` 를 `VirtualThreadSettings:17` 이 바인딩하고 `VirtualThreadMvcConfiguration:33` 이 게이트하며, `ndjson` 은 `MvcStreamingExecutorConfiguration:37` 이 게이트한다. 둘은 이름을 만들기만 하고 하나는 자바독이다. - -## 상수마다 이름을 만들어 찾아보면 - -:::evidence key="a14-f014-advanced-switches" alt="JVM 프로브 출력 19줄. 열거형을 불러 만든 상수 열둘과 각각이 계산한 속성 이름, 그 이름을 쓰는 main 파일 이름, 그리고 그런 파일이 없는 상수의 수가 표로 보인다." caption="상수별 속성 이름과 그 이름이 나오는 main 파일 — 19줄 · exit 0" zoom="true" -::: - -열거형을 불러 상수마다 이름을 만들고 main 소스 4,626 개에서 찾았다. 걸리는 것은 `MVC_VIRTUAL_THREADS` 와 `NDJSON` 둘이다. - -## ndjson 은 기록기를 만들지만 소비자가 없다 - -```java -// MvcStreamingExecutorConfiguration.java:72,84 - return new MvcStreamWriter(encoder, StreamFraming.NDJSON); - ... - return new MvcStreamWriter(encoder, StreamFraming.JSON_SEQUENCE); -``` - -기록기 빈이 둘 생긴다. `JSON_SEQUENCE` 가 자기 이름 없이 함께 켜지는 것이 이 자리다. 다만 `application/x-ndjson` 이나 `application/json-seq` 를 내는 라우트가 main 전체에 0 이고, `MvcStreamWriter` 를 부르는 main 코드도 `advanced` 패키지 자신 말고는 없다. - -## 가상 스레드 실행기는 이름을 두고 겹친다 - -```java -// VirtualThreadMvcConfiguration.java:60-66 - *

Named {@code applicationTaskExecutor} because that is the bean Spring Boot's MVC async - * support looks for. A differently named bean is created, is never used, and leaves the container - * default in place — which is the failure mode where the whole capability is switched on and - * nothing changes. - */ - @Bean("applicationTaskExecutor") - @ConditionalOnMissingBean(name = "applicationTaskExecutor") -``` - -Boot 의 MVC 비동기 지원이 찾는 이름이라 이것을 잡으면 요청 처리가 달라진다. 그런데 같은 이름을 선언하는 빈이 출하 조립 경로에 하나 더 있다. - -```java -// AsyncExecutorConfig.java:19,25,40-42 -@Configuration - public static final String EXECUTOR_BEAN_NAME = "applicationTaskExecutor"; - @Bean(name = EXECUTOR_BEAN_NAME) - @Primary - ThreadPoolTaskExecutor applicationTaskExecutor( -``` - -조건이 붙어 있지 않고, `bootstrap.async` 는 컴포넌트 스캔 제외 정규식에 없다. 가상 스레드 선언에는 그 이름이 없을 때만 만들라는 조건이 붙어 있어서, 결과는 두 설정이 등록되는 순서가 정한다. - -자바독이 든 실패는 빈 이름을 다르게 지었을 때다. 여기 겹침은 그 자바독이 시킨 이름을 그대로 따라서 생긴 것이라 자바독이 예상한 형태가 아니다. 어느 순서가 적용되는지는 확인하지 못했고, 두 순서의 결과도 같지 않다. 비동기 설정이 먼저면 가상 스레드 쪽이 만들어지지 않고, 반대면 같은 이름을 두 번 선언하는 것이 되는데 이 저장소는 빈 정의 덮어쓰기를 켜 두지 않았다. - -## 레인이 고르는 범위와 그 안의 단언 - -```groovy -// build.gradle:210-212 - useJUnitPlatform { - includeTags 'web-advanced' - } -``` - -`web-advanced` 태그를 단 시험 클래스가 스물둘이다. 롤백 시험은 그 하나다. - -일곱 중 `withPropertyValues` 를 부르는 것은 `:67` 하나이고, `mvc-virtual-threads` 와 `ndjson` 을 `false` 로 적는다. `:49` 는 값을 아예 두지 않은 채로 두 설정을 등록한다. `:103` `:119` `:134` 는 픽스처를 손으로 만들고, `:146` 은 승격 게이트의 기본값을 읽는다. 남은 `:86` 은 아래의 플래그 시험이다. - -같은 레인의 `MvcAdvancedConfigurationTest` 는 켜는 쪽을 단언한다. `backend.web.advanced.ndjson.enabled=true` 로 두고 기록기 빈 둘을 확인하고, 가상 스레드 쪽도 참으로 두고 실행기 빈을 확인한다. 다만 그 컨텍스트에는 가상 스레드 설정만 등록되어 있어서 위의 이름 경쟁은 재현되지 않는다. - -## 플래그 값을 쓰는 시험 - -```java -// WebAdvancedRollbackIT.java:87-97 (줄바꿈과 .as(..) 를 줄인 발췌) · WebAdvancedFeatureFlags.java:44-46 - assertThat(WebAdvancedFeatureFlags.none().stableBehaviourPreserved()).isTrue(); - for (WebAdvancedFeature feature : WebAdvancedFeature.values()) { - boolean preserved = WebAdvancedFeatureFlags.of(feature).stableBehaviourPreserved(); - assertThat(preserved).isEqualTo(!feature.affectsUnrelatedRequests()); - ... - public boolean stableBehaviourPreserved() { - return enabled.stream().noneMatch(WebAdvancedFeature::affectsUnrelatedRequests); - } -``` - -앞의 단언은 빈 집합에 대해 참을 확인하므로 자기 비교가 아니다. 뒤의 반복은 한 원소 집합에 `noneMatch(affectsUnrelatedRequests)` 를 돌린 결과를 `!feature.affectsUnrelatedRequests()` 와 맞추므로, 구현을 그 구현이 부르는 술어와 비교한다. - -`WebAdvancedFeatureFlags` 는 main 타입이지만 부르는 자리 다섯이 모두 시험이다. 레인 둘이 이 시험들을 돌린다. `web-advanced-nightly.yml:46` 과 `web-advanced-release.yml:53` 이 `webAdvancedTest` 를 부른다. - -`WebAdvancedReleaseTest:48` 은 `JSON_MERGE_PATCH.propertyName()` 을 단언한다. - -## 확인하지 못한 것 - -속성을 참으로 설정하고 애플리케이션을 띄우지 않았다. 이름을 읽는 조건이 없다는 것까지 확인했다. 오류나 경고가 나지 않는다는 것도 코드에서 읽은 것이다. - -가상 스레드 스위치를 켠 채로 출하 조립을 띄워 두 설정 중 어느 쪽 실행기가 남는지 확인하지 못했다. 켜는 쪽을 단언하는 시험은 가상 스레드 설정만 등록한 컨텍스트를 쓰므로 이 경쟁을 재현하지 않는다. - -프로브가 찾은 것은 속성 이름 문자열이 main 소스에 나오는지다. 문자열을 조립해 읽는 코드가 있다면 걸리지 않는다. 반대로 자바독에 이름이 적혀 있기만 해도 걸리는데, 이 코드베이스에서 주석만으로 걸린 파일은 없었다. 가상 스레드 쪽 두 파일은 각각 바인딩과 게이트로 걸린다. - -프로브가 훑은 것은 main 의 자바·yml·yaml·properties 4,626 개다. 같은 자리의 gradle·imports·sql 등 88 개는 보지 않았다. 확장자를 걸지 않고 다시 훑어도 게이트·바인딩이 있는 파일은 같은 셋뿐이었다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-a14-f015-virtualthreadprofile-propertyname.md b/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-a14-f015-virtualthreadprofile-propertyname.md deleted file mode 100644 index 6bcb6f0..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-a14-f015-virtualthreadprofile-propertyname.md +++ /dev/null @@ -1,181 +0,0 @@ ---- -kind: CASE -slug: a14-f015-virtualthreadprofile-propertyname -title: 켜는 속성이라고 적힌 메서드가 어느 게이트도 쓰지 않는 이름을 돌려준다 -topic: web-inbound-and-http-surface -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -evidenceCapturedOn: 2026-09-03 -rootTreeNode: case:a14-f015-virtualthreadprofile-propertyname -body: case-a14-f015-virtualthreadprofile-propertyname.body.md -assets: - - key: a14-f015-virtualthreadprofile-propertyname - file: ../../../final/evidence/rendered/a14-f015-virtualthreadprofile-propertyname.svg - - key: a14-f015-virtualthreadprofile-propertyname-bind - file: ../../../final/evidence/rendered/a14-f015-virtualthreadprofile-propertyname-bind.svg -evidence: - - ../../../final/evidence/raw/a14-f015-virtualthreadprofile-propertyname.txt - - ../../../final/evidence/raw/a14-f015-virtualthreadprofile-propertyname-bind.txt -source: - - 원본 분석 절은 final/document.md#a14#L1279 이고, 세 철자를 비교한 표는 §35.3 이다. ---- - -# 켜는 속성이라고 적힌 메서드가 어느 게이트도 쓰지 않는 이름을 돌려준다 - -가상 스레드 프로파일에 `propertyName()` 이 있고 자바독이 이것을 켜는 속성이라고 적었다. 그 메서드는 문자열을 그대로 적어 돌려주는데 `mvc-` 접두사가 빠져 있어서, 실제 바인딩과 게이트가 쓰는 이름과 다르다. 그 이름 자체는 저장소에 이 반환문에만 있다. 2026-08-13 계획 문서가 적은 게이트 접두사에 이름 `enabled` 를 합치면 나오는 키가 바로 그 이름이다. - -## 관계 - -- **선언된 Advanced 능력 열둘 중 속성 이름을 읽는 코드가 있는 것은 둘이다** - 거기서 이 메서드는 속성 이름을 만들기만 하는 둘 중 하나로 세었다. 그 이름이 왜 게이트와 어긋나는지는 여기서 다룬다. -- **호출자 없이 들어온 메서드이고, 도달 불가라던 가지는 켜지면 반대로 답한다** - 둘 다 부르는 곳이 없어서 지금은 아무 일도 일어나지 않는다. 다만 a14-f002 는 검사 메서드 전체가 그렇고, 여기는 배선된 타입에 붙은 정적 메서드 하나가 그렇다. -- **선언 순서를 단언하는 테스트는 그 순서를 읽는 코드가 있을 때만 게이트다** - 형제 두 타입에는 이름을 단언하는 시험이 있다. 기대값은 손으로 적은 문자열이거나 상수 개수다. 그 이름을 읽는 코드가 있는지는 어느 쪽도 보지 않는다. - -## 문제 - -가상 스레드 스위치의 이름이 코드 세 군데에 다른 철자로 나온다고 원문이 적었다. 그 셋을 확인하고, 같은 이름의 메서드를 가진 다른 타입까지 함께 봤다. - -## 결론 - -같지 않다. VirtualThreadProfile:74-77 만 mvc- 가 없는 이름을 돌려준다. - -propertyName() 이라는 메서드를 선언하는 타입이 이 저장소에 셋 있다. 웹 능력 열거형과 웹소켓 능력 열거형은 상수 이름에서 문자열을 만들고, 가상 스레드 프로파일만 문자열을 적어 두었다. 적어 둔 그 하나만 static 이고, 틀린 것도 그 하나다. - -발행된 이름 그대로는 저장소에 한 번, 자기 반환문에만 나온다. 접두사까지만 잘라 찾으면 한 줄이 더 나온다. 2026-08-13 확장 계획 문서가 아직 만들지 않은 설정 클래스를 virtual-threads 로 게이트하라고 적은 자리다. 계획대로 만들었다면 그 게이트가 읽었을 속성이 이 메서드가 돌려주는 이름이다. 구현된 게이트는 mvc-virtual-threads 를 쓴다. 원문은 계획 문서를 언급하지 않는다. - -코드에서 그 이름을 읽거나 쓰는 자리는 없다. 운영자용 문서 두 편은 맞는 이름을 쓴다. docs/web/advanced-capabilities.md:8 의 지원 표와 docs/web/virtual-thread-profile.md:25 의 yml 예시다. - -두 이름을 각각 참으로 두고 웹 컨텍스트를 띄웠다. 게이트된 설정 클래스만 올린 구성과, 루트가 이 리프에 거는 패키지를 그대로 스캔한 구성 둘이다. 두 구성의 표는 본문에 있다. - -출하 쪽 구성에서는 설정 빈이 늘 등록되고, 속성 이름이 좌우하는 것은 게이트다. 발행된 이름은 두어도 결과가 같다. 다만 그 구성은 SecuritySettings 가 요구하는 ca-skeleton.security.issuer-uri 를 채워야 기동한다. - -이 설정 클래스가 발행하는 설정 메타데이터에도 그 이름은 없다. 고정 소스에 설정 프로세서를 돌려 뽑으면 접두사 backend.web.advanced.mvc-virtual-threads 아래 이름 여섯이 나온다. 리프가 프로세서를 선언하도록 루트 빌드가 리프마다 check 에 걸어 두었으므로, 이 메타데이터는 배포판에 늘 들어간다. - -한편 이 타입 자체는 배선되어 있다. VirtualThreadSettings:77 이 레코드를 만들고 VirtualThreadMvcConfiguration:40 이 빈으로 등록하며 VirtualThreadSettings:91 이 기동 검증에서 생성자를 다시 부른다. 부르는 곳이 없는 것은 그 정적 메서드 하나뿐이다. - -원문은 이 건에 권고를 적지 않았다. 같은 절의 권고는 앞 항목인 P2 의 것이다. 이 메서드는 지우거나 실제 게이트 이름을 돌려주게 고치면 된다. 열거형이 같은 능력에 대해 맞는 이름을 계산하므로 그것을 부르면 된다. - -판정은 P3 이고 원문과 같다. 부르는 곳이 없어 실행 결과가 달라지지 않는다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Spring Boot : 4.0.8 -확인 방식 : 세 propertyName() 구현과 호출자·메서드 참조 대조, git 추적 파일 전체에서 발행된 이름 검색, 두 이름을 각각 참으로 둔 웹 컨텍스트 기동 두 구성, 이 설정 클래스의 메타데이터 재생성 -소스 수정 : x - -## 재현 조건 - -원문 근거는 분석 문서의 §35.3 표와 #L1279 절이다. - -1. propertyName() 을 선언하는 세 자리를 찾아 구현을 나란히 읽는다. -2. 셋 각각을 부르는 자리를 센다. 메서드 참조도 함께 센다. -3. git 추적 파일 전체에서 발행된 이름을 찾고, 접두사까지만 잘라서도 찾는다. 운영자용 문서가 쓰는 이름도 함께 본다. -4. 실제 바인딩 접두사와 게이트 조건을 읽는다. -5. 출하 조립 루트가 이 리프의 설정을 어디에서 등록하는지 확인한다. -6. 게이트된 설정만 올린 구성과, 루트와 같은 스캔을 이 하위 패키지에 건 구성에서 두 이름을 각각 참으로 두고 컨텍스트를 띄운다. -7. 이 설정 클래스 한 파일에만 설정 프로세서를 걸어 어떤 이름이 실리는지 본다. -8. 그 타입을 만들고 등록하는 자리를 찾아 타입 자체가 배선되어 있는지 확인한다. - -## 본문 - - - -자바독이 용도를 적고, 바로 아래에서 문자열을 그대로 돌려준다. - -```java -// VirtualThreadProfile.java:74-77 - /** The property that turns this on. */ - public static String propertyName() { - return "backend.web.advanced.virtual-threads.enabled"; - } -``` - -## 같은 메서드를 가진 형제 둘은 이름을 계산한다 - -:::evidence key="a14-f015-virtualthreadprofile-propertyname" alt="저장소 루트에서 돌린 정적 검색 출력 78줄. 선언 자리를 검색으로 센 결과 셋과 그 구현이 차례로 보이고, 앞의 둘은 상수 이름에서 문자열을 조립하며 셋째는 문자열을 그대로 적는다. 이어서 셋 각각을 부르는 자리가 메서드 참조까지 포함해 나오고, 발행된 이름 그대로 나오는 한 줄과 접두사까지만 잘라 찾았을 때 나오는 두 줄과 그 계획 문서가 접두사 다음 줄에 적은 name 항목, 운영자용 문서 두 편이 쓰는 이름, 실제 바인딩 접두사와 게이트 조건, 출하 조립 루트의 속성 스캔 애너테이션과 이 리프가 그 목록에 든 줄과 그 목록의 패키지 수, 그 타입을 만들고 빈으로 등록하는 자리, 루트가 거는 그 패키지 아래의 @ConfigurationProperties 타입 수와 그중 값을 요구하는 하나, 이 리프의 설정 프로세서 선언과 그것을 리프마다 check 에 거는 루트 빌드 블록이 보인다." caption="세 propertyName 과 그 이름이 나오는 자리 — 78줄 · exit 0" zoom="true" -::: - -앞의 둘은 `name()` 을 소문자로 바꾸고 밑줄을 하이픈으로 바꿔 접두사에 붙인다. 상수를 새로 넣어도 이름이 상수와 어긋나지 않는다. 셋째는 그 조립을 하지 않는다. - -호출은 앞의 둘에만 있다. `WebAdvancedReleaseTest` 의 `:48`·`:52` 와 `ResumeTokenTest` 의 `:187`·`:191` 이다. 각 쌍의 앞은 직접 호출이고 뒤는 메서드 참조다. 앞은 문자열을 그대로 적어 비교하고, 뒤는 그 열거형이 선언한 이름이 서로 겹치지 않는지만 센다. 웹 쪽은 열둘, 웹소켓 쪽은 열넷이다. - -## 그 접두사는 코드 밖에 한 번 더 있다 - -이름 그대로 찾으면 자기 반환문 한 줄이다. 접두사까지만 잘라 찾으면 2026-08-13 확장 계획 문서가 한 줄 더 나온다. - -```text -docs/web-superpowers-package/.../2026-08-13-web-advanced-capabilities-expansion-plan.md:195-202 - -Expected: FAIL because no virtual-thread MVC profile or admission guard exists. - -- [ ] **Step 3: Implement the minimum production contract** - -(199 의 java 펜스 한 줄 생략) -@Configuration -@ConditionalOnProperty( - prefix = "backend.web.advanced.virtual-threads", -``` - -계획은 이 설정 클래스가 아직 없다고 적은 뒤 이 접두사로 게이트하라고 지시한다. 다음 줄이 `name = "enabled"` 이므로 그 게이트가 읽는 속성은 이 메서드가 돌려주는 이름과 같다. 구현된 게이트는 다른 접두사를 쓴다. - -```java -// VirtualThreadSettings.java:17 · VirtualThreadMvcConfiguration.java:32-35 -@ConfigurationProperties(prefix = "backend.web.advanced.mvc-virtual-threads") -... -@ConditionalOnProperty( - prefix = "backend.web.advanced.mvc-virtual-threads", - name = "enabled", - havingValue = "true") -``` - -## 두 이름을 각각 두고 띄워 보면 - -:::evidence key="a14-f015-virtualthreadprofile-propertyname-bind" alt="JVM 프로브 출력 29줄. 메서드가 반환하는 이름과 실제 게이트 이름이 나란히 보인 뒤, 두 가지 구성의 결과가 표로 이어진다. 게이트된 설정만 올린 구성에서는 발행된 이름 쪽에 프로파일 빈도 설정 빈도 없고, 실제 이름 쪽에는 둘 다 있다. 루트가 이 리프에 거는 패키지를 그대로 스캔한 구성에서는 아무것도 채우지 않으면 보안 설정이 필수값을 요구해 기동이 멈추고, 그 한 값을 채운 세 행에서는 설정 빈이 모두 있고 enabled 값만 갈리며, 발행된 이름 행이 그 이름만 빼고 같게 둔 행과 같은 값을 낸다. 끝으로 고정 소스에서 새로 뽑은 설정 메타데이터의 이름 여섯과, 발행된 이름이 그 안에 없다는 확인이 보인다." caption="두 이름을 각각 참으로 둔 두 구성과 재생성한 설정 메타데이터 — 29줄 · exit 0" zoom="true" -::: - -두 구성 모두 예산 세 값을 늘 함께 준다. 그 값이 없으면 실제 이름을 참으로 둔 행이 바인딩 검증에서 멈춘다. - -첫 구성은 게이트된 설정 클래스만 올린다. 발행된 이름 쪽은 프로파일 빈도 설정 빈도 없다. 다만 그 설정 클래스가 이 구성에서 설정 빈을 등록하는 유일한 자리이므로, 설정 빈이 없는 것은 게이트가 닫힌 결과다. - -출하 조립은 게이트와 무관하게 이 설정을 등록한다. - -```java -// CaSkeletonApplication.java:58-80 중 세 줄 -@ConfigurationPropertiesScan( - basePackages = { - ... - "dev.caskeleton.adapter.inbound.web", -``` - -두 번째 구성은 그 애너테이션을 그 패키지 그대로 건다. 리프 전체를 걸면 `SecuritySettings` 가 `ca-skeleton.security.issuer-uri` 를 요구해 기동이 거기서 멈추므로, 그 한 값만 채우고 나머지는 두었다. 이 발견과 무관한 값이고, 리프의 여덟 설정 중 값을 요구하는 것은 그것 하나다. 그러면 설정 빈이 세 행 모두 있고 `enabled` 값만 갈린다. 발행된 이름을 참으로 둔 행은 그 이름을 빼고 같게 둔 `없음` 행과 똑같은 값을 낸다. 그 속성을 바인딩하겠다고 선언한 클래스가 없어서 기동을 막지도 않는다. - -같은 프로브가 이 설정 클래스에 설정 프로세서를 돌려 메타데이터를 새로 뽑는다. 나오는 여섯은 모두 접두사 `backend.web.advanced.mvc-virtual-threads` 아래에 있고, 발행된 이름은 없다. - -## 이름을 잘못 발행하는 타입은 살아 있다 - -```java -// VirtualThreadSettings.java:77-78 · VirtualThreadMvcConfiguration.java:39-41 - public VirtualThreadProfile toProfile() { - return new VirtualThreadProfile(enabled, admissionLimit, databasePoolSize, outboundBulkhead); - @Bean - public VirtualThreadProfile virtualThreadProfile(VirtualThreadSettings properties) { - return properties.toProfile(); -``` - -설정이 이 레코드를 만들고 설정 클래스가 빈으로 등록한다. `VirtualThreadSettings:91` 은 기동 검증에서 생성자를 다시 불러 예산이 맞는지 본다. 죽은 것은 타입이 아니라 거기 붙은 정적 메서드 하나다. - -## 확인하지 못한 것 - -발행된 이름을 설정했을 때 기동 로그에 아무 말도 나오지 않는지는 확인하지 못했다. 프로브가 로깅을 끄고 돌았기 때문이다. 확인한 것은 기동이 실패하지 않는다는 것까지다. - -두 번째 구성은 이 리프 하나만 스캔한다. 루트가 거는 스무 개 패키지를 모두 올린 것이 아니므로, 다른 리프의 설정이 이 결과에 영향을 주는지는 보지 않았다. - -메타데이터를 다시 만들 때 이 설정 클래스 한 파일만 프로세서에 넣었다. 여기 나온 여섯은 이 클래스가 발행하는 이름이지 리프 전체 목록이 아니다. - -계획 문서와 구현 중 어느 쪽 커밋이 먼저인지는 이력에서 보지 않았다. 확인한 것은 고정 리비전에 두 파일이 함께 있고 계획 쪽이 게이트 접두사로 이 이름을 적었다는 것까지다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-a14-f017-springmvcrouteinventorycollector.md b/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-a14-f017-springmvcrouteinventorycollector.md deleted file mode 100644 index e7eacd8..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-a14-f017-springmvcrouteinventorycollector.md +++ /dev/null @@ -1,163 +0,0 @@ ---- -kind: CASE -slug: a14-f017-springmvcrouteinventorycollector -title: 라우트 게이트가 실물을 읽어 오는 한 조각만 아무도 만들지 않는다 -topic: web-inbound-and-http-surface -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -evidenceCapturedOn: 2026-09-03 -rootTreeNode: case:a14-f017-springmvcrouteinventorycollector -body: case-a14-f017-springmvcrouteinventorycollector.body.md -assets: - - key: a14-f017-springmvcrouteinventorycollector - file: ../../../final/evidence/rendered/a14-f017-springmvcrouteinventorycollector.svg - - key: a14-f017-springmvcrouteinventorycollector-run - file: ../../../final/evidence/rendered/a14-f017-springmvcrouteinventorycollector-run.svg -evidence: - - ../../../final/evidence/raw/a14-f017-springmvcrouteinventorycollector.txt - - ../../../final/evidence/raw/a14-f017-springmvcrouteinventorycollector-run.txt -source: - - 원본 분석 절은 final/document.md#a14#L1494 이다. ---- - -# 라우트 게이트가 실물을 읽어 오는 한 조각만 아무도 만들지 않는다 - -`admin/route` 패키지가 라우트 목록을 만들고 규칙을 걸고 어긋나면 예외를 던지는 네 조각으로 되어 있다. 그중 셋은 서로를 부르고 시험 하나가 그것들을 돌린다. 실제 디스패처 매핑에서 목록을 읽어 오는 수집기만 만드는 곳이 없다. - -## 관계 - -- **운영 런북이 시작 검증기의 판정을 근거로 삼는데 그 검증기를 부르는 곳이 없다** - 같은 `admin` 패키지의 이웃이고 원문도 §44.1·§44.2 로 나란히 적었다. 저쪽은 검증기가 시작 훅에 걸리지 않았고 여기는 수집기가 생성되지 않는다. -- **소비자가 없는 fixture 셋** - 둘 다 이름 검색으로 드러났지만 결과가 다르다. 저쪽은 만들어진 것을 꺼내 쓰는 곳이 없고, 여기는 만드는 곳 자체가 없다. -- **@Bean이 있다는 것은 조립 증거가 아니다** - 거기서는 등록된 빈이 조립을 뜻하지 않는다고 적었다. 여기는 그보다 앞이다. 클래스가 있을 뿐 빈으로도 시험으로도 만들어지지 않는다. - -## 문제 - -라우트 목록이 릴리스 게이트 역할을 하도록 만들어져 있다. 그 목록이 실제 배포의 라우트로 채워지는지 확인했다. - -## 결론 - -채워지지 않는다. - -패키지의 네 파일 중 셋에는 자기 파일 밖에서 이름을 쓰는 파일이 있다. 수집기 138 줄만 main 도 test 도 0 이다. 다만 그 main 쪽 참조가 서로와 이 수집기뿐이고, 넷 다 스프링 애너테이션이 한 줄도 없어 스캔이 집어 갈 것도 없다. 프로덕션에서 실행되는 자리는 넷 다 없다. - -이름이 나오는 곳은 자기 클래스 선언과 생성자, 그리고 2026-08-13 실행 계획 문서 두 줄이다. 그 계획은 같은 자리에 수집기를 둘 적었는데 하나만 만들어졌다. - -인벤토리를 새로 만드는 main 파일도, 그 인벤토리에서 예외가 나가는 main 경로도 하나씩뿐이고 둘 다 이 수집기에서 시작한다. 수집기가 불리지 않으므로 목록이 만들어지지 않고, 목록이 없으니 게이트가 볼 것도 없다. - -시험은 목록을 손으로 채운다. WebRouteInventoryTest:123 의 도우미가 WebRouteContract 를 직접 만들어 넣고, 핸들러 매핑은 그 파일에 등장하지 않는다. 규칙은 검증되지만 배포가 실제로 내보내는 라우트에 대해서는 아무 말도 하지 않는다. - -수집기 자체는 고장 나 있지 않다. 출하 기본 접두를 건 컨텍스트에 물려 보니 게이트가 요구하는 목록을 만들어 내고 미등록 라우트에서 예외가 나온다. - -다만 버전은 읽어 내지 못한다. 원인은 경로 접두다. versionOf 는 패턴 첫머리만 보는데 접두가 모든 패턴 앞에 붙으므로, 출하 기본값 /v1 로 뜨는 레인에서는 어느 모양도 걸리지 않는다. /api 로 뜨는 레인에서는 걸릴 모양이 생기지만 그 경로를 선언한 컨트롤러 다섯이 꺼진 게이트 뒤에 있다. 원문에는 없는 대목이고, 수집기가 불리지 않으므로 지금은 드러날 자리도 없다. - -원문은 "저장소 전체에서 이 타입 이름이 등장하는 곳은 자기 파일의 클래스 선언과 생성자 두 줄뿐이다"라고 적었다. 저장소 전체로는 틀렸다. git 추적 파일을 다 찾으면 계획 문서 두 줄이 더 있어 넷이다. 두 줄이 맞는 것은 src/ 로 좁혔을 때뿐인데 원문은 그 범위를 적지 않았다. 사례의 무게도 참조 계수보다는 그 138 줄이 게이트의 유일한 입구라는 데 있다. - -판정은 P3 이고 원문과 같다. 게이트가 지금 어떤 배포도 막지 않으므로 동작이 달라지지 않는다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Spring Boot : 4.0.8 -확인 방식 : 패키지 네 파일의 main·test 참조 계수, git 추적 파일 전체 이름 검색, 계획 문서 대조, 실제 RequestMappingHandlerMapping 에 수집기를 물려 수집과 게이트 실행 -소스 수정 : x - -## 재현 조건 - -원문 근거는 분석 문서의 #L1494 절이다. - -1. admin/route 네 파일의 줄 수와, 각각을 자기 파일 밖에서 부르는 main·test 자리 수를 센다. -2. 수집기 이름을 git 추적 파일 전체에서 찾는다. -3. 2026-08-13 실행 계획이 이 자리에 적은 수집기 목록과 소스에 있는 것을 맞춰 본다. -4. main 에서 WebRouteInventory 를 새로 만드는 파일과 예외를 던지는 파일을 찾는다. -5. requireRegisteredOperations 가 나오는 자리를 main 과 test 로 갈라 센다. -6. 시험이 목록을 무엇으로 채우는지 읽고, 그 파일이 핸들러 매핑을 쓰는지 본다. -7. 맨 경로와 /api/v1 과 /v1 로 각각 시작하는 컨트롤러 셋을 올리고, 경로 접두만 /v1·""·/api 로 바꿔 가며 컨텍스트를 세 번 띄운다. 각각 핸들러 매핑을 꺼내 수집기에 넘기고, 기본 버전은 경로에 없는 값을 준다. -8. 나온 목록을 카탈로그에 두 번 댄다. 한 번은 연산 하나만 등록해 두고, 한 번은 나머지도 등록해 둔다. - -## 본문 - - - -수집기 자바독이 자기 역할을 적어 두었다. 애너테이션이 아니라 디스패처가 실제로 참조할 핸들러 매핑에서 읽는다는 것이고, 게이트는 배포가 내보내는 쪽을 봐야 하기 때문이다. - -## 네 조각 중 셋은 서로를 부르고 하나는 아무도 부르지 않는다 - -:::evidence key="a14-f017-springmvcrouteinventorycollector" alt="저장소 루트에서 돌린 정적 검색 출력 91줄. admin/route 네 파일의 줄 수와 자기 파일 밖에서 그 이름을 쓰는 main·test 파일 수가 표로 나오고 수집기만 양쪽이 0 이며, 그 참조 파일들이 어디에 있는지 목록이 이어진다. 이어서 수집기 자바독의 역할 서술, 그 이름이 git 추적 파일 전체에 나오는 네 줄, 2026-08-13 계획 문서가 적은 수집기 둘과 소스에 실제로 있는 하나, main 에서 인벤토리를 만드는 파일과 예외를 던지는 파일, requireRegisteredOperations 가 나오는 main 한 곳과 test 두 곳, 네 클래스의 스프링 애너테이션 줄 수, versionOf 가 찾는 모양을 선언한 main 두 자리와 그것을 가두는 조건 애너테이션과 그 속성이 저장소에 나오는 두 자리, `src` 안에서 경로 접두를 정하는 자리 전부와 배포 레인 셋이 고르는 프로파일, 접두가 /api 일 때 정규식에 걸릴 수 있는 컨트롤러 다섯의 이름과 그것을 가두는 속성이 web main 아홉 파일과 저장소 전체 스물아홉 파일에서 참조된다는 수와 그 속성의 값 셋, 시험이 계약을 손으로 만드는 도우미와 그 파일이 핸들러 매핑을 쓰지 않는다는 계수가 보인다." caption="admin/route 네 파일의 참조 파일 수와 계획 문서·경로 접두·게이트 대조 — 91줄 · exit 0" zoom="true" -::: - -`WebRouteContract` 는 main 둘, 나머지 둘은 main 하나씩이고, 셋 다 test 하나씩이다. 수집기 행만 비어 있다. - -다만 그 참조 파일이 전부 `admin/route` 안이다. 패키지 밖에서 이 네 타입을 쓰는 파일은 main 에도 test 에도 없다. 셋이 살아 있다는 것은 서로를 부른다는 뜻이지 누가 쓴다는 뜻이 아니다. - -계획 문서에는 수집기가 둘 적혀 있었다. 서블릿 쪽과 리액티브 쪽이다. 리액티브 쪽은 소스에 파일이 없다. - -## 게이트로 가는 길이 여기 하나뿐이다 - -```java -// SpringMvcRouteInventoryCollector.java:46-50 - public WebRouteInventory collect( - RequestMappingHandlerMapping mapping, ApiMajorVersion defaultVersion) { - ... - WebRouteInventory inventory = new WebRouteInventory(); -``` - -main 에서 `new WebRouteInventory()` 를 쓰는 파일은 이것뿐이다. 그리고 그 인벤토리 클래스가 그 예외를 던지는 유일한 main 파일이다. - -```java -// WebRouteInventory.java:50-64 - public void requireRegisteredOperations(WebOperationCatalog catalog) { - ... - throw new RouteInventoryMismatchException( - "routes serve unregistered operations, so they have no budget, authorization or" - + " idempotency policy: " -``` - -`requireRegisteredOperations` 가 나오는 자리는 이 선언과 시험 둘이다. 프로덕션에서 부르는 곳은 없다. - -## 시험은 매핑을 보지 않는다 - -```java -// WebRouteInventoryTest.java:123-127 - private static WebRouteContract route(String operation, String method, String path, int version) { - return new WebRouteContract( - new WebRouteId(method + " " + path), - new WebOperationName(operation.length() >= 3 ? operation : "op." + operation), - new ApiMajorVersion(version), -``` - -계약을 손으로 만들어 넣는다. 그 파일에 `RequestMappingHandlerMapping` 은 한 번도 나오지 않는다. 단언은 통과하지만 그 통과가 말하는 대상은 시험이 지어낸 라우트다. - -## 물려서 돌려 보면 동작한다 - -:::evidence key="a14-f017-springmvcrouteinventorycollector-run" alt="JVM 프로브 출력 28줄. 맨 첫 줄에 OpenJDK 판이 찍히고, 컨트롤러 셋의 성격과 넘긴 기본 버전을 밝힌 두 줄이 이어진다. 그다음 접두 세 값으로 띄운 세 구성이 차례로 나오는데, 각각 매핑 수와 라우트 셋의 경로·연산 이름·버전이 표로 보이고 아래에 한 줄 설명이 붙는다. 출하 기본값에서는 셋 다 기본 버전이고, 접두를 비우면 /api/v1 로 선언한 것이, 접두가 /api 이면 /v1 로 선언한 것이 v1 으로 읽힌다. 끝으로 출하 접두 목록을 카탈로그에 댔을 때 미등록 라우트를 지목하며 던진 예외와 다 등록한 뒤 통과한 결과가 보인다." caption="접두 세 값으로 수집과 게이트를 돌린 결과 — 28줄 · exit 0" zoom="true" -::: - -컨트롤러 셋을 올렸다. `HealthcheckController` 처럼 맨 경로를 적은 것, `OperationHttpController:43` 처럼 `/api/v1` 로 시작하는 것, `FileDownloadController:77` 처럼 `/v1` 로 시작하는 것이다. 접두만 갈아 끼우고 나머지는 세 구성이 같다. 수집기가 매핑에서 라우트를 읽어 핸들러 이름으로 연산 이름을 만들고, 카탈로그에 대면 미등록 라우트를 지목하며 거부한다. - -버전만 예외다. `PresentationWebConfig:23` 이 접두를 모든 컨트롤러 앞에 붙이므로, 접두가 비어 있지 않으면 등록된 패턴은 접두로 시작한다. `versionOf` 는 패턴 첫머리만 보기 때문에 출하 기본값 `/v1` 에서는 어느 라우트도 걸리지 않는다. - -접두는 이 저장소에서 세 값을 갖는다. 출하 기본값 `/v1`, `app-bootstrap` 시험 리소스의 `""`, 그리고 `application-local.yml:164` 와 `src/.env:112` 가 고정하는 `/api` 다. `src/.env:8` 이 그 프로파일을 켠다. 프로브를 그 셋으로 돌렸다. - -`""` 에서는 `/api/v1` 로 선언한 컨트롤러가 그대로 남아 읽힌다. `/api` 에서는 `/v1` 로 선언한 컨트롤러가 `/api/v1/...` 이 되어 읽힌다. 출하 기본값 `/v1` 에서만 셋 다 기본 버전으로 떨어진다. - -배포 레인이 고르는 접두는 둘이다. 컴포즈의 dev·prod 레인은 `PRESENTATION_API_BASE_PATH` 를 주지 않으므로 출하 기본값 `/v1` 로 뜨고, `src/.env` 가 고르는 로컬 레인은 `/api` 다. `""` 는 `app-bootstrap` 시험 리소스에만 있다. - -`/v1` 로 뜨는 레인에서는 게이트가 상관없다. 접두가 앞에 붙어 어느 모양도 `/api/v` 로 시작하지 못하므로, 속성을 켜도 버전은 그대로 기본값이다. 프로브의 첫 구성이 그것을 보인다. - -`/api` 레인에서만 게이트가 실제로 걸린다. 거기서 읽힐 수 있는 것은 `/v1` 로 선언한 파일서버 컨트롤러 다섯인데 `app.fileserver-platform.enabled` 가 `application.yml:854` 기본값으로도 `src/.env:161` 로도 거짓이다. `/api/v1` 로 선언한 둘은 접두가 붙어 `/api/api/v1/...` 이 되므로 자기 게이트와 무관하게 읽히지 않는다. - -프로덕션이 이 자리에서 하지 않는 일을 프로브가 대신 한 것뿐이다. 수집도 판정도 돈다. 버전만 이 배포의 경로 모양에서 읽히지 않는다. - -## 확인하지 못한 것 - -이 클래스가 과거에 조립되었는지 이력에서 확인하지 않았다. 확인한 것은 고정 리비전에서 부르는 곳이 없다는 것까지다. - -프로브가 띄운 것은 컨트롤러 셋짜리 최소 컨텍스트이고, 그 셋에는 실제 컨트롤러들이 달고 있는 조건 애너테이션이 없다. 출하 조립의 전체 라우트를 수집해 본 것이 아니므로, 실제 배포에서 이 게이트가 몇 건을 걸러 낼지는 모른다. 보인 것은 수집과 판정이 실제 매핑 위에서 동작한다는 것까지다. - -수집기의 `deprecationFor` 는 프로브가 부르지 않았다. 폐기 정책을 빈 것으로 넘겼으므로 폐기 표시가 붙은 라우트는 이 실행에 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-a14-f018-webplatformstartupvalidator.md b/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-a14-f018-webplatformstartupvalidator.md deleted file mode 100644 index 61e0f07..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-a14-f018-webplatformstartupvalidator.md +++ /dev/null @@ -1,144 +0,0 @@ ---- -kind: CASE -slug: a14-f018-webplatformstartupvalidator -title: 운영 런북이 시작 검증기의 판정을 근거로 삼는데 그 검증기를 부르는 곳이 없다 -topic: web-inbound-and-http-surface -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -evidenceCapturedOn: 2026-09-03 -rootTreeNode: case:a14-f018-webplatformstartupvalidator -body: case-a14-f018-webplatformstartupvalidator.body.md -assets: - - key: a14-f018-webplatformstartupvalidator - file: ../../../final/evidence/rendered/a14-f018-webplatformstartupvalidator.svg - - key: a14-f018-webplatformstartupvalidator-run - file: ../../../final/evidence/rendered/a14-f018-webplatformstartupvalidator-run.svg -evidence: - - ../../../final/evidence/raw/a14-f018-webplatformstartupvalidator.txt - - ../../../final/evidence/raw/a14-f018-webplatformstartupvalidator-run.txt -source: - - 원본 분석 절은 final/document.md#a14#L1500 이다. ---- - -# 운영 런북이 시작 검증기의 판정을 근거로 삼는데 그 검증기를 부르는 곳이 없다 - -플랫폼 시작 검증기가 선언된 통제 중 배선되지 않은 것이 있으면 기동을 거부하도록 만들어져 있다. 부르는 곳이 없다. 그런데 운영 런북이 그 검증기가 돈다는 전제로 장애 조사 첫 단계를 적어 두었다. - -## 관계 - -- **라우트 게이트가 실물을 읽어 오는 한 조각만 아무도 만들지 않는다** - 원문이 §44.1·§44.2 로 나란히 적은 짝이다. 저쪽은 수집기가 생성되지 않고 여기는 검증기가 시작 훅에 걸리지 않는다. -- **@Bean이 있다는 것은 조립 증거가 아니다** - 거기서는 등록된 빈이 조립을 뜻하지 않는다고 적었다. 여기는 그보다 앞이다. 클래스에 스테레오타입이 없어 스캔에도 걸리지 않는다. -- **시작 검증기가 도는지는 그 능력에 자동설정 루트가 있는지와 일치한다** - 이 리프에 그 루트가 없다. 자동설정 진입 목록이 `mvc` 와 `webflux` 뿐이고 `admin` 은 없다. -- **그러나 R0 경계가 문서에만 있고 compile 경로에서 닫히지 않는다** - 둘 다 문서가 코드보다 앞서 나갔다. 저쪽은 경계 서술이고 여기는 장애 대응 절차다. - -## 문제 - -플랫폼에 시작 검증기가 있고 이름이 실행 시점을 약속한다. 부르는 곳을 확인했다. - -## 결론 - -없다. - -admin/platform 의 두 파일 중 main 에서 쓰이는 것은 스냅숏 하나뿐이고, 그것을 쓰는 곳도 검증기다. 검증기를 만드는 다섯 자리는 모두 시험이다. - -스테레오타입도 없다. 출하보다 허용적인 스캔을 걸어도 두 클래스는 빈이 되지 않는다. 리프의 자동설정 진입점 둘도 mvc 와 webflux 뿐이라 admin 을 올리지 않는다. - -src 에서 *StartupValidator 로 선언된 클래스를 모두 찾으면 여덟이고, 넷은 자기 파일 밖 main 에 이름이 나오고 넷은 나오지 않는다. 나오는 넷 중 파일서버·HTTP 클라이언트·GraphQL 플랫폼은 app-bootstrap 이나 자기 리프 자동설정이 빈으로 걸었고, Mongo 는 기동 검사 빈이 안에서 직접 만들어 부른다. 나오지 않는 넷은 웹 플랫폼·웹소켓 플랫폼·gRPC 플랫폼·gRPC 서블릿이다. - -범위를 이름에서 런북으로 옮기면 더 많이 나온다. 런북들이 백틱으로 지목한 이름 중 main 에 같은 이름의 파일이 있으면서 자기 파일 밖 main 참조가 0 인 것이 열하나다. 그중 둘이 이 리프 런북에 있고, 둘 다 같은 추론 형태다. :27 이 이 검증기를 그렇게 쓰고, :89 가 WebCorsPolicyValidator 를 같은 문장 구조로 쓴다. 코퍼스가 그 두 번째 건은 이미 따로 적어 두었다. - -원문에 없는 대목은 여기다. docs/web/runbook.md:27 이 이 검증기를 장애 조사 근거로 쓴다. 검증기가 돌아야 성립하는 추론이고, 돌지 않으므로 그 결론은 근거를 잃는다. - -검증기 자체는 동작한다. 통제가 빠진 스냅숏에는 그 이름을 들어 거부하고 다 채워진 스냅숏은 통과시킨다. - -원문이 인용한 "Better to refuse to start" 는 NginxInternalUriMapper:30 의 attestMapping() 자바독이다. 그 메서드를 파일서버 기동 검사가 FileserverStartupConfiguration:87 에서 부르므로 인용은 맞다. - -판정은 P3 이고 원문과 같다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Spring Boot : 4.0.8 -확인 방식 : 두 클래스의 이름을 src 전체 자바 파일에서 전수 검색, 리프 자동설정 진입점과 스테레오타입 확인, 같은 스캔을 건 컨텍스트에서 빈 수 확인, 검증기를 두 스냅숏에 직접 실행, 운영 런북 두 편 대조, src 의 *StartupValidator 선언을 검색으로 모아 main 참조 수 비교, 원문이 인용한 문장의 실제 자리와 그것을 부르는 코드 확인 -소스 수정 : x - -## 재현 조건 - -원문 근거는 분석 문서의 #L1500 절이다. - -1. 검증기 자바독을 읽어 이름이 약속하는 시점과 거부 형태를 확인한다. -2. admin/platform 두 클래스를 자기 파일 밖에서 쓰는 자리를 main 과 test 로 갈라 센다. -3. 리프의 자동설정 진입 목록과 두 클래스의 스테레오타입 유무를 본다. -4. 출하 루트가 이 리프에 거는 것과 같은 스캔을 admin 에 걸고 두 타입의 빈 수를 센다. -5. app-bootstrap 이 다른 시작 검증기를 거는 형태를 읽고, 원문이 인용한 근거 문장이 어느 파일에 있고 무엇이 그것을 부르는지 확인한다. -6. 검증기를 직접 만들어 통제가 빠진 스냅숏과 다 채워진 스냅숏에 각각 넣는다. -7. 운영 런북에서 이 검증기를 근거로 쓰는 대목을 읽는다. -8. src 에 선언된 *StartupValidator 를 모두 찾아 각각 자기 파일 밖 main 파일 수를 센다. -9. 런북들이 백틱으로 지목한 이름을 모아, main 에 같은 이름의 파일이 있으면서 자기 파일 밖 main 참조가 0 인 것을 고른다. - -## 본문 - - - -검증기 자바독이 실행 시점과 거부 형태를 함께 적어 두었다. 기동 시점에 보는 이유는 그러지 않으면 장애로 알게 되기 때문이고, 경고가 아니라 거부인 이유는 경고를 남기는 검증기는 아무도 읽지 않기 때문이다. - -## 두 클래스를 쓰는 자리는 시험뿐이다 - -:::evidence key="a14-f018-webplatformstartupvalidator" alt="저장소 루트에서 돌린 정적 검색 출력 84줄. 검증기 자바독의 실행 시점과 거부 근거, admin/platform 두 클래스를 자기 파일 밖에서 쓰는 자리가 main 과 test 로 갈려 나오고 검증기 쪽은 test 뿐이다. 이어서 운영 런북이 이 검증기를 근거로 쓰는 세 줄, app-bootstrap 이 파일서버와 HTTP 클라이언트 시작 검증기를 빈으로 거는 두 자리, 이 리프의 자동설정 진입 목록 둘과 그 목록에서 admin 이라는 문자열을 담은 항목 수, 검증기 클래스의 스프링 스테레오타입 수, `src` 에 선언된 `*StartupValidator` 여덟과 각각의 이름이 자기 파일 밖 main 자바 파일 몇 개에 나오는지가 차례로 보인다. 자바독 언급도 그 수에 들어간다. 끝으로 런북들이 백틱으로 지목한 이름 중 main 참조가 0 인 열하나와, 그중 이 리프 런북이 같은 추론을 거는 두 자리가 나온다. 원문이 인용한 근거 주석과 그것을 부르는 기동 검사 자리도 그 사이에 있다." caption="admin/platform 두 클래스의 참조와 런북·형제 배선·검증기 여덟·런북 지목 식별자 대조 — 84줄 · exit 0" zoom="true" -::: - -검증기 이름이 자기 파일 밖에 나오는 여섯 줄이 모두 그 시험 파일이다. 그중 다섯이 생성이고 하나는 시험 클래스 자신의 선언이다. 스냅숏은 main 에서 검증기 하나가 쓴다. - -리프의 자동설정 진입점은 `mvc` 와 `webflux` 둘이고 `admin` 을 올리는 항목은 없다. 두 클래스에 스프링 스테레오타입도 붙어 있지 않다. - -## 같은 저장소가 이미 쓰는 배선 형태가 있다 - -```java -// FileserverStartupConfiguration.java:35-37 · :47-49 - @Bean - @ConditionalOnMissingBean - FileserverStartupValidator fileserverStartupValidator() { - ... - @Bean - FileserverStartupCheck fileserverStartupCheck( - FileserverStartupValidator validator, -``` - -검증기를 빈으로 만들고, 그것을 인자로 받는 두 번째 빈이 기동 시점에 실제 검사를 돌린다. HTTP 클라이언트 쪽도 같은 모양이다. 플랫폼 쪽에는 둘 다 없다. - -## 런북이 이 검증기를 근거로 쓴다 - -```text -docs/web/runbook.md:27-29 -- **Check first:** the platform snapshot's `uninstalledControls()`. `WebPlatformStartupValidator` - fails startup on a missing required control, so a running instance with one missing means it was - not in the required list. -``` - -통제가 설정만 되고 배선되지 않은 장애를 다루는 절이다. 조사자에게 미설치 통제를 먼저 보라고 하고, 인스턴스가 떠 있다는 사실 자체를 근거로 삼아 그 통제는 필수 목록에 없었다고 결론짓게 한다. - -검증기가 돌아야 성립하는 추론이다. 돌지 않으므로 누락된 통제가 필수 목록에 있었더라도 인스턴스는 그대로 떠 있고, 조사자는 반대 결론에 이른다. - -## 넣어 보면 거부한다 - -:::evidence key="a14-f018-webplatformstartupvalidator-run" alt="JVM 프로브 출력 14줄. 통제 셋 중 하나가 배선되지 않은 스냅숏을 넣었을 때 스냅숏이 보고하는 미설치 통제 이름과 검증기가 던진 예외 이름과 메시지, 셋 다 배선된 스냅숏을 넣었을 때 통과한 결과, 그리고 출하 루트의 기본 패키지를 제외 필터 없이 admin 에 걸었을 때 올라온 두 타입의 빈 수가 각각 0 이라는 결과가 보인다." caption="검증기를 두 스냅숏에 직접 넣고, 같은 스캔에서 빈 수를 센 결과 — 14줄 · exit 0" zoom="true" -::: - -통제 하나가 빠진 스냅숏을 넣으면 그 이름을 들어 거부한다. 거부 메시지가 런북과 같은 말을 한다. 설정만 되고 설치되지 않은 통제는 필요해지는 순간까지 동작하는 것과 구별되지 않으므로 여기서 기동을 실패시킨다는 것이다. - -셋 다 배선된 스냅숏은 통과한다. 같은 프로브가 출하 루트의 기본 패키지를 제외 필터 없이 `admin` 에 걸어 보면 두 타입 다 빈이 0 개다. - -## 확인하지 못한 것 - -애플리케이션을 통째로 띄워 검증기가 돌지 않는 것을 확인하지 않았다. 확인한 것은 main 에 만드는 곳이 없고 스캔으로도 올라오지 않는다는 것까지다. - -런북이 첫 단계로 지시하는 스냅숏 조회를 실제로 해 보지 않았다. 확인한 것은 main 에서 스냅숏을 쓰는 곳이 검증기 하나뿐이라는 것까지다. - -프로브가 넣은 통제 이름 셋은 지어낸 것이다. 실제 배포가 어떤 이름을 필수로 두는지는 확인하지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-analysis-finding-a14-f003.md b/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-analysis-finding-a14-f003.md deleted file mode 100644 index f769dab..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-analysis-finding-a14-f003.md +++ /dev/null @@ -1,164 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a14-f003 -title: 플랫폼 요청 컨텍스트가 서블릿에는 생산자가 없고, 리액티브에는 익명 액터로 고정되어 있다 -topic: web-inbound-and-http-surface -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a14-f003 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a14-f003.body.md -assets: - - key: analysis-finding-a14-f003 - file: ../../../final/evidence/rendered/analysis-finding-a14-f003.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a14-f003.txt -source: - - 원본 분석 절은 final/document.md#a14#L503 이다. ---- - -# 플랫폼 요청 컨텍스트가 서블릿에는 생산자가 없고, 리액티브에는 익명 액터로 고정되어 있다 - -서블릿 쪽은 컨텍스트를 저장하는 코드가 0 이라 그것을 요구하는 인자 해석기가 항상 던진다. 반응형 쪽은 생산자가 하나 있는데 열 성분 중 넷을 상수로 채운다. 인증 결과가 요청 컨텍스트에 도달하는 경로가 없다. - -## 관계 - -- **두 개의 outbox 중 하나만 조립되어 있다** - 같은 형태의 절반 조립이다. -- **한쪽의 javadoc이 다른 쪽의 동작을 규탄한다** - 이 사례가 그 규칙의 형태다. -- **픽스처가 생산자를 우회하면 게이트가 도는 것은 픽스처다** - 테스트가 못 잡는 이유다. - -## 문제 - -플랫폼 요청 컨텍스트는 컨트롤러 서명에서 하위 전송 타입을 몰아내려고 만든 값이다. - -두 전송에서 그 값이 어떻게 만들어지는지 확인했다. - -## 결론 - -두 전송이 각각 다르게 망가져 있다. - -서블릿 절반부터 본다. - -저장 메서드의 호출자가 저장소 전체에서 0 이다. - -그런데 그것을 읽는 쪽은 자동 설정이 등록한다. 인자 해석기를 설정에 추가하는 빈이 있다. - -그 해석기가 하는 일은 요구 메서드 하나이고, 속성이 없으면 던진다. - -던지는 메시지가 이유를 적는다. 여기서 하나를 지어내면 인증되지 않은 호출이 누군가 고른 익명 행위자처럼 보이게 된다는 것이다. - -속성을 놓는 코드가 없으므로 이 예외는 항상 던져진다. - -서블릿 컨트롤러가 그 컨텍스트를 매개변수로 선언하면 그 종점은 언제나 서버 오류다. - -컨트롤러의 서명에서 하위 전송 타입을 몰아내려고 만든 기능이, 쓰면 반드시 실패하는 기능이다. - -반응형 절반은 다르다. - -생산자가 하나 있고, 열 성분 중 넷을 상수로 채운다. 주 판본과 행위자와 소속과 지역이다. - -이 필터는 보안 문맥 보유자도, 보안 문맥 다리도, 인증 조회도 참조하지 않는다. - -인증 결과가 요청 컨텍스트에 도달하는 경로가 없다. - -인증된 호출자든 아니든 컨텍스트의 행위자는 익명이고 소속은 없음이다. - -같은 저장소가 이 정확한 위험을 서블릿 쪽에서는 이름 붙여 거부한다. - -요구 메서드의 메시지가 지어낸 컨텍스트는 인증되지 않은 호출을 누군가 고른 익명 행위자처럼 보이게 한다고 적는다. - -반응형 생산자가 하는 일이 정확히 그 지어내기다. - -두 전송이 같은 위험에 정반대로 대응했고, 한쪽의 자바독이 다른 쪽의 동작을 규탄한다. - -반응형 실패 시나리오는 이렇다. - -인증된 사용자가 자기 비동기 작업 결과를 조회한다. - -반응형 컨트롤러가 접근 정책을 부르고, 행위자의 인증 여부가 항상 거짓이므로 모든 조회가 거부된다. - -정책의 주석이 부재와 금지를 의도적으로 같게 답한다고 적으므로 클라이언트는 없음 응답을 받는다. - -비동기 작업 기능이 반응형 전송에서 동작하지 않는다. - -서블릿 실패 시나리오는 앞서 본 대로다. 같은 종점이 컨텍스트를 매개변수로 받으면 해석기가 던져 서버 오류이고, 받지 않으면 컨텍스트를 얻을 경로가 없다. - -방향은 닫힌 실패다. 데이터 유출이 아니라 기능 불능이다. - -그래서 보안 사고가 아니라 지금 틀린 동작으로 P1 이다. - -테스트가 잡지 못하는 이유는 구조에 있다. - -그 컨텍스트를 만드는 다른 열한 곳이 전부 테스트와 테스트킷이고 전부 손으로 채운다. - -계약 레인의 픽스처 컨트롤러조차 해석기를 쓰지 않고 자기 컨텍스트를 만든다. - -네 런타임을 가로지르는 교차 스택 게이트가 있어도, 그 게이트가 도는 픽스처가 생산자를 우회한다. - -권고는 셋이다. - -서블릿에 저장을 부르는 필터를 추가한다. 요청당 한 번 도는 자리가 이미 있다. - -두 생산자가 보안 문맥에서 행위자와 소속을 읽게 한다. 그 목적의 다리가 이미 존재한다. - -그리고 픽스처가 손으로 컨텍스트를 만드는 대신 해석기와 필터를 지나게 한다. 셋째 없이는 같은 상태가 다시 성립한다. - -## 검증 환경 - -Spring Boot : 4.0.8 -확인 방식 : 호출자 전수 검색과 생산자 코드 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/176 계열에 있다. - -1. 서블릿 컨텍스트 보유자의 저장 메서드 호출자를 센다. -2. 인자 해석기를 등록하는 자동 설정을 확인한다. -3. 해석기가 부르는 요구 메서드의 실패 경로를 읽는다. -4. 반응형 생산자가 각 성분을 무엇으로 채우는지 확인한다. -5. 그 필터가 보안 문맥을 참조하는지 확인한다. -6. 테스트와 픽스처가 컨텍스트를 어떻게 만드는지 센다. - -## 본문 - - - -**서블릿 절반.** `WebMvcRequestContextHolder.store(...)`의 호출자가 저장소 전체에서 **0**이다. - -## 서블릿 쪽 생산자 계수 - -:::evidence key="analysis-finding-a14-f003" alt="분석 문서 final/document.md#a14 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a14 발췌 — 15줄" zoom="true" -::: - -## 읽는 쪽은 자동설정이 등록한다 - -```java -// WebMvcPlatformAutoConfiguration.java:158-166 -@Bean @ConditionalOnMissingBean(name = "webMvcRequestContextConfigurer") -public WebMvcConfigurer webMvcRequestContextConfigurer() { - return new WebMvcConfigurer() { - @Override public void addArgumentResolvers(List resolvers) { - resolvers.add(new WebMvcRequestContextArgumentResolver()); - } - }; -} -``` - -## 리졸버가 하는 일은 하나이고 없으면 던진다 - -`WebMvcRequestContextHolder.require(request)` 하나이며, 속성이 없으면 던진다 — "no web request context on this request; a fabricated one here would make an unauthenticated call look like an anonymous actor somebody chose". 리액티브 쪽은 익명 액터로 고정되어 있다. P1. - -## 권고 - -(1) 서블릿에 `store`를 부르는 필터를 추가한다(`WebMvcEvidenceFilter`가 이미 요청당 한 번 도는 자리다). (2) 두 생산자가 보안 컨텍스트에서 액터·테넌트를 읽게 한다 — `WebSecurityContextBridge`가 그 목적으로 이미 존재한다(§12.2). (3) 픽스처가 손으로 컨텍스트를 만드는 대신 리졸버/필터를 지나게 한다. (3) 없이는 같은 상태가 다시 성립한다. - -## 확인하지 못한 것 - -두 종점에 실제 요청을 보내 오류와 거부를 재현하지 않았다. 호출자 부재와 상수 고정상 그 결과가 나온다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-analysis-finding-a14-f004.md b/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-analysis-finding-a14-f004.md deleted file mode 100644 index 3932a94..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-analysis-finding-a14-f004.md +++ /dev/null @@ -1,130 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a14-f004 -title: 프레임워크 자유 신원 모델과 교차 테넌트 가드가 프로덕션에서 한 번도 참조되지 않는다 -topic: web-inbound-and-http-surface -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a14-f004 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a14-f004.body.md -assets: - - key: analysis-finding-a14-f004 - file: ../../../final/evidence/rendered/analysis-finding-a14-f004.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a14-f004.txt -source: - - 원본 분석 절은 final/document.md#a14#L562 이다. ---- - -# 프레임워크 자유 신원 모델과 교차 테넌트 가드가 프로덕션에서 한 번도 참조되지 않는다 - -보안 패키지 열한 파일이 서로만 참조하고 바깥에서 들어오는 화살표가 없다. 그 안에 클라이언트가 소속을 제안하는 헤더를 거부하는 가드가 있는데, 도달 경로의 호출자가 테스트뿐이다. 지금 이 플랫폼은 그 헤더를 거부도 무시도 하지 않는다. - -## 관계 - -- **플랫폼 요청 컨텍스트가 서블릿에는 생산자가 없고 리액티브에는 익명 액터로 고정되어 있다** - 이 가드를 고칠 때 함께 연결해야 하는 사례다. -- **용량 보호 계층 전체가 자기 테스트 픽스처 안에서만 실행된다** - 같은 리프의 같은 형태다. -- **노출이 없는 것과 가드가 작동하는 것은 다르다** - 이 사례가 그 규칙의 형태다. - -## 문제 - -이 리프에 프레임워크에 의존하지 않는 신원 모델이 있다. - -그 패키지가 바깥에서 쓰이는지 확인했다. - -## 결론 - -쓰이지 않는다. - -보안 패키지 열한 파일이 서로만 참조하고 바깥에서 들어오는 화살표가 없다. - -인증 조회 값을 만드는 production 코드가 없으므로 보안 문맥 다리의 해석 메서드가 호출될 수 있는 상태 자체가 없다. - -가장 값이 큰 부분이 그 안에 있다. - -소속 문맥 해석기가 클라이언트가 소속을 제안하는 헤더 이름 넷을 알고 있고, 그 이름이 요청 입력에 있으면 거부 예외를 던진다. - -도달 경로는 보안 문맥 다리의 두 인자 오버로드 하나뿐이고, 그 오버로드의 호출자는 테스트뿐이다. - -그래서 지금 이 플랫폼은 그 헤더에 대해 거부도 무시도 하지 않는다. 그 헤더를 보는 코드가 아예 없다. - -노출은 아니다. - -소속을 소비하는 유일한 지점이 요청 컨텍스트의 소속을 읽는데, 그 값은 다른 사례 때문에 항상 비어 있다. - -헤더가 소속이 되는 경로가 없으므로 교차 소속 읽기도 없다. - -그러나 그것은 가드가 작동해서가 아니라 소속 기능 전체가 배선되지 않아서다. - -요청 컨텍스트를 고치면서 이 가드를 함께 연결하지 않으면, 그때 노출이 생긴다. - -이 패키지에는 교차 출처 정책 검증기와 위조 방지 정책 해석기도 있고 둘 다 production 참조가 0 이다. - -실제 두 결정은 보안 설정이 프레임워크 API 로 직접 내린다. - -같은 질문에 대한 두 번째 구현이 검증만 되고 쓰이지 않는다. - -판정은 P2 다. - -## 검증 환경 - -Spring Boot : 4.0.8 -확인 방식 : 패키지 간 참조 방향 확인과 호출자 전수 검색 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/176 계열에 있다. - -1. 보안 패키지의 파일 목록을 만든다. -2. 각 타입을 참조하는 바깥 코드를 검색한다. -3. 소속 해석기의 거부 메서드 도달 경로를 확인한다. -4. 그 경로의 호출자를 센다. -5. 소속을 소비하는 지점이 읽는 값이 무엇인지 확인한다. -6. 교차 출처와 위조 방지 결정이 실제로 어디서 내려지는지 확인한다. - -## 본문 - - - -`security` 패키지 11개 파일이 서로만 참조하고 바깥에서 들어오는 화살표가 없다(§11.1). - -## AuthenticationView 참조 위치 - -:::evidence key="analysis-finding-a14-f004" alt="코드베이스에서 AuthenticationView 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="AuthenticationView 코드베이스 검색 — 5줄 · exit 0" zoom="true" -::: - -## 호출될 수 있는 상태 자체가 없다 - -`AuthenticationView`를 만드는 프로덕션 코드가 없으므로 `WebSecurityContextBridge.resolve(...)`가 호출될 수 있는 상태 자체가 없다. - -## 가장 값이 큰 부분이 그 안에 있다 - -```java -// WebTenantContextResolver.java -private static final Set TENANT_INPUT_NAMES = - Set.of("x-tenant-id", "tenant-id", "tenantid", "tenant"); - -public void rejectTenantInput(Map requestInput) { ... throw new UntrustedTenantInputException(); } -``` - -클라이언트가 테넌트를 제안하는 헤더를 거부하는 가드다. 도달 경로는 `WebSecurityContextBridge.resolve(authentication, requestInput)` 오버로드 하나뿐이고, 그 오버로드의 호출자는 테스트뿐이다. 그래서 지금 이 플랫폼은 `X-Tenant-Id` 헤더에 대해 **거부도 무시도 하지 않는다** — 그 헤더를 보는 코드가 아예 없다. - -## 노출은 아니지만 가드가 작동해서가 아니다 - -테넌트를 소비하는 유일한 지점(`OperationAccessPolicy:37-40`)이 `context.tenant()`를 읽고, 그 값은 §12.1 때문에 항상 비어 있다. 헤더가 테넌트가 되는 경로가 없으므로 교차 테넌트 읽기도 없다. 그러나 그것은 **테넌트 기능 전체가 배선되지 않아서**다. §12.1을 고치면서 이 가드를 함께 연결하지 않으면 그때 노출이 생긴다. - -## 같은 질문에 대한 두 번째 구현 - -이 패키지에는 `WebCorsPolicyValidator`(96줄)와 `WebCsrfPolicyResolver`(43줄)도 있고 둘 다 프로덕션 참조 0이다. 실제 CORS·CSRF 결정은 `SecurityConfig`가 Spring Security API로 직접 내린다. P2. - -## 확인하지 못한 것 - -소속 제안 헤더를 붙여 요청을 보내 아무 일도 일어나지 않는 것을 재현하지 않았다. 참조 부재상 그 결과가 나온다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-analysis-finding-a14-f006.md b/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-analysis-finding-a14-f006.md deleted file mode 100644 index 4309b19..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-analysis-finding-a14-f006.md +++ /dev/null @@ -1,137 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a14-f006 -title: 용량 보호 계층 전체(41 main files)가 자기 테스트 픽스처 안에서만 실행된다 -topic: web-inbound-and-http-surface -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a14-f006 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a14-f006.body.md -assets: - - key: analysis-finding-a14-f006 - file: ../../../final/evidence/rendered/analysis-finding-a14-f006.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a14-f006.txt -source: - - 원본 분석 절은 final/document.md#a14#L677 이다. ---- - -# 용량 보호 계층 전체(41 main files)가 자기 테스트 픽스처 안에서만 실행된다 - -바이트 상한과 마감과 부하 차단과 반응형 속도 제한을 구현한 마흔한 파일 중 프로덕션 컨텍스트에 등록되는 것이 없다. 이 플랫폼을 그대로 배포하면 요청 본문 상한도 응답 상한도 마감도 동시성 상한도 대기열 상한도 없다. - -## 관계 - -- **멱등 실행 계층과 durable-operation 표면이 픽스처에서만 조립된다** - 같은 형태의 반복이다. -- **플랫폼 요청 컨텍스트가 서블릿에는 생산자가 없고 리액티브에는 익명 액터로 고정되어 있다** - 같은 리프의 다른 P1 이다. -- **레인은 게이트가 올바른가를 증명하고 플랫폼이 게이트를 설치하는가는 묻지 않는다** - 이 사례가 그 규칙의 형태다. - -## 문제 - -이 리프는 용량 보호 계층을 갖추고 있다. - -바이트 경계와 마감, 부하 차단, 속도 제한, 예산 초과 문제 문서다. - -각 능력이 프로덕션 배포에서 실제로 설치되는지 확인했다. - -## 결론 - -대부분 설치되지 않는다. - -바이트 경계와 마감을 구현한 일곱 타입이 등록되지 않는다. - -부하 차단의 네 타입도 등록되지 않는다. - -두 전송의 조절 필터도 등록되지 않는다. - -속도 제한은 인터셉터 경로 하나만 있다. 그것은 등록되지만 기본이 비활성이다. - -예산 초과 문제 문서 처리기는 기본이 꺼짐이고 의존 빈도 선언되지 않는다. - -즉 이 플랫폼을 그대로 배포하면 요청 본문 크기 상한도, 응답 크기 상한도, 요청 마감도, 동시성 상한도, 대기열 상한도 없다. - -표준 예산이 정의하고 예산 목록이 담고 있는 값들은 아무도 읽지 않는다. - -실패 시나리오는 이렇다. - -배포된 API 에 무제한 조각 본문이 도착한다. - -예산 필터가 필터 사슬에 없으므로 유계 요청 래퍼가 스트림을 감싸지 않고 예산 계량기가 바이트를 세지 않는다. - -컨테이너 기본값 외에 상한이 없다. 그 기본값은 다중 파트와 폼 인코딩에만 적용되고 임의 본문에는 적용되지 않는다. - -같은 요청에 대해 동시성 상한도 없으므로 승인 제어기가 내기로 되어 있던 과부하 응답도 나오지 않는다. - -왜 드러나지 않는가가 이 모듈의 핵심 형태다. - -이 리프는 교차 스택 게이트를 갖고 있다. 세 런타임의 유선 계약을 비교하는 레인과 개별 런타임 계약 레인들이다. - -그 레인들은 게이트가 올바른가를 증명한다. 플랫폼이 게이트를 설치하는가는 묻지 않는다. - -픽스처 애플리케이션이 필요한 것을 손수 배선해 레인을 돌리기 때문이다. - -판정은 P1 이다. - -## 검증 환경 - -Spring Boot : 4.0.8 -확인 방식 : 능력별 구현과 프로덕션 등록 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/176 계열에 있다. - -1. 용량 보호 능력 목록을 만든다. -2. 각 능력의 구현 타입을 확인한다. -3. 각 타입이 프로덕션 컨텍스트에 등록되는지 확인한다. -4. 표준 예산 값을 읽는 코드를 검색한다. -5. 계약 레인의 픽스처가 무엇을 손수 배선하는지 확인한다. - -## 본문 - - - -프로덕션 배포에서 실제로 설치되는 것과 설치되지 않는 것이 갈린다. - -| 능력 | 구현 | 프로덕션 등록 | -|---|---|---| -| 요청/응답 바이트 바운드 · 데드라인 | `WebMvcBudgetFilter`(122) · `WebFluxBudgetFilter`(122) · `BoundedHttpServletRequest`(115) · `BoundedHttpServletResponse`(151) · `BoundedServerWebExchange`(79) · `WebBudgetMeter`(89) · `WebRequestBudget`(141) | **없음** | -| 부하 차단(제한된 동시성 + 제한된 큐 + 503) | `SemaphoreAdmissionController`(118) · `AdmissionProfile`(80) · `AdmissionDecision`(59) · `AdmissionPermit`(21) | **없음** | -| 429 속도 제한 (`WebRateLimiter` 경로) | `WebMvcThrottleFilter`(136) · `WebFluxThrottleFilter`(142) | **없음** | -| 429 속도 제한 (인터셉터 경로) | `RateLimitInterceptor` ← `RateLimitWebConfig` | 있음, 기본 비활성(`APP_RATE_LIMIT_ENABLED:false`) | -| 예산 초과 문제 문서 | `WebMvcBudgetExceptionHandler`(84) | 기본 꺼짐 + 의존 빈 미선언 | - -## WebBudgetCatalog 참조 위치 - -:::evidence key="analysis-finding-a14-f006" alt="코드베이스에서 WebBudgetCatalog 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="WebBudgetCatalog 코드베이스 검색 — 5줄 · exit 0" zoom="true" -::: - -## 그대로 배포하면 상한이 하나도 없다 - -요청 본문 크기 상한도, 응답 크기 상한도, 요청 데드라인도, 동시성 상한도, 큐 상한도 없다. `WebRequestBudget.standard()`가 정의하고 `WebBudgetCatalog`가 담고 있는 값들은 아무도 읽지 않는다(§15.3). P1. - -## 왜 드러나지 않는가 - -이 leaf는 크로스 스택 게이트를 갖고 있다. `webCrossStackParityTest`가 Tomcat·Jetty·Reactor Netty 세 런타임의 와이어 계약을 비교하고, `JettyWebBudgetIT` · `ReactiveWebBudgetIT` · `JettyWebThrottleIT` · `ReactiveWebThrottleIT`가 예산과 스로틀을 실제로 검증한다. 그런데 그 IT들이 띄우는 것은 `BudgetFixtureApplication` 계열이고, **그 픽스처들이 `FilterRegistrationBean`으로 필터를 손수 등록한다**. 레인은 "필터가 올바르게 동작하는가"를 세 런타임에서 증명하고, "플랫폼이 필터를 설치하는가"는 어디서도 묻지 않는다. - -## 이 저장소가 이미 이름 붙인 형태다 - -`EndpointGuardCallSiteTest`(notification)의 javadoc이 그 문장을 갖고 있다 — "A green test on a control nothing invokes is the shape this repository keeps finding, and testing the helper again would not have caught it." 여기서는 그 형태가 41개 파일 규모로 반복된다. - -## 권고 - -`WebMvcPlatformAutoConfiguration`·`WebFluxPlatformAutoConfiguration`이 이미 `WebBudgetCatalog`를 등록하므로 자리는 있다. 네 필터와 `SemaphoreAdmissionController`, `BudgetProblemMapper`를 같은 자동설정에서 `backend.web.budgets.enabled` 게이트 아래 등록하고, **픽스처가 아니라 자동설정이 세운 컨텍스트에서** 하나 이상의 IT를 돌린다. - -## 확인하지 못한 것 - -배포해서 큰 본문을 보내 상한 부재를 재현하지 않았다. 등록 부재상 그 결과가 나온다. - -실패 시나리오 — 배포된 API에 무제한 청크 본문이 도착한다. WebMvcBudgetFilter가 필터 체인에 없으므로 BoundedHttpServletRequest가 스트림을 감싸지 않고, WebBudgetMeter가 바이트를 세지 않는다. 컨테이너 기본값(Tomcat maxPostSize는 multipart/form-data와 폼 인코딩에만 적용되고 임의 본문에는 적용되지 않는다) 외에 상한이 없다. 같은 요청에 대해 동시성 상한도 없으므로 SemaphoreAdmissionController가 내기로 되어 있던 503도 나오지 않는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-analysis-finding-a14-f007.md b/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-analysis-finding-a14-f007.md deleted file mode 100644 index 15ad1ec..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-analysis-finding-a14-f007.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a14-f007 -title: 리액티브 전송에는 속도 제한 경로가 하나도 없다 -topic: web-inbound-and-http-surface -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a14-f007 -evidenceCapturedOn: 2026-09-01 -assets: - - key: analysis-finding-a14-f007 - file: ../../../final/evidence/rendered/analysis-finding-a14-f007.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a14-f007.txt -source: - - 원본 분석 절은 final/document.md#a14#L701 이다. ---- - -# 리액티브 전송에는 속도 제한 경로가 하나도 없다 - -배선된 유일한 속도 제한기는 서블릿 전용 인터셉터다. 반응형 대응물은 등록되지 않고 반응형 자동 설정의 열한 빈에도 없다. 반응형 배포는 속도 제한 스위치를 켜도 걸리지 않는다. - -## 관계 - -- **용량 보호 계층 전체가 자기 테스트 픽스처 안에서만 실행된다** - 이 사례의 상위 사실이다. -- **동적 대상 DNS 핀 능력 검사가 블로킹 오버로드에만 있다** - 같은 형태의 전송 간 비대칭이다. -- **속성은 받아들여지고 소비자는 없다** - 이 사례가 그 규칙의 형태다. - -## 문제 - -이 리프는 두 전송을 지원한다. 서블릿과 반응형이다. - -속도 제한이 두 전송에서 모두 걸리는지 확인했다. - -## 결론 - -한쪽만 걸린다. - -배선된 유일한 속도 제한기는 서블릿 설정자의 인터셉터 추가로 붙는 전용 장치다. - -반응형 대응 필터가 존재하지만 등록되지 않는다. 반응형 플랫폼 자동 설정의 열한 빈에도 없다. - -따라서 반응형 전송을 쓰는 배포는 속도 제한 스위치를 참으로 설정해도 제한이 걸리지 않는다. - -속성은 받아들여지고 관련 포트 빈의 유일성까지 검증되지만, 그것을 소비하는 인터셉터가 반응형 사슬에 존재하지 않는다. - -서블릿 배포에서는 이 경로가 동작한다. 상위 사실인 용량 계층 미등록과 무관하다. - -이 발견은 반응형 전송에 한정된다. - -판정은 P2 다. - -## 검증 환경 - -Spring Boot : 4.0.8 -확인 방식 : 두 전송의 등록 지점 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/176 계열에 있다. - -1. 배선된 속도 제한기가 어떤 방식으로 붙는지 확인한다. -2. 그 방식이 어느 전송 전용인지 확인한다. -3. 반응형 대응 필터가 등록되는지 확인한다. -4. 반응형 자동 설정의 빈 목록을 확인한다. -5. 속도 제한 속성이 무엇을 검증하는지 확인한다. - -## 본문 - - - -배선된 유일한 속도 제한기 `RateLimitInterceptor`는 `WebMvcConfigurer.addInterceptors`로 붙는 MVC 전용 장치다(§15.2). - -## RateLimitInterceptor 참조 위치 - -:::evidence key="analysis-finding-a14-f007" alt="코드베이스에서 RateLimitInterceptor 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RateLimitInterceptor 코드베이스 검색 — 3줄 · exit 0" zoom="true" -::: - -## 리액티브 대응물은 등록되지 않는다 - -`WebFluxThrottleFilter`가 대응물이지만 등록되지 않는다(§16.1). `webflux/autoconfigure/WebFluxPlatformAutoConfiguration`의 11개 빈에도 없다. - -## 확인하지 못한 것 - -반응형 전송으로 띄우고 속도 제한을 켜서 제한이 걸리지 않는 것을 재현하지 않았다. 등록 부재상 그 결과가 나온다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-analysis-finding-a14-f008.md b/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-analysis-finding-a14-f008.md deleted file mode 100644 index 39bbee0..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-analysis-finding-a14-f008.md +++ /dev/null @@ -1,109 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a14-f008 -title: 멱등 실행 계층과 durable-operation 표면이 픽스처에서만 조립된다 -topic: web-inbound-and-http-surface -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a14-f008 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a14-f008.body.md -assets: - - key: analysis-finding-a14-f008 - file: ../../../final/evidence/rendered/analysis-finding-a14-f008.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a14-f008.txt -source: - - 원본 분석 절은 final/document.md#a14#L793 이다. ---- - -# 멱등 실행 계층과 durable-operation 표면이 픽스처에서만 조립된다 - -이 하위 범위에서 프로덕션 컨텍스트에 들어가는 것은 둘뿐이다. 나머지 서른여덟 파일은 테스트와 테스트킷에서만 생성된다. 멱등 키를 붙인 재시도가 두 번 실행된다. - -## 관계 - -- **용량 보호 계층 전체가 자기 테스트 픽스처 안에서만 실행된다** - 같은 형태의 반복이다. -- **플랫폼 요청 컨텍스트가 서블릿에는 생산자가 없고 리액티브에는 익명 액터로 고정되어 있다** - 같은 형태의 세 번째다. -- **헤더를 받아들이는 것처럼 보이는 API가 그것을 지키지 않는다** - 이 사례가 그 규칙의 형태다. - -## 문제 - -이 하위 범위는 멱등 실행 계층과 내구 연산 표면을 담는다. - -무엇이 프로덕션 컨텍스트에 들어가는지 확인했다. - -## 결론 - -둘뿐이다. - -두 요청 필터가 만드는 실행 증거 추적기와 빈 연산 목록이다. - -나머지 서른여덟 파일은 테스트와 테스트킷에서만 생성된다. - -게이트와 두 호출기와 응답 기록기와 지문 공장과 헤더 정책과 명령 부호기와 승인 판정과 응답 계획과 코덱, 그리고 비동기 연산 아홉 종 전부다. - -실패 시나리오는 이렇다. - -클라이언트가 멱등 키를 붙여 결제 생성을 보낸다. - -연결이 끊겨 같은 키로 재시도한다. - -멱등 게이트가 필터 사슬에도 인터셉터에도 컨트롤러 조언에도 없으므로 헤더는 읽히지 않는다. - -두 번째 요청은 첫 번째와 무관하게 그대로 실행된다. 결제가 두 번 생성된다. - -멱등 키를 받아들이는 것처럼 보이는 API 가 그것을 지키지 않으며, 헤더가 거부되지도 않으므로 클라이언트는 지켜졌다고 믿는다. - -같은 형태의 반복이다. - -요청 컨텍스트 생산자 부재와 용량 계층 미등록과 이것이다. 세 하위 범위에서 미조립된 주 파일이 아흔 개다. - -왜 드러나지 않는가도 같다. - -멱등 게이트는 테스트 다섯 곳과 테스트킷 두 곳에서 생성되고, 테스트킷의 픽스처 애플리케이션이 그것을 손수 배선해 계약 레인에서 돌린다. - -레인은 게이트가 올바른가를 증명하고 플랫폼이 게이트를 설치하는가는 묻지 않는다. - -판정은 P1 이다. - -## 검증 환경 - -Spring Boot : 4.0.8 -확인 방식 : 타입별 생성 지점 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/176 계열에 있다. - -1. 하위 범위의 주 파일 목록을 만든다. -2. 각 타입이 프로덕션 컨텍스트에 들어가는지 확인한다. -3. 멱등 게이트를 등록하는 지점을 검색한다. -4. 그 게이트를 생성하는 테스트와 테스트킷 지점을 센다. -5. 픽스처 애플리케이션이 무엇을 손수 배선하는지 확인한다. - -## 본문 - - - -이 sub-scope에서 프로덕션 컨텍스트에 들어가는 것은 `WebExecutionEvidenceTracker`(두 요청 필터가 만든다)와 빈 `InMemoryWebOperationCatalog` 둘뿐이다(§19.1·§19.2). - -## WebExecutionEvidenceTracker 참조 위치 - -:::evidence key="analysis-finding-a14-f008" alt="코드베이스에서 WebExecutionEvidenceTracker 를 검색한 출력 7줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="WebExecutionEvidenceTracker 코드베이스 검색 — 7줄 · exit 0" zoom="true" -::: - -## 나머지 38개는 테스트와 testkit에서만 생성된다 - -게이트, 두 invoker, 응답 writer, 지문 공장, 헤더 정책, 명령 인코더, 승인 판정, 응답 계획, 코덱, 그리고 `operationasync` 9종 전부다. - -## 확인하지 못한 것 - -같은 멱등 키로 두 번 보내 두 번 실행되는 것을 재현하지 않았다. 등록 부재상 그 결과가 나온다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-analysis-finding-a14-f009.md b/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-analysis-finding-a14-f009.md deleted file mode 100644 index e9563ba..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-analysis-finding-a14-f009.md +++ /dev/null @@ -1,112 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a14-f009 -title: 의미 지문이 길이 프레이밍 없이 구분자로 만들어진다 -topic: web-inbound-and-http-surface -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a14-f009 -evidenceCapturedOn: 2026-09-01 -assets: - - key: analysis-finding-a14-f009 - file: ../../../final/evidence/rendered/analysis-finding-a14-f009.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a14-f009.txt -source: - - 원본 분석 절은 final/document.md#a14#L807 이다. ---- - -# 의미 지문이 길이 프레이밍 없이 구분자로 만들어진다 - -경로 변수 값과 헤더 값이 이스케이프 없이 구분자로 이어 붙는다. 값 자체에 그 구분자가 들어가면 서로 다른 두 요청이 같은 정규 문자열을 만들 수 있다. 같은 저장소가 다른 세 모듈에서 같은 문제를 길이 프레이밍으로 닫았다. - -## 관계 - -- **멱등 실행 계층과 durable-operation 표면이 픽스처에서만 조립된다** - 이 게이트가 미조립이라는 상위 사실이다. -- **구분자 이어붙이기는 길이 프레이밍으로 닫는다** - 이 사례가 그 규칙의 형태다. -- **같은 문제를 다른 모듈에서는 닫았다** - 기록하는 이유다. - -## 문제 - -멱등 게이트가 요청의 의미 지문을 만든다. - -경로 변수 값과 헤더 값을 이어 정규 문자열을 만드는 방식이다. - -그 방식이 충돌을 만들 수 있는지 확인했다. - -## 결론 - -만들 수 있다. - -값들이 이스케이프 없이 구분자로 이어 붙는다. - -값 자체에 그 구분자가 들어가면 서로 다른 두 요청이 같은 정규 문자열을 만든다. - -퍼센트 인코딩된 구분자를 프레임워크가 디코딩해 경로 변수로 전달하므로 값에 구분자를 넣는 것이 가능하다. - -경로 변수가 둘 이상인 연산에서 하나를 통제하면 구성할 수 있다. - -악용 가치는 낮다. - -멱등 기록은 주체와 소속으로 범위가 잡히므로 충돌시킬 수 있는 것은 자기 자신의 이전 요청뿐이고, 그것으로 얻는 것이 없다. - -그리고 게이트 자체가 미조립이다. - -기록하는 이유는 일관성이다. - -이 저장소는 다른 세 모듈에서 같은 문제를 길이 프레이밍으로 닫았고 그 이유를 명시했다. - -여기서는 구분자를 골랐고 그 선택에 대한 근거가 없다. - -값 앞에 길이를 붙이는 형태 하나면 닫힌다. - -판정은 P3 다. - -## 검증 환경 - -Spring Boot : 4.0.8 -확인 방식 : 지문 조립 코드 확인과 다른 모듈의 같은 처리 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/176 계열에 있다. - -1. 지문 공장이 값을 어떻게 잇는지 확인한다. -2. 이스케이프나 길이 프레이밍이 있는지 본다. -3. 구분자가 값에 들어갈 수 있는 경로를 확인한다. -4. 멱등 기록의 범위 키를 확인한다. -5. 다른 모듈이 같은 문제를 어떻게 닫았는지 확인한다. - -## 본문 - - - -경로 변수 값과 헤더 값이 이스케이프 없이 구분자로 이어붙는다(§19.4). - -## 지문이 만들어지는 방식 - -:::evidence key="analysis-finding-a14-f009" alt="분석 문서 final/document.md#a14 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a14 발췌 — 15줄" zoom="true" -::: - -## 두 요청이 같은 정규 문자열을 만들 수 있다 - -값 자체에 그 구분자가 들어가면(퍼센트 인코딩 `%1F`를 Spring이 디코딩해 `@PathVariable`로 전달한다) 서로 다른 두 요청이 같은 정규 문자열을 만든다 — 경로 변수가 둘 이상인 연산에서 하나를 통제하면 구성 가능하다. - -## 악용 가치는 낮다 - -멱등 레코드는 `principal` + `tenant`로 범위가 잡히므로 충돌시킬 수 있는 것은 **자기 자신의** 이전 요청뿐이고, 그것으로 얻는 것이 없다. 그리고 게이트 자체가 미조립이다(§20.1). - -## 기록하는 이유는 일관성이다 - -이 저장소는 다른 세 모듈에서 같은 문제를 길이 프레이밍으로 닫았고 그 이유를 명시했다. 여기서는 구분자를 골랐고 그 선택에 대한 근거가 없다. 값 앞에 길이를 붙이는 형태 하나면 닫힌다. P3. - -## 확인하지 못한 것 - -두 요청으로 같은 지문을 만드는 것을 재현하지 않았다. 게이트가 미조립이라 실행 경로가 없다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-analysis-finding-a14-f016.md b/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-analysis-finding-a14-f016.md deleted file mode 100644 index b5636ce..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-analysis-finding-a14-f016.md +++ /dev/null @@ -1,108 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a14-f016 -title: 이 leaf의 리액티브 절반 29개 파일은 어떤 출하 배포에서도 활성화될 수 없다 -topic: web-inbound-and-http-surface -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a14-f016 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a14-f016.body.md -assets: - - key: analysis-finding-a14-f016 - file: ../../../final/evidence/rendered/analysis-finding-a14-f016.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a14-f016.txt -source: - - 원본 분석 절은 final/document.md#a14#L1378 이다. ---- - -# 이 leaf의 리액티브 절반 29개 파일은 어떤 출하 배포에서도 활성화될 수 없다 - -반응형 조건 애너테이션은 애플리케이션 형이 반응형일 때만 참이다. 클래스패스 추론은 서블릿 디스패처가 있으면 반응형 프레임워크가 함께 있어도 서블릿을 고른다. 이 저장소의 클래스패스가 서블릿을 고정한다. - -## 관계 - -- **플랫폼 요청 컨텍스트가 서블릿에는 생산자가 없고 리액티브에는 익명 액터로 고정되어 있다** - 이 사례가 그것을 완결한다. -- **리액티브 전송에는 속도 제한 경로가 하나도 없다** - 같은 리프의 반응형 사례다. -- **조건은 참이 될 수 있어야 조건이다** - 이 사례가 그 규칙의 형태다. - -## 문제 - -이 리프의 반응형 절반은 조건 애너테이션으로 활성화된다. - -그 조건이 이 저장소에서 참이 될 수 있는지 확인했다. - -## 결론 - -될 수 없다. - -조건은 애플리케이션 형이 반응형일 때만 참이다. - -클래스패스 추론 메서드는 서블릿 디스패처와 컨테이너 초기화기가 있으면 반응형 프레임워크가 함께 있어도 서블릿을 고른다. - -이 저장소의 클래스패스가 서블릿을 고정한다. 근거가 넷이다. - -이 리프의 빌드 파일이 서블릿 스타터를 구현 의존성으로 선언하고, 반응형은 서버 없는 프레임워크와 반응 라이브러리만 가져온다. 그 선택의 근거도 적혀 있다. 두 번째 내장 서버를 실행 클래스패스에 올리게 된다는 것이다. - -부트스트랩 잠금 파일에 서블릿 컨테이너와 서블릿 웹 항목이 있다. - -표본 애플리케이션의 빌드 파일도 서블릿 스타터를 선언한다. - -그리고 주 소스 어디에도 애플리케이션 형을 반응형으로 지정하는 코드가 없다. 테스트 하네스 세 곳에만 참조가 있고 전부 없음이나 서블릿이다. - -따라서 애플리케이션 형은 항상 서블릿이고, 이 리프의 모든 반응형 조건은 영구히 거짓이다. - -꺼진 채로 남는 것이 주 소스 스물아홉 파일이다. - -반응형 자동 설정 둘과 문맥 둘, 오류와 예산과 가드와 멱등과 연산과 조절 여덟, 파일 서버 반응형 열, 고급 반응형 일곱이다. - -그중 문맥 패키지가 결정적이다. 이 리프의 유일한 프로덕션 요청 컨텍스트 생산자가 거기 있다. - -그래서 요청 컨텍스트 사례가 이것으로 완결된다. - -서블릿에는 생산자가 없고 반응형에는 생산자가 있으나 그 절반이 켜지지 않는다. - -판정은 P1 이다. - -## 검증 환경 - -Spring Boot : 4.0.8 -확인 방식 : 클래스패스 추론 규칙 확인과 빌드 파일, 잠금 파일 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/176 계열에 있다. - -1. 반응형 조건 애너테이션이 무엇을 요구하는지 확인한다. -2. 클래스패스 추론 메서드의 우선순위를 확인한다. -3. 리프의 빌드 파일에서 서블릿 스타터 선언을 확인한다. -4. 부트스트랩과 표본의 잠금 파일과 빌드 파일을 확인한다. -5. 주 소스에서 애플리케이션 형 지정 코드를 검색한다. -6. 반응형 조건이 붙은 파일을 센다. - -## 본문 - - - -`@ConditionalOnWebApplication(type = REACTIVE)`는 Spring Boot의 `WebApplicationType`이 `REACTIVE`일 때만 참이다. `deduceFromClasspath()`는 `DispatcherServlet`과 `ServletContainerInitializer`가 있으면 WebFlux가 함께 있어도 `SERVLET`을 고른다. - -## WebApplicationType 참조 위치 - -:::evidence key="analysis-finding-a14-f016" alt="코드베이스에서 WebApplicationType 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="WebApplicationType 코드베이스 검색 — 3줄 · exit 0" zoom="true" -::: - -## 그래서 리액티브 절반 29개 파일이 활성화되지 않는다 - -어떤 출하 배포에서도 그렇다. - -## 확인하지 못한 것 - -애플리케이션을 띄워 반응형 빈이 없는 것을 확인하지 않았다. 추론 규칙과 클래스패스상 그 결과가 나온다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-analysis-finding-a14-f019.md b/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-analysis-finding-a14-f019.md deleted file mode 100644 index 6245836..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/web-inbound-and-http-surface/case/case-analysis-finding-a14-f019.md +++ /dev/null @@ -1,129 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a14-f019 -title: 크로스 스택 게이트가 검증하는 조립은 픽스처의 조립이고, 플랫폼의 조립이 아니다 -topic: web-inbound-and-http-surface -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a14-f019 -evidenceCapturedOn: 2026-09-01 -body: case-analysis-finding-a14-f019.body.md -assets: - - key: analysis-finding-a14-f019 - file: ../../../final/evidence/rendered/analysis-finding-a14-f019.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a14-f019.txt -source: - - 원본 분석 절은 final/document.md#a14#L1575 이다. ---- - -# 크로스 스택 게이트가 검증하는 조립은 픽스처의 조립이고, 플랫폼의 조립이 아니다 - -이 리프는 저장소에서 가장 정교한 검증 장치를 갖고 있다. 그 장치가 세우는 애플리케이션은 픽스처 애플리케이션이다. 증명되는 명제는 필터가 등록되면 계약이 지켜진다는 것이고, 플랫폼이 그 필터를 등록한다는 명제는 어떤 레인도 세우지 않는다. - -## 관계 - -- **용량 보호 계층 전체가 자기 테스트 픽스처 안에서만 실행된다** - 이 공백이 만든 결과 중 하나다. -- **멱등 실행 계층과 durable-operation 표면이 픽스처에서만 조립된다** - 같은 공백이 만든 다른 결과다. -- **능력이 올바른가와 플랫폼이 능력을 설치하는가는 다른 질문이다** - 이 사례가 그 규칙의 형태다. - -## 문제 - -이 리프는 저장소에서 가장 정교한 검증 장치를 갖고 있다. - -여섯 소스 집합과 다섯 사용자 정의 레인, 세 런타임 대칭 비교, 실제 역방향 프록시 컨테이너, 실제 소켓 고장 주입이다. - -그 장치가 무엇을 증명하는지 확인했다. - -## 결론 - -그 장치가 세우는 애플리케이션은 픽스처 애플리케이션이다. - -예산 픽스처 애플리케이션이 예산 필터의 등록 빈을 손수 등록한다. - -두 통합 테스트가 그 애플리케이션을 띄워 예산 계약을 세 런타임에서 증명한다. - -증명되는 명제는 이 필터가 등록되면 예산이 지켜진다는 것이다. - -플랫폼이 이 필터를 등록한다는 명제는 어떤 레인도 세우지 않는다. 그리고 플랫폼은 등록하지 않는다. - -같은 구조가 조절과 멱등과 서버 전송 이벤트와 요청 컨텍스트에 반복된다. - -빠진 검증은 하나다. - -두 플랫폼 자동 설정이 세운 컨텍스트에 무엇이 있는지 확인하는 테스트다. - -두 자동 설정 테스트가 존재하지만 자동 설정이 선언한 빈들을 확인할 뿐이다. 필터 사슬에 예산과 조절과 멱등이 있는지는 묻지 않는다. - -자동 설정이 그것들을 선언하지 않으므로 확인할 것도 없다. - -이 하위 범위에 P1 을 두는 이유는 이렇다. - -결함은 픽스처에 있지 않다. 픽스처는 정확하고 계약은 잘 쓰였다. - -결함은 검증 전략의 경계에 있다. - -이 리프는 능력이 올바른가를 다섯 레인으로 묻고, 플랫폼이 능력을 설치하는가를 묻는 레인을 하나도 갖지 않는다. - -그 공백이 다섯 개의 상위 발견이 초록 묶음 아래에서 성립할 수 있게 한 단일 원인이다. - -권고는 픽스처를 하나 더 만드는 것이 아니다. - -아무것도 등록하지 않는 픽스처를 하나 만드는 것이다. 부트 설정과 자동 설정 활성화만 있고 빈이 없는 애플리케이션을 띄워 필터 사슬과 컨트롤러 조언과 인터셉터 목록을 스냅숏으로 고정한다. - -그 스냅숏이 두 미등록 사례를 즉시 드러내고 이후 회귀도 막는다. - -이 리프에 기록과 비교 형태를 갖춘 계약 기록 장치가 이미 있으므로 형식은 있다. - -## 검증 환경 - -Spring Boot : 4.0.8 -확인 방식 : 레인이 띄우는 애플리케이션과 자동 설정 테스트 대상 확인 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/176 계열에 있다. - -1. 레인 목록과 각 레인이 띄우는 애플리케이션을 확인한다. -2. 픽스처 애플리케이션이 무엇을 손수 등록하는지 읽는다. -3. 두 자동 설정 테스트가 무엇을 단언하는지 확인한다. -4. 자동 설정이 선언한 빈 목록을 확인한다. -5. 필터 사슬 구성을 확인하는 테스트가 있는지 검색한다. - -## 본문 - - - -이 leaf는 이 저장소에서 가장 정교한 검증 장치를 갖고 있다 — 여섯 소스셋, 다섯 커스텀 레인, 세 런타임 패리티 비교, 실제 Nginx 컨테이너, 실제 소켓 고장 주입. - -## 이 leaf 가 갖춘 검증 장치 - -:::evidence key="analysis-finding-a14-f019" alt="분석 문서 final/document.md#a14 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a14 발췌 — 15줄" zoom="true" -::: - -## 증명되는 명제가 다르다 - -`BudgetFixtureApplication`이 `FilterRegistrationBean`를 손수 등록한다. `JettyWebBudgetIT` · `ReactiveWebBudgetIT`가 그 애플리케이션을 띄워 예산 계약을 세 런타임에서 증명한다. 증명되는 명제는 "**이 필터가 등록되면** 예산이 지켜진다"이고, "플랫폼이 이 필터를 등록한다"는 명제는 어떤 레인도 세우지 않는다 — 그리고 §16.1이 확인했듯 플랫폼은 등록하지 않는다. 같은 구조가 스로틀(§16.1) · 멱등성(§20.1) · SSE(§36.1) · 요청 컨텍스트(§12.1)에 반복된다. - -## 빠진 검증은 하나다 - -두 자동설정(`WebMvcPlatformAutoConfiguration` · `WebFluxPlatformAutoConfiguration`)이 세운 컨텍스트에 무엇이 있는지 확인하는 테스트. 두 `*AutoConfigurationTest`(각 112줄)가 존재하지만 자동설정이 **선언한** 빈들을 확인할 뿐, 필터 체인에 예산·스로틀·멱등성이 있는지는 묻지 않는다 — 자동설정이 그것들을 선언하지 않으므로 확인할 것도 없다. - -## 결함은 픽스처가 아니라 검증 전략의 경계에 있다 - -이 leaf는 "능력이 올바른가"를 다섯 레인으로 묻고 "플랫폼이 능력을 설치하는가"를 묻는 레인을 하나도 갖지 않는다. 그 공백이 §12.1 · §16.1 · §20.1 · §36.1 · §40.1 다섯 개의 P1/P2가 초록색 스위트 아래에서 성립할 수 있게 한 단일 원인이다. P1. - -## 권고 - -픽스처를 하나 더 만드는 것이 아니라, **아무것도 등록하지 않는** 픽스처를 하나 만든다 — `@SpringBootConfiguration` + `@EnableAutoConfiguration`만 있고 `@Bean`이 없는 애플리케이션을 띄워 필터 체인·컨트롤러 조언·인터셉터 목록을 스냅샷으로 고정한다. `WebPlatformContractRecording`이 이미 기록·비교 형태를 갖고 있으므로 형식은 있다. - -## 확인하지 못한 것 - -빈 픽스처를 만들어 스냅숏을 떠 보지 않았다. 문서 작업 범위에서 테스트를 추가하지 않는다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/websocket-and-realtime/case/case-a17-f002-stomp.md b/docs/clean-architecture-backend-template/tech-log-studio/websocket-and-realtime/case/case-a17-f002-stomp.md deleted file mode 100644 index dc25148..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/websocket-and-realtime/case/case-a17-f002-stomp.md +++ /dev/null @@ -1,145 +0,0 @@ ---- -kind: CASE -slug: a17-f002-stomp -title: 연결 티켓·origin 정책·메시지 권한·연결 예산이 요청 경로 밖이고, 그중 일부는 STOMP 어댑터가 다른 방식으로 대체한다 -topic: websocket-and-realtime -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a17-f002-stomp -evidenceCapturedOn: 2026-09-04 -body: case-a17-f002-stomp.body.md -assets: - - key: a17-f002-stomp - file: ../../../final/evidence/rendered/a17-f002-stomp.svg -evidence: - - ../../../final/evidence/raw/a17-f002-stomp.txt -source: - - 원본 분석 절은 final/document.md#a17#L273 이다. 등록 지점과 세 번째 인터셉터, 출하 그래프 부재, 정책 열 파일의 계수와 생성 지점, 그리고 22 의 출처는 위 정적 검색에서 확인할 수 있다. ---- - -# 연결 티켓·origin 정책·메시지 권한·연결 예산이 요청 경로 밖이고, 그중 일부는 STOMP 어댑터가 다른 방식으로 대체한다 - -`WebSocketConfig` 가 `AuthenticatedHandshakeInterceptor` 와 `WebSocketInboundAuthorizationInterceptor` 를 등록한다. `security`·`authz`·`idempotency`·`budget` 네 패키지의 정책 타입 열 개는 `stomp` 패키지가 한 번도 참조하지 않는다. 다만 이 모듈 자체가 두 출하 런타임 그래프에 모두 없어서, 어느 쪽도 지금 도는 코드는 아니다. - -## 관계 - -- **프레임워크 자유 신원 모델과 교차 테넌트 가드가 프로덕션에서 한 번도 참조되지 않는다** - 둘 다 보안 정책 타입 묶음의 프로덕션 참조가 0 이고, 실제로 도는 검사는 프레임워크 설정 쪽에 따로 있다. -- **능력 프로퍼티 이름을 만드는 코드와 실제 게이트가 다른 접두사를 쓴다** - 같은 리프에서, 선언된 이름이나 타입을 읽는 코드가 실제 등록 경로에 없다. 저쪽은 능력 이름을 읽는 조건이 없고, 여기는 정책 열 타입을 참조하는 코드가 `stomp` 에 없다. -- **중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다** - 인바운드 권한을 정하는 구현이 둘인데, `WebSocketConfig` 가 등록하는 것은 `WebSocketInboundAuthorizationInterceptor` 뿐이고 `MessageAuthorizationPolicy` 는 `validate` 에 실려 가지도 않는다. - -## 문제 - -인바운드 보안을 정하는 코드가 이 리프에 두 벌 있다. - -어느 쪽이 채널에 등록되는지, 등록되지 않는 쪽이 어디까지 소비되는지, 그리고 그 대비가 어느 배포에서 관측되는지 확인했다. - -## 결론 - -어댑터가 등록하는 것은 인터셉터 둘이다. - -핸드셰이크에 AuthenticatedHandshakeInterceptor(34줄)가 WebSocketConfig:53 에서, 클라이언트 인바운드 채널에 WebSocketInboundAuthorizationInterceptor(53줄)가 :59 에서 붙는다. 그 설정 클래스는 ca-skeleton.websocket.enabled 조건 아래에 있다. - -같은 채널에 붙는 인터셉터가 하나 더 있다. advanced/stomp/StompSecurityInterceptor(96줄)를 StompConfiguration:40 이 등록하고, 게이트는 app.websocket-platform.advanced.stomp.enabled 다. - -정책 계층은 네 패키지에 열 파일이다. security 넷은 WebSocketOriginPolicy(116줄), WebSocketConnectionTicket(77줄), WebSocketAuthenticationProfile(57줄), WebSocketTicketStore(32줄), authz 하나는 MessageAuthorizationPolicy(93줄), budget 하나는 WebSocketConnectionBudget(103줄), idempotency 넷은 CommandReconciliation(84줄)·CommittedResultLedger(61줄)·WebSocketCommandKey(40줄)·WebSocketCommandOutcome(33줄)이다. - -그 열 개 중 stomp 쪽이 이름이라도 언급하는 것은 하나도 없다. 검색 범위를 리프 전체로 넓히면 WebSocketOriginPolicy 와 WebSocketConnectionBudget 과 MessageAuthorizationPolicy 만 각각 한 자리에서 만들어진다. - -MessageAuthorizationPolicy 는 한 단 더 간다. 그것을 받는 main 코드는 WebSocketPlatformStartupValidator.validate 의 파라미터 하나인데, 그 검증기의 생성자는 불리언 하나를 받고 생성 지점은 자기 시험 두 줄뿐이다. 이 정책을 validate 에 실어 보내는 코드가 없다. - -노출로 볼 근거는 없다. 다만 근거는 문서 정합이 아니라 배선이다. app-bootstrap/build.gradle:196~:200 과 리프 CLAUDE.md:21~:23 이 이 모듈은 두 출하 런타임 그래프에 모두 없다고 적는다. 넓은 쪽만 꺼져 있는 것이 아니라 좁은 쪽도 지금은 돌지 않는다. - -기록하는 것은 한 리프에 인바운드 보안 코드가 두 벌 있고, stomp 가 정책 열 타입을 한 번도 참조하지 않으며, 그중 MessageAuthorizationPolicy 는 넘겨지는 자리조차 없다는 것이다. - -판정은 P2 이고 원문과 같다. 다만 원문 §13.1 이 정책 계층을 22 파일로 적은 것과 달리 실제로 센 것은 10 파일이고, 22 는 sub-scope 04 여덟 패키지의 main 합계다. - -## 검증 환경 - -확인 방식 : 어댑터와 advanced 쪽 인터셉터의 등록 지점과 게이트 확인, 출하 런타임 그래프 포함 여부 확인, 정책 네 패키지의 파일·선언 형태·줄 수 전수, 열 타입의 stomp 참조와 main 생성 지점 계수, MessageAuthorizationPolicy 의 공개 멤버와 소비 사슬 추적, 원문이 적은 22 의 출처 대조, 리프 CLAUDE.md 의 범위와 인바운드 정책 절 확인 -소스 수정 : x - -## 재현 조건 - -1. WebSocketConfig 에서 인터셉터를 만들고 등록하는 줄과 조건 애너테이션을 읽는다. -2. advanced/stomp 에서 같은 채널에 등록되는 인터셉터와 그 게이트를 찾는다. -3. app-bootstrap 빌드 파일과 리프 CLAUDE.md 에서 이 모듈의 출하 여부를 읽는다. -4. security·authz·idempotency·budget 네 패키지의 파일을 이름·선언 형태·줄 수까지 전부 센다. -5. 4에서 나열한 이름들을 stomp 쪽에서 되짚고, 같은 이름의 main 생성 지점도 함께 센다. -6. MessageAuthorizationPolicy 의 공개 멤버를 나열하고, 그것을 받는 main 코드와 그 코드의 생성자 시그니처와 생성 지점을 차례로 확인한다. -7. 원문이 22 로 적은 수가 어느 헤더에서 왔는지 찾고, 그 헤더가 나열한 패키지를 전부 세어 대조한다. -8. 리프 CLAUDE.md 의 범위 절과 인바운드 정책 절을 끝까지 읽는다. - -## 본문 - - - -이 리프에는 인바운드 보안을 정하는 코드가 두 벌 있다. 어댑터가 등록하는 인터셉터들과, `security`·`authz`·`idempotency`·`budget` 네 패키지의 정책 타입들이다. - -## 등록되는 인터셉터와 그 게이트 - -:::evidence key="a17-f002-stomp" alt="저장소 루트에서 돌린 정적 검색 출력 98줄. ca-skeleton.websocket 어댑터가 등록하는 인터셉터 둘의 줄 수와 WebSocketConfig 의 조건 애너테이션·필드 생성·addInterceptors·configureClientInboundChannel 줄이 먼저 나온다. 이어서 같은 채널에 붙는 세 번째 인터셉터 StompSecurityInterceptor 와 그것을 등록하는 StompConfiguration, 그리고 그쪽의 별도 프로퍼티 게이트가 나온다. 그다음 이 모듈이 두 출하 런타임 그래프에 모두 없다는 app-bootstrap 빌드 파일의 주석과 리프 CLAUDE.md 의 같은 서술이 이어진다. 정책 계층 열 파일이 패키지·이름·선언 형태·줄 수로 나열되고 합계가 10 으로 찍힌다. 그다음 열 타입 각각에 대해 stomp 참조 수와 main 생성 수와 같은 파일의 @Bean 유무가 표로 나오는데 stomp 참조는 전부 0 이고 main 생성은 셋만 1 이다. 이어서 원문이 22 로 적은 수가 sub-scope 04 여덟 패키지의 main 합계라는 것이 헤더와 계수로 확인되고, MessageAuthorizationPolicy 의 공개 메서드 여섯과 그것을 받는 자리, 그 받는 쪽의 생성자와 생성 지점이 나온다. 끝으로 리프 CLAUDE.md 의 범위 네 줄과 인바운드 정책 절 다섯 규칙이 보인다." caption="어댑터 인터셉터 둘과 세 번째 인터셉터 · 두 출하 그래프에 모두 없음 · 정책 열 파일과 stomp 참조 0 · 22 의 출처 · 검증기의 두 시그니처 · CLAUDE.md 범위 — 98줄 · exit 0" zoom="true" -::: - -`WebSocketConfig` 가 `AuthenticatedHandshakeInterceptor`(34줄)를 `:36`\~`:37` 에서 만들어 `:53` 의 `.addInterceptors` 로 핸드셰이크에 붙이고, `WebSocketInboundAuthorizationInterceptor`(53줄)를 `:44` 에서 만들어 `:59` 의 `configureClientInboundChannel` 에서 클라이언트 인바운드 채널에 등록한다. 이 설정 클래스는 `:32` 의 `@ConditionalOnProperty(prefix = "ca-skeleton.websocket", name = "enabled", havingValue = "true")` 아래에 있다. - -인터셉터가 둘만은 아니다. `advanced/stomp/StompSecurityInterceptor`(96줄)가 `StompConfiguration:40` 에서 **같은 클라이언트 인바운드 채널에** 등록된다. 게이트는 다른 프로퍼티 접두사다 — `:24`\~`:27` 의 `app.websocket-platform.advanced.stomp.enabled`. 아래에서 어댑터 쪽 둘을 셀 때 이 세 번째는 포함하지 않는다. - -## 어느 쪽도 출하 배포에서 돌지 않는다 - -`app-bootstrap/build.gradle:196`\~`:200` 이 이 리프를 `conditionalTransportTestImplementation` 으로만 물면서 주석에 적는다 — 이 프로젝트들은 main 의 `api`/`implementation`/`compileOnly`/`runtimeOnly` 에 없고 따라서 두 출하 런타임 그래프에서도 빠진다. 리프의 `CLAUDE.md:21`\~`:23` 이 같은 것을 다시 적는다. 등록되고 시험된다고 활성화되는 것이 아니며, 앞으로의 컴포지션이 의존을 의도적으로 추가하고 프로퍼티를 켜야 한다는 것이다. - -따라서 아래의 대비는 이 모듈을 컴포지션에 넣고 프로퍼티를 켠 배포를 가정한 것이다. - -## 정책 계층 열 파일 - -`security` 넷은 `WebSocketOriginPolicy`(final class, 116줄), `WebSocketConnectionTicket`(record, 77줄), `WebSocketAuthenticationProfile`(enum, 57줄), `WebSocketTicketStore`(interface, 32줄)다. `authz` 하나가 `MessageAuthorizationPolicy`(final class, 93줄), `budget` 하나가 `WebSocketConnectionBudget`(record, 103줄)이다. `idempotency` 넷 중 인터페이스가 둘(`CommandReconciliation` 84줄, `CommittedResultLedger` 61줄), 나머지가 `WebSocketCommandKey`(record, 40줄)와 `WebSocketCommandOutcome`(enum, 33줄)이다. - -열 타입의 이름을 `stomp` 패키지 안에서 하나씩 찾으면 전부 0 건이다. 어댑터가 등록하는 두 인터셉터는 이 타입들을 알지 못한다. - -다만 리프 전체로 넓히면 셋은 main 에서 생성된다. `WebSocketOriginPolicy`, `WebSocketConnectionBudget`, `MessageAuthorizationPolicy` 가 각각 한 자리씩이다. 그 자리들이 요청 경로에 오르는지까지는 이 검색이 답하지 않는다. - -## MessageAuthorizationPolicy 를 넘기는 코드가 없다 - -`MessageAuthorizationPolicy` 의 공개 멤버는 여섯이다. `of`(\:39), `permits`(\:57), `declares`(\:70), `undeclaredAmong`(\:81), `requirements`(\:90), 그리고 클래스 자신이다. - -이것을 자기 파일 밖에서 받는 main 코드는 `WebSocketPlatformStartupValidator` 하나다. 다만 생성자가 아니라 `validate(...)` 의 파라미터(`:56`)이고, 본문 `:106` 이 `undeclaredAmong` 을 부른다. - -그 검증기의 생성자는 `:38` 의 `WebSocketPlatformStartupValidator(boolean productionProfile)` 다. 그것을 생성하는 두 줄은 `WebSocketPlatformStartupValidatorTest:40`·`:42` 이고 넘기는 인자는 불리언이다. 즉 이 정책 객체를 `validate` 에 실어 보내는 코드는 main 에도 test 에도 나오지 않았다. - -## 등록된 인터셉터가 강제하는 것과 문서가 적은 것 - -리프 `CLAUDE.md:15`\~`:18` 이 이 모듈의 범위를 네 줄로 적고, 그중 `:16` 이 `"HTTP-handshake principal enforcement and client-inbound STOMP destination authorization"` 이다. - -`:52`\~`:60` 의 인바운드 정책 절은 규칙 다섯이다. HTTP 업그레이드에 비어 있지 않은 `Principal` 이 이미 있어야 하고 어댑터가 자격을 인증하지는 않는다, `SUBSCRIBE` 는 설정된 브로드캐스트 목적지에만 허용한다, 인증된 `SEND` 는 `/app/**` 아래만 허용한다, `/topic/**` 로 가는 클라이언트 `SEND` 는 거부한다, 그리고 클라이언트가 보는 모든 처리 실패는 고정된 `WEBSOCKET_REQUEST_REJECTED` ERROR 코드가 된다. - -앞의 넷이 두 인터셉터의 일이다. 다섯째는 `SafeStompSubProtocolErrorHandler` 가 맡는다. - -## 원문과 갈리는 자리 - -원문 §13.1 은 플랫폼 정책 계층을 22 파일로 적었다. 22 는 같은 문서 `:235` 의 sub-scope 04 헤더가 적은 수다 — `security`·`authz`·`idempotency`·`budget`·`error`·`observability`·`admin`·`release` 여덟 패키지의 main 합계이고, 세어 보면 4+1+4+1+5+2+2+3 = 22 다. §12.1 이 정책 계층으로 한정해 열거한 것은 앞의 네 패키지 10 파일이다. §13.1 이 sub-scope 계수를 정책 계층 계수 자리에 옮겨 썼다. - -원문 §12.2 는 `MessageAuthorizationPolicy` 의 등록을 "없음" 으로 적었다. 등록되지 않는다는 결론은 같지만, 그것을 `validate` 파라미터로 받는 main 코드가 하나 있고 그 검증기 자신이 다시 생성되지 않는다는 두 단은 그 표에 없다. - -원문 §18.1 은 `StompConfiguration` 을 `@Bean` 0 이고 리프 타입 import 가 없어 프레임워크 설정만 조정하는 부류로 봤다. 같은 패키지라 import 문이 없을 뿐, 생성자로 `StompSecurityInterceptor` 를 받아 인바운드 채널에 등록한다. - -그리고 원문 §13.1 의 "실제 배포에서 적용되는 보안은 `stomp` 패키지의 두 인터셉터" 는 이 리비전에서 성립하지 않는다. 그 두 인터셉터도 출하 런타임 그래프에 없다. - -## 확인하지 못한 것 - -WebSocket 클라이언트를 붙여 구독과 전송을 시도해 보지 않았다. 확인한 것은 어느 코드가 등록되고 어느 이름이 어디서 참조·생성되는가까지다. - -리프 밖 main 에서 세 타입을 생성하는 자리가 요청 경로에 오르는지는 추적하지 않았다. 나머지 일곱 타입의 리프 밖 참조도 세지 않았다. - -제목이 말하는 "다른 방식으로 대체한다" 중 이 기록이 확인한 것은 목적지 허용 목록 하나다. 연결 티켓과 출처 정책과 연결 예산을 어댑터가 대신하는지는 보지 않았다. - -WebSocket 클라이언트를 붙여 구독과 전송을 시도해 보지 않았다. 확인한 것은 어느 코드가 등록되고 어느 이름이 어디서 참조·생성되는가까지다. - -리프 밖 main 에서 세 타입을 생성하는 자리가 요청 경로에 오르는지는 추적하지 않았다. 나머지 일곱 타입의 리프 밖 참조도 세지 않았다. - -넓은 모델을 배선했을 때 좁은 모델과 어떤 충돌이 생기는지도 보지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/websocket-and-realtime/case/case-analysis-finding-a17-f003.md b/docs/clean-architecture-backend-template/tech-log-studio/websocket-and-realtime/case/case-analysis-finding-a17-f003.md deleted file mode 100644 index 9f09838..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/websocket-and-realtime/case/case-analysis-finding-a17-f003.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -kind: CASE -slug: analysis-finding-a17-f003 -title: 능력 프로퍼티 이름을 만드는 코드와 실제 게이트가 다른 접두사를 쓴다 -topic: websocket-and-realtime -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:analysis-finding-a17-f003 -evidenceCapturedOn: 2026-09-01 -assets: - - key: analysis-finding-a17-f003 - file: ../../../final/evidence/rendered/analysis-finding-a17-f003.svg -evidence: - - ../../../final/evidence/raw/analysis-finding-a17-f003.txt -source: - - 원본 분석 절은 final/document.md#a17#L426 이다. ---- - -# 능력 프로퍼티 이름을 만드는 코드와 실제 게이트가 다른 접두사를 쓴다 - -능력 열거형의 속성 이름 메서드가 한 이름공간의 키를 만든다. 그 키를 읽는 조건 애너테이션이 없다. 실제로 능력을 켜는 키는 다른 이름공간에 있다. - -## 관계 - -- **속성 이름 메서드가 아무것도 게이트하지 않는 이름을 반환한다** - 다른 리프의 같은 형태다. -- **연결 티켓과 origin 정책과 메시지 권한과 연결 예산이 요청 경로 밖이고 일부는 다른 방식으로 대체된다** - 같은 리프의 상위 사실이다. -- **운영자가 문서를 보고 설정하면 켜지지 않는다** - 기록하는 이유다. - -## 문제 - -고급 능력 열거형이 속성 이름을 계산하는 메서드를 가진다. - -그 이름이 실제 게이트와 같은지 확인했다. - -## 결론 - -다르다. - -그 메서드가 반환하는 이름공간의 키를 읽는 조건 애너테이션이 없다. - -운영자가 그 메서드가 알려 주는 키를 설정하면 아무 일도 일어나지 않는다. - -실제로 능력을 켜는 키는 다른 접두를 쓰는 이름공간에 있다. - -이것은 그 이름공간 전체가 소비자를 갖지 않는다는 상위 사실의 부분집합이다. - -판정은 P3 다. - -## 검증 환경 - -확인 방식 : 속성 이름 메서드 반환값과 조건 애너테이션 대조 -소스 수정 : x - -## 재현 조건 - -원문은 final/evidence/raw/185 계열에 있다. - -1. 능력 열거형의 속성 이름 메서드를 읽는다. -2. 그 이름공간의 키를 읽는 조건 애너테이션을 검색한다. -3. 실제로 능력을 켜는 키를 확인한다. -4. 두 이름공간의 접두를 대조한다. - -## 본문 - - - -`WebSocketAdvancedCapability.propertyName()`이 반환하는 `backend.websocket.advanced.*`를 읽는 `@ConditionalOnProperty`가 없다(§21.2). - -## propertyName() 이 반환하는 접두사 - -:::evidence key="analysis-finding-a17-f003" alt="분석 문서 final/document.md#a17 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a17 발췌 — 15줄" zoom="true" -::: - -## 운영자가 그 키를 설정하면 아무 일도 일어나지 않는다 - -실제로 능력을 켜는 키는 `app.websocket-platform.advanced.*`다. - -## 확인하지 못한 것 - -그 키를 설정하고 띄워 아무 일도 일어나지 않는 것을 재현하지 않았다. 소비자 부재상 그 결과가 나온다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-a-catalog-nine-entries-short.md b/docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-a-catalog-nine-entries-short.md deleted file mode 100644 index 5d5d9d8..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-a-catalog-nine-entries-short.md +++ /dev/null @@ -1,119 +0,0 @@ ---- -kind: CASE -slug: a-catalog-nine-entries-short -title: 패키지 카탈로그가 트리보다 아홉 개 적었고, 선언된 간선의 DAG 검사도 없었다 -topic: what-a-gate-does-not-prove -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a-catalog-nine-entries-short -evidenceCapturedOn: 2026-09-02 -assets: - - key: a-catalog-nine-entries-short - file: ../../../final/evidence/rendered/a-catalog-nine-entries-short.svg -evidence: - - ../../../final/evidence/raw/a-catalog-nine-entries-short.txt -source: - - 원본 분석 절은 final/document.md#a05 §115 이고, 리프가 단일 Gradle 모듈이 된 배경은 §1 에 있다. 카탈로그 크기와 사이클은 `JpaModuleBoundaryTest` 의 javadoc 과 DAG 테스트 단언 메시지가 사후 기록으로 소유하며, 값 쪽 잔여 설정의 근거는 evidence/raw/110-governance-doc-count-drift.txt §I 이다. ---- - -# 패키지 카탈로그가 트리보다 아홉 개 적었고, 선언된 간선의 DAG 검사도 없었다 - -모듈 경계를 강제하는 테스트가 두 가지를 놓치고 있었다. 패키지 카탈로그가 열세 개를 담은 채 트리에는 스물두 개가 있어서 아홉 개가 아무 규칙의 지배도 받지 않았고, 그 안의 새 패키지나 새 간선은 빠뜨림으로 초록불이었다. 그리고 선언된 간선이 DAG 를 이루는지 보는 검사가 없어서 `transaction` 과 `postgresql` 사이의 두 간선이 그대로 있었다. - -## 관계 - -- **빠뜨림이 통과가 되는 게이트는 게이트가 아니다** - 이 사례가 그 규칙을 만든 형태다. -- **@Bean이 있다는 것은 조립 증거가 아니다** - 빈 선언만으로 조립을 단정하지 말라는 확인 규칙이다. - -## 문제 - -설계상 여러 모듈인 이 리프가 한 리프 안의 패키지 묶음이 된 것은 저장소의 fail-closed 레지스트리가 배치보다 앞서기 때문이다. - -패키지는 그 자체로 아무것도 강제하지 않는다. 그래서 테스트가 모듈 의존 맵을 패키지 규칙으로 강제한다. 테스트가 빠지면 모듈 맵은 강제력을 잃는다. 경계를 넘는 임포트가 아무 저항 없이 컴파일된다. - -그리고 이 검사는 레지스트리 게이트가 대신해 주지 않는다. 레지스트리 게이트는 등록된 리프 사이의 간선을 다루는데, 여기서 단언하는 모든 간선은 한 리프 안에 있어 그 게이트에 보이지 않는다. - -## 결론 - -두 결함이고 원인이 다르다. - -카탈로그가 트리보다 아홉 개 적었다. 지배받지 않던 아홉 개는 audit · config · failure · fileserver · h2 · idempotency · lock · notification · outbox 다. 그 안에서 새로 생긴 패키지와 간선은 허가를 받은 것이 아니라 아무도 묻지 않아서 통과했다. - -사이클은 그것과 무관하다. 빠진 목록에 transaction 과 postgresql 이 들어 있지 않다. 둘 다 카탈로그 안에 있었고 간선도 선언돼 있었는데, 선언된 간선들이 순환을 이루는지 보는 검사가 없었다. - -수정도 둘이다. 카탈로그를 디스크의 실제 트리와 정확히 같은지 양방향으로 대조하게 만들었고, 선언된 간선이 DAG 인지 보는 테스트를 따로 두었다. 소스 루트를 찾지 못했을 때 스캔이 빈 집합으로 통과해 버리는 것을 막는 방어도 함께 들어갔다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Gradle : 9.0.0 -확인 방식 : 테스트 파일의 javadoc 과 단언 메시지 확인, 카탈로그 키와 빠진 아홉 개 대조 -소스 수정 : x - -## 재현 조건 - -1. JpaModuleBoundaryTest 의 PACKAGE_CATALOG 위 javadoc 을 읽는다. 이전 카탈로그 크기와 트리 크기와 빠진 아홉 개가 이름으로 적혀 있다. -2. 그 아홉 개에 transaction 과 postgresql 이 없다는 것과, 둘이 PACKAGE_CATALOG 의 키라는 것을 확인한다. -3. theDeclaredEdgesFormADag 의 단언 메시지를 읽는다. 두 간선이 모두 존재했다는 것이 거기 적혀 있다. -4. 카탈로그가 존재하는 패키지를 정확히 이름 대는지 검사하는 theCatalogNamesExactlyThePackagesThatExist 를 확인한다. -5. 소스 루트를 찾지 못할 때 스캔으로 통과하지 않도록 막는 방어를 확인한다. - -## 본문 - - - -이 리프는 설계상 여러 모듈인데 저장소의 fail-closed 레지스트리가 그 배치보다 우선해서, 모듈이 한 리프 안의 패키지가 되었다. 패키지는 그 자체로 아무것도 강제하지 않으므로 경계 테스트가 모듈 의존 맵을 패키지 규칙으로 대신 강제한다. - -그 테스트가 두 가지를 놓치고 있었고, 둘은 서로 다른 결함이다. - -## 카탈로그가 트리보다 아홉 개 적었다 - -:::evidence key="a-catalog-nine-entries-short" alt="코드베이스에서 JpaModuleBoundaryTest 의 두 구간을 잘라낸 출력 30줄. 이전 카탈로그가 열세 개이고 트리가 스물두 개였다는 javadoc 과, 사이클을 기록한 DAG 테스트의 단언 메시지가 그 출력에 그대로 보인다." caption="JpaModuleBoundaryTest — PACKAGE_CATALOG javadoc · DAG 테스트 — 30줄 · exit 0" zoom="true" -::: - -카탈로그는 닫힌 집합이어야 의미가 있는데, 열세 개를 담은 채 트리에는 스물두 개가 있었다. javadoc 이 빠진 아홉 개를 이름으로 적는다. - -- audit -- config -- failure -- fileserver -- h2 -- idempotency -- lock -- notification -- outbox - -이 아홉 개 안의 새 패키지나 새 간선은 규칙이 거부한 것이 아니라 규칙이 아무 말도 하지 않아서 초록불이었다. javadoc 의 표현이 그것이다 — "green by omission". - -## 사이클은 다른 이유로 통과했다 - -`transaction` 과 `postgresql` 은 빠진 아홉 개에 없다. 둘 다 카탈로그 안에 있었고 각자의 간선도 선언돼 있었다. 그런데 선언된 간선들이 순환을 이루는지 보는 검사가 없었다. - -DAG 테스트의 단언 메시지가 그 상태를 기록한다 — 두 간선이 모두 존재했고, 그래서 어느 쪽도 먼저 풀지 않고는 추출하거나 교체할 수 없었다. - -앞의 것은 규칙의 적용 범위가 좁았던 것이고, 뒤의 것은 선언된 범위 안에서 검사 항목이 하나 없었던 것이다. - -## 레지스트리 게이트가 대신해 주지 않는다 - -저장소에는 리프 사이의 간선을 보는 fail-closed 레지스트리 게이트가 따로 있다. 여기서 단언하는 간선은 전부 한 리프 안에 있어서 그 게이트에 보이지 않는다. 이 테스트가 없으면 모듈 맵은 제약이 아니라 그림이고, 그것을 넘는 첫 임포트가 그냥 컴파일된다. - -## 지금은 네 검사가 서로 다른 실패를 막는다 - -`theCatalogNamesExactlyThePackagesThatExist` 가 카탈로그와 디스크를 양방향으로 대조하고, `everyObservedEdgeIsDeclared` 가 관측된 간선이 선언된 것인지 보고, `theDeclaredEdgesFormADag` 가 선언된 간선이 순환하는지 본다. - -네 번째는 `importActuallyLoadedTheProductionClasses` 다. ArchUnit 의 `noClasses()` 규칙이 아무것도 매칭하지 않아 공허하게 통과하는 실패 모드를 막는다 — 규칙 모음에서 가장 자주 조용히 무너지는 지점이다. - -## 값 쪽은 아직 키 쪽만큼 검증되지 않는다 - -`postgresql` 의 허용 대상에 `"inbox"` 가 들어 있는데 그 이름의 top-level 패키지는 존재하지 않는다. 실제 inbox 는 `postgresql` 하위 패키지라서 최상위 이름을 돌려주는 함수가 언제나 `postgresql` 을 준다. - -`theCatalogNamesExactlyThePackagesThatExist` 는 키만 대조하므로 이런 값은 잡히지 않는다. 방향은 안전한 쪽이다 — 존재하지 않는 이름은 규칙을 더 엄격하게 만들 뿐 느슨하게 만들지 않는다. 그래서 결함이 아니라 잔여 설정이다. - -## 확인하지 못한 것 - -그 패키지들 안에 규칙 위반 간선이 실제로 있었는지까지는 보지 않았다. 이 기록이 든 결함은 규칙이 없다는 것이지 특정 위반이 아니다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-a-certifying-lane-that-compared-nothing.md b/docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-a-certifying-lane-that-compared-nothing.md index e5e3191..0f8566d 100644 --- a/docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-a-certifying-lane-that-compared-nothing.md +++ b/docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-a-certifying-lane-that-compared-nothing.md @@ -1,7 +1,7 @@ --- kind: CASE slug: a-certifying-lane-that-compared-nothing -title: '"certified"라 불리던 레인이 threshold를 하나도 비교하지 않고 있었다' +title: "certified"라 불리던 레인이 threshold를 하나도 비교하지 않고 있었다 topic: what-a-gate-does-not-prove project: clean-architecture-backend-template status: 게시 전 diff --git a/docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-a05-f012-identity.md b/docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-a05-f012-identity.md deleted file mode 100644 index 4286400..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-a05-f012-identity.md +++ /dev/null @@ -1,152 +0,0 @@ ---- -kind: CASE -slug: a05-f012-identity -title: 상위 클래스까지 올라가는 탐색이 게터는 읽지 않는다 -topic: what-a-gate-does-not-prove -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a05-f012-identity -evidenceCapturedOn: 2026-09-02 -body: case-a05-f012-identity.body.md -assets: - - key: a05-f012-identity - file: ../../../final/evidence/rendered/a05-f012-identity.svg - - key: a05-f012-identity-probe - file: ../../../final/evidence/rendered/a05-f012-identity-probe.svg -evidence: - - ../../../final/evidence/raw/a05-f012-identity.txt - - ../../../final/evidence/raw/a05-f012-identity-probe.txt -source: - - 원본 분석 절은 `final/document.md#a05` §38 이다. 탐지 거짓과 검증 통과라는 탐침 결과, 현재 출하 엔티티가 이 결함을 밟는다는 증거가 없다는 판정, 그리고 세 가지 수정 방향이 그 절에 있다. 등급은 P2 이고 이유는 제공자 가드의 정확성과 채택 안전이다. - - 그 절의 원본 탐침은 한 리비전 앞에서 돌았고 엔티티에 `@Entity` 가 없었다. 여기서는 현재 리비전에서 `@Entity` 를 붙여 다시 돌리고, 하이버네이트 부트스트랩까지 더했다. ---- - -# 상위 클래스까지 올라가는 탐색이 게터는 읽지 않는다 - -배치가 필요한 프로파일에서 IDENTITY 전략을 거절하는 가드가 선언된 필드만 읽는다. JPA 가 똑같이 허용하는 프로퍼티 접근으로 같은 매핑을 선언하면 통과한다. 하이버네이트는 그 매핑을 필드 접근과 똑같이 읽으므로, 통과한 것은 가드가 막겠다고 쓴 바로 그 동작이다. - -## 관계 - -- **접두사 시작 매칭은 시그니처에는 맞고 스니핑 패턴에는 맞지 않는다** - 탐지 방법이 대상의 실제 형태를 못 덮는다는 점이 같다. -- **@Bean이 있다는 것은 조립 증거가 아니다** - 같은 리프의 확인 규칙이다. - -## 문제 - -IDENTITY 컬럼의 값은 삽입할 때 데이터베이스가 붙인다. 하이버네이트는 키를 받아야 영속성 컨텍스트에 넣을 수 있어서 삽입을 하나씩 바로 보낸다. hibernate.jdbc.batch_size 값과 무관하다. - -가드의 javadoc 이 그 조용함을 문제로 지목한다. 설정은 맞아 보이고 임포트는 돌아가며, 증상은 예상보다 열 배쯤 느리다는 것뿐이다. 배치가 필요하다고 선언한 프로파일은 그것을 기동 실패로 바꾼다. - -## 결론 - -탐지는 @Id 가 붙은 필드를 찾는 것으로 시작한다. 탐색 범위가 상위 클래스까지인 이유는 javadoc 에 적혀 있다. 선언 클래스에서 멈추면 매핑된 상위 클래스가 식별자를 든 흔한 모양을 전부 IDENTITY 아님으로 분류하게 되고, 가드는 자기가 쓰인 이유인 엔티티들을 통과시킨다는 것이다. - -javadoc 이 든 근거는 프로퍼티 접근에도 그대로 성립한다. 탐색이 도는 것은 getDeclaredFields() 하나다. - -같은 IDENTITY 매핑을 두 방식으로 선언해 컴파일된 가드에 넘겼다. 필드 접근은 탐지 참, 검증 거절이다. 프로퍼티 접근은 탐지 거짓, 검증 통과다. - -그 통과가 무해한지 보려고 하이버네이트를 직접 세워 재 봤다. 7.2 메타모델은 앞의 것에 필드를, 뒤의 것에 메서드를 식별자 멤버로 잡는다. 배치 크기를 50 으로 두고 200행을 넣으면 둘 다 프리페어드 스테이트먼트가 200 개다. 같은 조건의 시퀀스 엔티티는 6 개다. 프로퍼티 접근 쪽도 배치가 꺼진다. - -가드를 부르는 프로덕션 코드는 없다. main 에서 이 타입을 만들거나 임포트하는 파일도, 같은 패키지의 배치 프로파일 레지스트리를 읽는 코드도 없다. 계획이 약속한 산출물은 기동 진단인데, 정작 기동에서 이것을 부르는 자리가 없다. - -막아야 할 대상 자체가 main 에서 사라졌다. GenerationType.IDENTITY 가 main 에 나오는 자리는 셋인데 전부 가드 자신 안이다. @GeneratedValue 자체가 main 에 없다. 출하되는 엔티티는 식별자를 애플리케이션에서 붙인다. - -접근 방식으로 갈리는 일도 없다. main 의 @Entity 서른둘 가운데 필드에 @Id 를 단 것이 스물일곱이다. @EmbeddedId 를 쓰는 것이 하나 있다. 게터에 @Id 를 단 곳도, 접근 방식을 뒤집는 @Access 도 없다. - -남는 것은 계약이다. 이 클래스는 IDENTITY 를 닫힌 방식으로 거절한다고 문서화한 일반 JPA 플랫폼 가드다. 채택자가 프로퍼티 접근을 쓰면 그 문장이 성립하지 않는다. - -수정 방향은 셋이다. JPA 메타모델로 실제 식별자 속성과 접근 전략을 해석하거나, 필드와 게터를 모두 검사하되 중복과 재정의 규칙까지 JPA 접근 의미와 맞추거나, 지원 매핑을 필드 접근으로 제한하고 그 제한을 아키텍처 규칙으로 강제하는 것이다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Hibernate ORM : 7.2.24.Final -확인 방식 : 컴파일된 가드에 두 접근 방식 투입, 하이버네이트 부트스트랩과 문장 계수, 참조와 매핑 계수 -소스 수정 : x - -## 재현 조건 - -1. 가드의 탐지 메서드와 그 아래 식별자 탐색 메서드를 읽는다. -2. 같은 IDENTITY 매핑을 필드 접근과 프로퍼티 접근으로 각각 선언한다. -3. 배치를 요구하는 프로파일과 함께 컴파일된 가드에 둘 다 넘긴다. -4. 같은 두 매핑에 시퀀스 엔티티를 더해 하이버네이트를 세우고, 200행을 넣어 프리페어드 스테이트먼트를 센다. -5. 그 가드를 만들거나 임포트하는 프로덕션 코드를 센다. -6. main 의 @Entity 와 @GeneratedValue 와 @Access 를 각각 센다. - -## 본문 - - - -IDENTITY 컬럼은 값을 데이터베이스가 삽입 시점에 정한다. 하이버네이트는 그 키를 알아야 영속성 컨텍스트에 넣을 수 있으므로 삽입을 하나씩 즉시 실행한다. `hibernate.jdbc.batch_size` 를 얼마로 잡든 마찬가지다. - -배치가 필요하다고 선언한 프로파일에서 `HibernateBatchConfigurationGuard` 는 그런 엔티티를 거절한다. - -## 식별자 탐색은 getDeclaredFields() 만 읽는다 - -:::evidence key="a05-f012-identity" alt="가드의 탐지 메서드와 식별자 탐색 메서드 전문, 이 타입을 언급하는 곳 전부와 프로덕션 인스턴스화 수, 같은 패키지 레지스트리의 프로덕션 독자 수, 계획 문서가 이 과제의 산출물로 적은 문장, 가드가 거절하려는 전략이 main 에 나타나는 줄과 main 의 GeneratedValue 수, 그리고 출하 엔티티의 식별자 접근 방식 계수를 출력한 터미널 기록." caption="탐지와 탐색 전문 · 프로덕션 인스턴스화 0 과 레지스트리 독자 0 · 계획서의 기동 진단 · main 의 GeneratedValue 0 · 엔티티 32 중 27이 필드 @Id, 게터 0, @Access 0 — 51줄 · exit 0" zoom="true" -::: - -탐색이 상위 클래스까지 올라가고, javadoc 이 이유를 적어 뒀다. - -```text -stopping at the declared class would silently classify every such entity as "not -identity" and let the guard pass on exactly the entities it was written for. -``` - -이 저장소에는 식별자를 든 매핑된 상위 클래스가 없다. main 의 `@MappedSuperclass` 는 하나이고 `@Id` 를 들지 않는다. 그 근거는 채택자를 위해 적힌 것이다. - -같은 근거가 프로퍼티 접근에도 성립한다. 탐색은 `getDeclaredFields()` 만 순회한다. - -## 하이버네이트는 게터에 단 것도 IDENTITY 로 읽는다 - -:::evidence key="a05-f012-identity-probe" alt="같은 IDENTITY 매핑을 필드 접근과 프로퍼티 접근으로 선언한 두 엔티티의 소스와 배치를 요구하는 프로파일에서의 탐지·검증 결과, 시퀀스 엔티티를 대조군으로 더해 하이버네이트를 세우고 200행을 넣었을 때의 식별자 멤버와 프리페어드 스테이트먼트 수, 그리고 가드의 근거 측정과 그것이 도는 레인과 워크플로를 출력한 터미널 기록." caption="두 접근 방식의 탐지·검증 결과 · 하이버네이트가 잡은 식별자 멤버 필드와 메서드 · 200삽입에 IDENTITY 200문장 대 시퀀스 6문장 · 근거 측정의 단언과 CI 레인 — 48줄 · exit 0" zoom="true" -::: - -가드에 넘기면 이렇게 갈린다. - -```text -FieldAccessIdentity IDENTITY 탐지 true 검증 거절 -PropertyAccessIdentity IDENTITY 탐지 false 검증 통과 -``` - -통과가 무해한 누락인지 확인하려고 하이버네이트를 직접 세웠다. 배치 크기 50, 200행이다. - -```text -필드 접근 식별자 멤버 Field 삽입 200 프리페어드 스테이트먼트 200 -프로퍼티 접근 식별자 멤버 Method 삽입 200 프리페어드 스테이트먼트 200 -시퀀스(대조군) 식별자 멤버 Field 삽입 200 프리페어드 스테이트먼트 6 -``` - -메타모델은 뒤의 것에 메서드를 식별자 멤버로 잡는다. 문장 수는 필드 접근과 같고, 같은 조건의 시퀀스 엔티티와는 서른 배 이상 다르다. 가드가 통과시킨 것은 가드가 막겠다고 쓴 동작이다. - -거절의 근거도 실측이다. `IdStrategyContractTest` 가 실제 PostgreSQL 에 200행을 배치 크기 50 으로 넣고, 시퀀스 엔티티는 `jdbcBatches` 가 1 보다 크고 IDENTITY 엔티티는 0 이라고 단언한다. 그 태그를 고르는 레인을 PR 워크플로와 야간 워크플로가 둘 다 호출한다. 여기서는 읽기만 했다. - -## 가드·프로파일·레지스트리는 서로만 참조한다 - -계획 문서는 이 과제의 산출물을 IDENTITY 와 시퀀스 불일치에 대한 기동 진단으로 적었다. 가드 javadoc 과 단위 테스트 javadoc 도 기동 실패를 말한다. - -main 에 그 기동 지점이 없다. 이 타입을 만들거나 임포트하는 main 파일이 0 이고, 같은 패키지의 배치 프로파일 레지스트리도 main 에서 읽히지 않는다. 이 타입을 언급하는 넷은 자기 자신, 실제로 부르는 단위 테스트 하나, 그리고 javadoc 으로만 부르는 계약 시험과 테스트 도구다. - -거절 대상도 main 에는 없다. `GenerationType.IDENTITY` 가 main 에 나타나는 세 줄은 가드 자신의 javadoc 과 예외 메시지와 비교식이다. `@GeneratedValue` 는 main 에 한 번도 나오지 않는다. 출하 엔티티의 식별자는 전부 애플리케이션이 정한다. - -접근 방식도 갈리지 않는다. main 의 `@Entity` 는 서른둘이고 스물일곱이 `@Id` 를 필드에 단다. 하나는 `@EmbeddedId` 를 쓴다. 게터에 `@Id` 를 단 것은 0 이고, 접근 방식을 뒤집는 `@Access` 는 저장소 전체에 0 이다. - -## 이 가드가 문서화한 보장의 범위 - -채택자가 프로퍼티 접근을 쓰면 이 가드는 그 매핑을 보지 못한다. 하이버네이트는 그것을 IDENTITY 로 부트스트랩하고 배치는 꺼진다. - -고칠 방향은 셋이다. - -- JPA 메타모델로 실제 식별자 속성과 접근 전략을 해석한다 -- 필드와 게터를 모두 검사하되 중복과 재정의 규칙까지 JPA 접근 의미와 맞춘다 -- 지원 매핑을 필드 접근으로 제한하고 그 제한을 아키텍처 규칙으로 강제한다 - -게터 리플렉션만 더하면 혼합 접근과 `@Access(AccessType.PROPERTY)` 가 남는다. 이 저장소에는 `@Access` 사용이 0 이라 지금은 드러나지 않는다. - -## 확인하지 못한 것 - -실제 PostgreSQL 에서 재지는 않았다. 부트스트랩 탐침은 H2 위에서 돌렸고, 하이버네이트가 삽입을 배치로 묶었는지는 프리페어드 스테이트먼트 수로 판정했다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-a05-f013-specificationpolicy-specification-unrestricted.md b/docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-a05-f013-specificationpolicy-specification-unrestricted.md deleted file mode 100644 index 9b992fd..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-a05-f013-specificationpolicy-specification-unrestricted.md +++ /dev/null @@ -1,156 +0,0 @@ ---- -kind: CASE -slug: a05-f013-specificationpolicy-specification-unrestricted -title: 널 검사가 술어 검사를 대신하고, 라이브러리는 널 아닌 조건 없음을 준다 -topic: what-a-gate-does-not-prove -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a05-f013-specificationpolicy-specification-unrestricted -evidenceCapturedOn: 2026-09-02 -body: case-a05-f013-specificationpolicy-specification-unrestricted.body.md -assets: - - key: a05-f013-specificationpolicy-specification-unrestricted - file: ../../../final/evidence/rendered/a05-f013-specificationpolicy-specification-unrestricted.svg - - key: a05-f013-specificationpolicy-specification-unrestricted-probe - file: ../../../final/evidence/rendered/a05-f013-specificationpolicy-specification-unrestricted-probe.svg -evidence: - - ../../../final/evidence/raw/a05-f013-specificationpolicy-specification-unrestricted.txt - - ../../../final/evidence/raw/a05-f013-specificationpolicy-specification-unrestricted-probe.txt -source: - - 원본 분석 절은 `final/document.md#a05` §47 이다. 문서 계약과 널 검사의 불일치, 그리고 4.0.7 이 널 아닌 제한 없음 명세를 제공한다는 바이트코드 확인(§47.1)이 거기 있다. - - §47.2 의 실행 탐침은 손으로 쓴 `(root, query, cb) -> null` 람다를 넣은 것이다. `Specification.unrestricted()` 자체를 두 메서드에 넣은 것, 페이지 경계가 남는다는 구분, 그리고 형제 정책의 같은 구멍은 이 기록에서 새로 확인했다. Querydsl 이 미채택 상태이지 죽은 코드가 아니라는 판정은 같은 문서 §48 에 있다. ---- - -# 널 검사가 술어 검사를 대신하고, 라이브러리는 널 아닌 조건 없음을 준다 - -술어 없는 명세를 거절한다는 문서 계약이 있고, 구현은 명세 객체가 널인지만 본다. Spring Data 4.0.7 이 공식으로 주는 제한 없음 명세는 널이 아니면서 술어로 널을 돌려주므로 두 검사를 다 지난다. 페이지 경계 검사는 남아 있어서, 통과한 결과물은 필터 없는 페이지 읽기다. - -## 관계 - -- **@Bean이 있다는 것은 조립 증거가 아니다** - 존재를 내용으로 오독한다는 점이 같다. -- **접두사 시작 매칭은 시그니처에는 맞고 스니핑 패턴에는 맞지 않는다** - 탐지 방법이 대상의 실제 형태를 못 덮는다는 점이 같다. -- **명령 카탈로그와 admission 아홉 단계** - 경계 없는 요청을 어느 단계에서 막아야 하는지 정리한 문서다. - -## 문제 - -클래스 javadoc 은 술어 없는 명세를 빌더의 옷을 입은 전체 테이블 스캔이라 부른다. 선택 필터가 전부 비어서 생기고, 어느 한 줄도 틀리지 않았기 때문에 코드 리뷰에서 무해해 보인다는 설명이 이어진다. - -그래서 술어가 있거나 전부 스캔하겠다는 명시적 토큰이 있어야 한다고 요구한다. 페이지 경계를 요구하는 근거도 그 javadoc 안에 같이 있다. - -## 결론 - -검사는 specification == null 이다. Spring Data JPA 4.0.7 의 Specification.unrestricted() 는 널이 아닌 객체를 돌려주면서 toPredicate 로는 널을 주므로, requireBounded 도 requirePredicate 도 이를 거절하지 않는다. 실제로 거절되는 값은 자바 널 하나다. - -바이트코드로 확인했다. 그 팩토리는 부트스트랩 0번을 통해 lambda$0 를 돌려주고, 그 메서드의 본문은 aconst_null 과 areturn 두 명령이다. - -뚫리는 것은 javadoc 이 적은 두 가지 중 하나다. requireBounded 는 pageable.isUnpaged() 를 여전히 거절하므로 통과한 쿼리는 필터가 없는 페이지 읽기다. requirePredicate 는 Pageable 인자 자체가 없어 이 완충이 없다. - -Querydsl 쪽 PredicatePolicy 는 토큰 문자열 allow-unbounded-scan 을 그대로 공유하면서 predicate == null 만 본다. 빈 BooleanBuilder 는 널이 아니고 hasValue 도 거짓인데 그대로 통과한다. - -두 정책 다 현재 호출자가 없다. 명세 쪽은 두 메서드를 부르는 코드가 레포 전체에 0 이다. Querydsl 쪽은 진입점이 정책을 부르지만 그 진입점 자신에게 프로덕션 호출자가 없는데, 이쪽은 미채택이라기보다 설계다. Querydsl 은 이 리프에서 compileOnly 이고 출하 런타임 클래스패스에 실리지 않는다고 클래스 javadoc 이 적는다. - -그래도 계약은 남는다. 이 클래스는 아키텍처 문서의 공개 API 표면 목록에 실려 있다. 두 메서드를 호출하는 코드가 생기면 술어 없는 명세는 걸러지지 않는다. - -검사가 볼 대상은 toPredicate 가 돌려주는 값이다. Criteria 컨텍스트가 있어야 하니 검사 자리가 실행 직전까지 밀린다. Querydsl 쪽은 hasValue 를 물으면 끝난다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Spring Data JPA : 4.0.7 -Querydsl : 5.1.0 -확인 방식 : 해석된 jar 역어셈블, 두 정책에 라이브러리 표현 투입, 호출처 계수 -소스 수정 : x - -## 재현 조건 - -1. javadoc 계약과 실제 검사 조건을 나란히 읽는다. -2. 락파일이 고정한 좌표로 클래스패스를 만들고, 제한 없음 명세 팩토리의 부트스트랩 표와 그 람다의 본문을 본다. -3. 그 명세와 자바 널을 두 검사에 각각 넣는다. -4. 페이지 경계 검사가 남아 있는지, requirePredicate 에 Pageable 인자가 있는지 확인한다. -5. Querydsl 의 빈 빌더를 형제 정책에 넣는다. -6. 두 정책의 호출자를 레포 전체에서 센다. - -## 본문 - - - -`SpecificationPolicy` 의 클래스 javadoc 은 술어 없는 명세를 이렇게 부른다. - -```text -A specification with no predicate is a full table scan wearing a builder's clothing. -``` - -같은 javadoc 이 페이지 경계도 같은 이유로 요구한다. 막겠다는 것은 두 가지다. - -## 검사는 객체가 있는지만 본다 - -:::evidence key="a05-f013-specificationpolicy-specification-unrestricted" alt="명세 정책의 javadoc 계약 전문과 두 검사 메서드의 구현, 이 타입을 언급하는 레포 전체의 줄과 두 검사 메서드를 부르는 코드 수, 그리고 같은 리프의 형제 정책이 쓰는 같은 토큰 상수와 같은 널 비교, 형제 쪽 호출처와 그것을 부르는 테스트를 출력한 터미널 기록." caption="javadoc 이 요구하는 두 가지와 실제 널 비교 · 레포 전체 언급과 호출 0 · 아키텍처 문서의 공개 API 목록에 등재 · 형제 정책의 같은 토큰과 같은 비교 — 61줄 · exit 0" zoom="true" -::: - -```java -if (specification == null && !ALLOW_UNBOUNDED_TOKEN.equals(allowUnboundedToken)) { -``` - -`requirePredicate` 도 같다. 명세 객체가 있으면 술어가 있는 것으로 다룬다. - -## 라이브러리가 널이 아닌 조건 없음을 준다 - -:::evidence key="a05-f013-specificationpolicy-specification-unrestricted-probe" alt="락파일 좌표에서 만든 클래스패스와 그 jar 목록, 제한 없음 명세 팩토리의 부트스트랩 표와 그것이 가리키는 람다의 본문, 그 명세와 자바 널을 두 검사에 넣은 결과, 페이지 경계 검사와 requirePredicate 의 시그니처, Querydsl 의 빈 빌더를 형제 정책에 넣은 결과, 그리고 형제 쪽 진입점의 limit 부착과 compileOnly 선언을 출력한 터미널 기록." caption="락파일로 만든 클래스패스 · 부트스트랩 0번이 가리키는 람다는 널 반환 · 두 검사 모두 통과, 자바 널만 거절 · 페이지 경계는 유지, requirePredicate 는 완충 없음 · Querydsl 빈 빌더도 통과 — 56줄 · exit 0" zoom="true" -::: - -부트스트랩 표가 0번을 `lambda$0` 로 링크하고, 그 메서드의 본문은 두 명령이다. - -```text -0: aconst_null -1: areturn -``` - -널이 아닌 명세 객체이면서, 술어를 물으면 널을 준다. - -`Specification.unrestricted()` 와 자바 널을 두 메서드에 각각 넣은 결과다. - -```text -Specification.unrestricted() 가 널인가 : false -그 명세의 toPredicate 가 돌려주는 값 : null -requireBounded(토큰 없이) : 통과 -requirePredicate : 통과 -requireBounded(널 명세) : 거절 -``` - -거절되는 것은 자바 널뿐이다. - -## 페이지 경계는 남아 있다 - -`requireBounded` 는 `pageable.isUnpaged()` 를 여전히 거절한다. 그래서 이 명세로 통과한 쿼리는 필터가 없는 페이지 읽기이지 전체 테이블 스캔은 아니다. javadoc 이 적은 두 가지 중 한쪽만 무너진다. - -`requirePredicate` 는 다르다. 시그니처에 `Pageable` 이 없으므로 이 완충이 걸리지 않는다. - -## Querydsl 쪽 PredicatePolicy - -같은 리프의 Querydsl 쪽 정책이 토큰 문자열 `allow-unbounded-scan` 을 그대로 공유하면서 `predicate == null` 만 본다. - -Querydsl 의 빈 `BooleanBuilder` 는 널이 아니고 `hasValue` 도 거짓이다. 넣으면 통과한다. - -형제 쪽 진입점은 그 널 아님을 술어로 받아 `where` 에 넘긴다. 다만 같은 메서드가 항상 `offset` 과 `limit` 을 붙이므로, 이쪽 결과물도 필터 없는 페이지 읽기다. - -## 지금 이 두 정책을 밟는 코드 - -`SpecificationPolicy` 의 두 메서드를 부르는 코드는 레포 전체에 0 이다. 정적 임포트 경유도 없다. 소스에서 이 타입을 언급하는 줄은 자기 클래스 선언과 private 생성자뿐이고, 나머지 언급은 계획 문서와 학습 문서, 그리고 아키텍처 문서의 공개 API 표면 목록이다. - -`QuerydslJpaSupport.select` 는 형제 정책을 부르지만 그 진입점 자신에게 프로덕션 호출자가 없다. 이쪽은 미채택이라기보다 설계다. `build.gradle` 이 Querydsl 을 `compileOnly` 로 잡고, 클래스 javadoc 이 출하 런타임 클래스패스가 그것을 싣지 않는다고 적는다. app-bootstrap 의 락파일에도 Querydsl 이 없다. - -그러므로 두 정책의 문제는 지금 도는 코드의 결함이 아니라, 채택 시점에 물려받을 계약의 결함이다. - -## 고칠 방향 - -검사가 봐야 할 것은 `toPredicate` 의 반환값이다. 그러려면 Criteria 컨텍스트가 필요하므로 검사 시점이 조립에서 실행 직전으로 옮겨진다. Querydsl 쪽은 빌더에 값이 있는지 물으면 된다. - -## 확인하지 못한 것 - -그 명세로 실제 쿼리를 실행해 필터 없는 페이지 읽기가 되는 것을 관측하지 않았다. 확인한 것은 두 정책이 통과시킨다는 사실과, 통과한 값이 술어를 갖고 있지 않다는 사실이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-a05-f014-collection-fetch-pagination.md b/docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-a05-f014-collection-fetch-pagination.md deleted file mode 100644 index 67a8cdc..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-a05-f014-collection-fetch-pagination.md +++ /dev/null @@ -1,199 +0,0 @@ ---- -kind: CASE -slug: a05-f014-collection-fetch-pagination -title: 게이트가 막겠다는 실패가 그 게이트 실행 안에서 일어나고, 단언은 그것을 보지 못한다 -topic: what-a-gate-does-not-prove -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a05-f014-collection-fetch-pagination -evidenceCapturedOn: 2026-09-02 -body: case-a05-f014-collection-fetch-pagination.body.md -assets: - - key: a05-f014-collection-fetch-pagination - file: ../../../final/evidence/rendered/a05-f014-collection-fetch-pagination.svg - - key: a05-f014-collection-fetch-pagination-probe - file: ../../../final/evidence/rendered/a05-f014-collection-fetch-pagination-probe.svg - - key: a05-f014-collection-fetch-pagination-lane - file: ../../../final/evidence/rendered/a05-f014-collection-fetch-pagination-lane.svg -evidence: - - ../../../final/evidence/raw/a05-f014-collection-fetch-pagination.txt - - ../../../final/evidence/raw/a05-f014-collection-fetch-pagination-probe.txt - - ../../../final/evidence/raw/a05-f014-collection-fetch-pagination-lane.txt -source: - - 원본 분석 절은 `final/document.md#a05` §52 다. 게이트 선언과 테스트 단언의 불일치가 §52.1 에, 생산자 태스크 불일치가 §52.2 에, 검증기가 그것을 잡지 못한다는 것이 §52.4 에, 집계 태스크가 이 테스트를 다른 레인으로 실행한다는 것이 §52.5 에 있다. - - 제공자 정책 클래스의 javadoc 대조와 해석된 제공자에 대한 실행은 그 절에 없고 이 기록에서 확인했다. ---- - -# 게이트가 막겠다는 실패가 그 게이트 실행 안에서 일어나고, 단언은 그것을 보지 못한다 - -컬렉션 페치 페이지네이션이 릴리스 게이트로 선언되어 있다. 테스트의 javadoc 은 단언 대상이 생성된 SQL 이라고 적는데, 본문의 단언 둘은 반환된 페이지 크기와 테스트가 방금 만든 객체의 필드다. 해석된 제공자에게 같은 조회를 시키면 상한이 메모리에서 적용되고 그 경고가 뜨는데도 두 단언은 참이다. - -## 관계 - -- **빠뜨림이 통과가 되는 게이트는 게이트가 아니다** - 이 게이트도 막겠다고 적은 실패 모드가 일어나는 중에 통과한다. -- **계약 테스트는 어댑터가 실제로 돌리는 statement를 실행해야 한다** - 이 게이트가 확인해야 할 대상을 다룬 규칙이다. -- **아무도 돌리지 않는 레인의 게이트는 마지막으로 돌린 사람이 본 것을 보고한다** - 아무도 돌리지 않는 레인의 게이트가 무엇을 보고하게 되는지 적은 규칙이다. - -## 문제 - -지원 매트릭스의 릴리스 게이트 표에 이 행이 있고, 막을 대상이 적혀 있다. 페이지 단위 컬렉션 페치가 조용히 전체 테이블을 읽고 메모리에서 페이지네이션하는 것이다. - -테스트의 javadoc 이 왜 반환된 페이지 크기로는 안 되는지 설명한다. 결과는 어느 쪽이든 같고, 상한이 어디서 적용됐는지는 SQL 만 말한다는 것이다. - -## 결론 - -그 메서드의 단언은 둘이다. 반환된 페이지가 기대 최대 이하인지, 그리고 기대값의 requiresDatabaseLimit 이 참인지다. - -둘째 단언이 보는 값은 테스트가 같은 메서드 첫 줄에서 만든 객체의 필드이고, 그 값은 팩토리에 상수로 적혀 있다. - -첫째 단언은 결과가 어느 쪽이든 참이 된다. 락파일이 고정한 제공자에게 게이트와 같은 형태의 컬렉션 페치 조회를 시켜 봤다. 상한을 메모리에서 적용한다는 경고가 뜨고, 프리페어드 스테이트먼트는 하나이며, 반환된 부모는 정확히 스무 개다. 게이트가 막겠다고 적은 실패 모드가 일어나는 중에도 두 단언이 모두 통과한다. - -javadoc 은 이 동작을 옛 제공자의 것으로 적지만, 이 저장소가 해석하는 제공자에서도 기본값이다. 그것을 실패로 바꾸는 설정을 이 저장소는 켜 두지 않았다. - -같은 모듈의 제공자 정책 클래스가 이 형태를 이름으로 적어 뒀다. 선언해 둔 기준선을 런타임 값처럼 단언하면 테스트가 그 상수를 같은 상수와 비교하게 되므로, 실제 런타임 동작을 확인하지 못한 채 통과한다는 것이다. 그 문장의 경고 대상은 제공자 버전 단언이었고, 이 게이트는 반대편 예시 자리에 있다. 게이트의 둘째 단언이 바로 그 형태다. - -증거의 출처도 어긋나 있다. 레지스트리는 이 게이트를 쿼리 플랜 레인에 묶는데, 레인은 태그로 테스트를 고르고 그 레인의 태그는 쿼리 플랜이며 이 테스트의 태그는 계약이다. 그 태그를 단 클래스는 따로 하나 있다. - -테스트 자체는 돈다. 그 레인은 PR 과 야간 작업에서 모두 실행되고, 릴리스 집계도 이 레인의 결과를 입력으로 읽는다. 어긋난 것은 실행 여부가 아니라 증거가 어디서 나오느냐다. 게이트 이름으로 지목된 태스크의 JUnit 결과에 이 테스트가 없다. - -그 불일치를 지나가게 하는 검증기가 있다. 게이트마다 태스크 경로가 절대 경로인지, 프로젝트가 있는지, 그 이름의 태스크가 있는지, 그것이 Test 타입인지 넷을 본다. 이 게이트에서 넷 다 참이다. 레지스트리를 읽는 단위 테스트의 게이트 단언은 경로가 콜론으로 시작하는지 한 줄이다. 이 게이트를 통과시키는 단언도 선언된 상수를 다시 읽어 비교한다. - -레지스트리에 나란히 적힌 blocking 은 아무 코드도 읽지 않는다. 여섯 게이트가 모두 참인데 매니페스트 파서와 게이트 검증기 어느 쪽도 그 필드를 읽지 않는다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Hibernate ORM : 7.2.24.Final -확인 방식 : 단언 대조, 락파일 좌표로 만든 클래스패스에서 같은 조회 실행, 레지스트리·레인·검증기 대조 -소스 수정 : x - -## 재현 조건 - -1. 매트릭스에서 게이트의 선언과 막는 대상, 테스트의 javadoc 을 읽는다. -2. 그 메서드의 단언 두 줄과, 둘째 단언이 보는 값을 만드는 팩토리를 확인한다. -3. 락파일 좌표로 클래스패스를 만들고 게이트와 같은 형태의 조회를 실행한다. 경고와 문장 수와 반환 크기를 본다. -4. 메모리 페이지네이션을 실패로 바꾸는 설정이 켜져 있는지 센다. -5. 레지스트리의 태스크와 그 레인의 태그, 테스트의 태그를 비교한다. -6. 게이트 검증기가 무엇을 보는지와, 레지스트리를 읽는 테스트의 게이트 단언을 읽는다. - -## 본문 - - - -지원 매트릭스의 릴리스 게이트 표가 `collection-fetch-pagination` 행을 두고, 막는 대상을 이렇게 적는다. - -```text -a paged collection fetch silently reading the whole table and paginating in memory -``` - -## javadoc 은 단언 대상을 SQL 이라고 적는다 - -:::evidence key="a05-f014-collection-fetch-pagination" alt="지원 매트릭스의 게이트 행, 계약 테스트의 javadoc 전문과 태그와 픽스처 상수, 그 테스트가 실제로 쓰는 단언 목록, 둘째 단언이 보는 값을 만드는 팩토리, 그리고 같은 모듈의 제공자 정책 클래스가 이 형태를 적어 둔 javadoc 을 출력한 터미널 기록." caption="매트릭스의 게이트 행 · 단언 대상이 SQL 이라는 javadoc · 실제 단언 목록 · 기대값 팩토리가 적어 둔 true · 제공자 정책의 같은 경고 — 51줄 · exit 0" zoom="true" -::: - -```text -The assertion is therefore on the generated SQL, not on the returned page size. -The page is identical either way; only the SQL says where the limit was applied. -``` - -## 메서드가 실제로 쓰는 단언 둘 - -```java -assertThat(page).hasSizeLessThanOrEqualTo(expected.maxReturnedParents()); -assertThat(expected.requiresDatabaseLimit()).isTrue(); -``` - -`expected` 는 같은 메서드 첫 줄에서 만든 값이고, 만드는 팩토리는 이렇게 되어 있다. - -```java -public static FetchPaginationExpectation hibernate74PostgreSql(int pageSize) { - return new FetchPaginationExpectation(pageSize, true, pageSize * 100); -} -``` - -둘째 인자가 `requiresDatabaseLimit` 이다. 이 단언은 팩토리가 34행에 적어 둔 `true` 를 다시 읽는다. - -기대값 레코드의 javadoc 도 그 필드가 반환된 페이지가 아니라 생성된 SQL 에 대해 단언된다고 적는다. 테스트 javadoc 과 레코드 javadoc 이 같은 약속을 적어 두고, 메서드 본문은 그 약속을 실행하지 않는다. - -## 그 실패는 지난 일이 아니다 - -:::evidence key="a05-f014-collection-fetch-pagination-probe" alt="락파일이 고정한 하이버네이트와 H2 의 버전, 저장소에서 메모리 페이지네이션을 실패로 바꾸는 설정을 켜는 곳의 수, 게이트와 같은 모양의 컬렉션 페치 조회를 그 제공자에게 직접 시켰을 때의 경고와 반환 부모 수와 프리페어드 스테이트먼트 수, 그리고 서버 없이 도는 레인에 있는 같은 형태의 테스트를 출력한 터미널 기록." caption="락파일의 제공자 7.2.24 · 메모리 페이지네이션을 막는 설정 0 · 같은 조회에서 HHH90003004 경고와 문장 1개, 반환 20 · 서버 없는 레인의 같은 형태 — 21줄 · exit 0" zoom="true" -::: - -javadoc 은 이 동작을 옛 제공자의 것으로 적는다. 락파일이 고정한 제공자는 Hibernate 7.2.24.Final 이고, 게이트와 같은 형태의 조회를 시키면 이렇게 된다. - -```text -WARN org.hibernate.orm.query -- HHH90003004: firstResult/maxResults specified with -collection fetch; applying in memory -반환된 부모 수 : 20 -그 조회가 낸 프리페어드 스테이트먼트 : 1 -fail_on_pagination_over_collection_fetch : false -``` - -메모리 페이지네이션을 실패로 바꾸는 설정을 저장소가 켜지 않는다. 그래서 첫째 단언은 어느 쪽이어도 참이다. SQL 로 밀리면 상한이 붙어 스무 개고, 메모리로 밀리면 하이버네이트가 중복을 걷어낸 리스트를 스무 개까지 잘라 돌려준다. 이 단언이 깨지는 경우는 상한이 아예 무시될 때뿐이고, 그것은 이 게이트가 막겠다고 적은 실패가 아니다. - -게이트가 막겠다고 적은 실패 모드는 지난 일이 아니라 이 게이트가 도는 조건이다. - -## 같은 모듈의 javadoc 이 적어 둔 같은 형태 - -같은 리프의 제공자 정책 클래스가 다른 선택지를 설명하면서 이렇게 적는다. - -```text -The alternative — asserting the declared baseline as if it were the runtime one — would -give a green check that proves the constant equals itself while the collection-fetch- -pagination gate runs against a different provider entirely. -``` - -이 문장이 경고하는 형태는 제공자 버전 단언이고, 이 게이트는 그 반대편에 예시로 놓여 있다. 그런데 게이트의 둘째 단언이 하는 일이 정확히 그 형태다. - -## 같은 클래스의 다른 단언들 - -두 번째와 세 번째 테스트는 하이버네이트 통계를 읽는다. 준비된 문장이 둘 이하인지, 페치 조인 없이는 컬렉션 페치가 하나를 넘는지다. 둘 다 제공자 동작을 실제로 측정한다. - -네 번째는 행 증폭 상한을 본다. 픽스처는 부모 50 에 부모당 자식 4 이므로 자식 행이 200 이고, 상한은 페이지 크기 20 의 100 배다. - -같은 형태가 서버를 요구하지 않는 레인에도 하나 있다. `FetchPaginationExpectationTest` 의 「one collection page requires a database-level parent bound」는 팩토리가 만든 객체의 `maxReturnedParents` 가 20 인지와 `requiresDatabaseLimit` 이 참인지를 본다. 이쪽은 PostgreSQL 없이 매 빌드마다 돈다. - -## 레지스트리가 지목한 생산자와 실제 생산자 - -:::evidence key="a05-f014-collection-fetch-pagination-lane" alt="릴리스 레지스트리에서 이 게이트에 붙은 생산자 태스크와 blocking 표기, 그 필드를 읽는 코드 수, 프로젝트가 등록하는 레인들과 각 레인의 태그와 게이트 테스트의 태그, 게이트 검증기가 위반으로 보는 네 가지, 레지스트리를 읽는 단위 테스트의 게이트 단언, 그리고 계약 레인이 도는 워크플로와 태스크 의존을 출력한 터미널 기록." caption="게이트의 생산자는 쿼리 플랜 레인, 테스트 태그는 jpa-contract · blocking 을 읽는 코드 0 · 검증기가 보는 네 가지에 태그 없음 · 게이트 단언은 콜론 시작 여부 한 줄 · 계약 레인은 PR·야간·릴리스에서 돎 — 46줄 · exit 0" zoom="true" -::: - -레지스트리는 이 게이트를 `jpaPlatformQueryPlanTest` 에 묶는다. 레인은 태그로 테스트를 고르고, 그 레인의 태그는 `jpa-queryplan` 이며 이 테스트의 태그는 `jpa-contract` 다. 쿼리 플랜 태그를 단 클래스는 다른 하나뿐이다. - -나란히 적힌 `blocking: true` 는 아무 코드도 읽지 않는다. 여섯 게이트 전부 참이고, 매니페스트 파서도 게이트 검증기도 그 필드를 보지 않는다. - -그렇다고 이 테스트가 안 도는 것은 아니다. `jpa-contract` 를 고르는 레인이 PR 워크플로와 야간 워크플로에서 돌고, 릴리스 집계 태스크도 그 레인을 의존한다. 틀린 것은 실행 여부가 아니라 증거의 출처다. 게이트 이름으로 지목된 태스크의 JUnit 결과에는 이 테스트가 들어 있지 않고, 게이트 태스크만 단독으로 재검증하면 대상 시나리오는 한 번도 실행되지 않는다. - -## 이 불일치를 지나가게 하는 검증기 - -레지스트리에는 검증기가 붙어 있다. 게이트마다 네 가지를 본다. - -```text -gate task must be an absolute Gradle path -no project at '' for gate task -no task '' in '' -'' is not a Test task, so it produces no JUnit evidence -``` - -넷 다 이 게이트에서 참이다. 태스크가 어떤 테스트를 고르는지는 검사 항목에 없다. - -레지스트리를 읽는 단위 테스트의 게이트 단언은 한 줄이다. - -```java -assertThat(manifest.taskFor(gate)).startsWith(":"); -``` - -이 사례가 고발하는 형태와 같은 형태의 단언이 이 게이트를 지키고 있다. - -## 확인하지 못한 것 - -실제 PostgreSQL 에서 같은 조회를 재지 않았다. 탐침은 H2 위에서 돌렸고, 상한이 SQL 로 갔는지는 제공자 경고와 문장 수로 판정했다. - -계약 레인을 실제로 돌려 JUnit 결과를 확인하지도 않았다. 워크플로와 태스크 의존을 읽었다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-a05-f033-jpa-flyway-migration.md b/docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-a05-f033-jpa-flyway-migration.md deleted file mode 100644 index 6603b7e..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-a05-f033-jpa-flyway-migration.md +++ /dev/null @@ -1,170 +0,0 @@ ---- -kind: CASE -slug: a05-f033-jpa-flyway-migration -title: 선택된 카드의 생산자 태스크가 지금 빌드를 실패시킨다 -topic: what-a-gate-does-not-prove -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:a05-f033-jpa-flyway-migration -evidenceCapturedOn: 2026-09-02 -body: case-a05-f033-jpa-flyway-migration.body.md -assets: - - key: a05-f033-jpa-flyway-migration - file: ../../../final/evidence/rendered/a05-f033-jpa-flyway-migration.svg - - key: a05-f033-jpa-flyway-migration-run - file: ../../../final/evidence/rendered/a05-f033-jpa-flyway-migration-run.svg - - key: a05-f033-jpa-flyway-migration-gate - file: ../../../final/evidence/rendered/a05-f033-jpa-flyway-migration-gate.svg -evidence: - - ../../../final/evidence/raw/a05-f033-jpa-flyway-migration.txt - - ../../../final/evidence/raw/a05-f033-jpa-flyway-migration-run.txt - - ../../../final/evidence/raw/a05-f033-jpa-flyway-migration-gate.txt -source: - - 원본 분석 절은 `final/document.md#a05` §132 다. 등급은 P1 이다. 생산자가 현재 리비전에서 실패한다는 판정, 두 실패의 원인, 두 스트림의 파일 수, 단언이 마지막으로 갱신된 시점과 그 뒤 들어온 마이그레이션 다섯의 날짜, 그리고 이 레인을 도는 자동 경로가 그 절에 있다. - - 이 카드를 선행 조건으로 두는 카드 전수와 집계 게이트가 카드 id 를 하드코딩하는 자리, 게이트를 부르는 잡의 방아쇠, 그리고 47행 실패에 가려진 62행 단언은 이 기록에서 확인했다. ---- - -# 선택된 카드의 생산자 태스크가 지금 빌드를 실패시킨다 - -준비도 카드 하나가 선택 상태다. 그 카드의 생산자 태스크를 돌리면 네 시험 중 둘이 47행과 89행에서 멈추고 빌드가 실패한다. 두 실패 모두 스트림에 마이그레이션이 들어왔는데 그 스트림의 적용 집합을 목록 리터럴로 고정한 단언이 그대로인 것이다. - -## 관계 - -- **아무도 돌리지 않는 레인의 게이트는 마지막으로 돌린 사람이 본 것을 보고한다** - 이 사례가 그 규칙의 형태다. -- **마이그레이션 스트림은 자기 history 테이블을 갖는다** - 스트림별 버전 목록을 한 곳에 고정하는 구조다. -- **컨테이너가 필요한 레인의 실제 결과를 실행으로 확인하지 않았다** - 이 레인이 그 질문의 대상 중 하나다. - -## 문제 - -카드마다 증거를 만들 태스크가 이름으로 묶여 있다. 이 카드를 선행 조건으로 두는 카드가 열하나이고 그중 셋이 선택 상태이며, 셋 중 하나가 R2 집계 게이트다. - -## 결론 - -두 실패는 같은 기제의 두 사례다. 단언이 스트림의 적용 집합을 목록 리터럴로 고정하는데, 그 스트림에 마이그레이션이 들어오면 리터럴이 낡는다. 시나리오는 다르다. 하나는 기존 history 채택이고 다른 하나는 새 코어 스트림 초기화다. 그 차이는 실패에 관여하지 않는다. - -기반 스트림은 적용 집합을 1, 3, 4, 5, 6 으로 고정했는데 실제로는 9, 10, 11, 12 가 더 있다. 코어 스트림은 1 로 고정했는데 실제로는 2 가 더 있다. - -단언은 7월 31일 이후로 바뀌지 않았다. 그 뒤 두 스트림에 마이그레이션 다섯이 들어왔고, 지금 실패하는 두 단언을 함께 낡게 만든 것은 8월 15일의 커밋 하나다. 그 커밋이 기반 스트림의 V9 와 코어 스트림의 V2 를 동시에 넣었다. - -고칠 자리는 셋이다. 47행과 89행은 지금 실패하고, 62행은 47행 실패에 가려 실행되지 않는다. 47행을 고치면 62행이 곧바로 같은 이유로 실패한다. - -같은 파일의 네 번째 고정 단언은 통과한다. 그 스트림은 픽스처라 자라지 않았다. 깨지는 것은 스트림이 자란 자리뿐이다. - -재발 방지는 같은 소스 세트가 이미 쓰는 방식을 기반 스트림에도 적용하는 것이다. 그 방식은 스트림마다 표 이름과 버전 목록을 한 자리에 두고, 그 목록에 붙은 주석이 목록을 스트림의 계약이라고 못박는다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Gradle : 9.0.0 -데이터베이스 : PostgreSQL, Testcontainers -확인 방식 : 깨끗한 작업 트리에서 재실행을 강제해 생산자 태스크 실행, 게이트의 태스크 그래프 조회 -소스 수정 : x - -## 재현 조건 - -1. 준비도 카드 정의에서 이 카드의 상태와 생산자 태스크를 확인한다. -2. 이 카드를 선행 조건으로 두는 카드를 전수로 열거하고 각각의 상태를 읽는다. -3. 그 태스크가 고정한 적용 집합 넷을 읽는다. -4. 두 스트림의 실제 마이그레이션 파일을 나열하고, 단언 파일과 각 마이그레이션의 최종 커밋 날짜를 비교한다. -5. 깨끗한 작업 트리에서 재실행을 강제해 그 태스크를 돌린다. -6. 풀 리퀘스트 워크플로가 부르는 게이트의 태스크 그래프에 이 태스크가 있는지 조회한다. - -## 본문 - - - -카드 정의는 카드마다 증거를 만드는 태스크를 이름으로 지정한다. `jpa-flyway-migration` 의 그 태스크가 지금 무엇을 하는지가 이 기록의 전부다. - -## 카드 하나에 걸린 것 - -:::evidence key="a05-f033-jpa-flyway-migration" alt="준비도 카드 정의에서 이 카드의 상태와 생산자 태스크, 이 카드를 선행 조건으로 두는 카드 전수와 각각의 상태, R2 집계 게이트의 선행 조건 목록과 게이트가 카드 id 를 하드코딩한 자리, 그 태스크가 고정한 적용 집합 네 개, 두 스트림의 실제 마이그레이션 파일, 단언 파일과 각 마이그레이션의 최종 커밋 날짜, 그리고 같은 소스 세트가 이 실패 유형을 적어 둔 주석을 출력한 터미널 기록." caption="카드는 selected, 생산자는 postgresqlMigrationIntegrationTest · 이 카드를 선행 조건으로 두는 카드 열하나, 그중 selected 셋 · 고정 단언 넷은 47·62·89·123행 · 실제 스트림은 아홉 개와 둘 · 단언은 7월 31일 이후 미갱신 — 56줄 · exit 0" zoom="true" -::: - -카드 정의가 스스로 적는 선행 조건은 둘뿐이다. 반대 방향이 크다. 이 카드를 선행 조건으로 두는 카드가 열하나이고, 그중 `jpa-primary-foundation` 은 R2 집계 게이트다. - -## 네 시험 중 둘이 47행과 89행에서 멈춘다 - -:::evidence key="a05-f033-jpa-flyway-migration-run" alt="작업 트리가 깨끗한 것을 확인하고 카드의 생산자 태스크를 재실행 강제로 돌린 결과와 실패한 두 시험의 이름과 줄 번호, 통과·실패 수, 실패한 태스크 이름, 빌드 결과, 그리고 두 실패의 단언 메시지 전문을 출력한 터미널 기록. 채집 스크립트가 파이프와 || true 로 실패를 삼켜 스크립트 자신은 0 으로 끝난다. 빌드는 실패했다." caption="작업 트리 변경 0줄 · 재실행 강제 · 네 시험 중 둘 실패 · BUILD FAILED, 태스크 종료 1 · 기반 스트림은 9·10·11·12 가, 코어 스트림은 2 가 예상 밖 — 27줄 · 채집 스크립트 exit 0" zoom="true" -::: - -```text -PostgreSqlMigrationIntegrationTest > adoptsImmutableLegacyHistoryThenRunsTheIndependentCoreStream() FAILED - org.opentest4j.AssertionFailedError at PostgreSqlMigrationIntegrationTest.java:47 -PostgreSqlMigrationIntegrationTest > freshCoreStreamInitializesWithoutLegacyHistory() FAILED - org.opentest4j.AssertionFailedError at PostgreSqlMigrationIntegrationTest.java:89 -4 tests completed, 2 failed -> Task :adapter:outbound:persistence-jpa:postgresqlMigrationIntegrationTest FAILED -BUILD FAILED in 31s -``` - -늘어난 버전이 단언 메시지에 그대로 찍힌다. - -```text -Expecting actual: - ["1", "3", "4", "5", "6", "9", "10", "11", "12"] -to contain exactly (and in same order): - ["1", "3", "4", "5", "6"] -but some elements were not expected: - ["9", "10", "11", "12"] -``` - -단언 파일과 마이그레이션 파일의 최종 커밋 날짜를 나란히 놓으면 언제부터인지가 나온다. - -```text -2026-07-31 PostgreSqlMigrationIntegrationTest.java -2026-08-15 jpa/core/V2, postgresql/V9 (같은 커밋) -2026-08-18 postgresql/V10 -2026-08-28 postgresql/V11, postgresql/V12 -``` - -## 넷 중 셋이 낡았다 - -같은 파일에 구조가 똑같은 고정 단언이 넷 있다. - -```java -47: .containsExactly("1", "3", "4", "5", "6"); -62: assertThat(appliedVersions(postgres, "flyway_jpa_core_history")).containsExactly("0", "1"); -89: assertThat(appliedVersions(fresh, "flyway_jpa_core_history")).containsExactly("1"); -123: assertThat(appliedVersions(interrupted, "flyway_readiness_interrupted")).containsExactly("1"); -``` - -62행은 47행과 같은 시험 안에 있어서, 47행이 죽으면 실행되지 않는다. 그 시험은 코어 스트림을 `0` 으로 baseline 한 뒤 migrate 하고 조회 도우미는 baseline 행까지 읽는다. 코어 스트림에는 V1 과 V2 가 있으므로 62행이 받을 값은 `0, 1, 2` 다. - -123행은 통과한다. `flyway_readiness_interrupted` 는 픽스처 스트림이라 마이그레이션이 늘지 않았다. - -## 같은 소스 세트가 이 실패를 이미 적어 두었다 - -```java -// This list is the stream's contract, not a note about its length: the lifecycle below -// disables, re-enables and interrupts the stream and asserts the applied set is unchanged -// each time. So a migration added to the stream belongs here, and the version that landed -// without being added is why the lane failed the first time anybody ran it. -List.of("0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"), -``` - -`PostgreSqlOptionalStreamLifecycle` 은 같은 소스 세트에 있고 스트림마다 표 이름과 버전 목록을 한 자리에 둔다. 마이그레이션을 더할 때 고칠 자리가 그 목록 하나다. 기반 스트림 쪽은 그 자리가 시험 본문 세 곳에 흩어져 있다. - -## 이름으로 부르는 워크플로는 없지만 그래프에는 있다 - -:::evidence key="a05-f033-jpa-flyway-migration-gate" alt="이 태스크를 실행하는 워크플로 둘과 각각의 방아쇠, 각 워크플로가 부르는 게이트 태스크, 매니페스트 태스크가 활성 카드의 준비도 태스크에 의존하는 코드, 풀 리퀘스트 게이트의 실제 태스크 그래프에 이 준비도 태스크가 들어 있음을 보이는 dry-run 출력, 그리고 R2 집계 게이트가 이름으로 요구하는 카드 일곱을 출력한 터미널 기록." caption="ci-quality-gates 는 pull_request 와 main push 에서 돌고 jpa-r2-evidence 는 수동 실행뿐 · 매니페스트 태스크가 활성 카드의 준비도 태스크에 dependsOn · dry-run 그래프에 postgresqlMigrationIntegrationTest 존재 — 37줄 · exit 0" zoom="true" -::: - -이 태스크를 이름으로 부르는 워크플로는 없다. 태그 레인 다섯 어디에도 속하지 않고 릴리스 게이트에도 없다. 대신 매니페스트를 만드는 태스크가 활성 카드마다 그 카드의 준비도 태스크에 의존하고, 이 태스크가 그중 하나다. - -그래서 `verifyJpaCandidateEvidence` 의 그래프에 이 태스크가 들어온다. 그 게이트를 부르는 잡은 풀 리퀘스트마다, 그리고 main 으로 push 할 때마다 돈다. - -R2 집계 게이트 쪽은 수동 실행뿐이고, 그 게이트는 카드 일곱의 매니페스트가 전부 R2 여야 통과한다. 그 일곱에 이 카드가 이름으로 들어 있다. 다만 게이트는 그 검사에 닿기 전에 멈춘다. 매니페스트를 만드는 태스크가 먼저 실패하기 때문이다. - -## 확인하지 못한 것 - -47행을 고친 뒤 62행이 실패하는 것을 실행으로 보지는 않았다. 소스를 고치지 않는 조건이라, 62행이 받을 값은 같은 스트림을 baseline 없이 도는 다른 시험이 실제로 낸 `1, 2` 와 조회 도우미가 baseline 행까지 읽는다는 것에서 도출했다. - -같은 준비도 묶음의 다른 레인 하나는 이 컨테이너의 네트워크 구성 때문에 실패한다. 인증서가 로컬 이름으로 발급되어 있고 검증 모드가 전체 검증이므로, 컨테이너의 매핑 포트가 시험 JVM 의 루프백에서 열려 있어야 한다. 이 분석은 Docker 소켓을 공유하는 형제 컨테이너에서 실행돼 매핑 포트가 브리지 주소에만 열렸고 실패는 연결 예외다. - -그 레인은 건너뛰지 않고 실패하도록 설계되어 있으므로 동작 자체는 의도대로다. 다만 건너뛰지 않는다는 선택의 대가로 Docker 호스트와 시험 JVM 이 루프백을 공유하는 환경이 그 레인의 암묵적 전제가 된다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-test-names-that-assert-what-their-bodies-do-not.md b/docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-test-names-that-assert-what-their-bodies-do-not.md deleted file mode 100644 index bb236c0..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-test-names-that-assert-what-their-bodies-do-not.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -kind: CASE -slug: test-names-that-assert-what-their-bodies-do-not -title: 이름이 검사한다고 말하는 것을 본문이 검사하지 않는 테스트 넷 -topic: what-a-gate-does-not-prove -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:test-names-that-assert-what-their-bodies-do-not -evidenceCapturedOn: 2026-09-01 -assets: - - key: test-names-that-assert-what-their-bodies-do-not - file: ../../../final/evidence/rendered/test-names-that-assert-what-their-bodies-do-not.svg -evidence: - - ../../../final/evidence/raw/test-names-that-assert-what-their-bodies-do-not.txt -source: - - 원본 분석 절은 final/document.md#a19-messaging-pulsar-experimental §17.3 · final/document.md#a19-messaging-nats-experimental §17.4 · final/document.md#a20-grpc-advanced-bootstrap §17.4 이다. ---- - -# 이름이 검사한다고 말하는 것을 본문이 검사하지 않는 테스트 넷 - -네 테스트가 이름과 설명 메시지로 무엇을 붙든다고 말하는데 본문이 다른 것을 확인하거나 아무것도 배제하지 않는다. 어느 것도 잘못된 동작을 통과시키지는 않는다. 틀리는 것은 커버리지 지도다. - -## 관계 - -- **빠뜨림이 통과가 되는 게이트는 게이트가 아니다** - 같은 계열의 규칙이다. -- **다중 타깃 검증을 확인한다는 테스트가 다른 가드에 걸려 통과했다** - 같은 형태가 다른 리프에서 나타난 사례다. -- **산문이 선언한 게이트는 빌드에 있는 게이트가 아니다** - 이 사례가 그 규칙이 왜 오래 눈에 띄지 않는지를 설명한다. - -## 문제 - -테스트 이름은 이 저장소에서 문장으로 쓰인다. 무엇을 붙드는지를 이름이 말하고, 설명 메시지가 왜 그것을 붙드는지를 말한다. 그 관행이 읽기를 빠르게 만든다. - -빠른 읽기는 이름을 커버리지 지도로 쓰게 만든다. 어떤 규칙에 테스트가 있는지 확인할 때 본문 대신 이름을 센다. - -## 결론 - -네 곳에서 이름과 본문이 갈린다. - -첫째. 이름이 검증기가 키 공유 프로파일을 받아들인다고 말하는데 본문에 검증기가 없다. 프로파일 생성자만 부르고 예외가 나지 않는 것을 확인한다. 그 리프에서 검증기를 언급하는 유일한 테스트 이름이 이것이라, 이름만 읽으면 검증기에 커버리지가 있다고 읽힌다. 실제로 그 검증기는 저장소 전체에서 자기 선언 한 줄 말고 아무 데도 없다. - -둘째. 이름이 소비자 팩토리 없이 만든 전송이 등록을 거절한다고 말한다. 그 거절은 기본 팩토리가 던지는 예외다. 본문은 등록 메서드에 널을 넘겨 첫 줄의 널 검사에 걸린다. 단언하는 예외 타입도 그 널 검사의 것이다. 겨냥한 거절 코드는 저장소에서 한 번도 실행되지 않는다. - -셋째. 설명 메시지가 모든 결과가 영으로 보고되던 회귀를 막는다고 적는다. 단언은 경과 시간이 영 이상이라는 것이다. 경과 시간은 시작 시점에서 잰 값이라 음수가 될 수 없으므로 이 단언은 구현이 무엇을 하든 통과한다. 영을 배제하려던 단언이 영을 통과시킨다. - -넷째. 이름이 상위 등급이 되는 데 더 긴 담금이 필요하다고 말한다. 본문이 실제로 평가하는 전이는 철회 방향이다. 겨냥한 등급이 열거형에 없어서 그것을 밟을 방법이 없었고, 남은 값 중 하나를 골라야 했던 흔적이다. - -넷 다 통과한다. 그리고 넷 다 그 아래에 진짜 공백을 하나씩 두고 있다. 첫째와 둘째는 호출자 없는 검증기와 실행되지 않는 거절 경로, 셋째는 측정되지 않는 경과 시간, 넷째는 존재하지 않는 등급을 위해 쓰인 임계값이다. 이름이 커버리지를 주장했기 때문에 그 공백들이 오래 보이지 않았다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Gradle : 9.0.0 -확인 방식 : 테스트 본문과 이름 및 설명 메시지 대조, 그리고 단언 의미론 확인 -소스 수정 : x - -## 재현 조건 - -1. 각 테스트의 이름과 설명 메시지가 무엇을 주장하는지 적는다. -2. 본문이 실제로 만드는 객체와 부르는 메서드를 나열한다. -3. 단언이 배제하는 값의 집합을 구한다. -4. 이름이 지목한 코드 경로에 도달하는지 확인한다. - -## 본문 - - - -네 형태가 같은 결과를 낳는다. - -## PulsarProfile 참조 위치 - -:::evidence key="test-names-that-assert-what-their-bodies-do-not" alt="코드베이스에서 PulsarProfile 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="PulsarProfile 코드베이스 검색 — 5줄 · exit 0" zoom="true" -::: - -## 넷이 각각 놓치는 것 - -1. 이름이 `theValidatorAccepts…` 인데 본문에 검증기가 없다 — `PulsarProfile` 생성자만 부른다. 그 리프에서 검증기를 언급하는 유일한 테스트 이름이라, 이름만 읽으면 커버리지가 있다고 읽힌다. -2. 이름이 "소비자 팩토리 없이 만든 전송이 등록을 거절한다" 인데 본문은 `register(null)` 을 불러 첫 줄의 널 검사에 걸린다 — 겨냥한 `PULSAR_CONSUMER_NOT_CONFIGURED` 는 한 번도 실행되지 않는다. -3. `as()` 가 "모든 결과가 `Duration.ZERO` 였다" 는 회귀를 막는다고 적는데 단언이 `isGreaterThanOrEqualTo(Duration.ZERO)` 라 `Duration.ZERO` 도 통과한다 — 구현이 무엇을 하든 참이다. -4. 이름이 "Stable default 가 되는 데 더 긴 담금이 필요하다" 인데 실제로 평가하는 전이는 `ADVANCED_STABLE → DISABLED`(철회)다. - -## 틀리는 것은 커버리지 지도다 - -어느 것도 잘못된 동작을 통과시키지는 않는다. 그래서 그 아래의 진짜 공백이 오래 눈에 띄지 않았다. - -## 확인하지 못한 것 - -테스트를 실행하지 않았다. 판정은 단언 의미론과 호출 경로에 대한 것이다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-three-versions-declared-one-executed.md b/docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-three-versions-declared-one-executed.md deleted file mode 100644 index 5961cfb..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-three-versions-declared-one-executed.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -kind: CASE -slug: three-versions-declared-one-executed -title: 릴리스 레인이 매트릭스 세 버전 중 첫 번째만 돌리고 세 개를 커버로 기록했다 -topic: what-a-gate-does-not-prove -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:three-versions-declared-one-executed -evidenceCapturedOn: 2026-09-01 -assets: - - key: three-versions-declared-one-executed - file: ../../../final/evidence/rendered/three-versions-declared-one-executed.svg -evidence: - - ../../../final/evidence/raw/three-versions-declared-one-executed.txt -source: - - 원본 분석 절은 final/document.md#a05 §15.4 이다. ---- - -# 릴리스 레인이 매트릭스 세 버전 중 첫 번째만 돌리고 세 개를 커버로 기록했다 - -계약 suite 가 여러 PostgreSQL major 를 선택받으면 첫 번째만 실제로 돌면서 지원 매트릭스에는 세 개 모두 전체 계약 suite 로 기록했다. 지금은 여러 개가 선택되면 시작 자체를 거부한다. - -## 관계 - -- **빠뜨림이 통과가 되는 게이트는 게이트가 아니다** - 커버리지 기록과 실제 실행이 어긋난 형태다. -- **컨테이너가 필요한 레인의 실제 결과를 실행으로 확인하지 않았다** - 이 레인의 현재 상태에 대한 미해결 질문이다. - -## 문제 - -계약 suite 는 여러 major 를 한 번에 커버할 수 없다. 그러려면 버전마다 모든 테스트를 반복해야 하고 그것은 별도의 작업이다. - -그런데 예전에는 여러 개가 선택되면 첫 번째 하나로 컨테이너를 띄우고 진행했다. 지원 매트릭스에는 세 개 모두 전체 계약 suite 로 기록됐다. - -18 에서만 깨지는 매핑이나 Hibernate 방언 차이나 Flyway 업그레이드가 초록불 릴리스와 함께 출하될 수 있는 상태였다. - -## 결론 - -여러 개가 선택되면 시작을 거부한다. - -정확히 하나가 아니면 IllegalStateException 을 던지고, 메시지가 왜 그런지와 어떻게 해야 하는지를 함께 적는다. 하나의 JVM 에서 정확히 하나의 major 에 대해 돌아야 하고, 실행을 major 당 한 잡으로 나누라는 것이며, 여기서 여러 버전을 선택하면 첫 번째만 조용히 테스트되면서 전부에 대한 커버리지가 보고된다는 것이다. - -정직한 형태는 major 당 한 잡이다. CI 매트릭스가 팬아웃하고 이 코드는 그렇지 않은 척하기를 거부한다. - -같은 계열의 문제가 릴리스 레지스트리에도 기록되어 있다. 예전에는 지원 매트릭스가 언급하는 모든 PostgreSQL NN 문자열과 모든 게이트 표기를 수집했고, 일치가 어느 표에서 왔는지도 그 행이 어떤 지원 등급을 선언했는지도 알지 못했다. 그래서 실험 등급 PG19 가 안정 major 와 같은 목록에 들어가고, 문장에서 한 번 언급된 버전이 지원으로 집계되고, PG17 을 실험으로 강등해도 문서 어딘가에 문자열이 살아 있으면 아무것도 바뀌지 않았다. 게이트 목록은 두 곳에 더 있었고 그중 어느 것도 실제로 존재하는 Gradle 태스크에 묶여 있지 않았다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Gradle : 9.0.0 -데이터베이스 : PostgreSQL, Testcontainers -근거 : 레지스트리 주석과 코드 javadoc 이 사후 기록으로 남긴 회귀 -소스 수정 : x - -## 재현 조건 - -1. JpaPlatformContractSupport 의 인자 없는 start 메서드와 그 위 javadoc 을 읽는다. -2. 선택 버전이 하나가 아닐 때의 예외 메시지를 확인한다. -3. release-registry.json 의 주석 배열을 읽는다. 이전 수집 방식의 문제가 열거되어 있다. - -## 본문 - - - -릴리스 레인이 `-Pjpa.matrix.versions=16,17,18`을 `selectedVersions().get(0)`을 쓰는 지원 클래스에 넘겼고 **통합 suite 전체가 PostgreSQL 16에 대해 돌았다.** - -## 레인이 넘긴 값과 지원 클래스가 쓴 값 - -:::evidence key="three-versions-declared-one-executed" alt="분석 문서 final/document.md#a05 에서 이 기록의 근거 절을 그대로 잘라낸 16줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a05 발췌 — 16줄" zoom="true" -::: - -## 나머지 두 major 를 커버로 만든 것 - -3개 assertion짜리 smoke test 하나다. 지원 표는 그 힘으로 17과 18을 완전 커버로 기록했다. - -## 수정이 세 곳을 동시에 건드린다 - -`start()`가 다중 선택을 아예 거부하고, workflow가 major당 job으로 fan-out하며, promotion job이 세 major의 증거가 **같은 commit SHA**를 담기를 요구한다. - -## 확인하지 못한 것 - -CI 워크플로가 실제로 major 당 한 잡으로 팬아웃하는지 실행으로 확인하지 않았다. 이 사이클에서 jpaPlatformFailureTest 를 포함한 일부 컨테이너 레인은 돌리지 않았다. - - diff --git a/docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-two-files-name-a-build-gate-that-no-build-runs.md b/docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-two-files-name-a-build-gate-that-no-build-runs.md deleted file mode 100644 index 65539a3..0000000 --- a/docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-two-files-name-a-build-gate-that-no-build-runs.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -kind: CASE -slug: two-files-name-a-build-gate-that-no-build-runs -title: 두 파일이 같은 검증기를 "빌드를 실패시키는 것"이라 적고, 어떤 빌드도 그것을 부르지 않는다 -topic: what-a-gate-does-not-prove -project: clean-architecture-backend-template -status: 게시 전 -sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 -rootTreeNode: case:two-files-name-a-build-gate-that-no-build-runs -evidenceCapturedOn: 2026-09-01 -assets: - - key: two-files-name-a-build-gate-that-no-build-runs - file: ../../../final/evidence/rendered/two-files-name-a-build-gate-that-no-build-runs.svg -evidence: - - ../../../final/evidence/raw/two-files-name-a-build-gate-that-no-build-runs.txt -source: - - 원본 분석 절은 final/document.md#a20-grpc-proto-contract §17.4 · final/document.md#a20-grpc-codegen §17.1 이다. ---- - -# 두 파일이 같은 검증기를 "빌드를 실패시키는 것"이라 적고, 어떤 빌드도 그것을 부르지 않는다 - -스키마 규칙 엔진과 스키마 거버넌스 정책이 서로를 가리키며 상대가 이 저장소의 빌드 게이트라고 적는다. 두 쪽 다 어떤 빌드 스크립트나 CI 정의에도 나오지 않는다. - -## 관계 - -- **산문이 선언한 게이트는 빌드에 있는 게이트가 아니다** - 이 사례가 그 규칙을 만들었다. -- **아무도 돌리지 않는 레인의 게이트는 마지막으로 돌린 사람이 본 것을 보고한다** - 그 규칙의 앞 단계에 해당한다. 여기서는 레인이 도는지가 아니라 게이트가 존재한다는 문장의 근거를 묻는다. -- **릴리스 게이트가 읽는 증거를 아무도 생산하지 않는다** - 같은 가족의 다른 층위에서 나타난 형태다. -- **build gate라 불리는 catalog drift 검사가 어디에서도 실행되지 않는다** - 다른 어댑터에서 같은 형태가 나타난 사례다. - -## 문제 - -이 저장소는 스키마 도구 체인의 CLI 를 싣지 않기로 했다. 그 결정의 근거가 두 곳에 적혀 있고 논증이 같다. 바이너리가 없을 때 조용히 통과하는 게이트는 없는 것보다 나쁘므로, 커밋된 스키마에서 같은 판정을 계산하는 자바 규칙 엔진을 대신 둔다는 것이다. - -그 논증의 결론이 문장으로 남아 있다. 스키마 모듈 설정 파일의 머리 주석은 규칙 엔진이 이 저장소의 빌드를 실패시킨다고 적는다. 코드 생성 거버넌스 정책의 javadoc 도 같은 문장을 갖는다. - -같은 정책이 네 개의 수명주기 태스크 이름을 상수로 들고 있고, 그 javadoc 은 이름을 정책에 두는 이유를 적는다. 단계가 사라지면 아무도 모르게 사라지는 대신 테스트가 실패하게 만들기 위해서라는 것이다. - -## 결론 - -규칙 엔진을 부르는 빌드 코드가 없다. - -Gradle 태스크도, 검증 훅도, 다른 모듈의 호출도 없다. 규칙 아홉 개를 실제로 실행하는 것은 그 리프의 단위 테스트 하나이고, 그 테스트가 판정하는 대상은 하드코딩된 두 파일 목록이다. 세 번째 스키마 파일을 같은 디렉터리에 추가하면 그 목록을 함께 고치기 전까지 판정되지 않는다. - -네 태스크 이름도 마찬가지다. 그 이름들은 어떤 빌드 파일에도 없다. 그것을 확인한다는 테스트는 목록을 리터럴과 비교하고, 다시 목록을 자기 자신과 비교한다. 어느 단언도 빌드가 그 단계를 등록했는지 묻지 않는다. - -두 문장이 서로를 가리킨다. 설정 파일은 CLI 가 없으니 자바 검증기가 게이트라고 하고, 정책은 태스크 이름이 CI 의 계약이고 자바 검증기가 실제 게이트라고 한다. 두 쪽 다 상대가 게이트라고 말하고 어느 쪽도 실행되지 않는다. - -CLI 가 없다는 사실 자체는 이미 밝혀져 있으므로 태스크가 없는 것은 놀랍지 않다. 어긋난 것은 그 자리를 무엇이 대신하는지에 대한 문장이다. - -## 검증 환경 - -OpenJDK : 21.0.12 -Gradle : 9.0.0 -확인 방식 : 빌드 스크립트와 CI 정의에 대한 타입 이름 및 태스크 이름 전수 검색, 그리고 테스트 단언 목록 확인 -소스 수정 : x - -## 재현 조건 - -1. 스키마 모듈 설정 파일의 머리 주석에서 게이트를 지목하는 문장을 확인한다. -2. 코드 생성 거버넌스 정책의 javadoc 에서 같은 문장을 확인한다. -3. 그 규칙 엔진의 타입 이름으로 빌드 스크립트와 CI 정의를 검색한다. -4. 네 수명주기 태스크 이름으로 같은 대상을 검색한다. -5. 규칙 엔진을 실행하는 테스트가 어떤 파일 목록을 쓰는지 확인한다. - -## 본문 - - - -두 파일이 같은 논증을 편다 — Buf CLI 가 이 툴체인에 없으므로 자바로 구현한 규칙 엔진이 그 자리를 대신하고, "그것이 실제로 이 저장소의 빌드를 실패시킨다". `buf.yaml` 주석과 `GrpcBufPolicy` javadoc 이 각각 그 문장을 갖는다. - -## GrpcBufPolicy 참조 위치 - -:::evidence key="two-files-name-a-build-gate-that-no-build-runs" alt="코드베이스에서 GrpcBufPolicy 를 검색한 출력 4줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcBufPolicy 코드베이스 검색 — 4줄 · exit 0" zoom="true" -::: - -## 아홉 규칙을 실행하는 것은 단위 테스트 하나다 - -`GrpcProtoContractValidator` 를 부르는 Gradle 태스크도, 검증 훅도, 다른 모듈의 호출도 없다. 그 테스트가 판정하는 대상은 **하드코딩된 두 파일**이다. - -## 반대 방향으로도 같다 - -`GrpcBufPolicy` 가 계약이라고 든 네 태스크 이름(`bufFormatCheck`·`bufLint`·`bufBuild`·`bufBreaking`)이 어떤 빌드 파일에도 없다. 그 javadoc 은 "a missing stage is a test failure rather than a stage nobody noticed was gone" 라고 적는데 테스트는 그 목록을 리터럴 및 자기 자신과 비교한다. 두 쪽이 서로를 게이트라고 가리키고 어느 쪽도 실행되지 않는다. - -## 확인하지 못한 것 - -리플렉션이나 서비스 로더처럼 이름이 문자열로만 등장하는 호출 형태는 배제하지 못했다. 이 기록은 타입 이름과 태스크 이름에 대한 검색 결과에 근거한다. - - diff --git a/docs/keycloak/final/assets/tech-log-studio/ap3-bff-session-flow.svg b/docs/keycloak/final/assets/tech-log-studio/ap3-bff-session-flow.svg new file mode 100644 index 0000000..8a70424 --- /dev/null +++ b/docs/keycloak/final/assets/tech-log-studio/ap3-bff-session-flow.svg @@ -0,0 +1,84 @@ + + +AP3 session cookie에서 BFF downstream Bearer까지 +브라우저가 Authorization header 없이 AP3_SESSION cookie로 /bff/api/me를 호출한다. BFF는 현재 Authentication으로 authorized-client manager를 호출해 server-held access token을 얻고 Resource Server의 /api/me에 Bearer header를 붙인다. Resource Server가 JWT를 검증해 사용자 JSON을 반환하면 BFF가 ResponseEntity로 받아 브라우저에 중계한다. 브라우저 session cookie는 downstream으로 전달되지 않는다. +{"techviz":{"spec_version":"1.1","id":"ap3-bff-session-flow","profile":"sequence"},"source_context":{"document":"document.md","document_sha256":"df4d1a604c74e756672b5b40510abfedb8c67b39af280a5f51985ea9972f5371","anchor":{"kind":"marker","value":"ap3-bff-session-flow","line":908}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true} + + + + + + + + +브라우저 + + +Spring BFF + + +Authorized-client +store + + +Resource Server + + + +1. GET /bff/api/me + AP3_SESSION + + +2. authorize current principal + + +3. server-held access token + + +4. GET /api/me · Bearer access token + + +5. subject · username · issuer · audience + + +6. BFF ResponseEntity → browser JSON + diff --git a/docs/keycloak/final/assets/tech-log-studio/ap3-csrf-boundary.svg b/docs/keycloak/final/assets/tech-log-studio/ap3-csrf-boundary.svg new file mode 100644 index 0000000..17ec905 --- /dev/null +++ b/docs/keycloak/final/assets/tech-log-studio/ap3-csrf-boundary.svg @@ -0,0 +1,104 @@ + + +AP3의 masked CSRF 응답과 raw POST credential +왼쪽의 BFF CSRF endpoint에서 두 결과가 갈라진다. XSRF-TOKEN cookie에는 raw token이 저장되고 JSON body에는 XOR와 Base64로 masked된 token 및 headerName이 담긴다. 두 결과는 SPA의 POST 조립 단계로 모이지만, JSON에서는 headerName만 사용하고 실제 X-XSRF-TOKEN 값은 document.cookie에서 읽은 raw token이다. POST에는 같은 raw 값을 가진 cookie와 header가 함께 도달하고 Spring CSRF filter가 일치 여부를 확인한다. +{"techviz":{"spec_version":"1.1","id":"ap3-csrf-boundary","profile":"component-flow"},"source_context":{"document":"document.md","document_sha256":"df4d1a604c74e756672b5b40510abfedb8c67b39af280a5f51985ea9972f5371","anchor":{"kind":"marker","value":"ap3-csrf-boundary","line":858}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true} + + + + + + + + + +Cookie raw = Header raw + + +masked JSON + + +Set-Cookie · raw + + +headerName only + + +document.cookie · raw + + +BFF · /bff/csrf + + + +JSON body · masked + +token = XOR/Base64 +headerName metadata +POST token 값으로 미사용 + + + +Browser cookie · raw + +XSRF-TOKEN +JavaScript-readable +실제 header data source + + + +SPA POST 조립 + +Cookie 자동 첨부 +document.cookie raw → header +JSON headerName만 사용 + + + +Spring CSRF filter + +raw cookie = raw header 비교 +일치 → controller +부재·불일치 → 403 + + diff --git a/docs/keycloak/final/assets/tech-log-studio/ap4-edge-forward-auth-flow.svg b/docs/keycloak/final/assets/tech-log-studio/ap4-edge-forward-auth-flow.svg new file mode 100644 index 0000000..1a60b42 --- /dev/null +++ b/docs/keycloak/final/assets/tech-log-studio/ap4-edge-forward-auth-flow.svg @@ -0,0 +1,83 @@ + + +AP4 proxy session에서 trusted identity JSON까지 +브라우저가 AP4_SESSION cookie로 Nginx의 /api/edge를 호출한다. Nginx는 oauth2-proxy의 internal auth endpoint에 subrequest를 보내고 인증된 user와 email 결과를 받는다. 이어서 client가 보낸 동명 header를 사용하지 않고 oauth2-proxy 결과와 Nginx 환경의 internal token으로 /edge/me 요청을 새로 조립한다. Spring controller가 user header와 internal token을 함께 확인해 identity JSON을 만들고 Nginx가 브라우저에 전달한다. +{"techviz":{"spec_version":"1.1","id":"ap4-edge-forward-auth-flow","profile":"sequence"},"source_context":{"document":"document.md","document_sha256":"df4d1a604c74e756672b5b40510abfedb8c67b39af280a5f51985ea9972f5371","anchor":{"kind":"marker","value":"ap4-edge-forward-auth-flow","line":1108}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true} + + + + + + + + +브라우저 + + +Nginx edge + + +oauth2-proxy + + +Spring upstream + + + +1. GET /api/edge + AP4_SESSION + + +2. internal /oauth2/auth subrequest + + +3. authenticated user + email + + +4. GET /edge/me · trusted headers + internal token + + +5. trusted identity JSON + + +6. pattern + user + email + identityHeader + diff --git a/docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/case/case-ap3-bff-session-csrf.md b/docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/case/case-ap3-bff-session-csrf.md index cfa35e2..317df7a 100644 --- a/docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/case/case-ap3-bff-session-csrf.md +++ b/docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/case/case-ap3-bff-session-csrf.md @@ -16,6 +16,8 @@ assets: file: ../../../final/assets/tech-log-studio/ap3-bff-custody.svg - key: ap3-csrf-split-501dd1f7 file: ../../../final/assets/tech-log-studio/ap3-csrf-split.svg + - key: ap3-bff-session-flow + file: ../../../final/assets/tech-log-studio/ap3-bff-session-flow.svg sourceRevision: keycloak-patterns-lab@2026-08 source: - final/document.md#검토한-선택지와-막힌-지점-ap3 @@ -141,6 +143,9 @@ BFF(Backend For Frontend)는 화면에 필요한 API를 브라우저 대신 호 ### 쿠키 하나로 시작한 요청이 Bearer 요청이 된다 +:::evidence key="ap3-bff-session-flow" alt="브라우저에서 Spring BFF, Authorized-client store, Resource Server로 이어지는 여섯 단계 흐름. AP3_SESSION을 실은 GET /bff/api/me로 시작해 BFF가 현재 principal을 authorize하고 저장소에서 server-held access token을 받는다. 그 토큰으로 GET /api/me를 Bearer로 부르고 subject·username·issuer·audience를 받아 브라우저에 JSON으로 돌려준다." caption="" zoom="true" +::: + 브라우저가 `/bff/api/me`를 부를 때 요청에 붙는 자격 증명은 쿠키뿐이라, `Authorization` 헤더도 없고 브라우저 코드에는 액세스 토큰을 담는 변수도 없다. ```http label="브라우저 입력 — cookie 하나" diff --git a/docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/concept/concept-cookie-auth-csrf.md b/docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/concept/concept-cookie-auth-csrf.md index 83cb9a9..9365792 100644 --- a/docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/concept/concept-cookie-auth-csrf.md +++ b/docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/concept/concept-cookie-auth-csrf.md @@ -10,6 +10,9 @@ status: 게시 전 version: 4 basisVersion: Spring Security 6 CSRF · AP3 BFF 구성 studio: "https://hyeonworks.com/studio/documents/5c8f12d5-1ead-469b-8e91-2de69401df48/edit" +assets: + - key: ap3-csrf-boundary + file: ../../../final/assets/tech-log-studio/ap3-csrf-boundary.svg sourceRevision: keycloak-patterns-lab@2026-08 source: - final/document.md#선택의-이유와-지킨-경계-ap3 @@ -60,6 +63,9 @@ Cookie: AP3_SESSION= } ``` +:::evidence key="ap3-csrf-boundary" alt="BFF의 /bff/csrf 하나에서 두 갈래가 갈리는 그림. Set-Cookie로 나가는 CSRF 쿠키에는 가리지 않은 원본 값이 들어가고 JSON 본문에는 가린 토큰과 headerName이 들어간다. 브라우저 코드는 JSON에서 headerName만 쓰고 실제 헤더 값은 쿠키의 원본 값을 쓴다. Spring CSRF filter가 raw cookie와 raw header를 대조해 일치하면 controller로 보내고 부재나 불일치면 403을 낸다." caption="" zoom="true" +::: + ## body의 token과 cookie의 값은 다르다 같은 CSRF 값이 응답 본문, 쿠키, 요청 헤더 세 곳에 서로 다른 형태로 놓인다. diff --git a/docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/concept/concept-forward-auth-and-auth-request.md b/docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/concept/concept-forward-auth-and-auth-request.md index a309d5a..383b6db 100644 --- a/docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/concept/concept-forward-auth-and-auth-request.md +++ b/docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/concept/concept-forward-auth-and-auth-request.md @@ -10,6 +10,9 @@ status: 게시 전 version: 4 basisVersion: oauth2-proxy 7.15.2 · Nginx auth_request module studio: "https://hyeonworks.com/studio/documents/a3493786-d3fb-4b01-b1c5-ecb23c3d5497/edit" +assets: + - key: ap4-edge-forward-auth-flow + file: ../../../final/assets/tech-log-studio/ap4-edge-forward-auth-flow.svg sourceRevision: keycloak-patterns-lab@2026-08 source: - final/document.md#선택의-이유와-지킨-경계-ap4 @@ -34,6 +37,9 @@ forward-auth는 실제 요청을 업스트림으로 넘기기 전에 별도의 ## 요청 하나가 두 번 평가된다 +:::evidence key="ap4-edge-forward-auth-flow" alt="브라우저에서 Nginx edge, oauth2-proxy, Spring upstream으로 이어지는 여섯 단계 흐름. AP4_SESSION을 실은 /api/edge 요청이 들어오면 Nginx가 internal /oauth2/auth로 subrequest를 보내 authenticated user와 email을 받는다. 그 값으로 만든 trusted header와 internal token을 붙여 /edge/me를 부르고, upstream이 돌려준 trusted identity JSON이 브라우저로 나간다." caption="" zoom="true" +::: + 브라우저 요청이 들어와도 Nginx는 업스트림을 바로 호출하지 않는다. 업스트림은 Nginx가 요청을 최종으로 넘기는 뒤쪽 서버이고, 이 구성에서는 `app:8081`의 Spring 애플리케이션이다. 바로 넘기지 않는 것은 `location /`에 다음 지시어가 있기 때문이다. ```nginx label="general location의 auth_request" diff --git a/docs/keycloak/tech-log-studio/tech-log-tree.json b/docs/keycloak/tech-log-studio/tech-log-tree.json index 924c0f4..ffd7707 100644 --- a/docs/keycloak/tech-log-studio/tech-log-tree.json +++ b/docs/keycloak/tech-log-studio/tech-log-tree.json @@ -57,6 +57,41 @@ "BLOCKED": "원본이 불완전하거나 서로 어긋난다" } }, + "assetLedger": { + "note": "final/assets/ 의 그림 13장 가운데 글감에 배정한 것과, 배정하지 않은 것의 이유를 적는다. verify-project-layout.py 의 「기록이 쓰지 않는 SSOT 그림」이 세는 숫자가 여기서 설명된다", + "assigned": [ + "ap3-bff-session-flow", + "ap3-csrf-boundary", + "ap4-edge-forward-auth-flow" + ], + "unassigned": [ + { + "asset": [ + "ap1-direct-architecture", + "ap2-mediator-architecture", + "ap3-bff-architecture", + "ap4-edge-trust-architecture" + ], + "reason": "같은 구조를 그린 그림이 assets/tech-log-studio/ 에 따로 있고 기록이 그것을 쓴다. 한 기록에 같은 것을 두 번 그리지 않는다" + }, + { + "asset": [ + "ap1-browser-bearer-flow", + "ap2-mediator-handoff-flow" + ], + "reason": "마지막 단계인 /api/me 응답 4필드(subject·username·issuer·audience)를 두 기록의 본문이 말하지 않는다. 근거는 final/document.md L474 에 있으므로 본문을 먼저 보강해야 이을 수 있다 — 그림만 넣으면 설명 없는 주장이 남는다" + }, + { + "asset": [ + "four-pattern-request-boundaries", + "credential-custody-map", + "credential-contract-migration", + "login-api-phase-split" + ], + "reason": "네 패턴을 나란히 비교하는 그림이라 붙을 자리가 Reference 인데 Reference 에는 본문이 없다. 이 비교를 담을 Case 나 Concept 이 아직 없다" + } + ] + }, "topics": { "oauth-oidc-auth-boundary": { "topic": "oauth-oidc-auth-boundary", @@ -93,6 +128,9 @@ "assets": [ "ap1-custody-v3-6e0376d2" ], + "assetFiles": [ + "ap1-credential-custody" + ], "evidenceFiles": [] }, { @@ -125,6 +163,9 @@ "assets": [ "ap2-split-custody-779cb791" ], + "assetFiles": [ + "ap2-split-custody" + ], "evidenceFiles": [] }, { @@ -150,6 +191,9 @@ "decision:bff-owns-token-when-browser-must-not", "question:bff-session-authorized-client-store" ], + "ssot-assets": [ + "ap3-bff-session-flow" + ], "kind": "case", "publication": "게시됨", "file": "oauth-oidc-auth-boundary/case/case-ap3-bff-session-csrf.md", @@ -157,7 +201,13 @@ "studioId": "d85bd6af-7599-4ef7-9407-6609927d5b5c", "assets": [ "ap3-bff-custody-82fa18bd", - "ap3-csrf-split-501dd1f7" + "ap3-csrf-split-501dd1f7", + "ap3-bff-session-flow" + ], + "assetFiles": [ + "ap3-bff-custody", + "ap3-csrf-split", + "ap3-bff-session-flow" ], "evidenceFiles": [] }, @@ -191,6 +241,9 @@ "assets": [ "ap4-edge-trust-1cff2399" ], + "assetFiles": [ + "ap4-edge-trust" + ], "evidenceFiles": [] } ], @@ -214,6 +267,7 @@ "status": "게시 전", "studioId": "75c6c657-3e03-47a0-a9d0-5637fce9dd3f", "assets": [], + "assetFiles": [], "evidenceFiles": [] }, { @@ -235,6 +289,7 @@ "status": "게시 전", "studioId": "bb5c37ae-2d94-48f7-ad4e-a37c61c3fd07", "assets": [], + "assetFiles": [], "evidenceFiles": [] }, { @@ -256,6 +311,7 @@ "status": "게시 전", "studioId": "87000d59-b69f-4010-9481-0b71c8bde32d", "assets": [], + "assetFiles": [], "evidenceFiles": [] }, { @@ -271,12 +327,20 @@ "case:bff-session-csrf-responsibility", "reference:bff-authentication-design-criteria" ], + "ssot-assets": [ + "ap3-csrf-boundary" + ], "kind": "concept", "publication": "게시됨", "file": "oauth-oidc-auth-boundary/concept/concept-cookie-auth-csrf.md", "status": "게시 전", "studioId": "5c8f12d5-1ead-469b-8e91-2de69401df48", - "assets": [], + "assets": [ + "ap3-csrf-boundary" + ], + "assetFiles": [ + "ap3-csrf-boundary" + ], "evidenceFiles": [] }, { @@ -292,12 +356,20 @@ "case:identity-header-trust", "reference:forward-auth-identity-header-trust" ], + "ssot-assets": [ + "ap4-edge-forward-auth-flow" + ], "kind": "concept", "publication": "게시됨", "file": "oauth-oidc-auth-boundary/concept/concept-forward-auth-and-auth-request.md", "status": "게시 전", "studioId": "a3493786-d3fb-4b01-b1c5-ecb23c3d5497", - "assets": [], + "assets": [ + "ap4-edge-forward-auth-flow" + ], + "assetFiles": [ + "ap4-edge-forward-auth-flow" + ], "evidenceFiles": [] }, { @@ -319,6 +391,7 @@ "status": "게시 전", "studioId": "d99fdec9-fe9e-4e0f-a50b-6fb9b9ed5719", "assets": [], + "assetFiles": [], "evidenceFiles": [] } ], @@ -346,6 +419,7 @@ "status": "게시 중", "studioId": "3f886154-1b85-407b-bda4-57d28370e745", "assets": [], + "assetFiles": [], "evidenceFiles": [] }, { @@ -369,6 +443,7 @@ "status": "게시 중", "studioId": "39fdf472-82c4-43ed-abec-73de672f08ae", "assets": [], + "assetFiles": [], "evidenceFiles": [] }, { @@ -393,6 +468,7 @@ "status": "게시 중", "studioId": "ede6b9ce-eeed-40c8-9175-9e8116029395", "assets": [], + "assetFiles": [], "evidenceFiles": [] }, { @@ -416,6 +492,7 @@ "status": "게시 중", "studioId": "66c18e42-116c-459f-86bd-b7e4bf394866", "assets": [], + "assetFiles": [], "evidenceFiles": [] }, { @@ -441,6 +518,7 @@ "status": "게시 중", "studioId": "97eddd97-1096-426a-a2c6-a6c5bf1cd09f", "assets": [], + "assetFiles": [], "evidenceFiles": [] }, { @@ -465,6 +543,7 @@ "status": "게시 중", "studioId": "004dd0a2-5fb3-4f25-80c9-576f709de331", "assets": [], + "assetFiles": [], "evidenceFiles": [] }, { @@ -487,6 +566,7 @@ "status": "게시 중", "studioId": "1a00a640-8987-4075-a9e4-7ec023cdffbb", "assets": [], + "assetFiles": [], "evidenceFiles": [] }, { @@ -509,6 +589,7 @@ "status": "게시 전", "studioId": "", "assets": [], + "assetFiles": [], "evidenceFiles": [] } ], @@ -537,6 +618,7 @@ "status": "게시 중", "studioId": "18a5cde2-dd1e-4bff-9f1c-997577ae438f", "assets": [], + "assetFiles": [], "evidenceFiles": [] }, { @@ -561,6 +643,7 @@ "status": "게시 중", "studioId": "c72656b5-842d-45d9-b5f6-82b66b09d0b9", "assets": [], + "assetFiles": [], "evidenceFiles": [] }, { @@ -585,6 +668,7 @@ "status": "게시 중", "studioId": "9ae4ec71-a32e-49a7-88c2-f7368541c28d", "assets": [], + "assetFiles": [], "evidenceFiles": [] }, { @@ -609,6 +693,7 @@ "status": "게시 중", "studioId": "7ff40767-a00b-4db2-98f6-0cdfce8c8936", "assets": [], + "assetFiles": [], "evidenceFiles": [] } ], @@ -638,6 +723,7 @@ "status": "게시 중", "studioId": "19b55c39-c583-4161-9775-df954280a568", "assets": [], + "assetFiles": [], "evidenceFiles": [] }, { @@ -664,6 +750,7 @@ "status": "게시 중", "studioId": "8c1ebea7-204e-445c-9812-0421d9eb0e9c", "assets": [], + "assetFiles": [], "evidenceFiles": [] } ] diff --git a/scripts/build-tech-log-tree.py b/scripts/build-tech-log-tree.py index eb27908..cbc5c25 100755 --- a/scripts/build-tech-log-tree.py +++ b/scripts/build-tech-log-tree.py @@ -29,7 +29,8 @@ import techlog # noqa: E402 from techlog import KINDS # noqa: E402 ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -DERIVED = ("file", "publication", "status", "studioId", "assets", "evidenceFiles") +DERIVED = ("file", "publication", "status", "studioId", "assets", "assetFiles", + "evidenceFiles") def front_matter(path: str) -> tuple[dict, str]: @@ -84,6 +85,10 @@ def build(project: str) -> tuple[dict | None, list[str]]: "studioId": studio_id, "publication": "게시됨" if studio_id else "초안", "assets": re.findall(r"^ - key: (\S+)", text, re.M), + # 배정한 SSOT 그림을 기록이 실제로 쓰는지 대조하려면 key 가 아니라 파일이 필요하다 + "assetFiles": [os.path.basename(f)[:-4] if f.endswith(".svg") + else os.path.basename(f) + for f in re.findall(r"^ file: (\S+)", text, re.M)], "evidenceFiles": re.findall(r"^ - (\.\./\S+)", text, re.M), }) used.add(key) diff --git a/scripts/tests/test_tech_log_tree.py b/scripts/tests/test_tech_log_tree.py index 8ad4d65..e6e5214 100644 --- a/scripts/tests/test_tech_log_tree.py +++ b/scripts/tests/test_tech_log_tree.py @@ -109,7 +109,8 @@ class Fixture: def read(self) -> dict: return json.load(open(self.index_path, encoding="utf-8")) - def diagram(self, name: str, *, bundled: bool = True, with_source: bool = True) -> None: + def diagram(self, name: str, *, bundled: bool = True, with_source: bool = True, + cited: bool = False) -> None: assets = os.path.join(self.base, "final/assets") target = os.path.join(assets, name) if bundled else assets os.makedirs(target, exist_ok=True) @@ -118,6 +119,19 @@ class Fixture: src = os.path.join(self.base, "final/.techviz", name) os.makedirs(src, exist_ok=True) open(os.path.join(src, "spec.json"), "w", encoding="utf-8").write("{}") + if cited: + self.cite(name) + + def cite(self, name: str) -> None: + """그림을 Studio 자리로 복사하고 기록이 그것을 가리키게 한다 — 실제 작업 순서다.""" + studio_assets = os.path.join(self.base, "final/assets/tech-log-studio") + os.makedirs(studio_assets, exist_ok=True) + open(os.path.join(studio_assets, f"{name}.svg"), "w", encoding="utf-8").write("") + record = os.path.join(self.studio, "session-custody/case/case-session-split.md") + open(record, "w", encoding="utf-8").write(RECORD.replace( + "status: 게시 전\n", + f"status: 게시 전\nassets:\n - key: {name}\n" + f" file: ../../../final/assets/tech-log-studio/{name}.svg\n")) def __enter__(self): self._saved = (verifier.ROOT, builder.ROOT, layout.ROOT) @@ -293,6 +307,36 @@ class ContractTest(unittest.TestCase): self.assertIn("slug 가 두 글감에 있다", verifier.verify("fixture").errors) +class SsotAssetTest(unittest.TestCase): + """SSOT 가 이미 그린 그림을 글감에 배정하고, 기록이 그것을 쓰는지 본다.""" + + def _verify(self, fx): + with contextlib.redirect_stdout(io.StringIO()): + builder.main(["build", "fixture"]) + return verifier.verify("fixture") + + def test_assigned_diagram_the_record_uses_is_clean(self): + with Fixture(mutate(**{"ssot-assets": ["session-custody-map"]})) as fx: + fx.diagram("session-custody-map", cited=True) + self.assertEqual(self._verify(fx).errors, {}) + + def test_assigned_diagram_the_record_never_uses_is_an_error(self): + with Fixture(mutate(**{"ssot-assets": ["session-custody-map"]})) as fx: + fx.diagram("session-custody-map") + self.assertIn("배정한 SSOT 그림을 기록이 쓰지 않는다", self._verify(fx).errors) + + def test_assigned_diagram_that_does_not_exist_is_an_error(self): + with Fixture(mutate(**{"ssot-assets": ["never-drawn"]})) as fx: + self.assertIn("배정한 SSOT 그림이 final/assets 에 없다", self._verify(fx).errors) + + def test_assigned_evidence_the_record_never_cites_is_an_error(self): + with Fixture(mutate(**{"ssot-evidence": ["raw/explain/plan-a.txt"]})) as fx: + raw = os.path.join(fx.base, "final/evidence/raw/explain") + os.makedirs(raw) + open(os.path.join(raw, "plan-a.txt"), "w", encoding="utf-8").write("EXPLAIN\n") + self.assertIn("배정한 SSOT 증거를 기록이 쓰지 않는다", self._verify(fx).errors) + + class BuildTest(unittest.TestCase): def test_derived_fields_come_from_the_record_file(self): with Fixture(): @@ -330,11 +374,23 @@ class BuildTest(unittest.TestCase): class LayoutTest(unittest.TestCase): def test_a_diagram_with_its_source_is_clean(self): with Fixture() as fx: - fx.diagram("session-custody-map") + fx.diagram("session-custody-map", cited=True) report = layout.verify("fixture") self.assertEqual(report.errors, {}, report.errors) self.assertEqual(report.warns, {}, report.warns) + def test_ssot_diagram_no_record_cites_is_counted(self): + # 배정하지 않은 그림은 글을 쓸 때 새로 그리게 된다. keycloak 이 그렇게 됐다 + with Fixture() as fx: + fx.diagram("session-custody-map") + self.assertIn("기록이 쓰지 않는 SSOT 그림", layout.verify("fixture").warns) + + def test_a_project_with_no_record_yet_is_not_counted(self): + # 아직 안 쓴 것이지 안 쓰기로 한 것이 아니다 + with Fixture(with_record=False) as fx: + fx.diagram("session-custody-map") + self.assertNotIn("기록이 쓰지 않는 SSOT 그림", layout.verify("fixture").warns) + def test_svg_without_a_techviz_source_is_counted(self): with Fixture() as fx: fx.diagram("hand-drawn", with_source=False) diff --git a/scripts/verify-project-layout.py b/scripts/verify-project-layout.py index 1434069..9914a60 100755 --- a/scripts/verify-project-layout.py +++ b/scripts/verify-project-layout.py @@ -141,12 +141,19 @@ def verify(project: str) -> Report: if os.path.isdir(studio): wrong = 0 broken = 0 + records = 0 + cited_figures: set[str] = set() + cited_evidence: set[str] = set() for path in sorted(glob.glob(os.path.join(studio, "*", "*", "*.md"))): if os.path.basename(os.path.dirname(os.path.dirname(path))).startswith("_"): continue + records += 1 text = open(path, encoding="utf-8").read() for m in re.finditer(r"^ file: (\S+)$", text, re.M): target = os.path.normpath(os.path.join(os.path.dirname(path), m.group(1))) + # 이름이 같으면 같은 그림이다. tech-log-studio/ 사본도 SSOT 원본을 쓴 것으로 센다 + cited_figures.add(os.path.basename(target)[:-4] + if target.endswith(".svg") else os.path.basename(target)) if not os.path.exists(target): broken += 1 if broken <= 5: @@ -155,9 +162,28 @@ def verify(project: str) -> Report: continue if f"assets{os.sep}{STUDIO_ASSETS}{os.sep}" not in target: wrong += 1 + for m in re.finditer(r"^ - (\.\./\S*final/evidence/\S+)$", text, re.M): + cited_evidence.add( + os.path.normpath(os.path.join(os.path.dirname(path), m.group(1)))) if wrong: rep.warn("Studio 자산이 assets/tech-log-studio/ 밖에 있다", f"{wrong}건 — 다른 프로젝트는 전부 그 폴더를 쓴다") + + # ── SSOT 가 만들어 둔 것을 기록이 쓰고 있나 ──────────────────── + # 기록이 하나도 없는 프로젝트는 아직 안 쓴 것이지 안 쓰기로 한 것이 아니다 + if records: + unused_figures = [rel for stem, rel in _svg_stems(assets) + if stem not in cited_figures] + for rel in unused_figures: + rep.warn("기록이 쓰지 않는 SSOT 그림", f"final/assets/{rel}") + unused_evidence = [ + p for p in sorted(glob.glob(os.path.join(evidence, "raw", "**", "*"), + recursive=True)) + if os.path.isfile(p) + and os.path.basename(p) != "README.txt" + and p not in cited_evidence] + for p_ev in unused_evidence: + rep.warn("기록이 인용하지 않는 raw 증거", os.path.relpath(p_ev, base)) return rep diff --git a/scripts/verify-tech-log-tree.py b/scripts/verify-tech-log-tree.py index 25f4a3a..969a76d 100755 --- a/scripts/verify-tech-log-tree.py +++ b/scripts/verify-tech-log-tree.py @@ -233,6 +233,25 @@ def verify(project: str) -> Report: rep.error("decision-status 값이 계약에 없다", f"{where} — {status}") if has_contract and not _values(node, "relations"): rep.warn("관계가 없는 노드", where) + + # ── SSOT 가 이미 가진 그림·증거를 이 글감에 배정했는가 ───────── + # 배정만 해 두고 기록이 쓰지 않으면 글 쓸 때 새로 그리게 된다. 그것을 여기서 센다 + for name in _values(node, "ssot-assets"): + stem = os.path.basename(name)[:-4] if name.endswith(".svg") else os.path.basename(name) + if not glob.glob(os.path.join(base, "final", "assets", "**", f"{stem}.svg"), + recursive=True): + rep.error("배정한 SSOT 그림이 final/assets 에 없다", f"{where} — {name}") + elif node.get("file") and stem not in (node.get("assetFiles") or []): + rep.error("배정한 SSOT 그림을 기록이 쓰지 않는다", + f"{where} — {stem} — 기록의 assets 가 가리키지 않는다") + for name in _values(node, "ssot-evidence"): + rel_ev = name[len("final/evidence/"):] if name.startswith("final/evidence/") else name + if not os.path.exists(os.path.join(base, "final", "evidence", rel_ev)): + rep.error("배정한 SSOT 증거가 final/evidence 에 없다", f"{where} — {name}") + elif node.get("file") and not any( + f.endswith(rel_ev) for f in (node.get("evidenceFiles") or [])): + rep.error("배정한 SSOT 증거를 기록이 쓰지 않는다", + f"{where} — {rel_ev} — 기록의 evidence 가 가리키지 않는다") anchors = " ".join(_values(node, "source")) if anchors and ssot_rel not in anchors: rep.warn("근거가 SSOT 밖에만 있다", f"{where} — {anchors[:60]}")