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:
co-authored by
Claude Fable 5.1
parent
9d2a3725c5
commit
b25357c48a
@@ -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 '`'
|
||||
```
|
||||
-12
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
-12
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
-610
@@ -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자임; 사건/메커니즘 이름으로 줄일 수 있는지 검토"
|
||||
}
|
||||
]
|
||||
}
|
||||
-184
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-75
@@ -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**
|
||||
-158
@@ -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` 절에 적혀 있다.
|
||||
-1607
File diff suppressed because it is too large
Load Diff
-319
@@ -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 의 판정대로 유지한다.
|
||||
-157
@@ -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
-65
@@ -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` 편집 패스이며, 체크리스트가 별도 패스로 규정한 것이다.
|
||||
-56
@@ -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개 리프에 대해서는 재분석 완료 후 재확인이 필요하다.
|
||||
+1
-1
@@ -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
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
---
|
||||
|
||||
|
||||
+2
-2
@@ -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 -->
|
||||
|
||||
+1
-1
@@ -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
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
---
|
||||
|
||||
|
||||
+2
-2
@@ -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 방지의 완결이 아니라고 적는다
|
||||
|
||||
+1
-1
@@ -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
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -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를 요구하고, 실제로 조립되는 생산자에는 그 설정이 없다
|
||||
|
||||
+1
-1
@@ -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 로 흡수된다.
|
||||
- 설정 빈이 속성을 받고도 거짓을 보고한다는 것, 그 두 경우의 빈 집합이 같다는 것, 검사 빈 자체에 조건이 있다는 것, 그리고 런북 전체에 단독 서버·오플로그·해당 오류 코드가 없다는 것은 이 기록에서 확인했다.
|
||||
|
||||
+2
-2
@@ -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"
|
||||
:::
|
||||
|
||||
## 리포트와 컨텍스트가 어긋났다
|
||||
|
||||
+2
-2
@@ -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"
|
||||
:::
|
||||
|
||||
## 여덟 빈이 조용히 사라졌다
|
||||
|
||||
+1
-1
@@ -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 이다.
|
||||
---
|
||||
|
||||
# 넓은 스캔을 좁히자 여덟 컴포넌트에 아무것도 도달하지 않았다
|
||||
|
||||
+1
-1
@@ -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인자 생성자로 되돌렸다
|
||||
|
||||
+1
-1
@@ -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 플랫폼 쪽이 아니다
|
||||
|
||||
+1
-1
@@ -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 이다.
|
||||
---
|
||||
|
||||
# 스캔에서 뺀 다섯 패키지의 컴포넌트 여섯을 두 자동설정 어느 쪽도 소유하지 않았다
|
||||
|
||||
+1
-1
@@ -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개 규칙이 유일한 조립 지점에서 호출되지 않는다
|
||||
|
||||
+1
-1
@@ -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 조립의 세 경로와 각각이 결정하는 것
|
||||
|
||||
+2
-2
@@ -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"
|
||||
:::
|
||||
|
||||
## 이 저장소가 그 함정을 밟았다
|
||||
|
||||
+2
-2
@@ -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
|
||||
---
|
||||
|
||||
# 마스터 스위치는 루트 하나가 소유하고 자식 설정은 조건을 갖지 않는다
|
||||
|
||||
+2
-2
@@ -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가 있나"로 묻는다
|
||||
|
||||
+2
-2
@@ -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"
|
||||
:::
|
||||
|
||||
## 각 순서가 막는 것
|
||||
|
||||
+1
-1
@@ -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가 로그에 남았다
|
||||
|
||||
+1
-1
@@ -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 이다.
|
||||
---
|
||||
|
||||
# 카디널리티 경계를 타입으로 표현하기
|
||||
|
||||
+1
-1
@@ -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 이다.
|
||||
---
|
||||
|
||||
# 서명된 커서의 구조와 검증 순서
|
||||
|
||||
+1
-1
@@ -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
|
||||
---
|
||||
|
||||
# 커서에 서명하는 이유는 기밀성이 아니라 무결성이다
|
||||
|
||||
+2
-2
@@ -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는 메트릭 태그가 되지 않는다
|
||||
|
||||
+1
-1
@@ -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` 이 조건부라는 것, 그리고 주석의 주어절은 맞고 괄호만 틀렸다는 것은 이 기록에서 확인했다.
|
||||
---
|
||||
|
||||
+1
-1
@@ -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 바이트가 답이다. 셋을 최대로 채워도 상수 상한을 넘지 않는다. 넘는 경로는 슬롯 태그 쪽이고, 그것도 채널이 아니라 키에서 일어난다.
|
||||
---
|
||||
|
||||
+1
-1
@@ -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 명령 전부에서 관문의 요청 바이트 검사가 발화하지 못한다.
|
||||
---
|
||||
|
||||
+1
-1
@@ -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 가 초록인 것은 셋이다.
|
||||
---
|
||||
|
||||
|
||||
+2
-2
@@ -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"
|
||||
:::
|
||||
|
||||
## 데이터 위험은 없다
|
||||
|
||||
+1
-1
@@ -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 승인만 호출자가 아니라 배포에 대해 이루어진다
|
||||
|
||||
+1
-1
@@ -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
+2
-2
@@ -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 -->
|
||||
|
||||
+1
-1
@@ -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
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -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 의 두 주장과 실제 호출 시점·예외 처리의 대조, 그리고 두 시험이 각각 절반만 덮는다는 것은 이 기록에서 확인했다.
|
||||
---
|
||||
|
||||
|
||||
+2
-2
@@ -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"
|
||||
:::
|
||||
|
||||
## 틀린 문단이 권위로 지목하는 문서는 이미 정정을 마쳤다
|
||||
|
||||
+1
-1
@@ -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 의 의미와 어긋난다
|
||||
|
||||
+1
-1
@@ -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 이다.
|
||||
---
|
||||
|
||||
# 능력 선언의 세 출처와 그것이 파생되지 않을 때
|
||||
|
||||
+72
-42
@@ -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 -->
|
||||
|
||||
-111
@@ -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 -->
|
||||
-100
@@ -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 -->
|
||||
-99
@@ -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 -->
|
||||
-148
@@ -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 -->
|
||||
+103
-66
@@ -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 -->
|
||||
|
||||
+39
-42
@@ -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 을 화이트리스트에 넣는 것을 생성자에서 거부한다.
|
||||
|
||||
## 영향
|
||||
|
||||
감수하는 것 : 조정 큐가 생기고 그것을 읽는 운영 절차가 필요하다. 자동으로 해소되지 않는 항목이 쌓인다.
|
||||
|
||||
감수하는 것 : 실제로는 롤백된 트랜잭션도 조정으로 넘어간다. 드라이버가 말해 주지 않았으므로 구별할 수 없다.
|
||||
|
||||
감수하는 것 : 큐가 무의미해지지 않도록 모호성 판정 조건을 좁게 유지해야 한다. 넓히면 평범한 풀 고갈과 서버 재시작이 큐로 밀려들고, 운영자는 큐를 읽지 않고 비우는 습관을 배운다.
|
||||
|
||||
얻는 것 : 중복 쓰기가 자동 경로에서 발생하지 않는다.
|
||||
|
||||
얻는 것 : 모르는 것이 상태로 남아 나중에 사람이 판단할 재료가 보존된다.
|
||||
|
||||
-69
@@ -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를 통째로 비껴갔다**
|
||||
이 재시도 경로가 실제로는 돌지 않았던 사례다.
|
||||
- **모르는 것은 성공도 실패도 아닌 세 번째 결과여야 한다**
|
||||
예산과 무관하게 재시도하지 않는 두 실패 중 하나가 이 규칙에서 나온다.
|
||||
|
||||
-66
@@ -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 쪽에서 채택된 결정이다.
|
||||
|
||||
-68
@@ -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에 있었다**
|
||||
이 질문이 검증하려는 규칙을 만든 사례다.
|
||||
|
||||
+74
@@ -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 로 연다.
|
||||
-54
@@ -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은 자동으로도 수동으로도 재시도하지 않는다**
|
||||
이 판정이 만들어 내는 예외를 다루는 결정이다.
|
||||
|
||||
-57
@@ -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 쪽에서 적용된 결정이다.
|
||||
|
||||
-58
@@ -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를 통째로 비껴갔다**
|
||||
이 순서를 계약으로 만든 사례다.
|
||||
- **모르는 것은 성공도 실패도 아닌 세 번째 결과여야 한다**
|
||||
번역이 없으면 분류도 없고, 분류가 없으면 세 번째 결과도 만들어지지 않는다.
|
||||
|
||||
+52
-39
@@ -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 위에 있었으므로 브로커가 들고 있을 수 있기 때문이다.
|
||||
|
||||
+1
-1
@@ -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
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
---
|
||||
|
||||
|
||||
+2
-2
@@ -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 -->
|
||||
|
||||
+1
-1
@@ -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
|
||||
---
|
||||
|
||||
|
||||
+2
-2
@@ -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
Reference in New Issue
Block a user