docs(clean-architecture-backend-template): fold analysis into final and re-select one topic

- analysis/·source-index·state.json 을 final/document.md 제2부·제3부로 접었다. SSOT 는 하나다
- 파일럿 — commit-ambiguity-as-a-result 를 새 기준으로 재선별. 후보 14 → 글감 5
  (PROMOTE 5 · MERGE_INTO 3 · KEEP_IN_SSOT 4 · 보류 2). 기록 5건을 다시 썼고 그림 1개를
  techviz 로 만들었다
- 재선별이 잡은 것: 제1부 §6.2·§11.1 이 자기 §13.2 와 어긋나 있었다(레인을 안 돌렸다 vs
  돌렸다) — 정정. 이미 답이 나와 있던 Question 을 HEAD 재실행 질문으로 다시 세웠다.
  Concept 이 인용한 코드가 SSOT 에 없어 뺐다
- candidateScope·sourceRepository 기록. 나머지 43개 주제는 재선별 대기(PENDING 905)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-09-07 12:39:20 +09:00
co-authored by Claude Fable 5.1
parent 9d2a3725c5
commit b25357c48a
1080 changed files with 135560 additions and 120406 deletions
@@ -1,65 +0,0 @@
# 사람이 읽고 정해야 남는 것
자료가 뒷받침하는 것은 다 채웠다. 아래는 자료에 없어서 쓰면 지어내는 것이 된다.
하나를 끝내면 이 파일에서 지운다.
## 관계가 비어 있다
`root-tree.md` 는 949개 노드 중 374개에만 관계를 적었다. 나머지 575개 중 204개는 트리가
적은 규칙 — 「근거 사건은 같은 `source` 의 finding 이 소유한다」 — 으로 채웠다. 남은 것은
같은 분석 리프에 짝이 될 기록이 없는 경우다.
Studio 는 Decision 만 관계를 필수로 요구한다. 비어도 게시된다. 다만 관계 없는 기록은 다른
기록에서 도달할 수 없다.
- case 125건
- concept 239건
- reference 5건
- question 2건
## Question 에 가정·제약이 없다
이 칸은 확인하지 않고 전제한 것을 적는 자리다. 원본이 적지 않았으므로 지어내지 않는다.
그 질문을 다시 읽고 무엇을 전제했는지 정해야 채워진다.
- 가정 없음 14건
- tech-log-studio/contract-domain-and-bounds/question/openquestion-messaging-claim-check-f05.md
- tech-log-studio/declaration-and-document-drift/question/openquestion-messaging-inbox-jdbc-postgresql-f04.md
- tech-log-studio/drift-direction/question/openquestion-widen-doc-contract-assertions.md
- tech-log-studio/runtime-reachability-and-composition/question/openquestion-messaging-core-api-f01.md
- tech-log-studio/runtime-reachability-and-composition/question/openquestion-messaging-core-api-f03.md
- tech-log-studio/runtime-reachability-and-composition/question/openquestion-messaging-kafka-share-experimental-f03.md
- tech-log-studio/runtime-reachability-and-composition/question/openquestion-messaging-policy-f02.md
- tech-log-studio/runtime-reachability-and-composition/question/openquestion-messaging-reliability-api-f03.md
- tech-log-studio/runtime-reachability-and-composition/question/openquestion-messaging-schema-json-f03.md
- tech-log-studio/schema-and-data-contracts/question/openquestion-messaging-cloudevents-f04.md
- tech-log-studio/schema-and-data-contracts/question/openquestion-messaging-schema-protobuf-f03.md
- tech-log-studio/security-policy-enforcement/question/openquestion-messaging-security-f03.md
- tech-log-studio/transport-and-provider-semantics/question/openquestion-messaging-observability-f03.md
- tech-log-studio/transport-and-provider-semantics/question/openquestion-messaging-spring-cloud-stream-bridge-f02.md
- 제약 없음 14건
- tech-log-studio/contract-domain-and-bounds/question/openquestion-messaging-claim-check-f05.md
- tech-log-studio/declaration-and-document-drift/question/openquestion-messaging-inbox-jdbc-postgresql-f04.md
- tech-log-studio/drift-direction/question/openquestion-widen-doc-contract-assertions.md
- tech-log-studio/runtime-reachability-and-composition/question/openquestion-messaging-core-api-f01.md
- tech-log-studio/runtime-reachability-and-composition/question/openquestion-messaging-core-api-f03.md
- tech-log-studio/runtime-reachability-and-composition/question/openquestion-messaging-kafka-share-experimental-f03.md
- tech-log-studio/runtime-reachability-and-composition/question/openquestion-messaging-policy-f02.md
- tech-log-studio/runtime-reachability-and-composition/question/openquestion-messaging-reliability-api-f03.md
- tech-log-studio/runtime-reachability-and-composition/question/openquestion-messaging-schema-json-f03.md
- tech-log-studio/schema-and-data-contracts/question/openquestion-messaging-cloudevents-f04.md
- tech-log-studio/schema-and-data-contracts/question/openquestion-messaging-schema-protobuf-f03.md
- tech-log-studio/security-policy-enforcement/question/openquestion-messaging-security-f03.md
- tech-log-studio/transport-and-provider-semantics/question/openquestion-messaging-observability-f03.md
- tech-log-studio/transport-and-provider-semantics/question/openquestion-messaging-spring-cloud-stream-bridge-f02.md
- 선택지 없음 1건
- tech-log-studio/drift-direction/question/openquestion-widen-doc-contract-assertions.md
## 평문 칸에 백틱이 있다
본문을 뺀 모든 칸은 평문이라 백틱이 글자 그대로 보인다. 368개 칸이 그렇다. 지우면 되지만
식별자를 어떻게 드러낼지는 칸마다 다르므로 한 번에 훑고 정한다.
```bash
grep -rn '^## \(문제\|결론\|검증 환경\|재현 조건\|목적\|규칙\)' -A5 tech-log-studio/*/*/*.md | grep '`'
```
@@ -1,12 +0,0 @@
[
{
"code": "LOW_HEADING_SIGNATURE_DIVERSITY",
"severity": "warning",
"message": "본문 402개가 heading signature 12종만 사용함(3.0%). 고정 목차 template 반복 여부를 검토한다.",
"examples": [
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-a-red-test-misread-as-a-product-defect.body.md",
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-a-build-gate-that-is-not-in-the-build.body.md",
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-a-release-gate-with-no-evidence-producer.body.md"
]
}
]
@@ -1,12 +0,0 @@
[
{
"code": "LOW_HEADING_SIGNATURE_DIVERSITY",
"severity": "warning",
"message": "본문 386개가 heading signature 18종만 사용함(4.7%). 고정 목차 template 반복 여부를 검토한다.",
"examples": [
"/shared/Tech-Log-Document/clean-architecture-backend-template/concept/concept-adapter-inbound-graphql-c09.body.md",
"/shared/Tech-Log-Document/clean-architecture-backend-template/concept/concept-adapter-inbound-graphql-c07.body.md",
"/shared/Tech-Log-Document/clean-architecture-backend-template/concept/concept-adapter-inbound-graphql-c03.body.md"
]
}
]
@@ -1,610 +0,0 @@
{
"/shared/Tech-Log-Document/clean-architecture-backend-template/concept/concept-adapter-outbound-fileserver-c01.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 63자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/concept/concept-adapter-outbound-fileserver-c05.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 67자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/concept/concept-adapter-outbound-httpclient-c09.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 56자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/concept/concept-adapter-outbound-persistence-jpa-c03.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 58자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/concept/concept-adapter-outbound-persistence-jpa-c04.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 57자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/concept/concept-adapter-outbound-persistence-jpa-c05.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 59자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/concept/concept-adapter-outbound-persistence-jpa-c12.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 72자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/concept/concept-adapter-outbound-persistence-jpa-c16.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 56자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/concept/concept-adapter-outbound-persistence-jpa-c19.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 57자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/concept/concept-adapter-outbound-persistence-jpa-c25.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 62자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/concept/concept-adapter-outbound-persistence-jpa-c26.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 62자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/concept/concept-adapter-outbound-persistence-jpa-c27.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 74자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/concept/concept-adapter-outbound-persistence-jpa-c28.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 58자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/concept/concept-adapter-outbound-persistence-jpa-c29.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 59자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/concept/concept-adapter-outbound-persistence-jpa-c30.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 65자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/concept/concept-adapter-outbound-persistence-jpa-c31.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 76자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/concept/concept-adapter-outbound-persistence-jpa-c35.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 76자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/concept/concept-adapter-outbound-persistence-jpa-c36.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 66자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/concept/concept-adapter-outbound-persistence-jpa-c37.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 81자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/concept/concept-adapter-outbound-persistence-jpa-c47.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 59자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/concept/concept-adapter-outbound-persistence-jpa-c51.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 73자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/concept/concept-adapter-outbound-persistence-jpa-c53.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 62자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/concept/concept-adapter-outbound-support-c02.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 56자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/concept/concept-adapter-outbound-support-c04.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 72자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/concept/concept-application-core-c03.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 64자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/concept/concept-application-core-c06.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 60자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/concept/concept-application-core-c08.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 70자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/concept/concept-application-core-c09.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 59자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/concept/concept-messaging-policy-c07.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 57자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-a-five-second-string-that-broke-every-prod-deploy.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 13,
"message": "제목이 58자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-a01-f002-idfactory-newid.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 60자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-a05-f004-retrydecision-reason.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 60자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-a05-f005-jparetrypolicy.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 13,
"message": "제목이 59자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-a05-f006-stable.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 68자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-a05-f007-transactionprofileregistry.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 69자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-a05-f013-specificationpolicy-specification-unrestricted.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 13,
"message": "제목이 64자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-a05-f014-collection-fetch-pagination.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 13,
"message": "제목이 66자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-a05-f022-stable.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 13,
"message": "제목이 63자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-a05-f025-filequotaservice-commit.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 58자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-a05-f027-maximum-attempts.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 70자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-a05-f029-for-update-skip-locked.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 65자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-a05-f033-jpa-flyway-migration.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 13,
"message": "제목이 69자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-a06-f016-flamingock.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 58자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-a06-f018-changestreams-false.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 13,
"message": "제목이 58자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-a14-f005-publicpaths-restrictedpathrule.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 13,
"message": "제목이 56자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-a17-f002-stomp.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 13,
"message": "제목이 59자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-analysis-finding-a03-f001.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 59자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-analysis-finding-a04-f002.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 13,
"message": "제목이 73자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-analysis-finding-a04-f004.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 57자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-analysis-finding-a05-f024.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 58자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-analysis-finding-a05-f028.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 61자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-analysis-finding-a05-f030.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 72자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-analysis-finding-a05-f031.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 13,
"message": "제목이 61자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-analysis-finding-a05-f034.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 69자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-analysis-finding-a06-f002.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 57자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-analysis-finding-a06-f008.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 13,
"message": "제목이 62자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-analysis-finding-a06-f027.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 13,
"message": "제목이 66자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-analysis-finding-a16-f001.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 13,
"message": "제목이 56자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-analysis-finding-a19-f013.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 13,
"message": "제목이 62자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-autoconfiguration-in-name-only.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 61자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-conditionalonbean-evaluated-at-parse-time.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 57자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-grpc-proto-contract-f01.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 57자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-messaging-inbox-jdbc-postgresql-f01.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 59자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-messaging-kafka-f01.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 69자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-messaging-nats-experimental-f01.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 65자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-messaging-nats-experimental-f03.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 72자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-messaging-rabbit-f04.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 58자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-messaging-reliability-api-f08.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 61자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-messaging-spring-boot-starter-f02.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 57자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-readme-said-no-beans-there-are-eight.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 60자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-the-public-surface-contract-test-lives-outside-the-family.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 59자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/case/case-trust-policy-lives-in-nginx-not-in-the-code.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 12,
"message": "제목이 59자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/openquestion/openquestion-a05-f003-capabilitysupport-constraints.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 13,
"message": "제목이 56자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/openquestion/openquestion-analysis-finding-a02-f003.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 13,
"message": "제목이 59자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/openquestion/openquestion-analysis-finding-a02-f005.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 13,
"message": "제목이 67자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
],
"/shared/Tech-Log-Document/clean-architecture-backend-template/openquestion/openquestion-analysis-finding-a04-f006.md": [
{
"code": "LONG_TITLE",
"severity": "info",
"line": 13,
"message": "제목이 57자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
}
]
}
@@ -1,184 +0,0 @@
{
"schemaVersion": 1,
"project": "clean-architecture-backend-template",
"reviewedAt": "2026-09-01T15:43:48Z",
"note": "검토를 마치고 유지하기로 한 editorial finding 의 원장이다. 감사 도구는 여기 있는 항목을 ACCEPTED 로, 없는 항목을 UNREVIEWED 로 보고한다. 제목이 바뀌면 titleSha256 이 어긋나 그 항목은 다시 UNREVIEWED 가 된다 — 한 번 승인했다고 영원히 넘어가지 않는다. 제목을 다시 써서 60자 아래로 내려간 항목은 가리킬 발견이 없으므로 여기서 지운다 — 낡은 항목은 tools/editorial_quality/detect_stale_exceptions.py 가 찾는다.",
"criteria": {
"PROTECTED_IDENTIFIER": "길이가 보호 대상 식별자(코드 심볼 · 설정 키 · SQL 구문 · 고유명)에서 온다",
"PAIRED_CONTRAST": "사건 자체가 두 사실의 대비이고, 한쪽을 빼면 사건이 사라진다",
"ENUMERATION_IS_THE_EVENT": "열거된 항목 자체가 관측의 실질이다",
"TECHNICAL_QUALIFIER": "수치나 위치 같은 기술적 한정어가 결함의 규모·범위를 정한다"
},
"exceptions": {
"LONG_TITLE": {
"a-digest-that-covered-who-but-not-what": {
"status": "ACCEPTED",
"category": "PAIRED_CONTRAST",
"reason": "덮은 것과 덮지 않은 것의 대비가 digest 결함의 정의다.",
"path": "case/case-a-digest-that-covered-who-but-not-what.md",
"title": "transition digest가 \"누가·언제\"만 덮고 \"무엇\"을 덮지 않아 다른 전이를 같다고 보고했다",
"titleSha256": "b3fc797d1c1ba2153d91cef24a0b120b215f9caf2e85976967196d585a711629"
},
"a-five-second-string-that-broke-every-prod-deploy": {
"status": "ACCEPTED",
"category": "PROTECTED_IDENTIFIER",
"reason": "connection-timeout: 5s 는 설정 키와 값 그대로다. 그 문자열이 사건의 원인이다.",
"path": "case/case-a-five-second-string-that-broke-every-prod-deploy.md",
"title": "connection-timeout: 5s가 모든 prod 배포를 시작 실패시켰고 local만 통과했다",
"titleSha256": "b3e9857acb4647200bc420b861507cf22389bf9b05b5eed35a905afe15ef6e54"
},
"a16-f004-graphqloperationnamepolicy": {
"status": "ACCEPTED",
"category": "PAIRED_CONTRAST",
"reason": "배선된 쪽과 미배선 쪽 중 정책 타입을 쓰는 것이 미배선 쪽이라는 역전이 사건이다.",
"path": "case/case-a16-f004-graphqloperationnamepolicy.md",
"title": "연산 이름 정책의 두 구현 중 하나만 배선되고, 미배선 쪽만 GraphQlOperationNamePolicy를 쓴다",
"titleSha256": "d72780971c6a7742d779b1b6df1600edd6ee83b8c14f4059b987a983bd8175c3"
},
"a17-f002-stomp": {
"status": "ACCEPTED",
"category": "ENUMERATION_IS_THE_EVENT",
"reason": "연결 티켓·origin 정책·메시지 권한·연결 예산 네 통제의 열거가 실질이고, 뒤 절이 그중 일부의 예외를 정한다.",
"path": "case/case-a17-f002-stomp.md",
"title": "연결 티켓·origin 정책·메시지 권한·연결 예산이 요청 경로 밖이고, 그중 일부는 STOMP 어댑터가 다른 방식으로 대체한다",
"titleSha256": "19246d62702500370586a97fd1bb90dce46e5c277ee3498773f1433e3233c5f7"
},
"a19-f003-messaging-reliability-api": {
"status": "ACCEPTED",
"category": "TECHNICAL_QUALIFIER",
"reason": "main 13파일 · 817 LOC · 테스트 0개 라는 세 수치의 비율이 관측 자체다. 수치를 빼면 남는 것이 없다.",
"path": "case/case-a19-f003-messaging-reliability-api.md",
"title": "messaging-reliability-api는 main 13파일 · 817 LOC에 테스트가 0개다",
"titleSha256": "d94de5ec6f540c39653aa918da71f98fe4975b8609140889143911fb76f4ff50"
},
"analysis-finding-a06-f027": {
"status": "ACCEPTED",
"category": "PAIRED_CONTRAST",
"reason": "실제로 차단하는 것과 없는 것의 대비가 검증 지형 관측 자체다.",
"path": "case/case-analysis-finding-a06-f027.md",
"title": "release gate가 실제로 차단하는 것은 hermetic test 3개이고, mongo용 CI workflow는 없다",
"titleSha256": "1e576f89ecf30db51c9b63a612d7fc3588fd631531fbcc5362818874fb7463f7"
},
"analysis-finding-a08-f004": {
"status": "ACCEPTED",
"category": "PAIRED_CONTRAST",
"reason": "경로 기반이라는 것과 그것을 지키는 사전검사가 스스로 근사라고 적었다는 인용의 대비다.",
"path": "case/case-analysis-finding-a08-f004.md",
"title": "발행 rename만 경로 기반이고, 그것을 지키는 것은 이 모듈이 \"근사에 불과하다\"고 적은 사전검사다",
"titleSha256": "ed49f9e7783938fc6810385172713197c4c3aa4f51405125e2fa5346dca44f32"
},
"analysis-finding-a13-f010": {
"status": "ACCEPTED",
"category": "PAIRED_CONTRAST",
"reason": "FCM 만 규칙 밖이라는 것과 그 FCM 이 두 계약 집합 어디에도 없다는 것이 함께여야 사각지대가 성립한다.",
"path": "case/case-analysis-finding-a13-f010.md",
"title": "FCM만 \"커밋 후 응답 손실 = ambiguous\" 규칙 밖에 있고, 그 FCM이 두 계약 집합 어디에도 없다",
"titleSha256": "4b12e9c90a97c8df82f6ccc5fa6336cabf14618675e78b1e187d94ca18cf0eac"
},
"analysis-finding-a16-f001": {
"status": "ACCEPTED",
"category": "ENUMERATION_IS_THE_EVENT",
"reason": "스키마 조립·계약 정체성·해시 사슬 셋이 통째로 미배선이라는 열거가 규모를 정하고, 액추에이터 엔드포인트가 네 번째다.",
"path": "case/case-analysis-finding-a16-f001.md",
"title": "스키마 조립·계약 정체성·해시 사슬이 통째로 미배선이고, 그것을 발행할 액추에이터 엔드포인트도 등록되지 않는다",
"titleSha256": "e5f6e1f12deaa7f333a81801998a0ed09245ce00dbfaac04b4b7e76249456d58"
},
"autoconfiguration-in-name-only": {
"status": "ACCEPTED",
"category": "TECHNICAL_QUALIFIER",
"reason": "세 클래스라는 수와 capability 리포트에 Stable 로 올랐다는 결과가 사건의 두 축이다.",
"path": "case/case-autoconfiguration-in-name-only.md",
"title": "이름만 AutoConfiguration이던 세 클래스가 capability 리포트에 Stable로 올라 있었다",
"titleSha256": "fed765b2bcafabffa436bbb66b658d38be4889bf0a29428fe3ce577666a01a42"
},
"conditionalonbean-evaluated-at-parse-time": {
"status": "ACCEPTED",
"category": "PROTECTED_IDENTIFIER",
"reason": "@ConditionalOnBean(DataSource.class) 는 애너테이션 표기 그대로가 원인이다. 인자 타입까지가 조건이다.",
"path": "case/case-conditionalonbean-evaluated-at-parse-time.md",
"title": "@ConditionalOnBean(DataSource.class)가 클래스 파싱 시점에 평가되어 여덟 빈이 사라졌다",
"titleSha256": "2f89d628a8e05f3943701661fc17a44b77d6b2a131943fb5a11a5927b7ece67e"
},
"grpc-proto-contract-f01": {
"status": "ACCEPTED",
"category": "PROTECTED_IDENTIFIER",
"reason": "reserved 2 to 5; 는 오탐을 만드는 원문 구문이고 RESERVED_HISTORY 는 위반 코드다. 둘 다 그대로여야 재현된다.",
"path": "case/case-grpc-proto-contract-f01.md",
"title": "reserved 2 to 5; 범위가 개별 숫자로만 수집되어 RESERVED_HISTORY 오탐이 된다",
"titleSha256": "ddb7fe8063fa09028571f1a8647c86b29e73bfc042bcabfaf7c013e4c6d6a2f0"
},
"messaging-inbox-jdbc-postgresql-f01": {
"status": "ACCEPTED",
"category": "PAIRED_CONTRAST",
"reason": "구현돼 있다는 것과 호출되지 않는다는 것의 대비이고, 뒤 절이 그 결과다.",
"path": "case/case-messaging-inbox-jdbc-postgresql-f01.md",
"title": "bounded purge가 구현돼 있고 호출되지 않아, cleanup이 스스로 막겠다고 한 장애를 일으킨다",
"titleSha256": "11e1b5262a9f7739bdd6872a67886b4874d00c121ee2d004d71ae40408d303ab"
},
"messaging-nats-experimental-f01": {
"status": "ACCEPTED",
"category": "PAIRED_CONTRAST",
"reason": "무조건 참인 선언과 창이 있을 때만 일어나는 실제 동작의 대비다. 조건절을 빼면 결함이 성립하지 않는다.",
"path": "case/case-messaging-nats-experimental-f01.md",
"title": "deduplicatedPublish 를 무조건 참으로 선언하는데 실제 중복 제거는 프로파일에 창이 있을 때만 일어난다",
"titleSha256": "d88fd462e81989a10308e98413fc5c0496662ce130606e4d25a947cbacc397a9"
},
"messaging-rabbit-f04": {
"status": "ACCEPTED",
"category": "PAIRED_CONTRAST",
"reason": "능력 상수가 참이라는 것과 그 지연을 제공할 토폴로지가 없다는 것의 대비다.",
"path": "case/case-messaging-rabbit-f04.md",
"title": "능력 상수의 delayedDelivery 가 무조건 참이고, 그 지연을 제공할 토폴로지는 조립되지 않는다",
"titleSha256": "2ff213b705421e39147bad47dbcf8b3ed0dc22fa76daa1227fa62787041e7c34"
},
"messaging-reliability-api-f08": {
"status": "ACCEPTED",
"category": "PAIRED_CONTRAST",
"reason": "두 오버로드를 나란히 노출한다는 것과 호출자가 무제한 쪽을 골랐다는 것이 원인과 결과다.",
"path": "case/case-messaging-reliability-api-f08.md",
"title": "포트가 bounded/unbounded purge 두 오버로드를 나란히 노출하고, 호출자가 무제한 쪽을 고른다",
"titleSha256": "d569175440aa856b591bd4e607f2830320fe877691c9dfc2660888afd53f4af7"
},
"messaging-spring-boot-starter-f02": {
"status": "ACCEPTED",
"category": "PAIRED_CONTRAST",
"reason": "아무도 공급하지 않는 빈 뒤에 있다는 것과 사슬이 자기 클래스 안을 가리킨다는 것이 함께여야 자기참조 구조가 드러난다.",
"path": "case/case-messaging-spring-boot-starter-f02.md",
"title": "출고되는 신뢰성 체인 전체가 아무도 공급하지 않는 빈 뒤에 있고, 그 사슬이 자기 클래스 안을 가리킨다",
"titleSha256": "3c751db7f8344dd35c7f13610b20a384e88e53c6b1d7eab162787773ca881192"
},
"readme-said-no-beans-there-are-eight": {
"status": "ACCEPTED",
"category": "PAIRED_CONTRAST",
"reason": "README 인용과 실제 bean 여덟이 대비 자체다. 한쪽만 남기면 사건이 사라진다.",
"path": "case/case-readme-said-no-beans-there-are-eight.md",
"title": "README가 \"노출된 setting도 bean도 없다\"고 적은 능력에 production bean 여덟이 있다",
"titleSha256": "c1f78904149820c311d1b1882827d808eb0e282e305606062f69dd5f93710f00"
},
"the-public-surface-contract-test-lives-outside-the-family": {
"status": "ACCEPTED",
"category": "PROTECTED_IDENTIFIER",
"reason": "MessagingPublicSurfaceContractTest 한 이름이 제목의 절반이고, app-bootstrap 이 실제 소재다.",
"path": "case/case-the-public-surface-contract-test-lives-outside-the-family.md",
"title": "MessagingPublicSurfaceContractTest가 가족 밖(app-bootstrap)에 있다",
"titleSha256": "fcf1375b7b32bea7173dcb0ab7d9e407e0c8a58cef56a12575dedaad420828d1"
},
"the-transport-and-the-validator-answer-differently": {
"status": "ACCEPTED",
"category": "PAIRED_CONTRAST",
"reason": "두 쪽이 다르게 답한다는 것과 런타임이 쓰는 쪽이 틀렸다는 것이 함께 있어야 심각도가 정해진다.",
"path": "case/case-the-transport-and-the-validator-answer-differently.md",
"title": "같은 어댑터의 능력을 전송과 검증기가 다르게 답하고, 런타임이 쓰는 쪽이 record 의 의미와 어긋난다",
"titleSha256": "092faa5727dcae07b8486a17ddb1f6ff64c1141f33fedac9085641bd3e0edb74"
},
"trust-policy-lives-in-nginx-not-in-the-code": {
"status": "ACCEPTED",
"category": "TECHNICAL_QUALIFIER",
"reason": "421 LOC 이 배선되지 않은 정책의 규모이고, Nginx 라는 실제 판정 위치가 대비의 반대쪽이다.",
"path": "case/case-trust-policy-lives-in-nginx-not-in-the-code.md",
"title": "forwarded 헤더 신뢰 판정이 Nginx에만 있고 Java 정책 421 LOC은 배선되지 않았다",
"titleSha256": "1d2ef0398997a68c91b2712b2a55af15b2cf2a25f5720e6cfdbc01e092950a14"
}
}
}
}
@@ -1,75 +0,0 @@
# Editorial / Visual Quality Audit — 2026-09-01 (초기 스냅숏)
:::warning
**상태: HISTORICAL_SNAPSHOT**
기준 시점: semantic editorial pass **이전**. 이 문서는 현재 완료 상태를 나타내지 않는다.
아래의 `lastEditorialAt: None` · `EDITORIAL_PENDING` · Concept signature 18종 ·
Case signature 12종 · diagram 217 · LONG_TITLE 76 은 전부 그 시점의 값이고 지금은 다르다.
숫자를 덮어쓰지 않고 그대로 둔다 — 어디서 출발했는지가 이 문서의 쓸모다.
현재 상태의 정본은 두 곳이다.
- `_meta/editorial/final-editorial-validation-2026-09-01.md` — 최종 검증 결과
- `_meta/editorial/semantic-pass-progress.md``Correction pass` 절 — 무엇을 왜 고쳤는지
:::
## 상태
- technical records: **949** — CASE 402 · CONCEPT 386 · REFERENCE 105 · OPEN QUESTION 31 · DECISION 25
- rich body: **788** — Concept 386 · Case 402
- `lastEditorialAt`: `None`
- `lastValidationAt`: `None`
- 판정: **EDITORIAL_PENDING**. 생성 완료와 편집 완료를 같은 상태로 보지 않는다.
## 이번에 즉시 수정한 것
- terminal evidence 788개를 raw evidence hash 확인 후 새 renderer로 재생성했다. 긴 줄은 `<tspan>`으로 시각적 wrap하며 raw 문자는 바꾸지 않는다.
- explanatory SVG 217개와 terminal SVG 788개는 새 SVG validator를 통과했다.
- Concept의 고정 첫 절 `무엇을 해결하는가` 383개를 제거했다.
- Case의 고정 첫 절 `무엇이 일어났나` 402개를 제거했다.
- Concept main record의 중복 `무엇을 해결하는가` 357개를 제거했다.
- provenance/asset 설명 boilerplate 1,656개를 body에서 제거했다. 이 정보는 asset/meta와 source provenance가 소유한다.
- 분석자 경험처럼 보이던 `처음에는 … 의심했다/후보로 봤다` Concept 3개를 source anchor를 다시 읽고 객관적인 메커니즘 서술로 교정했다.
## 개별 문서 audit
- flagged artifacts: **76**
- warning: **0**
- info: **76**
- code counts: `{'LONG_TITLE': 76}`
현재 warning은 0이다. `LONG_TITLE`은 자동 수정 대상이 아니라 편집 검토 신호다.
## corpus 구조 audit
### Concept
- LOW_HEADING_SIGNATURE_DIVERSITY: 본문 386개가 heading signature 18종만 사용함(4.7%). 고정 목차 template 반복 여부를 검토한다.
### Case
- LOW_HEADING_SIGNATURE_DIVERSITY: 본문 402개가 heading signature 12종만 사용함(3.0%). 고정 목차 template 반복 여부를 검토한다.
고정 첫 절과 반복 boilerplate는 제거했지만, 전체 목차 signature가 Concept 18종 / Case 12종에 집중되어 있다. 이 문제는 제목을 기계적으로 바꾸는 방식으로 해결하지 않는다. 다음 editorial pass에서 각 문서의 실제 메커니즘·사건을 읽고 내용 기반 절 이름으로 재구성한다.
## visual audit
- explanatory diagrams: 217
- terminal evidence SVG: 788
- XML/viewBox/title/desc/role/script/external-reference/text-overflow validator: **PASS**
- explanatory diagram layout signatures: **10종**
- most common signatures: [(('880', '276', 4, 4, 2, 1), 80), (('880', '366', 4, 4, 3, 1), 56), (('880', '186', 4, 4, 1, 1), 28), (('880', '276', 5, 5, 2, 1), 18), (('880', '186', 3, 3, 1, 1), 16)]
기계적 SVG 결함은 현재 막혔다. 다만 217개 다이어그램의 layout signature가 10종에 집중된 것은 시각 편집 문제다. 앞으로는 `designing-tech-log-visuals` Skill에서 boundary/sequence/state/data-flow/failure/before-after 중 논지에 맞는 visual grammar를 먼저 고르고, 그림 quota를 두지 않는다.
## 완료 조건
`lastEditorialAt`은 아직 채우지 않는다. 다음 단계에서 Concept/Case body를 topic/module 단위로 읽으면서 낮은 heading-signature 다양성을 해소하고, 76개 긴 제목 후보를 실제 문맥 기준으로 검토한 뒤 corpus audit를 다시 실행한다. 단순 정규식 치환으로 PASS를 만들지 않는다.
## state integrity
- record/body hash 또는 line mismatch: **0**
@@ -1,158 +0,0 @@
# Final Editorial Validation — 2026-09-01
**상태: COMPLETE.** 이 문서가 `clean-architecture-backend-template` Tech Log 의 편집 완료 상태를
나타내는 정본이다. 다른 editorial artifact 는 전부 이 문서를 가리킨다.
검증 시각 `2026-09-01T15:48:45Z`. `_meta/state.json``lastEditorialAt` ·
`lastValidationAt` 과 같은 값이다.
같은 디렉터리의 다른 문서와의 관계는 이렇다.
| 문서 | 성격 |
|---|---|
| `editorial-visual-audit-2026-09-01.md` | pass **이전**의 초기 audit. `HISTORICAL_SNAPSHOT` |
| `editorial-audit-2026-09-01.json` | 같은 시점의 도구 원본 출력(LONG_TITLE 76). 가공하지 않는다 |
| `concept-structure-audit-2026-09-01.json` · `case-structure-audit-2026-09-01.json` | 같은 시점의 구조 audit 원본(signature 18종 · 12종) |
| `semantic-pass-progress.md` | 작업 로그. 앞부분은 진행 중 스냅숏, 하단 `Correction pass` 가 정본 |
| `svg-semantic-review-2026-09-01.md` | 33개 후보의 시각 semantic 검토 판정 |
| `editorial-exceptions.json` | 검토를 마치고 유지하기로 한 finding 의 원장 |
| **이 문서** | **최종 검증 수치** |
`.json` 은 도구가 그대로 뱉은 출력이라 안에 표시를 넣지 않는다 — 넣으면 더 이상 도구
출력이 아니다. 어느 세대인지는 파일명의 날짜와 이 표가 말한다.
---
## Corpus
```text
records = 949
CASE = 402
CONCEPT = 386
REFERENCE = 105
OPEN QUESTION = 31
DECISION = 25
rich body = 788 (CONCEPT 386 · CASE 402)
```
## Editorial
```text
corpus structural findings = 0
concept bodies=386 records=386 findings=0
case bodies=402 records=402 findings=0
reference / openquestion / decision records=105 / 31 / 25 findings=0
main-record repeated explanatory prose = 0
LONG_TITLE candidates = 60
reviewed = 60
accepted = 60
unreviewed = 0
stale exceptions = 0
```
LONG_TITLE 60건은 미해결 finding 이 아니다. `editorial-exceptions.json` 이 60건 각각에 대해
승인 사유를 기록한다.
| category | 수 | 뜻 |
|---|---:|---|
| `PROTECTED_IDENTIFIER` | 36 | 길이가 코드 심볼 · 설정 키 · SQL 구문 · 고유명에서 온다 |
| `PAIRED_CONTRAST` | 17 | 사건 자체가 두 사실의 대비다 |
| `TECHNICAL_QUALIFIER` | 4 | 수치나 위치가 결함의 규모를 정한다 |
| `ENUMERATION_IS_THE_EVENT` | 3 | 열거된 항목 자체가 관측의 실질이다 |
승인은 영구 침묵이 아니다. 각 항목이 승인 당시 제목의 `titleSha256` 을 들고 있어서, 제목이
바뀌면 그 항목은 다시 `UNREVIEWED` 가 된다. 승인한 finding 이 더 이상 나오지 않으면
`STALE_EXCEPTION` 으로 보고된다. 새 기록이 55자를 넘는 제목을 갖고 원장에 없으면 그대로
`UNREVIEWED` 로 나타난다.
## References
```text
source/evidence files checked = 1737
invalid references = 0
```
## Visual
```text
explanatory SVG = 181
terminal SVG = 788
mechanical validation = PASS (diagram · terminal)
semantic negative-edge candidates reviewed = 33
redesigned = 3
accepted = 30
coordinate precision violation = 0
```
## Integrity
```text
record hash mismatch = 0
body hash mismatch = 0
diagram svg hash mismatch = 0
raw evidence hash mismatch = 0
line count mismatch = 0
```
## Upstream
```text
crossScopeAnalysis sha state 538e4e72cf033a9e262d5bbccd4914ccb4f2aea405432efd034b719c64cd25ff
crossScopeAnalysis sha file 538e4e72cf033a9e262d5bbccd4914ccb4f2aea405432efd034b719c64cd25ff
crossScopeAnalysis lines state 552 file 552
→ MATCH
```
`verify_pipeline.py` 가 매 실행마다 이 대조를 수행한다 (`upstream document metadata: matches files`).
## Tests
```text
PYTHONPATH=tools python3 -m unittest discover -s tools/tests
Ran 98 tests
OK
python3 tools/verify_pipeline.py /shared
PIPELINE VERIFICATION: PASS
- required paths: 67
- analysis queue contract: valid
- root-tree contract: present
- upstream document metadata: matches files
- forbidden legacy dependency: absent
```
---
## 완료 판정 명령
이 상태를 재확인하는 한 줄은 다음이다. `--fail-on-unreviewed` 가 붙어야 완료 판정이 된다 —
`findings == 0` 이 아니라 `unreviewed == 0` 이 조건이기 때문이다.
```bash
python3 tools/editorial_quality/audit_korean_tech_writing.py \
clean-architecture-backend-template/concept \
clean-architecture-backend-template/case \
clean-architecture-backend-template/reference \
clean-architecture-backend-template/openquestion \
clean-architecture-backend-template/decision \
--exceptions clean-architecture-backend-template/_meta/editorial/editorial-exceptions.json \
--fail-on-unreviewed
```
## state 의 완료 의미
`_meta/state.json``lastEditorialAt` · `lastValidationAt` 은 "편집 pass 가 끝났고 그 시점에
검증이 돌았다" 를 뜻하며, 그 날짜의 closure artifact 가 결과를 소유한다.
`editorialStatus` 같은 상태 필드는 **추가하지 않았다.** `_templates/project/_meta/state.json`
이 상태의 계약이고, 필드를 하나 추가하면 그 템플릿과 그것으로 만들어질 모든 프로젝트의 계약이
바뀐다. 이번 closure 가 요구하는 범위를 넘는다. 완료 여부는 이 문서와 위 명령의 exit code 로
읽는다. 이 규칙은 `humanizing-korean-tech-writing` Skill 의
`Audit artifacts have generations; say which one is current` 절에 적혀 있다.
@@ -1,319 +0,0 @@
# Semantic Editorial Pass — 진행 기록
**현재 상태: COMPLETE**
:::warning
이 문서는 작업 로그다. 아래 여러 절은 **작업 당시의 incremental progress snapshot** 이다.
`진행 중` 표기, 중간 count, 이전 audit 결과는 historical record 이며 현재 상태를 뜻하지 않는다.
예를 들어 `state-machines-and-ownership — 78/119 (진행 중)` 은 그 시점의 값이고, 그 topic 은
CONCEPT 386건 완료 시점에 끝났다.
정본은 두 곳이다.
- 이 문서 하단의 `Correction pass`
- `_meta/editorial/final-editorial-validation-2026-09-01.md` — 최종 검증 수치
:::
시작 2026-09-01. 대상 949 records. 우선순위 CONCEPT -> CASE -> REFERENCE/QUESTION/DECISION.
## Historical progress snapshot — 이번 pass 에서 함께 고친 것
- 생성 과정에서 문장이 절 경계에서 잘려 나간 body 를 원본 source anchor 로 복원한다. 말줄임(…)으로 끝난 문장, 목록이 한 줄로 뭉개진 문단, 코드 스팬 중간에서 끊긴 문장이 대상이다.
- 절 제목을 그 문서가 실제로 설명하는 메커니즘·사건·경계 이름으로 바꾼다. 고정 목차를 새로 만들지 않는다.
- 제목이 SSOT 절 머리글(모듈의 정체와 경계, 실패 경로와 복구/번역 등)이거나 분석 번호((8.2) 등)이거나 분석 과정형(Confirmed —)이면 그 문서의 메커니즘 이름으로 바꾼다.
- 그림은 논지에 맞는 visual grammar 로 다시 그리거나, 논지와 무관하면 제거한다.
## Historical progress snapshot — 완료 topic (CONCEPT)
- result-and-failure-algebra — 10/10
- query-and-pagination-models — 6/6
- observability-models — 7/7
- identity-and-value-contracts — 8/8
- capability-and-disclosure-models — 11/11
- security-and-trust-boundaries — 20/20
## Historical progress snapshot — 그림 처리 기록
- 재설계: adapter-inbound-graphql-c10, adapter-outbound-cache-redis-c03, adapter-outbound-support-c02, application-core-c01, messaging-testkit-c04, adapter-inbound-web-c01, adapter-outbound-objectstorage-c01, adapter-outbound-persistence-jpa-c24, grpc-advanced-diagnostics-c02, adapter-inbound-web-c03, adapter-inbound-web-c04, application-core-c02, grpc-advanced-diagnostics-c01, grpc-observability-c01, grpc-spring-boot-starter-c01
- 제거: grpc-core-api-c01(기록의 논지와 다른 그림), app-bootstrap-c01(표로 읽는 것이 명확), adapter-outbound-notification-c04(같음), grpc-advanced-bootstrap-c03(같음)
- admission-budget-and-backpressure — 21/21
- composition-and-lifecycle-models — 26/26
- transaction-and-consistency-models — 27/27
- state-machines-and-ownership — 78/119 (진행 중)
## CONCEPT 386건 완료 (2026-09-01)
- 일반 heading(구조 / 명령이 보여 주는 것 / 어떻게 움직이는가 / 무엇이 불변식인가 / 어디까지가 경계인가 / 왜 그렇게 되나 / 무엇이 남는가 / 실패하면 어떤 상태가 되는가 / 근거 절 원문)을 남긴 CONCEPT body 0건.
- audit_corpus_structure.py concept: bodies=386 findings=0.
- state.json 과 frontmatter/H1 사이 title 불일치 132건을 state.json 쪽(보호 대상 식별자가 살아 있는 쪽)으로 정본화. 단 test-names-that-assert-what-their-bodies-do-not 은 본문이 네 사례를 열거하므로 "테스트 넷" 으로 확정하고 state.json 을 고쳤다.
- diagram 217 → 181. 제거 36건은 (a) 내용이 목록/표라서 그림이 정보를 더하지 않는 경우, (b) alt 가 다른 기록의 주장을 그리고 있던 경우다.
- CASE 402건은 ca0..ca30 청크로 진행한다.
## CASE 402건 완료 (2026-09-01)
- ca0..ca30 청크 전량 편집. 일반 heading 잔존 0건.
- audit_corpus_structure.py case: bodies=402 findings=0.
- CASE 구간에서 diagram 재설계 다수, 제거 다수. 최종 diagram 수는 pass 종료 시 재보고.
- 다음: REFERENCE 105 / OPEN QUESTION 31 / DECISION 25 (본문 파일 없음, record .md 만 편집).
## Semantic editorial pass — 1차 (2026-09-01, 이후 정정됨)
:::warning
아래는 1차 pass 당시의 스냅숏이다. 두 수치가 틀렸고 correction pass 에서 정정했다 —
`20개 이상 문서에서 동일한 문단 = 0` 은 body 만 센 것이고 main record 를 세지 않았으며,
테스트 수는 `test_verify_pipeline` 한 파일만 돈 결과다. 최신 상태는 이 문서 끝의
"Correction pass" 절을 본다.
:::
전체 949건 편집을 마쳤다. `lastEditorialAt` / `lastValidationAt` = `2026-09-01T10:39:26Z`.
### 구조 다양성
| 지표 | 이전 | 이후 |
|---|---|---|
| CONCEPT heading signature | 18 / 386 (4.7%) | 382 / 386 (99.0%) |
| CASE heading signature | 12 / 402 (3.0%) | 400 / 402 (99.5%) |
| CASE 서로 다른 `##` heading | — | 1,191 |
| CONCEPT 서로 다른 `##` heading | — | 1,054 |
| 20개 이상 body 에서 동일한 문단 | 다수 | 0 |
| 20개 이상 main record 에서 동일한 문장 | 측정하지 않음 | 측정하지 않음 — correction pass 가 163건을 찾았다 |
`## 출처` 는 provenance 절이므로 전 기록이 공유한다. 그 다음으로 빈도가 높은 것은
CASE `## 수정` 37건(9.2%), CONCEPT `## 이 기록이 다루는 파일 범위` 34건(8.8%) 이다.
`audit_corpus_structure.py` 의 FIRST_HEADING_DOMINANCE(80%) · LOW_HEADING_SIGNATURE_DIVERSITY(15%)
· REPEATED_BOILERPLATE(20) 세 규칙 모두 findings=0.
### 이번 회차에 마지막으로 처리한 것
- CASE ca29 · ca30 청크 25건. CASE 402건 전량 완료.
- REFERENCE 40건: `목적`/`규칙`/`적용 조건`/`예외`/`근거` 가 전부 boilerplate 였던 기록을
각 source anchor 의 SSOT finding(사실·근거·왜 문제인가·확인 방법)으로 다시 썼다.
- QUESTION 13건: `미지수`("현재 근거로는 답이 닫히지 않는다")와 `다음 검증` boilerplate 를
각 기록의 실제 미지수와 확인 방법으로 바꾸고, SSOT 의 후보를 `선택지` 절로 옮겼다.
- `## 원본 판정이 무엇을 적었나` 206건(CASE 186 · CONCEPT 20)을 각 기록이 인용하는
근거가 무엇을 보여 주는지로 개별 명명했다. 편집 과정에서 이 heading 자체가
두 번째 template 이 된 것을 계측으로 발견해 되돌린 것이다.
- 제목: LONG_TITLE 후보를 전수 검토했다. 9건을 줄였고 60건은 유지했다.
### 제목 판정
줄인 것 9건.
- `analysis-finding-a03-f003``legacy compatibility surface의 제거 조건을 세 가지로 고정한다`
(다른 두 기록의 `## 관계` 가 이미 이 이름으로 이 규칙을 가리키고 있었다. 끊긴 링크가 붙었다)
- `a05-f003-capabilitysupport-constraints``CapabilitySupport.constraints 의 경계가 타입에 없다`
- `analysis-finding-a04-f006``cache-redis 와 httpclient 의 support 간선이 죽었는지 확정되지 않았다`
- `a06-f003-change-streams-true`(88자) → 두 절 중 중복(`거부되지 않고`/`조용히 버려지며`)을 뺐다
- `a13-f002-authentication-failed-resumehealthy`(77자) → 한 절로 줄였다
- `analysis-finding-a19-f013`(75자) → 인용 문장을 본문에 남기고 제목은 사건 이름으로 바꿨다
- `a14-f005-publicpaths-restrictedpathrule`(73자) → 뒤 절이 앞 절을 일반어로 반복하고 있었다
- `messaging-nats-experimental-f03`(72자) → 제목에 두 문장이 들어 있었다
- `messaging-kafka-f01`(69자) → 세 절 중 사건을 이루는 두 절만 남겼다
유지한 60건은 길이가 (a) `MessagingPublicSurfaceContractTest` · `GrpcSerializedStreamWriter` 같은
보호 대상 식별자, (b) 사건 자체가 두 사실의 대조인 경우(문서 X · 코드 Y), (c) 열거가 실질인 경우
(`연결 티켓·origin 정책·메시지 권한·연결 예산`)에서 온다. 줄이면 사실이 빠진다.
### 시각 자료
- diagram 217 → 181. 제거 36건은 내용이 목록/표라 그림이 정보를 더하지 않거나,
alt 가 다른 기록의 주장을 그리고 있던 경우다.
- terminal SVG 788건은 손대지 않았다.
- `validate_svg.py assets/svg --kind diagram` PASS, `assets/terminal --kind terminal` PASS.
### 최종 검증
| 명령 | 결과 |
|---|---|
| `PYTHONPATH=tools python3 -m unittest tools.tests.test_verify_pipeline` | OK (38 tests — 한 파일만. 전체 discover 아님) |
| `python3 tools/verify_pipeline.py /shared` | PASS |
| `audit_korean_tech_writing.py concept` | scanned=772 findings=0 |
| `audit_korean_tech_writing.py case` | scanned=804 findings=60 (LONG_TITLE, 전건 검토 후 유지) |
| `audit_korean_tech_writing.py reference` | scanned=105 findings=0 |
| `audit_korean_tech_writing.py openquestion` | scanned=31 findings=0 |
| `audit_korean_tech_writing.py decision` | scanned=25 findings=0 |
| `audit_corpus_structure.py concept` | bodies=386 findings=0 |
| `audit_corpus_structure.py case` | bodies=402 findings=0 |
| `validate_svg.py assets/svg --kind diagram` | PASS |
| `validate_svg.py assets/terminal --kind terminal` | PASS |
| record/body SHA ↔ `_meta/state.json` | 949건 전건 일치 |
### 남은 editorial warning
CASE LONG_TITLE 60건뿐이다. 위 판정대로 유지 결정이며, 규칙을 다시 적용할 때는
길이가 아니라 "제목이 사건을 이름 짓는가"로 판정한다.
### 편집 중 발견해 고친 사실 오류 하나
`case-an-unselectable-broker-listed-with-features``## 출처` 가 존재하지 않는 절
`analysis/messaging/messaging-rabbit.md §17` 을 가리키고 있었다. SSOT 를 grep 해
`analysis/19-messaging-platform.md` §6.2 가 그 finding 의 실제 소유자임을 확인하고 고쳤다.
---
# Correction pass (2026-09-01)
독립 재검증에서 나온 결함만 고치고, 같은 형태가 다시 완료 판정을 통과하지 못하도록 validation
contract 를 보강한 회차다. 949건을 다시 쓰지 않았다. `lastEditorialAt` · `lastValidationAt` =
`2026-09-01T11:32:12Z`.
## 1. main record boilerplate 163건
1차 pass 의 audit 은 `*.body.md` 만 glob 했다. main record 는 대상 밖이었고, 그래서 다음 두 문장이
CASE 163건과 158건에 그대로 남아 있었다.
```
확인 방식 : 원본 분석 절의 판정을 옮겼고, 이 기록에 붙은 자산의 명령만 이번 회차에 실행했다
이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
```
둘 다 기록이 아니라 생성 파이프라인을 설명하는 문장이다. 163건 전부를 owning SSOT 43개의 §16
(확인하지 못한 것)과 각 기록에 붙은 자산으로 다시 썼다.
- `확인 방식` — 그 기록을 실제로 무엇이 확정했는지. 예: `GrpcAdmissionController 참조 16건 전수
검색과 두 클래스의 공개 표면 대조`, `HeaderValue 참조 26건 검색과 파서의 종료 지점 탐색 코드
확인. jshell 리플렉션으로 왕복 손상을 런타임 재현`.
- `확인하지 못한 것` — owning SSOT §16 이 그 finding 에 대해 소유한 실제 미검증 범위. SSOT 가
그 finding 을 닫아 둔 한 건(`messaging-outbox-jdbc-postgresql-f03`)만 `없음 — …` 으로 적었고,
읽지 않고 `없음` 을 쓴 기록은 없다.
고정 필드(`## 요약` · `## 문제` · `## 결론` · `## 검증 환경` · `## 재현 조건` ·
`## 확인하지 못한 것`)는 record contract 이므로 건드리지 않았다. 공통 환경값
(`OpenJDK : 21.0.12` · `Gradle : 9.0.0` · `소스 수정 : x`)도 그대로 두었다.
덤으로 같은 형태 하나를 더 찾아 고쳤다 — CASE 5건과 그 body 5건이
`가족 문서의 판정을 옮긴 것이다. 실행 확인은 그 문서의 §확인하지 못한 것이 소유한다.` 를 갖고
있었는데, `analysis/19-messaging-platform.md` 에는 그런 절이 없다. 끊긴 포인터였다.
## 2. corpus audit 의 근본 원인
`audit_corpus_structure.py` 를 두 층으로 나눴다.
| 층 | 대상 | 검사 |
|---|---|---|
| rich body | `*.body.md` | heading signature · 첫 heading 편중 · 반복 문단 |
| main record | `*.md` (body 제외) | **section 내부 설명형 prose 반복만** |
main record 층은 고정 schema heading 을 검사하지 않는다. CASE 의 여섯 절이 100% 같은 것은 계약이
동작하는 것이지 결함이 아니다. 대신 `키 : 값` 줄의 **값이 값인지 설명인지**를 가른다 —
한국어 종결형이 붙거나 문장이 둘 이상이면 설명으로 본다. `21.0.12 java -version 으로 확인` 은
값이고, `원본 분석 절의 판정을 옮겼고 …` 는 설명이다.
`## 관계` · `## 근거 기록` · `## 출처` 아래의 `-` 링크 줄은 대상에서 뺐다. 그 줄은 다른 기록의
제목을 그대로 반복해야 링크가 성립한다 — 참조이지 설명이 아니다. 링크 아래의 설명 줄은 그대로
검사한다.
regression test: `tools/tests/test_main_record_boilerplate.py` 8건.
- 고정 CASE heading 이 100% 같아도 fail 하지 않는다
- 공통 Java/Gradle 값만 같아도 fail 하지 않는다
- 같은 설명 문장이 20개 이상 main record 에 반복되면 fail 한다
- 19건이면 fail 하지 않는다 (threshold 경계)
- 관계 링크 줄은 반복해도 fail 하지 않는다
## 3. 잘못된 evidence reference 4건
owning SSOT 를 다시 읽고 정본으로 고쳤다. 없는 파일을 새로 만들지 않았다.
| 기록 | 잘못된 참조 | 정본 |
|---|---|---|
| `a-resumed-redrive-skips-what-it-could-not-move` | `306-redrive-resume-skips-failed-items.txt` | `306-redrive-resume-skips-unmoved.txt` (EVD-306) |
| `an-authorization-denial-recorded-as-a-configuration-error` | `287-messaging-security-access-path.txt` | `287-messaging-security-duplicate-checks.txt` §C (EVD-287) |
| `blocking-means-startup-fails-and-nothing-runs-it` | `302-messaging-admin-service-unwired.txt` | `302-admin-api-topology-guarantee-unwired.txt` (EVD-302) |
| `the-forgeable-approval-survived-on-the-irreversible-half` | `307-destructive-approval-not-verified.txt` | `308-admin-runtime-api-and-dependency-defects.txt` (EVD-308) |
넷째는 접두사 정정이 아니다. `Approved` 가 `VerifiedApproval` 이 아니라 `AdminApproval` 을 담는다는
사실은 `analysis/messaging/messaging-admin-runtime.md` 의 §14 표에서 EVD-**308** 에 귀속돼 있다
(EVD-307 이 소유하는 것은 구현 0건 쪽이다).
## 4. source/evidence reference validator
`tools/validate_source_references.py` 를 추가하고 `verify_pipeline.py` 의 REQUIRED_PATHS 에 등록했다.
| 참조 형태 | 규칙 |
|---|---|
| `analysis/foo.md` | 파일 존재 |
| `analysis/foo.md#L123` | 파일 존재 + line 범위 유효 |
| `evidence/raw/<full-name>.txt` | **정확히 그 파일** 존재. 접두사가 같은 이웃으로 대체하지 않는다 |
| `evidence/raw/287` | unique prefix 로 정확히 하나에 해석 |
두 evidence 형태를 비대칭으로 둔 것이 이 도구의 요점이다. 전체 파일명을 적은 것은 그 파일에 대한
단언이므로, 없으면 FAIL 이어야 한다. 번호만 적은 shorthand 는 evidence id 를 가리키므로 접두사
해석을 허용한다.
regression test: `tools/tests/test_source_reference_validation.py` 8건 — 이번 네 오류의 형태가
전부 포함돼 있다.
## 5. SVG semantic defect
정상 방향 화살표는 실재하는 관계를 뜻하고, 도착 상자를 빗금 치는 것으로 그 뜻이 뒤집히지 않는다.
33개 후보를 전수 검토해 3건을 재설계하고 30건을 유지했다. 상세는
`_meta/editorial/svg-semantic-review-2026-09-01.md`.
| 판정 | 수 |
|---|---:|
| 재설계 | 3 |
| 유지 — 들어오는 전이는 실재하고 부재는 나가는 쪽 | 3 |
| 유지 — 빗금 상자에 화살표가 닿지 않음 | 2 |
| 유지 — 빗금 상자 없음, 화살표가 전부 실재 흐름 | 25 |
규칙을 `designing-tech-log-visuals` 의 SKILL hard gate · editorial rules · review checklist 세 곳에
명문화하고, 그 셋이 존재하는지 검사하는 테스트를 붙였다.
layout signature 수는 목표로 삼지 않았다. 재설계 3건은 화살표가 기록과 반대를 말했기 때문에
고친 것이고, 나머지 30건은 grammar 가 generic 하다는 이유로 건드리지 않았다.
## 6. SVG 좌표 직렬화
설명용 SVG 40개의 좌표 340개가 소수점 둘 이상을 갖고 있었다. 소수점 한 자리로 정규화했고
(기하 변화 최대 0.05px), 생성기와 `validate_svg.py`(`COORDINATE_PRECISION`) 양쪽에 규칙을 넣었다.
터미널 SVG 는 대상이 아니다 — 원래 정수 좌표만 쓴다.
## 7. cross-scope metadata
`document-detail/.../state.json` 의 `crossScopeAnalysis` 가 재생성 이전 값을 들고 있었다.
```
sha256 481b3728… → 538e4e72…
lines 428 → 552
```
이것을 놓친 이유는 어떤 verifier 도 기록된 수치를 파일과 대조하지 않았기 때문이다.
`verify_pipeline.py` 에 `_verify_upstream_document_metadata` 를 추가했다 — state.json 안에서 `path`
와 `sha256` 을 함께 가진 모든 블록(crossScopeAnalysis · finalDocument · rootTree · candidateLedger ·
sourceManifest)을 실제 파일과 대조한다. regression test 5건.
## 8. LONG_TITLE 60건
길이만 보고 줄이지 않았다. 네 조건(독립 사건 둘 혼재 · 본문에 내려도 되는 설명 · 같은 뜻 반복 ·
분석 과정 문장)으로 60건을 다시 훑었고 해당하는 것이 없었다. 남은 길이는 보호 대상 식별자
(`MessagingPublicSurfaceContractTest` · `GrpcSerializedStreamWriter`), 사건 자체가 두 사실의 대조인
경우(문서 X · 코드 Y), 열거가 실질인 경우에서 온다.
## 9. 최종 검증 (fresh)
| 항목 | 결과 |
|---|---|
| records 총수 | 949 (CASE 402 · CONCEPT 386 · REFERENCE 105 · QUESTION 31 · DECISION 25) |
| record hash mismatch | 0 |
| body hash mismatch | 0 |
| diagram svg hash mismatch | 0 |
| raw evidence hash mismatch | 0 |
| line count mismatch | 0 |
| invalid source reference | 0 |
| repeated main-record prose | 0 |
| `unittest discover -s tools/tests` | **Ran 80 tests — OK** |
| `verify_pipeline.py /shared` | PASS (required paths 67 · upstream document metadata matches files) |
| `audit_korean_tech_writing.py` (5개 디렉터리 1,737 파일) | findings 60 — 전부 LONG_TITLE |
| `audit_corpus_structure.py concept` | bodies 386 · records 386 · findings 0 |
| `audit_corpus_structure.py case` | bodies 402 · records 402 · findings 0 |
| `audit_corpus_structure.py reference / openquestion / decision` | records 105 / 31 / 25 · findings 0 |
| `validate_svg.py assets/svg --kind diagram` | PASS (181개) |
| `validate_svg.py assets/terminal --kind terminal` | PASS (788개) |
| `validate_source_references.py` (신규) | findings 0 |
| crossScope metadata | 실제 파일과 일치 |
남은 editorial warning 은 CASE LONG_TITLE 60건뿐이며, §8 의 판정대로 유지한다.
@@ -1,157 +0,0 @@
# SVG semantic review — 2026-09-01
기계 검증(`validate_svg.py`)은 통과하지만 그림이 기록의 주장과 반대를 말하는지 확인한 회차다.
`validate_svg.py` 는 라벨 길이 · 문장형 · 상자 수 · 뷰포트만 본다. **화살표가 무엇을 뜻하는지는
보지 않는다.** 그래서 이번 검토는 사람이 했다.
## 검토 모집단
`<desc>` 가 부재(없 · 않 · 미배선 · 도달하지 · 연결되지 · 0건 · 존재하지 · 못하 · 빠진 · 비어 ·
아니 · 끊)를 말하면서 정상 방향 화살표도 가진 설명용 SVG **33개**. 181개 중 그 조건에 걸리는
전부이며, 나머지 148개는 부재를 말하지 않으므로 이 회차의 대상이 아니다.
33개를 일괄 치환하지 않았다. 각각에 대해 다음 다섯을 대조했다.
- 기록의 주장(`## 요약` · 본문)
- owning SSOT 의 해당 finding
- SVG 의 `<desc>`
- 보이는 라벨
- 화살표의 방향과 도착 지점
## 판정 기준
정상 방향 화살표는 **실재하는 호출 · 의존 · 전이 · 데이터 흐름 · 도달성**을 뜻한다. 그래서
도착 상자가 빗금이라는 사실만으로 화살표의 뜻이 뒤집히지 않는다. 그림이 결함인 경우는
**화살표가 가리키는 관계 자체가 없을 때**다.
반대로, 들어오는 전이는 실재하고 기록이 말하는 부재가 **나가는** 전이인 경우가 있다. 상태
기계의 종착 상태가 그 형태다. 그 경우 빗금 상자로 들어가는 화살표는 정확하다.
## 결과
| 판정 | 수 |
|---|---:|
| 재설계 | 3 |
| 유지 — 들어오는 전이는 실재하고 부재는 나가는 쪽 | 3 |
| 유지 — 빗금 상자에 화살표가 닿지 않음 | 2 |
| 유지 — 빗금 상자가 없고 화살표가 전부 실재 흐름 | 25 |
## 재설계 3건
### `adapter-inbound-graphql-c04.svg`
기록의 주장은 "요청 계층만 배선되고, 네 파생 계층은 메서드로 존재하되 프로덕션 호출자가 0" 이다.
기존 그림은 배선된 요청 데드라인에서 네 파생 계층으로 정상 화살표 넷을 그리고 그 위에
`파생 없음` 이라는 글자를 얹었다. 화살표가 글자를 이긴다.
고친 형태는 화살표를 **하나도 그리지 않는다.** 네 계층은 `파생 없음 — 프로덕션 호출자 0` 이라고
이름 붙인 별도 영역 안에 놓인다.
### `adapter-inbound-graphql-c10.svg`
주장은 "자동설정이 참조하는 두 타입에서 실제로 닿는 패키지는 dataloader 하나이고, fetch ·
pagination · mutation 41개 파일은 어떤 배선 경로에도 없다" 이다. 기존 그림은 네 갈래 전부에
정상 화살표를 그렸다.
고친 형태는 `dataloader` 에만 화살표를 두고, 나머지 셋은 `배선 경로 없음 — 화살표 없음` 영역에
놓는다.
### `adapter-inbound-websocket-c01.svg`
주장은 "설정 접두사 셋 중 `backend.websocket` 만 그것을 읽어 조립하는 `@Configuration` 이 없고,
그 접두사가 규정하는 범위가 main 169 파일 중 약 90개로 가장 넓다" 이다. 기존 그림은 세 갈래에
모두 정상 화살표를 그려, 접두사가 소비자에 닿는다는 뜻과 닿지 않는다는 뜻이 한 그림 안에서
충돌했다.
고친 형태는 소비자가 있는 두 접두사에만 화살표를 두고, `backend.websocket`
`읽는 Configuration 없음` 영역에 화살표 없이 놓는다.
## 유지 3건 — 들어오는 전이는 실재한다
### `adapter-outbound-objectstorage-c05.svg` · `adapter-outbound-fileserver-c03.svg` 계열
`<desc>` 가 "그 네 상태에서 **나오는** 전이는 없다" 라고 적는다. 그림의 화살표 넷은 정상 사슬에서
그 상태로 **들어가는** 전이이고, 그것은 실재한다. 부재는 반대 방향이며 `<desc>` 가 방향을 밝힌다.
정확한 그림이다.
### `adapter-outbound-persistence-jpa-c51.svg`
주장은 "Stable persistence unit 의 스캔 문자열 목록에 experimental package 가 **이미 들어 있다**" 이다.
화살표는 그 목록이 그 항목을 담는다는 실재하는 포함 관계이고, 빗금은 부재가 아니라
바로 아래 라벨이 말하는 `조건 없음` 을 표시한다.
### `messaging-kafka-c02.svg`
주장은 "연속 구간만 커밋되고 빈 자리 뒤는 완료돼 있어도 보류된다" 이다. 화살표는 완료 표시 맵이
두 구간으로 갈리는 실재하는 분할이고, 빗금 상자는 없는 것이 아니라 `보류` 상태다. 라벨이
`빈 자리 뒤 구간 — 보류` 로 그것을 말한다.
## 유지 2건 — 빗금 상자에 화살표가 닿지 않는다
`grpc-policy-f02.svg``messaging-nats-experimental-f01.svg` 는 빗금 상자를 갖지만 어떤 화살표도
그 안으로 들어가지 않는다. 이미 이번 회차가 정한 규칙을 지키는 형태다.
## 유지 25건 — 빗금 상자가 없다
나머지 25개의 `<desc>` 에 나오는 부정 표현은 부재가 아니라 **규칙이나 순서**를 말한다 —
"반대 방향 의존이 없다", "뒤 규칙이 앞 규칙이 금지한 것을 다시 허용할 수 없다",
"검증되지 않은 설정을 쓰는 bean 이 생기지 않는다" 같은 형태다. 화살표는 전부 실재하는 흐름이고
빗금 상자 자체가 없다.
## 규칙을 어디에 고정했나
이 판정 기준을 `designing-tech-log-visuals` 에 명문화했다.
- `SKILL.md` — hard gate 한 줄
- `references/diagram-editorial-rules.md` — "A normal arrow asserts that the relation exists" 절과,
부재를 그리는 세 형태(no edge · separated region · 명시적으로 라벨된 broken/crossed edge)
- `references/review-checklist.md` — 검토 항목 한 줄
- `tools/tests/test_writing_quality_contracts.py` — 위 셋이 실제로 존재하는지 검사하는 테스트
## 전 파일 분류
| SVG | 화살표 | 빗금 | 빗금 진입 | 판정 |
|---|---:|---:|---:|---|
| `adapter-inbound-graphql-c04.svg` | 0 | 4 | 0 | 재설계 |
| `adapter-inbound-graphql-c10.svg` | 1 | 3 | 0 | 재설계 |
| `adapter-inbound-web-c01.svg` | 2 | 0 | 0 | 유지 — 빗금 없음, 화살표가 전부 실재 흐름 |
| `adapter-inbound-websocket-c01.svg` | 2 | 1 | 0 | 재설계 |
| `adapter-outbound-cache-redis-c01.svg` | 2 | 0 | 0 | 유지 — 빗금 없음, 화살표가 전부 실재 흐름 |
| `adapter-outbound-cache-redis-c03.svg` | 1 | 0 | 0 | 유지 — 빗금 없음, 화살표가 전부 실재 흐름 |
| `adapter-outbound-cache-redis-c08.svg` | 2 | 0 | 0 | 유지 — 빗금 없음, 화살표가 전부 실재 흐름 |
| `adapter-outbound-cache-redis-c12.svg` | 2 | 0 | 0 | 유지 — 빗금 없음, 화살표가 전부 실재 흐름 |
| `adapter-outbound-cache-redis-c13.svg` | 2 | 0 | 0 | 유지 — 빗금 없음, 화살표가 전부 실재 흐름 |
| `adapter-outbound-fileserver-c03.svg` | 2 | 0 | 0 | 유지 — 빗금 없음, 화살표가 전부 실재 흐름 |
| `adapter-outbound-httpclient-c04.svg` | 3 | 0 | 0 | 유지 — 빗금 없음, 화살표가 전부 실재 흐름 |
| `adapter-outbound-objectstorage-c05.svg` | 4 | 4 | 4 | 유지 — 들어오는 전이는 실재 |
| `adapter-outbound-persistence-jpa-c02.svg` | 1 | 0 | 0 | 유지 — 빗금 없음, 화살표가 전부 실재 흐름 |
| `adapter-outbound-persistence-jpa-c24.svg` | 1 | 0 | 0 | 유지 — 빗금 없음, 화살표가 전부 실재 흐름 |
| `adapter-outbound-persistence-jpa-c26.svg` | 3 | 0 | 0 | 유지 — 빗금 없음, 화살표가 전부 실재 흐름 |
| `adapter-outbound-persistence-jpa-c51.svg` | 2 | 1 | 1 | 유지 — 들어오는 전이는 실재 |
| `adapter-outbound-persistence-mongo-c03.svg` | 3 | 0 | 0 | 유지 — 빗금 없음, 화살표가 전부 실재 흐름 |
| `adapter-outbound-support-c02.svg` | 2 | 0 | 0 | 유지 — 빗금 없음, 화살표가 전부 실재 흐름 |
| `application-core-c01.svg` | 1 | 0 | 0 | 유지 — 빗금 없음, 화살표가 전부 실재 흐름 |
| `application-core-c04.svg` | 3 | 0 | 0 | 유지 — 빗금 없음, 화살표가 전부 실재 흐름 |
| `commit-evidence-phases.svg` | 3 | 0 | 0 | 유지 — 빗금 없음, 화살표가 전부 실재 흐름 |
| `grpc-advanced-resilience-c01.svg` | 3 | 0 | 0 | 유지 — 빗금 없음, 화살표가 전부 실재 흐름 |
| `grpc-policy-f02.svg` | 3 | 1 | 0 | 유지 — 빗금 상자에 화살표 없음 |
| `messaging-claim-check-c01.svg` | 3 | 0 | 0 | 유지 — 빗금 없음, 화살표가 전부 실재 흐름 |
| `messaging-inbox-jdbc-postgresql-c06.svg` | 2 | 0 | 0 | 유지 — 빗금 없음, 화살표가 전부 실재 흐름 |
| `messaging-kafka-c02.svg` | 2 | 1 | 1 | 유지 — 들어오는 전이는 실재 |
| `messaging-nats-experimental-f01.svg` | 3 | 1 | 0 | 유지 — 빗금 상자에 화살표 없음 |
| `messaging-policy-c03.svg` | 3 | 0 | 0 | 유지 — 빗금 없음, 화살표가 전부 실재 흐름 |
| `messaging-pulsar-experimental-c01.svg` | 1 | 0 | 0 | 유지 — 빗금 없음, 화살표가 전부 실재 흐름 |
| `messaging-rabbit-c01.svg` | 2 | 0 | 0 | 유지 — 빗금 없음, 화살표가 전부 실재 흐름 |
| `messaging-security-c03.svg` | 3 | 0 | 0 | 유지 — 빗금 없음, 화살표가 전부 실재 흐름 |
| `messaging-testkit-c04.svg` | 2 | 0 | 0 | 유지 — 빗금 없음, 화살표가 전부 실재 흐름 |
| `messaging-transport-spi-c03.svg` | 3 | 0 | 0 | 유지 — 빗금 없음, 화살표가 전부 실재 흐름 |
## 좌표 직렬화
같은 회차에 정리했다. 설명용 SVG 40개의 좌표 340개가 소수점 둘 이상을 갖고 있었다 —
`225.33333333333334` · `440.00000000000006` 같은 형태다. 렌더링은 같지만 생성기가 float repr 을
그대로 속성에 넣었다는 뜻이고, 같은 그림의 두 판을 diff 할 때 읽히지 않는다.
소수점 한 자리로 정규화했다(기하 변화 최대 0.05px). 그리고 규칙을 두 곳에 고정했다 —
생성기가 좌표를 직렬화할 때 한 자리로 자르고, `validate_svg.py``COORDINATE_PRECISION` 으로
설명용 SVG 를 검사한다. 터미널 SVG 는 대상이 아니다(원래 정수 좌표만 쓴다).
File diff suppressed because it is too large Load Diff
@@ -1,65 +0,0 @@
# Tech Log 초안 리뷰 체크리스트 — cycle 2 재실행
실행일 : 2026-09-01 (cycle 2 downstream 재생성 후)
대상 : clean-architecture-backend-template · 레코드 333건 (CONCEPT 21 · CASE 217 · REFERENCE 53 · QUESTION 17 · DECISION 25)
근거 root tree : root-tree.md (노드 334 · 생성 차단 1)
소스 리비전 : 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
## 이번 실행이 이전 실행과 다른 점
첫 실행(같은 날, 레코드 326건)에서 유일하게 열려 있던 항목은 upstream 상태였다 —
document-detail 이 cycle 2 재분석 중이라 analysisStatus 가 IN_PROGRESS 였고 downstream 셋이 NOT_STARTED 였다.
그 항목이 닫혔다.
## Provenance
- [x] **upstream 전부 COMPLETE** — analysisStatus COMPLETE · 스코프 61 COMPLETE + 1 EXCLUDED ·
crossScopeAnalysis / finalDocument / rootTree 전부 COMPLETE · reanalysis.completedAt 기록됨
- [x] 23개 리프가 canonical SSOT 를 갖고 FULL_READ 로 전환됐다. STRUCTURAL_ONLY 잔여 0.
- [x] 레코드 333건 전부 root tree 노드에 slug 로 대응. 미대응 0.
- [x] readiness READY/OPEN 만 생성. NEEDS_DECISION 노드 1건은 decision-evidence 부재로 차단 유지.
- [x] 레코드 sourceRevision 333건 전부 root tree 리비전과 일치.
- [x] root tree 노드 334건 전부 source 앵커 보유. 참조 파일 290건 중 결측 0.
- [x] 라인 앵커 전부 헤딩 착지. 어긋남 0.
## Record kind
- [x] kind ↔ 노드 kind 일치 333/333 · 디렉터리 배치 일치
- [x] bodyMarkdown 은 CASE·CONCEPT 만 사용
- [x] DECISION 25건 전부 근거 기록 보유 · OPEN QUESTION 17건 전부 미지수와 다음 검증 보유
## Facts / Body / Relations
- [x] 비본문 필드의 마크다운 문법 유입 0 · 제목 중복 0
- [x] body-syntax 화이트리스트 위반 0
- [x] 관계 설명 누락 0 · root tree 끊긴 relations 0
## downstream 재생성 요약
| 산출물 | 변경 |
|---|---|
| `analysis/99-cross-scope.md` | 306 → 428줄. §1.2(전수 통독) 신설 · §3.2·§3.5 확장 · §3.7(전송 계열 가정) 신설 · §4 6~9항 · §6 |
| `final/document.md` | 1,680 → 1,765줄. §14.2 신설 · §5.5 확장 · §8.2 9~13항 · 헤더 표 |
| `root-tree.md` | 4,097 → 4,174줄. 노드 327 → 334 |
| `candidate-ledger.json` | 340/291 → 347/298 |
| 레코드 | 326 → 333 |
## 새로 낸 노드 7건
| 노드 | Topic |
|---|---|
| `case:assigned-id-turns-claim-into-upsert` | state-ownership-and-concurrency |
| `case:complete-drain-rolls-back-a-rotation` | 〃 |
| `case:startup-validator-is-the-only-reader-of-four-keys` | runtime-reachability-and-composition |
| `case:validator-declared-and-never-injected` | 〃 |
| `reference:a-validator-is-enforced-by-injection` | 〃 |
| `case:capability-constant-outlives-its-condition` | transport-and-provider-semantics |
| `case:ipv4-only-mask-passes-every-other-form` | security-policy-enforcement |
통독의 새 finding 52건 중 노드로 낸 것은 7건이다. 나머지는 리프 SSOT §17 에 남는다 —
기존 REFERENCE 가 이미 그 규칙을 갖고 있거나(예: `atomic-type-is-not-atomicity`),
같은 사건의 다른 인스턴스여서 노드를 늘릴 이유가 없는 경우다.
## 미결
없다. 다음 단계는 `humanizing-korean-tech-writing` 편집 패스이며, 체크리스트가 별도 패스로 규정한 것이다.
@@ -1,56 +0,0 @@
# Tech Log 초안 리뷰 체크리스트 실행 결과
실행일 : 2026-09-01
대상 : clean-architecture-backend-template · 레코드 326건 (CONCEPT 21 · CASE 211 · REFERENCE 52 · QUESTION 17 · DECISION 25)
근거 root tree : /shared/document-detail/clean-architecture-backend-template/root-tree.md (노드 327 · 생성 차단 1)
소스 리비전 : 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
## Provenance
- [x] 레코드 326건 전부 root tree 노드에 slug 로 대응한다. 미대응 0.
- [x] readiness 가 READY 또는 OPEN 인 노드만 생성됐다. NEEDS_DECISION 노드 1건(`widen-doc-contract-assertions`)은 decision-evidence 부재로 차단 유지.
- [x] 레코드 sourceRevision 326건 전부 root tree 리비전과 일치.
- [x] root tree 노드 327건 전부 source 앵커를 갖는다. 참조 파일 283건 중 결측 0 (codebase 경로 1건 포함 확인).
- [x] 라인 앵커 155건 전부 헤딩에 정확히 착지. 어긋남 0.
- [ ] **upstream 상태가 COMPLETE 가 아니다.** document-detail state.json 이 cycle 2 재분석 중이다 — analysisStatus IN_PROGRESS, 스코프 COMPLETE 38 / IN_PROGRESS 23 / EXCLUDED 1, crossScopeAnalysis·finalDocument·rootTree 는 NOT_STARTED(stale). 상세는 아래 "미결 항목".
## Record kind
- [x] 레코드 kind 가 root tree 노드 kind 와 326건 전부 일치.
- [x] 디렉터리 배치가 kind 와 일치.
- [x] bodyMarkdown 은 CASE·CONCEPT 만 사용. 다른 kind 의 body 참조 0.
- [x] DECISION 25건 전부 근거 기록 1건 이상 보유.
- [x] OPEN QUESTION 17건 전부 미지수와 다음 검증을 갖는다.
## Facts
- [x] 비본문 필드에 백틱·파이프 등 마크다운 문법 유입 0 (평문 규약 준수).
- [x] 제목 중복 0.
- [x] 각 레코드가 검증 환경·재현 조건·확인하지 못한 것을 분리해 적는다. 실행하지 않은 것은 실행하지 않았다고 적었다.
## Concept / Case body
- [x] body-syntax 화이트리스트 위반 0 — raw HTML·각주·체크박스·중첩 리스트·중첩 인용·취소선·링크 타이틀 모두 검출 0.
- [x] body 파일 참조 결측 0.
## Relations
- [x] 관계 항목 전부가 제목 나열이 아니라 설명 줄을 동반한다. 설명 없는 관계 0.
- [x] root tree 끊긴 relations 0.
## 이번 실행에서 고친 것
1. `a19-f022-messagingpublicsurfacecontracttest` — 레저 앵커가 §9.3(문서 계약 테스트 커버리지)에 착지해 §9.3 내용으로 초안이 작성됐다. 그 절은 이미 `doc-contract-test-boundary-predicted-the-drift` 가 담당한다. 노드 제목이 가리키는 §9.5(공개 표면 계약 테스트 위치)로 레코드를 다시 썼다.
2. root tree 라인 앵커 4건 정정 — `analysis-finding-a19-f018` L911→L926, `analysis-finding-a19-f019` L960→L975, `a19-f020-messaging-admin-api` L982→L997, `a19-f022-...` L1099→L1114.
## 미결 항목 (편집 패스 전에 판단 필요)
document-detail 파이프라인이 cycle 2 재분석 중이다.
- 사유(state.json): 23개 리프의 production 구현이 cycle 1 에서 STRUCTURAL_ONLY 였고, full-read 게이트를 통과할 때까지 downstream 합성을 무효로 둔다.
- 대상: messaging 5개(kafka·rabbit·pulsar-exp·nats-exp·spring-boot-starter), grpc 18개 전부.
- 리비전은 그대로다 — baselineRevision == targetRevision, changedPaths 없음. 즉 코드가 바뀐 것이 아니라 읽기 깊이를 올리는 재분석이다.
- 영향 범위: 이 23개 리프에서 나온 레코드는 가족 통합 문서 `analysis/19-messaging-platform.md`·`analysis/20-grpc-platform.md`(역할 INTEGRATION_ONLY)를 근거로 한다. 리프별 canonical SSOT(`analysis/messaging/*.md`·`analysis/grpc/*.md`)는 파일로 존재하지만 상태가 IN_PROGRESS 다.
- 그 밖의 레코드(38개 COMPLETE 스코프 근거)는 이 항목의 영향을 받지 않는다.
체크리스트의 "owning registered leaf 가 자기 canonical module SSOT 를 갖고, 초안이 가족/최종 요약에만 근거하지 않는다" 항목은 이 23개 리프에 대해서는 재분석 완료 후 재확인이 필요하다.
@@ -16,7 +16,7 @@ assets:
evidence:
- ../../../final/evidence/raw/adapter-inbound-graphql-c04.txt
source:
- 원본 분석 절은 analysis/16-adapter-inbound-graphql.md#L354 이다.
- 원본 분석 절은 final/document.md#a16#L354 이다.
module: adapter-inbound-graphql
---
@@ -14,7 +14,7 @@ assets:
evidence:
- ../../../final/evidence/raw/adapter-inbound-graphql-c05.txt
source:
- 원본 분석 절은 analysis/16-adapter-inbound-graphql.md#L376 이다.
- 원본 분석 절은 final/document.md#a16#L376 이다.
module: adapter-inbound-graphql
---
@@ -14,7 +14,7 @@ assets:
evidence:
- ../../../final/evidence/raw/adapter-inbound-web-c05.txt
source:
- 원본 분석 절은 analysis/14-adapter-inbound-web.md#L666 이다.
- 원본 분석 절은 final/document.md#a14#L666 이다.
module: adapter-inbound-web
---
@@ -14,7 +14,7 @@ assets:
evidence:
- ../../../final/evidence/raw/adapter-inbound-web-c14.txt
source:
- 원본 분석 절은 analysis/14-adapter-inbound-web.md#L1098 이다.
- 원본 분석 절은 final/document.md#a14#L1098 이다.
module: adapter-inbound-web
---
@@ -42,7 +42,7 @@ ForwardedHeaderSanitizer 2 test 1 testkit
## 분석 원문의 참조 집계
:::evidence key="adapter-inbound-web-c14" alt="분석 문서 analysis/14-adapter-inbound-web.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/14-adapter-inbound-web.md 발췌 — 15줄" zoom="true"
:::evidence key="adapter-inbound-web-c14" alt="분석 문서 final/document.md#a14 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a14 발췌 — 15줄" zoom="true"
:::
<!-- body:end -->
@@ -14,7 +14,7 @@ assets:
evidence:
- ../../../final/evidence/raw/adapter-inbound-web-c18.txt
source:
- 원본 분석 절은 analysis/14-adapter-inbound-web.md#L1336 이다.
- 원본 분석 절은 final/document.md#a14#L1336 이다.
module: adapter-inbound-web
---
@@ -16,7 +16,7 @@ assets:
evidence:
- ../../../final/evidence/raw/adapter-outbound-cache-redis-c04.txt
source:
- 원본 분석 절은 analysis/10-adapter-outbound-cache-redis.md#L245 이다.
- 원본 분석 절은 final/document.md#a10#L245 이다.
module: adapter-outbound-cache-redis
---
@@ -14,7 +14,7 @@ assets:
evidence:
- ../../../final/evidence/raw/adapter-outbound-cache-redis-c05.txt
source:
- 원본 분석 절은 analysis/10-adapter-outbound-cache-redis.md#L324 이다.
- 원본 분석 절은 final/document.md#a10#L324 이다.
module: adapter-outbound-cache-redis
---
@@ -34,7 +34,7 @@ module: adapter-outbound-cache-redis
## 분석 원문의 규칙 서술
:::evidence key="adapter-outbound-cache-redis-c05" alt="분석 문서 analysis/10-adapter-outbound-cache-redis.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/10-adapter-outbound-cache-redis.md 발췌 — 15줄" zoom="true"
:::evidence key="adapter-outbound-cache-redis-c05" alt="분석 문서 final/document.md#a10 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a10 발췌 — 15줄" zoom="true"
:::
## 이 검사가 PII 방지의 완결이 아니라고 적는다
@@ -16,7 +16,7 @@ assets:
evidence:
- ../../../final/evidence/raw/adapter-outbound-cache-redis-c12.txt
source:
- 원본 분석 절은 analysis/10-adapter-outbound-cache-redis.md#L684 이다.
- 원본 분석 절은 final/document.md#a10#L684 이다.
module: adapter-outbound-cache-redis
---
@@ -14,7 +14,7 @@ assets:
evidence:
- ../../../final/evidence/raw/adapter-outbound-fileserver-c06.txt
source:
- 원본 분석 절은 analysis/08-adapter-outbound-fileserver.md#L507 이다.
- 원본 분석 절은 final/document.md#a08#L507 이다.
module: adapter-outbound-fileserver
---
@@ -14,7 +14,7 @@ assets:
evidence:
- ../../../final/evidence/raw/adapter-outbound-httpclient-c01.txt
source:
- 원본 분석 절은 analysis/11-adapter-outbound-httpclient.md#L90 이다.
- 원본 분석 절은 final/document.md#a11#L90 이다.
module: adapter-outbound-httpclient
---
@@ -16,7 +16,7 @@ assets:
evidence:
- ../../../final/evidence/raw/adapter-outbound-httpclient-c05.txt
source:
- 원본 분석 절은 analysis/11-adapter-outbound-httpclient.md#L323 이다.
- 원본 분석 절은 final/document.md#a11#L323 이다.
module: adapter-outbound-httpclient
---
@@ -14,7 +14,7 @@ assets:
evidence:
- ../../../final/evidence/raw/adapter-outbound-httpclient-c08.txt
source:
- 원본 분석 절은 analysis/11-adapter-outbound-httpclient.md#L573 이다.
- 원본 분석 절은 final/document.md#a11#L573 이다.
module: adapter-outbound-httpclient
---
@@ -14,7 +14,7 @@ assets:
evidence:
- ../../../final/evidence/raw/adapter-outbound-objectstorage-c02.txt
source:
- 원본 분석 절은 analysis/09-adapter-outbound-objectstorage.md#L92 이다.
- 원본 분석 절은 final/document.md#a09#L92 이다.
module: adapter-outbound-objectstorage
---
@@ -14,7 +14,7 @@ assets:
evidence:
- ../../../final/evidence/raw/application-core-c08.txt
source:
- 원본 분석 절은 analysis/03-application-core.md#L190 이다.
- 원본 분석 절은 final/document.md#a03#L190 이다.
module: application-core
---
@@ -14,7 +14,7 @@ assets:
evidence:
- ../../../final/evidence/raw/grpc-advanced-bootstrap-c01.txt
source:
- 원본 분석 절은 analysis/grpc/grpc-advanced-bootstrap.md#L53 이다.
- 원본 분석 절은 final/document.md#a20-grpc-advanced-bootstrap#L53 이다.
module: grpc-advanced-bootstrap
---
@@ -14,7 +14,7 @@ assets:
evidence:
- ../../../final/evidence/raw/grpc-advanced-streaming-c01.txt
source:
- 원본 분석 절은 analysis/grpc/grpc-advanced-streaming.md#L86 이다.
- 원본 분석 절은 final/document.md#a20-grpc-advanced-streaming#L86 이다.
module: grpc-advanced-streaming
---
@@ -14,7 +14,7 @@ assets:
evidence:
- ../../../final/evidence/raw/messaging-admin-api-c04.txt
source:
- 원본 분석 절은 analysis/messaging/messaging-admin-api.md#L493 이다.
- 원본 분석 절은 final/document.md#a19-messaging-admin-api#L493 이다.
module: messaging-admin-api
---
@@ -14,7 +14,7 @@ assets:
evidence:
- ../../../final/evidence/raw/messaging-policy-c05.txt
source:
- 원본 분석 절은 analysis/messaging/messaging-policy.md#L423 이다.
- 원본 분석 절은 final/document.md#a19-messaging-policy#L423 이다.
module: messaging-policy
---
@@ -14,7 +14,7 @@ assets:
evidence:
- ../../../final/evidence/raw/messaging-schema-avro-c07.txt
source:
- 원본 분석 절은 analysis/messaging/messaging-schema-avro.md#L457 이다.
- 원본 분석 절은 final/document.md#a19-messaging-schema-avro#L457 이다.
module: messaging-schema-avro
---
@@ -16,7 +16,7 @@ assets:
evidence:
- ../../../final/evidence/raw/messaging-spring-cloud-stream-bridge-c03.txt
source:
- 원본 분석 절은 analysis/messaging/messaging-spring-cloud-stream-bridge.md#L146 이다.
- 원본 분석 절은 final/document.md#a19-messaging-spring-cloud-stream-bridge#L146 이다.
module: messaging-spring-cloud-stream-bridge
---
@@ -14,7 +14,7 @@ assets:
evidence:
- ../../../final/evidence/raw/shared-contract-c02.txt
source:
- 원본 분석 절은 analysis/02-shared-contract.md#L69 이다.
- 원본 분석 절은 final/document.md#a02#L69 이다.
module: shared-contract
---
@@ -17,7 +17,7 @@ evidence:
- ../../../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
source:
- 원본 분석 절은 analysis/messaging/messaging-spring-boot-starter.md 의 §17.1 이다.
- 원본 분석 절은 final/document.md#a19-messaging-spring-boot-starter 의 §17.1 이다.
---
# 검증기가 운영에 TLS를 요구하고, 실제로 조립되는 생산자에는 그 설정이 없다
@@ -18,7 +18,7 @@ evidence:
- ../../../final/evidence/raw/a06-f018-changestreams-false.txt
- ../../../final/evidence/raw/a06-f018-changestreams-false-wiring.txt
source:
- 원본 분석 절은 analysis/06-adapter-outbound-persistence-mongo.md#L1069 이다. 등급은 P2 다. 주석의 전제가 더 이상 사실이 아니라는 판정, 형제 불리언과의 대비표, 검증기 분기가 도달 불가라는 사실, 그리고 실패가 장애 조치 런북으로 분류된다는 서술이 그 절에 있다.
- 원본 분석 절은 final/document.md#a06#L1069 이다. 등급은 P2 다. 주석의 전제가 더 이상 사실이 아니라는 판정, 형제 불리언과의 대비표, 검증기 분기가 도달 불가라는 사실, 그리고 실패가 장애 조치 런북으로 분류된다는 서술이 그 절에 있다.
- 같은 문서 `#L1015` 는 소비자가 도는 조건을 포크가 다섯을 공급하는 경우로 한정한다. 이 저장소 안에서는 그 다섯의 구현이 전부 시험 픽스처다.
- 같은 문서 `#L156` 은 같은 코드를 P3 으로 판정하면서 변경 스트림 실행체가 애초에 출하되지 않는다는 것을 근거로 든다. 이 리비전에서 그 근거가 성립하지 않으므로 그 절에 붙은 "현재 잘못된 동작을 만들지는 않는다"는 유지될 수 없고, 실질 등급은 이 절의 P2 로 흡수된다.
- 설정 빈이 속성을 받고도 거짓을 보고한다는 것, 그 두 경우의 빈 집합이 같다는 것, 검사 빈 자체에 조건이 있다는 것, 그리고 런북 전체에 단독 서버·오플로그·해당 오류 코드가 없다는 것은 이 기록에서 확인했다.
@@ -14,7 +14,7 @@ assets:
evidence:
- ../../../final/evidence/raw/autoconfiguration-in-name-only.txt
source:
- 원본 분석 절은 analysis/05 §14.4 이다.
- 원본 분석 절은 final/document.md#a05 §14.4 이다.
---
# 이름만 AutoConfiguration이던 세 클래스가 capability 리포트에 Stable로 올라 있었다
@@ -74,7 +74,7 @@ Spring Boot : 4.0.8
## 세 클래스가 갖지 않은 것
:::evidence key="autoconfiguration-in-name-only" alt="분석 문서 analysis/05-adapter-outbound-persistence-jpa.md 에서 이 기록의 근거 절을 그대로 잘라낸 16줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/05-adapter-outbound-persistence-jpa.md 발췌 — 16줄" zoom="true"
:::evidence key="autoconfiguration-in-name-only" alt="분석 문서 final/document.md#a05 에서 이 기록의 근거 절을 그대로 잘라낸 16줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a05 발췌 — 16줄" zoom="true"
:::
## 리포트와 컨텍스트가 어긋났다
@@ -14,7 +14,7 @@ assets:
evidence:
- ../../../final/evidence/raw/conditionalonbean-evaluated-at-parse-time.txt
source:
- 원본 분석 절은 analysis/05 §14.4 이다.
- 원본 분석 절은 final/document.md#a05 §14.4 이다.
---
# @ConditionalOnBean(DataSource.class)가 클래스 파싱 시점에 평가되어 여덟 빈이 사라졌다
@@ -70,7 +70,7 @@ Spring Boot : 4.0.8
## 조건이 평가된 시점
:::evidence key="conditionalonbean-evaluated-at-parse-time" alt="분석 문서 analysis/05-adapter-outbound-persistence-jpa.md 에서 이 기록의 근거 절을 그대로 잘라낸 16줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/05-adapter-outbound-persistence-jpa.md 발췌 — 16줄" zoom="true"
:::evidence key="conditionalonbean-evaluated-at-parse-time" alt="분석 문서 final/document.md#a05 에서 이 기록의 근거 절을 그대로 잘라낸 16줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a05 발췌 — 16줄" zoom="true"
:::
## 여덟 빈이 조용히 사라졌다
@@ -14,7 +14,7 @@ assets:
evidence:
- ../../../final/evidence/raw/narrowing-the-scan-orphaned-eight-components.txt
source:
- 원본 분석 절은 analysis/05 §14.2 이다.
- 원본 분석 절은 final/document.md#a05 §14.2 이다.
---
# 넓은 스캔을 좁히자 여덟 컴포넌트에 아무것도 도달하지 않았다
@@ -16,7 +16,7 @@ evidence:
- ../../../final/evidence/raw/observation-downgraded-by-the-composition.txt
- ../../../final/evidence/raw/tl-messaging-observation-noop.txt
source:
- 원본 분석 절은 final/document.md#5-2 · analysis/19 §5.1 이다.
- 원본 분석 절은 final/document.md#5-2 · final/document.md#a19 §5.1 이다.
---
# 관측을 필수 생성자 인자로 만든 수정을 조립이 6인자 생성자로 되돌렸다
@@ -16,7 +16,7 @@ evidence:
- ../../../final/evidence/raw/outbox-chain-behind-an-unsatisfiable-condition.txt
- ../../../final/evidence/raw/tl-outbox-unsatisfiable-condition.txt
source:
- 원본 분석 절은 final/document.md#4-3 · analysis/19 §7.1 이다.
- 원본 분석 절은 final/document.md#4-3 · final/document.md#a19 §7.1 이다.
---
# outbox 가 둘이고, 출하되는 것은 messaging 플랫폼 쪽이 아니다
@@ -16,7 +16,7 @@ evidence:
- ../../../final/evidence/raw/scan-exclusion-without-an-owner.txt
- ../../../final/evidence/raw/tl-web-six-unowned-components.txt
source:
- 원본 분석 절은 final/document.md#3-1 · analysis/14 §8.1, §51 이다.
- 원본 분석 절은 final/document.md#3-1 · final/document.md#a14 §8.1, §51 이다.
---
# 스캔에서 뺀 다섯 패키지의 컴포넌트 여섯을 두 자동설정 어느 쪽도 소유하지 않았다
@@ -14,7 +14,7 @@ assets:
evidence:
- ../../../final/evidence/raw/thirteen-startup-rules-never-run.txt
source:
- 원본 분석 절은 final/document.md#5-3 · analysis/20 §3.1 이다.
- 원본 분석 절은 final/document.md#5-3 · final/document.md#a20 §3.1 이다.
---
# 시작 검증기 13개 규칙이 유일한 조립 지점에서 호출되지 않는다
@@ -17,7 +17,7 @@ evidence:
- ../../../final/evidence/raw/three-assembly-paths.txt
- ../../../final/evidence/raw/tl-web-six-unowned-components.txt
source:
- 원본 분석 절은 final/document.md#1-4, #7-1 · analysis/18 · analysis/14 §7.1 이다.
- 원본 분석 절은 final/document.md#1-4, #7-1 · final/document.md#a18 · final/document.md#a14 §7.1 이다.
---
# Spring 조립의 세 경로와 각각이 결정하는 것
@@ -14,7 +14,7 @@ assets:
evidence:
- ../../../final/evidence/raw/when-conditions-are-evaluated.txt
source:
- 원본 분석 절은 analysis/05 §14.4 이다.
- 원본 분석 절은 final/document.md#a05 §14.4 이다.
---
# 조건부 빈의 평가 시점 — 파싱 시점과 등록 시점
@@ -38,7 +38,7 @@ source:
## 등록 방식이 평가 시점을 정한다
:::evidence key="when-conditions-are-evaluated" alt="분석 문서 analysis/05-adapter-outbound-persistence-jpa.md 에서 이 기록의 근거 절을 그대로 잘라낸 16줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/05-adapter-outbound-persistence-jpa.md 발췌 — 16줄" zoom="true"
:::evidence key="when-conditions-are-evaluated" alt="분석 문서 final/document.md#a05 에서 이 기록의 근거 절을 그대로 잘라낸 16줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a05 발췌 — 16줄" zoom="true"
:::
## 이 저장소가 그 함정을 밟았다
@@ -14,8 +14,8 @@ source:
- src/adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/MongoRootAutoConfiguration.java
- src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/persistencejpa/PersistenceJpaRootAutoConfiguration.java
- src/app-bootstrap/src/main/resources/META-INF/spring.factories
- analysis/19-messaging-platform.md
- analysis/05-adapter-outbound-persistence-jpa.md
- final/document.md#a19
- final/document.md#a05
---
# 마스터 스위치는 루트 하나가 소유하고 자식 설정은 조건을 갖지 않는다
@@ -11,8 +11,8 @@ decisionStatus: ADOPTED
decidedOn: 2026-08-30
source:
- src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/persistencejpa/DataSourceRequirement.java
- analysis/05-adapter-outbound-persistence-jpa.md
- analysis/18-app-bootstrap.md
- final/document.md#a05
- final/document.md#a18
---
# 풀이 필요한지는 "JPA가 켜졌나"가 아니라 "커넥션이 필요한 capability가 있나"로 묻는다
@@ -14,7 +14,7 @@ assets:
evidence:
- ../../../final/evidence/raw/cursor-verification-order.txt
source:
- 원본 분석 절은 analysis/05 §2.4 이다.
- 원본 분석 절은 final/document.md#a05 §2.4 이다.
---
# 커서 서명 검증이 길이·상수시간·순서를 전부 지켜야 했던 이유
@@ -81,7 +81,7 @@ OpenJDK : 21.0.12
## 다섯 방어와 그 순서
:::evidence key="cursor-verification-order" alt="분석 문서 analysis/05-adapter-outbound-persistence-jpa.md 에서 이 기록의 근거 절을 그대로 잘라낸 12줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/05-adapter-outbound-persistence-jpa.md 발췌 — 12줄" zoom="true"
:::evidence key="cursor-verification-order" alt="분석 문서 final/document.md#a05 에서 이 기록의 근거 절을 그대로 잘라낸 12줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a05 발췌 — 12줄" zoom="true"
:::
## 각 순서가 막는 것
@@ -15,7 +15,7 @@ assets:
evidence:
- ../../../final/evidence/raw/pii-through-an-exception-message.txt
source:
- 원본 분석 절은 final/document.md#8-1 · analysis/04 §4 이다.
- 원본 분석 절은 final/document.md#8-1 · final/document.md#a04 §4 이다.
---
# 시그니처가 payload를 받지 않는데 예외 메시지로 PII가 로그에 남았다
@@ -16,7 +16,7 @@ assets:
evidence:
- ../../../final/evidence/raw/cardinality-bounds-as-types.txt
source:
- 원본 분석 절은 final/document.md#4-1, #5-2 · analysis/05 §2.1, §12.2 · analysis/02 이다.
- 원본 분석 절은 final/document.md#4-1, #5-2 · final/document.md#a05 §2.1, §12.2 · final/document.md#a02 이다.
---
# 카디널리티 경계를 타입으로 표현하기
@@ -16,7 +16,7 @@ assets:
evidence:
- ../../../final/evidence/raw/signed-cursor-structure.txt
source:
- 원본 분석 절은 final/document.md#10-4 · analysis/05 §2.4 · analysis/19 §4.x 이다.
- 원본 분석 절은 final/document.md#10-4 · final/document.md#a05 §2.4 · final/document.md#a19 §4.x 이다.
---
# 서명된 커서의 구조와 검증 순서
@@ -12,7 +12,7 @@ decidedOn: 2026-08-30
source:
- src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/query/SignedJsonCursorCodec.java
- src/grpc/grpc-policy/src/main/java/dev/caskeleton/grpc/policy/streaming/GrpcResumeTokenCodec.java
- analysis/05-adapter-outbound-persistence-jpa.md
- final/document.md#a05
---
# 커서에 서명하는 이유는 기밀성이 아니라 무결성이다
@@ -14,8 +14,8 @@ source:
- src/shared-contract/src/main/java/dev/caskeleton/shared/metrics/ForbiddenMetricTags.java
- src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/JpaMetricTags.java
- src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/experimental/tenant/TenantId.java
- analysis/02-shared-contract.md
- analysis/05-adapter-outbound-persistence-jpa.md
- final/document.md#a02
- final/document.md#a05
---
# tenant id는 메트릭 태그가 되지 않는다
@@ -18,7 +18,7 @@ evidence:
- ../../../final/evidence/raw/a10-f001-readme.txt
- ../../../final/evidence/raw/a10-f001-readme-counts.txt
source:
- 원본 분석 절은 analysis/10-adapter-outbound-cache-redis.md#L121 이다. 등급은 P2 다. 표 네 행 중 셋이 사실과 다르다는 판정, 패키지별 파일과 줄 수, `@Bean` 메서드 일곱, 빌드 파일 주석의 임포트 계수, 그리고 무겁게 보는 세 이유가 그 절에 있다.
- 원본 분석 절은 final/document.md#a10#L121 이다. 등급은 P2 다. 표 네 행 중 셋이 사실과 다르다는 판정, 패키지별 파일과 줄 수, `@Bean` 메서드 일곱, 빌드 파일 주석의 임포트 계수, 그리고 무겁게 보는 세 이유가 그 절에 있다.
- 그 절은 이 수치를 LOC 라고 적지만 실제로 센 것은 물리 줄 수여서, 여기서는 줄 수라고만 적는다.
- 표가 둘째 축의 증거로 지목한 시험이 표를 반증한다는 것, 같은 README 의 산문이 세 줄 뒤에서 표와 어긋난다는 것, 다섯 포트 중 세션만 표가 맞다는 것, 일곱째 `@Bean` 이 조건부라는 것, 그리고 주석의 주어절은 맞고 괄호만 틀렸다는 것은 이 기록에서 확인했다.
---
@@ -18,7 +18,7 @@ evidence:
- ../../../final/evidence/raw/a10-f004-pub-sub.txt
- ../../../final/evidence/raw/a10-f004-pub-sub-bound.txt
source:
- 원본 분석 절은 analysis/10-adapter-outbound-cache-redis.md#L263 이다. 등급은 P3 이다. 세 타입 중 하나만 크기 검사를 부른다는 표, 채널 이름이 Redis 에서 키와 같은 문자열 공간을 쓰고 같은 상한을 받는다는 지적, 구성 요소가 각자 토큰 길이를 제한하므로 현실적인 초과는 어렵다는 단서, 그리고 두 채널 타입의 렌더 본문이 같다는 관찰이 그 절에 있다. 원본이 P3 의 근거로 든 것은 검사가 패턴에는 있고 채널에는 없다는 비대칭 자체다.
- 원본 분석 절은 final/document.md#a10#L263 이다. 등급은 P3 이다. 세 타입 중 하나만 크기 검사를 부른다는 표, 채널 이름이 Redis 에서 키와 같은 문자열 공간을 쓰고 같은 상한을 받는다는 지적, 구성 요소가 각자 토큰 길이를 제한하므로 현실적인 초과는 어렵다는 단서, 그리고 두 채널 타입의 렌더 본문이 같다는 관찰이 그 절에 있다. 원본이 P3 의 근거로 든 것은 검사가 패턴에는 있고 채널에는 없다는 비대칭 자체다.
- 이 기록이 더한 것은 셋이다. 원본이 「현실적인 초과는 어렵다」고만 적은 것에 수를 붙였다 — 최대 388 바이트, 기본 이름공간에서 221 바이트다. 같은 규칙을 받은 조각들로 만든 키가 슬롯 태그가 붙으면 519 바이트가 되어 같은 검사에 거부된다. 그리고 그 검사가 보는 상한이 주입 인자인데 저장소에 주입하는 곳이 없다.
- 원본 backlog 가 reachability 로 적어 둔 「긴 namespace/entity/identifier 조합」은 388 바이트가 답이다. 셋을 최대로 채워도 상수 상한을 넘지 않는다. 넘는 경로는 슬롯 태그 쪽이고, 그것도 채널이 아니라 키에서 일어난다.
---
@@ -21,7 +21,7 @@ evidence:
- ../../../final/evidence/raw/a10-f005-hyperloglog-merge-scope.txt
- ../../../final/evidence/raw/a10-f005-hyperloglog-merge-budget.txt
source:
- 원본 분석 절은 analysis/10-adapter-outbound-cache-redis.md#L277 이다. 등급은 P3 이다. 다중 키 fan-in 표 다섯 줄 중 확률적 집계 둘만 서명에 예산 인자가 없다는 표, 두 명령의 비용이 입력 레지스터 수에 비례한다는 지적, 레지스터가 12KB 고정이라 폭발 범위가 좁다는 판단, 그리고 규칙의 예외가 이유 없이 존재한다는 문장이 그 절에 있다. 그 표의 첫 줄이 집합 대수 셋을 묶은 것이라 연산 수로는 일곱이다.
- 원본 분석 절은 final/document.md#a10#L277 이다. 등급은 P3 이다. 다중 키 fan-in 표 다섯 줄 중 확률적 집계 둘만 서명에 예산 인자가 없다는 표, 두 명령의 비용이 입력 레지스터 수에 비례한다는 지적, 레지스터가 12KB 고정이라 폭발 범위가 좁다는 판단, 그리고 규칙의 예외가 이유 없이 존재한다는 문장이 그 절에 있다. 그 표의 첫 줄이 집합 대수 셋을 묶은 것이라 연산 수로는 일곱이다.
- 이 기록에서 확인한 것은 셋이다. 두 명령도 예산 없이는 관문을 통과하지 못하고, 그 예산은 SDK가 채우며, 채운다는 설계는 상한 타입의 첫 문단에 적혀 있다.
- 남는 문제는 원본이 든 것과 다르다. 예산을 만드는 세 메서드가 요청 바이트 상한을 잴 대상에서 그대로 가져오므로, 이 두 명령뿐 아니라 SDK가 예산을 채우는 R2 명령 전부에서 관문의 요청 바이트 검사가 발화하지 못한다.
---
@@ -18,7 +18,7 @@ evidence:
- ../../../final/evidence/raw/a10-f006-requireidentifier.txt
- ../../../final/evidence/raw/a10-f006-requireidentifier-branch.txt
source:
- 원본 분석 절은 analysis/10-adapter-outbound-cache-redis.md#L375 이다. 등급은 P3 이다. 문자 클래스가 메일과 전화 형태의 필수 문자를 이미 배제하므로 두 분기가 도달 불가라는 관찰, 대응 test 가 예외 타입만 보므로 그 사실을 가리지 않는다는 지적, 보안 효과는 그대로이고 잃는 것은 진단 품질과 검증 겹수의 착시라는 판단, 그리고 두 분기를 지우거나 검사 순서를 뒤집으라는 제안이 그 절에 있다.
- 원본 분석 절은 final/document.md#a10#L375 이다. 등급은 P3 이다. 문자 클래스가 메일과 전화 형태의 필수 문자를 이미 배제하므로 두 분기가 도달 불가라는 관찰, 대응 test 가 예외 타입만 보므로 그 사실을 가리지 않는다는 지적, 보안 효과는 그대로이고 잃는 것은 진단 품질과 검증 겹수의 착시라는 판단, 그리고 두 분기를 지우거나 검사 순서를 뒤집으라는 제안이 그 절에 있다.
- 이 기록이 더한 것은 셋이다. 두 입력에 실제로 돌아오는 메시지가 문자 클래스 메시지라는 실행 결과. 웹 토큰 분기는 도달하지만 그것을 겨냥한 test 입력이 접두 검사에도 걸려 지워도 초록이고, 혼자 잡는 것은 실제 토큰이 아니라 합성 값이라는 것. 그리고 네 메시지를 단언하는 곳이 저장소에 하나도 없다는 것이다. 원본이 도달 불가로 센 것은 둘이고, 지워도 test 가 초록인 것은 셋이다.
---
@@ -15,7 +15,7 @@ assets:
evidence:
- ../../../final/evidence/raw/analysis-finding-a10-f003.txt
source:
- 원본 분석 절은 analysis/10-adapter-outbound-cache-redis.md#L251 이다.
- 원본 분석 절은 final/document.md#a10#L251 이다.
---
# SDK가 선언한 두 진입점에 구현이 없다
@@ -86,7 +86,7 @@ SDK가 선언한 두 진입점에 구현이 없다.
## SDK 가 선언한 두 진입점
:::evidence key="analysis-finding-a10-f003" alt="분석 문서 analysis/10-adapter-outbound-cache-redis.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/10-adapter-outbound-cache-redis.md 발췌 — 15줄" zoom="true"
:::evidence key="analysis-finding-a10-f003" alt="분석 문서 final/document.md#a10 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a10 발췌 — 15줄" zoom="true"
:::
## 데이터 위험은 없다
@@ -15,7 +15,7 @@ assets:
evidence:
- ../../../final/evidence/raw/analysis-finding-a10-f007.txt
source:
- 원본 분석 절은 analysis/10-adapter-outbound-cache-redis.md#L485 이다.
- 원본 분석 절은 final/document.md#a10#L485 이다.
---
# 패턴 구독의 R2 승인만 호출자가 아니라 배포에 대해 이루어진다
@@ -15,7 +15,7 @@ assets:
evidence:
- ../../../final/evidence/raw/analysis-finding-a10-f008.txt
source:
- 원본 분석 절은 analysis/10-adapter-outbound-cache-redis.md#L502 이다.
- 원본 분석 절은 final/document.md#a10#L502 이다.
---
# permit 정책 이름이 세 곳에 문자열로 존재하고 교차 검사가 없다
File diff suppressed because it is too large Load Diff
@@ -16,7 +16,7 @@ assets:
evidence:
- ../../../final/evidence/raw/adapter-inbound-web-c01.txt
source:
- 원본 분석 절은 analysis/14-adapter-inbound-web.md#L116 이다.
- 원본 분석 절은 final/document.md#a14#L116 이다.
module: adapter-inbound-web
---
@@ -50,7 +50,7 @@ WebFlux: @ConditionalOnWebApplication(REACTIVE) + @ConditionalOnProperty(backend
## 분석 원문의 게이트 비교
:::evidence key="adapter-inbound-web-c01" alt="분석 문서 analysis/14-adapter-inbound-web.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/14-adapter-inbound-web.md 발췌 — 15줄" zoom="true"
:::evidence key="adapter-inbound-web-c01" alt="분석 문서 final/document.md#a14 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a14 발췌 — 15줄" zoom="true"
:::
<!-- body:end -->
@@ -14,7 +14,7 @@ assets:
evidence:
- ../../../final/evidence/raw/adapter-inbound-web-c17.txt
source:
- 원본 분석 절은 analysis/14-adapter-inbound-web.md#L1255 이다.
- 원본 분석 절은 final/document.md#a14#L1255 이다.
module: adapter-inbound-web
---
@@ -16,7 +16,7 @@ assets:
evidence:
- ../../../final/evidence/raw/adapter-outbound-objectstorage-c01.txt
source:
- 원본 분석 절은 analysis/09-adapter-outbound-objectstorage.md#L63 이다.
- 원본 분석 절은 final/document.md#a09#L63 이다.
module: adapter-outbound-objectstorage
---
@@ -14,7 +14,7 @@ assets:
evidence:
- ../../../final/evidence/raw/adapter-outbound-objectstorage-c08.txt
source:
- 원본 분석 절은 analysis/09-adapter-outbound-objectstorage.md#L560 이다.
- 원본 분석 절은 final/document.md#a09#L560 이다.
module: adapter-outbound-objectstorage
---
@@ -16,7 +16,7 @@ assets:
evidence:
- ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c24.txt
source:
- 원본 분석 절은 analysis/05-adapter-outbound-persistence-jpa.md#L1610 이다.
- 원본 분석 절은 final/document.md#a05#L1610 이다.
module: adapter-outbound-persistence-jpa
---
@@ -14,7 +14,7 @@ assets:
evidence:
- ../../../final/evidence/raw/app-bootstrap-c01.txt
source:
- 원본 분석 절은 analysis/18-app-bootstrap.md#L179 이다.
- 원본 분석 절은 final/document.md#a18#L179 이다.
module: app-bootstrap
---
@@ -14,7 +14,7 @@ assets:
evidence:
- ../../../final/evidence/raw/app-bootstrap-c02.txt
source:
- 원본 분석 절은 analysis/18-app-bootstrap.md#L185 이다.
- 원본 분석 절은 final/document.md#a18#L185 이다.
module: app-bootstrap
---
@@ -14,7 +14,7 @@ assets:
evidence:
- ../../../final/evidence/raw/app-bootstrap-c03.txt
source:
- 원본 분석 절은 analysis/18-app-bootstrap.md#L264 이다.
- 원본 분석 절은 final/document.md#a18#L264 이다.
module: app-bootstrap
---
@@ -16,7 +16,7 @@ assets:
evidence:
- ../../../final/evidence/raw/grpc-advanced-diagnostics-c02.txt
source:
- 원본 분석 절은 analysis/grpc/grpc-advanced-diagnostics.md#L51 이다.
- 원본 분석 절은 final/document.md#a20-grpc-advanced-diagnostics#L51 이다.
module: grpc-advanced-diagnostics
---
@@ -14,7 +14,7 @@ assets:
evidence:
- ../../../final/evidence/raw/messaging-kafka-share-experimental-c07.txt
source:
- 원본 분석 절은 analysis/messaging/messaging-kafka-share-experimental.md#L399 이다.
- 원본 분석 절은 final/document.md#a19-messaging-kafka-share-experimental#L399 이다.
module: messaging-kafka-share-experimental
---
@@ -14,7 +14,7 @@ assets:
evidence:
- ../../../final/evidence/raw/messaging-testkit-c05.txt
source:
- 원본 분석 절은 analysis/messaging/messaging-testkit.md#L452 이다.
- 원본 분석 절은 final/document.md#a19-messaging-testkit#L452 이다.
module: messaging-testkit
---
@@ -15,7 +15,7 @@ assets:
evidence:
- ../../../final/evidence/raw/a05-f022-stable.txt
source:
- 원본 분석 절은 `analysis/05-adapter-outbound-persistence-jpa.md` §69 다. 등급은 P1 이다. 검증기 자체는 빈으로 구성되지만 정책을 적용하는 호출자가 없다는 판정과, 액추에이터 불리언이 생성 권한 일부만 확인한다는 관찰이 그 절에 있다.
- 원본 분석 절은 `final/document.md#a05` §69 다. 등급은 P1 이다. 검증기 자체는 빈으로 구성되지만 정책을 적용하는 호출자가 없다는 판정과, 액추에이터 불리언이 생성 권한 일부만 확인한다는 관찰이 그 절에 있다.
- 검증기 javadoc 의 두 주장과 실제 호출 시점·예외 처리의 대조, 그리고 두 시험이 각각 절반만 덮는다는 것은 이 기록에서 확인했다.
---
@@ -14,7 +14,7 @@ assets:
evidence:
- ../../../final/evidence/raw/the-support-matrix-says-nothing-is-deployed-and-eighteen-are.txt
source:
- 원본 분석 절은 analysis/messaging/messaging-core-api.md §17 이다.
- 원본 분석 절은 final/document.md#a19-messaging-core-api §17 이다.
---
# 운영자용 지원 매트릭스가 런타임 편입을 반대로 적고, 틀린 쪽이 옳은 쪽을 권위로 지목한다
@@ -69,7 +69,7 @@ Gradle : 9.0.0
## 매트릭스의 문장과 레지스트리의 값
:::evidence key="the-support-matrix-says-nothing-is-deployed-and-eighteen-are" alt="분석 문서 analysis/messaging/messaging-core-api.md 에서 이 기록의 근거 절을 그대로 잘라낸 18줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/messaging/messaging-core-api.md 발췌 — 18줄" zoom="true"
:::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"
:::
## 틀린 문단이 권위로 지목하는 문서는 이미 정정을 마쳤다
@@ -14,7 +14,7 @@ assets:
evidence:
- ../../../final/evidence/raw/the-transport-and-the-validator-answer-differently.txt
source:
- 원본 분석 절은 analysis/messaging/messaging-pulsar-experimental.md §17.1 이다.
- 원본 분석 절은 final/document.md#a19-messaging-pulsar-experimental §17.1 이다.
---
# 같은 어댑터의 능력을 전송과 검증기가 다르게 답하고, 런타임이 쓰는 쪽이 record 의 의미와 어긋난다
@@ -16,7 +16,7 @@ assets:
evidence:
- ../../../final/evidence/raw/three-sources-of-a-capability-answer.txt
source:
- 원본 분석 절은 analysis/messaging/messaging-core-api.md §4.12 · analysis/99-cross-scope.md §3.2 이다.
- 원본 분석 절은 final/document.md#a19-messaging-core-api §4.12 · final/document.md#a99 §3.2 이다.
---
# 능력 선언의 세 출처와 그것이 파생되지 않을 때
@@ -1,102 +1,132 @@
---
id:
kind: CASE
slug: commit-ambiguity-is-not-only-sqlstate-08
title: pg_terminate_backend가 57P01로 도착하고 커밋 레코드는 이미 WAL에 있었다
title: pg_terminate_backend 가 57P01 로 도착하고 커밋 레코드는 이미 WAL 에 있었다
topic: commit-ambiguity-as-a-result
topicName: 커밋 모호성 — 「모른다」를 결과로 유지하기
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:commit-ambiguity-is-not-only-sqlstate-08
evidenceCapturedOn: 2026-09-01
body: case-commit-ambiguity-is-not-only-sqlstate-08.body.md
assets:
- key: commit-ambiguity-is-not-only-sqlstate-08
file: ../../../final/evidence/rendered/commit-ambiguity-is-not-only-sqlstate-08.svg
evidence:
- ../../../final/evidence/raw/commit-ambiguity-is-not-only-sqlstate-08.txt
studio: ""
lastVerifiedOn: 2026-08-29
source:
- 원본 분석 절은 final/document.md#4-1 · analysis/05 §3.5 이다.
- final/document.md#4-1
- final/document.md#a05 §136
- final/document.md#a05 §139
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
evidence:
- ../../../final/evidence/raw/050-jpa-commit-ambiguity-probe.txt
- ../../../final/evidence/raw/115-integration-lane-original-verification.txt
---
# pg_terminate_backend가 57P01로 도착하고 커밋 레코드는 이미 WAL에 있었다
# pg_terminate_backend 가 57P01 로 도착하고 커밋 레코드는 이미 WAL 에 있었다
커밋 모호성을 SQLSTATE class 08로만 정의했던 규칙이, 서버가 자기 종료를 알리는 57P01 앞에서 성립하지 않았다. 그 시점에 커밋 레코드는 이미 WAL에 있을 수 있다.
커밋 모호성을 SQLSTATE class 08 로만 정의 규칙이, 서버가 자기 종료를 알리는 57P01 앞에서 성립하지 않았다. 그 코드가 도착하는 시점에 커밋 레코드는 이미 WAL 에 있을 수 있다.
## 관계
- **트랜잭션 결과 대수 — 다섯 변형이 각각 답하는 질문**
이 사례의 결론이 어느 결과 변형으로 들어가는지를 정의한다.
- **모르는 것은 성공도 실패도 아닌 세 번째 결과여야 한다**
이 사례가 그 세 번째 결과를 필요로 하는 이유다.
- **커밋 모호성 판정은 넓혀도 좁혀도 해롭다**
이 사례가 규칙을 넓힌 쪽이고, 그 규칙의 반대편 비용을 함께 다룬다.
- **completion-unknown은 자동으로도 수동으로도 재시도하지 않는다**
- **completion-unknown 은 자동으로도 수동으로도 재시도하지 않는다**
이 분류가 만들어 내는 예외를 어떻게 다룰지 정한 결정이다.
- **커밋 모호성 계약 레인이 이 리비전에서 통과하는지 실행으로 확인되지 않았다**
이 사례의 회귀 방지 레인에 대한 미해결 질문이다.
- **커밋 모호성 레인이 HEAD 에서 통과하는지 실행이 아니라 드리프트 0 으로 답했다**
이 사례를 고정하는 계약 레인에 남은 질문이다.
## 문제
설계는 커밋 모호성을 SQLSTATE class 08, 즉 연결 예외로 정의했다. 그 정의는 클라이언트가 자기 연결에서 무슨 일이 일어났는지를 기준으로 삼는다.
설계는 커밋 모호성을 SQLSTATE class 08, 즉 연결 예외로 정의했다. 그 정의는 클라이언트가 자기 연결에서 무슨 일을 겪었는지를 기준으로 삼는다.
문제는 서버가 스스로 종료를 알리는 경우다. in-flight 커밋 중인 백엔드를 pg_terminate_backend 로 끊으면 클라이언트는 57P01 을 받는다. 이것은 class 08 이 아니다. 클라이언트의 연결 시도에 대한 이야기가 아니라 서버가 자기 종료를 알린 것이기 때문이다.
서버가 스스로 종료를 알리는 경우가 그 정의 밖으로 나간다. 커밋이 진행 중인 백엔드를 pg_terminate_backend 로 끊으면 클라이언트는 57P01 을 받는다. 이 코드는 class 08 이 아니다. 클라이언트의 연결 시도가 아니라 서버가 자기 종료를 알린 것이어서 계열이 다르다.
그러나 커밋 입장에서 결과는 같고 오히려 더 나쁘다. 57P01 이 도착한 시점에 커밋 레코드가 이미 WAL 에 있을 수 있다. class 08 만 모호성으로 보는 규칙은 이 실패를 평범한 실패로 분류하고, 평범한 실패는 재시도된다. 커밋됐을 수도 있는 쓰기를 다시 실행하는 경로가 여기서 열린다.
커밋 입장에서 결과는 같고 오히려 더 나쁘다. class 08 만 모호성으로 보는 규칙은 이 실패를 평범한 실패로 분류하고, 평범한 실패는 재시도된다. 커밋됐을 수도 있는 쓰기를 다시 실행하는 경로가 여기서 열린다.
## 결론
규칙이 57P01 과 57P02 와 57P03 까지 넓어졌다.
현재 CommitFailureClassifier 는 네 가지를 모호성으로 본다.
CommitFailureClassifier 가 모호성으로 보는 상태는 네 가지다.
40003 : statement completion unknown
08 로 시작하는 상태 : connection exception
08 로 시작하는 상태 : connection exception
57P01 57P02 57P03 : 서버가 자기 종료를 알린 상태
transport 수준 단절
세 상태를 함께 넣은 이유는 코드 주석에 남아 있다. 57P01 은 종료된 백엔드나 fast shutdown 이나 failover 가 보고하는 것이고, 그것이 도착할 때 커밋 레코드 이미 WAL 에 있을 수 있다. 57P02 는 crash shutdown, 57P03 은 지금 접속할 수 없음이며, 서버가 in-flight 커밋을 어떻게 했든 클라이언트가 그것을 알지 못했다는 점에서 같다.
세 상태를 함께 넣은 근거는 코드 주석에 남아 있다. 57P01 은 종료된 백엔드나 fast shutdown 이나 failover 가 보고하고, 그것이 도착할 때 커밋 레코드 이미 WAL 에 있을 수 있다. 57P02 는 crash shutdown, 57P03 은 지금 접속할 수 없음이며, 서버가 진행 중인 커밋을 어떻게 처리했든 클라이언트가 그것을 알지 못했다는 점에서 셋이 같다.
넓어진 규칙이 다시 좁아지지 못하도록 CommitAmbiguityContractTest 가 SQLSTATE 를 직접 assert 한다.
넓어진 규칙이 다시 좁아지지 못하도록 CommitAmbiguityContractTest 가 SQLSTATE 를 직접 단언한다.
이 규칙에는 반대 방향의 비용도 함께 기록되어 있다. 커밋 단계의 모든 연결 오류를 모호성으로 표시하면 평범한 풀 고갈과 서버 재시작이 조정 큐로 밀려들고, 운영자는 그 큐를 읽지 않고 비우는 습관을 배운다. 그리고 정작 중요한 항목 하나가 나머지와 함께 지워진다.
반대 방향의 비용도 같은 자료에 적혀 있다. 커밋 단계의 모든 연결 오류를 모호성으로 표시하면 평범한 풀 고갈과 서버 재시작이 조정 큐로 밀려든다. 그러면 운영자는 그 큐를 읽지 않고 비우는 습관을 배우고, 중요했던 한 건이 나머지와 같이 지워진다. 규칙은 넓혀도 좁혀도 해롭고, 그래서 커밋 단계와 드라이버의 침묵이 동시에 성립할 때로 조건이 좁혀져 있다.
## 검증 환경
OpenJDK : 21.0.12
OpenJDK : 21
Gradle : 9.0.0
Spring Boot : 4.0.8
데이터베이스 : PostgreSQL
관측 출처 : 저장소가 기록한 컨테이너 레인. 이 분석에서 재실행하지 않았다
데이터베이스 : PostgreSQL, Testcontainers 로 기동
레인 리비전 : a24ece9cf797f7ea647e33bf846b115208ed1ba5
레인 실행 시각 : 2026-08-29T14:46:23+00:00
## 재현 조건
원래 관측은 컨테이너 레인에서 나왔다. 절차는 다음과 같다.
1. 커밋이 진행 중인 트랜잭션을 만든다.
2. 그 백엔드를 pg_terminate_backend 로 끊는다.
3. 클라이언트가 받 SQLSTATE 와 그 시점의 WAL 상태를 확인한다.
3. 클라이언트가 받 SQLSTATE 와 그 시점의 커밋 레코드 상태를 확인한다.
4. CommitFailureClassifier 가 그 상태를 TransactionCompletionUnknownException 과 retryable=false 로 번역하는지 본다.
번 사이클에서 확인한 것은 규칙의 현재 코드 형태와 그 근거 문장이다.
절차는 CommitAmbiguityContractTest 가 수행하고, 그 클래스는 jpaPlatformFailureTest 레인에 있다.
## 본문
<!-- body:start -->
설계가 "커밋 모호성은 SQLSTATE class 08뿐"이라고 적었다.
## 규칙이 처음 적혀 있던 형태
## 컨테이너 레인이 보인 것
설계 문서는 커밋 모호성을 한 문장으로 정의했다.
in-flight 커밋 중인 백엔드를 `pg_terminate_backend`로 끊었을 때 `57P01`(admin_shutdown)이 도착하며, 그 시점에 커밋 레코드가 이미 WAL에 있을 수 있다.
> 커밋 모호성은 SQLSTATE class 08 뿐이다.
## CommitAmbiguityContractTest 참조 위치
class 08 은 connection exception 계열이고, 클라이언트가 연결을 잃었다는 사실을 말한다. 커밋 응답을 받지 못한 상황을 여기에 대응시키는 것은 처음에는 맞아 보인다.
:::evidence key="commit-ambiguity-is-not-only-sqlstate-08" alt="코드베이스에서 CommitAmbiguityContractTest 를 검색한 출력 1줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="CommitAmbiguityContractTest 코드베이스 검색 — 1줄 · exit 0" zoom="true"
:::
## 레인이 보여 준 SQLSTATE
## 규칙이 넓어지고 다시 좁아지지 못하게 고정됐다
컨테이너 레인은 커밋이 진행 중인 백엔드를 `pg_terminate_backend` 로 끊는다. 클라이언트가 받은 것은 `08006` 이 아니라 `57P01` 이었다.
`57P01/57P02/57P03`까지 넓어졌고 `CommitAmbiguityContractTest`가 SQLSTATE를 직접 assert한다.
`57P01``admin_shutdown` 이고, 서버가 자기 종료를 알린 것이다. PostgreSQL 은 이 상황을 클라이언트 연결 문제로 보고하지 않는데, 커밋을 요청한 쪽에서 보면 두 상황은 구별되지 않는다 — 응답이 오지 않았고, 서버가 무엇을 했는지 알 수 없다.
그리고 `57P01` 이 도착할 때 커밋 레코드는 이미 WAL(write-ahead log, 미리 쓰기 로그)에 기록돼 있을 수 있다. 종료 신호와 커밋 기록 사이에 순서 보장이 없으므로 둘 중 어느 쪽이 먼저였는지 클라이언트는 알지 못한다.
## 넓힌 규칙과 그것을 고정하는 단언
현재 규칙은 네 갈래다.
| 상태 | 무엇을 뜻하나 |
|---|---|
| `40003` | statement completion unknown |
| `08*` | connection exception |
| `57P01` · `57P02` · `57P03` | 서버가 자기 종료를 알렸다 |
| transport 단절 | 드라이버가 응답을 받지 못했다 |
`CommitAmbiguityContractTest` 는 이 목록을 문장으로 서술하지 않고 SQLSTATE 값을 직접 단언한다. 규칙을 좁히는 변경은 그 단언을 깨뜨려야만 들어올 수 있다.
## 프로브가 보여 준 경계
같은 사이클에서 돌린 프로브는 다른 것을 보여 준다. 가짜 `PlatformTransactionManager` 의 커밋이 `08006` 을 던지게 하고 현재의 벤더 번역기를 태우면 결과는 이렇다.
```text
type=ConnectionUnavailableException
category=CONNECTION_UNAVAILABLE
completionUnknown=false
retryable=false
```
`08006` 이 그 자체로 completion-unknown 을 만들지 않는다. 판정에는 관측된 단계가 함께 필요하고, 이 프로브는 실제 소켓 유실을 흉내 내지 않으며 데이터베이스가 실제로 커밋했음을 증명하지도 않는다. 프로브 파일의 관측 경계에 그 한계가 적혀 있다.
## 확인하지 못한 것
jpaPlatformFailureTest 를 이 리비전에서 실행하지 않았다. 57P01 관측이 PostgreSQL 16 과 17 과 18 전부에서 재현되는지도 확인하지 않았다. 관련 미해결 질문에 그 조건을 적어 두었다.
레인은 `a24ece9c` 에서 돌았다. 다섯 태그 레인을 `--rerun-tasks` 로 실행해 51 클래스 244 tests 가 0 failures 로 끝났고 `jpaPlatformFailureTest` 는 그중 2 클래스 6 tests 였다. HEAD 인 `21234e38` 에서는 다시 돌리지 않았고, 두 리비전 사이에 persistence-jpa 경로의 변경 파일이 0 이라는 측정으로 대신했다.
`57P01` 관측이 PostgreSQL 16 과 17 과 18 에서 같게 나오는지도 재지 않았다. 레인의 매트릭스는 실행 한 번에 major 하나만 고르는데 이 사이클은 한 번만 돌렸다.
<!-- body:end -->
@@ -1,111 +0,0 @@
---
kind: CASE
slug: high-water-mark-swallowed-a-redelivered-change
title: high-water mark가 "본 위치"를 뜻해서 재전달된 변경이 영구히 사라졌다
topic: commit-ambiguity-as-a-result
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:high-water-mark-swallowed-a-redelivered-change
evidenceCapturedOn: 2026-09-01
body: case-high-water-mark-swallowed-a-redelivered-change.body.md
assets:
- key: high-water-mark-swallowed-a-redelivered-change
file: ../../../final/evidence/rendered/high-water-mark-swallowed-a-redelivered-change.svg
evidence:
- ../../../final/evidence/raw/high-water-mark-swallowed-a-redelivered-change.txt
source:
- 원본 분석 절은 final/document.md#4-2 · analysis/06 §67 이다.
---
# high-water mark가 "본 위치"를 뜻해서 재전달된 변경이 영구히 사라졌다
change stream 파이프라인의 high-water mark가 이벤트 수신 즉시 전진한다. 그래서 투영이 끝나기 전에 failover가 나면 재전달분이 mark에 걸려 버려지고, 다음 이벤트의 checkpoint가 그것을 지나친다. 구독 상태는 RUNNING이다.
## 관계
- **모르는 것은 성공도 실패도 아닌 세 번째 결과여야 한다**
본 적 있지만 완료되지 않은 위치라는 제3의 상태가 없어서 생긴 손실이다.
- **조건이 만족될 수 없는 outbox 사슬**
같은 형태의 손실이 다른 리프에서 나타난 사례다.
## 문제
MongoChangeStreamPipeline 은 이벤트마다 위치를 비교해 앞으로 가는 것만 처리한다.
processOne : advancesPosition 이 false 면 Flux.empty
advancesPosition : highWaterMark 를 getAndAccumulate 로 갱신하고 전진 여부를 반환
문제는 이 갱신이 투영 실행 전에 일어난다는 점이다. mark 는 완료된 위치가 아니라 본 위치를 뜻한다. 그리고 resume 이 같은 pipeline 인스턴스를 재사용하므로 그 mark 가 남는다.
## 결론
worker 하나, 평범한 failover 만으로 변경이 영구히 사라진다. probe 세 개가 그것을 관측했다.
probe C 가 결정적이다. 스트림 1 이 이벤트 E 를 전달하고 투영이 200 밀리초 걸리는 동안 50 밀리초 시점에 재개 가능한 드라이버 오류로 스트림이 끊긴다. 서버가 재개하면 checkpoint 가 E 를 지나지 않았으므로 E 를 다시 보내고 이어서 F 를 보낸다.
관측 결과는 이렇다.
terminal : COMPLETED, opens 2
projector : started 2, completed 1
results : APPLIED 하나
checkpoints saved : token-6
highWaterMark : 6.1
state : RUNNING, runbook 비어 있음
E 의 투영은 시작됐다가 failover 로 취소됐다. 재개 시 파이프라인은 재전달된 E 를 버렸다. E 의 첫 전달 때 이미 mark 가 5.1 로 전진했기 때문이다. 그 뒤 F 가 투영되고 checkpoint 가 저장되면서 저장 위치가 E 를 지나쳤다. change stream 은 checkpoint 가 지난 것을 다시 재생하지 않으므로 E 는 도달 불가능해졌다.
그런데 구독은 RUNNING 이고 runbook 은 비어 있고 호출자의 Flux 는 정상 완료한다. 손실을 알리는 신호가 없다.
probe A 는 같은 손실이 BUSY 청구와 재개 가능 실패의 조합으로도 나타남을 보인다. token-5 는 투영되지 않았는데 checkpoint 는 그것을 지나쳤다.
근본 원인은 상태 어휘다. 본 적 있지만 완료되지 않은 위치라는 제3의 상태가 없어서, 파이프라인은 본 것과 완료한 것을 같은 값으로 다룬다.
## 검증 환경
OpenJDK : 21.0.12
probe 구성 : in-memory, reactor 와 플랫폼 자체 타입만 사용, 서버 없음
조립 방식 : MongoPlatformAutoConfiguration 과 동일하게 consumer 를 조립
probe 클래스 : 임시로 추가하고 실행 후 제거
production 소스 수정 : x
## 재현 조건
원문은 final/evidence/raw/135a-mongo-changestream-execution-probes.txt 에 있고, 매니페스트는 135-mongo-changestream-manifest-and-probes.txt 에 있다.
probe C 의 조건은 다음과 같다.
1. worker 하나, 중복 제거는 항상 청구를 허용한다.
2. 스트림 1 이 클러스터 시각 5.1 의 이벤트 E 를 전달한다.
3. 투영은 200 밀리초 걸린다.
4. 50 밀리초 시점에 재개 가능한 드라이버 오류로 스트림이 끊긴다. errorLabels 는 ResumableChangeStreamError, code 는 133 이다.
5. 스트림 2 가 E 를 재전달하고 이어서 클러스터 시각 6.1 의 F 를 전달한다.
## 본문
<!-- body:start -->
mark가 이벤트 수신 즉시 전진하므로 "완료된 위치"가 아니라 "본 위치"를 뜻하고, resume이 같은 pipeline 인스턴스를 재사용해 mark가 남는다.
## mark 가 전진하는 시점
:::evidence key="high-water-mark-swallowed-a-redelivered-change" alt="분석 문서 final/document.md 에서 이 기록의 근거 절을 그대로 잘라낸 18줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md 발췌 — 18줄" zoom="true"
:::
## probe 세 개가 관측한 것
worker 하나·평범한 failover에서 checkpoint 없이 지나간 이벤트를 다음 이벤트의 checkpoint가 추월하고, 그 사이 재전달분을 pipeline이 삼킨다.
## 아무 신호도 나지 않는다
구독은 `RUNNING`, runbook 비어 있음, caller의 `Flux`는 정상 완료.
## 세 테스트가 각각 절반씩만 본다
"본 적 있지만 완료되지 않은 위치"라는 제3의 상태가 없다.
## 확인하지 못한 것
probe 는 in-memory 구성이다. 실제 replica set failover 에서 같은 순서가 재현되는지는 확인하지 않았다.
<!-- body:end -->
@@ -1,100 +0,0 @@
---
kind: CASE
slug: two-owners-popped-the-evidence-frame
title: 커밋 증거 프레임을 두 주인이 pop해서 바깥 트랜잭션의 실패가 익명이 됐다
topic: commit-ambiguity-as-a-result
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:two-owners-popped-the-evidence-frame
evidenceCapturedOn: 2026-09-01
body: case-two-owners-popped-the-evidence-frame.body.md
assets:
- key: two-owners-popped-the-evidence-frame
file: ../../../final/evidence/rendered/two-owners-popped-the-evidence-frame.svg
evidence:
- ../../../final/evidence/raw/two-owners-popped-the-evidence-frame.txt
source:
- 원본 분석 절은 final/document.md#4-1 · analysis/05 §3.4 이다.
---
# 커밋 증거 프레임을 두 주인이 pop해서 바깥 트랜잭션의 실패가 익명이 됐다
증거 프레임을 pop 하는 주인이 둘이었다. REQUIRES_NEW 안쪽 트랜잭션에서 executor 가 바깥 프레임을 자기 것으로 오인해 pop 했고, 이후 바깥의 커밋 실패는 operation 도 조정 키도 없이 보고됐다.
## 관계
- **모르는 것은 성공도 실패도 아닌 세 번째 결과여야 한다**
증거가 사라지면 세 번째 결과를 만들 재료도 사라진다.
- **재시도 단위는 statement가 아니라 유스케이스 전체다**
executor 의 finally 가 프레임을 정리하려 한 이유가 그 재시도 단위에 있다.
- **커밋 증거 단계 — NOT_STARTED에서 UNKNOWN까지**
이 프레임이 담는 단계 모델이다.
## 문제
증거 컨텍스트는 스레드에 묶인 프레임 스택이다. 스택인 이유는 REQUIRES_NEW 가 같은 스레드에서 바깥 트랜잭션을 suspend 하고 안쪽을 시작하기 때문이다. 슬롯이 하나면 안쪽의 커밋이 바깥의 phase 를 덮어쓰고, 나중에 바깥에서 난 커밋 실패가 이미 끝난 작업의 증거로 분류된다.
문제는 그 프레임을 pop 하는 주인이 둘이었다는 점이다.
트랜잭션 매니저 : commit 과 rollback 에서 자기 프레임을 정리
executor 의 finally : operation 과 attempt 가 일치하면 top 프레임을 정리
바깥과 안쪽이 같은 operation 이고 같은 attempt 인 경우, executor 의 조건은 안쪽 프레임만큼이나 바깥 프레임도 정확하게 서술한다. 그리고 기본 경로에서는 둘 다 attempt 1 이므로 이 조건이 항상 성립한다.
순서는 이렇게 된다. 안쪽 매니저가 안쪽 프레임을 pop 하고, executor 가 바깥 프레임을 자기 것으로 오인해 pop 한다. 이후 바깥 트랜잭션의 커밋 실패는 operation 없이, attempt 1 로, 조정 키 없이 보고된다.
## 결론
소유권을 내용이 아니라 깊이로 식별하도록 바꿨다.
scope 가 스택에서의 자기 위치를 들고, 닫을 때 그 위치가 top 일 때만 pop 한다. 매니저는 phase 만 표시하고 아무것도 pop 하지 않는다.
깊이를 기준으로 삼은 이유는 내용이 변하기 때문이다. phase 전이마다 프레임 값이 교체되지만 깊이는 변하지 않고, 엄격하게 중첩된 수명주기에서는 이 scope 가 push 한 깊이의 프레임이 곧 이 scope 의 프레임이다.
순서를 어겨 닫으면 아무것도 pop 하지 않는다. 이것은 의도된 동작이다. 중첩되지 않은 수명주기는 버그이고, 무관한 프레임을 지워서 그것을 덮는 것이 원래 결함이 밖에서 보이던 모습이기 때문이다.
프레임이 남는 것은 프레임이 없는 것보다 나쁘다. 풀링된 요청 스레드가 낡은 COMMITTING 을 무관한 작업으로 들고 가고, 플랫폼은 존재한 적 없는 트랜잭션에 대해 completion-unknown 을 보고하게 된다.
ThreadLocal 을 withInitial 로 만들지 않은 것도 같은 계열의 판단이다. 초기화하는 스레드 로컬은 읽을 때마다 값을 설치하므로, 마지막 프레임을 지운 뒤의 읽기가 clear 가 방금 제거한 것을 다시 등록한다.
## 검증 환경
OpenJDK : 21.0.12
Gradle : 9.0.0
Spring Boot : 4.0.8
근거 : 저장소의 javadoc 이 사후 기록으로 남긴 회귀와 현재 코드 형태
## 재현 조건
수정된 형태를 확인하는 절차는 다음과 같다.
1. TransactionEvidenceContext 의 클래스 javadoc 에서 두 주인 문제의 서술을 읽는다.
2. TransactionEvidenceScope 가 depth 를 들고 ownsTopOf 로 판정하는지 확인한다.
3. close 가 멱등인지 확인한다. 두 번째 close 는 그 사이에 들어온 것을 pop 하지 않고 아무것도 하지 않는다.
4. TransactionEvidenceScopeTest 의 hasRawThreadLocalValue 단언을 확인한다. 프레임이 남지 않았음을 스레드 로컬 수준에서 검사한다.
## 본문
<!-- body:start -->
증거 프레임을 pop하는 주인이 둘이었다 — 트랜잭션 매니저가 commit/rollback에서, executor의 `finally`가 operation·attempt 일치 시.
## 두 주인이 pop 하는 조건
:::evidence key="two-owners-popped-the-evidence-frame" alt="분석 문서 final/document.md 에서 이 기록의 근거 절을 그대로 잘라낸 18줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md 발췌 — 18줄" zoom="true"
:::
## 기본 경로에서는 항상 겹친다
바깥과 같은 operation·같은 attempt를 가진 `REQUIRES_NEW` 안쪽 트랜잭션에서 — 기본 경로에서는 둘 다 attempt 1이라 **항상** 그렇다 — 안쪽 매니저가 안쪽을 pop하고 executor가 바깥을 자기 것으로 오인해 pop했다. 이후 바깥의 커밋 실패는 operation 없이, attempt 1로, reconciliation key 없이 보고됐다.
## 해결
깊이로 소유권을 식별하고 자기 프레임이 top일 때만 pop한다.
## 확인하지 못한 것
없다. 수정된 형태와 그 테스트를 코드로 확인했다.
<!-- body:end -->
@@ -1,99 +0,0 @@
---
kind: CASE
slug: untranslated-contention-bypassed-the-retry-catch
title: 번역되지 않은 경합 예외가 재시도 코디네이터의 catch를 통째로 비껴갔다
topic: commit-ambiguity-as-a-result
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:untranslated-contention-bypassed-the-retry-catch
evidenceCapturedOn: 2026-09-01
body: case-untranslated-contention-bypassed-the-retry-catch.body.md
assets:
- key: untranslated-contention-bypassed-the-retry-catch
file: ../../../final/evidence/rendered/untranslated-contention-bypassed-the-retry-catch.svg
evidence:
- ../../../final/evidence/raw/untranslated-contention-bypassed-the-retry-catch.txt
source:
- 원본 분석 절은 final/document.md#4-1 · analysis/05 §3.6 이다.
---
# 번역되지 않은 경합 예외가 재시도 코디네이터의 catch를 통째로 비껴갔다
재시도 코디네이터가 자기 플랫폼 예외 하나만 catch 하는데 executor 는 아무것도 번역하지 않았다. 경합이 실제로 만들어 내는 예외들이 번역되지 않은 채 나가서 catch 를 비껴갔고, 프로덕션 경합은 재시도되지 않았다.
## 관계
- **실패 번역 사슬의 순서는 계약이다**
이 사례가 그 순서를 계약으로 만든 이유다.
- **모르는 것은 성공도 실패도 아닌 세 번째 결과여야 한다**
번역이 없으면 분류도 없고, 분류가 없으면 세 번째 결과도 만들어지지 않는다.
- **아무도 부르지 않는 재시도 구현**
같은 형태의 공백이 다른 곳에서 나타난 사례다.
## 문제
재시도 코디네이터는 JpaPersistenceException 만 catch 한다. 그런데 executor 는 트랜잭션 템플릿을 돌리면서 아무것도 번역하지 않았다.
경합이 실제로 만들어 내는 실패는 이런 것들이다.
Hibernate 의 OptimisticLockException
Spring 의 OptimisticLockingFailureException
raw 직렬화 실패 또는 데드락 DataAccessException
셋 다 JpaPersistenceException 이 아니다. 번역되지 않은 채 executor 를 떠나면 코디네이터의 catch 에 걸리지 않는다. 그래서 프로덕션에서 경합은 재시도되지 않았다.
단위 픽스처는 초록불이었다. 픽스처가 이미 번역된 예외를 던졌기 때문이다.
## 결론
번역 사슬이 단일 지점으로 만들어졌고, 그 순서가 계약으로 고정됐다.
PersistenceFailureTranslatorChain 의 클래스 javadoc 이 이 회귀를 사후 기록으로 남긴다. 그리고 네 단계 순서를 계약이라고 명시한다.
1단계 : 이미 분류된 실패는 그대로 통과시킨다. 다시 번역하면 그 실패가 이미 들고 있는 attempt 와 key 와 completion 판정을 잃는다.
2단계 : 낙관적 충돌. provider 예외이고 SQLSTATE 가 없으므로 SQLSTATE 기반 번역기가 알아볼 수 없다.
3단계 : 벤더 SQLSTATE. 직렬화 실패와 데드락과 제약 계열을 덮는다.
4단계 : 나머지는 손대지 않고 반환한다. 도메인 예외나 assertion 실패나 NullPointerException 은 퍼시스턴스 실패가 아니며, 그것을 퍼시스턴스 실패로 포장하면 프로그래밍 에러가 재시도 가능한 것처럼 보인다.
모든 번역기는 한 번의 호출에서 같은 operation 과 attempt 와 elapsed 와 trace 값을 받는다. 두 번역기가 같은 시도를 다르게 서술할 수 없게 하기 위해서다.
## 검증 환경
OpenJDK : 21.0.12
Gradle : 9.0.0
Spring Boot : 4.0.8
근거 : 저장소의 javadoc 이 사후 기록으로 남긴 회귀
## 재현 조건
이 회귀 자체는 저장소가 이미 고쳤고, 그 기록이 코드 주석에 남아 있다. 현재 형태를 확인하는 절차는 다음과 같다.
1. PersistenceFailureTranslatorChain 의 클래스 javadoc 을 읽어 네 단계 순서와 그 근거를 확인한다.
2. 사슬 구현이 그 순서대로 실행되는지 확인한다.
3. 재시도 코디네이터가 catch 하는 예외 타입을 확인한다.
## 본문
<!-- body:start -->
재시도 코디네이터가 `JpaPersistenceException`만 catch하는데 executor는 아무것도 번역하지 않고 템플릿을 돌렸다.
## JpaPersistenceException 참조 위치
:::evidence key="untranslated-contention-bypassed-the-retry-catch" alt="코드베이스에서 JpaPersistenceException 를 검색한 출력 37줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="JpaPersistenceException 코드베이스 검색 — 37줄 · exit 0" zoom="true"
:::
## 경합이 실제로 만들어내는 실패 셋
Hibernate `OptimisticLockException`, Spring `OptimisticLockingFailureException`, raw 직렬화/데드락 `DataAccessException` — 이것들이 번역되지 않은 채 executor를 떠나 catch를 완전히 비껴갔고 **production 경합은 재시도되지 않았다.**
## 픽스처가 초록불이던 이유
단위 픽스처는 이미 번역된 예외를 던졌다.
## 확인하지 못한 것
현재 사슬이 네 단계 순서를 지키는지는 코드로 확인했으나, 경합을 실제로 일으켜 재시도가 일어나는 것을 관측하지 않았다.
<!-- body:end -->
@@ -1,148 +0,0 @@
---
kind: CONCEPT
slug: commit-evidence-phases
title: 커밋 증거 단계 — NOT_STARTED에서 UNKNOWN까지
topic: commit-ambiguity-as-a-result
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: concept:commit-evidence-phases
evidenceCapturedOn: 2026-09-01
assets:
- key: commit-evidence-phases
file: ../../../final/evidence/rendered/commit-evidence-phases.svg
- key: commit-evidence-phases-diagram
file: ../../../final/assets/diagrams/commit-evidence-phases.svg
evidence:
- ../../../final/evidence/raw/commit-evidence-phases.txt
source:
- 원본 분석 절은 final/document.md#3-2 · analysis/05 §3.4, §2.3 이다.
---
# 커밋 증거 단계 — NOT_STARTED에서 UNKNOWN까지
트랜잭션이 어디까지 갔는지를 여섯 단계로 기록하고, COMMITTING 단계에서 관측된 실패만 completion-unknown 이 될 수 있게 하는 메커니즘이다.
## 관계
- **커밋 증거 프레임을 두 주인이 pop해서 바깥 트랜잭션의 실패가 익명이 됐다**
이 메커니즘의 저장 구조가 실제로 깨졌던 사례다.
- **pg_terminate_backend가 57P01로 도착하고 커밋 레코드는 이미 WAL에 있었다**
이 단계 모델의 다른 절반인 SQLSTATE 판정이 넓어진 사례다.
- **모르는 것은 성공도 실패도 아닌 세 번째 결과여야 한다**
UNKNOWN 이 실제 상태인 이유를 규칙으로 옮긴 것이다.
- **completion-unknown은 자동으로도 수동으로도 재시도하지 않는다**
이 모델의 UNKNOWN 을 어떻게 다룰지 정한 결정이다.
## 본문
<!-- body:start -->
트랜잭션이 어디까지 갔는지를 여섯 단계로 기록하는 메커니즘의 설명이다. `NOT_STARTED → ACTIVE → COMMITTING → COMMITTED | ROLLED_BACK | UNKNOWN`이고, **`COMMITTING` 단계에서 관측된 실패만** completion-unknown이 될 수 있다는 것이 전체 모델의 핵심이다.
## COMMITTING 에서만 갈리는 세 결과
:::evidence key="commit-evidence-phases-diagram" alt="COMMITTING 에서 COMMITTED 와 ROLLED_BACK 과 UNKNOWN 세 갈래가 나온다" caption="COMMITTING 에서만 갈리는 세 결과" zoom="false"
:::
## EvidenceAwareJpaTransactionManager 참조 위치
:::evidence key="commit-evidence-phases" alt="코드베이스에서 EvidenceAwareJpaTransactionManager 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="EvidenceAwareJpaTransactionManager 코드베이스 검색 — 3줄 · exit 0" zoom="true"
:::
## 표식을 provider commit 직전에 찍는 이유
`mark(COMMITTING)`을 provider commit **직전**에 찍는다. 그 안에서 프로세스가 죽으면 마지막 기록이 "물어봤고 모른다"여야 하기 때문이다.
## 컨텍스트가 단일 슬롯이 아닌 이유
`ArrayDeque` 스택이다 — `REQUIRES_NEW`가 같은 스레드에서 바깥을 suspend한다. `ThreadLocal.withInitial`을 쓰지 않는 이유도 이 개념의 일부다 — 읽을 때마다 값을 설치하면 `clear()`가 방금 제거한 것을 다시 등록한다.
:::note
없음 — 구조와 근거를 코드로 확인했다
:::
## 여섯 단계
```java
public enum TransactionCompletionEvidence {
/** No transaction was begun for this unit of work. */
NOT_STARTED,
/** A transaction is open and statements are executing. */
ACTIVE,
/** The commit has been handed to the provider and no result has come back yet. */
COMMITTING,
/** The provider confirmed the commit. */
COMMITTED,
/** The provider confirmed the rollback. */
ROLLED_BACK,
/** The commit outcome could not be determined; reconciliation owns the resolution. */
UNKNOWN
}
```
전이는 `NOT_STARTED → ACTIVE → COMMITTING → COMMITTED | ROLLED_BACK | UNKNOWN`이다.
이 열거형의 javadoc이 모델의 핵심을 한 문장으로 적는다.
> This is evidence, not a guess. `UNKNOWN` is a real, reportable state: it means the
> driver could neither confirm the commit nor confirm the rollback, and the platform refuses to
> collapse that into either. Only a failure observed while the phase is `COMMITTING` may
> become `TransactionCompletionUnknownException`.
마지막 문장이 판정의 게이트다. SQLSTATE만으로는 부족하고 phase만으로도 부족하며, 둘의 교집합에서만 completion-unknown이 생긴다.
## 왜 프레임워크 없는 코어에 있는가
```java
/**
* <p>The enum lives in the framework-free core rather than in the Spring transaction adapter
* because the design types the exception's evidence field, and the core error contract may not
* depend on the adapter.
*/
```
예외의 증거 필드가 이 타입으로 선언되어 있으므로, 이 타입이 어댑터에 있으면 코어 오류 계약이 어댑터에 의존하게 된다.
## 왜 스택인가
증거는 스레드에 묶인 `ArrayDeque` 스택으로 보관된다. 단일 슬롯이 아닌 이유는 `REQUIRES_NEW`가 같은 스레드에서 바깥 트랜잭션을 suspend하고 안쪽을 시작하기 때문이다.
```java
/**
* <p>The state is a stack rather than a single value because {@code REQUIRES_NEW} suspends an outer
* transaction and begins an inner one on the same thread. With a single slot, the inner
* transaction's commit would overwrite the outer transaction's phase, and a later commit failure on
* the outer one would be classified against evidence that belongs to work that already finished.
*/
```
프레임의 소유권은 스택에서의 깊이로 식별한다. 내용은 phase 전이마다 바뀌지만 깊이는 바뀌지 않는다.
## withInitial을 쓰지 않는 이유
```java
/**
* Plain, not {@code withInitial}. An initialising thread-local installs a value on every read, so
* a read after the last frame was cleared re-registered exactly what {@code clear()} had removed.
*/
private static final ThreadLocal<Deque<TransactionEvidenceFrame>> FRAMES = new ThreadLocal<>();
```
이것도 개념의 일부다. 정리 경로가 아무리 정확해도 읽기 경로가 값을 되살리면 남은 프레임이 생긴다.
:::note
남은 프레임은 프레임이 없는 것보다 나쁘다. 풀링된 요청 스레드가 낡은 COMMITTING을 무관한 작업으로 들고 가고, 플랫폼은 존재한 적 없는 트랜잭션에 대해 completion-unknown을 보고한다.
:::
<!-- body:end -->
@@ -1,69 +1,42 @@
---
id:
kind: CONCEPT
slug: transaction-result-algebra
title: 트랜잭션 결과 대수 — 다섯 변형이 각각 답하는 질문
topic: commit-ambiguity-as-a-result
topicName: 커밋 모호성 — 「모른다」를 결과로 유지하기
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: concept:transaction-result-algebra
evidenceCapturedOn: 2026-09-01
assets:
- key: transaction-result-algebra
file: ../../../final/evidence/rendered/transaction-result-algebra.svg
evidence:
- ../../../final/evidence/raw/transaction-result-algebra.txt
studio: ""
basisVersion: Spring Boot 4.0.8 · Java 21 · 리비전 21234e38
source:
- 원본 분석 절은 final/document.md#3-2 · analysis/03(소유 SSOT — TransactionResult는 application-core에 있다) · analysis/05 §3.2(SpringPolicyTransactionPort 쪽) 이다.
- final/document.md#3-2
- final/document.md#a05
- final/document.md#a05 §3.4
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
assets:
- key: commit-evidence-phase-machine
file: ../../../final/assets/tech-log-studio/commit-evidence-phase-machine.svg
---
# 트랜잭션 결과 대수 — 다섯 변형이 각각 답하는 질문
정책 기반 트랜잭션 결과를 sealed interface 다섯 변형으로 표현한다. 각 변형이 호출자에게 허용하는 행동이 다르고, 그중 둘은 boolean 이나 예외로 표현할 수 없다.
정책 기반 트랜잭션 실행기는 결과를 sealed interface 다섯 변형으로 돌려준다. 변형마다 호출자에게 허용하는 행동이 다르고, 그중 둘은 boolean 이나 예외로 표현되지 않는다. 어느 변형이 나올지는 커밋 증거 단계가 정한다.
## 관계
- **커밋 증거 단계 — NOT_STARTED에서 UNKNOWN까지**
결과를 만들어 내는 증거 모델이다.
- **모르는 것은 성공도 실패도 아닌 세 번째 결과여야 한다**
Indeterminate 변형이 그 규칙의 구현이다.
- **위험한 조합은 정책이 아니라 생성자가 거부하게 만든다**
이 타입의 컴팩트 생성자가 그 규칙의 예다.
- **pg_terminate_backend 가 57P01 로 도착하고 커밋 레코드는 이미 WAL 에 있었다**
대수의 Indeterminate 변형이 실제로 필요해진 사례다.
- **completion-unknown 은 자동으로도 수동으로도 재시도하지 않는다**
Indeterminate 를 받은 호출자가 무엇을 해도 되는지 정한 결정이다.
## 본문
<!-- body:start -->
`TransactionResult<T>`가 sealed interface로 다섯 변형을 갖는 이유의 설명이다.
## 다섯 변형이 각각 답하는 질문
| 변형 | 무엇을 주장하나 |
|---|---|
| `Committed` | 물리 커밋 확인 |
| `Participating` | 바깥 트랜잭션에 참여 — **커밋을 주장하지 않는다** |
| `DeterminateRollback` | 롤백 확인 |
| `Indeterminate` | **replay 권한을 주지 않는다** |
| `CommittedWithPostCommitFailure` | 커밋 후 후처리 실패 |
## 다섯 변형이 sealed 로 묶인 이유
:::evidence key="transaction-result-algebra" alt="분석 문서 final/document.md 에서 이 기록의 근거 절을 그대로 잘라낸 18줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md 발췌 — 18줄" zoom="true"
:::
## boolean이나 예외로 표현할 수 없는 두 상태
각 변형이 caller에게 허용하는 행동이 다르고, 특히 `Participating``Indeterminate`가 그렇다.
## 물리 소유자일 때만 phase를 기록한다
`PhaseSentinel``Ordered.HIGHEST_PRECEDENCE`로 등록된다.
:::note
없음
:::
## 다섯 변형
`TransactionResult<T>` 의 javadoc 두 문장이 이 타입의 전부를 말한다.
```java
/**
@@ -77,7 +50,7 @@ public sealed interface TransactionResult<T> {
TransactionOutcome outcome();
```
두 문장짜리 javadoc이 이 타입의 전부를 말한다. 참여 결과는 커밋을 주장하지 않고, 불확정 결과는 재실행 권한을 주지 않는다.
참여 결과는 커밋을 주장하지 않고, 불확정 결과는 재실행 권한을 주지 않는다. 나머지 셋은 그 두 문장 사이를 채운다.
| 변형 | 답하는 질문 | 호출자가 할 수 있는 것 |
|---|---|---|
@@ -87,9 +60,9 @@ public sealed interface TransactionResult<T> {
| `Indeterminate` | 결과를 알 수 없는가 | 조정으로 넘김. 재실행 금지 |
| `CommittedWithPostCommitFailure` | 커밋은 됐는데 이후가 실패했는가 | 커밋을 되돌리지 않고 운영 실패만 보고 |
## boolean으로 표현할 수 없는 둘
## boolean 으로 접히지 않는 두 변형
`Participating`은 이 호출이 바깥 트랜잭션 안에서 실행됐고 따라서 커밋 여부를 말할 위치에 있지 않다는 뜻이다.
`Participating` 은 이 호출이 바깥 트랜잭션 안에서 실행됐고 커밋 여부를 말할 위치에 있지 않다는 뜻이다.
```java
record Participating<T>(T value) implements TransactionResult<T> {
@@ -101,9 +74,9 @@ record Participating<T>(T value) implements TransactionResult<T> {
}
```
성공으로 접으면 커밋되지 않은 을 커밋으로 보고하고, 실패로 접으면 정상 경로를 실패로 보고다.
성공으로 접으면 커밋되지 않은 작업을 커밋으로 보고하고, 실패로 접으면 정상 경로를 실패로 보고하므로 두 방향 모두 사실과 어긋난다.
`Indeterminate`는 마지막으로 관측된 phase와 조정 참조를 함께 들고 다닌다.
`Indeterminate` 는 마지막으로 관측된 단계와 조정 참조를 함께 들고 다닌다.
```java
record Indeterminate<T>(
@@ -111,31 +84,95 @@ record Indeterminate<T>(
TransactionPhase lastObservedPhase,
Optional<ReconciliationReference> reconciliationReference)
implements TransactionResult<T> {
public Indeterminate {
...
if (operationId.isEmpty() && reconciliationReference.isPresent()) {
throw new IllegalArgumentException("reconciliationReference requires a stable operationId");
}
}
```
컴팩트 생성자의 마지막 검사가 이 개념의 일부다. 조정 참조가 있는데 안정적인 operationId가 없는 만들 수 없다 — 조정할 대상을 지목할 수 없는 조정 참조는 쓸모가 없기 때문이다.
세 성분 가운데 조정 참조는 지금 채워지지 않는다. `SpringPolicyTransactionPort` 가 만드는 `Indeterminate` 는 두 생성 경로에서 모두 `Optional.empty()` 를 넣는다. 타입은 durable reconciliation reference 를 담을 수 있는데 이 어댑터가 그 만들지 않아서, 조정으로 넘어간 쪽이 지목할 수 있는 것은 `operationId`이다.
## 다섯 번째 변형
값이 표현 불가능한 조합을 거부하는 기법은 실패 컨텍스트에 걸려 있다.
```java
record CommittedWithPostCommitFailure<T>(
T value, Optional<OperationId> operationId, RuntimeException operationalFailure)
implements TransactionResult<T> {
// JpaFailureContext
if (completionUnknown && retryable) {
throw new IllegalArgumentException("completion unknown failures are never retryable");
}
```
커밋은 확정됐고 그 이후의 무언가가 실패한 상태다. 이것을 실패로 접으면 호출자가 이미 커밋된 작업을 재시도하고, 성공으로 접으면 운영 실패가 사라진다.
`Indeterminate` 를 만드는 쪽과 그 결과를 재시도 정책에 넘기는 쪽이 서로 다른 타입이어서, 같은 불변식이 양쪽 생성자에 따로 서 있다.
## PhaseSentinel이 하는 일
## 커밋 증거 단계가 변형을 고른다
`SpringPolicyTransactionPort`의 private 중첩 클래스 `PhaseSentinel``TransactionSynchronization`으로 등록된다. 등록은 물리 소유자일 때만 일어나고, 우선순위는 `Ordered.HIGHEST_PRECEDENCE`다.
핵심 실행 루틴은 `COMMIT_REQUESTED` 를 관측한 뒤 provider commit 을 부르고, 돌아온 것을 두 번 물어 네 변형 중 하나를 고른다.
물리 소유자 조건이 중요하다. 참여 트랜잭션이 phase를 기록하면 바깥의 phase를 덮게 되고, 그것이 `Participating`이 커밋을 주장하지 않는다는 규칙을 코드 수준에서 깨는 경로다.
:::evidence key="commit-evidence-phase-machine" alt="COMMIT_REQUESTED 에서 provider commit 을 거쳐 Committed 와 CommittedWithPostCommitFailure 와 DeterminateRollback 과 Indeterminate 네 결과로 갈라지는 흐름도" caption="커밋 요청 뒤의 결과 판정" zoom="false"
:::
`lastObservedPhase` 가 담는 값은 여섯 단계짜리 열거형이다.
```java
public enum TransactionCompletionEvidence {
/** No transaction was begun for this unit of work. */
NOT_STARTED,
/** A transaction is open and statements are executing. */
ACTIVE,
/** The commit has been handed to the provider and no result has come back yet. */
COMMITTING,
/** The provider confirmed the commit. */
COMMITTED,
/** The provider confirmed the rollback. */
ROLLED_BACK,
/** The commit outcome could not be determined; reconciliation owns the resolution. */
UNKNOWN
}
```
전이는 `NOT_STARTED → ACTIVE → COMMITTING → COMMITTED | ROLLED_BACK | UNKNOWN` 이고, 같은 열거형의 javadoc 이 판정의 게이트를 적는다.
> This is evidence, not a guess. `UNKNOWN` is a real, reportable state: it means the
> driver could neither confirm the commit nor confirm the rollback, and the platform refuses to
> collapse that into either. Only a failure observed while the phase is `COMMITTING` may
> become `TransactionCompletionUnknownException`.
마지막 문장이 SQLSTATE 판정과 단계 판정을 잇는다. SQLSTATE 만으로도 단계만으로도 completion-unknown 이 되지 않고, 둘의 교집합에서만 생긴다. 그래서 `mark(COMMITTING)` 은 provider commit 직전에 찍힌다 — 그 안에서 프로세스가 죽으면 마지막 기록이 「물어봤고 모른다」여야 한다.
## 증거를 단일 슬롯이 아니라 스택으로 두는 이유
```java
/**
* <p>The state is a stack rather than a single value because {@code REQUIRES_NEW} suspends an outer
* transaction and begins an inner one on the same thread. With a single slot, the inner
* transaction's commit would overwrite the outer transaction's phase, and a later commit failure on
* the outer one would be classified against evidence that belongs to work that already finished.
*/
```
프레임의 소유권은 스택에서의 깊이로 식별한다. 내용은 단계 전이마다 바뀌지만 깊이는 바뀌지 않는다.
읽기 경로에도 같은 제약이 걸린다.
```java
/**
* Plain, not {@code withInitial}. An initialising thread-local installs a value on every read, so
* a read after the last frame was cleared re-registered exactly what {@code clear()} had removed.
*/
private static final ThreadLocal<Deque<TransactionEvidenceFrame>> FRAMES = new ThreadLocal<>();
```
정리 경로가 아무리 정확해도 읽기 경로가 값을 되살리면 프레임이 남는다. 남은 프레임은 프레임이 없는 것보다 나쁘다. 풀링된 요청 스레드가 낡은 `COMMITTING` 을 무관한 작업으로 들고 가고, 플랫폼은 존재한 적 없는 트랜잭션에 대해 completion-unknown 을 보고한다.
## 단계를 기록하는 쪽을 물리 소유자로 제한한다
`SpringPolicyTransactionPort` 의 private 중첩 클래스 `PhaseSentinel``TransactionSynchronization` 으로 등록된다. 등록은 물리 소유자일 때만 일어나고 우선순위는 `Ordered.HIGHEST_PRECEDENCE` 다.
참여 트랜잭션이 단계를 기록하면 바깥의 단계를 덮어서, `Participating` 이 커밋을 주장하지 않는다는 규칙이 코드 수준에서 깨진다.
## 이 개념이 보장하지 않는 것
다섯 변형은 결과를 무엇으로 부를지 정하고, 그 결과가 실제로 무엇이었는지를 알아내지는 않는다. `Indeterminate` 를 받은 호출자는 커밋 여부를 여전히 모른다. 그 값이 하는 일은 모른다는 사실을 잃지 않고 조정으로 넘기는 것까지다.
<!-- body:end -->
@@ -1,61 +1,58 @@
---
id:
kind: PROJECT_DECISION
slug: completion-unknown-is-never-retried
title: completion-unknown은 자동으로도 수동으로도 재시도하지 않는다
title: completion-unknown 은 자동으로도 수동으로도 재시도하지 않는다
topic: commit-ambiguity-as-a-result
topicName: 커밋 모호성 — 「모른다」를 결과로 유지하기
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: decision:completion-unknown-is-never-retried
studio: ""
decisionStatus: ADOPTED
decidedOn: 2026-08-11
source:
- docs/adr/ADR-JPA-003-completion-unknown.md — Status Accepted, Date 2026-08-11, Design §17
- src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CommitFailureClassifier.java
- src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/TransactionCompletionEvidence.java
- analysis/05-adapter-outbound-persistence-jpa.md
- final/document.md#10-2
- final/document.md#4-1
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
---
# completion-unknown은 자동으로도 수동으로도 재시도하지 않는다
# completion-unknown 은 자동으로도 수동으로도 재시도하지 않는다
## 결정문
커밋 결과를 알 수 없는 실패는 자동으로도 수동으로도 재시도하지 않고 조정으로 넘긴다.
## 판단 이유
ADR 의 근거는 이 실패가 일시적인 것이 아니라 인식론적이라는 관찰이다. 연결이 커밋 도중에 끊기면 서버는 커밋했을 수도 있고 확인 응답만 유실됐을 수도 있으며, 드라이버는 그 둘을 구별하지 못한다.
커밋됐을 수도 있는 쓰기를 재시도하는 것은 이 플랫폼이 할 수 있는 가장 해로운 일이다. 재시도가 성공하면 중복이 생기고, 그 중복은 원래 실패보다 알아채기 어렵다.
그래서 이 예외는 좁은 조건에서만 만들어진다. 관측된 단계가 COMMITTING 이고, 동시에 SQLSTATE 나 예외 타입이 결과를 말해 주지 못하는 경우다.
그리고 이 불변식은 정책이 아니라 타입 수준에서 강제된다. completionUnknown 이면서 retryable 인 실패 컨텍스트는 생성자가 거부하므로 존재할 수 없다. 정책 버그가 그 조합을 만들 수 있는 경로 자체가 없다.
## 영향
감수하는 것
조정 큐가 생기고 그것을 읽는 운영 절차가 필요하다. 자동으로 해소되지 않는 항목이 쌓인다.
조정 큐가 무의미해지지 않도록 모호성 규칙을 좁게 유지해야 한다. 넓히면 평범한 풀 고갈과 서버 재시작이 큐로 밀려들고, 운영자는 큐를 읽지 않고 비우는 습관을 배운다.
실제로는 롤백된 트랜잭션도 조정으로 넘어간다. 드라이버가 말해 주지 않았으므로 구별할 수 없다.
얻는 것
중복 쓰기가 자동 경로에서 발생하지 않는다.
모르는 것이 상태로 남아서 나중에 사람이 판단할 재료가 보존된다.
커밋 결과를 알 수 없는 실패는 조정으로 넘기고 어느 경로로도 재시도하지 않는다. 이 불변식은 정책이 아니라 생성자가 강제한다.
## 근거
- **pg_terminate_backend가 57P01로 도착하고 커밋 레코드는 이미 WAL에 있었다**
- **pg_terminate_backend 가 57P01 로 도착하고 커밋 레코드는 이미 WAL 에 있었다**
이 결정이 다루는 실패가 실제로 어떤 모습인지 보여 주는 사례다.
- **모르는 것은 성공도 실패도 아닌 세 번째 결과여야 한다**
이 결정이 전제하는 상태 어휘다.
- **위험한 조합은 정책이 아니라 생성자가 거부하게 만든다**
이 결정을 타입 수준에서 강제하는 방법이다.
- **커밋 모호성 판정은 넓혀도 좁혀도 해롭다**
이 결정의 조건을 어디에 둘지 정하는 기준이다.
- **트랜잭션 결과 대수 — 다섯 변형이 각각 답하는 질문**
재시도 금지가 어느 변형에 붙는지를 정의한다.
## 결정문
관측된 단계가 COMMITTING 이고 드라이버가 결과를 말해 주지 못한 실패는 completion-unknown 으로 분류하고, 자동 재시도 정책과 수동 재실행 경로 양쪽에서 제외한다.
## 판단 이유
이 실패는 일시적인 것이 아니라 인식론적이다. 연결이 커밋 도중에 끊기면 서버는 커밋했을 수도 있고 확인 응답만 유실됐을 수도 있는데, 드라이버는 그 둘을 구별하지 못한다.
커밋됐을 수도 있는 쓰기를 재실행하는 것은 이 플랫폼이 할 수 있는 가장 해로운 동작이다. 재시도가 성공하면 중복이 생기고, 그 중복은 원래 실패보다 알아채기 어렵다.
그래서 정책이 아니라 타입이 그 조합을 막는다. JpaFailureContext 의 생성자는 completionUnknown 과 retryable 이 동시에 참인 컨텍스트를 IllegalArgumentException 으로 거부한다. 그 검사는 트랜잭션 결과 대수 기록의 생성자 절에 코드로 있다.
리뷰 규칙으로 두면 그 조합을 만드는 경로마다 같은 검사가 필요하다. 생성자가 거부하므로 그 조합을 가진 값은 프로그램 안에 존재하지 않는다.
방어는 두 겹 더 있다. TransactionCompletionUnknownException 의 생성자는 forceCompletionUnknown 으로 컨텍스트를 다시 만들어 재구성 경로가 검사를 우회하지 못하게 한다. RetryProfile 은 COMPLETION_UNKNOWN 을 화이트리스트에 넣는 것을 생성자에서 거부한다.
## 영향
감수하는 것 : 조정 큐가 생기고 그것을 읽는 운영 절차가 필요하다. 자동으로 해소되지 않는 항목이 쌓인다.
감수하는 것 : 실제로는 롤백된 트랜잭션도 조정으로 넘어간다. 드라이버가 말해 주지 않았으므로 구별할 수 없다.
감수하는 것 : 큐가 무의미해지지 않도록 모호성 판정 조건을 좁게 유지해야 한다. 넓히면 평범한 풀 고갈과 서버 재시작이 큐로 밀려들고, 운영자는 큐를 읽지 않고 비우는 습관을 배운다.
얻는 것 : 중복 쓰기가 자동 경로에서 발생하지 않는다.
얻는 것 : 모르는 것이 상태로 남아 나중에 사람이 판단할 재료가 보존된다.
@@ -1,69 +0,0 @@
---
kind: PROJECT_DECISION
slug: retry-unit-is-the-use-case
title: 재시도 단위는 statement가 아니라 유스케이스 전체다
topic: commit-ambiguity-as-a-result
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: decision:retry-unit-is-the-use-case
decisionStatus: ADOPTED
decidedOn: 2026-08-11
source:
- docs/adr/ADR-JPA-002-full-transaction-retry.md — Status Accepted, Date 2026-08-11, Design §19.2
- src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java
- src/application-core/src/main/java/dev/caskeleton/application/transaction/IrreversibleSideEffectContext.java
- analysis/05-adapter-outbound-persistence-jpa.md
---
# 재시도 단위는 statement가 아니라 유스케이스 전체다
## 결정문
재시도는 실패한 statement 나 트랜잭션이 아니라 유스케이스 전체를 새 트랜잭션과 새 Persistence Context 에서 다시 실행한다.
## 판단 이유
statement 수준 재시도는 지금 재시도하려는 실패들에 대해 정확히 틀린 선택이다. 낙관적 충돌은 시도가 계산의 기준으로 삼은 상태가 더 이상 커밋된 상태가 아니라는 뜻이므로, 같은 statement 를 다시 쏘면 이미 움직인 버전에 대해 같은 틀린 답을 계산한다. 도메인 규칙이 다시 읽은 데이터 위에서 다시 돌아야 하고, 그것은 곧 유스케이스 전체다.
Persistence Context 재사용도 같은 이유로 틀리다. 두 번째 시도가 1차 캐시에서 첫 시도의 낡은 엔티티를 읽게 된다.
advice 순서도 결정의 일부다. 재시도 advice 가 Spring 의 트랜잭션 advice 바깥에 놓여야 각 시도가 새 트랜잭션을 시작한다. 순서가 뒤집히면 재시도 루프가 이미 rollback-only 로 표시된 하나의 트랜잭션 안에서 돌고, 두 번째 시도는 아무것도 실행하지 못한 채 즉시 실패한다.
예산은 시도 횟수와 경과 시간 두 상한을 함께 갖는다. 코디네이터가 매 시도마다 경과를 계산해 두 상한을 함께 확인한다.
두 가지 실패는 예산과 무관하게 재시도하지 않는다. 완료를 알 수 없는 실패는 조정으로 가고, 정책이 실패로 분류한 것은 그대로 실패다.
## 영향
감수하는 것
재시도 가능한 유스케이스는 처음부터 다시 실행해도 안전해야 한다. 커밋 전에 되돌릴 수 없는 외부 효과가 있으면 안 된다.
그 조건이 성립하지 않는 유스케이스는 스스로 선언해야 한다. IrreversibleSideEffectContext 가 그 선언을 받고, 정책은 남은 예산과 무관하게 재시도를 거부한다.
재시도 한 번의 비용이 statement 재시도보다 크다. 유스케이스 전체가 다시 돈다.
얻는 것
낙관적 충돌과 직렬화 실패가 실제로 해소된다. 다시 읽은 데이터 위에서 도메인 규칙이 다시 판단하기 때문이다.
두 프로파일이 하나의 재시도를 나눠 결정하는 상황이 없다. 호출자가 넘긴 프로파일 하나가 적격성과 백오프와 예산을 모두 정한다.
2026-09-01 재검증 재검증 결과다.
결정은 채택되어 있고 코디네이터는 조건부 빈으로 생성된다. 그러나 그 빈을 주입받아 호출하는 프로덕션 코드가 없다.
ADR 의 Enforcement 절은 두 가지를 지목한다. 첫 번째인 FullTransactionRetryCoordinatorTest 는 실재하며 코디네이터 자체를 검증한다. 두 번째인 RetryableJpaTransactionInterceptor 는 이 저장소에 존재하지 않고 구현 계획과 코드 리뷰와 이 ADR 세 문서에만 이름으로 남아 있다.
app-bootstrap 과 persistence-jpa 의 main 에는 Advisor 도 MethodInterceptor 도 Pointcut 도 Aspect 도 없다.
따라서 현재 상태는 이렇다. 코디네이터의 동작은 테스트로 고정되어 있고, 그것이 어떤 유스케이스에 적용되는지는 고정되어 있지 않다. 이 절의 근거는 EVD-336 이다.
## 근거
- **번역되지 않은 경합 예외가 재시도 코디네이터의 catch를 통째로 비껴갔다**
이 재시도 경로가 실제로는 돌지 않았던 사례다.
- **모르는 것은 성공도 실패도 아닌 세 번째 결과여야 한다**
예산과 무관하게 재시도하지 않는 두 실패 중 하나가 이 규칙에서 나온다.
@@ -1,66 +0,0 @@
---
kind: PROJECT_DECISION
slug: three-axes-of-evidence
title: 전송·업무·스트림 증거는 세 축이고 서로를 함의하지 않는다
topic: commit-ambiguity-as-a-result
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: decision:three-axes-of-evidence
decisionStatus: ADOPTED
decidedOn: 2026-08-30
source:
- docs/adr/ADR-GRPC-003-three-axis-execution-evidence.md — Status accepted, Date 2026-08-30, Scope grpc-core-api, grpc-policy, grpc-testkit
- analysis/20-grpc-platform.md
- analysis/grpc/grpc-core-api.md
---
# 전송·업무·스트림 증거는 세 축이고 서로를 함의하지 않는다
## 결정문
RPC 에서 일어난 일을 전송 증거와 업무 증거와 스트림 증거 세 축으로 나누어 기록하고, 축 사이의 추론을 거부한다.
## 판단 이유
ADR 의 출발점은 상태 코드가 호출자의 실제 질문에 대한 답이 아니라는 관찰이다.
변경 연산에 대한 DEADLINE_EXCEEDED 는 그 변경이 일어났는지 말해 주지 않는다. 요청이 전송된 뒤의 UNAVAILABLE 은 서버가 그것을 보지 않았다는 뜻이 아니다. 응답 헤더가 도착했다는 것은 트랜잭션이 커밋됐다는 뜻이 아니다.
셋 다 그럴듯한 추론이 중복 쓰기나 손실을 만드는 지점이고, 정상 경로만 검사하는 테스트에서는 어느 것도 보이지 않는다.
그래서 세 축을 독립적으로 두고 각 축이 자기 질문에만 답하게 한다.
전송 증거는 클라이언트가 회선에서 관측한 것을 기록하고, 클라이언트가 자기 전송 실패를 본 NOT_SENT 와 그 외 아무것도 알 수 없는 UNOBSERVED 를 구별한다.
업무 증거는 애플리케이션이 확인한 것을 기록하고, COMMIT_UNKNOWN 을 자리 표시자가 아니라 실제 상태로 둔다.
스트림 증거는 sealed 계층이며, 비어 있지 않은 경우는 전부 위치를 함께 들고 다닌다. 마지막 시퀀스 없는 부분 상태는 재개할 수도 조정할 수도 없기 때문이다.
그리고 관측 불가능한 조합은 실행 증거 타입이 생성 시점에 거부한다. 응답 헤더를 확정 커밋으로 승격하려면 한 메서드를 고쳐야만 가능하다.
## 영향
감수하는 것
값이 세 개로 늘어난다. 호출자가 세 축을 각각 읽어야 하고, 하나만 보고 판단하면 이 결정이 막으려던 추론을 다시 하게 된다.
세 축을 모두 채우는 책임이 전송 계층에 붙는다. 새 전송 구현마다 같은 품질로 채워야 한다.
조합 검증 때문에 표현할 수 없는 값이 생긴다. 테스트 픽스처가 편의상 만들던 조합 중 일부는 더 이상 만들 수 없다.
얻는 것
상태 코드에서 결과를 추론하는 경로가 타입 수준에서 닫힌다.
같은 원칙이 HTTP 쪽 전송 증거와 JPA 쪽 커밋 증거와 한 어휘를 이룬다.
## 근거
- **모르는 것은 성공도 실패도 아닌 세 번째 결과여야 한다**
세 축 각각이 이 규칙을 따른다.
- **위험한 조합은 정책이 아니라 생성자가 거부하게 만든다**
축 사이의 추론을 막는 구현 방법이다.
- **completion-unknown은 자동으로도 수동으로도 재시도하지 않는다**
같은 원칙이 JPA 쪽에서 채택된 결정이다.
@@ -1,68 +0,0 @@
---
kind: QUESTION
slug: commit-ambiguity-lane-not-executed
title: 커밋 모호성 계약 레인이 이 리비전에서 통과하는지 실행으로 확인되지 않았다
topic: commit-ambiguity-as-a-result
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: open-question:commit-ambiguity-lane-not-executed
questionStatus: OPEN
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
---
# 커밋 모호성 계약 레인이 이 리비전에서 통과하는지 실행으로 확인되지 않았다
커밋 모호성 규칙의 현재 형태는 코드로 확인했지만, 그것을 고정하는 계약 레인을 이 리비전에서 실행하지 않았다.
## 사실
CommitFailureClassifier 의 현재 규칙 형태를 코드로 확인했다. 40003 과 08 로 시작하는 상태와 57P01 부터 57P03 까지, 그리고 transport 단절 네 종류다.
CommitAmbiguityContractTest 가 SQLSTATE 를 직접 단언한다.
해당 레인은 Docker 가 없을 때 skip 이 아니라 실패한다.
이 컨테이너에 Docker 가 있다는 것은 이번 사이클에서 확인했다.
## 가정
57P01 관측이 PostgreSQL 의 여러 major 버전에서 동일하게 재현될 것이라고 전제하고 있다. 이 전제는 확인하지 않았다.
## 미지수
이 리비전에서 jpaPlatformFailureTest 가 실제로 통과하는가.
57P01 관측이 PostgreSQL 16 과 17 과 18 전부에서 재현되는가.
## 제약
매트릭스는 major 당 한 번만 선택할 수 있다. 다중 선택은 거부된다.
이 분석은 애플리케이션 소스를 수정하지 않는다.
## 선택지
레인을 major 별로 세 번 실행한다
각 실행에서 SQLSTATE 단언이 통과하는지 확인한다. 비용은 컨테이너 기동 세 번이다.
한 major 에서만 실행한다
가장 빠르지만 버전 간 차이를 확인하지 못한다. 규칙이 특정 major 의 동작에 기대고 있는지 알 수 없다.
## 다음 검증
다음 명령을 실행한다.
./gradlew :adapter:outbound:persistence-jpa:jpaPlatformFailureTest --console=plain
매트릭스 지정은 major 당 한 번씩이다. 예를 들어 -Pjpa.matrix.versions=16 형태로 지정한다.
세 major 에서 통과하면 이 질문을 닫고, 관련 Case 의 확인하지 못한 것 항목을 지운다.
실패하면 그 실패가 새로운 Case 가 된다.
## 관계
- **pg_terminate_backend가 57P01로 도착하고 커밋 레코드는 이미 WAL에 있었다**
이 질문이 검증하려는 규칙을 만든 사례다.
@@ -0,0 +1,74 @@
---
id:
kind: QUESTION
slug: commit-ambiguity-lane-not-rerun-at-head
title: 커밋 모호성 레인이 HEAD 에서도 통과하는지는 실행이 아니라 드리프트 0 으로 답했다
topic: commit-ambiguity-as-a-result
topicName: 커밋 모호성 — 「모른다」를 결과로 유지하기
project: clean-architecture-backend-template
status: 게시 전
studio: ""
questionStatus: OPEN
source:
- final/document.md#6-2
- final/document.md#11
- final/document.md#13
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
evidence:
- ../../../final/evidence/raw/115-integration-lane-original-verification.txt
---
# 커밋 모호성 레인이 HEAD 에서도 통과하는지는 실행이 아니라 드리프트 0 으로 답했다
레인은 실제로 돌았다. 다만 돌린 리비전이 a24ece9c 이고 이 분석이 정본으로 삼은 HEAD 가 아니다. 그 사이를 메우는 것은 재실행이 아니라 변경 파일 수 측정이다.
## 관계
- **pg_terminate_backend 가 57P01 로 도착하고 커밋 레코드는 이미 WAL 에 있었다**
이 레인이 고정하는 규칙을 만든 사례다.
## 사실
다섯 태그 레인을 --rerun-tasks 로 실행해 51 클래스 244 tests 가 0 skipped · 0 failures 로 끝났다. BUILD SUCCESSFUL in 3m 10s 였고 실행 전후의 git status 가 모두 clean 이었다.
그중 jpaPlatformFailureTest 는 2 클래스 6 tests 다. CommitAmbiguityContractTest 가 그 레인에 속한다.
실행 리비전은 a24ece9cf797f7ea647e33bf846b115208ed1ba5 이고 실행 시각은 2026-08-29T14:46:23+00:00 이다.
a24ece9c 와 HEAD 21234e38 사이는 커밋 하나인데, 그 커밋이 건드린 것은 grpc 블록과 공통 파일 둘이다. persistence-jpa 를 포함한 18 개 리프 경로의 변경 파일 수는 0 이다.
## 가정
변경 파일 수가 0 이면 레인 결과도 같으리라고 전제하고 있다. 이 전제는 빌드 설정과 의존성 해석이 두 리비전에서 같다는 것까지 포함한다.
57P01 관측이 PostgreSQL 의 여러 major 버전에서 같게 재현되리라고도 전제하고 있다.
## 미지수
HEAD 에서 jpaPlatformFailureTest 를 돌리면 무엇이 나오는가.
57P01 이 PostgreSQL 16 과 17 과 18 전부에서 같은 SQLSTATE 로 도착하는가.
## 제약
매트릭스는 실행 한 번에 major 하나만 고른다. 다중 선택은 start 단계에서 거부된다.
이 분석은 애플리케이션 소스를 수정하지 않는다.
## 선택지
### 1. HEAD 에서 한 major 로 한 번 돌린다
가장 싸다. 드리프트 0 이라는 간접 근거가 실행 결과로 바뀌는데 버전 간 차이는 여전히 모른다.
### 2. HEAD 에서 major 세 번 돌린다
버전 간 차이까지 확인하는데 비용은 컨테이너 기동 세 번이다.
## 다음 검증
1. HEAD 를 체크아웃하고 Docker 가 있는 환경에서 다음을 돌린다.
./gradlew :adapter:outbound:persistence-jpa:jpaPlatformFailureTest --rerun-tasks --console=plain
2. major 를 바꿔 반복하려면 -Pjpa.matrix.versions 로 하나씩 지정한다.
닫는 조건 : 0 failures 가 나오면 이 질문을 닫고 관련 Case 의 확인하지 못한 것에서 재실행 항목을 지운다. 실패가 나오면 드리프트 0 을 근거로 삼은 §6.2 정정을 철회하고 그 실패를 새 Case 로 연다.
@@ -1,54 +0,0 @@
---
kind: REFERENCE
slug: ambiguity-rule-hurts-both-ways
title: 커밋 모호성 판정은 넓혀도 좁혀도 해롭다
topic: commit-ambiguity-as-a-result
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: reference:ambiguity-rule-hurts-both-ways
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
---
# 커밋 모호성 판정은 넓혀도 좁혀도 해롭다
## 목적
모호성 규칙을 한 방향으로만 조정해서, 반대편 비용을 보지 못한 채 규칙을 옮기는 것을 막는다.
## 규칙
1. 좁게 두면 모호한 실패가 확정 롤백으로 오인된다
그리고 확정 롤백은 재시도된다. 커밋됐을 수도 있는 쓰기가 다시 실행되는 경로가 여기서 열린다.
2. 넓게 두면 조정 큐가 무의미해진다
평범한 풀 고갈과 서버 재시작이 조정 큐로 밀려들면 운영자는 그 큐를 읽지 않고 비우는 습관을 배운다. 정작 중요한 항목 하나가 나머지와 함께 지워진다.
3. 기준은 두 조건의 교집합이다
이 SQLSTATE 가 커밋 단계에서 발생했는가, 그리고 드라이버가 어느 쪽인지 말해 주지 못하는가. 둘 다 참일 때만 모호성이다.
4. 판정 재료는 두 가지이며 어느 하나로는 부족하다
관측된 트랜잭션 단계와 SQLSTATE 또는 예외 타입이다.
5. 넓힌 규칙은 테스트로 고정한다
상태 목록을 조용히 줄이는 경로를 닫아야 한다. SQLSTATE 를 직접 단언하는 계약 테스트가 그 역할을 한다.
## 적용 조건
커밋 실패 분류를 갖는 모든 데이터 계층
## 예외
연결 유실이 커밋을 불명으로 남겼는지는 단계에 달렸다. 따라서 08 로 시작하는 상태를 그 자체로 completion-unknown 으로 두면 안 된다. 벤더 분류기는 그것을 연결 불가로만 두고, 단계를 아는 커밋 분류기가 따로 판정한다.
## 예시
57P01 은 class 08 이 아니다. 클라이언트의 연결 시도가 아니라 서버가 자기 종료를 알린 것이기 때문이다. 그러나 커밋 입장에서는 결과가 같고 더 나쁘다. 그 상태가 도착할 때 커밋 레코드가 이미 WAL 에 있을 수 있다.
## 관계
- **pg_terminate_backend가 57P01로 도착하고 커밋 레코드는 이미 WAL에 있었다**
이 규칙이 넓혀진 사례다.
- **completion-unknown은 자동으로도 수동으로도 재시도하지 않는다**
이 판정이 만들어 내는 예외를 다루는 결정이다.
@@ -1,57 +0,0 @@
---
kind: REFERENCE
slug: make-the-unsafe-state-unrepresentable
title: 위험한 조합은 정책이 아니라 생성자가 거부하게 만든다
topic: commit-ambiguity-as-a-result
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: reference:make-the-unsafe-state-unrepresentable
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
---
# 위험한 조합은 정책이 아니라 생성자가 거부하게 만든다
## 목적
이 조합은 하면 안 된다는 규칙을 리뷰나 정책 코드에 두어, 정책 버그가 그 조합을 만들 수 있게 남겨 두는 것을 막는다.
## 규칙
1. 값이 존재할 수 없게 만든다
생성자가 거부하면 그 조합을 가진 값이 프로그램 안에 존재하지 않는다. 정책이 거부하면 정책을 지나치는 경로마다 같은 검사가 필요하다.
2. 검사는 값 타입 안에 둔다
두 필드의 조합이 의미상 불가능한 값 타입이 대상이다. 컴팩트 생성자 한 줄이면 되고 비용이 거의 없다.
3. 예외를 재구성할 때도 같은 팩토리를 지난다
컨텍스트를 다시 만드는 경로가 검사를 우회하면 방어가 반쪽이 된다. 안전 팩토리를 통해서만 재구성하게 해서 이중으로 막는다.
4. 조합이 다른 객체의 상태에 의존하면 자리가 다르다
그때는 생성자가 아니라 조립 지점의 시작 검증기가 맡는다.
## 적용 조건
두 필드의 조합이 의미상 불가능한 모든 값 타입
이 저장소의 예 : JpaFailureContext 가 completionUnknown 과 retryable 의 동시 참을 거부한다. PublishEvidence 가 두 조합을 거부한다. GrpcExecutionEvidence 가 스트림 증거를 가진 unary 호출을 거부한다. TransactionResult.Indeterminate 가 operationId 없는 조정 참조를 거부한다.
## 예외
유효성이 다른 객체의 상태나 실행 시점의 설정에 의존하면 생성자 검증으로 표현되지 않는다. 조립 지점의 시작 검증기가 그 자리다.
## 예시
completionUnknown 이면서 retryable 인 실패 컨텍스트는 만들 수 없다. 그 조합이 존재하면 재시도 정책이 커밋됐을 수도 있는 쓰기를 재시도한다.
조정 참조가 있는데 안정적인 operationId 가 없는 불확정 결과는 만들 수 없다. 조정할 대상을 지목할 수 없는 조정 참조이기 때문이다.
## 관계
- **모르는 것은 성공도 실패도 아닌 세 번째 결과여야 한다**
이 규칙이 지키려는 상태 어휘를 정의한다.
- **completion-unknown은 자동으로도 수동으로도 재시도하지 않는다**
이 규칙으로 타입 수준에서 강제되는 결정이다.
- **전송·업무·스트림 증거는 세 축이고 서로를 함의하지 않는다**
같은 규칙이 gRPC 쪽에서 적용된 결정이다.
@@ -1,58 +0,0 @@
---
kind: REFERENCE
slug: translation-chain-order-is-a-contract
title: 실패 번역 사슬의 순서는 계약이다
topic: commit-ambiguity-as-a-result
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: reference:translation-chain-order-is-a-contract
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
---
# 실패 번역 사슬의 순서는 계약이다
## 목적
번역 사슬의 단계를 재배치하거나 건너뛰어서, 이미 분류된 판정을 잃거나 프로그래밍 에러를 재시도 가능하게 만드는 것을 막는다.
## 규칙
1. 이미 분류된 실패는 그대로 통과시킨다
다시 번역하면 그 실패가 들고 있던 시도 번호와 키와 완료 판정을 새로 만들면서 잃는다.
2. SQLSTATE 없는 provider 예외를 그다음에 둔다
낙관적 충돌이 대표적이다. SQLSTATE 가 없으므로 SQLSTATE 기반 번역기가 알아보지 못하고, 뒤에 두면 마지막 단계로 떨어진다.
3. 벤더 SQLSTATE 를 그다음에 둔다
직렬화 실패와 데드락과 제약 계열을 덮는다.
4. 나머지는 손대지 않고 반환한다
도메인 예외나 assertion 실패나 NullPointerException 은 퍼시스턴스 실패가 아니다. 그것을 퍼시스턴스 실패로 포장하면 프로그래밍 에러가 재시도 가능한 것처럼 보인다.
5. 모든 번역기가 같은 시도 값을 받는다
operation 과 attempt 와 elapsed 와 trace 를 호출 지점에서 한 번 만들어 넘긴다. 두 번역기가 같은 시도를 다르게 서술할 수 없게 된다.
## 적용 조건
예외를 계층 간에 옮기는 모든 어댑터
번역기 없이 실행되는 경로가 있는지 확인해야 하는 조립 지점
## 예외
정책의 화이트리스트는 어떤 범주가 재시도될 수 있는지를 넓힌다. 이 실패에 대한 분류기의 판정을 뒤집지 않는다.
## 예시
재시도 코디네이터가 플랫폼 예외 하나만 잡는데 executor 가 아무것도 번역하지 않으면, 경합이 실제로 만들어 내는 예외들이 번역되지 않은 채 나가서 catch 를 비껴간다. 단위 픽스처가 이미 번역된 예외를 던지면 그 공백이 초록불로 덮인다.
벤더 번역기가 없는 구성에서도 사슬은 존재하고 벤더 단계만 아무것도 돌려주지 않는다. 사슬 자체를 건너뛰는 경로를 두지 않는다.
## 관계
- **번역되지 않은 경합 예외가 재시도 코디네이터의 catch를 통째로 비껴갔다**
이 순서를 계약으로 만든 사례다.
- **모르는 것은 성공도 실패도 아닌 세 번째 결과여야 한다**
번역이 없으면 분류도 없고, 분류가 없으면 세 번째 결과도 만들어지지 않는다.
@@ -1,57 +1,70 @@
---
id:
kind: REFERENCE
slug: unknown-is-a-third-result
title: 모르는 것은 성공도 실패도 아닌 세 번째 결과여야 한다
topic: commit-ambiguity-as-a-result
topicName: 커밋 모호성 — 「모른다」를 결과로 유지하기
project: clean-architecture-backend-template
status: 게시 전
studio: ""
source:
- final/document.md#9
- final/document.md#3-3
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: reference:unknown-is-a-third-result
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
---
# 모르는 것은 성공도 실패도 아닌 세 번째 결과여야 한다
## 목적
완료 여부를 알 수 없는 상태를 성공이나 실패로 접어서 그 정보가 영원히 사라지는 것을 막는다.
## 규칙
1. 결과 타입에 세 번째 변형이 있는가를 묻는다
호출자가 재시도와 조정을 구별하려면 그 구별을 담을 값이 필요하다. boolean 이나 예외 하나로는 표현되지 않는다.
2. 접으면 정보가 복구되지 않는다
모르는 것을 실패로 접으면 재시도가 일어나고 커밋됐을 수도 있는 쓰기가 중복된다. 성공으로 접으면 확인되지 않은 작업이 확인된 것으로 하류에 흘러간다. 어느 쪽도 나중에 되돌릴 수 없다.
3. 세 번째 변형은 조정 정보를 함께 들고 다닌다
상태 이름만으로는 부족하다. 마지막으로 관측된 단계와 조정에 쓸 안정적인 식별자가 그 값 안에 있어야 조정이 대상을 지목할 수 있다.
4. 남발하지 않는다
조정 큐가 커지면 운영자가 읽지 않고 비우는 습관을 배우고, 정작 중요한 항목이 나머지와 함께 지워진다.
## 적용 조건
커밋과 발행과 전달처럼 관측이 결과를 확정하지 못할 수 있는 모든 경계
이 저장소의 구현 예 : RetryDisposition.RECONCILE, TransactionResult.Indeterminate, WriteDisposition.UNDETERMINED, PublishCompletion.AMBIGUOUS, GrpcBusinessEvidence.COMMIT_UNKNOWN, ReplicaLagMonitor.replayedThrough 의 Optional 반환
## 예외
아무것도 프로세스를 떠나지 않은 실패는 확정적이다. 세 번째 변형이 아니라 확정 실패다.
## 예시
change stream 파이프라인에는 본 적 있지만 완료되지 않은 위치라는 상태가 없었다. 그래서 재전달된 이벤트가 이미 처리된 것과 같은 값으로 다뤄져 영구히 사라졌다.
커밋 증거 열거형은 UNKNOWN 을 실제 상태로 두고, 드라이버가 커밋도 롤백도 확인해 주지 못한 경우를 어느 쪽으로도 접지 않는다.
완료 여부를 알 수 없는 상태를 성공이나 실패로 접으면 그 정보가 영원히 사라진다. 결과 타입에 세 번째 변형을 두고, 그 변형이 조정에 필요한 것을 함께 들고 다니게 한다.
## 관계
- **pg_terminate_backend가 57P01로 도착하고 커밋 레코드는 이미 WAL에 있었다**
- **pg_terminate_backend 가 57P01 로 도착하고 커밋 레코드는 이미 WAL 에 있었다**
세 번째 결과가 필요한 대표 사례다.
- **high-water mark가 본 위치를 뜻해서 재전달된 변경이 영구히 사라졌다**
세 번째 결과가 없어서 손실이 난 사례다.
- **completion-unknown은 자동으로도 수동으로도 재시도하지 않는다**
- **completion-unknown 은 자동으로도 수동으로도 재시도하지 않는다**
이 규칙을 정책으로 옮긴 결정이다.
## 목적
모르는 것을 실패로 접으면 재시도가 일어나고, 커밋됐을 수도 있는 쓰기가 중복된다. 성공으로 접으면 확인되지 않은 작업이 확인된 것으로 하류에 흘러간다. 어느 쪽도 나중에 되돌릴 수 없다.
## 규칙
### 1. 결과 타입에 세 번째 변형을 둔다
호출자가 재시도와 조정을 구별하려면 그 구별을 담을 값이 필요하다. boolean 하나나 예외 하나로는 표현되지 않는다.
### 2. 세 번째 변형은 조정 정보를 함께 들고 다닌다
상태 이름만으로는 부족하다. 마지막으로 관측된 단계와 조정에 쓸 안정적인 식별자가 값 안에 있어야 조정이 대상을 지목한다.
### 3. 판정 조건을 좁게 유지한다
조정 큐가 커지면 운영자가 읽지 않고 비우는 습관을 배우고, 중요했던 한 건이 나머지와 같이 지워진다.
## 적용 조건
관측이 결과를 확정하지 못할 수 있는 경계 : 커밋, 발행, 전달, 복제 지연 조회
이 저장소의 구현은 다섯이다.
재시도 처분 : RetryDisposition.RECONCILE
트랜잭션 결과 : TransactionResult.Indeterminate
쓰기 처분 : WriteDisposition.UNDETERMINED
발행 완료 : PublishCompletion.AMBIGUOUS
복제 지연 : ReplicaLagMonitor.replayedThrough 의 Optional 반환
## 예외
아무것도 프로세스를 떠나지 않은 실패는 확정이다. 발행 경로에서 access 가 거부되거나 encode 가 실패하면 결과는 REJECTED 이고 ambiguous 가 아니다.
어떤 브로커도 보지 못한 메시지를 모호하다고 보고하면, 호출자는 확인할 것이 없는 조정으로 보내진다.
준비 중에 데드라인이 소진된 경우도 같다. 아직 전송 전이므로 확정 거부다.
## 예시
커밋 증거 열거형은 UNKNOWN 을 실제 상태로 두고, 드라이버가 커밋도 롤백도 확인해 주지 못한 경우를 어느 쪽으로도 접지 않는다.
발행 경로는 transport 단계에서 실패하거나 데드라인이 지나면 AMBIGUOUS 로 보고한다. 요청이 wire 위에 있었으므로 브로커가 들고 있을 수 있기 때문이다.
@@ -14,7 +14,7 @@ assets:
evidence:
- ../../../final/evidence/raw/adapter-inbound-graphql-c03.txt
source:
- 원본 분석 절은 analysis/16-adapter-inbound-graphql.md#L336 이다.
- 원본 분석 절은 final/document.md#a16#L336 이다.
module: adapter-inbound-graphql
---
@@ -14,7 +14,7 @@ assets:
evidence:
- ../../../final/evidence/raw/adapter-inbound-graphql-c06.txt
source:
- 원본 분석 절은 analysis/16-adapter-inbound-graphql.md#L471 이다.
- 원본 분석 절은 final/document.md#a16#L471 이다.
module: adapter-inbound-graphql
---
@@ -16,7 +16,7 @@ assets:
evidence:
- ../../../final/evidence/raw/adapter-inbound-graphql-c09.txt
source:
- 원본 분석 절은 analysis/16-adapter-inbound-graphql.md#L660 이다.
- 원본 분석 절은 final/document.md#a16#L660 이다.
module: adapter-inbound-graphql
---
@@ -14,7 +14,7 @@ assets:
evidence:
- ../../../final/evidence/raw/adapter-inbound-web-c02.txt
source:
- 원본 분석 절은 analysis/14-adapter-inbound-web.md#L140 이다.
- 원본 분석 절은 final/document.md#a14#L140 이다.
module: adapter-inbound-web
---
@@ -14,7 +14,7 @@ assets:
evidence:
- ../../../final/evidence/raw/adapter-inbound-web-c12.txt
source:
- 원본 분석 절은 analysis/14-adapter-inbound-web.md#L986 이다.
- 원본 분석 절은 final/document.md#a14#L986 이다.
module: adapter-inbound-web
---
@@ -14,7 +14,7 @@ assets:
evidence:
- ../../../final/evidence/raw/adapter-inbound-web-c13.txt
source:
- 원본 분석 절은 analysis/14-adapter-inbound-web.md#L1071 이다.
- 원본 분석 절은 final/document.md#a14#L1071 이다.
module: adapter-inbound-web
---
@@ -16,7 +16,7 @@ assets:
evidence:
- ../../../final/evidence/raw/adapter-inbound-websocket-c01.txt
source:
- 원본 분석 절은 analysis/17-adapter-inbound-websocket.md#L24 이다.
- 원본 분석 절은 final/document.md#a17#L24 이다.
module: adapter-inbound-websocket
---
@@ -49,7 +49,7 @@ advanced/stomp/rabbit/RabbitBrokerRelayConfiguration.java prefix "app.websocket
## 분석 원문의 접두사 표
:::evidence key="adapter-inbound-websocket-c01" alt="분석 문서 analysis/17-adapter-inbound-websocket.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/17-adapter-inbound-websocket.md 발췌 — 15줄" zoom="true"
:::evidence key="adapter-inbound-websocket-c01" alt="분석 문서 final/document.md#a17 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a17 발췌 — 15줄" zoom="true"
:::
<!-- body:end -->
@@ -16,7 +16,7 @@ assets:
evidence:
- ../../../final/evidence/raw/adapter-outbound-cache-redis-c01.txt
source:
- 원본 분석 절은 analysis/10-adapter-outbound-cache-redis.md#L85 이다.
- 원본 분석 절은 final/document.md#a10#L85 이다.
module: adapter-outbound-cache-redis
---
@@ -14,7 +14,7 @@ assets:
evidence:
- ../../../final/evidence/raw/adapter-outbound-cache-redis-c02.txt
source:
- 원본 분석 절은 analysis/10-adapter-outbound-cache-redis.md#L107 이다.
- 원본 분석 절은 final/document.md#a10#L107 이다.
module: adapter-outbound-cache-redis
---
@@ -38,7 +38,7 @@ test `enabledRejectsAMissingRawAllowlistResource`와 `enabledAcceptsAReadableRaw
## 분석 원문의 확인 절차
:::evidence key="adapter-outbound-cache-redis-c02" alt="분석 문서 analysis/10-adapter-outbound-cache-redis.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/10-adapter-outbound-cache-redis.md 발췌 — 15줄" zoom="true"
:::evidence key="adapter-outbound-cache-redis-c02" alt="분석 문서 final/document.md#a10 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a10 발췌 — 15줄" zoom="true"
:::
<!-- body:end -->

Some files were not shown because too many files have changed in this diff Show More