fix: 하네스 제거 및 keycloak 문서 보강
This commit is contained in:
@@ -1 +0,0 @@
|
||||
../../../vault/90-archive/archive/branch-notes/feature-template-instantiation-contract.md
|
||||
@@ -0,0 +1,217 @@
|
||||
---
|
||||
title: branch / feature-template-instantiation-contract
|
||||
source_type: branch-note
|
||||
status: raw
|
||||
branch: feature-template-instantiation-contract
|
||||
parent_branch:
|
||||
related_projects: []
|
||||
tags: [branch]
|
||||
created: 2026-06-15
|
||||
target_merge:
|
||||
status_label: abandoned
|
||||
archive_reason: uninstantiated-template-scaffold
|
||||
---
|
||||
|
||||
# branch: feature-template-instantiation-contract
|
||||
|
||||
> Layer: `raw/branch-notes/` — 단일 브랜치의 **TODO·결정·진행 기록**. 머지/종료 후 verified 결과는 `/ingest`로 `wiki/projects/`에 추출. 원본은 raw에 영구 보관.
|
||||
> `status_label`: `in-progress` | `review` | `merged` | `abandoned`
|
||||
> **계층 표기**: "root branch" 라는 별도 개념은 없음. project 의 직접 자식 branch 는 `parent_branch:` 를 **비워두고** `related_projects` 만 채움. 다른 branch 의 자식이면 `parent_branch: <부모 branch 이름>` 명시 + `## Parent` 섹션의 부모 wikilink 필수.
|
||||
|
||||
## Parent / 부모 (필수)
|
||||
|
||||
> 이 branch 가 어느 작업 묶음에 속하는지. 모든 branch 는 예외 없이 upward link 보유.
|
||||
|
||||
다음 중 정확히 하나:
|
||||
|
||||
- **Project 의 직접 자식 branch** (`parent_branch:` 비어있음): `[[raw/project-notes/{{project-name}}]]` 만 명시
|
||||
- **다른 branch 의 자식** (`parent_branch:` 채워짐): `[[raw/branch-notes/{{parent-branch}}]]` 명시 + `frontmatter.parent_branch` 와 일치
|
||||
|
||||
선택 (있을 때):
|
||||
|
||||
- 형제 branch (같은 부모의 다른 자식):
|
||||
- `[[raw/branch-notes/{{sibling-1}}]]`
|
||||
- `[[raw/branch-notes/{{sibling-2}}]]`
|
||||
|
||||
## 목표 / WHY
|
||||
|
||||
이 브랜치에서 해결하려는 문제. 관련 이슈 / PR 링크.
|
||||
|
||||
- 이슈:
|
||||
- PR:
|
||||
|
||||
## 범위
|
||||
|
||||
### In scope
|
||||
|
||||
- 항목 1
|
||||
- 항목 2
|
||||
|
||||
### Out of scope
|
||||
|
||||
> 의도적으로 제외한 것. 면접 등에서 "이건 범위에 없었습니다"라고 답할 근거.
|
||||
|
||||
- 항목 1
|
||||
|
||||
## Sources / 근거 (필수, 최소 1개+)
|
||||
|
||||
> 이 branch의 구현·설계 결정의 **근거가 되는 외부 자료**. 공식 문서·대기업 기술 블로그·강의 등 raw 자료를 인용. 같은 자료가 여러 결정의 근거면 결정 표시와 함께 여러 번 등장 가능.
|
||||
|
||||
| Source | 정당화하는 결정 |
|
||||
|---|---|
|
||||
| `[[raw/official-docs/<...>]]` | <어떤 결정의 근거인지 한 줄> |
|
||||
| `[[raw/company-tech-blogs/<...>]]` | <한 줄> |
|
||||
| `[[raw/lectures/<...>]]` | <한 줄> |
|
||||
|
||||
근거 자료가 raw에 아직 없다면 먼저 `raw-source-template` 또는 `lecture-note-template` 으로 raw에 등록한 뒤 여기서 링크.
|
||||
|
||||
## TODO
|
||||
|
||||
각 항목 옆에 증거 등급 표기: `actually-implemented` | `locally-verified` | `prod-verified` | `documented-only` | `planned` | `needs-confirmation`
|
||||
|
||||
- [ ] 작업 1 — 등급: `planned`
|
||||
- [ ] 작업 2 — 등급: `planned`
|
||||
- [x] 작업 3 — 등급: `actually-implemented`
|
||||
|
||||
## 진행 중 메모
|
||||
|
||||
작업하며 떠오른 메모. 자유 형식.
|
||||
|
||||
## 결정 사항 / Decisions
|
||||
|
||||
> 추후 면접/회고에서 "왜 이렇게 했나" 답할 근거. 대안과 함께 기록. 각 결정의 근거는 위 Sources 또는 새로 추가된 raw 자료를 가리킬 것.
|
||||
|
||||
- YYYY-MM-DD: <결정 내용> / 이유: <왜> / 검토한 대안: <대안> / 근거: `[[raw/official-docs/<...>]]`
|
||||
|
||||
## Decision Evidence Map / 결정-근거 매핑
|
||||
|
||||
> 각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시한다.
|
||||
> `Decision ID` 는 이 branch-note 안에서 안정적으로 유지한다. 예: `D1`, `D2`.
|
||||
> `Supporting Claims` 는 `raw/<category>/<slug>.md#C1` 형식으로 연결한다.
|
||||
|
||||
> `선택 조건` 열(R2): "이 조건일 때 이 결정, 다른 조건이면 어떤 대안". 분기 없으면 `N/A`.
|
||||
|
||||
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|
||||
|---|---|---|---|---|---|
|
||||
| D1 | <결정 내용> | <이 조건일 때 이 결정, 다른 조건이면 어떤 대안> | `raw/official-docs/<slug>.md#C1`, `raw/company-tech-blogs/<slug>.md#C2` | `official-vendor-doc + company-case-study` | <아직 검증해야 할 위험> |
|
||||
| D2 | <결정 내용> | <선택 조건 또는 N/A> | `raw/official-docs/<slug>.md#C3` | `official-standard` | <위험 또는 N/A> |
|
||||
|
||||
## 구현 가이드 / Implementation Specification
|
||||
|
||||
> *결정 (Decisions)* 이 "*무엇* 을 할 것인가" 라면, 본 §는 "*어디에 어떻게* 구현될 것인가" 의 사전 명세 — *문서가 모호해서 구현자가 임의로 정해야 했던 결정* 카탈로그. 작성 목표는 다음 구현자가 *되묻지 않아도 코드를 작성할 수 있는 수준*.
|
||||
>
|
||||
> **본 §는 일률적 anchor list 를 강제하지 않는다.** branch 마다 구현 내용·범위가 다르므로 sub-section 은 *이 branch 의 결정과 근거에서 도출되는 것만* 작성. 어떤 branch 는 error mapping 표 + 정적 강제 카탈로그, 어떤 branch 는 migration 단계 + wiring, 어떤 branch 는 sequence + state machine. 형식 예시는 `[[raw/branch-notes/feature-boundary-validation-mapping-contract]]` 의 §구현 가이드 참조.
|
||||
>
|
||||
> **3-rule meta principle (필수 준수)**:
|
||||
>
|
||||
> 1. **R1. Reference 필수** — 각 sub-section / row / cell 은 본 branch 의 `Decision ID` (예: D1, D2) + 그 결정의 `Supporting Claim ID` (예: `RAW-SLUG-C1`) 를 reference. *근거 없는 결정 금지* — 모든 구현 detail 은 결정 + 근거의 *도출* 이어야 함.
|
||||
> 2. **R2. UNSUPPORTED_IMPL_DECISION 명시** — 근거 raw 가 *원칙* 만 권고하고 *detail* (메커니즘 선택 / 클래스/rule 명명 / glob 패턴 / algorithm / factory API 모양 등) 은 권고하지 않는 cell 은 `UNSUPPORTED_IMPL_DECISION` 라벨 + 사용자 trade-off 근거 한 줄. 이게 *근거 있는 결정 vs 사용자 임의 trade-off* 의 경계.
|
||||
> 3. **R3. OUT_OF_BRANCH_SCOPE 정제** — 본 branch 결정 범위 밖 cell 은 §구현 가이드에 *남기지 않음*. 별도 branch 또는 canonical SSOT 로 이관 (이관 history 는 별도 § "Audit & Findings" 등에 보존). 도메인 특화 detail (ca-tmpl skeleton 범위 밖) 도 동일하게 정제.
|
||||
>
|
||||
> **각 sub-section 의 권장 헤더 패턴**:
|
||||
>
|
||||
> ```markdown
|
||||
> ### N. <sub-section 제목>
|
||||
>
|
||||
> > **Trace**: <In-scope row 들 + Decision ID + Supporting Claim ID 의 매핑 (한 줄/한 단락)>
|
||||
> >
|
||||
> > - **UNSUPPORTED_IMPL_DECISION**: <근거 없는 사용자 임의 결정 항목들 + 각각의 trade-off 근거 한 줄>
|
||||
>
|
||||
> <표 또는 명확한 구조 — 자유 텍스트 = 모호함 = 되묻기 원인>
|
||||
> ```
|
||||
|
||||
### 1. <sub-section 제목 — 본 branch 의 결정 영역 안에서만>
|
||||
|
||||
> **Trace**: <Decision ID + Supporting Claim ID 매핑>
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**: <임의 결정 항목 + trade-off 한 줄>
|
||||
|
||||
(표 / 명세 / 카탈로그 / 절차 — 본 branch 의 결정 도출 detail)
|
||||
|
||||
### 2. ... (필요 시 추가)
|
||||
|
||||
## 엣지·실패·의존 / Edge · Failure · Dependency
|
||||
|
||||
> R4(깊이 게이트) 캡처용. 정상 경로 외에 *구현 중 부딪힐* 실패/엣지/다른 계약 의존을 미리 열거. 없으면 "해당 없음" 명시(공란 금지).
|
||||
|
||||
- **실패·엣지 경로**: <입력 경계 / 타임아웃 / 부분 실패 / 동시성 등 — 각 경로의 기대 동작>
|
||||
- **다른 계약 의존**: `[[raw/branch-notes/<other-branch>]]` 의 `D<n>` 에 의존 — <무엇을 consume 하는지, 그 계약이 바뀌면 본 브랜치 영향>
|
||||
|
||||
## 검증해야 할 주장 / Claims To Verify
|
||||
|
||||
> 공식 문서나 사례는 근거지만, 내 프로젝트에서의 동작을 자동으로 보장하지 않는다.
|
||||
> 구현 전/중/후에 실제로 검증해야 하는 주장을 분리한다.
|
||||
|
||||
| Claim | Why uncertain | How to verify | Status |
|
||||
|---|---|---|---|
|
||||
| <검증할 주장> | <불확실한 이유> | <테스트/grep/실행 검증 방법> | `needs-confirmation` |
|
||||
| <검증할 주장> | <이유> | <방법> | `planned` |
|
||||
|
||||
|
||||
## Coverage / 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
|
||||
|
||||
> `/coverage` 가 채우는 **생성물** — 손으로 유지하지 않는다. governing 문서(frontmatter `governing_docs`)가 요구하는 관심사를 이 브랜치가 빠짐없이 덮는지의 결과. 기준: `rules/coverage-gate.md`.
|
||||
> 상태: `covered-here`(이 브랜치 결정) / `delegated`(다른 owner 브랜치) / `missing`(아무도 안 맡음 → Blocking).
|
||||
|
||||
| 관심사 | 상태 | owner | 심각도 | 근거 |
|
||||
|--------|------|-------|--------|------|
|
||||
| <governing doc 의 관심사> | covered-here | — | — | D<n> |
|
||||
| <관심사> | delegated | feature-<owner> | OK/Should-fix | §Audit 위임 링크 |
|
||||
| <관심사> | missing | (없음) | 🔴 Blocking | governing doc §<x> 요구, 결정 없음 |
|
||||
|
||||
## 마주친 문제
|
||||
|
||||
> 짧은 메모만. 깊이 있는 트러블슈팅은 `raw/errors/` 로 분리하고 아래 Cluster에 연결.
|
||||
|
||||
- 이슈 1
|
||||
- 원인:
|
||||
- 시도:
|
||||
- 해결: (또는 미해결이면 `needs-confirmation`)
|
||||
- 별도 에러 노트로 분리됨: `[[raw/errors/<...>]]` (생성 시)
|
||||
|
||||
## Cluster / 묶음 (이 branch에서 파생된 자료)
|
||||
|
||||
> 이 branch는 단일 노트가 아니라 **작업 묶음의 entry point**. 이 branch에서 파생된 모든 raw 노트를 카테고리별로 명시. 자식 노트가 forward link만 박아도 Obsidian backlink로 자동 발견되지만, 읽기 흐름과 분류를 위해 hub가 명시적으로 그룹화한다.
|
||||
|
||||
### Sub-branches (세부 작업)
|
||||
|
||||
- `[[raw/branch-notes/<sub-branch-1>]]` — <한 줄 요약>
|
||||
- `[[raw/branch-notes/<sub-branch-2>]]` — <한 줄 요약>
|
||||
|
||||
### Errors (이 branch 작업 중 발생)
|
||||
|
||||
- `[[raw/errors/<...>]]` — <한 줄 요약>
|
||||
|
||||
### Interview prep (이 작업에서 나올 수 있는 면접 질문)
|
||||
|
||||
- `[[raw/interviews/<...>]]` — <한 줄 요약>
|
||||
|
||||
### Lectures (이 작업을 위해 학습한 강의)
|
||||
|
||||
- `[[raw/lectures/<...>]]` — <한 줄 요약>
|
||||
|
||||
### Blog topics / job-posting tie-ins (이 작업에서 파생된 글감)
|
||||
|
||||
- `[[raw/blog-topics/<...>]]` — <채용공고가 아닌 작업·학습·트러블슈팅 기반 글감 후보>
|
||||
- `[[raw/job-postings/<...>]]` — <채용공고에서 파생된 글감 후보>
|
||||
- derived blog: 생성 전. 생성 시 `wiki/blog/<slug>-YYYY-MM-DD.md` 후보
|
||||
|
||||
## 관련 일일 노트 / Daily notes
|
||||
|
||||
> 이 브랜치를 작업한 날짜들. 양방향 nav 유지.
|
||||
|
||||
- `[[raw/daily-notes/YYYY-MM-DD]]`
|
||||
- `[[raw/daily-notes/YYYY-MM-DD]]`
|
||||
|
||||
## 완료 후 정리 / Closure
|
||||
|
||||
> 머지/종료 시점에 채움. `/ingest`가 이 섹션을 기준으로 wiki/projects/에 추출.
|
||||
|
||||
- PR 링크:
|
||||
- 리뷰 메모:
|
||||
- 머지 결과 / 배포 환경: (로컬/dev/staging/prod 어디까지 검증됐는지)
|
||||
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
|
||||
- `actually-implemented` 항목:
|
||||
- `locally-verified` 항목:
|
||||
- `prod-verified` 항목:
|
||||
- **추출하지 않을 항목** (planned / documented-only / abandoned):
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/api-deprecation-sunset-header-migration-window-2026-07-02.md
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
title: blog-topic / api-deprecation-sunset-header-migration-window
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-api-compatibility-deprecation-contract]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, api-design, ietf, api-contract, version-scheme]
|
||||
created: 2026-07-02
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: api-deprecation-sunset-header-migration-window
|
||||
|
||||
> Layer: `raw/blog-topics/` — 작업 중 나온 블로그 글감 원석. 최종 블로그는 canonical 정제 후 `wiki/blog/`에서 작성한다.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-api-compatibility-deprecation-contract]] — API deprecation과 compatibility contract 설계에서 나온 글감.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-07-02
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-api-compatibility-deprecation-contract]]
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: API versioning과 deprecation을 분리하고 `Sunset`/`Deprecation` header, migration window, compatibility fixture를 어떻게 연결할지 정리한다.
|
||||
- 예상 제목 후보:
|
||||
- API deprecation은 versioning과 어떻게 다를까
|
||||
- Sunset header를 보낸다고 deprecation 운영이 끝나는 것은 아니다
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- branch에 D5-D8로 `Sunset`/`Deprecation` header pairing, migration window, compatibility fixture가 정리되어 있다 — 근거 후보: [[raw/branch-notes/feature-api-compatibility-deprecation-contract]] D5-D8, section+line `:143-170`.
|
||||
- 경험 후보:
|
||||
- 90/30 migration window와 OpenAPI `deprecated` 근거는 확인 필요 경계로 남아 있다.
|
||||
- 의견/해석 후보:
|
||||
- deprecation은 "버전을 하나 더 만드는 일"이 아니라 client에게 시간, 신호, migration path를 제공하는 운영 계약이다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. versioning과 deprecation을 분리하기 — 새 버전 제공과 기존 surface 종료 예고는 다른 문제다.
|
||||
2. HTTP header는 사람용 공지가 아니라 machine-readable signal이다 — `Sunset`, `Deprecation`, `Link` 관계를 나눈다.
|
||||
3. migration window는 project policy다 — 외부 표준처럼 말하지 않고 ca-tmpl 결정으로 표시한다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/api-evolution-and-schema.md` 후보:
|
||||
- ca-tmpl API deprecation project decision.
|
||||
- `wiki/concepts/api-evolution-and-schema.md` 후보:
|
||||
- API compatibility, deprecation, sunset header 일반 개념.
|
||||
- 필요한 추가 검증:
|
||||
- OpenAPI `deprecated` 근거, migration window의 project-local status, header pairing source claim.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-api-compatibility-deprecation-contract]] — D5-D8 deprecation decision 근거.
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: 90/30 window와 OpenAPI deprecated marker가 source-backed인지 project convention인지.
|
||||
- 과장하면 안 되는 부분: 실제 운영 deprecation 경험처럼 쓰면 안 된다. ca-tmpl은 운영 배포 검증이 없다.
|
||||
- 블로그로 쓰기 전에 필요한 canonical 정제: `UNSUPPORTED_DECISION`과 source-backed decision 분리.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/api-evolution-and-schema.md` 의 compatibility/deprecation documented-only 섹션에 반영했다. 일반 개념은 기존 `wiki/concepts/api-evolution-and-schema.md` 가 `Sunset` / `Deprecation` 역할 분리와 OpenAPI marker 근거를 이미 포함한다.
|
||||
- 다음 단계: source canonical이 `verified` 상태이므로 이후 `blogify` 대상으로 삼을 수 있다. 단 블로그 본문에서는 90/30 window를 project-local policy로 표기하고 운영 경험처럼 쓰지 않는다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/feature-api-compatibility-deprecation-contract]]
|
||||
- 관련 error:
|
||||
- 관련 interview prep:
|
||||
- derived blog: 생성 전. 생성 시 `wiki/blog/api-deprecation-sunset-header-migration-window-YYYY-MM-DD.md` 후보
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/archunit-generic-return-type-purity-query-port-2026-06-05.md
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
title: blog-topic / archunit-generic-return-type-purity-query-port-2026-06-05
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-application-query-bypass-contract]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, archunit, fitness-function, cqrs, read-model, generics, clean-architecture]
|
||||
created: 2026-06-05
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: archunit-generic-return-type-purity-query-port-2026-06-05
|
||||
|
||||
> Layer: `raw/blog-topics/` — 채용공고가 아닌 작업·학습·트러블슈팅에서 나온 블로그 글감 원석. canonical 정제 전 raw 후보이며, `wiki/blog/` 직접 생성 근거가 아니다.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-application-query-bypass-contract]] — D1 purity guardrail 구현(`query_ports_do_not_leak_domain_jpa_or_web_types`)에서 추출. 본 글감은 그 ArchUnit rule 의 *generic type argument 검사* 기법 단독 추출.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- CQRS-lite projection read 의 핵심 가드레일("read port 가 도메인/JPA/web 타입을 누출하지 않는다")을 ArchUnit 으로 *기계 강제*하려 했는데, `List<DomainType>` 같은 **generic type argument 누출**이 통상적인 raw-return-type 검사(`notHaveRawReturnType`)로는 안 잡힌다는 점.
|
||||
|
||||
## 글감 코어 / Core idea
|
||||
|
||||
- **문제**: `methods().should().notHaveRawReturnType(...)` 는 메서드 반환의 *raw type* 만 본다. `List<WorkLog>` 의 raw type 은 `java.util.List` 라 통과 → 도메인 aggregate 가 generic 인자로 조용히 누출.
|
||||
- **해결**: custom `ArchCondition<JavaMethod>` 에서 `method.getReturnType().getAllInvolvedRawTypes()` 사용. 이 API 는 반환 타입 + **모든 generic 인자를 재귀적으로** erasure 로 평탄화한다(예: `Map<? extends Serializable, List<Integer>>` → `[Map, Serializable, List, Integer]`). 평탄화된 각 `JavaClass` 를 금지 패키지 술어(`resideInAnyPackage(..domain.., ..adapter.., jakarta.persistence.., org.springframework.web.., ...)`)로 검사.
|
||||
- **검증 (violations-as-data + over-block)**: ① raw-leak fixture(도메인 타입 직접 반환), ② **generic-only leak fixture**(`List<FakeDomainEntity>` — raw 검사라면 vacuous pass 할 케이스로 generic 검사 자체를 증명), ③ over-block guard(`List<String>` 반환 clean port 는 미플래그). 셋을 격리 corpus 로 각각 평가.
|
||||
- **타겟팅**: naming convention 으로 read port 식별 — `..application..` 패키지 + simple name `*QueryPort`. through-aggregate read(repository port → 도메인 aggregate)는 의도적으로 rule scope 밖(코어가 강제하는 건 purity 가드레일뿐, projection 사용 자체는 프로젝트 선택).
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: query port return type purity는 raw return type만 보면 generic 인자 leak을 놓치므로 ArchUnit signature traversal로 보강해야 한다.
|
||||
- 예상 제목 후보:
|
||||
- Java generics 때문에 새는 query port purity를 ArchUnit으로 잡기
|
||||
- `List<DomainType>` leak을 정적 분석으로 막는 방법
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- `notHaveRawReturnType`는 `List<DomainType>`의 `DomainType` 인자를 보지 못한다.
|
||||
- `getAllInvolvedRawTypes()`는 return type과 generic 인자를 평탄화해 검사할 수 있다.
|
||||
- 의견/해석 후보:
|
||||
- read projection port의 순수성은 raw type뿐 아니라 signature 전체를 봐야 유지된다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. raw return type 검사만으로는 generic-only leak이 통과하는 이유를 설명한다.
|
||||
2. `getAllInvolvedRawTypes()`로 signature 전체를 평탄화하는 방식을 정리한다.
|
||||
3. violations-as-data fixture와 over-block guard로 rule의 non-vacuity를 확인한다.
|
||||
|
||||
## 왜 의미 있나 / Why it matters
|
||||
|
||||
- "아키텍처 규칙을 코드리뷰 신뢰가 아니라 fitness function 으로 기계 강제" 라는 스켈레톤 가치의 구체 사례. 특히 Java generics 의 type erasure 가 정적 분석의 사각지대를 만드는 지점을 ArchUnit 의 signature traversal API 로 메우는 패턴.
|
||||
- 한계: bytecode 의 generic signature 에 의존 → reflection/`Object` 다운캐스트로 우회하는 누출은 못 잡음(정적 분석 공통 한계). 글에서 이 경계를 솔직히 명시할 것.
|
||||
|
||||
## 관련 / Related
|
||||
|
||||
- [[raw/blog-topics/archunit-violations-as-data-pattern-2026-05-28]] — fixture 로 rule 을 역검증하는 상위 패턴.
|
||||
- [[raw/branch-notes/feature-application-port-usecase-contract]] — `QueryUseCase` / capability fitness function(본 rule 이 보완하는 선행 계약).
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/clean-architecture-package-layout.md` 후보:
|
||||
- application query port generic return type purity guardrail 글감.
|
||||
- 필요한 추가 검증:
|
||||
- 현재 ca-tmpl 코드의 `query_ports_do_not_leak_domain_jpa_or_web_types` rule과 fixture 존재 여부.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-application-query-bypass-contract]]
|
||||
- [[raw/blog-topics/archunit-violations-as-data-pattern-2026-05-28]]
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: generic signature traversal rule의 현재 구현 위치와 테스트명.
|
||||
- 과장하면 안 되는 부분: 정적 분석이 reflection/Object downcast 누출까지 잡는다고 쓰지 않는다.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/clean-architecture-package-layout.md` 에 query port generic return type purity guardrail 글감으로 반영한다.
|
||||
- 다음 단계: source canonical은 일부 verified 영역을 포함하지만, 이 specific rule은 blogify 전 code/branch evidence 재확인이 필요하다.
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/archunit-jackson-default-typing-cve-2019-14379-block-2026-05-29.md
|
||||
@@ -0,0 +1,101 @@
|
||||
---
|
||||
title: blog-topic / archunit-jackson-default-typing-cve-2019-14379-block-2026-05-29
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-boundary-validation-mapping-contract]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, archunit, jackson, security, cve, fitness-function, polymorphic-deserialization]
|
||||
created: 2026-05-29
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: archunit-jackson-default-typing-cve-2019-14379-block-2026-05-29
|
||||
|
||||
> Layer: `raw/blog-topics/` — 채용공고가 아닌 작업·학습·트러블슈팅에서 나온 블로그 글감 원석. canonical 정제 전 raw 후보이며, `wiki/blog/` 직접 생성 근거가 아니다.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-boundary-validation-mapping-contract]] — B5 결정 + Claims to Verify 의 두 행 `actually-implemented` 승급. 본 글감은 그 enforcement 패스의 보안 측면 단독 추출.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-05-29
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-boundary-validation-mapping-contract]] — 1차 enforcement 패스에서 `no_jackson_laissez_faire_subtype_validator` + `no_jackson_enable_default_typing_call` 두 ArchUnit 규칙 + `DefaultTypingFixture` violations-as-data 테스트로 CVE-2019-14379 의 코드 진입점을 정적으로 차단한 작업.
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: Jackson polymorphic deserialization 의 RCE 게이트 (`ObjectMapper.enableDefaultTyping()` / `activateDefaultTyping(LaissezFaireSubTypeValidator)`) 는 *런타임 dependency check 가 아니라 컴파일/테스트 단계의 fitness function* 으로 막아야 안전하다 — 한번 머지된 뒤에는 production 트래픽으로 RCE 가 터지는 게 검출 시점이므로 너무 늦다.
|
||||
- 떠오른 계기: feature-boundary-validation-mapping-contract B5 결정의 enforcement 단계. CVE 자체의 발견 (2019) 과 Jackson 2.10 의 `@Deprecated` 조치 (2019-09) 가 있었음에도, *우리 코드가 호출하지 않는다* 는 사실은 매 PR 마다 사람이 보장해야 하는 규약이었다. 이걸 ArchUnit fitness function 으로 commit 으로 박은 사례.
|
||||
- 예상 제목 후보:
|
||||
- CVE-2019-14379 의 호출 경로를 ArchUnit 으로 정적 봉쇄하기
|
||||
- "Jackson 을 안전하게 쓴다" 를 컨벤션이 아닌 fitness function 으로 박는 방법
|
||||
- `enableDefaultTyping()` 한 줄이 RCE 가 되는 이유와 정적 차단
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- CVE-2019-14379 의 RCE 경로는 `ObjectMapper.enableDefaultTyping()` 활성화 + `ehcache` 같은 gadget class 가 classpath 에 있는 조건 — 근거: NVD CVE-2019-14379 (CVSS 9.8), Jackson 보안 가이드 `enableDefaultTyping` 의 `@Deprecated since 2.10` 표기.
|
||||
- Jackson 2.10 의 공식 대체 API 는 `activateDefaultTyping(PolymorphicTypeValidator)` 이며, allowlist 구현체 `BasicPolymorphicTypeValidator` 또는 `@JsonTypeInfo(use = NAME) + @JsonSubTypes` 명시가 정식 — 근거: `raw/official-docs/schema-jackson-polymorphic-deserialization.md#JACK-POLY-C1..C5`.
|
||||
- `LaissezFaireSubTypeValidator` 는 *모든 subtype 허용* 의 명시적 anti-allowlist — 클래스 이름 그 자체가 "보안 검증 없음" 의 표지 — 근거: `JACK-POLY-C2`.
|
||||
- ca-tmpl 의 적용: `src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java` 에 두 규칙 추가 — `no_jackson_laissez_faire_subtype_validator` (클래스 참조 차단), `no_jackson_enable_default_typing_call` (메서드 호출 차단). 위반 fixture 는 `violations/boundary/DefaultTypingFixture.java`.
|
||||
- 규칙 두 개를 분리한 이유: `LaissezFaireSubTypeValidator` *없이* `enableDefaultTyping()` 만 호출해도 (`ObjectMapper.DefaultTyping.NON_FINAL` 등 deprecated overload) 위험. 호출 차단과 import 차단이 *독립적인 진입점* 이므로 둘 다 닫아야 한다.
|
||||
- 경험 후보:
|
||||
- ArchUnit DSL 의 `callMethodWhere(target(name(...)))` 패턴은 호출자 의도와 무관하게 *메서드 이름* 으로 catch. `enableDefaultTyping` 이라는 메서드 이름이 ObjectMapper 외부에 존재할 가능성이 거의 0 이므로 owner 필터를 생략해도 false positive 없음 — 단순 규칙이 충분.
|
||||
- violations-as-data fixture 는 `DefaultTypingFixture.unsafe()` 한 메서드로 두 규칙을 동시에 catch (호출 + 참조). 한 fixture 가 *서로 다른 규칙 두 개를 동시에 검증* 하는 케이스 — 규칙별 1:1 fixture 가 아니어도 됨.
|
||||
- `app-bootstrap` 의 test classpath 에 Jackson 이 없어서 컴파일 실패 → `testImplementation 'org.springframework.boot:spring-boot-starter-json'` 추가. *fitness function 자체에 의존성을 끌어들이는 cost* 가 있음을 의식해야 한다.
|
||||
- 의견 / 해석 후보:
|
||||
- 라이브러리 *버전* 차단 (jackson-databind ≥ 2.10) 은 supply-chain branch 의 책임이지만, *코드 사용 차단* (deprecated API 호출 금지) 은 boundary contract 의 책임. 두 가지를 같은 PR 에서 묶으면 책임 경계가 흐려진다.
|
||||
- "CVE 가 알려진 후에는 사람이 review 로 막으면 된다" 는 흔한 반론은 *시간 경과에 따른 attention decay* 를 무시한다. 5년 뒤 합류한 신입이 PR review 할 때 `enableDefaultTyping` 이 안전한지 즉시 판단하기 어렵다 — fitness function 은 *지식 보존 비용* 의 외부화.
|
||||
- Jackson 2.15 의 sealed type 자동 인식 같은 *조용한 행동 변화* 가 들어와도, 본 규칙은 호출 자체를 차단하므로 회귀 없음 — Jackson 의 mitigation 진화를 기다리는 대신 *진입점 자체를 봉쇄* 하는 전략의 정당화.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. CVE-2019-14379 의 한 줄 — `enableDefaultTyping()` + ehcache gadget 으로 RCE (CVSS 9.8).
|
||||
2. Jackson 의 공식 대응 — 2.10 `@Deprecated` + `PolymorphicTypeValidator` + `BasicPolymorphicTypeValidator` allowlist.
|
||||
3. 하지만 *우리 코드가 호출하지 않는다* 는 사실의 유지비용 — review fatigue, 시간 경과, 신입 합류.
|
||||
4. fitness function 으로의 외부화 — ArchUnit 규칙 두 개 (`callMethodWhere` + `dependOnClassesThat`) 의 의도와 분리 이유.
|
||||
5. violations-as-data 로 *규칙이 실제로 catch 하는지* 박기 — `DefaultTypingFixture` 한 메서드가 두 규칙을 동시에 검증.
|
||||
6. fitness function 의 비용 — `spring-boot-starter-json` 을 test classpath 에 끌어들임.
|
||||
7. 보안 책임의 경계 — *코드 사용 차단* (boundary contract) vs *버전 차단* (supply-chain) 의 분리.
|
||||
8. 정리 — CVE 차단은 *지식 보존 비용* 의 코드화. fitness function 이 review 의 검토 부담을 commit 으로 이전한다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/archunit-jackson-cve-block.md` 후보:
|
||||
- 실제 두 규칙 + `DefaultTypingFixture` 의 코드 발췌.
|
||||
- `testImplementation 'spring-boot-starter-json'` 추가의 비용.
|
||||
- violations-as-data 1 fixture × 2 negative test 패턴.
|
||||
- `wiki/concepts/fitness-function-for-known-cve.md` 후보:
|
||||
- "*우리 코드가 호출하지 않는다*" 를 review 로 유지하는 비용 분석.
|
||||
- 코드 사용 차단 vs 버전 차단의 책임 경계.
|
||||
- 라이브러리 mitigation 진화와 *진입점 차단* 전략의 trade-off.
|
||||
- 필요한 추가 검증:
|
||||
- `BasicPolymorphicTypeValidator` 사용을 *권장* 하는 메시지를 ArchUnit 위반 메시지에 포함할 가치가 있는지 — false-positive 시 개발자가 즉시 대안을 알 수 있게.
|
||||
- sealed `Command` 타입 도입 시 본 규칙이 `@JsonTypeInfo` 명시 패턴과 충돌하지 않는지 (충돌 없음 가설 — 검증은 future use case 에서).
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-boundary-validation-mapping-contract]] — B5 결정 + 1차 enforcement 패스 결과.
|
||||
- [[raw/official-docs/schema-jackson-polymorphic-deserialization]] — `JACK-POLY-C1..C5` (CVE, deprecated API, allowlist 표준).
|
||||
- [[raw/blog-topics/archunit-violations-as-data-pattern-2026-05-28]] — violations-as-data 메타 패턴. 본 글감은 그 패턴의 *보안* 특화 사례.
|
||||
- [[raw/branch-notes/feature-architecture-enforcement-rules]] — 같은 fitness-function 자체 검증 라인.
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: violation message에 `BasicPolymorphicTypeValidator` 대안을 포함할지 여부.
|
||||
- 과장하면 안 되는 부분: ArchUnit rule은 ca-tmpl 코드의 특정 호출/참조를 차단하는 것이며, Jackson RCE 일반 위험을 모두 제거한다고 쓰지 않는다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/feature-boundary-validation-mapping-contract]]
|
||||
- 관련 raw topic: [[raw/blog-topics/archunit-violations-as-data-pattern-2026-05-28]]
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/boundary-validation-mapping.md` 에 Jackson default typing CVE static block 글감으로 반영했다.
|
||||
- 다음 단계: source canonical이 `verified` 상태이므로 이후 `blogify` 대상으로 삼을 수 있다. 단 ArchUnit rule이 탐지 가능한 호출/참조 범위로 제한된다는 점을 유지한다.
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/archunit-testcompileonly-fixture-annotation-pattern-2026-06-02.md
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
title: testCompileOnly 타입을 ArchUnit fixture 에서 안전하게 참조하기 — annotation-only 패턴
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-streaming-response-contract]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, archunit, gradle, testcompileonly, fixture]
|
||||
created: 2026-06-02
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# testCompileOnly 타입을 ArchUnit fixture 에서 안전하게 참조하기
|
||||
|
||||
## Parent
|
||||
|
||||
- [[raw/branch-notes/feature-streaming-response-contract]]
|
||||
|
||||
## 글감 요약
|
||||
|
||||
ArchUnit violations-as-data 패턴에서 금지 타입을 `testCompileOnly` 로만 선언할 때 발생하는 `NoClassDefFoundError` 와, **annotation-only 참조** 로 해결하는 패턴.
|
||||
|
||||
### 핵심 발견
|
||||
|
||||
- `testCompileOnly` jar 는 compile-time 에만 존재 → JUnit 이 class 로드 시 superclass resolve 불가 → `NoClassDefFoundError`.
|
||||
- ArchUnit 의 `ClassFileImporter` 는 바이트코드 직접 파싱 → class loading 불필요. 문제는 JUnit 스캐닝.
|
||||
- **annotation 참조** 는 JVM 이 class load 시 즉시 resolve 하지 않으므로 안전.
|
||||
- `@EnableWebSocket` (spring-websocket) 을 annotation 으로만 달면: (1) ArchUnit 이 `org.springframework.web.socket..` 의존 탐지 성공, (2) runtime classpath 에 jar 없어도 class 로드 성공.
|
||||
|
||||
### 독자
|
||||
|
||||
Spring Boot + Gradle 멀티모듈 + ArchUnit 조합에서 architecture enforcement 를 구현하는 백엔드 개발자.
|
||||
|
||||
### 구성 아이디어
|
||||
|
||||
1. 문제: violations-as-data fixture 와 `testCompileOnly` 충돌
|
||||
2. 원인 분석: JVM class loading vs ArchUnit bytecode parsing
|
||||
3. 해결: annotation-only 참조 패턴
|
||||
4. 추가 발견: `jakarta.websocket-api` server-only jar 이슈
|
||||
5. 패턴 정리표 (annotation / method return type / extends 별 `testCompileOnly` 안전성)
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-06-02
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-streaming-response-contract]]
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: `testCompileOnly` 금지 타입을 ArchUnit fixture에서 검출하려면 class loading을 유발하지 않는 annotation-only 참조가 안전하다.
|
||||
- 예상 제목 후보:
|
||||
- ArchUnit fixture에서 testCompileOnly 타입을 안전하게 참조하기
|
||||
- JVM class loading과 ArchUnit bytecode parsing의 차이
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- `testCompileOnly` jar는 JUnit class loading 시점에는 없을 수 있다.
|
||||
- ArchUnit importer는 bytecode를 직접 읽으므로 annotation 참조만으로 dependency detection이 가능하다.
|
||||
- 의견/해석 후보:
|
||||
- violations-as-data fixture는 runtime classpath 안정성까지 고려해야 한다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. `testCompileOnly` fixture가 `NoClassDefFoundError`를 만드는 경로를 설명한다.
|
||||
2. annotation-only 참조가 왜 class loading을 덜 유발하는지 정리한다.
|
||||
3. streaming/WebSocket ban rule fixture에 적용할 때의 한계를 적는다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/streaming-response-support.md` 후보:
|
||||
- streaming/WebSocket ban ArchUnit fixture 안정화 글감.
|
||||
- `wiki/projects/ca-tmpl/skeleton-governance-registry-verification-test-scorecard.md` 후보:
|
||||
- ArchUnit fixture/testing pattern 글감.
|
||||
- 필요한 추가 검증:
|
||||
- 현재 fixture가 annotation-only 패턴으로 유지되는지.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-streaming-response-contract]]
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: 현재 test runtime classpath와 fixture 참조 방식.
|
||||
- 과장하면 안 되는 부분: annotation-only가 모든 `testCompileOnly` 참조를 안전하게 만든다고 일반화하지 않는다.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/streaming-response-support.md` 와 `wiki/projects/ca-tmpl/skeleton-governance-registry-verification-test-scorecard.md` 에 ArchUnit fixture 안정화 글감으로 반영한다.
|
||||
- 다음 단계: streaming canonical은 verified지만, fixture classpath 세부는 blogify 전 재확인한다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/feature-streaming-response-contract]]
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/archunit-violations-as-data-pattern-2026-05-28.md
|
||||
@@ -0,0 +1,111 @@
|
||||
---
|
||||
title: blog-topic / archunit-violations-as-data-pattern-2026-05-28
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-architecture-enforcement-rules, feature-application-port-usecase-contract]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, archunit, testing, fitness-function, spring-modulith, negative-test]
|
||||
created: 2026-05-28
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: archunit-violations-as-data-pattern-2026-05-28
|
||||
|
||||
> Layer: `raw/blog-topics/` — 채용공고가 아닌 작업·학습·트러블슈팅에서 나온 블로그 글감 원석. canonical 정제 전 raw 후보이며, `wiki/blog/` 직접 생성 근거가 아니다.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-architecture-enforcement-rules]] — `violations-as-data` pattern 을 Claims to Verify 의 `planned` 에서 `actually-implemented` 로 승급시킨 round 2 작업.
|
||||
- [[raw/branch-notes/feature-application-port-usecase-contract]] — D14 (KEYED idempotency freeze) 의 ArchUnit custom condition 도 같은 fixture 로 보증.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-05-28
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-architecture-enforcement-rules]] — Claims to Verify 의 마지막 행 ("ArchUnit rule 이 위반을 실제로 catch 한다는 commit 된 증명") 을 Spring Modulith `example/ninvalid` 패턴으로 구현한 작업.
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: ArchUnit rule 은 _없는 위반_ 에 대해 vacuously pass 한다 — production 코드에 위반이 우연히 없을 때도, 분석 scope 자체가 비어있을 때도 동일하게 SUCCESS. **위반 fixture + negative test** 로 _rule 이 실제로 catch 하는지_ 를 commit 으로 박아두지 않으면 silent regression 이 누적된다.
|
||||
- 떠오른 계기: round 1 작업에서 발견한 [[raw/errors/archunit-test-scope-sample-ticket-inclusion-2026-05-28]] (ArchUnit scope 가 production classpath 만 보고 vacuous pass 한 사례) + Spring Modulith 의 `example/ninvalid` 패턴 발견.
|
||||
- 예상 제목 후보:
|
||||
- ArchUnit rule 을 _믿을 수 있게_ 만드는 violations-as-data 패턴
|
||||
- Spring Modulith 의 `example/ninvalid` 를 ca-skeleton 에 차용한 6개 negative test
|
||||
- "rule 이 작동하는지" 를 commit 으로 박아두기 — fitness function 의 self-verification
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- ArchUnit 의 vacuous pass 함정은 두 갈래로 발생 — (a) production code 에 위반이 우연히 없음, (b) `@AnalyzeClasses` 의 import scope 가 비어 있음 (classpath 누락) — 근거: [[raw/errors/archunit-test-scope-sample-ticket-inclusion-2026-05-28]] §직접 원인.
|
||||
- Spring Modulith 공식 incubator 가 _자기 rule 들을 검증_ 하기 위해 `example/ninvalid` fixture package 와 `modules.detectViolations().getMessages()` assertion 을 사용 — 근거: `raw/company-tech-blogs/spring-modulith-archunit-generated-exemption-and-violations-as-data.md#SPRING-MOD-AU-C2` (`SPRING-MOD-AU-C2`).
|
||||
- ca-tmpl 의 적용: `src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/violations/` 에 6 fixture (domain 1 + application 5) + `ArchitectureViolationFixtureTest` 에 6 negative test — 근거: `feature-architecture-enforcement-rules.md` 구현 결과 round 2 + Claims to Verify 마지막 행 → `actually-implemented`.
|
||||
- fixture 는 `src/test/...` 위치이므로 main `@AnalyzeClasses(importOptions = DoNotIncludeTests.class)` 에 _자동으로_ 제외됨 — main suite 가 fixture 때문에 실패하지 않음 — 근거: `feature-architecture-enforcement-rules.md` 진행 중 메모 round 2.
|
||||
- negative test 는 `new ClassFileImporter().importPackages("...violations")` 로 fixture _만_ 로드한 뒤 rule 의 `EvaluationResult.hasViolation() == true` 를 단순 assert — 근거: `src/app-bootstrap/src/test/.../ArchitectureViolationFixtureTest.java`.
|
||||
- 경험 후보:
|
||||
- KEYED idempotency rule (D14) 은 ArchUnit DSL 로 표현 불가능 → custom `ArchCondition<JavaClass>` 작성. `JavaAnnotation.get("idempotency")` 가 `JavaEnumConstant` 를 반환하므로 bytecode 만으로 enum value 검사 가능 (reflection 없음) — 근거: `CleanArchitectureTest#notDeclareKeyedIdempotency` 메서드 + branch-note D14.
|
||||
- `TransactionalAnnotatedFixture` 가 `@Transactional` 을 import 하려면 `app-bootstrap/build.gradle` 의 `testCompileOnly 'org.springframework:spring-tx'` 가 필요. production scope 에 영향 없음 (test 만) — 근거: `feature-architecture-enforcement-rules.md` round 2 메모.
|
||||
- fixture 클래스를 `package-private` 으로 유지해 _외부 사용 불가_ 명시 + ArchUnit fixture import 만 동작 — 의도 노이즈 차단.
|
||||
- 의견 / 해석 후보:
|
||||
- ArchUnit rule 은 _코드_ 다. 코드는 테스트 없이 믿으면 안 된다 — fitness function 도 동일.
|
||||
- vacuous pass 는 _"rule 이 안 잡혔다"_ 가 아니라 _"rule 이 무엇을 잡는지 아무도 검증 안 했다"_ 의 신호. 본 패턴은 후자를 commit 으로 박는 게 목적.
|
||||
- **간단한 rule (`noClasses().that(pkg).should().dependOn(pkg2)`) 은 vacuous pass 위험이 _제일 큼_** — 술어가 단순할수록 production 매칭이 우연히 0개가 되기 쉽다. _복잡한 custom condition (D14) 은 명시적으로 짠 거니까 더 안전_ 이라는 직관과 반대.
|
||||
- Spring Modulith 가 _자기 자신_ 을 검증하는 데 쓰는 패턴이라는 점이 글의 강한 thesis — "rule 의 production-readiness 의 골든 스탠다드".
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. 동기 — ArchUnit 의 vacuous pass 함정 두 갈래 → **rule 이 잡는다고 _믿는_ 것과 _증명_ 하는 것의 차이.**
|
||||
2. Spring Modulith 의 self-verification 패턴 (`example/ninvalid` + `detectViolations().getMessages()`) → **OSS 공식 incubator 가 자기 rule 을 같은 방식으로 검증한다는 신뢰 신호.**
|
||||
3. ca-tmpl 의 차용 — `violations/` package + `ArchitectureViolationFixtureTest` → **6 fixture × 6 negative test 의 1:1 매칭.**
|
||||
4. fixture 의 위치 결정 — `src/test/...` 안에 두면 main `DoNotIncludeTests` 가 자동 제외 → **main suite 와 negative test 가 _서로를 깨뜨리지 않는_ 격리.**
|
||||
5. custom ArchCondition (D14) — `JavaAnnotation.get(...)` 로 enum value 검사 → **reflection 없이 bytecode 만으로 annotation parameter catch 가능.**
|
||||
6. fixture 의 deps — `testCompileOnly 'org.springframework:spring-tx'` 의 비대칭 의존 → **`@Transactional` 을 _import_ 만 하고 production scope 에는 안 들어감.**
|
||||
7. 한계 — string-key bean lookup / `Class.forName(String)` 의 bypass 는 여전히 catch 불가 (D12) → **fitness function 의 정직한 한계.**
|
||||
8. 정리 — rule 은 코드다. 코드는 negative test 없이 믿지 말자.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/archunit-violations-as-data.md` 후보:
|
||||
- 실제 6 fixture + 6 negative test 의 코드 발췌.
|
||||
- `testCompileOnly 'org.springframework:spring-tx'` 비대칭 의존 패턴.
|
||||
- `JavaAnnotation.get("idempotency")` custom condition 코드.
|
||||
- `wiki/concepts/archunit-violations-as-data.md` 후보:
|
||||
- "vacuous pass 함정 두 갈래" 의 project-agnostic 정리.
|
||||
- Spring Modulith `example/ninvalid` 패턴의 일반화.
|
||||
- test-scope fixture + main DoNotIncludeTests 의 격리 패턴.
|
||||
- 필요한 추가 검증:
|
||||
- 본 패턴이 _큰 codebase_ (rule 수십 개) 에 적용했을 때 negative test 가 production rule 의 작은 변경에 같이 깨지는지 (regression sensitivity 측정).
|
||||
- `feature-archunit-negative-fixture-baseline` 같은 후속 branch 를 분리할 가치가 있는지.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-architecture-enforcement-rules]] — Claims to Verify 마지막 행 `actually-implemented` 승급 + 진행 중 메모 round 2 + Closure 갱신.
|
||||
- [[raw/branch-notes/feature-application-port-usecase-contract]] — D14 의 custom ArchCondition 으로 KEYED enum value catch.
|
||||
- [[raw/errors/archunit-test-scope-sample-ticket-inclusion-2026-05-28]] — vacuous pass 의 두 번째 갈래 (classpath scope) 의 실 사례.
|
||||
- [[raw/company-tech-blogs/spring-modulith-archunit-generated-exemption-and-violations-as-data]] — `example/ninvalid` 패턴 (`SPRING-MOD-AU-C2`) + `annotatedWith(Generated.class)` 예시 (`SPRING-MOD-AU-C1`).
|
||||
- [[raw/official-docs/archunit-user-guide]] — `JavaAnnotation` / `JavaEnumConstant` / `EvaluationResult.hasViolation()` 의 공식 API 근거 (`ARCHUNIT-UG-C5`).
|
||||
- [[raw/interviews/archunit-static-analysis-limits]] — 같은 작업에서 파생된 면접 질문.
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: negative test 가 _rule wording 변경_ 에 얼마나 민감하게 깨지는지 — false-positive regression 비용 vs 진짜 regression catch 비용의 균형.
|
||||
- 아직 확인해야 할 사실: fixture 클래스의 _수_ 가 늘어날 때 (예: 50 rule × 50 fixture) 관리 비용. Spring Modulith 의 실 fixture 디렉터리 크기와 비교 필요.
|
||||
- 과장하면 안 되는 부분: 본 패턴은 _ArchUnit static analysis 의 한계 (D12 string bypass)_ 를 보완하지 _않는다_. negative test 도 정적이라 reflection bypass 는 잡지 못함.
|
||||
- 과장하면 안 되는 부분: ca-tmpl 의 6 fixture / 6 test 는 _proof-of-concept_ 규모. 실 사업 도메인의 30+ rule 적용 시 관리 비용 측정 미수행.
|
||||
- 블로그로 쓰기 전에 필요한 canonical 정제: `wiki/projects/ca-tmpl/archunit-violations-as-data.md` + `wiki/concepts/archunit-violations-as-data.md` 정제. 1~2개 추가 branch 적용 사례 누적.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/clean-architecture-package-layout.md` 와 `wiki/projects/ca-tmpl/skeleton-governance-registry-verification-test-scorecard.md` 에 violations-as-data / negative fixture 글감으로 반영한다.
|
||||
- 다음 단계: blogify 전 적용 branch 수와 fixture 관리 비용을 확인한다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/feature-architecture-enforcement-rules]] (본 패턴의 직접 적용), [[raw/branch-notes/feature-application-port-usecase-contract]] (D14 custom condition 의 negative test).
|
||||
- 관련 errors: [[raw/errors/archunit-test-scope-sample-ticket-inclusion-2026-05-28]] (vacuous pass 의 다른 갈래).
|
||||
- 관련 interview prep: [[raw/interviews/archunit-static-analysis-limits]] (static analysis 한계 + violations-as-data 보완), [[raw/interviews/clean-architecture-boundary-enforcement]] (선행 — boundary 자동 검증의 자매 토픽).
|
||||
- 관련 blog topics: [[raw/blog-topics/clean-architecture-boundary-enforcement-2026-05-28]] (boundary 자동 검증의 자매 글감).
|
||||
- derived blog: 생성 전. 생성 시 `wiki/blog/archunit-violations-as-data-pattern-YYYY-MM-DD.md` 후보.
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/binary-readiness-scorecard-clean-architecture-skeleton-2026-07-02.md
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
title: blog-topic / binary-readiness-scorecard-clean-architecture-skeleton
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-implementation-readiness-scorecard]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, architecture, testing, ci-cd, clean-architecture, api-contract]
|
||||
created: 2026-07-02
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: binary-readiness-scorecard-clean-architecture-skeleton
|
||||
|
||||
> Layer: `raw/blog-topics/` — 작업 중 나온 블로그 글감 원석. 최종 블로그는 canonical 정제 후 `wiki/blog/`에서 작성한다.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-implementation-readiness-scorecard]] — 15-area binary readiness scorecard와 dry-run evidence에서 나온 글감.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-07-02
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-implementation-readiness-scorecard]]
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: "좋아 보이는 skeleton"과 "도입 가능한 skeleton"을 15개 영역의 binary gate로 분리한 이유를 정리한다.
|
||||
- 예상 제목 후보:
|
||||
- Clean Architecture skeleton의 준비 상태를 점수화해 본 이유
|
||||
- 도입 가능한 skeleton인지 판단하는 binary scorecard
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- branch에 15-area binary readiness scorecard, dry-run evidence, local Gradle/shell gate 통과 기록이 있다 — 근거 후보: [[raw/branch-notes/feature-implementation-readiness-scorecard]] section+line `:25-28`, `:137-160`, `:179-187`, `:298-304`.
|
||||
- 경험 후보:
|
||||
- hosted CI/provenance는 미확인으로 남아 있어 local evidence와 hosted evidence를 분리해야 한다.
|
||||
- 의견/해석 후보:
|
||||
- readiness는 감상적 완성도가 아니라 "새 프로젝트가 복제했을 때 어떤 계약이 실행되는가"로 봐야 한다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. skeleton은 README보다 gate가 중요하다 — 도입자는 설명보다 실패 조건을 믿는다.
|
||||
2. binary scorecard의 장점과 손실 — 애매한 점수보다 통과/미통과가 action을 만든다.
|
||||
3. local evidence와 hosted evidence를 분리하기 — 내 컴퓨터에서 통과한 것과 CI/provenance는 다른 주장이다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/skeleton-readiness-scorecard.md` 후보:
|
||||
- ca-tmpl readiness scorecard 적용 사실.
|
||||
- `wiki/projects/ca-tmpl/skeleton-governance-registry-verification-test-scorecard.md` 후보:
|
||||
- governance/verification/test/scorecard 통합 문서로 병합 가능.
|
||||
- 필요한 추가 검증:
|
||||
- 15개 area 목록, pass/fail 산식, hosted CI/provenance 미확인 경계.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-implementation-readiness-scorecard]] — readiness scorecard와 dry-run evidence 근거.
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: hosted CI, provenance, external adoption 여부.
|
||||
- 과장하면 안 되는 부분: local readiness를 production readiness나 외부 채택 가능성으로 확대하지 않는다.
|
||||
- 블로그로 쓰기 전에 필요한 canonical 정제: scorecard 결과와 evidence grade 분리.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/skeleton-governance-registry-verification-test-scorecard.md` 에 binary readiness scorecard 글감으로 반영했다.
|
||||
- 다음 단계: target canonical이 아직 `draft` 이므로 `blogify` 전 review/verify가 필요하다. local readiness를 production readiness로 확대하지 않는다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/feature-implementation-readiness-scorecard]]
|
||||
- 관련 error:
|
||||
- 관련 interview prep:
|
||||
- derived blog: 생성 전. 생성 시 `wiki/blog/binary-readiness-scorecard-clean-architecture-skeleton-YYYY-MM-DD.md` 후보
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/boundary-validation-mapper-responsibility-map-2026-07-02.md
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
title: blog-topic / boundary-validation-mapper-responsibility-map
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-boundary-validation-mapping-contract]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, validation, mapper, bean-validation, partial-update, anti-corruption-layer]
|
||||
created: 2026-07-02
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: boundary-validation-mapper-responsibility-map
|
||||
|
||||
> Layer: `raw/blog-topics/` — 채용공고가 아닌 작업·학습·트러블슈팅에서 나온 블로그 글감 원석. 다듬어진 블로그 초안은 canonical (`wiki/concepts/` 또는 `wiki/projects/`) 정제 후 `/blogify` 또는 수동 작성으로 `wiki/blog/`에 별도 작성한다.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-boundary-validation-mapping-contract]] — 입력 경계 검증과 DTO-domain mapper 책임을 B1-B8로 분리한 verified branch에서 나온 상위 글감.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-07-02
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-boundary-validation-mapping-contract]]
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: 입력 syntax, application policy, domain invariant, persistence integrity, mapper normalization 책임을 한 계층에 몰아넣지 않고 경계별로 나눈 이유를 정리한다.
|
||||
- 예상 제목 후보:
|
||||
- Clean Architecture에서 validation과 mapper 책임을 나누는 법
|
||||
- DTO mapper를 단순 변환기가 아니라 경계 정책으로 본 이유
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- B1-B8 결정 묶음이 branch의 decision 영역과 extraction 영역에 존재한다 — 근거 후보: [[raw/branch-notes/feature-boundary-validation-mapping-contract]] section+line `:87-101`, `:385-407`.
|
||||
- 경험 후보:
|
||||
- 기존 Jackson default typing CVE 글감은 B5에 가까운 좁은 보안 사례라, B1-B8 전체 책임 지도 글감은 별도로 필요하다 — 근거 후보: lane-01 inventory, 기존 [[raw/blog-topics/archunit-jackson-default-typing-cve-2019-14379-block-2026-05-29]].
|
||||
- 의견/해석 후보:
|
||||
- mapper를 "보일러플레이트 제거 도구"로만 보면 normalization, masking, public-field boundary 같은 정책 책임이 흐려진다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. validation이라는 단어가 너무 넓다 — syntax, policy, invariant, persistence integrity는 실패 위치와 책임자가 다르다.
|
||||
2. mapper는 단순 변환기만은 아니다 — 외부 DTO와 내부 domain 사이에서 normalization과 public-field 정책을 고정한다.
|
||||
3. ArchUnit rule은 boundary drift를 데이터로 만든다 — 계층 의도를 빌드 실패 조건으로 바꾸는 것이 핵심이다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/boundary-validation-mapping.md` 후보:
|
||||
- ca-tmpl에서 B1-B8로 검증/매핑 책임을 분리한 프로젝트 결정과 검증 등급.
|
||||
- `wiki/concepts/boundary-validation-and-dto-mapping.md` 후보:
|
||||
- Bean Validation, partial update, anti-corruption mapper 책임 분리 일반 개념.
|
||||
- 필요한 추가 검증:
|
||||
- B1-B8 항목의 실제 구현/테스트 상태와 `locally-verified` 범위 확인.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-boundary-validation-mapping-contract]] — B1-B8 결정과 extraction 후보의 근거.
|
||||
- [[raw/blog-topics/archunit-jackson-default-typing-cve-2019-14379-block-2026-05-29]] — 좁은 하위 topic과의 중복 경계.
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: B1-B8 각각의 구현 파일/테스트 anchor.
|
||||
- 과장하면 안 되는 부분: 모든 validation 책임을 이 구조 하나로 해결한다고 쓰면 안 된다. ca-tmpl의 경계 분리 결정과 검증된 범위로 제한한다.
|
||||
- 블로그로 쓰기 전에 필요한 canonical 정제: project 문서의 verified 항목과 concept 문서의 일반 개념을 분리.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/boundary-validation-mapping.md` 에 boundary/mapper 책임 분리 글감으로 반영했다.
|
||||
- 다음 단계: source canonical이 `verified` 상태이므로 이후 `blogify` 대상으로 삼을 수 있다. 단 ca-tmpl 내부 taxonomy와 일반 표준을 혼동하지 않는다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/feature-boundary-validation-mapping-contract]]
|
||||
- 관련 error:
|
||||
- 관련 interview prep:
|
||||
- derived blog: 생성 전. 생성 시 `wiki/blog/boundary-validation-mapper-responsibility-map-YYYY-MM-DD.md` 후보
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/cache-backend-router-fail-open-decorator-2026-07-02.md
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
title: blog-topic / cache-backend-router-fail-open-decorator
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-cachestore-multi-backend-router]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, caching, spring-boot, fail-open-fail-closed, circuit-breaker]
|
||||
created: 2026-07-02
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: cache-backend-router-fail-open-decorator
|
||||
|
||||
> Layer: `raw/blog-topics/` — 작업 중 나온 블로그 글감 원석. 최종 블로그는 canonical 정제 후 `wiki/blog/`에서 작성한다.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-cachestore-multi-backend-router]] — cache backend routing과 fail-open decorator 구현 경험에서 나온 글감.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-07-02
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-cachestore-multi-backend-router]]
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: cache 장애를 backend 내부 `try/catch`로 흩뿌리지 않고 router/decorator 조립 계약으로 중앙화한 이유를 정리한다.
|
||||
- 예상 제목 후보:
|
||||
- Cache fail-open을 decorator로 분리한 이유
|
||||
- Cache backend router로 OCP와 장애 정책을 같이 지키기
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- `FailOpenCacheStore`, `CacheStoreRouter`, `CacheBackend` 기여 모델이 implemented/local evidence로 정리됐다 — 근거 후보: [[raw/branch-notes/feature-cachestore-multi-backend-router]] D1-D3, section+line `:78-83`, 구현 결과 `:104-112`, closure `:121-125`.
|
||||
- 경험 후보:
|
||||
- backend별 장애 처리 분기가 늘어날수록 정책이 흩어지므로, fail-open/fail-closed를 조립 계층에서 명시하는 편이 낫다.
|
||||
- 의견/해석 후보:
|
||||
- cache는 correctness owner가 아니라 availability optimization일 수 있으므로, 실패 정책을 use case 밖에서 드러내야 한다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. cache backend가 늘어나면 장애 정책도 늘어난다 — Redis/Caffeine/noop을 같은 interface로 묶는 것만으로는 부족하다.
|
||||
2. fail-open은 내부 catch가 아니라 contract다 — 어떤 exception을 삼키고 무엇을 관측할지 중앙에서 정한다.
|
||||
3. router/decorator 구조가 OCP를 지키는 지점 — backend 추가와 정책 변경을 분리한다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/data-layer-persistence-cache-outbound.md` 후보:
|
||||
- ca-tmpl cache backend router와 fail-open decorator 구현 사실.
|
||||
- `wiki/concepts/fail-open-fail-closed.md` 후보:
|
||||
- cache에서 fail-open을 선택할 수 있는 조건과 경계.
|
||||
- 필요한 추가 검증:
|
||||
- 실제 class/test anchor와 exception classification 범위.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-cachestore-multi-backend-router]] — D1-D3와 구현/검증 결과.
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: fail-open 적용 대상 exception, metric/log 기록 방식.
|
||||
- 과장하면 안 되는 부분: 모든 cache 실패를 삼켜도 된다는 뜻이 아니다. ca-tmpl에서 정한 cache role과 검증된 backend 범위로 제한한다.
|
||||
- 블로그로 쓰기 전에 필요한 canonical 정제: project 문서의 data-layer cache section 보강.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/data-layer-persistence-cache-outbound.md` 에 cache router/decorator 글감으로 반영했다.
|
||||
- 다음 단계: target canonical이 아직 `draft` 이므로 `blogify` 전 review/verify가 필요하다. 모든 cache 실패를 삼켜도 된다고 쓰지 않는다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/feature-cachestore-multi-backend-router]]
|
||||
- 관련 error:
|
||||
- 관련 interview prep:
|
||||
- derived blog: 생성 전. 생성 시 `wiki/blog/cache-backend-router-fail-open-decorator-YYYY-MM-DD.md` 후보
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/cache-consistency-after-commit-stampede-contract-2026-07-02.md
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
title: blog-topic / cache-consistency-after-commit-stampede-contract
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-cache-consistency-contract]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, caching, persistence, spring-boot, transaction-synchronization]
|
||||
created: 2026-07-02
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: cache-consistency-after-commit-stampede-contract
|
||||
|
||||
> Layer: `raw/blog-topics/` — 작업 중 나온 블로그 글감 원석. 최종 블로그는 canonical 정제 후 `wiki/blog/`에서 작성한다.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-cache-consistency-contract]] — cache invalidation, stampede, negative cache, consistency window 결정에서 나온 글감.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-07-02
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-cache-consistency-contract]]
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: cache invalidation은 after-commit으로, stampede guard는 single/multi-instance별로, negative TTL과 consistency window는 별도 계약으로 나눠야 한다는 주제.
|
||||
- 예상 제목 후보:
|
||||
- cache invalidation을 transaction commit 뒤로 미루는 이유
|
||||
- cache consistency와 stampede guard를 한데 묶으면 안 되는 이유
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- branch에 D2-D8로 after-commit invalidation, stampede, negative cache, consistency window가 정리되어 있다 — 근거 후보: [[raw/branch-notes/feature-cache-consistency-contract]] D2-D8, section+line `:87-107`, tests `:137-143`.
|
||||
- 경험 후보:
|
||||
- D5-D9에는 unsupported/planned 경계가 있어 raw topic 단계에서 보강 필요로 둬야 한다.
|
||||
- 의견/해석 후보:
|
||||
- cache consistency는 하나의 기법이 아니라 invalidation timing, concurrency guard, stale window의 조합이다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. commit 전 invalidation의 함정 — DB transaction과 cache state가 엇갈릴 수 있다.
|
||||
2. stampede guard는 deployment model을 탄다 — single-instance lock과 multi-instance lock은 다른 문제다.
|
||||
3. negative cache와 consistency window는 숫자 정책이다 — source-backed fact와 project convention을 분리한다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/data-layer-persistence-cache-outbound.md` 후보:
|
||||
- ca-tmpl cache consistency project decision.
|
||||
- `wiki/concepts/data-layer-persistence-cache-outbound.md` 후보:
|
||||
- cache-aside, after-commit invalidation, stampede guard 일반 개념.
|
||||
- 필요한 추가 검증:
|
||||
- D5-D9의 source support, planned test 구현 여부.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-cache-consistency-contract]] — cache consistency decisions.
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: consistency window 수치와 stampede guard 구현 상태.
|
||||
- 과장하면 안 되는 부분: planned test와 unsupported decision을 implemented처럼 쓰지 않는다.
|
||||
- 블로그로 쓰기 전에 필요한 canonical 정제: source-backed claim과 project-local policy 분리.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/data-layer-persistence-cache-outbound.md` 에 after-commit invalidation/stampede 글감으로 반영했다.
|
||||
- 다음 단계: target canonical이 아직 `draft` 이므로 `blogify` 전 review/verify가 필요하다. planned test와 unsupported decision을 implemented처럼 쓰지 않는다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/feature-cache-consistency-contract]]
|
||||
- 관련 error:
|
||||
- 관련 interview prep:
|
||||
- derived blog: 생성 전. 생성 시 `wiki/blog/cache-consistency-after-commit-stampede-contract-YYYY-MM-DD.md` 후보
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/ci-gate-wiring-vs-policy-ownership-2026-06-20.md
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
title: blog-topic / ci-gate-wiring-vs-policy-ownership
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-ci-quality-gates-contract]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, ci, github-actions, gradle]
|
||||
created: 2026-06-20
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: ci-gate-wiring-vs-policy-ownership
|
||||
|
||||
> Layer: `raw/blog-topics/` — 작업·트러블슈팅에서 나온 블로그 글감 원석.
|
||||
> `status_label`: `captured`
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-ci-quality-gates-contract]] — "어떤 게이트가 도느냐(wiring)" 와 "그 게이트의 도구·임계값(policy)" 을 다른 branch 가 소유하도록 분리한 경험에서 도출.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-06-20
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-ci-quality-gates-contract]]
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 줄 요약: CI 품질 게이트를 "배선(wiring)" 과 "정책(policy)" 으로 쪼개면, 20개 게이트를 한 워크플로에 욱여넣지 않고도 소유권이 깨끗해지고 게이트가 silent-pass 하지 않는다.
|
||||
- 풀어야 할 질문들:
|
||||
- **소유권 분리.** vulnerability 스캔의 *release-blocking 여부*(wiring)와 *scanner 선택·severity 임계값*(policy)을 왜 다른 owner 가 갖나? → 한 branch 가 둘 다 가지면 정책 변경이 매번 게이트 그래프를 건드려 결합도가 폭증. ca-tmpl 은 `.github/ci-gate-matrix.yml`(20행 SSOT) + `verify-gate-matrix.sh` cross-check 로 "표 ↔ 실제 task/test/job" 정합을 매 PR 강제.
|
||||
- **fan-in 이 차단을 보장하려면.** `needs + if: success()` aggregator 는 상위 실패 시 *skipped* — 차단 안 됨. `always()` + `needs.*.result` 스캔이라야 "1건 실패 → 릴리스 block". (evidence-first: 공식 문서가 보장하는 건 매핑 가능성뿐, 실제 차단은 의도적 실패 잡으로 검증해야.)
|
||||
- **위임 게이트의 정직한 표현.** owner branch 가 아직 없는 게이트(SBOM/Cosign/SLSA/gitleaks)는 가짜 통과 잡으로 채우지 말고 `delegated-pending` 으로 *명시적으로 미구현* 이라 표시 → cross-check 가 개수까지 보고.
|
||||
- 독자가 얻어갈 것: "게이트를 늘리는 것" 보다 "게이트가 실제로 막는지 + 누가 그 정책을 소유하는지" 가 본질이라는 관점.
|
||||
|
||||
## 확장 메모 / Expansion notes
|
||||
|
||||
- 곁가지: flaky quarantine 의 14일 sunset 을 Gradle 거버넌스 태스크(`verifyQuarantineSunset`)로 강제 — `@Tag("quarantine")` 격리 + repo-루트 레지스트리 + drift/sunset 이중 검사. quarantine 이 *영구 주차장* 이 되는 걸 빌드가 막는다. (별도 글감 가능.)
|
||||
- 대비 사례: `.trivyignore` suppression 거버넌스([[raw/blog-topics/trivy-suppression-governance-static-gate-2026-06-20]])와 같은 패턴 — "정책 파일 + CI 필드검증 + CODEOWNERS merge 승인" 삼중 통제.
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- CI gate matrix는 gate wiring과 policy owner를 분리해 기록한다.
|
||||
- GitHub Actions fan-in은 `always()`와 `needs.*.result` 확인을 써야 upstream failure가 skipped로 묻히지 않는다.
|
||||
- 의견/해석 후보:
|
||||
- CI gate의 본질은 gate 수가 아니라 실제 차단 여부와 policy ownership의 분리다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. gate wiring과 policy ownership을 분리하는 이유를 설명한다.
|
||||
2. `needs + if: success()` fan-in의 skipped 함정을 다룬다.
|
||||
3. delegated-pending gate를 가짜 green으로 만들지 않는 표현 방식을 정리한다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/devops-ci-supply-chain-dx.md` 후보:
|
||||
- CI gate wiring vs policy ownership 글감.
|
||||
- 필요한 추가 검증:
|
||||
- 실제 workflow fan-in 실패 검증과 matrix cross-check 구현 여부.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-ci-quality-gates-contract]]
|
||||
- [[raw/errors/ci-fan-in-skipped-not-failed-and-gitignored-config-2026-06-20]]
|
||||
- [[raw/interviews/ci-release-gate-fan-in-blocking-2026-06-20]]
|
||||
- [[raw/blog-topics/trivy-suppression-governance-static-gate-2026-06-20]]
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: hosted CI에서 fan-in 차단 검증이 수행됐는지.
|
||||
- 과장하면 안 되는 부분: delegated-pending gate를 구현 완료 gate처럼 쓰지 않는다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/feature-ci-quality-gates-contract]]
|
||||
- 관련 error: [[raw/errors/ci-fan-in-skipped-not-failed-and-gitignored-config-2026-06-20]]
|
||||
- 관련 interview: [[raw/interviews/ci-release-gate-fan-in-blocking-2026-06-20]]
|
||||
- 유사 거버넌스 글감: [[raw/blog-topics/trivy-suppression-governance-static-gate-2026-06-20]]
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/devops-ci-supply-chain-dx.md` 에 CI gate wiring과 policy ownership 분리 글감으로 반영했다.
|
||||
- 다음 단계: target canonical이 아직 `draft` 이므로 `blogify` 전 GitHub Actions fan-in 차단 검증과 delegated-pending 범위를 분리한다.
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/clean-architecture-boundary-enforcement-2026-05-28.md
|
||||
@@ -0,0 +1,121 @@
|
||||
---
|
||||
title: blog-topic / clean-architecture-boundary-enforcement-2026-05-28
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-architecture-enforcement-rules]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, architecture, testing, archunit, clean-architecture, gradle]
|
||||
created: 2026-05-28
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: clean-architecture-boundary-enforcement-2026-05-28
|
||||
|
||||
> Layer: `raw/blog-topics/` — 채용공고가 아닌 작업·학습·트러블슈팅에서 나온 블로그 글감 원석. canonical 정제 전 raw 후보이며, `wiki/blog/` 직접 생성 근거가 아니다.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-architecture-enforcement-rules]] — Clean Architecture 경계를 Gradle / ArchUnit fitness function 으로 실 강제한 구현·검증 경험 (Decisions D1~D10 + Claims to Verify 표).
|
||||
- [[raw/project-notes/ca-skeleton-operational-contract]] — ca-tmpl skeleton 의 module boundary / operational contract SSOT.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-05-28
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-architecture-enforcement-rules]] — `Decision D1` (CA 경계는 architecture test 로 강제) 을 실제 코드와 테스트로 붙인 작업.
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: Clean Architecture 는 문서로만 선언하면 시간이 지나면 무너지므로, **Gradle module dependency rule (1차) + ArchUnit bytecode fitness function (2차)** 으로 _역할을 나눠_ 자동 검증해야 한다. 한쪽만으로는 항상 false-pass 가 남는다.
|
||||
- 떠오른 계기: `feature-architecture-enforcement-rules` 작업에서 ca-tmpl `src/build.gradle` 의 `verifyCleanArchitectureDependencies` 와 `src/app-bootstrap/src/test/.../CleanArchitectureTest.java` 를 함께 보강하고, 임시 위반 코드로 red/green 검증까지 마침.
|
||||
- 예상 제목 후보:
|
||||
- Clean Architecture 경계를 _문서가 아니라 테스트_ 로 지키기 — Gradle + ArchUnit 의 분업
|
||||
- Gradle 이 잡는 것 vs ArchUnit 이 잡는 것 — module graph 와 bytecode rule 의 역할 분리
|
||||
- 빈 anchor module 도 ArchUnit 으로 검증할 수 있을까? — `allowEmptyShould(true)` 의 정직한 사용
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
> 아직 canonical 이 아니다. 사실/경험/의견 후보를 분리한다. 각 항목은 branch-note Decision ID 또는 외부 source claim ID 로 근거를 인용한다.
|
||||
|
||||
- 사실 후보:
|
||||
- ca-tmpl 의 boundary 강제는 **Gradle 의 project-dependency 매트릭스 + ArchUnit fitness function** 두 층으로 구성된다. Gradle 은 _build graph 수준_, ArchUnit 은 _bytecode/import 수준_ 을 막는다. 둘은 잡는 위반의 종류가 다르다 — 근거: `feature-architecture-enforcement-rules.md` D1 (CA 경계 = architecture test 강제), D2 (package rule = Gradle multi-module boundary). 외부 근거: `raw/official-docs/governance-archunit-official.md#AU-OFF-C1`, `raw/official-docs/archunit-conditional-on-property-3-layer-pattern.md#AUCP-C5`.
|
||||
- `domain-core` 는 `org.springframework..`, `jakarta.persistence..`, `..adapter..`, `..application..`, `..bootstrap..` import 모두 금지 — 근거: `feature-architecture-enforcement-rules.md` D3 (domain-core forbidden import). 외부 근거: `raw/company-tech-blogs/woowahan-hexagonal-multimodule.md#WW-HEX-C1` (Domain / Application / Framework / Bootstrap 4-module 격리 사례).
|
||||
- `application-core` 가 `org.springframework.transaction.annotation.Transactional` 을 _직접 import_ 하면 ArchUnit rule 이 실패. Spring 공식은 `@Transactional` 직접 부착을 권고 (다수파) 이므로 ca-tmpl 은 의도적 소수파 결정 — 근거: `feature-architecture-enforcement-rules.md` D8 (application `@Transactional` 직접 import 금지) + `feature-application-port-usecase-contract.md` D3 (TransactionPort abstraction). 외부 근거: `raw/official-docs/at-transactional-spring-official.md#AT-TX-C1` (Spring 공식 권고).
|
||||
- `production_code_does_not_depend_on_sample_ticket` ArchUnit rule + Gradle `verifyCleanArchitectureDependencies` 가 동시에 sample-ticket 역수입을 차단 — 근거: `feature-architecture-enforcement-rules.md` D7 (sample-ticket production 역수입 금지) + Claims to Verify "production module 이 `sample-ticket` 에 의존하면 실패한다" → `locally-verified`.
|
||||
- 경험 후보:
|
||||
- 임시 위반 코드 (`shared.ticket` package, controller 의 domain return, application 의 `@Transactional`, `app-bootstrap -> sample-ticket` Gradle dep) 를 각각 추가해 신규 rule 이 실패함을 red/green 으로 확인 → 위반 제거 후 `cd src && ./gradlew :app-bootstrap:test verifyCleanArchitectureDependencies` 와 `cd src && ./gradlew test` 모두 통과 — 근거: `feature-architecture-enforcement-rules.md` §진행 중 메모 2026-05-28 항목 + Closure §`locally-verified`.
|
||||
- 빈 skeleton anchor module 이 ArchUnit empty-should failure 를 일으켜 `allowEmptyShould(true)` 를 _빈 상태가 의도된 rule 에만_ 선별 적용 — 근거: `feature-architecture-enforcement-rules.md` §마주친 문제 + 파생 에러 [[raw/errors/archunit-empty-should-anchor-2026-05-27]].
|
||||
- Codex sandbox 의 read-only `~/.gradle` 권한 때문에 wrapper 가 lock 파일을 못 만들어 실행이 실패 → 사용자 승인 escalation 으로 재실행 — 근거: 파생 에러 [[raw/errors/gradle-wrapper-readonly-cache-2026-05-28]].
|
||||
- 의견 / 해석 후보:
|
||||
- Gradle 의 project dependency 매트릭스만으로는 _세부 import_ (e.g., controller 가 JPA entity 를 return type 으로 노출) 를 못 잡는다. ArchUnit 이 이걸 보완. 반대로 ArchUnit 만으로는 _module 간 build-graph 사이클_ 을 깔끔하게 못 잡는다. **둘을 분업하는 게 작은 skeleton 에서는 Spring Modulith 도입보다 가볍다** — 근거: `feature-architecture-enforcement-rules.md` §결정 사항 2026-05-27 "Spring Modulith verifier 도입은 out of scope §범위" + D5 Open Risk ("Spring Modulith 없이 public API 강제는 약함").
|
||||
- **ArchUnit 은 reflection / runtime lookup 우회를 잡지 못한다** (`ApplicationContext#getBean` 류). 이건 ArchUnit 의 한계로 솔직히 인정해야 하며, Sonar custom rule 또는 review checklist 로 보완 — 근거: `feature-architecture-enforcement-rules.md` Claims to Verify "runtime lookup 우회는 ArchUnit 으로 잡히지 않는다" → `planned`.
|
||||
- **다수파 (`@Transactional` 직접 부착) 도 합리적이다**. ca-tmpl 의 boundary 강제는 _template repository 라서_ 의도적으로 엄격한 소수파. 단일 DB / 단일 transaction manager 상황의 작은 팀은 다수파가 boilerplate 비용 측면에서 낫다 — 근거: `feature-architecture-enforcement-rules.md` D8 Open Risk + `feature-application-port-usecase-contract.md` 외부 근거 §대안 비교.
|
||||
|
||||
## Outline seed
|
||||
|
||||
> 각 섹션 옆에 `→ 핵심 메시지` 를 함께 명시한다.
|
||||
|
||||
1. 동기 — Clean Architecture 를 "했다" 고 말하지만 controller 가 repository 를 import 해도 빌드가 도는 흔한 상황 → **문서만으로는 boundary drift 가 누적된다.**
|
||||
2. 두 층의 분업 — Gradle project-dependency matrix vs ArchUnit bytecode rule → **각자 잡는 위반 종류가 다르고 한쪽만으로는 false-pass 가 남는다.**
|
||||
3. 실제 구현 스케치 — `verifyCleanArchitectureDependencies` 의 allowed map + `CleanArchitectureTest` 의 12개 rule → **rule 은 _도메인 추가_ 보다 _도메인 누락_ 으로 더 자주 깨진다 (예: 새 module 추가 시 양쪽 다 업데이트 필요).**
|
||||
4. red/green 으로 rule 을 _믿을 수 있게_ 만들기 — 임시 위반 코드 4종 추가 → 실패 확인 → 제거 → 통과 → **rule 이 _진짜로 잡는지_ 를 매번 검증하지 않으면 silent regression 이 생긴다.**
|
||||
5. 빈 anchor 문제 — skeleton template 에서 production 코드가 비어 있는 상태도 valid → **`allowEmptyShould(true)` 는 _빈 상태가 의도된 rule_ 에만 선별 적용. 모든 rule 에 일괄 적용하면 안 됨.**
|
||||
6. ArchUnit 의 한계와 보완 — runtime reflection / MapStruct generated path / Spring Modulith → **솔직한 한계 인정 + 보완 도구 (Sonar / review checklist / Modulith) 의 분업.**
|
||||
7. template repository 라서 가능한 엄격함 — production project 와의 trade-off → **boundary 비용을 _learning cost_ 로 흡수할 수 있는 환경에서 강제하라.**
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
> `wiki/blog/` 로 바로 가지 않는다. 먼저 어떤 canonical 문서로 정제할지 기록한다.
|
||||
|
||||
- `wiki/projects/ca-tmpl/architecture-enforcement-rules.md` 후보:
|
||||
- 실제 적용된 Gradle dependency matrix (모듈별 allowed list).
|
||||
- 실제 작성된 ArchUnit rule 12종 (이름 + 잡는 위반).
|
||||
- red/green 검증 절차 (`feature-architecture-enforcement-rules.md` Closure 의 `locally-verified` 5개 항목).
|
||||
- `wiki/concepts/architecture-enforcement-testing.md` 후보:
|
||||
- Gradle build-graph rule 과 ArchUnit bytecode rule 의 _역할 분리_ 패턴 (project-agnostic).
|
||||
- `allowEmptyShould` 와 skeleton template 의 빈 anchor 처리 패턴.
|
||||
- "ArchUnit 의 한계: runtime reflection / generated code" 일반 원칙.
|
||||
- 필요한 추가 검증:
|
||||
- runtime lookup / reflection 우회가 현재 rule 을 실제로 false-pass 하는지 PoC 실험 (`feature-architecture-enforcement-rules.md` Claims to Verify 의 `planned` 항목).
|
||||
- MapStruct generated mapper exemption 경로 확인 (동일 표의 `needs-confirmation` 항목).
|
||||
- Spring Modulith 를 도입했을 때 ArchUnit rule 과의 중복/대체 관계.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-architecture-enforcement-rules]] — 구현 결정 (D1~D10), 검증 결과, Claims to Verify 의 status grading, Closure 의 `locally-verified` / `documented-only` 분리.
|
||||
- [[raw/branch-notes/feature-skeleton-package-blueprint-contract]] — module boundary 의 자매 결정 (D1~D8). 본 글의 Gradle dependency matrix 항목은 두 branch 결정의 교집합.
|
||||
- [[raw/branch-notes/feature-application-port-usecase-contract]] — application 의 `@Transactional` 직접 import 금지 결정 (D3) 와 TransactionPort 추상화. 본 글의 다수파 vs 소수파 trade-off 단락 근거.
|
||||
- [[raw/errors/archunit-empty-should-anchor-2026-05-27]] — 빈 anchor 와 `allowEmptyShould(true)` 의 선별 적용 사례.
|
||||
- [[raw/errors/gradle-wrapper-readonly-cache-2026-05-28]] — sandbox 환경에서 build tool 실행 검증의 함정.
|
||||
- [[raw/errors/archunit-test-scope-sample-ticket-inclusion-2026-05-28]] — `production_code_does_not_depend_on_sample_ticket` rule 의 test-scope inclusion 미묘함.
|
||||
- [[raw/official-docs/archunit-user-guide]] — ArchUnit rule DSL / JUnit 통합 (`ARCHUNIT-UG-C5`, `ARCHUNIT-UG-C6`).
|
||||
- [[raw/official-docs/governance-archunit-official]] — architecture test 로 governance 강제하는 일반 근거 (`AU-OFF-C1`, `AU-OFF-C2`).
|
||||
- [[raw/official-docs/archunit-conditional-on-property-3-layer-pattern]] — predicate/condition 기반 fitness function 의 bytecode 모델 (`AUCP-C1` ~ `AUCP-C5`).
|
||||
- [[raw/company-tech-blogs/woowahan-hexagonal-multimodule]] — Domain / Application / Framework / Bootstrap 4-module 분리 사례 (`WW-HEX-C1` ~ `WW-HEX-C5`).
|
||||
- [[raw/company-tech-blogs/modulith-kakaobank-techblog-2025]] — Gradle multi-module + Hexagonal + Spring Modulith 사례 (`KAKAOBANK-MOD-C2`, `KAKAOBANK-MOD-C4`).
|
||||
- [[raw/interviews/clean-architecture-boundary-enforcement]] — 같은 경험에서 파생된 예상 면접 질문.
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: runtime lookup / reflection 우회가 현재 ArchUnit rule 을 실제로 false-pass 하는지 실험 미수행 (`feature-architecture-enforcement-rules.md` Claims to Verify `planned`).
|
||||
- 아직 확인해야 할 사실: MapStruct generated mapper exemption 의 build path 가 실제 빌드 도구 설정에 따라 어떻게 달라지는지 (`feature-architecture-enforcement-rules.md` D9 `UNSUPPORTED_DECISION`).
|
||||
- 과장하면 안 되는 부분: 본 글의 모든 검증은 **`locally-verified`** 다. ca-tmpl 은 template repository 이고 prod 운영 검증은 없다. 글에 "운영에서 검증된" 같은 표현 금지.
|
||||
- 과장하면 안 되는 부분: "Gradle + ArchUnit 분업이 Spring Modulith 보다 우월하다" 가 아니라 "_작은 skeleton 에서는_ 가볍다" 까지만 주장 가능. Modulith verifier 를 도입한 사례 (kakaobank) 도 동등한 합리성을 가짐.
|
||||
- 블로그로 쓰기 전에 필요한 canonical 정제: `wiki/projects/ca-tmpl/architecture-enforcement-rules.md` 를 최신 코드 상태 (모듈 매트릭스, ArchUnit rule 12종 이름) 로 맞춘 뒤 verified 항목만 blog 초안으로 이동.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/clean-architecture-package-layout.md` 에 Gradle + ArchUnit boundary enforcement 글감으로 반영한다.
|
||||
- 다음 단계: runtime lookup PoC / MapStruct exemption 같은 planned 항목은 blogify 전 과장 금지로 유지한다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/feature-architecture-enforcement-rules]], [[raw/branch-notes/feature-skeleton-package-blueprint-contract]], [[raw/branch-notes/feature-application-port-usecase-contract]] (자매 결정).
|
||||
- 관련 errors: [[raw/errors/archunit-empty-should-anchor-2026-05-27]], [[raw/errors/gradle-wrapper-readonly-cache-2026-05-28]], [[raw/errors/archunit-test-scope-sample-ticket-inclusion-2026-05-28]].
|
||||
- 관련 interview prep: [[raw/interviews/clean-architecture-boundary-enforcement]], [[raw/interviews/clean-architecture-module-blueprint]] (자매 질문), [[raw/interviews/transaction-port-vs-spring-transactional]] (`@Transactional` 다수파 vs 소수파 trade-off 단락의 자매).
|
||||
- 관련 blog topics: [[raw/blog-topics/clean-architecture-module-blueprint-2026-05-28]] (자매 글감), [[raw/blog-topics/transaction-port-abstraction-over-spring-transactional-2026-05-28]] (다수파/소수파 trade-off 글감), [[raw/blog-topics/post-implementation-knowledge-capture-workflow-2026-05-28]] (캡처 워크플로우 자매).
|
||||
- derived blog: 생성 전. 생성 시 `wiki/blog/clean-architecture-boundary-enforcement-YYYY-MM-DD.md` 후보.
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/clean-architecture-module-blueprint-2026-05-28.md
|
||||
@@ -0,0 +1,127 @@
|
||||
---
|
||||
title: blog-topic / clean-architecture-module-blueprint-2026-05-28
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-skeleton-package-blueprint-contract]
|
||||
related_projects: [ca-skeleton]
|
||||
tags: [blog-topic, ca-skeleton, architecture, gradle, clean-architecture, hexagonal, multi-module]
|
||||
created: 2026-05-28
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: clean-architecture-module-blueprint-2026-05-28
|
||||
|
||||
> Layer: `raw/blog-topics/` — 채용공고가 아닌 작업·학습·트러블슈팅에서 나온 블로그 글감 원석. canonical 정제 전 raw 후보이며, `wiki/blog/` 직접 생성 근거가 아니다.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-skeleton-package-blueprint-contract]] — Clean Architecture skeleton 의 package/module blueprint 를 single-module feature-first 에서 Gradle multi-module Hexagonal boundary 로 _수정_ 한 결정 (Decisions D1~D8 + Default Module Blueprint tree).
|
||||
- [[raw/project-notes/ca-skeleton-operational-contract]] — ca-tmpl skeleton 의 운영 계약 SSOT (§20 Skeleton Blueprint Contract 영역).
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-05-27
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-skeleton-package-blueprint-contract]] — 초기 single-module feature-first 결정 (`결정 사항 2026-05-22`) 을 `결정 사항 2026-05-27` 에서 Gradle multi-module Hexagonal 로 _명시적으로 수정_ 한 점이 글감의 핵심 사건.
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: Clean Architecture skeleton 은 패키지 이름을 예쁘게 나누는 것만으로는 부족하다. **Gradle module boundary 가 1차 강제선, package 내부 책임이 2차 분류** 가 되어야 새 도메인 기능이 들어와도 경계가 무너지지 않는다.
|
||||
- 떠오른 계기: ca-tmpl 의 초기 결정이 single-module feature-first 였다가 우아한형제들 / 카카오뱅크 사례 검토 후 multi-module Hexagonal 로 _명시적으로 수정_ 된 과정 — 의사결정의 _뒤집힘_ 자체가 글감.
|
||||
- 예상 제목 후보:
|
||||
- Clean Architecture 템플릿에서 package 이름보다 먼저 정해야 할 것 — module boundary
|
||||
- 우리는 왜 single-module feature-first 에서 multi-module Hexagonal 로 _바꿨나_
|
||||
- reference code 를 production 에서 빼고 `sample-ticket` 으로 격리한 이유
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
> 각 항목은 branch-note Decision ID 또는 외부 source claim ID 로 근거를 인용한다.
|
||||
|
||||
- 사실 후보:
|
||||
- ca-tmpl 의 기본 module 은 `app-bootstrap`, `domain-core`, `application-core`, `adapter-web`, `adapter-persistence`, `adapter-outbound`, `shared-contract`, `sample-ticket` 8개. module boundary 가 1차 강제선이고 module 내부 package 는 2차 책임 분류 — 근거: `feature-skeleton-package-blueprint-contract.md` D1 (Phase C2 기본 구조 = Gradle multi-module + Clean Architecture / Hexagonal), `결정 사항 2026-05-27` ("module boundary 가 1차 강제선이고, module 내부 package 는 2차 책임 분류"). 외부 근거: `raw/company-tech-blogs/woowahan-hexagonal-multimodule.md#WW-HEX-C1`, `raw/company-tech-blogs/modulith-kakaobank-techblog-2025.md#KAKAOBANK-MOD-C4`.
|
||||
- `domain-core` 는 framework-neutral POJO 로 유지. Spring annotation, JPA annotation, HTTP DTO 를 모두 모름 — 근거: `feature-skeleton-package-blueprint-contract.md` D2 (domain-core = framework-neutral). 외부 근거: `raw/company-tech-blogs/woowahan-hexagonal-multimodule.md#WW-HEX-C1`, `raw/official-docs/arch-clean-architecture-uncle-bob.md` (Dependency Rule).
|
||||
- `application-core` 는 `domain-core` 와 `shared-contract` 에만 의존. adapter 구현체 / Spring Web / JPA / Redis / Kafka / outbound HTTP client 모두 adapter 밖으로 들어오면 안 됨 — 근거: `feature-skeleton-package-blueprint-contract.md` D3 (application-core = domain + shared only). 외부 근거: `raw/company-tech-blogs/woowahan-hexagonal-multimodule.md#WW-HEX-C2`, `raw/company-tech-blogs/modulith-kakaobank-techblog-2025.md#KAKAOBANK-MOD-C4`.
|
||||
- `shared-contract` 는 response envelope / error code / header / MDC / metric / registry / annotation 같은 _skeleton-wide operational contract_ 만 허용. business / domain concept 는 금지 — 근거: `feature-skeleton-package-blueprint-contract.md` D6 (shared-contract = operational contract only). 외부 근거: `raw/official-docs/feature-first-uncle-bob-screaming-architecture-2011.md#SCREAM-C1`.
|
||||
- `sample-ticket` 은 fixture/sample consumer 이며 production module 이 import 하거나 dependency 로 선언하면 실패 — 근거: `feature-skeleton-package-blueprint-contract.md` D7 (sample-ticket production 역수입 금지) + `feature-architecture-enforcement-rules.md` D7 (자매 ArchUnit rule). 외부 근거: ca-tmpl 자체 결정 (project-decision).
|
||||
- 초기 결정 (single-module feature-first) 은 2026-05-22, 수정 결정 (Gradle multi-module + Clean Architecture / Hexagonal) 은 2026-05-27 — 근거: `feature-skeleton-package-blueprint-contract.md` §결정 사항 ("2026-05-22: 초기 문서의 기본 구조는 single-module feature-first package layout이었다", "2026-05-27: Phase C2 기본 구조는 ... 로 수정한다").
|
||||
- 경험 후보:
|
||||
- 기존 reference code (blog domain) 를 production module 에서 `sample-ticket/src/main/java/dev/caskeleton/sample/ticket/...` 로 격리. production module 은 `package-info.java` + skeleton anchor 중심으로 정리 — 근거: `feature-skeleton-package-blueprint-contract.md` Closure §`actually-implemented` "기존 reference code는 production module에서 `sample-ticket` 내부 `dev.caskeleton.sample.ticket.*` package로 격리됨".
|
||||
- production package root 를 `dev.caskeleton` 으로 rename + `BlogApplication` → `CaSkeletonApplication` + `blog.*` 설정 prefix → `ca-skeleton.*` 전환 — 근거: 동일 Closure 항목.
|
||||
- 빈 skeleton anchor module 이 ArchUnit empty should failure 를 일으켜 `allowEmptyShould(true)` 를 _빈 상태가 의도된 rule 에만_ 선별 적용 — 근거: 파생 에러 [[raw/errors/archunit-empty-should-anchor-2026-05-27]] §해결 ("빈 상태가 skeleton contract상 유효한 rule에만 `allowEmptyShould(true)`").
|
||||
- `sample-ticket` 격리 후 sample 내부 `GlobalExceptionHandler` 가 `InvalidBearerTokenException` 을 import 하지만 sample build.gradle 에 `spring-boot-starter-oauth2-resource-server` 가 없어서 compile 실패 → starter 명시 추가 — 근거: 파생 에러 [[raw/errors/sample-ticket-oauth2-resource-server-dependency-2026-05-27]] §해결.
|
||||
- 의견 / 해석 후보:
|
||||
- 템플릿 프로젝트의 완성도 기준은 "예시 도메인이 잘 돈다" 가 아니라 **"예시를 통째로 들어내도 경계가 남는다"** 이다. ca-tmpl 의 `sample-ticket` 격리는 이 기준의 직접 검증.
|
||||
- `common` / `shared` 모듈은 _편의_ 보다 _오염 방지 규칙_ 을 먼저 가져야 한다. ca-tmpl 의 `shared-contract` 는 8개 sub-package allowlist (`response/error/headers/logging/tracing/metrics/registry/annotation`) 로 명시 제한.
|
||||
- **single-module feature-first 도 작은 프로젝트엔 합리적**이다. ca-tmpl 이 multi-module 을 택한 건 _template repository 라서_ 새 프로젝트가 시작될 때 경계가 흐트러지지 않도록 학습 비용을 미리 흡수한다는 결정 — 근거: `feature-skeleton-package-blueprint-contract.md` D8 Open Risk ("작은 프로젝트에서는 single-module이 비용이 낮을 수 있음. ca-tmpl은 skeleton template이므로 boundary 학습/검증 비용을 감수").
|
||||
- **company-tech-blog 사례 (우아한형제들 / 카카오뱅크) 는 best practice 가 아니라 case study** 다. 본 글에서 이 두 사례를 인용하되 "공식 표준" 으로 승격하지 않는 정직함이 중요 — 근거: `feature-skeleton-package-blueprint-contract.md` Decision Evidence Map 의 Evidence Strength 컬럼 (`company-case-study`).
|
||||
|
||||
## Outline seed
|
||||
|
||||
> 각 섹션 옆에 `→ 핵심 메시지` 를 함께 명시한다.
|
||||
|
||||
1. 의사결정의 _뒤집힘_ — 2026-05-22 의 single-module feature-first 결정을 2026-05-27 에 명시적으로 수정한 과정 → **template 의 첫 결정도 case study 검토 뒤 뒤집을 수 있다는 정직함.**
|
||||
2. module boundary 가 _1차_ 강제선인 이유 — package convention 만으로는 import 가 자유롭다 → **Gradle dependency graph 가 컴파일 단계에서 위반을 막는다.**
|
||||
3. 8개 module 의 책임 — `domain-core`, `application-core`, `adapter-{web,persistence,outbound}`, `shared-contract`, `sample-ticket`, `app-bootstrap` → **dependency direction 표 + 각 모듈의 forbidden import 매트릭스.**
|
||||
4. `shared-contract` 를 좁게 잡는 이유 — 8개 sub-package allowlist (`response/error/headers/...`) → **business common dumping ground 방지가 _편의_ 보다 우선.**
|
||||
5. `sample-ticket` 격리 — production module 의 ArchUnit rule + Gradle dependency rule + 별도 `*Application` 가 _없음_ → **"예시를 들어내도 경계가 남는다" 가 template repository 의 완성도 기준.**
|
||||
6. 구현 중 드러난 작은 실패들 — 빈 anchor 의 `allowEmptyShould` 선별 적용 + sample-ticket compile classpath 누락 → **template repository 의 "비어 있음" 은 의도된 상태일 수 있다.**
|
||||
7. 다른 선택지의 정직한 비교 — single-module / layer-first / pure hexagonal / Spring Modulith → **ca-tmpl 의 선택이 _유일한 정답_ 이 아니라 _이 맥락에서의 최적_ 임을 명시.**
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/clean-architecture-package-layout.md` 후보:
|
||||
- 실제 적용된 8 module + 각 모듈의 sub-package 책임 트리 (`feature-skeleton-package-blueprint-contract.md` §Default Module Blueprint).
|
||||
- dependency direction 매트릭스 (`Module Dependency Rule` 표).
|
||||
- `dev.caskeleton` 으로의 package rename + `CaSkeletonApplication` / `BootstrapSettings` / `ca-skeleton.*` 설정 prefix 전환.
|
||||
- local verification 결과 4종 (`./gradlew test`, `verifyCleanArchitectureDependencies`, `:app-bootstrap:test --tests '*CleanArchitectureTest'`, `:adapter-web:test --tests '*SettingsTest'`).
|
||||
- `wiki/concepts/clean-architecture-package-layout.md` 후보:
|
||||
- multi-module Hexagonal-inspired skeleton layout 의 일반화 원칙 (project-agnostic).
|
||||
- "module boundary 1차, package convention 2차" 분업 원칙.
|
||||
- `shared` 모듈을 좁게 잡는 _operational contract only_ rule.
|
||||
- "예시를 들어내도 경계가 남는다" 의 template completeness 기준.
|
||||
- 필요한 추가 검증:
|
||||
- canonical 문서가 최신 코드 상태 (`dev.caskeleton`, `sample-ticket` 격리, Spring Boot 3.5.14, 새로 추가된 `feature-application-port-usecase-contract` 의 `application-core` 패키지 구조) 까지 반영하는지.
|
||||
- Spring Modulith named interface 를 후속 도입했을 때 Gradle multi-module + ArchUnit 구성과의 중복/대체 관계.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-skeleton-package-blueprint-contract]] — 결정 D1~D8, Default Module Blueprint tree, Module Dependency Rule 표, Closure §`actually-implemented` / `locally-verified`.
|
||||
- [[raw/branch-notes/feature-architecture-enforcement-rules]] — 자매 결정 D1~D10. 본 글의 ArchUnit 단락 근거.
|
||||
- [[raw/branch-notes/feature-application-port-usecase-contract]] — `application-core` package 구조의 후속 결정 (D1: inbound `*UseCase` / outbound `*Port` naming). canonical 정제 시 통합 필요.
|
||||
- [[raw/errors/archunit-empty-should-anchor-2026-05-27]] — 빈 anchor 와 `allowEmptyShould(true)` 선별 적용.
|
||||
- [[raw/errors/sample-ticket-oauth2-resource-server-dependency-2026-05-27]] — sample 격리 후 compile dependency 누락.
|
||||
- [[raw/interviews/clean-architecture-module-blueprint]] — 같은 작업에서 파생된 예상 면접 질문.
|
||||
- [[raw/interviews/shared-contract-and-sample-isolation]] — shared/sample 책임 경계 예상 질문.
|
||||
- [[raw/company-tech-blogs/woowahan-hexagonal-multimodule]] — Domain / Application / Framework / Bootstrap 4-module 격리 사례 (`WW-HEX-C1`, `WW-HEX-C2`).
|
||||
- [[raw/company-tech-blogs/modulith-kakaobank-techblog-2025]] — Gradle multi-module + Hexagonal + Spring Modulith 사례 (`KAKAOBANK-MOD-C2`, `KAKAOBANK-MOD-C4`).
|
||||
- [[raw/official-docs/hexagonal-cockburn-wikipedia-summary]] — Ports & Adapters 원형 (`HEX-WIKI-C5`).
|
||||
- [[raw/official-docs/arch-clean-architecture-uncle-bob]] — Dependency Rule 의 클래식 근거 (`engineering-blog`, official standard 아님).
|
||||
- [[raw/official-docs/feature-first-uncle-bob-screaming-architecture-2011]] — feature/use-case 가 framework 위에 드러나야 한다는 사상 (`SCREAM-C1`).
|
||||
- [[raw/official-docs/hexagonal-thombergs-buckpal-github]] — feature/package 내부 port-adapter 책임 분리 참고.
|
||||
- [[raw/official-docs/layer-first-baeldung-clean-architecture-spring-boot]] — _대안 1: layer-first_ 의 입문형 사례.
|
||||
- [[raw/official-docs/onion-palermo-original-2008]] — _대안 4: onion_ 의 원형.
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: ca-tmpl 의 8 module 구조가 _실제 사업 도메인_ 이 들어왔을 때 module 분할 또는 새 adapter (e.g., `adapter-messaging`) 추가가 자연스럽게 가능한지. 현재는 reference (sample-ticket) 만 검증.
|
||||
- 아직 확인해야 할 사실: Spring Modulith 의 named interface 검증을 추가하면 ArchUnit rule 중 어느 것이 _중복_ 이고 어느 것이 _보완_ 인지.
|
||||
- 과장하면 안 되는 부분: 본 글의 모든 검증은 **`locally-verified`** 이며 prod 운영 검증 없음. "운영에서 검증됐다" 라는 표현 금지.
|
||||
- 과장하면 안 되는 부분: 우아한형제들 / 카카오뱅크 사례는 _case study_ 다. "대기업에서 표준" 또는 "industry standard" 같은 표현으로 격상시키지 말 것 — `feature-skeleton-package-blueprint-contract.md` Decision Evidence Map Open Risk 컬럼이 이 한계를 명시.
|
||||
- 블로그로 쓰기 전에 필요한 canonical 정제: `wiki/projects/ca-tmpl/clean-architecture-package-layout.md` 를 _최신 코드 상태_ (특히 `feature-application-port-usecase-contract` 작업으로 추가된 `application-core` 패키지 구조) 까지 반영한 뒤 verified 항목만 글로 이동.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/clean-architecture-package-layout.md` 에 module/package blueprint 글감으로 반영했다.
|
||||
- 다음 단계: source canonical이 `verified` 상태이므로 이후 `blogify` 대상으로 삼을 수 있다. 단 운영 검증이나 전체 Phase C2 완료처럼 쓰지 않는다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/feature-skeleton-package-blueprint-contract]], [[raw/branch-notes/feature-architecture-enforcement-rules]] (자매 — boundary 강제), [[raw/branch-notes/feature-application-port-usecase-contract]] (후속 — application 내부 패키지 구조).
|
||||
- 관련 errors: [[raw/errors/archunit-empty-should-anchor-2026-05-27]], [[raw/errors/sample-ticket-oauth2-resource-server-dependency-2026-05-27]].
|
||||
- 관련 interview prep: [[raw/interviews/clean-architecture-module-blueprint]], [[raw/interviews/shared-contract-and-sample-isolation]], [[raw/interviews/clean-architecture-boundary-enforcement]].
|
||||
- 관련 blog topics: [[raw/blog-topics/clean-architecture-boundary-enforcement-2026-05-28]] (자매 글감 — boundary 강제), [[raw/blog-topics/transaction-port-abstraction-over-spring-transactional-2026-05-28]] (자매 글감 — application 의 framework 격리).
|
||||
- derived blog: 생성 전. 생성 시 `wiki/blog/clean-architecture-module-blueprint-YYYY-MM-DD.md` 후보.
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/clean-architecture-reference-project-adoption-2026-06-17.md
|
||||
@@ -0,0 +1,86 @@
|
||||
---
|
||||
title: blog-topic / clean-architecture-reference-project-adoption
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-sample-removal-adoption-contract]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, architecture, testing, clean-architecture, ddd, api-contract]
|
||||
created: 2026-06-17
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: clean-architecture-reference-project-adoption
|
||||
|
||||
> Layer: `raw/blog-topics/` — 채용공고가 아닌 작업·학습·트러블슈팅에서 나온 블로그 글감 원석. canonical 정제 전 raw 후보이며, 바로 `wiki/blog/` 로 승격하지 않는다.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-sample-removal-adoption-contract]] — 레퍼런스 프로젝트 비교와 sample/adoption 계약 정리 과정에서 나온 글감.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-06-17
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-sample-removal-adoption-contract]]
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: Clean Architecture 스켈레톤을 고도화할 때 레퍼런스 프로젝트를 그대로 베끼지 않고, 계약 검증·event reliability·bounded context·도메인 모델링·운영 도구로 분해해 흡수하는 방법.
|
||||
- 예상 제목 후보:
|
||||
- Clean Architecture 템플릿을 레퍼런스 프로젝트로 고도화하는 법
|
||||
- 여러 DDD/Hexagonal 프로젝트에서 스켈레톤에 흡수할 것과 버릴 것
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- ca-tmpl 은 현재 module dependency gate, outbox relay, OpenAPI snapshot, env/one-type verification, runbook/registry 기반 운영 계약을 이미 갖고 있다. — 근거 후보: [[raw/branch-notes/feature-sample-removal-adoption-contract]], `docs/superpowers/plans/2026-06-17-reference-project-adoption.md`
|
||||
- 조사 대상 레퍼런스들은 contract verification, acceptance-test, saga/outbox/inbox, modular monolith, domain modeling, observability, generator 측면에서 서로 다른 강점을 갖는다. — 근거 후보: [[raw/branch-notes/feature-sample-removal-adoption-contract]]
|
||||
- 경험 후보:
|
||||
- 로컬 README/구조/대표 구현을 evidence matrix 로 나누고, 그대로 흡수 금지 항목을 별도 표로 분리했다. — 근거 후보: [[raw/branch-notes/feature-sample-removal-adoption-contract]]
|
||||
- 의견/해석 후보:
|
||||
- 스켈레톤 프로젝트는 기능을 많이 담는 것보다, 새 프로젝트가 안전하게 확장할 수 있는 검증 가능한 seam 을 제공하는 편이 실무에 더 가깝다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. 레퍼런스 프로젝트를 그대로 복제하면 생기는 문제 — stack drift, layer rule 충돌, sample 과 production 의 혼동을 설명한다.
|
||||
2. 흡수 후보를 기능 축으로 재분류하기 — contract, event reliability, bounded context, domain modeling, ops/tooling 으로 나눈다.
|
||||
3. ca-tmpl 에 먼저 적용할 P0 — contract verification, acceptance-test module, consumer inbox/dedupe 가 왜 가장 효과적인지 정리한다.
|
||||
4. 보류해야 할 것들 — Spring Modulith, WebFlux, chaos, generator 는 optional spike 로 두는 이유를 적는다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/reference-project-adoption.md` 후보:
|
||||
- ca-tmpl 에 실제로 적용한 레퍼런스 흡수 전략과 검증 결과.
|
||||
- `wiki/concepts/clean-architecture-template-evolution.md` 후보:
|
||||
- Clean Architecture 템플릿을 진화시킬 때 레퍼런스를 평가하는 일반 기준.
|
||||
- 필요한 추가 검증:
|
||||
- Phase 1~2 구현 후 실제 테스트/빌드 결과.
|
||||
- Spring Cloud Contract, Springwolf, Spring Modulith 의 ca-tmpl 현재 스택 호환성.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-sample-removal-adoption-contract]] — 레퍼런스 흡수와 sample/adoption 계약 정리 방향.
|
||||
- `docs/superpowers/plans/2026-06-17-reference-project-adoption.md` — 레퍼런스 프로젝트 evidence matrix 와 적용 plan.
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: 각 P0/P1 항목은 구현 전 focused audit 과 dependency compatibility 확인이 필요하다.
|
||||
- 과장하면 안 되는 부분: 현재 상태는 `documented-only` 계획이며, contract/inbox/acceptance-test 구현이 완료된 것이 아니다.
|
||||
- 블로그로 쓰기 전에 필요한 canonical 정제: 실제 Phase 1 또는 Phase 2 구현 결과와 검증 로그를 `wiki/projects/ca-tmpl/...` 로 승격해야 한다.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/sample-fixture-and-adoption.md` 에 reference project adoption / sample-adoption 전략 글감으로 반영한다.
|
||||
- 다음 단계: 정확성 감사에서 발견된 결함을 반영한 계획만 blogify 근거로 사용한다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/feature-sample-removal-adoption-contract]]
|
||||
- 관련 error: 없음
|
||||
- 관련 interview prep: 없음
|
||||
- derived blog: 생성 전. 생성 시 `wiki/blog/clean-architecture-reference-project-adoption-2026-06-17.md` 후보
|
||||
- 정확성 감사 (2026-06-19): 이 글감의 근거가 된 계획 문서의 README 라인 인용/장점 요약을 19개 원본 프로젝트와 대조한 결과 — 13개 정확, 6개 결함(library "domain/integration event 구분"은 미구현 placeholder인 phantom 장점; dddsample-core/food-ordering는 장점 실재하나 인용 라인 오류; 라인-정밀도 결함 묶음). 산출물: `ca-tmpl/docs/superpowers/specs/2026-06-19-reference-project-adoption-accuracy-report.md`. **블로그로 승격 시 위 결함이 수정된 계획을 근거로 삼을 것** — 미수정 인용을 그대로 인용하면 글의 사실성이 깨진다.
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/contract-registry-schema-owner-vs-row-owner-gate-2026-06-20.md
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
title: blog-topic / contract-registry-schema-owner-vs-row-owner-gate-2026-06-20
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-contract-registry-governance]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, registry, governance, contract, yaml, test, ownership]
|
||||
created: 2026-06-20
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: contract-registry-schema-owner-vs-row-owner-gate-2026-06-20
|
||||
|
||||
> Layer: `raw/blog-topics/` — 채용공고가 아닌 작업·학습에서 나온 블로그 글감 원석. canonical 정제 전 raw 후보이며, `wiki/blog/` 직접 생성 근거가 아니다.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-contract-registry-governance]] — 본 branch 의 schema-owner 게이트(`ContractRegistrySchemaGovernanceTest`) 구현(Phase C2, 2026-06-20)에서 추출.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 7개 contract registry(error/env/secrets/header/mdc/metric/capability)의 **schema** 를 한 branch 가 소유하되 **row 값** 은 8개 sibling branch 에 위임하는 구조에서, "schema 가 실제로 강제되는가" 를 기계로 증명하려 했다. 기존 테스트는 전부 *단일 registry 의 값/enum drift* 만 봤고, **registry 들이 공통 schema 를 따르는지** 를 보는 테스트는 없었다.
|
||||
|
||||
## 글감 코어 / Core idea
|
||||
|
||||
- **문제 분리(separation of ownership)**: registry governance 에는 두 종류의 소유권이 있다 — (a) *schema owner*: 어떤 column 이 있어야 하는가(구조·저장 형식·변경 절차), (b) *row owner*: 어떤 code/key/name 값이 존재하는가. 이 둘을 한 테스트로 섞으면 위임이 깨진다. ca-skeleton 은 7 registry 의 schema 를 단일 branch 가, 값은 8 sibling 이 소유.
|
||||
- **schema 게이트의 단언 집합**: ① N family 존재(파일 부재 = 누락 = FAIL, silent skip 아님) ② 각 파일의 `# Schema owner:` 헤더 ③ 모든 row 의 identity + 위임 포인터(`owner_branch`) ④ full row 의 universal contract column(`compatibility_impact` legal enum + `required_test` = "모든 registry 항목은 최소 1개 contract test 와 연결") ⑤ **문서화된 면제**(reference row)의 명시적 검증.
|
||||
- **면제를 검증 가능하게**: 한 registry(secrets)는 다른 registry(env-keys)로 값을 위임하는 *reference row* 를 둔다. 이들은 contract column 을 생략한다. 게이트가 이를 그냥 skip 하면 "면제" 와 "누락" 을 구분 못한다 → reference row 는 `reference:` target 보유를 별도 단언. (이 함정의 디버그 기록: [[raw/errors/contract-registry-reference-row-universal-column-false-fail-2026-06-20]])
|
||||
- **gitignored seed 위의 테스트 이중 모드**: registry SSOT 가 `/docs`(gitignore)에 있어 CI/fresh checkout 엔 부재. 부재 → `Assumptions.assumeTrue` 로 SKIP(거짓 green 아님), 존재 → 위반 hard FAIL. "데이터가 없으면 통과" 가 아니라 "데이터가 없으면 검사 안 함, 있으면 엄격" 이 정직한 drift gate 의 기본형.
|
||||
- **검증(게이트의 실효성 증명)**: seed 로 6 tests green 만으로는 vacuous 일 수 있다 → 음성 변이(illegal `compatibility_impact` 주입 → 해당 단언 FAIL → 원복) 로 게이트가 실제로 막는지 증명. "green 한 번" 이 아니라 "틀린 데이터에 red" 까지 봐야 신뢰.
|
||||
|
||||
## 왜 의미 있나 / Why it matters
|
||||
|
||||
- "문서로만 있는 거버넌스 규칙은 쉽게 깨진다" 를 fitness function 으로 메우는 스켈레톤 가치의 구체 사례 — 단, 이번엔 *코드 구조* 가 아니라 *데이터 계약(registry yaml)* 자체가 대상.
|
||||
- 멀티-owner registry 에서 "schema vs row" 소유권 분리는 monorepo/플랫폼 팀에서 흔한 구조(공통 schema 팀 + 도메인 팀). 그 경계를 테스트로 박제하는 패턴은 이식성이 높다.
|
||||
- 한계(글에서 솔직히 명시할 것): 이 게이트는 *artifact 가 schema 를 따르는가* 만 본다. "registry 에 없는 token 이 코드에 등장하는가" 의 정적 탐지(ArchUnit custom rule)는 별개 PoC 로 미구현(`planned`). 즉 schema 정합 ≠ token 사용 강제.
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: contract registry governance에서는 schema owner와 row owner를 분리하고, schema gate가 면제 row까지 명시적으로 검증해야 drift를 줄일 수 있다.
|
||||
- 예상 제목 후보:
|
||||
- Registry schema owner와 row owner를 나눈 이유
|
||||
- YAML registry governance를 테스트로 고정하기
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- registry schema와 row 값은 서로 다른 owner가 가질 수 있다.
|
||||
- reference row는 full row와 다른 검증 경로가 필요하다.
|
||||
- 의견/해석 후보:
|
||||
- schema 정합과 token 사용 강제는 다른 gate이며 같은 테스트로 섞으면 소유권이 흐려진다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. schema owner와 row owner의 책임을 분리한다.
|
||||
2. universal contract column과 reference row 면제 검증을 설명한다.
|
||||
3. schema gate의 한계와 runtime token 강제의 별도 owner를 구분한다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/skeleton-governance-registry-verification-test-scorecard.md` 후보:
|
||||
- registry schema owner vs row owner gate 글감.
|
||||
- 필요한 추가 검증:
|
||||
- 실제 `ContractRegistrySchemaGovernanceTest`와 negative mutation 검증 여부.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-contract-registry-governance]]
|
||||
- [[raw/branch-notes/feature-contract-verification-test-suite]]
|
||||
- [[raw/errors/contract-registry-reference-row-universal-column-false-fail-2026-06-20]]
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: schema gate 구현과 negative mutation 검증이 현재 코드에 남아 있는지.
|
||||
- 과장하면 안 되는 부분: schema 정합을 token 사용 강제나 runtime verification으로 확대하지 않는다.
|
||||
|
||||
## 관련 / Related
|
||||
|
||||
- [[raw/branch-notes/feature-contract-registry-governance]]
|
||||
- [[raw/branch-notes/feature-contract-verification-test-suite]] — runtime token 강제(11 release-blocking gates) 를 소유하는 sibling. 본 글감의 "schema 정합 ≠ token 사용 강제" 경계의 반대편.
|
||||
- [[raw/errors/contract-registry-reference-row-universal-column-false-fail-2026-06-20]]
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/skeleton-governance-registry-verification-test-scorecard.md` 에 contract registry schema owner vs row owner gate 글감으로 반영했다.
|
||||
- 다음 단계: target canonical이 아직 `draft` 이므로 `blogify` 전 registry schema gate 구현·negative mutation 검증 여부를 재확인한다.
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/contract-verification-suite-release-gates-2026-07-02.md
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
title: blog-topic / contract-verification-suite-release-gates
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-contract-verification-test-suite]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, testing, ci-cd, junit5, api-contract, static-analysis]
|
||||
created: 2026-07-02
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: contract-verification-suite-release-gates
|
||||
|
||||
> Layer: `raw/blog-topics/` — 작업 중 나온 블로그 글감 원석. 최종 블로그는 canonical 정제 후 `wiki/blog/`에서 작성한다.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-contract-verification-test-suite]] — release-blocking contract suite 구현과 검증에서 나온 글감.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-07-02
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-contract-verification-test-suite]]
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: skeleton의 운영 계약을 문서로만 두지 않고 release-blocking test suite로 묶은 이유와 설계 단위를 정리한다.
|
||||
- 예상 제목 후보:
|
||||
- 운영 계약을 release gate로 바꾸는 방법
|
||||
- Clean Architecture skeleton에서 contract verification suite를 둔 이유
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- 11개 release-blocking gate, ApprovalTests/OpenAPI snapshot, optional adapter skip proof, masking regex 함정이 branch에 정리되어 있다 — 근거 후보: [[raw/branch-notes/feature-contract-verification-test-suite]] D6/D7 line `:134-136`, 구현 `:104-107`, blog seeds `:349-354`.
|
||||
- 경험 후보:
|
||||
- registry drift, ArchUnit isolation, OpenAPI committed snapshot은 각각 다른 실패 모드를 잡기 때문에 하나의 "품질 테스트"로 뭉개기 어렵다.
|
||||
- 의견/해석 후보:
|
||||
- skeleton project의 핵심은 기능 수가 아니라 복제 후 깨지면 안 되는 계약을 실행 가능하게 만드는 데 있다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. contract verification은 단일 테스트가 아니다 — registry, API snapshot, architecture rule, adapter skip proof가 서로 다른 drift를 본다.
|
||||
2. release-blocking과 advisory check를 구분해야 한다 — 모든 검사를 같은 severity로 두면 운영이 어려워진다.
|
||||
3. snapshot nondeterminism도 계약 설계의 일부다 — 재현 가능한 출력이 있어야 gate가 신뢰된다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/skeleton-governance-registry-verification-test-scorecard.md` 후보:
|
||||
- ca-tmpl contract verification suite 구현 사실.
|
||||
- `wiki/concepts/skeleton-governance-registry-verification-test-scorecard.md` 후보:
|
||||
- registry, verification test, scorecard 일반 개념.
|
||||
- 필요한 추가 검증:
|
||||
- 11개 gate의 목록, release-blocking 여부, 실제 CI/Gradle 연결 상태.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-contract-verification-test-suite]] — release-blocking suite와 blog seed 근거.
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: hosted CI에서 gate가 실제 release를 차단한 사례가 있는지.
|
||||
- 과장하면 안 되는 부분: local verification과 hosted CI/prod evidence를 섞으면 안 된다.
|
||||
- 블로그로 쓰기 전에 필요한 canonical 정제: project 문서의 gate matrix와 evidence grade 보강.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/skeleton-governance-registry-verification-test-scorecard.md` 에 verification suite/release gate 글감으로 반영했다.
|
||||
- 다음 단계: target canonical이 아직 `draft` 이므로 `blogify` 전 review/verify가 필요하다. local verification과 hosted CI/prod evidence를 섞지 않는다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/feature-contract-verification-test-suite]]
|
||||
- 관련 error:
|
||||
- 관련 interview prep:
|
||||
- derived blog: 생성 전. 생성 시 `wiki/blog/contract-verification-suite-release-gates-YYYY-MM-DD.md` 후보
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/digest-first-java-release-pipeline-2026-06-21.md
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
title: blog-topic / digest-first-java-release-pipeline
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-build-release-supply-chain-contract]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, ci-cd, gradle, docker, supply-chain, reproducible-builds]
|
||||
created: 2026-06-21
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: digest-first-java-release-pipeline
|
||||
|
||||
> Layer: `raw/blog-topics/` — 실제 Java 21/Gradle 멀티모듈 release pipeline 구현에서 나온 글감 원석.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-build-release-supply-chain-contract]] — digest-first build/sign/verify/promotion과 rollback audit를 구현한 작업.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-06-21
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-build-release-supply-chain-contract]].
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: 재현 가능한 JAR부터 digest-bound SBOM·Cosign·SLSA 검증과 rollback manifest까지 하나의 release-blocking DAG로 묶어야 mutable tag가 공급망 SSOT가 되는 일을 막을 수 있다.
|
||||
- 예상 제목 후보:
|
||||
- Java 릴리스를 태그가 아니라 Digest로 승격하는 공급망 파이프라인
|
||||
- Gradle Lock부터 SLSA까지: 재빌드 없는 컨테이너 릴리스 설계
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- Gradle archive timestamp/order/mode와 JDK pin을 고정한 두 clean build에서 같은 SHA-256을 얻었다. 근거: [[raw/branch-notes/feature-build-release-supply-chain-contract]] D10, §구현 결과.
|
||||
- Cosign signer identity/issuer와 SLSA exact builder ID 검증을 통과한 digest만 version tag로 promotion하도록 DAG를 배선했다. 근거: 같은 branch D4, D7, D12, D13.
|
||||
- 경험 후보:
|
||||
- `dependencies` report가 lock 누락을 출력하고도 exit 0인 fail-open을 발견해 실제 resolution task와 negative lock-drift test로 교체했다. 근거: 같은 branch §마주친 문제.
|
||||
- rollback audit가 release asset 이름뿐 아니라 manifest의 source revision/image digest와 GHCR digest 일치까지 검증하도록 보강했다. 근거: 같은 branch D11, §구현 결과.
|
||||
- 의견/해석 후보:
|
||||
- 공급망 파이프라인의 핵심은 도구 수가 아니라, immutable identity가 모든 gate와 rollback 경로를 관통하도록 만드는 것이다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. Tag-only release의 빈틈 — mutable tag와 재빌드가 검증 대상/배포 대상의 동일성을 깨뜨린다.
|
||||
2. Build contract — strict dependency locks, SemVer+sha, reproducible archives, pinned JDK/base digest로 입력을 닫는다.
|
||||
3. Evidence DAG — High/Critical scan, SPDX SBOM, Cosign identity, SLSA exact builder를 promotion 전에 fan-in한다.
|
||||
4. Promotion과 rollback — 검증된 digest를 재빌드 없이 tag하고 manifest/SBOM/GHCR digest retention을 audit한다.
|
||||
5. 검증의 정직한 경계 — local fixture와 live GitHub OIDC/Rekor/GHCR 증거를 분리한다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/build-release-supply-chain.md` 후보:
|
||||
- ca-tmpl에 실제 적용된 release DAG, lock task, manifest/retention audit.
|
||||
- `wiki/concepts/digest-first-release-promotion.md` 후보:
|
||||
- immutable digest 중심 verification/promotion/rollback 일반 패턴.
|
||||
- 필요한 추가 검증:
|
||||
- GitHub release candidate tag로 OIDC/Rekor/GHCR live pipeline 실행.
|
||||
- generated SBOM과 Gradle resolved dependency 비교.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-build-release-supply-chain-contract]] — 구현 결정 D1~D13과 local evidence.
|
||||
- [[raw/errors/sandbox-build-verification-boundaries-2026-06-21]] — verification 경계와 안전한 대체.
|
||||
- [[raw/interviews/digest-first-supply-chain-release-gates]] — 설계 질문과 답변 경계.
|
||||
- [[raw/official-docs/supply-chain-cosign-keyless-sigstore]] — Cosign keyless 근거.
|
||||
- [[raw/official-docs/supply-chain-slsa-provenance-framework]] — SLSA provenance 근거.
|
||||
- [[raw/official-docs/gradle-reproducible-archives-working-with-files]] — reproducible archive 근거.
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: GitHub-hosted live provenance payload, Rekor entry, GHCR referrer retention.
|
||||
- 과장하면 안 되는 부분: local workflow/static/fixture 검증을 production release 성공으로 표현하지 않는다.
|
||||
- 블로그로 쓰기 전에 필요한 canonical 정제: branch 결과를 project/concept canonical로 승격하고 live CI evidence를 별도 등급으로 병합한다.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/devops-ci-supply-chain-dx.md` 에 digest-first release pipeline / supply-chain DAG 글감으로 반영한다.
|
||||
- 다음 단계: live release evidence 확보 전까지 production release 성공으로 표현하지 않는다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/feature-build-release-supply-chain-contract]].
|
||||
- 관련 error: [[raw/errors/sandbox-build-verification-boundaries-2026-06-21]].
|
||||
- 관련 interview prep: [[raw/interviews/digest-first-supply-chain-release-gates]].
|
||||
- derived blog: 생성 전.
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/distributed-lock-transaction-commit-boundary-2026-07-02.md
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
title: blog-topic / distributed-lock-transaction-commit-boundary
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-distributed-lock-contract]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, persistence, postgresql, distributed-lock, lock-lease, transaction-isolation]
|
||||
created: 2026-07-02
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: distributed-lock-transaction-commit-boundary
|
||||
|
||||
> Layer: `raw/blog-topics/` — 작업 중 나온 블로그 글감 원석. 최종 블로그는 canonical 정제 후 `wiki/blog/`에서 작성한다.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-distributed-lock-contract]] — distributed lock provider와 transaction commit boundary 검증에서 나온 글감.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-07-02
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-distributed-lock-contract]]
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: 분산 락에서 `lock.close()`와 DB commit 순서가 잘못 맞물리면 lost update 경계가 생기는 이유를 정리한다.
|
||||
- 예상 제목 후보:
|
||||
- 분산 락은 언제 풀어야 안전할까
|
||||
- lock close와 transaction commit 사이의 위험한 틈
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- branch에 `distributedLockProvider` owner, lock port/JdbcLockRegistry, lease/CME handling, 로컬 검증 항목이 정리되어 있다 — 근거 후보: [[raw/branch-notes/feature-distributed-lock-contract]] section+line `:36`, `:48`, `:341-345`, `:370-384`.
|
||||
- 경험 후보:
|
||||
- branch 자체에 파생 글감 seed가 여러 개 있으며, lock release와 commit boundary는 기존 raw topic에 exact duplicate가 없다.
|
||||
- 의견/해석 후보:
|
||||
- 분산 락의 correctness는 "락을 잡았다"보다 "락을 언제까지 잡고 있었는가"에 더 민감하다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. 락 획득보다 해제가 더 무섭다 — commit 전 unlock은 다른 writer에게 잘못된 신호를 줄 수 있다.
|
||||
2. transaction boundary와 lock lifecycle을 같은 그림에 놓기 — DB commit, exception, lease 만료를 함께 본다.
|
||||
3. local verification으로 증명할 수 있는 것과 없는 것 — single-node/JDBC lock registry 검증과 multi-node 운영 리스크를 분리한다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/distributed-lock-provider-contract.md` 후보:
|
||||
- ca-tmpl distributed lock provider 구현/검증 사실.
|
||||
- `wiki/concepts/distributed-lock.md` 후보:
|
||||
- lock lease, unlock timing, transaction interaction 일반 개념.
|
||||
- 필요한 추가 검증:
|
||||
- lock close/commit ordering 테스트와 lease 만료/exception path 검증.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-distributed-lock-contract]] — lock provider와 verification 근거.
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: multi-instance 환경에서 검증된 범위.
|
||||
- 과장하면 안 되는 부분: local/JDBC lock 검증을 운영 분산 환경 보장으로 표현하지 않는다.
|
||||
- 블로그로 쓰기 전에 필요한 canonical 정제: distributed lock project 문서 생성 또는 기존 data-layer 문서에 통합.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/data-layer-persistence-cache-outbound.md` 에 distributed lock transaction commit boundary 글감으로 반영했다.
|
||||
- 다음 단계: target canonical이 아직 `draft` 이므로 `blogify` 전 review/verify가 필요하다. local/JDBC lock 검증을 운영 분산 환경 보장으로 표현하지 않는다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/feature-distributed-lock-contract]]
|
||||
- 관련 error:
|
||||
- 관련 interview prep:
|
||||
- derived blog: 생성 전. 생성 시 `wiki/blog/distributed-lock-transaction-commit-boundary-YYYY-MM-DD.md` 후보
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/domain-modeling-guardrails-as-archunit-fitness-functions-2026-06-05.md
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
---
|
||||
title: blog-topic / domain-modeling-guardrails-as-archunit-fitness-functions-2026-06-05
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-domain-modeling-guardrails]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, archunit, fitness-function, ddd, value-object, aggregate, domain-event, jqwik, clean-architecture]
|
||||
created: 2026-06-05
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: domain-modeling-guardrails-as-archunit-fitness-functions-2026-06-05
|
||||
|
||||
> Layer: `raw/blog-topics/` — 작업·트러블슈팅에서 나온 글감 원석. canonical 정제 전 raw.
|
||||
|
||||
## Parent
|
||||
|
||||
- [[raw/branch-notes/feature-domain-modeling-guardrails]]
|
||||
|
||||
## 한 줄 글감
|
||||
|
||||
"DDD 전술 패턴을 README 권고가 아니라 빌드 깨짐으로 강제하기 — stereotype 애너테이션 + ArchUnit fitness function."
|
||||
|
||||
## 본문 뼈대 (초안)
|
||||
|
||||
1. **문제**: rich domain model / 값 객체 불변식 / transport-free 도메인 이벤트는 보통 "문서 권고"로 남고 시간이 지나면 침식된다(anemic 회귀, public setter, 도메인에 Kafka 타입 누출).
|
||||
2. **접근**: 의미를 드러내는 마커 애너테이션을 도메인 코어에 둔다 — `@ValueObject`/`@AggregateRoot`/`@DomainEvent` (java.lang.annotation 만 의존, 프레임워크 0).
|
||||
3. **강제**: ArchUnit 규칙이 마커를 키로 평가
|
||||
- `value_objects_have_no_public_no_arg_constructor` — 빈 생성자 = 불변식 우회 백도어 차단. record(컴포넌트 보유)는 자동 충족.
|
||||
- `aggregate_root_setters_are_not_public` — `set*` 비공개 강제(Vernon Option A: ORM 외부 매핑 가시성).
|
||||
- `domain_events_are_records` + `domain_events_are_transport_free` — immutable record + Kafka/HTTP/JAX-RS 패키지 의존 금지.
|
||||
- `domain_has_no_logger` — 도메인은 로그 대신 안전한 명사형 reason enum 예외로 위반을 표현.
|
||||
4. **owner 경계 교훈**: "도메인 순수성" 규칙(다른 branch 소유)에 logger 금지를 끼워넣지 않고 별도 규칙으로 분리한 이유 — 규칙 소유권/위반 메시지 명확성.
|
||||
5. **정직성 교훈**: logger 금지는 공식 표준이 아니라 프로젝트 자체 규약. 사실 등급을 격상하지 않는다.
|
||||
6. **비공허(non-vacuous) 증명**: 규칙마다 의도적 위반 fixture + 격리 코퍼스로 "실제로 잡는다"를 테스트(violations-as-data). transport glob 은 broker별 격리 증명.
|
||||
7. **함정**: `testCompileOnly` 타입을 record component 로 쓰면 JUnit *discovery* 가 죽는다 → method body `.class` 참조로 회피([[raw/errors/archunit-testcompileonly-class-loading-2026-06-02]]).
|
||||
8. **불변식 검증의 깊이**: 예시 테스트 대신 jqwik property-based test 로 값 객체 입력 공간 전체를 무작위 검증.
|
||||
9. **이벤트 경계 PoC**: `WorkLogReserved`(domain) → `WorkLogReservedIntegrationEvent`(application mapper) — 도메인은 wire 를 모른다.
|
||||
|
||||
## Cross-links
|
||||
|
||||
- [[raw/blog-topics/archunit-testcompileonly-fixture-annotation-pattern-2026-06-02]]
|
||||
- [[raw/blog-topics/archunit-violations-as-data-pattern-2026-05-28]]
|
||||
- [[raw/interviews/domain-modeling-guardrails-archunit-2026-06-05]]
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-06-05
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-domain-modeling-guardrails]]
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: DDD tactical pattern을 README 권고가 아니라 marker annotation + ArchUnit fitness function으로 빌드 단계에서 강제한다.
|
||||
- 예상 제목 후보:
|
||||
- DDD guardrail을 ArchUnit fitness function으로 만들기
|
||||
- Value Object와 Domain Event 규칙을 빌드에서 검증하기
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- `@ValueObject`, `@AggregateRoot`, `@DomainEvent` 같은 marker를 ArchUnit rule의 평가 key로 쓴다.
|
||||
- 의견/해석 후보:
|
||||
- domain modeling 규칙은 공식 표준이 아니라 project-local guardrail이므로 사실 등급을 조심해야 한다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. tactical DDD rule이 문서 권고로만 남을 때 침식되는 경로를 설명한다.
|
||||
2. framework-free marker annotation과 ArchUnit rule의 역할을 나눈다.
|
||||
3. violations-as-data와 jqwik property test로 guardrail이 실제로 bite하는지 확인한다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/clean-architecture-package-layout.md` 후보:
|
||||
- domain modeling guardrail / ArchUnit fitness function 글감.
|
||||
- 필요한 추가 검증:
|
||||
- 현재 marker annotation, ArchUnit rule, jqwik test 존재 여부.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-domain-modeling-guardrails]]
|
||||
- [[raw/blog-topics/archunit-violations-as-data-pattern-2026-05-28]]
|
||||
- [[raw/errors/archunit-testcompileonly-class-loading-2026-06-02]]
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: domain guardrail 구현과 property-based test의 현재 상태.
|
||||
- 과장하면 안 되는 부분: logger ban이나 marker taxonomy를 DDD 공식 표준처럼 쓰지 않는다.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/clean-architecture-package-layout.md` 에 domain modeling guardrail 글감으로 반영한다.
|
||||
- 다음 단계: blogify 전 project-local rule과 구현 증거를 분리한다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/feature-domain-modeling-guardrails]]
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/env-example-drift-gate-gradle-2026-06-06.md
|
||||
@@ -0,0 +1,101 @@
|
||||
---
|
||||
title: blog-topic / env-example-drift-gate-gradle-2026-06-06
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-env-driven-runtime-configuration]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, gradle, configuration, 12-factor, fail-fast, developer-experience]
|
||||
created: 2026-06-06
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: env-example-drift-gate-gradle-2026-06-06
|
||||
|
||||
> Layer: `raw/blog-topics/` — 작업에서 나온 블로그 글감 원석. canonical 정제 전 raw 후보.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-env-driven-runtime-configuration]]
|
||||
|
||||
## 글감 한 줄
|
||||
|
||||
"`.env.example` 을 추가하려다 깨달은 것 — 우리 `.env` 는 이미 tracked 였다. drift 게이트의 정답 소스는 템플릿이 아니라 코드가 실제로 요구하는 surface 다."
|
||||
|
||||
## 핵심 논지
|
||||
|
||||
- 흔한 패턴: secret 때문에 `.env` 를 gitignore 하고 redacted `.env.example` 을 commit. 하지만 `.env.example` 은 "작성 시점 스냅샷"이라 새 env 가 생겨도 갱신 안 돼 drift → 신규 합류자가 복사해 띄우면 누락 env startup 실패.
|
||||
- **반전(이 프로젝트의 실제 결정)**: ca-tmpl 은 `src/.env` 자체를 git-tracked 로 둔다(로컬 dev 기본값 포함, secret 은 로컬 sentinel). 이 경우 `.env.example` 은 **password 만 가린 중복 사본**이라 가치가 거의 없다. 처음엔 D7 문언대로 `.env.example` + `verifyEnvExample` 을 만들었다가, 리뷰에서 "`.env` 가 이미 tracked 인데 example 이 왜 필요?"라는 지적으로 제거 → task 를 `verifyEnvKeys` 로 rename.
|
||||
- 결론적 게이트(custom Gradle task `verifyEnvKeys`)는 **template 파일이 아니라 코드 surface 를 기준**으로 두 방향만 강제:
|
||||
1. `application.yml` 의 `${VAR}`(inline default 없는 것=required) 가 전부 `src/.env` 에 존재(누락 0).
|
||||
2. `src/.env` 의 모든 키가 `application.yml` 어딘가 `${...}` 로 실제 소비됨(orphan/stale 0).
|
||||
- `inputs.files(...)` 선언으로 Gradle up-to-date 캐싱과 호환, `check` 에 `dependsOn` 연결해 CI 필수 게이트화.
|
||||
- 교훈: "`.env.example` drift 막기"는 수단이지 목적이 아니다. 진짜 목적은 "코드가 요구하는 env 와 운영자가 가진 env 가 일치하는가". `.env` 가 tracked 라면 example 은 군더더기이고, 게이트는 application.yml ↔ `.env` 를 직접 보는 게 맞다.
|
||||
|
||||
## 왜 registry 기준이 아니라 application.yml surface 기준인가 (설계 결정)
|
||||
|
||||
- contract registry(`env-keys.yaml`)는 **여러 미구현 branch 의 키까지** 포함 → registry 와 `.env` 를 1:1 강제하면 코드에 없는 phantom 키 수십 개를 넣어야 함(운영자가 무시할 값).
|
||||
- "운영자가 `.env` 만으로 앱을 띄울 수 있는가"가 진짜 목적 → 검증 기준은 **실제 config surface(application.yml placeholder)** 가 맞다. registry 는 governance SSOT 로 별도 유지.
|
||||
- 교훈: drift gate 의 "정답 소스"는 빌드 가능한 표면이어야지, 미래 계약을 담은 레지스트리가 아니다.
|
||||
|
||||
## 곁가지 주제
|
||||
|
||||
- 12-factor §III config 와 `APP_` prefix 전면 통일(외부 의존 env 와 시각 분리)의 트레이드오프.
|
||||
- boolean `true/false`-only, Duration `30s`-only 같은 "기계적으로는 동등하나 팀 규약으로 1택" 결정을 어떻게 문서화/강제하나.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-06-06
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-env-driven-runtime-configuration]]
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: `.env.example` drift를 막는 목적은 example 파일 유지가 아니라 실제 config surface와 실행 env key의 정합성을 검증하는 것이다.
|
||||
- 예상 제목 후보:
|
||||
- `.env.example`이 아니라 실제 config surface를 검증하기
|
||||
- Gradle task로 env key drift를 막는 방법
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- ca-tmpl은 env-driven runtime configuration branch에서 env key drift gate를 다뤘다.
|
||||
- tracked `.env`와 `application.yml` placeholder의 양방향 정합을 보는 방향이 글감의 핵심이다.
|
||||
- 의견/해석 후보:
|
||||
- drift gate의 정답 소스는 미래 registry가 아니라 현재 빌드 가능한 runtime surface여야 한다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. `.env.example`은 snapshot이라 drift가 생기기 쉽다.
|
||||
2. tracked `.env` 정책에서는 example 사본보다 key surface 검증이 더 중요하다.
|
||||
3. Gradle `verifyEnvKeys`가 application config와 env key를 양방향으로 확인한다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/config-and-adapter-templates.md` 후보:
|
||||
- env key drift gate와 `.env` tracked policy.
|
||||
- 필요한 추가 검증:
|
||||
- 실제 `verifyEnvKeys` 구현 여부와 현재 ca-tmpl의 `.env` 추적 정책.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-env-driven-runtime-configuration]]
|
||||
- [[raw/interviews/startup-fail-fast-config-validation-2026-06-06]]
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: `verifyEnvKeys`가 현재 ca-tmpl 코드에 존재하는지.
|
||||
- 과장하면 안 되는 부분: draft canonical 확인 전까지 구현 완료처럼 쓰지 않는다.
|
||||
|
||||
## 관련 / Related
|
||||
|
||||
- [[raw/branch-notes/feature-env-driven-runtime-configuration]]
|
||||
- [[raw/interviews/startup-fail-fast-config-validation-2026-06-06]]
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/config-and-adapter-templates.md` 에 env key drift gate 글감으로 반영했다.
|
||||
- 다음 단계: target canonical이 아직 `draft` 이므로 `blogify` 전 branch-note/code evidence와 현재 `.env` tracked 정책을 재확인한다.
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/executable-clean-architecture-onboarding-2026-06-25.md
|
||||
@@ -0,0 +1,86 @@
|
||||
---
|
||||
title: blog-topic / executable-clean-architecture-onboarding-2026-06-25
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-domain-feature-onboarding-contract]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, architecture, testing, archunit, clean-architecture, multi-module]
|
||||
created: 2026-06-25
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: executable-clean-architecture-onboarding-2026-06-25
|
||||
|
||||
> Layer: `raw/blog-topics/` — multi-module Clean Architecture onboarding checklist 를 실행 가능한 테스트로 만든 경험 글감.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-domain-feature-onboarding-contract]] — 문서 중심 onboarding contract를 ArchUnit/JUnit dry-run으로 구현한 작업에서 파생.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-06-25
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-domain-feature-onboarding-contract]]
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: Clean Architecture 체크리스트는 README에만 있으면 약하고, test-only dry-run slice와 negative fixture로 만들면 새 도메인 추가 경계가 CI에서 반복 검증된다.
|
||||
- 예상 제목 후보:
|
||||
- Clean Architecture 온보딩 체크리스트를 테스트로 바꾸기
|
||||
- 새 도메인 추가가 아키텍처를 깨지 않는다는 걸 어떻게 증명할까
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- ca-tmpl은 read-only/write onboarding slice를 분리해 정의한다 — 근거 후보: [[raw/branch-notes/feature-domain-feature-onboarding-contract]] D3, D4.
|
||||
- 이번 구현은 `DomainFeatureOnboardingContractTest`와 `CleanArchitectureTest` rule로 해당 계약을 검증한다 — 근거 후보: [[raw/branch-notes/feature-domain-feature-onboarding-contract]] §구현 결과.
|
||||
- 경험 후보:
|
||||
- `./gradlew verifyCleanArchitectureDependencies`, ArchUnit focused test, focused onboarding suite, 전체 `./gradlew test`까지 통과시켰다 — 근거 후보: [[raw/branch-notes/feature-domain-feature-onboarding-contract]] §Verification commands.
|
||||
- 의견/해석 후보:
|
||||
- “문서로 합의한 아키텍처”와 “실제로 실패하는 guardrail” 사이에는 큰 차이가 있다. 단, 모든 것을 정적 분석으로 잡을 수는 없으므로 Open Risk를 문서화해야 한다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. 문제: 새 도메인 추가는 controller-only shortcut으로 무너지기 쉽다 — 체크리스트만으로는 반복 검증이 어렵다.
|
||||
2. 접근: read-only/write 최소 slice를 test-only Ticket fixture로 만든다 — 실제 production domain을 추가하지 않고도 계약을 검증한다.
|
||||
3. 실패도 데이터로 만든다 — missing transaction boundary와 shared-contract domain pollution을 negative fixture로 잡는다.
|
||||
4. 한계: ArchUnit direct-call 분석은 helper 뒤를 못 본다 — guardrail과 review의 경계를 같이 적어야 한다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/domain-feature-onboarding-guardrails.md` 후보:
|
||||
- ca-tmpl에서 새 도메인 기능을 추가할 때 통과해야 하는 read/write dry-run guardrail.
|
||||
- `wiki/concepts/executable-architecture-guardrails.md` 후보:
|
||||
- 아키텍처 문서 계약을 JUnit/ArchUnit positive/negative fixture로 전환하는 일반 패턴.
|
||||
- 필요한 추가 검증:
|
||||
- 실제 downstream 새 도메인 branch에서 false positive/negative 관찰.
|
||||
- `sampleOffTest`까지 포함한 check matrix에서 시간 비용 측정.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-domain-feature-onboarding-contract]] — 구현 결정과 local verification evidence.
|
||||
- [[raw/errors/gradle-wrapper-sandbox-lock-2026-06-25]] — sandboxed Gradle 검증 문제.
|
||||
- [[raw/interviews/clean-architecture-domain-onboarding-guardrails]] — 예상 면접 질문 원석.
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: downstream fork에서 fixture 없이 실제 production domain slice를 추가했을 때 rule coverage가 충분한지.
|
||||
- 과장하면 안 되는 부분: 이 작업은 local verification이며 운영 검증이나 보편 표준 증명은 아니다.
|
||||
- 블로그로 쓰기 전에 필요한 canonical 정제: `wiki/projects/ca-tmpl/`에 project fact로 승격 후 blog derive.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/clean-architecture-package-layout.md` 에 executable onboarding guardrails 글감으로 반영했다.
|
||||
- 다음 단계: source canonical이 `verified` 상태이므로 이후 `blogify` 대상으로 삼을 수 있다. 단 운영 검증이나 보편 표준 증명처럼 쓰지 않는다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/feature-domain-feature-onboarding-contract]]
|
||||
- 관련 error: [[raw/errors/gradle-wrapper-sandbox-lock-2026-06-25]]
|
||||
- 관련 interview prep: [[raw/interviews/clean-architecture-domain-onboarding-guardrails]]
|
||||
- derived blog: 생성 전
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/five-stage-local-bootstrap-contract-2026-06-24.md
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
title: blog-topic / five-stage local bootstrap contract
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-developer-experience-contract]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, ci-cd, gradle, docker]
|
||||
created: 2026-06-24
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: five-stage-local-bootstrap-contract
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-developer-experience-contract]] — single-command bootstrap 구현·검증에서 파생.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-06-24
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-developer-experience-contract]]
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: 단일 bootstrap 명령의 가치는 명령 수가 아니라 compile, dependency, migration, contract, HTTP smoke 실패를 서로 다른 증거로 분리하는 데 있다.
|
||||
- 예상 제목 후보:
|
||||
- Spring Boot 템플릿의 첫 실행을 5단계 Gradle 계약으로 만든 이유
|
||||
- docker compose up만으로는 잡지 못한 local bootstrap 실패 두 가지
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- `./gradlew bootstrap`이 다섯 task를 순서대로 실행한다 — 근거: [[raw/branch-notes/feature-developer-experience-contract]] D3, §2026-06-24 구현 결과.
|
||||
- README 명령 drift가 `check`에서 검증된다 — 근거: 같은 branch D4.
|
||||
- 경험 후보:
|
||||
- host 5432 publish 제거와 slim JRE RNG bean 수정 뒤 실제 container health + HTTP smoke가 통과했다 — 근거: branch §마주친 문제, 두 error note.
|
||||
- 의견/해석 후보:
|
||||
- fresh-clone DX는 문서 친절도보다 실행 가능한 실패 계약으로 평가하는 편이 더 재현 가능하다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. 왜 단일 명령인가 — 사용자가 기억할 entrypoint를 하나로 줄이되 내부 실패는 숨기지 않는다.
|
||||
2. 다섯 단계의 경계 — compile, dependency, migration/start, delegated sample contract, HTTP smoke가 잡는 결함이 서로 다르다.
|
||||
3. 실제로 잡힌 두 실패 — host port exposure와 slim JRE provider parity가 unit test만으로 남는 이유.
|
||||
4. 문서도 빌드 입력이다 — README command drift와 link-rot를 CI 계약으로 만드는 방법.
|
||||
5. 검증 등급의 경계 — local green과 CI/OS/prod 검증을 구분한다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/developer-experience-bootstrap.md` 후보:
|
||||
- 5단계 task graph, failure contract, local verification 결과.
|
||||
- `wiki/concepts/runtime-image-parity.md` 후보:
|
||||
- full JDK test와 slim JRE provider/module parity gap.
|
||||
- 필요한 추가 검증:
|
||||
- Linux clean clone, Apple Silicon, WSL2 실행 시간/성공률.
|
||||
- GitHub Actions link-check 실제 실행과 false-positive 수렴.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-developer-experience-contract]] D3, D4, D8, D10.
|
||||
- [[raw/errors/bootstrap-postgres-port-collision-2026-06-24]].
|
||||
- [[raw/errors/slim-jre-random-generator-missing-2026-06-24]].
|
||||
- [[raw/interviews/single-command-local-bootstrap]].
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: 지원 OS별 first-run 시간과 GitHub link-check 결과.
|
||||
- 과장하면 안 되는 부분: Linux local 검증을 모든 OS/CI/prod에서의 보장으로 표현하지 않는다.
|
||||
- 블로그로 쓰기 전에 필요한 canonical 정제: 위 project/concept 후보에 code/test evidence를 추출한다.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/devops-ci-supply-chain-dx.md` 에 five-stage local bootstrap 글감으로 반영했다.
|
||||
- 다음 단계: target canonical이 아직 `draft` 이므로 `blogify` 전 review/verify가 필요하다. Linux local 검증을 모든 OS/CI/prod 보장으로 표현하지 않는다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- [[raw/branch-notes/feature-developer-experience-contract]].
|
||||
- [[raw/errors/bootstrap-postgres-port-collision-2026-06-24]].
|
||||
- [[raw/errors/slim-jre-random-generator-missing-2026-06-24]].
|
||||
- [[raw/interviews/single-command-local-bootstrap]].
|
||||
- derived blog: 생성 전. canonical 정제 후 `wiki/blog/<slug>-YYYY-MM-DD.md` 후보.
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/framework-free-method-authorization-clean-architecture-2026-06-08.md
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
title: blog-topic / Spring Security 없이 application layer 에 method-level 인가 걸기
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-authentication-authorization-contract]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, security, authorization, clean-architecture, spring-security, method-security]
|
||||
created: 2026-06-08
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: framework-free method authorization in Clean Architecture
|
||||
|
||||
> Layer: `raw/blog-topics/` — 구현에서 나온 블로그 글감 seed. canonical 추출은 `/ingest` 시 별도.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-authentication-authorization-contract]]
|
||||
|
||||
## 글감 한 줄
|
||||
|
||||
"`@PreAuthorize` 를 쓰면 application layer 가 Spring Security 에 결합된다. annotation 은 core 에, 집행은 adapter 에 두면 layer 순수성을 지키면서 method-level 인가를 걸 수 있다."
|
||||
|
||||
## 핵심 논지 / Outline
|
||||
|
||||
1. **문제**: Clean Architecture 에서 application/domain 은 framework-free 여야 하는데, Spring method security(`@PreAuthorize`/`@Secured`)는 bean 을 `org.springframework.security` 에 결합시킨다.
|
||||
2. **분리**: *결정(decision)* 과 *메커니즘(mechanism)* 분리.
|
||||
- core: `@RequiresPermission`(plain annotation) + `AuthorizationPort`(plain interface) + `Permission`(value object) + `AuthorizationPrincipal`(raw roles).
|
||||
- adapter: `AuthorizationManager<MethodInvocation>` 가 annotation 을 읽고 principal 을 매핑해 port 에 위임. `@EnableMethodSecurity(prePostEnabled=false)` + custom `Advisor`(ROLE_INFRASTRUCTURE) 로 wiring.
|
||||
3. **거부 → 403 의 2-hop**: core 가 자체 예외 throw → adapter 가 Spring `AccessDeniedException` 으로 변환 → 에러 envelope.
|
||||
4. **permission-centric RBAC**: role=permission 묶음, registry 로 raw role→effective permission 해소(case-insensitive, fail-closed, no wildcard=least-privilege).
|
||||
5. **함정**: CGLIB vs JDK proxy(concrete 주입 시 `proxyTargetClass=true` 필수), AOP self-invocation bypass, unauthenticated(`AuthenticationException`) vs unauthorized(`AccessDeniedException`) 구분. → [[raw/errors/method-security-cglib-vs-jdk-proxy-usecase-injection-2026-06-08]]
|
||||
6. **마이그레이션 path**: port interface 덕분에 RBAC→ABAC 전환이 구현체 교체로 끝남.
|
||||
|
||||
## 차별점
|
||||
|
||||
대부분의 Spring 튜토리얼은 `@PreAuthorize` 를 service 에 바로 붙인다. 이 글은 "왜 그게 hexagonal/clean 구조에서 부채인가 + 어떻게 분리하나"를 코드(TransactionPort 선례와 동일 패턴)로 보여준다.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-06-08
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-authentication-authorization-contract]]
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: application layer에 Spring Security annotation을 직접 붙이지 않고 plain annotation + port + adapter method-security로 method-level authorization을 구현한다.
|
||||
- 예상 제목 후보:
|
||||
- Clean Architecture에서 framework-free method authorization 만들기
|
||||
- `@PreAuthorize` 없이 application layer 인가 걸기
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- core annotation과 authorization port는 Spring Security type을 직접 의존하지 않는다.
|
||||
- adapter가 Spring `AuthorizationManager`와 advisor wiring을 소유한다.
|
||||
- 의견/해석 후보:
|
||||
- authorization decision과 framework mechanism을 분리하면 RBAC→ABAC migration path가 단순해진다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. `@PreAuthorize`가 application layer purity를 깨는 경로를 설명한다.
|
||||
2. core annotation/port와 adapter enforcement를 분리한다.
|
||||
3. 403 envelope, CGLIB/JDK proxy, self-invocation bypass 한계를 적는다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/security-baseline-jwt-actuator-secrets.md` 후보:
|
||||
- framework-free method authorization 글감.
|
||||
- 필요한 추가 검증:
|
||||
- 현재 `@RequiresPermission`, `AuthorizationPort`, method security adapter 구현 여부.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-authentication-authorization-contract]]
|
||||
- [[raw/errors/method-security-cglib-vs-jdk-proxy-usecase-injection-2026-06-08]]
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: method security custom advisor의 현재 구현·검증 여부.
|
||||
- 과장하면 안 되는 부분: Spring Security 자체를 부정하지 않고 ca-tmpl layer boundary 선택으로 제한한다.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/security-baseline-jwt-actuator-secrets.md` 에 framework-free method authorization 글감으로 반영한다.
|
||||
- 다음 단계: blogify 전 구현 증거와 proxy/self-invocation 한계를 확인한다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/feature-authentication-authorization-contract]]
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/gitea-act-dependency-security-gate-portability-2026-07-02.md
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
title: blog-topic / gitea-act-dependency-security-gate-portability
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-dependency-vulnerability-management-contract]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, ci-cd, docker, supply-chain, static-analysis]
|
||||
created: 2026-07-02
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: gitea-act-dependency-security-gate-portability
|
||||
|
||||
> Layer: `raw/blog-topics/` — 작업 중 나온 블로그 글감 원석. 최종 블로그는 canonical 정제 후 `wiki/blog/`에서 작성한다.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-dependency-vulnerability-management-contract]] — dependency security gate를 GitHub Actions 전용 action에서 Gitea/act runner 환경으로 옮기는 과정에서 나온 글감.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-07-02
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-dependency-vulnerability-management-contract]]
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: GitHub Actions 전용 dependency-review/trivy-action을 Gitea와 act_runner 환경에서 플랫폼 독립 CLI gate로 조정한 이유를 정리한다.
|
||||
- 예상 제목 후보:
|
||||
- dependency security gate를 GitHub Actions 밖으로 옮기기
|
||||
- Gitea와 act_runner에서 supply chain gate를 유지하는 법
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- branch에 Gitea/act adaptation 기록과 suppression governance 기존 topic이 정리되어 있다 — 근거 후보: [[raw/branch-notes/feature-dependency-vulnerability-management-contract]] Gitea/act notes `:138-149`, existing suppression topic linkage.
|
||||
- 경험 후보:
|
||||
- 기존 Trivy suppression topic은 suppression governance이고, 이 글감은 CI platform portability 실패와 CLI 전환이 초점이다.
|
||||
- 의견/해석 후보:
|
||||
- supply chain gate는 특정 CI product action에 묶이면 재사용성이 떨어질 수 있다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. GitHub Actions action은 편하지만 platform coupling이 생긴다 — Gitea/act runner에서 깨지는 지점을 본다.
|
||||
2. CLI gate로 옮기기 — 입력/출력/exit code를 명시하면 CI provider를 바꿔도 계약을 유지할 수 있다.
|
||||
3. suppression governance와 portability를 분리하기 — 보안 정책과 runner wiring은 다른 소유권이다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/devops-ci-supply-chain-dx.md` 후보:
|
||||
- ca-tmpl dependency vulnerability gate portability decision.
|
||||
- `wiki/concepts/devops-ci-supply-chain-dx.md` 후보:
|
||||
- CI provider portability와 supply chain gate 일반 개념.
|
||||
- 필요한 추가 검증:
|
||||
- 실제 Gitea/act failure log, CLI invocation, exit code behavior.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-dependency-vulnerability-management-contract]] — Gitea/act adaptation 근거.
|
||||
- [[raw/blog-topics/trivy-suppression-governance-static-gate-2026-06-20]] — suppression governance 인접 topic.
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: Gitea/act runner에서 어떤 action이 왜 깨졌는지의 재현 로그.
|
||||
- 과장하면 안 되는 부분: 모든 CI에서 동작한다고 쓰지 않는다. portability를 높인 설계로 제한한다.
|
||||
- 블로그로 쓰기 전에 필요한 canonical 정제: devops project 문서의 CI gate evidence 보강.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/devops-ci-supply-chain-dx.md` 에 Gitea/act dependency security gate portability 글감으로 반영했다.
|
||||
- 다음 단계: target canonical이 아직 `draft` 이므로 `blogify` 전 review/verify가 필요하다. 모든 CI에서 동작한다고 쓰지 않는다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/feature-dependency-vulnerability-management-contract]]
|
||||
- 관련 error:
|
||||
- 관련 interview prep:
|
||||
- derived blog: 생성 전. 생성 시 `wiki/blog/gitea-act-dependency-security-gate-portability-YYYY-MM-DD.md` 후보
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/gradle9-java21-static-analysis-baseline-2026-06-20.md
|
||||
@@ -0,0 +1,92 @@
|
||||
---
|
||||
title: blog-topic / gradle9-java21-static-analysis-baseline-2026-06-20
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-static-analysis-quality-contract]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, static-analysis, gradle, spotless, checkstyle, spotbugs, errorprone, java21]
|
||||
created: 2026-06-20
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: gradle9-java21-static-analysis-baseline-2026-06-20
|
||||
|
||||
> Layer: `raw/blog-topics/` — 채용공고가 아닌 작업·학습에서 나온 블로그 글감 원석. canonical 정제 전 raw 후보이며, `wiki/blog/` 직접 생성 근거가 아니다.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-static-analysis-quality-contract]] — Spotless + Checkstyle + SpotBugs + FindSecBugs + ErrorProne 5종을 Gradle 9.0.0 / Java 21 멀티모듈에 도입한 작업에서 추출.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 정적 분석 도구가 전무한 greenfield 스켈레톤(10 모듈)에 "비중복·로컬·infra-free" 원칙으로 5종 도구를 한 번에 도입. 도구 선택은 문서에서 끝났지만, 실제 wiring 에서 (1) formatter↔linter 책임 중복, (2) 기존 코드 대량 위반, (3) BOM↔도구 classpath 충돌이 줄줄이 나왔다.
|
||||
|
||||
## 글감 코어 / Core idea
|
||||
|
||||
- **formatter 와 linter 의 책임 분리(중복 제거)**: google-java-format(Spotless) 가 *포맷·import order* 를 소유하면, Checkstyle 은 그 모듈(`Indentation`/`LineLength`/`WhitespaceAround`/`CustomImportOrder`)을 **반드시 빼야** 한다. 안 그러면 formatter 가 고친 걸 linter 가 reject → CI 무한 reformat 루프(checkstyle #6527). Checkstyle 은 formatter 가 못 하는 것(naming·Javadoc·logical)만 남긴다. "두 도구가 같은 규칙을 강제하지 않게 하는 게 도입의 핵심" 이라는 한 줄.
|
||||
- **기존 코드에 blocking 게이트를 씌우는 3가지 전략**: ① 전부 컴플라이언스(reformat + Javadoc 327개 작성 — 비현실적·부정확 위험), ② 포맷은 전체 적용 + Javadoc 은 warning-tier 로 시작(추후 승급), ③ ratchet(변경 파일만). 이 스켈레톤은 ②를 택함 — `spotlessApply` 로 654 파일 일괄 포맷(포매터 도입의 표준 절차)하되, Checkstyle Javadoc 규칙은 `severity=warning` + `maxWarnings=∞` 로 reported-but-non-blocking. naming/logical 은 error-tier 유지.
|
||||
- **idiom false-positive 는 rename 이 아니라 calibrate**: ConstantName 이 SLF4J `private static final Logger log` 를 25건 잡는다 — 하지만 Logger 는 mutable observable state 라 Google §5.2.4 상 *상수가 아님* → `log`/`logger` 를 패턴에 허용. InterfaceTypeParameterName 이 F-bounded self-type `ResourceId<SELF ...>` 의 `SELF` 를 잡는다 → 타입 파라미터 패턴을 `^[A-Z][A-Z0-9]*$` 로 완화. "규칙이 관용구를 잡으면 코드를 망치지 말고 규칙을 보정한다."
|
||||
- **BOM 이 도구 classpath 를 오염시킨다**: `io.spring.dependency-management` 는 BOM managed version 을 **모든 configuration**(런타임뿐 아니라 `spotbugs` 도구 설정)에 적용. SpotBugs 4.10.2 가 요구하는 commons-lang3 3.20.0 이 Boot BOM 의 3.17.0 으로 강등 → `NoClassDefFoundError: org.apache.commons.lang3.Strings` 로 분석 worker crash. `resolutionStrategy.force` 는 안 먹히고 `ext['commons-lang3.version']='3.20.0'` 로 managed property 를 override 해야 함. (디버그 전말: [[raw/errors/spotbugs-commons-lang3-bom-downgrade-noclassdef-2026-06-20]])
|
||||
- **SpotBugs 노이즈는 reportLevel 로 끊는다**: 기본(medium)에서 78건 중 38건이 EI_EXPOSE_REP/REP2 — 생성자가 주입받은 EntityManager/repository/Clock/ObjectMapper 를 "방어적 복사 안 했다" 고 잡는 노이즈(DI 협력자는 복사하면 안 됨). `reportLevel='high'` 로 high-confidence 만 blocking → 노이즈 제거. 남는 high 보안 finding(SPRING_CSRF_PROTECTION_DISABLED)은 stateless JWT API 에서 의도된 설정이라 exclude.xml 로 근거와 함께 suppress.
|
||||
- **게이트 실효성 증명**: `./gradlew check` 가 green 한 번으로 끝내지 말고, 의도적 위반(나쁜 포맷 + `Bad_Method_Name`)을 주입해 spotlessCheck/checkstyleMain 이 실제로 BUILD FAILED 하는지(gate bites) 확인 후 원복.
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: Gradle 9 / Java 21 멀티모듈에 정적 분석 baseline을 넣을 때 핵심은 plugin 나열이 아니라 formatter-linter 책임 분리, 기존 코드 마이그레이션, 도구 classpath 충돌 처리다.
|
||||
- 예상 제목 후보:
|
||||
- Gradle 9와 Java 21에서 static analysis baseline을 잡는 법
|
||||
- Spotless, Checkstyle, SpotBugs, ErrorProne을 한 번에 넣으며 배운 것
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- Spotless가 format/import order를 소유하면 Checkstyle의 중복 formatting rule은 제거해야 한다.
|
||||
- Spring dependency management BOM은 SpotBugs tool configuration의 transitive dependency에도 영향을 줄 수 있다.
|
||||
- Javadoc rule은 warning-tier로 시작하고 naming/logical rule은 blocking으로 둘 수 있다.
|
||||
- 의견/해석 후보:
|
||||
- static analysis baseline은 도구 도입보다 기존 코드와 CI가 감당할 수 있는 승급 경로 설계가 더 중요하다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. formatter와 linter가 같은 규칙을 강제할 때 생기는 reformat loop를 설명한다.
|
||||
2. 기존 코드 위반을 한 번에 blocking하지 않고 warning-tier/ratchet/전면 수정 중 선택하는 기준을 정리한다.
|
||||
3. Spring BOM이 SpotBugs classpath를 오염시킨 사례와 해결 방향을 적는다.
|
||||
4. 의도적 위반 주입으로 gate가 실제로 실패하는지 확인하는 절차를 남긴다.
|
||||
|
||||
## 왜 의미 있나 / Why it matters
|
||||
|
||||
- "정적 분석 도구 도입" 은 plugin 한 줄이 아니라, **책임 중복 제거 + 기존 코드 마이그레이션 전략 + 도구/BOM classpath 충돌** 의 묶음이다. 실무에서 그대로 부딪히는 함정들이라 이식성이 높다.
|
||||
- Gradle 9 + Java 21(record/sealed bytecode) 환경에서 5종 도구의 버전 호환을 실측으로 확정한 사례. 문서가 "Gradle 7+/JRE 17+" 만 명시할 때 실제로 도는지는 별개라는 점.
|
||||
- 한계(글에서 명시): Javadoc 은 아직 warning-tier(blocking 미승급), SonarQube 는 외부 서비스라 기본 배제(opt-in 문서만). 즉 "완성된 게이트" 가 아니라 "정직하게 단계적으로 조이는 baseline".
|
||||
|
||||
## 관련 / Related
|
||||
|
||||
- [[raw/errors/spotbugs-commons-lang3-bom-downgrade-noclassdef-2026-06-20]]
|
||||
- [[raw/branch-notes/feature-static-analysis-quality-contract]]
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/devops-ci-supply-chain-dx.md` 후보:
|
||||
- Gradle 9 / Java 21 static analysis baseline 글감.
|
||||
- 필요한 추가 검증:
|
||||
- 현재 Spotless/Checkstyle/SpotBugs/ErrorProne wiring과 warning-tier 상태.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-static-analysis-quality-contract]]
|
||||
- [[raw/errors/spotbugs-commons-lang3-bom-downgrade-noclassdef-2026-06-20]]
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: Javadoc warning-tier가 blocking으로 승급됐는지.
|
||||
- 과장하면 안 되는 부분: static analysis baseline을 운영 품질 보장처럼 쓰지 않는다.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/devops-ci-supply-chain-dx.md` 에 Gradle 9 / Java 21 static analysis baseline 글감으로 반영한다.
|
||||
- 다음 단계: blogify 전 실제 current tool versions와 gate-bites evidence를 확인한다.
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/hikaricp-inter-knob-constraints-startup-guard-2026-06-09.md
|
||||
@@ -0,0 +1,101 @@
|
||||
---
|
||||
title: "HikariCP knob 간 제약을 Spring Boot 시작 guard 로 강제하는 패턴"
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-database-connection-pool-contract]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, hikaricp, spring-boot, startup-validation, connection-pool, clean-architecture]
|
||||
created: 2026-06-09
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# HikariCP inter-knob constraints as a Spring Boot startup guard
|
||||
|
||||
## Parent
|
||||
|
||||
- [[raw/branch-notes/feature-database-connection-pool-contract]]
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-06-09
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-database-connection-pool-contract]]
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: HikariCP knob 간 제약을 runtime 경고에 맡기지 않고 Spring Boot startup guard로 수집해 fail-fast시키는 패턴이다.
|
||||
- 예상 제목 후보:
|
||||
- HikariCP 설정 오류를 startup에서 잡기
|
||||
- Connection pool knob 제약을 Spring Boot guard로 고정하기
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- `validationTimeout < connectionTimeout`, `keepaliveTime < maxLifetime` 같은 inter-knob 제약이 있다.
|
||||
- `SmartInitializingSingleton`과 `ApplicationContextRunner`로 startup guard를 검증할 수 있다.
|
||||
- 의견/해석 후보:
|
||||
- pool 설정 오류는 traffic을 받기 전 startup phase에서 실패시키는 편이 운영적으로 더 명확하다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. HikariCP knob 간 제약과 runtime 경고/reset의 한계를 정리한다.
|
||||
2. String 기반 defensive parse로 Duration drift를 안전하게 처리한다.
|
||||
3. `ApplicationContextRunner`로 guard failure를 작은 테스트로 고정한다.
|
||||
|
||||
## 핵심 아이디어
|
||||
|
||||
HikariCP 에는 knob 간 순서 제약이 있다:
|
||||
- `validationTimeout < connectionTimeout`
|
||||
- `keepaliveTime < maxLifetime`
|
||||
- `connectionTimeout >= 250 ms`
|
||||
- `leakDetectionThreshold >= 2000 ms` (0 = disabled 허용)
|
||||
|
||||
이 제약들은 HikariCP 내부에서 경고 또는 reset 으로만 처리되고, 설정 오류가 runtime 에서만 드러나는 경우가 많다. `SmartInitializingSingleton` + `Environment.getProperty(key)` (String, not typed) 패턴으로 context refresh 완료 직전에 모든 위반을 한꺼번에 수집해 `IllegalStateException` 으로 boot fail 시키면, 잘못된 pool 설정이 prod 에 배포되는 것을 막을 수 있다.
|
||||
|
||||
## 흥미로운 구현 포인트
|
||||
|
||||
### Defensive parseMillis (CONNECTION_TIMEOUT_FORMAT_DRIFT)
|
||||
|
||||
`environment.getProperty("spring.datasource.hikari.connection-timeout", Long.class)` 는 env-keys.yaml default 가 `"5s"` (Duration string) 일 때 `ConversionFailedException` 을 던진다. 대신 `getProperty(key)` 로 String 을 받아 `Long.parseLong(raw.trim())` + `NumberFormatException catch → return null` 패턴으로 방어 파싱하면:
|
||||
1. 숫자 ms 값은 정상 검증
|
||||
2. Duration string 은 null (absent 취급) — 크래시 없이 skip
|
||||
3. 명세에서 두 포맷이 공존하는 drift 환경에서 안전
|
||||
|
||||
### ApplicationContextRunner 기반 단위 테스트
|
||||
|
||||
`@SpringBootTest` 없이 `ApplicationContextRunner.withUserConfiguration(ValidatorConfig.class)` 만으로 `SmartInitializingSingleton` 의 `afterSingletonsInstantiated()` 가 호출된다. `context.hasFailed()` + `context.getStartupFailure().hasStackTraceContaining(...)` 으로 각 위반 케이스를 격리 검증.
|
||||
|
||||
## 글감 방향
|
||||
|
||||
- Spring Boot startup contract 패턴 시리즈 (`SmartInitializingSingleton` vs `ApplicationListener<ContextRefreshedEvent>` vs `@PostConstruct`)
|
||||
- HikariCP 운영에서 놓치기 쉬운 knob 간 제약 총정리
|
||||
- "설정 오류를 runtime 이 아닌 startup 에서 잡는다" 원칙의 구현 패턴들
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/data-layer-persistence-cache-outbound.md` 후보:
|
||||
- HikariCP inter-knob constraint startup guard를 data-layer/pool configuration 글감으로 연결.
|
||||
- 필요한 추가 검증:
|
||||
- 실제 validator class, `ApplicationContextRunner` 테스트, env duration drift 처리 범위.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-database-connection-pool-contract]]
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: 현재 ca-tmpl 코드에 startup guard와 관련 테스트가 존재하는지.
|
||||
- 과장하면 안 되는 부분: HikariCP 자체가 모든 오류를 방치한다고 쓰지 않고, ca-tmpl에서 선택한 fail-fast 보강으로 제한한다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/feature-database-connection-pool-contract]]
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/data-layer-persistence-cache-outbound.md` 에 HikariCP startup guard 글감으로 반영했다.
|
||||
- 다음 단계: target canonical이 아직 `draft` 이므로 `blogify` 전 pool guard 구현·검증 여부를 branch-note/code 기준으로 확인한다.
|
||||
-1
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/idempotency-executor-application-layer-clean-architecture-2026-06-09.md
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
---
|
||||
title: blog-topic / idempotency-executor-application-layer-clean-architecture-2026-06-09
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-rate-limit-idempotency-contract]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, idempotency, clean-architecture, rate-limit]
|
||||
created: 2026-06-09
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: 멱등성을 application layer 실행기로 — Clean Architecture에서 rate-limit/idempotency 운영 계약
|
||||
|
||||
> Layer: `raw/blog-topics/` — feature-rate-limit-idempotency-contract 구현에서 나온 글감.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-rate-limit-idempotency-contract]]
|
||||
|
||||
## 글감 / Topic
|
||||
|
||||
운영 멱등성과 rate-limit을 "프레임워크 미들웨어"가 아니라 **계층 소유권**으로 배치한 사례.
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: idempotency는 application-layer executor가, rate-limit은 presentation interceptor가 소유하도록 나눠 Clean Architecture 경계를 명시한다.
|
||||
- 예상 제목 후보:
|
||||
- 멱등성을 application layer 실행기로 두기
|
||||
- Clean Architecture에서 idempotency와 rate-limit 소유권 나누기
|
||||
|
||||
### 다룰 포인트
|
||||
|
||||
1. **owner_layer 분리**: idempotency 코드(409/422)는 `owner_layer: application` → `IdempotencyExecutor`
|
||||
포트 + `IdempotencyStore` 포트 + DB 어댑터로 application/infra에 둠. rate-limit(429)은
|
||||
`owner_layer: presentation` → adapter-web `HandlerInterceptor`. 같은 "운영 횡단 관심사"라도 코드/응답
|
||||
소유 계층이 다르다.
|
||||
2. **filter vs interceptor**: rate-limit key가 `IP + uri_template(normalized)`를 요구 →
|
||||
servlet filter는 handler mapping 이전이라 route template(`/v1/worklogs/{id}`)을 모름.
|
||||
`HandlerInterceptor`로 옮겨 `BEST_MATCHING_PATTERN_ATTRIBUTE`를 사용.
|
||||
3. **명시적 실행기 vs AOP**: KEYED use case를 BeanPostProcessor/AOP로 감싸는 대신 `executor.execute(ctx, action, codec)`
|
||||
명시 호출. 호출부가 약간 장황하지만 ArchUnit/테스트 단순성과 스켈레톤 투명성을 얻음.
|
||||
4. **insert-or-read + unique 제약을 동시성 중재자로**: 200ms in-flight wait는 IETF 즉시-409 SHOULD의
|
||||
"운영 친화적 변형"(표준 그대로 아님). fingerprint(SHA-256) mismatch는 IETF 422 권고 정합.
|
||||
5. **port-codec 분리로 application의 wire-format 중립성**: 실행기는 `IdempotentResponseCodec<R>`(web JSON 소유)로
|
||||
직렬화만 위임 → application은 transport/storage 중립.
|
||||
6. **UNSUPPORTED_IMPL_DECISION 정직성**: 200ms·SHA-256·8KB·fixed-window·canonicalization 미적용은
|
||||
외부 표준이 강제하지 않음을 코드 주석에 명시 — "표준 따름" 과장 금지.
|
||||
|
||||
## 관련
|
||||
|
||||
- [[wiki/concepts/idempotency-key-design]]
|
||||
- [[raw/errors/idempotency-expired-row-reclaim-409-loop-2026-06-09]]
|
||||
- [[raw/errors/scheduled-reaper-wrong-config-prefix-2026-06-09]]
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/idempotency-key-design.md` 후보:
|
||||
- idempotency executor의 application-layer ownership와 rate-limit presentation ownership 분리.
|
||||
- 필요한 추가 검증:
|
||||
- `IdempotencyExecutor`, `IdempotencyStore`, response codec, interceptor 구현 여부.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-06-09
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-rate-limit-idempotency-contract]]
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- idempotency와 rate-limit은 같은 운영 횡단 관심사처럼 보여도 owner layer가 다르다.
|
||||
- idempotency executor는 application/use case 실행 경계에 놓고, rate-limit은 route template을 아는 web interceptor가 맡는 구조다.
|
||||
- 의견/해석 후보:
|
||||
- AOP보다 명시 실행기가 skeleton 투명성과 테스트 용이성을 준다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. owner layer를 application과 presentation으로 나눈다.
|
||||
2. filter와 interceptor가 route template을 볼 수 있는 시점 차이를 설명한다.
|
||||
3. `IdempotencyExecutor`와 response codec 분리로 wire format 중립성을 유지한다.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-rate-limit-idempotency-contract]]
|
||||
- [[wiki/concepts/idempotency-key-design]]
|
||||
- [[raw/errors/idempotency-expired-row-reclaim-409-loop-2026-06-09]]
|
||||
- [[raw/errors/scheduled-reaper-wrong-config-prefix-2026-06-09]]
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: 현재 ca-tmpl 코드의 executor/storage/interceptor 구현 여부.
|
||||
- 과장하면 안 되는 부분: IETF draft 준수와 ca-tmpl의 200ms wait 변형을 섞지 않는다.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/idempotency-key-design.md` 에 application-layer idempotency executor 글감으로 반영했다.
|
||||
- 다음 단계: target canonical이 아직 `draft` 이므로 `blogify` 전 실제 구현 등급을 재확인한다.
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/identifier-governance-rule-scoping-by-id-kind-2026-06-01.md
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
title: blog-topic / identifier-governance-rule-scoping-by-id-kind-2026-06-01
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-resource-identifier-contract, feature-boundary-validation-mapping-contract]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, archunit, identifier, architecture, ddd, clean-architecture]
|
||||
created: 2026-06-01
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: identifier-governance-rule-scoping-by-id-kind-2026-06-01
|
||||
|
||||
> Layer: `raw/blog-topics/` — 채용공고가 아닌 작업·학습·트러블슈팅에서 나온 블로그 글감 원석.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-resource-identifier-contract]] — D5(도메인 port + application orchestration) + D17(4 ArchUnit rule) 결정.
|
||||
- [[raw/errors/archunit-no-uuid-random-trace-id-false-positive-2026-06-01]] — resource-id rule이 trace-id 생성을 잘못 잡은 사건.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work` / `error`
|
||||
- 트리거 날짜: 2026-06-01
|
||||
- 트리거 연결 노트: [[raw/errors/archunit-no-uuid-random-trace-id-false-positive-2026-06-01]]
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: 한 시스템에는 ID가 여러 종류(resource / trace / session / idempotency-key / api-key)가 공존하고, 각각 **생성 주체·형식·수명·책임 branch가 다르다**. "모든 `UUID.randomUUID()`를 금지"하는 ArchUnit rule은 합법적인 trace-id 생성을 잡는 false positive를 낳는다 — 거버넌스 규칙은 *ID 종류별로* scope해야 한다.
|
||||
- 떠오른 계기: resource-id 전용 `no_uuid_random_in_controller`가 `RequestLoggingFilter`의 correlation-id 생성을 잡음.
|
||||
- 예상 제목 후보:
|
||||
- "ID 종류별 거버넌스": 한 규칙으로 모든 식별자를 다스리려다 생긴 false positive
|
||||
- Clean Architecture에서 도메인 식별자를 인프라 결합 없이 생성하기 (port + application orchestration)
|
||||
- ArchUnit fitness function의 scope 설계: 결정 텍스트 vs reference 코드
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- DDD factory pattern은 "entity가 자기 ID를 minting"하라고 요구하지 않는다 — factory는 도메인 *service/port*이고, 생성 *호출 시점*은 use case orchestration이다. 도메인 순수성(인프라 라이브러리 미결합)과 server-assigned id를 동시에 만족.
|
||||
- 식별자 거버넌스 ArchUnit rule은 대상 ID의 *종류*를 명시해야 한다: resource id는 controller/use case에서 직접 생성 금지(factory 강제), trace id는 filter에서 생성 정상(distributed-tracing 책임), idempotency-key는 client 생성(rate-limit 책임).
|
||||
- rule selector는 spec의 "결정 텍스트(좁은 의도)"와 "reference 코드(넓은 예시)"가 어긋날 때 결정 텍스트를 따른다.
|
||||
- 사람이 만든 sealed 계층 enumeration이 모듈 경계로 불가능할 때, ArchUnit rule(`no_long_id_pk`)이 compile-time `sealed permits`의 빌드타임 대체가 된다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. ID는 하나의 범주가 아니라 resource/trace/session/idempotency/api key처럼 책임이 나뉜다.
|
||||
2. 너무 넓은 ArchUnit rule은 합법적인 trace-id 생성까지 잡는 false positive를 만든다.
|
||||
3. governance rule은 결정 텍스트의 좁은 의도와 ID kind별 owner를 기준으로 scope한다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/resource-identifier-format.md` 후보:
|
||||
- ca-tmpl identifier governance rule scoping 결정과 false-positive boundary.
|
||||
- `wiki/concepts/resource-identifier-format.md` 후보:
|
||||
- ID kind별 governance 일반 개념.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-resource-identifier-contract]] — resource identifier governance 결정.
|
||||
- [[raw/errors/archunit-no-uuid-random-trace-id-false-positive-2026-06-01]] — trace-id false-positive 사건.
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: resource-id rule과 tracing/correlation-id rule의 owner 경계를 concept 문서에 어느 수준까지 일반화할지.
|
||||
- 과장하면 안 되는 부분: 모든 `UUID.randomUUID()` 호출을 금지하는 것이 정답이라고 쓰지 않는다.
|
||||
- 블로그로 쓰기 전에 필요한 canonical 정제: 이미 `wiki/projects/ca-tmpl/resource-identifier-format.md`에 연결됨. ID kind별 일반 개념은 blogify 전 확인한다.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/resource-identifier-format.md` 에 identifier kind별 governance scoping 글감으로 반영했다.
|
||||
- 다음 단계: source canonical이 `verified` 상태이므로 이후 `blogify` 대상으로 삼을 수 있다. 단 모든 `UUID.randomUUID()` 금지가 정답이라고 쓰지 않는다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/feature-resource-identifier-contract]]
|
||||
- 관련 error: [[raw/errors/archunit-no-uuid-random-trace-id-false-positive-2026-06-01]]
|
||||
- 관련 interview prep:
|
||||
- derived blog: 생성 전. 생성 시 `wiki/blog/identifier-governance-rule-scoping-by-id-kind-YYYY-MM-DD.md` 후보
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/java21-context-propagation-strategy-virtual-threads-2026-07-02.md
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
title: blog-topic / java21-context-propagation-strategy-virtual-threads
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-runtime-context-propagation-contract]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, runtime, java-21, loom, virtual-threads, thread-local]
|
||||
created: 2026-07-02
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: java21-context-propagation-strategy-virtual-threads
|
||||
|
||||
> Layer: `raw/blog-topics/` — 작업 중 나온 블로그 글감 원석. 최종 블로그는 canonical 정제 후 `wiki/blog/`에서 작성한다.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-runtime-context-propagation-contract]] — ThreadLocal, Micrometer Context Propagation, Java 21 ScopedValue 선택 기준에서 나온 글감.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-07-02
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-runtime-context-propagation-contract]]
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: Java 21 환경에서 request/security/tenant context를 ThreadLocal, Micrometer Context Propagation, ScopedValue 중 어디까지 다룰지 선택 기준을 정리한다.
|
||||
- 예상 제목 후보:
|
||||
- Java 21에서 context propagation을 다시 봐야 하는 이유
|
||||
- ThreadLocal에서 ScopedValue로 바로 갈 수 없는 이유
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- branch가 Java 21 ScopedValue, Micrometer Context Propagation, ThreadLocal 선택 기준 topic을 명시한다 — 근거 후보: [[raw/branch-notes/feature-runtime-context-propagation-contract]] section+line `:268-271`.
|
||||
- 경험 후보:
|
||||
- 기존 async TaskDecorator topic은 executor/MDC 쪽에 가깝고, 이 글감은 runtime context propagation strategy 자체를 다룬다.
|
||||
- 의견/해석 후보:
|
||||
- context propagation은 API 선택 문제가 아니라 thread model, observability, security context boundary를 같이 보는 문제다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. ThreadLocal은 익숙하지만 thread model에 묶인다 — executor, virtual thread, async boundary에서 다시 검토해야 한다.
|
||||
2. Micrometer Context Propagation은 관측성 중심의 장점이 있다 — trace/log context와 application context를 섞지 않아야 한다.
|
||||
3. ScopedValue는 매력적이지만 adoption boundary가 있다 — Java version, framework support, migration cost를 본다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/runtime-context-propagation.md` 후보:
|
||||
- ca-tmpl runtime context propagation project decision.
|
||||
- `wiki/concepts/context-propagation-java-virtual-threads.md` 후보:
|
||||
- Java 21 context propagation 일반 개념.
|
||||
- 필요한 추가 검증:
|
||||
- ca-tmpl의 실제 ThreadLocal implementation과 virtual thread 지원 여부.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-runtime-context-propagation-contract]] — context propagation topic seed 근거.
|
||||
- [[raw/blog-topics/spring-async-taskdecorator-bounded-executor-saturation-shutdown-2026-06-13]] — 인접한 async context topic.
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: Java 21 ScopedValue를 실제 production path에 적용했는지 여부.
|
||||
- 과장하면 안 되는 부분: ScopedValue 채택 경험처럼 쓰면 안 된다. 선택 기준/후보로 분리한다.
|
||||
- 블로그로 쓰기 전에 필요한 canonical 정제: runtime context propagation project 문서 생성 또는 runtime 문서 통합.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/runtime-container-health-migration.md` 에 Java 21 context propagation 선택 기준 글감으로 반영했다.
|
||||
- 다음 단계: target canonical이 아직 `draft` 이므로 `blogify` 전 review/verify가 필요하다. ScopedValue 채택 경험처럼 쓰지 않는다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/feature-runtime-context-propagation-contract]]
|
||||
- 관련 error:
|
||||
- 관련 interview prep:
|
||||
- derived blog: 생성 전. 생성 시 `wiki/blog/java21-context-propagation-strategy-virtual-threads-YYYY-MM-DD.md` 후보
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/jdk-httpclient-dns-connectexception-classification-2026-07-02.md
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
title: blog-topic / jdk-httpclient-dns-connectexception-classification
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-outbound-http-client-baseline]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, integration, networking, spring-boot, retry-policy, api-contract]
|
||||
created: 2026-07-02
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: jdk-httpclient-dns-connectexception-classification
|
||||
|
||||
> Layer: `raw/blog-topics/` — 작업 중 나온 블로그 글감 원석. 최종 블로그는 canonical 정제 후 `wiki/blog/`에서 작성한다.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-outbound-http-client-baseline]] — outbound HTTP failure taxonomy에서 DNS 실패 분류 edge case로 나온 글감.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-07-02
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-outbound-http-client-baseline]]
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: JDK HttpClient에서 DNS 실패를 connection failure로 분류할 때 exception wrapping과 retry category를 어떻게 다뤘는지 정리한다.
|
||||
- 예상 제목 후보:
|
||||
- JDK HttpClient DNS 실패는 어떤 outbound failure일까
|
||||
- DNS failure를 retryable connection error로 분류할 때 조심할 점
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- branch가 DNS 실패 분류 topic을 명시한다 — 근거 후보: [[raw/branch-notes/feature-outbound-http-client-baseline]] line `:373`.
|
||||
- 경험 후보:
|
||||
- DNS failure classification은 outbound failure taxonomy의 실제 edge case로 남아 있다.
|
||||
- 의견/해석 후보:
|
||||
- retry policy는 HTTP status만 보아서는 부족하고, connect/DNS/TLS/read timeout 같은 transport failure를 별도로 분류해야 한다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. HTTP client failure는 HTTP status만이 아니다 — DNS, connect, TLS, read timeout을 transport layer로 분리한다.
|
||||
2. JDK HttpClient exception wrapping 읽기 — root cause와 exposed exception이 다를 수 있다.
|
||||
3. retry category로 연결하기 — connection failure와 remote 5xx를 같은 방식으로 다루지 않는다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/outbound-http-client-baseline.md` 후보:
|
||||
- ca-tmpl outbound HTTP failure classification 구현 사실.
|
||||
- `wiki/concepts/outbound-http-failure-classification.md` 후보:
|
||||
- outbound failure taxonomy 일반 개념.
|
||||
- 필요한 추가 검증:
|
||||
- JDK HttpClient DNS 실패 재현 테스트와 exception class chain.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-outbound-http-client-baseline]] — DNS classification topic seed 근거.
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: DNS failure가 실제 코드에서 어떤 exception path로 들어오는지.
|
||||
- 과장하면 안 되는 부분: 운영 장애 사례처럼 쓰지 않는다. local/test evidence 중심 글감으로 제한한다.
|
||||
- 블로그로 쓰기 전에 필요한 canonical 정제: outbound HTTP project 문서의 failure taxonomy 갱신.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/data-layer-persistence-cache-outbound.md` 에 JDK HttpClient DNS/ConnectException classification 글감으로 반영했다.
|
||||
- 다음 단계: target canonical이 아직 `draft` 이므로 `blogify` 전 review/verify가 필요하다. 운영 장애 사례처럼 쓰지 않는다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/feature-outbound-http-client-baseline]]
|
||||
- 관련 error:
|
||||
- 관련 interview prep:
|
||||
- derived blog: 생성 전. 생성 시 `wiki/blog/jdk-httpclient-dns-connectexception-classification-YYYY-MM-DD.md` 후보
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/jvm-oom-vs-container-oomkill-exit-137-2026-07-02.md
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
title: blog-topic / jvm-oom-vs-container-oomkill-exit-137
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-container-runtime-contract]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, runtime, docker, kubernetes, graceful-shutdown]
|
||||
created: 2026-07-02
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: jvm-oom-vs-container-oomkill-exit-137
|
||||
|
||||
> Layer: `raw/blog-topics/` — 작업 중 나온 블로그 글감 원석. 최종 블로그는 canonical 정제 후 `wiki/blog/`에서 작성한다.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-container-runtime-contract]] — container runtime 계약과 OOM 137 구분 검증에서 나온 글감.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-07-02
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-container-runtime-contract]]
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: JVM OOM과 container OOMKill이 모두 exit 137처럼 보일 때 heap dump/native stderr/runtime signal로 구분하는 방법을 정리한다.
|
||||
- 예상 제목 후보:
|
||||
- exit 137만 보고 JVM OOM과 OOMKill을 구분할 수 있을까
|
||||
- container runtime에서 Java OOM을 관측 가능하게 만들기
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- branch가 JVM OOM과 kubelet/container OOMKill 구분을 blog seed로 남겼다 — 근거 후보: [[raw/branch-notes/feature-container-runtime-contract]] D5 section+line `:210-219`, blog seed `:331-335`, verification `:286`.
|
||||
- 경험 후보:
|
||||
- Docker runtime 검증 기록이 있어 운영 면접/블로그 소재로 확장 가능하다.
|
||||
- 의견/해석 후보:
|
||||
- exit code는 진단의 출발점일 뿐 원인 판정 근거가 아니다. JVM 내부 OOM과 외부 kill signal을 분리할 관측 자료가 필요하다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. exit 137은 증상이지 원인이 아니다 — JVM process가 죽은 이유를 runtime 계층별로 나눠야 한다.
|
||||
2. JVM OOM에는 JVM이 남길 수 있는 흔적이 있다 — heap dump, error log, stderr가 핵심 단서가 된다.
|
||||
3. container OOMKill은 외부에서 죽인다 — runtime event와 memory limit 관측이 필요하다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/runtime-container-health-migration.md` 후보:
|
||||
- ca-tmpl container runtime OOM 관측 계약.
|
||||
- `wiki/concepts/runtime-container-health-migration.md` 후보:
|
||||
- JVM OOM과 container OOMKill 분리 일반 개념.
|
||||
- 필요한 추가 검증:
|
||||
- 실제 Docker/Kubernetes 재현 절차와 로그/exit code evidence.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-container-runtime-contract]] — D5, blog seed, verification 근거.
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: Kubernetes 환경에서의 event/log evidence와 ca-tmpl 검증 범위.
|
||||
- 과장하면 안 되는 부분: 운영 Kubernetes에서 검증된 장애 대응 경험처럼 쓰면 안 된다. local/Docker 검증과 운영 가정을 분리한다.
|
||||
- 블로그로 쓰기 전에 필요한 canonical 정제: runtime project 문서의 evidence grade 확인.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/runtime-container-health-migration.md` 에 JVM OOM vs container OOMKill 글감으로 반영했다.
|
||||
- 다음 단계: target canonical이 아직 `draft` 이므로 `blogify` 전 review/verify가 필요하다. 운영 Kubernetes 장애 대응 경험처럼 쓰지 않는다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/feature-container-runtime-contract]]
|
||||
- 관련 error:
|
||||
- 관련 interview prep:
|
||||
- derived blog: 생성 전. 생성 시 `wiki/blog/jvm-oom-vs-container-oomkill-exit-137-YYYY-MM-DD.md` 후보
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/logback-layer1-secret-masking-json-vs-pattern-2026-06-14.md
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
title: blog-topic / logback-layer1-secret-masking-json-vs-pattern-2026-06-14
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-log-management-contract]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, logback, logstash-encoder, masking, security, observability, redaction]
|
||||
created: 2026-06-14
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: logback-layer1-secret-masking-json-vs-pattern-2026-06-14
|
||||
|
||||
> Layer: `raw/blog-topics/` — 글감 원석. canonical `wiki/blog` 정제 전.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-log-management-contract]] — Redaction Layer 1(DRIFT-2) 구현에서 파생.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
"ERROR/WARN 로그에 token/password/Authorization 헤더를 `****` 로 가린다"는 계약을 `%replace(%msg){...}` 한 줄로 끝내려다, 운영 포맷이 `LogstashEncoder`(JSON) 임을 깨달음. `%replace` 는 PatternLayout converter 인데 JSON encoder 는 PatternLayout 을 **우회**한다 → JSON 로그에는 마스킹이 안 걸린다. 정작 가려야 할 production(JSON) 경로가 무방비.
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
"Logback 에서 secret 마스킹을 제대로 하려면 — `%replace` 가 JSON 을 못 가리는 이유와 단일 정규식 SSOT 설계":
|
||||
|
||||
1. **두 갈래 인코딩 경로**: 사람이 읽는 `PatternLayout`(local/dev) vs 구조화 `LogstashEncoder`(staging/prod). `%replace` 는 전자에만 적용.
|
||||
2. **JSON 경로의 올바른 도구**: `net.logstash.logback.mask.MaskingJsonGeneratorDecorator` + `ValueMasker` — JSON 생성 시점에 모든 string value(message/MDC/stack trace)를 정규식으로 치환. `<jsonGeneratorDecorator>` 로 encoder 에 장착.
|
||||
3. **pattern 경로**: `MessageConverter` 를 상속한 custom converter(`%maskedMsg`)로 같은 정규식 적용.
|
||||
4. **단일 SSOT**: 두 경로가 **같은** `LogMaskingPatterns`(컴파일된 `List<Pattern>` + capture-group replacement)를 공유 → profile/포맷 전환이 마스킹 대상을 바꾸지 못함(D10 "가독성 전환이 secret 노출로 이어지지 않게").
|
||||
5. **정규식 설계**: keyword(group1)+separator(group2)+value(group3) 로 캡처 후 `$1$2****` 치환 → 키는 남기고 값만 가림. 부정 문자클래스(`[^\s"',&}]+`)로 catastrophic backtracking 회피. Authorization/Bearer 별도 룰.
|
||||
6. **한계 명시**: 정규식 기반은 obfuscated 인코딩(Base64URL blob, prefix 없는 토큰)을 놓칠 수 있음 → 1차 방어는 여전히 "logger 가 payload/body 인자를 안 받음(by construction)". 마스킹은 defence-in-depth.
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- `%replace` 만으로 "로그 마스킹 했다"는 JSON 운영에서 거짓 안심이다 — encoder 가 PatternLayout 을 우회하면 무력.
|
||||
- 마스킹 정규식은 **인코딩 경로마다 중복하지 말고** 단일 Java SSOT 로 두고 decorator/converter 두 어댑터가 참조해야 한다 — 그래야 profile 전환이 보안 동작을 못 바꾼다.
|
||||
- value 캡처 그룹 + `$n` 역참조 치환으로 "키는 보존, 값만 마스킹" → 진단 가능성과 보안의 균형.
|
||||
|
||||
## 관련 / Related
|
||||
|
||||
- [[raw/branch-notes/feature-log-management-contract]]
|
||||
- 동반 면접 노트(드롭 메트릭 결정론 테스트 + 마스킹 Q): [[raw/interviews/deterministic-logback-asyncappender-drop-metric-test-2026-06-14]]
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. PatternLayout `%replace`가 JSON encoder 경로를 우회하는 문제를 설명한다.
|
||||
2. `MaskingJsonGeneratorDecorator`와 custom `%maskedMsg`가 같은 SSOT regex를 공유하게 한다.
|
||||
3. regex masking의 한계와 logger signature의 by-construction 방어를 분리한다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/observability-log-metric-trace-runbook.md` 후보:
|
||||
- Logback JSON vs pattern masking Layer 1 글감.
|
||||
- 필요한 추가 검증:
|
||||
- 현재 `LogMaskingPatterns`, JSON decorator, pattern converter 구현 여부.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-log-management-contract]]
|
||||
- [[raw/interviews/deterministic-logback-asyncappender-drop-metric-test-2026-06-14]]
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: 현재 ca-tmpl 코드에 JSON masking decorator와 pattern converter가 존재하는지.
|
||||
- 과장하면 안 되는 부분: regex masking이 모든 secret 형태를 잡는다고 쓰지 않는다.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/observability-log-metric-trace-runbook.md` 에 Logback JSON/pattern masking 글감으로 반영한다.
|
||||
- 다음 단계: blogify 전 masking 구현 등급과 payload logging 금지 rule을 분리한다.
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/manifest-driven-agent-harness-policy-engine.md
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
title: blog-topic / manifest-driven-agent-harness-policy-engine
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [chore-harness-policy-engine-alignment]
|
||||
related_projects: [ca-skeleton]
|
||||
tags: [blog-topic, ca-skeleton, architecture, build-tooling, code-generation]
|
||||
created: 2026-07-20
|
||||
status_label: captured
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: manifest-driven-agent-harness-policy-engine
|
||||
|
||||
## 부모
|
||||
|
||||
- [[raw/branch-notes/chore-harness-policy-engine-alignment]] — 실제 harness drift 감사와 구현에서 나온 글감.
|
||||
|
||||
## 트리거
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-07-20
|
||||
- 트리거 연결 노트: [[raw/branch-notes/chore-harness-policy-engine-alignment]]
|
||||
|
||||
## 글감
|
||||
|
||||
- 한 문장 요지: 중복 prompt 모음을 module registry, strict evidence, deterministic renderer, risk profile을 가진 실행 가능한 policy engine으로 바꾼 과정.
|
||||
- 예상 제목 후보:
|
||||
- Clean Architecture Agent Harness를 Manifest-Driven Policy Engine으로 바꾸기
|
||||
- Prompt 동기화가 아니라 Mutation Test로 지키는 멀티 플랫폼 개발 하네스
|
||||
|
||||
## 핵심 주장 후보
|
||||
|
||||
- 사실 후보: flat path gate는 실제 nested adapter production 경로를 놓쳤다 — 근거: branch §마주친 문제.
|
||||
- 사실 후보: 19개 leaf registry를 Gradle/import/agent consumer가 함께 사용한다 — 근거: branch D1.
|
||||
- 경험 후보: 세 차례 architecture review와 spec review에서 revision surface, risk, verdict evidence의 우회를 mutation test로 닫았다 — 근거: branch §검증 결과.
|
||||
- 의견/해석 후보: agent prompt를 문서가 아니라 생성·검증 가능한 artifact로 다뤄야 장기 drift를 줄일 수 있다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. 감사에서 드러난 topology drift — legacy flat path가 왜 green test 뒤에 숨었는지.
|
||||
2. registry와 immutable task packet — module owner, profile, rule hash를 한 번 resolve하는 방식.
|
||||
3. platform renderer와 thin adapter — 공통 의미와 제품별 hook 문법을 분리하는 방식.
|
||||
4. fail-closed evidence chain — counts, command rows, revision, upstream artifact를 검증한 이유.
|
||||
5. risk-based ceremony — N!·전수 matrix 대신 low/medium/high와 evidence profile을 쓴 이유.
|
||||
6. 검증의 경계 — static parity는 external authenticated E2E가 아니며 baseline failure도 별도로 남겨야 한다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/manifest-driven-agent-harness.md` 후보: ca-tmpl 실제 구조·테스트·review 결과.
|
||||
- `wiki/concepts/agent-harness-policy-engine.md` 후보: registry, renderer, evidence identity의 일반 패턴.
|
||||
- 필요한 추가 검증: 실제 세 플랫폼 golden run, production full check green, CI에서 physical surface 설치·parity 재현.
|
||||
|
||||
## 근거 후보
|
||||
|
||||
- [[raw/branch-notes/chore-harness-policy-engine-alignment]] — D1-D5와 local verification.
|
||||
- [[raw/errors/ca-tmpl-preexisting-check-baseline-failures-2026-07-20]] — baseline-aware verification 한계.
|
||||
- [[raw/interviews/manifest-driven-multi-platform-agent-harness]] — 예상 설계 질문.
|
||||
- [[raw/official-docs/google-antigravity-hooks]] — platform hook contract.
|
||||
|
||||
## 미해결
|
||||
|
||||
- 아직 확인해야 할 사실: authenticated products에서 같은 seeded task의 verdict/evidence parity.
|
||||
- 과장하면 안 되는 부분: repository-local static parity와 mutation test만 `locally-verified`다.
|
||||
- 블로그 전에 필요한 canonical 정제: code path/command evidence를 `wiki/projects` 문서로 승격하고 external golden 결과를 추가한다.
|
||||
|
||||
## 처리 결정
|
||||
|
||||
- 액션: `keep-as-topic`
|
||||
- 이유: 구현과 local evidence는 충분하지만 external golden과 production full check가 남아 있다.
|
||||
- 다음 단계: 후속 검증 뒤 project/concept canonical로 정제한다.
|
||||
|
||||
## 관련
|
||||
|
||||
- [[raw/branch-notes/chore-harness-policy-engine-alignment]]
|
||||
- [[raw/errors/ca-tmpl-preexisting-check-baseline-failures-2026-07-20]]
|
||||
- [[raw/interviews/manifest-driven-multi-platform-agent-harness]]
|
||||
- derived blog: 생성 전.
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/micrometer-meterfilter-resilience4j-functioncounter-2026-07-02.md
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
title: blog-topic / micrometer-meterfilter-resilience4j-functioncounter
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-outbound-http-client-baseline]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, observability, micrometer, circuit-breaker, metric-naming, high-cardinality]
|
||||
created: 2026-07-02
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: micrometer-meterfilter-resilience4j-functioncounter
|
||||
|
||||
> Layer: `raw/blog-topics/` — 작업 중 나온 블로그 글감 원석. 최종 블로그는 canonical 정제 후 `wiki/blog/`에서 작성한다.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-outbound-http-client-baseline]] — Micrometer MeterFilter와 Resilience4j FunctionCounter 충돌 회피 쟁점에서 나온 글감.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-07-02
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-outbound-http-client-baseline]]
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: outbound HTTP observability에서 Micrometer `MeterFilter`와 Resilience4j `FunctionCounter` registration/tag policy가 충돌할 수 있는 지점을 정리한다.
|
||||
- 예상 제목 후보:
|
||||
- MeterFilter가 Resilience4j metric을 만날 때 생기는 문제
|
||||
- outbound HTTP metric tag policy를 어디서 강제할까
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- branch가 Micrometer MeterFilter와 Resilience4j FunctionCounter 충돌 회피 topic을 명시한다 — 근거 후보: [[raw/branch-notes/feature-outbound-http-client-baseline]] line `:374`.
|
||||
- 경험 후보:
|
||||
- metrics tag/registration 문제를 운영 관측성 글감으로 분리할 수 있다.
|
||||
- 의견/해석 후보:
|
||||
- metric naming/cardinality policy는 application metric만이 아니라 library-generated metric에도 영향을 준다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. library metric도 내 관측성 계약 안에 들어온다 — Resilience4j가 생성한 meter를 어떻게 다룰지 정해야 한다.
|
||||
2. MeterFilter는 강력하지만 순서와 scope가 중요하다 — registration 시점의 tag policy를 확인한다.
|
||||
3. cardinality guard와 circuit breaker metric의 균형 — 필요한 label과 금지 label을 나눈다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/outbound-http-client-baseline.md` 후보:
|
||||
- outbound HTTP metrics integration decision.
|
||||
- `wiki/concepts/micrometer-meterfilter-ordering.md` 후보:
|
||||
- Micrometer MeterFilter와 library meter registration 일반 개념.
|
||||
- 필요한 추가 검증:
|
||||
- 실제 meter 이름, tag set, filter ordering test.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-outbound-http-client-baseline]] — Micrometer/Resilience4j topic seed 근거.
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: FunctionCounter registration과 MeterFilter 적용 순서.
|
||||
- 과장하면 안 되는 부분: Micrometer/Resilience4j 자체의 일반 결함처럼 쓰지 않는다. ca-tmpl metric contract와 integration edge로 제한한다.
|
||||
- 블로그로 쓰기 전에 필요한 canonical 정제: official docs/raw source 보강 필요.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/data-layer-persistence-cache-outbound.md` 에 Micrometer/Resilience4j metric registration edge 글감으로 반영했다.
|
||||
- 다음 단계: target canonical이 아직 `draft` 이므로 `blogify` 전 review/verify가 필요하다. Micrometer/Resilience4j 자체의 일반 결함처럼 쓰지 않는다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/feature-outbound-http-client-baseline]]
|
||||
- 관련 error:
|
||||
- 관련 interview prep:
|
||||
- derived blog: 생성 전. 생성 시 `wiki/blog/micrometer-meterfilter-resilience4j-functioncounter-YYYY-MM-DD.md` 후보
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/nplus1-lab-checkoutable-api-replay-2026-07-15.md
|
||||
@@ -0,0 +1,105 @@
|
||||
---
|
||||
title: blog-topic / checkoutable N+1 API replay
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [experiment-nplus1-feed-api-replay]
|
||||
related_projects: [nplus1-presentation-prep, ca-skeleton]
|
||||
tags: [blog-topic, nplus1-presentation-prep, persistence, api-design, postgresql, docker, hands-on-lab]
|
||||
created: 2026-07-15
|
||||
status_label: expanded
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: checkoutable N+1 API replay
|
||||
|
||||
> Layer: `raw/blog-topics/` — 테스트 코드에서만 보이던 N+1 관찰값을 checkout 가능한 Git stage, 로컬 HTTP API, PostgreSQL row 확인으로 바꾼 작업의 글감이다. 이 문서는 블로그 초안이나 canonical 문서가 아니다.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/experiment-nplus1-feed-api-replay]] — D1의 11개 checkout checkpoint, D3의 Crown/L12 분리, D4의 실제 Docker PostgreSQL HTTP+DB smoke에서 나온 글감이다.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-07-15
|
||||
- 트리거 연결 노트: [[raw/branch-notes/experiment-nplus1-feed-api-replay]]
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: N+1 학습을 테스트 결과 읽기로 끝내지 않고, 각 Git tag를 checkout해 fixture reset → HTTP 응답 → PostgreSQL row를 직접 보는 11단계 실습으로 바꾼 과정을 기록한다.
|
||||
- 예상 제목 후보:
|
||||
- 테스트만으로는 보이지 않는 N+1: 11개 checkout point로 만든 API·DB 실습
|
||||
- L1의 N+1부터 Crown까지: Git tag, curl, psql로 따라가는 JPA 조회 실험
|
||||
- 쿼리 수 최적화와 read model을 같은 해법으로 말하지 않기
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
> 아직 canonical이 아니다. 각 사실 후보의 검증 범위는 아래 근거에 적힌 로컬 환경까지다.
|
||||
|
||||
- 사실 후보:
|
||||
- 학습 경로는 `L1 → L2 → L3 → L4 → L5 → L6 → L14 → L15 → L16 → Crown → L12` 순서의 11개 checkout 가능한 tag로 고정되었다. — 근거: [[raw/branch-notes/experiment-nplus1-feed-api-replay]] D1, §구현 가이드 / 고정 replay checkpoint
|
||||
- final L12 tag의 로컬 Docker Compose smoke에서 reset 100건, Crown feed의 prepared statement 1·entity load 0, L12 read model의 부모당 Top-3, marker row count 100이 관찰되었다. — 근거: [[raw/branch-notes/experiment-nplus1-feed-api-replay]] D3, D4, §검증 기록 / Docker HTTP + PostgreSQL smoke
|
||||
- 별도 L1 historical smoke에서 lazy highlight 전략과 `collectionFetches=10`이 관찰되어, 마지막 상태만 보는 방식과 다른 출발점의 문제를 HTTP 응답으로 확인할 수 있었다. — 근거: [[raw/branch-notes/experiment-nplus1-feed-api-replay]] D1, §검증 기록 / Historical L1 smoke
|
||||
- 경험 후보:
|
||||
- 학습자는 stage를 checkout한 뒤 lab fixture를 reset하고 API 응답을 본 다음 `psql` marker row를 확인하는 같은 루프를 반복할 수 있다. — 근거: [[raw/branch-notes/experiment-nplus1-feed-api-replay]] D2, D4, §목표 / WHY
|
||||
- Crown의 one-query endpoint와 L12의 same-store CQRS-lite two-query read port를 별도 경로로 두면, "쿼리 수 최소화"와 "application read-model 분리"를 한 결과로 오해하지 않게 된다. — 근거: [[raw/branch-notes/experiment-nplus1-feed-api-replay]] D3, §Crown과 L12의 의도적 차이
|
||||
- 의견/해석 후보:
|
||||
- N+1 실습의 핵심 산출물은 최종 쿼리 하나가 아니라, 각 선택이 response·Hibernate 관찰값·DB 데이터에 어떻게 나타나는지 비교할 수 있는 반복 가능한 관찰 루프다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. 왜 마지막 Crown 코드만으로는 학습이 어려웠는가 — 최종 상태는 출발점의 lazy collection N+1과 중간 선택지를 숨긴다는 점을 보여준다.
|
||||
2. 11개 tag를 실습 단위로 고정한 방법 — Git checkout을 문서 목차가 아니라 실행 가능한 실험의 시작점으로 사용한다.
|
||||
3. fixture reset, HTTP, psql의 관찰 루프 — 테스트 assertion 밖에서 response shape와 marker-owned row를 함께 확인하는 이유를 설명한다.
|
||||
4. L1에서 무엇을 보고 Crown에서 무엇이 달라지는가 — collection fetch 수와 one-query/zero-entity-load 관찰값을 같은 질문으로 비교한다.
|
||||
5. Crown과 L12를 분리해서 읽어야 하는 이유 — one native query 최적화와 same-store CQRS-lite projection은 해결하려는 문제가 다르다는 점을 정리한다.
|
||||
6. 재현 결과를 과장하지 않는 법 — 로컬 Docker 검증은 production latency, deployment profile, 다른 machine의 모든 tag 재현을 증명하지 않는다고 명시한다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
> `wiki/blog/`로 바로 가지 않는다. 먼저 아래 후보를 canonical로 정제한다.
|
||||
|
||||
- `wiki/projects/nplus1-presentation-prep/nplus1-feed-api-replay.md` 후보:
|
||||
- 11-stage replay catalog, lab-only API boundary, local Docker/PostgreSQL smoke의 구현 사실과 evidence grade를 분리해 기록한다.
|
||||
- `wiki/concepts/n-plus-one-query-observability.md` 후보:
|
||||
- lazy loading, fetch join paging, batch fetch, projection, Top-N, keyset의 관찰 지표를 일반 개념으로 정리한다.
|
||||
- 필요한 추가 검증:
|
||||
- 깨끗한 clone/worktree에서 11개 tag의 compose/API smoke를 반복한다.
|
||||
- deployment manifest/env registry에서 `lab` profile이 운영 환경에 활성화되지 않는지 확인한다.
|
||||
- base architecture failure를 분리 수정한 뒤 full `./gradlew check` 결과를 기록한다.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
> 글감 단계의 후보 링크다. 최종 블로그의 사실 근거는 canonical 문서에서 다시 검증한다.
|
||||
|
||||
- [[raw/branch-notes/experiment-nplus1-feed-api-replay]] — 구현·Docker smoke·검증 한계의 직접 근거.
|
||||
- [[raw/official-docs/test-taxonomy-testcontainers-official]] — D4가 실제 PostgreSQL integration evidence를 택한 외부 근거.
|
||||
- [[raw/official-docs/cqrs-pattern-azure-architecture-center]] — D3의 same-store CQRS-lite와 별도 read store CQRS 구분 근거.
|
||||
- [[raw/official-docs/spring-data-jpa-projections-spring-official]] — D3의 application 반환용 projection/read shape 분리 근거.
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실:
|
||||
- 다른 깨끗한 machine/worktree에서도 모든 11개 tag가 동일한 Compose/API guide로 재현되는지는 확인되지 않았다.
|
||||
- 운영 환경에서 `lab` profile이 활성화되지 않는다는 deployment-level 증거는 아직 없다.
|
||||
- 과장하면 안 되는 부분:
|
||||
- 기록된 HTTP·`psql` 결과는 local Docker Compose PostgreSQL smoke이며 production 성능, latency SLA, 운영 권한 경계를 증명하지 않는다.
|
||||
- L12는 Crown과 동등한 visibility/keyset 해법이 아니라 same-store CQRS-lite의 two-query projection이다.
|
||||
- 블로그로 쓰기 전에 필요한 canonical 정제:
|
||||
- stage/tag와 guide의 매핑을 history rewrite 이후에도 다시 확인한다.
|
||||
- 사실 후보별 evidence grade와 관찰 명령을 canonical project 문서에 고정한다.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `keep-as-topic`
|
||||
- 이유: checkout replay와 로컬 smoke는 evidence가 있지만, 아직 canonical project/concept 문서와 다른 machine 재현 근거가 없다.
|
||||
- 다음 단계: `wiki/projects/nplus1-presentation-prep/nplus1-feed-api-replay.md` 후보를 evidence grade와 함께 정제한 뒤에만 `/blogify`를 검토한다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/experiment-nplus1-feed-api-replay]]
|
||||
- 관련 error: 생성 전. replay worktree root discovery 및 base architecture failure는 Parent의 Cluster에서 별도 raw error로 추적한다.
|
||||
- 관련 interview prep: 생성 전. Crown one-query와 CQRS-lite read model의 구분은 Parent의 Cluster에서 별도 raw interview note로 추적한다.
|
||||
- derived blog: 생성 전. 생성 시 `wiki/blog/nplus1-lab-checkoutable-api-replay-2026-07-15.md` 후보
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/operational-error-envelope-meta-category-migration-2026-06-01.md
|
||||
@@ -0,0 +1,90 @@
|
||||
---
|
||||
title: blog-topic / operational-error-envelope-meta-category-migration-2026-06-01
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-operational-error-observability-foundation]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, error-handling, observability, api-design, mdc, logging, testing, spring-boot]
|
||||
created: 2026-06-01
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: operational-error-envelope-meta-category-migration-2026-06-01
|
||||
|
||||
> Layer: `raw/blog-topics/` — 채용공고가 아닌 작업·학습·트러블슈팅에서 나온 블로그 글감 원석. canonical 정제 전 raw 후보이며, `wiki/blog/` 직접 생성 근거가 아니다.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-operational-error-observability-foundation]] — 운영 에러 분류 enum + 응답 envelope `meta`/`category` + snake_case MDC + 헤더 sanitization 구현·검증 경험.
|
||||
- [[raw/project-notes/ca-skeleton-operational-contract]] — ca-tmpl 운영 계약 SSOT.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
이미 동작하는 응답 envelope(`{success,data,error,traceId}`)을, 관측성 계약이 요구하는 richer shape(`{success,data,error.{code,category,...},meta.{requestId,traceId,correlationId}}`)으로 *기존 계약을 깨지 않고* 끌어올리는 마이그레이션을 직접 했다. 그 과정에서 (a) 인터페이스 추상 메서드 추가의 blast radius, (b) 동일 식별자의 계층별 case 매핑, (c) inbound 헤더 log injection 방어, (d) Spring Boot 슬라이스 테스트의 컨텍스트 오염 트러블슈팅까지 한 묶음으로 나왔다.
|
||||
|
||||
## 글감 후보 / Candidate angles
|
||||
|
||||
1. **"운영 에러 분류를 SSOT enum 으로 고정하기"** — 13개 임시 목록 → 10-category enum 으로 수렴. category(식별)와 retryable(런타임 신호)을 분리한 이유(per-code retryable, `INTERNAL_ERROR` 처럼 category default 와 다른 코드 허용).
|
||||
2. **"이미 직렬화되는 응답 계약에 필드를 안전하게 추가하는 법"** — record 컴포넌트 추가가 모든 호출부를 깨뜨리는 게 *오히려 안전장치*. 컴파일러가 숨은 consumer(`PortfolioErrorCode`, `BulkEnvelopeTest`)를 전부 노출 → 한 패스 마이그레이션. ProblemDetail 거부 결정(D5/D6)을 불변으로 둔 additive 확장.
|
||||
3. **"같은 ID 가 로그·JSON·헤더에서 다르게 쓰이는 건 버그가 아니다"** — `request_id`(snake) ↔ `meta.requestId`(camel) ↔ `X-Request-Id`(kebab)/`traceparent`(W3C lowercase). registry 로 3열 1:1 매핑을 고정하고 변환 지점을 한 군데(`ResponseMetaFactory`)로 모으는 패턴.
|
||||
4. **"구조화 로깅에서의 log injection(CWE-117) 방어"** — 평문 로깅과 달리 JSON 로깅에서의 진짜 위협은 CR/LF 줄 위조. reject/encode 가 아니라 strip + length cap 을 고른 trade-off.
|
||||
5. **(트러블슈팅) "@WebMvcTest 의 nested @SpringBootConfiguration 이 같은 패키지 다른 테스트를 조용히 깨뜨린 사건"** — git stash / 파일 mv 비파괴 격리로 원인 좁히기 → adapter 모듈 standalone MockMvc 로 재설계. (→ [[raw/errors/webmvctest-nested-springbootconfiguration-context-pollution-2026-06-01]])
|
||||
|
||||
## 핵심 메시지 / Thesis (raw)
|
||||
|
||||
운영 에러/관측성 "기반 계약"은 화려한 기능이 아니라 *모든 어댑터가 같은 실패 언어를 쓰게 만드는 어휘 고정*이다. 그 어휘를 (1) enum SSOT, (2) registry 매핑, (3) 단일 변환 지점, (4) 컴파일러로 강제되는 additive 확장으로 박아두면, 이후 모든 기능 branch 가 그 위에서 일관되게 쌓인다.
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: 기존 envelope을 깨지 않고 `error.category`와 `meta`를 추가해 운영 에러 어휘와 관측성 식별자를 한 응답 계약으로 묶는다.
|
||||
- 예상 제목 후보:
|
||||
- API error envelope에 meta와 category를 추가한 이유
|
||||
- 운영 에러 어휘를 enum과 response meta로 고정하기
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- operational error foundation branch가 category enum, response meta, MDC key, header sanitization을 다룬다.
|
||||
- additive record component 추가는 호출부 compile error로 migration blast radius를 드러낸다.
|
||||
- 의견/해석 후보:
|
||||
- 운영 에러 기반 계약은 모든 adapter가 같은 실패 언어를 쓰게 만드는 어휘 고정 작업이다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. 기존 envelope에 `meta`와 `category`를 추가해야 했던 이유를 설명한다.
|
||||
2. enum SSOT, registry mapping, response meta factory의 역할을 나눈다.
|
||||
3. header sanitization과 log injection 방어를 관측성 계약의 일부로 다룬다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/api-error-envelope-design.md` 후보:
|
||||
- verified envelope meta/category migration 글감.
|
||||
- 필요한 추가 검증:
|
||||
- blogify 전 운영 검증이 아니라 local verification 범위임을 유지한다.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-operational-error-observability-foundation]]
|
||||
- [[raw/project-notes/ca-skeleton-operational-contract]]
|
||||
- [[raw/errors/webmvctest-nested-springbootconfiguration-context-pollution-2026-06-01]]
|
||||
- [[raw/interviews/operational-error-envelope-and-observability-foundation]]
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: 운영 배포/측정 근거는 없다.
|
||||
- 과장하면 안 되는 부분: `verified` canonical이더라도 prod verification으로 확대하지 않는다.
|
||||
|
||||
## 관련
|
||||
|
||||
- [[raw/branch-notes/feature-operational-error-observability-foundation]]
|
||||
- [[raw/errors/webmvctest-nested-springbootconfiguration-context-pollution-2026-06-01]]
|
||||
- [[raw/interviews/operational-error-envelope-and-observability-foundation]]
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/api-error-envelope-design.md` 에 meta/category migration과 operational error vocabulary 글감으로 반영했다.
|
||||
- 다음 단계: source canonical이 `verified` 상태이므로 이후 `blogify` 대상으로 삼을 수 있다. 단 운영 검증으로 확대하지 않는다.
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/persistence-audit-metadata-clean-architecture-2026-07-02.md
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
title: blog-topic / persistence-audit-metadata-clean-architecture
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-persistence-auditing-contract]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, persistence, spring-data, hibernate, auditing, clean-architecture]
|
||||
created: 2026-07-02
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: persistence-audit-metadata-clean-architecture
|
||||
|
||||
> Layer: `raw/blog-topics/` — 작업 중 나온 블로그 글감 원석. 최종 블로그는 canonical 정제 후 `wiki/blog/`에서 작성한다.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-persistence-auditing-contract]] — audit metadata를 domain 밖 persistence adapter에서 채운 결정에서 나온 글감.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-07-02
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-persistence-auditing-contract]]
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: audit column을 domain model에 섞지 않고 persistence adapter에서 채우는 선택과 Spring Data JPA Auditing을 바로 쓰지 않은 경계를 정리한다.
|
||||
- 예상 제목 후보:
|
||||
- Clean Architecture에서 audit metadata를 어디에 둘까
|
||||
- createdAt은 domain인가 persistence detail인가
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- branch가 audit metadata를 도메인 밖으로 분리하는 persistence contract와 Manual explicit-set vs JPA Auditing topic 후보를 명시한다 — 근거 후보: [[raw/branch-notes/feature-persistence-auditing-contract]] line `:300`.
|
||||
- 경험 후보:
|
||||
- 구현된 manual explicit-set 경계를 중심으로 써야 하며, JPA Auditing 채택을 구현 사실처럼 쓰면 안 된다.
|
||||
- 의견/해석 후보:
|
||||
- audit metadata는 도메인 정책일 수도 있고 persistence concern일 수도 있으므로, skeleton에서는 기본 경계를 좁게 잡는 편이 낫다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. audit field가 항상 domain language는 아니다 — created/updated metadata의 소유자를 정해야 한다.
|
||||
2. persistence adapter에서 채우는 방식 — domain purity를 지키지만 mapping 책임이 늘어난다.
|
||||
3. Spring Data JPA Auditing과의 trade-off — 편의성, framework coupling, explicitness를 비교한다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/persistence-auditing-contract.md` 후보:
|
||||
- ca-tmpl persistence auditing 구현 사실.
|
||||
- `wiki/concepts/audit-metadata-clean-architecture.md` 후보:
|
||||
- audit metadata 소유권 일반 개념.
|
||||
- 필요한 추가 검증:
|
||||
- entity/mapper/test anchor와 Spring Data JPA Auditing 미채택 사유.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-persistence-auditing-contract]] — audit metadata branch와 topic seed 근거.
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: auditor identity, clock injection, update timestamp 처리 방식.
|
||||
- 과장하면 안 되는 부분: JPA Auditing이 나쁘다고 쓰지 않는다. ca-tmpl skeleton의 boundary choice로 제한한다.
|
||||
- 블로그로 쓰기 전에 필요한 canonical 정제: project implementation evidence와 concept trade-off 분리.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/data-layer-persistence-cache-outbound.md` 에 persistence audit metadata boundary 글감으로 반영했다.
|
||||
- 다음 단계: target canonical이 아직 `draft` 이므로 `blogify` 전 review/verify가 필요하다. JPA Auditing이 나쁘다고 쓰지 않는다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/feature-persistence-auditing-contract]]
|
||||
- 관련 error:
|
||||
- 관련 interview prep:
|
||||
- derived blog: 생성 전. 생성 시 `wiki/blog/persistence-audit-metadata-clean-architecture-YYYY-MM-DD.md` 후보
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/post-implementation-knowledge-capture-workflow-2026-05-28.md
|
||||
@@ -0,0 +1,113 @@
|
||||
---
|
||||
title: blog-topic / post-implementation-knowledge-capture-workflow-2026-05-28
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-architecture-enforcement-rules]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, workflow, documentation, agent-workflow, llm-wiki]
|
||||
created: 2026-05-28
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: post-implementation-knowledge-capture-workflow-2026-05-28
|
||||
|
||||
> Layer: `raw/blog-topics/` — 채용공고가 아닌 작업·학습·트러블슈팅에서 나온 블로그 글감 원석. canonical 정제 전 raw 후보이며, `wiki/blog/` 직접 생성 근거가 아니다.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-architecture-enforcement-rules]] — ca-tmpl 작업 종료 조건에 LLM Wiki capture (branch-note + derived raw notes) 를 _명시적으로_ 추가한 결정 (`결정 사항 2026-05-28: non-trivial 구현 종료 조건에 LLM Wiki capture를 포함한다`).
|
||||
- [[raw/project-notes/ca-skeleton-operational-contract]] — ca-tmpl skeleton 의 workflow 운영 계약 맥락.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-05-28
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-architecture-enforcement-rules]] — 본 결정이 branch-note 의 `Decision (2026-05-28: 종료 조건에 LLM Wiki capture)` 한 줄에 압축됨. 같은 날 다른 모든 코드 결정 (Gradle / ArchUnit rule) 과 _대등한 격_ 으로 기록한 점이 핵심.
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: "구현 완료" 의 정의에 _지식 캡처 (branch-note + 파생 raw notes) 까지_ 포함해야 코드만 남고 의사결정 / 트러블슈팅 / 글감이 사라지는 걸 막을 수 있다.
|
||||
- 떠오른 계기: ca-tmpl 작업에서 "branch 마치고 나면 다음 세션에서 이 결정의 _이유_ 와 _대안_ 을 다시 답할 수 없는" 반복 문제. 사용자가 매번 채팅으로 "branch-note 도 써줘" 를 요청하던 비용을 줄이기 위해 _agent prompt + repo-local rule_ 두 층에 capture rule 을 추가.
|
||||
- 예상 제목 후보:
|
||||
- "구현 완료" 의 정의에 지식 캡처를 포함하기 — repo-local workflow 설계
|
||||
- LLM 에게 "코드 끝나면 branch-note 도 써" 라고 매번 요청하지 않으려면
|
||||
- branch-note · errors · interviews · blog-topics 의 _네 갈래_ 캡처 워크플로우
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- ca-tmpl repo 의 종료 조건 워크플로우는 `AGENTS.md` + 루트 `CLAUDE.md` + `.agents/plugins/ca-superpowers/rules/llm-wiki-capture.md` + `.claude/skills/ca-superpowers-workflow/SKILL.md` 의 _네 곳_ 에 capture rule 이 흩어져 있고, 각 위치는 트리거가 다르다 (대화 시작, 모듈별 작업, 비-자명한 구현 종료, skill 호출) — 근거: `feature-architecture-enforcement-rules.md` §진행 중 메모 2026-05-28 "워크플로우 반영: ca-tmpl repo 내부 `AGENTS.md`, `CLAUDE.md`, `.agents/plugins/ca-superpowers/`, `.claude/`, `.codex/` 지침에 ... 캡처 규칙을 추가".
|
||||
- 캡처 단위는 4종 — `raw/branch-notes/`, `raw/errors/`, `raw/interviews/`, `raw/blog-topics/` — 그리고 _canonical_ (`wiki/...`) 은 _명시 요청 없으면 생성 금지_ — 근거: `.agents/plugins/ca-superpowers/rules/llm-wiki-capture.md` §"canonical 추출 요청이 없는 한 wiki/blog/wiki/interview/wiki/portfolio/wiki/concepts/wiki/projects를 바로 만들지 않는다".
|
||||
- 모든 derived note 는 `## Parent` 로 branch-note 에 upward link, branch-note 는 `## Cluster` 로 derived note 에 downward link — _양방향 nav_ 가 의무 — 근거: 동일 rule 4번 ("파생 문서는 `## Parent`에서 branch-note로 upward link하고, branch-note의 `## Cluster / 묶음`에는 파생 문서 wikilink를 되돌려 적는다").
|
||||
- 종료 응답에는 반드시 `Wiki capture` 라인 — 갱신된 노트 / 의도적으로 생성 안 한 derived note (없음 명시) / 차단 (BLOCKED) 중 하나를 보고 — 근거: 동일 rule §Final Response Requirement.
|
||||
- 경험 후보:
|
||||
- 본 결정을 _다른 코드 결정과 대등한 격_ 으로 branch-note 의 §결정 사항 마지막 한 줄로 추가 (`2026-05-28: non-trivial 구현 종료 조건에 LLM Wiki capture를 포함한다`) — 근거: `feature-architecture-enforcement-rules.md` §결정 사항 마지막 항목.
|
||||
- 본 결정의 _대안 비교_ 까지 명시: (a) 사용자 수동 요청 (누락 위험), (b) Wiki vault 내부 규칙만 (ca-tmpl 작업자가 인식 못함), (c) ca-tmpl repo-local rule (채택) — 근거: 동일 §결정 사항 마지막 항목 `검토한 대안:`.
|
||||
- 본 글의 자매 branch (`feature-application-port-usecase-contract`) 가 _이 워크플로우를 실제로 적용한 첫 사례_ — branch-note 갱신 + 3개 derived note (error, interview, blog-topic) 생성을 마지막 응답에 `Wiki capture` 로 보고 — 근거: [[raw/branch-notes/feature-application-port-usecase-contract]] §완료 후 정리 + §Cluster.
|
||||
- 워크플로우 패치 도중 도구 자동 승인 검토가 차단되어 사용자 명시 승인 후 재개한 사례 — 근거: 파생 에러 [[raw/errors/apply-patch-auto-approval-rejected-2026-05-28]].
|
||||
- 의견 / 해석 후보:
|
||||
- 문서화는 _후행 작업_ 이 아니라 _완료 조건의 일부_ 가 되어야 누락이 줄어든다. 단, "기록을 강제하는 자동 장치 (CI / git hook)" 까지는 아직 가지 않았다 — 현재는 _agent workflow rule_ 수준이며 honesty 차원에서 명시 — 근거: `feature-architecture-enforcement-rules.md` 진행 중 메모 2026-05-28 "등급: `documented-only` (repo-local workflow docs; 자동 강제 장치 아님)".
|
||||
- 캡처를 _네 갈래_ (branch / error / interview / blog-topic) 로 _구조화_ 하면 코드 끝난 뒤 _즉시_ 처분 가능 — "branch-note 만 적으면 errors 가 묻히고, errors 만 적으면 interview 가 묻힘". 4갈래 분리가 _분실 방지_ 의 핵심.
|
||||
- **derived note 가 _없을 때_ "없음" 을 명시하는 것** 이 의외로 중요하다 (`Errors: 없음`, `Interview prep: 없음`). 빈 cluster section 은 "정말 없는지 검토했음" 의 증거이고, _없으면 그냥 비워두는 것_ 보다 사후 검증 가능.
|
||||
- 모든 raw note 가 _line-cited evidence_ 를 갖는 것이 (전체 글의 모든 사실 후보를 `D3` / `AT-TX-C5` 같은 ID 로 인용) **canonical wiki 로 승급할 때 _재검증 가능_** 하게 만드는 가장 큰 차이.
|
||||
|
||||
## Outline seed
|
||||
|
||||
> 각 섹션 옆에 `→ 핵심 메시지` 를 함께 명시한다.
|
||||
|
||||
1. 문제 — 코드는 끝났는데 _왜 이렇게 했는지_ 와 _고려한 대안_ 이 채팅 로그에만 남아 다음 세션에서 휘발 → **"기록을 매번 요청한다" 는 비용을 줄이는 게 글의 출발점.**
|
||||
2. 캡처를 _완료 조건_ 으로 옮기기 — repo-local `AGENTS.md` + `CLAUDE.md` + `llm-wiki-capture.md` + skill 의 _네 위치_ → **트리거를 코드 작업 가까이에 둬야 워크플로우가 실행된다.**
|
||||
3. 네 갈래 raw 구조 — branch-notes / errors / interviews / blog-topics → **분리하지 않으면 한 갈래가 다른 갈래를 묻는다.**
|
||||
4. _없을 때 없음을 명시_ — empty cluster section 의 honesty → **"검토 안 함" 과 "검토 후 없음" 을 구분.**
|
||||
5. 라인 인용으로 _승급 가능_ 하게 — `D3`, `AT-TX-C5` 인용 패턴 → **raw 가 canonical 로 갈 때 _재검증 가능_ 한 것은 line-cited evidence 뿐.**
|
||||
6. 한계 — 자동 강제 (CI / git hook) 아님, _agent workflow rule_ 수준 → **honesty 차원에서 documented-only 등급을 글에 명시.**
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-skeleton/knowledge-capture-workflow.md` 후보:
|
||||
- ca-tmpl 의 실제 4 위치 capture rule (AGENTS / CLAUDE / llm-wiki-capture / skill) + 종료 응답의 `Wiki capture` 라인 형식.
|
||||
- `feature-application-port-usecase-contract` 의 적용 사례 (branch-note + 3 derived notes).
|
||||
- "양방향 nav" 강제 (`## Parent` ↔ `## Cluster`) 의 검증 방법.
|
||||
- `wiki/concepts/post-implementation-knowledge-capture.md` 후보:
|
||||
- "구현 완료 조건에 지식 캡처 포함" 의 일반 원칙 (project-agnostic).
|
||||
- 캡처 단위를 4갈래 (branch / errors / interviews / blog-topics) 로 분리하는 _why_.
|
||||
- canonical 승급의 게이트 (line-cited evidence + status grading).
|
||||
- 필요한 추가 검증:
|
||||
- 이후 다른 branch 작업에서 실제로 derived note 가 _자동으로_ 기록되는지 반복 관찰 (현재 1 사례 = `feature-application-port-usecase-contract`).
|
||||
- 자동 강제 장치 (git hook / CI step) 를 추가했을 때의 비용 / 효과.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-architecture-enforcement-rules]] — workflow rule 반영 결정 (§결정 사항 2026-05-28 마지막 항목) + §진행 중 메모 2026-05-28 워크플로우 반영 내역.
|
||||
- [[raw/branch-notes/feature-application-port-usecase-contract]] — 본 워크플로우의 첫 적용 사례 (Wiki capture 결과: branch-note 갱신 + 3 derived notes).
|
||||
- [[raw/errors/apply-patch-auto-approval-rejected-2026-05-28]] — workflow 문서 패치 중 도구 차단 사례.
|
||||
- [[raw/interviews/post-implementation-knowledge-capture]] — 같은 작업에서 파생된 예상 면접 질문.
|
||||
- repo file: `ca-tmpl/.agents/plugins/ca-superpowers/rules/llm-wiki-capture.md` — Required Capture Sequence + When No Derived Note Is Needed + Final Response Requirement (4 sections).
|
||||
- repo file: `ca-tmpl/AGENTS.md` §LLM Wiki 캡처 워크플로우.
|
||||
- repo file: `ca-tmpl/CLAUDE.md` §LLM Wiki capture.
|
||||
- repo file: `ca-tmpl/.claude/skills/ca-superpowers-workflow/SKILL.md` §LLM Wiki Capture Before Completion.
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: 문서 규칙 _만_ 으로 _장기적으로_ agent session 누락이 줄어드는지 (현재 1 사례 검증).
|
||||
- 아직 확인해야 할 사실: 자동 강제 장치 (git hook / CI step / agent runtime check) 가 _필요한지_, 아니면 documented rule 로 충분한지.
|
||||
- 과장하면 안 되는 부분: 본 워크플로우는 **`documented-only`** 다. CI / git hook 으로 자동 강제하지 않음. "자동으로 캡처된다" 같은 표현 금지.
|
||||
- 과장하면 안 되는 부분: agent runtime 이 본 rule 파일들을 _실제로_ 로드하는지는 plugin/skill 구현 의존이며 ca-tmpl repo 외부 의존성 — 글에서 "어떤 runtime 에서도 동작" 같은 일반화 금지.
|
||||
- 블로그로 쓰기 전에 필요한 canonical 정제: 2~3개 추가 branch 사례를 거쳐 워크플로우 안정성 확인 → `wiki/projects/ca-skeleton/knowledge-capture-workflow.md` 정제 → blog 초안.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: 신규 `wiki/projects/ca-tmpl/knowledge-capture-workflow.md` 에 post-implementation knowledge capture workflow 글감으로 반영한다.
|
||||
- 다음 단계: blogify 전 자동 강제 장치가 아니라 documented workflow rule임을 유지한다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/feature-architecture-enforcement-rules]] (결정), [[raw/branch-notes/feature-application-port-usecase-contract]] (첫 적용 사례).
|
||||
- 관련 errors: [[raw/errors/apply-patch-auto-approval-rejected-2026-05-28]] (workflow 문서 패치 중 도구 차단).
|
||||
- 관련 interview prep: [[raw/interviews/post-implementation-knowledge-capture]].
|
||||
- 관련 blog topics: [[raw/blog-topics/clean-architecture-boundary-enforcement-2026-05-28]] (같은 branch 의 자매 글감), [[raw/blog-topics/transaction-port-abstraction-over-spring-transactional-2026-05-28]] (첫 적용 사례에서 나온 글감 — 본 워크플로우의 _효과 증거_).
|
||||
- derived blog: 생성 전. 후보 `wiki/blog/post-implementation-knowledge-capture-workflow-YYYY-MM-DD.md`.
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/repository-capability-archunit-fitness-function-2026-07-02.md
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
title: blog-topic / repository-capability-archunit-fitness-function
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-repository-access-permission-contract]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, persistence, testing, archunit, static-analysis, clean-architecture]
|
||||
created: 2026-07-02
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: repository-capability-archunit-fitness-function
|
||||
|
||||
> Layer: `raw/blog-topics/` — 작업 중 나온 블로그 글감 원석. 최종 블로그는 canonical 정제 후 `wiki/blog/`에서 작성한다.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-repository-access-permission-contract]] — repository 접근 capability를 annotation, registry, ArchUnit rule로 계약화한 branch에서 나온 글감.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-07-02
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-repository-access-permission-contract]]
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: repository 접근 권한을 사람의 리뷰 기억에 맡기지 않고 annotation + registry + ArchUnit fitness function으로 강제한 이유를 정리한다.
|
||||
- 예상 제목 후보:
|
||||
- Repository 접근 권한을 ArchUnit rule로 고정하기
|
||||
- Clean Architecture에서 persistence capability를 계약으로 다루기
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- branch가 repository capability registry drift와 3층 검증을 blog topic 후보로 명시한다 — 근거 후보: [[raw/branch-notes/feature-repository-access-permission-contract]] section+line `:311-314`.
|
||||
- 경험 후보:
|
||||
- 기존 boundary enforcement topic은 계층 의존성 중심이고, 이 글감은 repository capability annotation/registry coherence라는 좁은 실패 모드를 다룬다.
|
||||
- 의견/해석 후보:
|
||||
- repository 접근 제한은 "어느 package에서 접근했는가"보다 "어떤 capability로 접근을 허용했는가"까지 내려가야 drift를 줄일 수 있다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. package boundary만으로는 repository intent를 알 수 없다 — 접근 권한의 의미를 capability로 드러낸다.
|
||||
2. annotation, registry, ArchUnit의 역할 분리 — 선언, 목록, 검증이 서로를 보완한다.
|
||||
3. fitness function의 한계 — helper/mapper indirection까지 자동 검출한다고 과장하지 않는다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/repository-access-capability-contract.md` 후보:
|
||||
- ca-tmpl repository access permission contract 구현 사실.
|
||||
- `wiki/concepts/archunit-fitness-function.md` 후보:
|
||||
- ArchUnit rule을 architecture fitness function으로 사용하는 일반 개념.
|
||||
- 필요한 추가 검증:
|
||||
- annotation 이름, registry schema, ArchUnit rule/test anchor.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-repository-access-permission-contract]] — blog seed와 capability contract 근거.
|
||||
- [[raw/blog-topics/clean-architecture-boundary-enforcement-2026-05-28]] — 기존 boundary topic과의 경계.
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: helper/mapper 우회 경로 검출 가능 범위.
|
||||
- 과장하면 안 되는 부분: 모든 repository misuse를 자동 검출한다고 쓰면 안 된다. 정적 분석 rule이 볼 수 있는 구조로 제한한다.
|
||||
- 블로그로 쓰기 전에 필요한 canonical 정제: project 문서의 implemented/local evidence 정리.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/data-layer-persistence-cache-outbound.md` 에 repository capability ArchUnit fitness function 글감으로 반영했다.
|
||||
- 다음 단계: target canonical이 아직 `draft` 이므로 `blogify` 전 review/verify가 필요하다. 모든 repository misuse를 자동 검출한다고 쓰지 않는다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/feature-repository-access-permission-contract]]
|
||||
- 관련 error:
|
||||
- 관련 interview prep:
|
||||
- derived blog: 생성 전. 생성 시 `wiki/blog/repository-capability-archunit-fitness-function-YYYY-MM-DD.md` 후보
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/runbook-coverage-junit-contract-test-2026-07-02.md
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
title: blog-topic / runbook-coverage-junit-contract-test
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-operational-runbook-contract]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, observability, testing, junit5, api-contract]
|
||||
created: 2026-07-02
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: runbook-coverage-junit-contract-test
|
||||
|
||||
> Layer: `raw/blog-topics/` — 작업 중 나온 블로그 글감 원석. 최종 블로그는 canonical 정제 후 `wiki/blog/`에서 작성한다.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-operational-runbook-contract]] — 운영 runbook coverage gate를 테스트로 구현한 경험에서 나온 글감.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-07-02
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-operational-runbook-contract]]
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: 운영 runbook 링크가 문서에만 존재하는지, 실제 error registry와 연결되어 release를 막을 수 있는지 JUnit contract test로 확인한 이유를 정리한다.
|
||||
- 예상 제목 후보:
|
||||
- Runbook coverage를 JUnit 테스트로 막아본 이유
|
||||
- 운영 문서도 release gate가 될 수 있을까
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- branch에 runbook coverage gate와 link-check smoke가 구현/검증된 것으로 정리되어 있고 blog topic 후보가 Cluster에 명시되어 있다 — 근거 후보: [[raw/branch-notes/feature-operational-runbook-contract]] line `:301`.
|
||||
- 경험 후보:
|
||||
- Gradle task가 아니라 test suite에 넣는 방식은 release-blocking semantics를 명확히 하는 장점이 있다.
|
||||
- 의견/해석 후보:
|
||||
- runbook은 "있으면 좋은 문서"가 아니라 retryable failure와 연결될 때 운영 계약이 된다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. error code와 runbook을 따로 관리하면 drift가 생긴다 — registry row와 문서 링크를 같은 gate로 본다.
|
||||
2. JUnit으로 문서 coverage를 검사하는 이유 — application test lifecycle에 운영 artifact를 포함한다.
|
||||
3. link-check와 semantic coverage는 다르다 — URL이 살아 있어도 runbook이 충분하다는 뜻은 아니다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/operational-runbook-coverage-gate.md` 후보:
|
||||
- ca-tmpl runbook coverage gate 구현 사실.
|
||||
- `wiki/concepts/runbook-coverage-gate.md` 후보:
|
||||
- error registry와 runbook coverage를 연결하는 일반 패턴.
|
||||
- 필요한 추가 검증:
|
||||
- 테스트명, registry schema, retryable=true row 처리 방식.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-operational-runbook-contract]] — runbook coverage gate topic seed와 구현/검증 근거.
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: link-check smoke와 coverage gate의 정확한 차이.
|
||||
- 과장하면 안 되는 부분: runbook 내용 품질까지 자동 보장한다고 쓰면 안 된다. coverage와 link existence 검증으로 제한한다.
|
||||
- 블로그로 쓰기 전에 필요한 canonical 정제: runbook coverage gate project 문서 생성 또는 observability 문서 통합.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/observability-log-metric-trace-runbook.md` 에 runbook coverage JUnit contract test 글감으로 반영했다.
|
||||
- 다음 단계: source canonical이 `verified` 상태이므로 이후 `blogify` 대상으로 삼을 수 있다. 단 runbook 내용 품질까지 자동 보장한다고 쓰지 않는다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/feature-operational-runbook-contract]]
|
||||
- 관련 error:
|
||||
- 관련 interview prep:
|
||||
- derived blog: 생성 전. 생성 시 `wiki/blog/runbook-coverage-junit-contract-test-YYYY-MM-DD.md` 후보
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/sample-domain-contract-fixture-clean-architecture-2026-06-10.md
|
||||
@@ -0,0 +1,84 @@
|
||||
---
|
||||
title: blog-topic / sample-domain-contract-fixture-clean-architecture
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-sample-domain-contract-fixture]
|
||||
related_projects: [ca-skeleton]
|
||||
tags: [blog-topic, ca-skeleton, architecture, testing, clean-architecture, api-contract]
|
||||
created: 2026-06-10
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: sample-domain-contract-fixture-clean-architecture
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-sample-domain-contract-fixture]] — sample-portfolio fixture를 문서 계약대로 구현하면서 나온 글감.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-06-10
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-sample-domain-contract-fixture]]
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: Clean Architecture 템플릿의 샘플 도메인은 데모 기능이 아니라 validation, mapper, transaction, response, conflict 계약을 실제 흐름으로 검증하는 fixture가 될 수 있다.
|
||||
- 예상 제목 후보:
|
||||
- Clean Architecture 템플릿에 sample domain fixture를 남기는 이유
|
||||
- 샘플 기능이 아니라 계약 검증 도구로서의 WorkLog 도메인
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- ca-tmpl은 `sample-portfolio`를 production module이 의존하지 않는 fixture/reference consumer로 둔다 — 근거 후보: [[raw/branch-notes/feature-sample-domain-contract-fixture]] D2/D5.
|
||||
- 2026-06-10 구현은 `WorkLogStatus` 상태 머신과 `WorkLogOwner` minimum model을 domain→application→persistence→web에 연결했다 — 근거 후보: [[raw/branch-notes/feature-sample-domain-contract-fixture]] §진행 중 메모.
|
||||
- 경험 후보:
|
||||
- focused RED에서 missing enum/value object/accessor/command status patch 컴파일 실패를 확인하고, GREEN 후 `:sample-portfolio:test`, architecture guard, full `test`, `check`를 통과시켰다 — 근거 후보: [[raw/branch-notes/feature-sample-domain-contract-fixture]] §진행 중 메모.
|
||||
- 의견/해석 후보:
|
||||
- 템플릿의 sample은 "보여주기용 CRUD"보다 "경계 계약을 깨뜨리면 테스트가 실패하는 살아있는 fixture"일 때 유지 비용을 정당화하기 쉽다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. 문제: sample을 지우면 contract 흐름 검증이 빈다 — validation/mapper/error/transaction이 unit test 조각으로만 남는 위험.
|
||||
2. 설계: sample-portfolio를 production과 분리된 fixture consumer로 둔다 — 모듈 경계와 ArchUnit guard가 핵심.
|
||||
3. 구현: WorkLog minimum model을 계층별로 흘린다 — status state machine, owner, optimistic version, response/persistence round-trip.
|
||||
4. 검증: RED-GREEN과 architecture/full Gradle check — contract fixture는 테스트 증거로 말한다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/sample-domain-contract-fixture.md` 후보:
|
||||
- sample-portfolio fixture의 실제 구현 파일과 검증 명령.
|
||||
- `wiki/concepts/sample-domain-contract-fixture.md` 후보:
|
||||
- sample domain을 contract fixture로 설계하는 일반 패턴.
|
||||
- 필요한 추가 검증:
|
||||
- canonical `sample-fixture-and-adoption` 명명 drift 정리.
|
||||
- sample-off/profile isolation owner branch 결과 확인.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-sample-domain-contract-fixture]] — 구현 결정, scenario matrix, 2026-06-10 검증 기록.
|
||||
- [[raw/errors/gradle-wrapper-lock-read-only-sandbox-2026-06-10]] — 검증 중 sandbox tooling 이슈.
|
||||
- [[raw/interviews/sample-domain-contract-fixture-clean-architecture]] — 같은 작업에서 나온 면접 질문 원석.
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: sample-off CI/profile isolation 구현 branch의 최종 상태.
|
||||
- 과장하면 안 되는 부분: 이번 글감은 locally-verified 구현 원석이며 canonical 정제 전이다.
|
||||
- 블로그로 쓰기 전에 필요한 canonical 정제: branch-note의 implemented claims를 `wiki/projects/ca-tmpl/` 문서로 승격.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/sample-fixture-and-adoption.md` 에 sample domain contract fixture 글감으로 반영했다.
|
||||
- 다음 단계: target canonical이 아직 `draft` 이므로 `blogify` 전 review/verify가 필요하다. sample domain을 production feature처럼 쓰지 않는다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/feature-sample-domain-contract-fixture]]
|
||||
- 관련 error: [[raw/errors/gradle-wrapper-lock-read-only-sandbox-2026-06-10]]
|
||||
- 관련 interview prep: [[raw/interviews/sample-domain-contract-fixture-clean-architecture]]
|
||||
- derived blog: 생성 전.
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/sample-fixture-dual-mode-build-matrix-2026-06-25.md
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
title: Sample fixture dual-mode build matrix
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-sample-removal-adoption-contract]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, clean-architecture, gradle, template-repository, testing]
|
||||
created: 2026-06-25
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# Sample fixture dual-mode build matrix
|
||||
|
||||
## Parent
|
||||
|
||||
- [[raw/branch-notes/feature-sample-removal-adoption-contract]]
|
||||
|
||||
## Angle
|
||||
|
||||
템플릿 저장소에서 예제 도메인을 완전히 삭제하지 않고도, production/core 계약이 예제 코드에 의존하지 않음을 증명하는 방법.
|
||||
|
||||
## Outline
|
||||
|
||||
1. Sample module을 삭제하지 않는 이유: fixture, reference, contract coverage.
|
||||
2. Runtime toggle이 부적절했던 이유: production app에는 애초에 sample runtime wiring이 없다.
|
||||
3. Dual-mode를 build/test matrix로 재정의: sample-on과 sample-off.
|
||||
4. Gradle 구현: declarable fixture configuration, custom source set, dedicated test task.
|
||||
5. 테스트 정리: core test의 sample import 제거, sample-owned contract는 sample module로 이동.
|
||||
6. CI release gate: hosted workflow wiring과 local gate matrix verification.
|
||||
7. 함정: dependency locks, ArchUnit import option, empty corpus, static-analysis policy.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. Sample module을 삭제하지 않는 이유를 fixture, reference, contract coverage 관점으로 설명한다.
|
||||
2. runtime toggle이 아니라 sample-on/sample-off build matrix가 필요한 이유를 정리한다.
|
||||
3. Gradle fixture configuration, custom source set, dedicated test task의 역할을 나눈다.
|
||||
4. CI release gate와 local verification에서 sample-off가 실제로 막아야 하는 실패를 기록한다.
|
||||
|
||||
## Evidence to cite later
|
||||
|
||||
- `src/app-bootstrap/build.gradle` `sampleFixture` / `sampleOffTest`.
|
||||
- `SampleRemovalSmokeContractTest`.
|
||||
- `ProductionClassImportOption`.
|
||||
- `.github/workflows/ci-quality-gates.yml` `sample-off` job.
|
||||
- [[raw/errors/gradle-custom-source-set-isolation-failures-2026-06-25]].
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-06-25
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-sample-removal-adoption-contract]]
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: sample domain을 runtime toggle이 아니라 sample-on/sample-off build matrix로 격리해 production/core 계약이 sample에 의존하지 않음을 검증한다.
|
||||
- 예상 제목 후보:
|
||||
- Sample-off build matrix로 템플릿 의존성 검증하기
|
||||
- 예제 도메인을 지우지 않고 production 경계를 증명하기
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- sample fixture source set과 sample-off test task가 sample 의존성 격리를 검증한다.
|
||||
- 의견/해석 후보:
|
||||
- template repository에서 sample은 제거 대상이 아니라 contract fixture일 수 있다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/sample-fixture-and-adoption.md` 후보:
|
||||
- sample fixture dual-mode build matrix 글감.
|
||||
- 필요한 추가 검증:
|
||||
- 현재 `sampleFixture`, `sampleOffTest`, CI sample-off job 존재 여부.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-sample-removal-adoption-contract]]
|
||||
- [[raw/errors/gradle-custom-source-set-isolation-failures-2026-06-25]]
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: hosted CI sample-off job 실제 차단 검증 여부.
|
||||
- 과장하면 안 되는 부분: runtime toggle로 검증한다고 쓰지 않고 build/test matrix로 제한한다.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/sample-fixture-and-adoption.md` 에 sample fixture dual-mode build matrix 글감으로 반영한다.
|
||||
- 다음 단계: blogify 전 local vs hosted CI evidence를 분리한다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/feature-sample-removal-adoption-contract]]
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/secret-source-port-restart-only-rotation-2026-07-02.md
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
title: blog-topic / secret-source-port-restart-only-rotation
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-secrets-config-source-contract]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, security, spring-boot, externalized-config, api-contract]
|
||||
created: 2026-07-02
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: secret-source-port-restart-only-rotation
|
||||
|
||||
> Layer: `raw/blog-topics/` — 작업 중 나온 블로그 글감 원석. 최종 블로그는 canonical 정제 후 `wiki/blog/`에서 작성한다.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-secrets-config-source-contract]] — `SecretSource` port와 restart-only reload guard 구현 경험에서 나온 글감.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-07-02
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-secrets-config-source-contract]]
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: secret source를 설정 문서의 문자열 규칙으로만 두지 않고 `SecretSource` port, restart-only rotation, `@RefreshScope` 금지로 닫은 이유를 정리한다.
|
||||
- 예상 제목 후보:
|
||||
- SecretSource port로 secret loading 경계를 고정하기
|
||||
- 왜 ca-tmpl은 secret reload를 restart-only로 제한했나
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- branch에 `SecretSource` port, restart-only reload, `@RefreshScope` 금지, locally-verified 구현 기록이 있다 — 근거 후보: [[raw/branch-notes/feature-secrets-config-source-contract]] D5 line `:131`, implementation `:149-159`, `:207-223`, closure `:348-352`.
|
||||
- 경험 후보:
|
||||
- `.env` drift gate와 달리 이 글감은 secret source abstraction과 reload lifecycle을 다룬다.
|
||||
- 의견/해석 후보:
|
||||
- secret rotation을 runtime reload로 풀면 lifecycle과 connection/cache state 문제가 따라오기 때문에 skeleton 기본값은 좁게 잡는 편이 안전하다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. secret은 config key와 다르다 — source, masking, reload lifecycle을 함께 봐야 한다.
|
||||
2. `SecretSource` port가 주는 이점 — adapter 교체 가능성과 application boundary를 동시에 얻는다.
|
||||
3. restart-only rotation의 trade-off — 단순하고 검증 가능하지만 runtime rotation 요구는 별도 설계가 필요하다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/security-baseline-jwt-actuator-secrets.md` 후보:
|
||||
- ca-tmpl secret config source 구현 사실.
|
||||
- `wiki/concepts/security-baseline-jwt-actuator-secrets.md` 후보:
|
||||
- externalized secret source와 reload lifecycle 일반 개념.
|
||||
- 필요한 추가 검증:
|
||||
- `SecretSource` 구현체, test anchor, `@RefreshScope` 금지 rule 여부.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-secrets-config-source-contract]] — secret source port와 restart-only reload 근거.
|
||||
- [[raw/blog-topics/env-example-drift-gate-gradle-2026-06-06]] — 인접하지만 다른 `.env` drift topic.
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: runtime rotation 미지원 범위와 future extension point.
|
||||
- 과장하면 안 되는 부분: Vault/KMS dynamic secret 운영을 구현했다고 쓰면 안 된다. restart-only contract로 제한한다.
|
||||
- 블로그로 쓰기 전에 필요한 canonical 정제: secret config project 문서 verified 범위 확인.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/security-baseline-jwt-actuator-secrets.md` 에 SecretSource/restart-only rotation 글감으로 반영했다.
|
||||
- 다음 단계: target canonical이 아직 `draft` 이므로 `blogify` 전 review/verify가 필요하다. Vault/KMS dynamic secret 운영을 구현했다고 쓰지 않는다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/feature-secrets-config-source-contract]]
|
||||
- 관련 error:
|
||||
- 관련 interview prep:
|
||||
- derived blog: 생성 전. 생성 시 `wiki/blog/secret-source-port-restart-only-rotation-YYYY-MM-DD.md` 후보
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/skip-locked-outbox-per-aggregate-fifo-gate-2026-06-11.md
|
||||
@@ -0,0 +1,90 @@
|
||||
---
|
||||
title: blog-topic / skip-locked-outbox-per-aggregate-fifo-gate-2026-06-11
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-domain-event-outbox-contract]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, outbox, skip-locked, fifo, postgresql, testcontainers, clean-architecture]
|
||||
created: 2026-06-11
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: skip-locked-outbox-per-aggregate-fifo-gate-2026-06-11
|
||||
|
||||
> Layer: `raw/blog-topics/` — 채용공고가 아닌 작업·학습·트러블슈팅에서 나온 블로그 글감 원석. canonical 정제 전 raw 후보이며, `wiki/blog/` 직접 생성 근거가 아니다.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-domain-event-outbox-contract]] — outbox relay 구현 중 SKIP LOCKED 와 per-aggregate FIFO 의 충돌을 claim query 의 `NOT EXISTS` 게이트로 해소한 경험 단독 추출.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- `FOR UPDATE SKIP LOCKED` 폴링 outbox 는 멀티 인스턴스 claim 경합을 우아하게 풀지만, PostgreSQL/MySQL 공식 문서가 명시하듯 **순서를 깬다(inconsistent view)**. "per-aggregate FIFO 보장" 계약과 정면 충돌 — 1차 구현이 실제로 게이트를 빠뜨려 리뷰에서 잡혔고(head FAILED 인데 tail 이 먼저 발행되는 경로), 수정 과정 자체가 글감.
|
||||
|
||||
## 글감 코어 / Core idea
|
||||
|
||||
- **문제**: SKIP LOCKED 는 "락 못 잡으면 건너뛴다" — 같은 aggregate 의 이벤트 e1, e2 가 서로 다른 publisher 에 분산 claim 되거나, e1 이 FAILED(backoff 대기) 인 동안 e2 가 먼저 나가면 consumer 가 순서 역전을 본다.
|
||||
- **해결**: claim query 에 상관 서브쿼리 게이트 —
|
||||
`NOT EXISTS (SELECT 1 FROM outbox_event p WHERE p.aggregate_id = o.aggregate_id AND p.occurred_at < o.occurred_at AND p.status <> 'PUBLISHED')`.
|
||||
배치에는 aggregate 당 head 1건만 들어오고, head 가 비-PUBLISHED(FAILED/IN_FLIGHT/**DEAD 포함**)인 동안 후행은 구조적으로 claim 불가. READ_COMMITTED 스냅숏을 읽는 게이트라 보수적(차단 우위)으로 동작.
|
||||
- **트레이드오프 (strict FIFO)**: DEAD 가 후행을 영구 차단 → poison event 1건이 aggregate 스트림을 멈춘다. 자동 우회 대신 runbook 수동 처분(재발행 `PENDING` 리셋 vs skip `PUBLISHED` 마킹 — 이벤트 갭 승인 필요)으로 설계. backlog 증가는 `outbox.pending.size` P2 alert 가 감지.
|
||||
- **검증**: Testcontainers PG 계약 테스트 3종 — ① 2개 Spring context × 1000 rows 동시 claim, 합계 1000·중복 0 (SKIP LOCKED 단일 claim), ② head FAILED/DEAD 시 tail 차단·head PUBLISHED 후 해제 (FIFO 게이트), ③ `next_attempt_at` 을 visibility timeout 으로 재사용한 IN_FLIGHT orphan 재claim.
|
||||
- **부가 발견**: 공유 HikariDataSource 를 두 context 에 등록하면 첫 close 가 풀을 닫는다(`setDestroyMethodName("")` 필요) — [[raw/errors/testcontainers-two-context-shared-datasource-close-2026-06-11]].
|
||||
|
||||
## 왜 의미 있나 / Why it matters
|
||||
|
||||
- 국내외 outbox 글 대부분이 "SKIP LOCKED 로 폴링하면 된다"에서 멈춘다. **ordering 계약과의 충돌**과 그 해소(쿼리 레벨 게이트 + strict FIFO 의 운영 비용 명문화 + 계약 테스트로 고정)까지 다루는 글은 드물다.
|
||||
- fail-open publisher(use case 직발행)와 fail-closed publisher(outbox relay)가 한 코드베이스에 공존해야 하는 이유도 곁들일 수 있는 실전 소재.
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: `FOR UPDATE SKIP LOCKED`는 claim 경합을 줄이지만 per-aggregate FIFO 보장과 충돌할 수 있어 head gate가 필요하다.
|
||||
- 예상 제목 후보:
|
||||
- SKIP LOCKED outbox에서 순서를 지키는 방법
|
||||
- per-aggregate FIFO를 깨지 않는 outbox claim query
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- `SKIP LOCKED`는 잠긴 row를 skip하므로 동일 aggregate의 tail이 먼저 claim될 수 있다.
|
||||
- `NOT EXISTS` head gate는 앞선 미발행 row가 있을 때 tail claim을 막는 방식이다.
|
||||
- 의견/해석 후보:
|
||||
- strict FIFO는 poison event가 aggregate stream을 멈추는 운영 비용을 동반한다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. SKIP LOCKED가 해결하는 문제와 새로 만드는 ordering 문제를 분리한다.
|
||||
2. head gate query로 per-aggregate FIFO를 보강한다.
|
||||
3. DEAD row가 tail을 막는 strict FIFO의 운영 비용과 runbook 필요성을 설명한다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/transactional-outbox-pattern.md` 후보:
|
||||
- SKIP LOCKED vs per-aggregate FIFO gate 글감.
|
||||
- 필요한 추가 검증:
|
||||
- branch-note/code 기준 실제 Testcontainers 검증 여부.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-domain-event-outbox-contract]]
|
||||
- [[raw/errors/testcontainers-two-context-shared-datasource-close-2026-06-11]]
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: 현재 canonical의 구현 없음 기록과 raw seed의 검증 주장 간 차이.
|
||||
- 과장하면 안 되는 부분: SKIP LOCKED가 ordering을 자동 보장한다고 쓰지 않는다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/feature-domain-event-outbox-contract]]
|
||||
- 관련 error: [[raw/errors/testcontainers-two-context-shared-datasource-close-2026-06-11]]
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/transactional-outbox-pattern.md` 에 SKIP LOCKED와 per-aggregate FIFO gate 글감으로 반영했다.
|
||||
- 다음 단계: target canonical이 아직 `draft` 이므로 `blogify` 전 outbox 구현·Testcontainers 검증 여부를 branch-note/code 기준으로 재확인한다.
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/spring-actuator-health-probe-group-split-2026-07-02.md
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
title: blog-topic / spring-actuator-health-probe-group-split
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-runtime-health-lifecycle-contract]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, runtime, spring-boot, kubernetes, graceful-shutdown, sigterm]
|
||||
created: 2026-07-02
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: spring-actuator-health-probe-group-split
|
||||
|
||||
> Layer: `raw/blog-topics/` — 작업 중 나온 블로그 글감 원석. 최종 블로그는 canonical 정제 후 `wiki/blog/`에서 작성한다.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-runtime-health-lifecycle-contract]] — liveness/readiness/startup probe group split 구현/검증 후보에서 나온 글감.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-07-02
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-runtime-health-lifecycle-contract]]
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: Spring Actuator health group을 liveness/readiness/startup으로 나누고, startup guard와 shutdown lifecycle을 같은 운영 계약으로 본 이유를 정리한다.
|
||||
- 예상 제목 후보:
|
||||
- Spring Actuator health group을 세 개로 나눈 이유
|
||||
- readiness와 startup을 같은 endpoint로 보면 생기는 문제
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- branch에 runtime health lifecycle probe group과 startup guard 구현/검증 기록이 있다 — 근거 후보: [[raw/branch-notes/feature-runtime-health-lifecycle-contract]] section+line `:359-385`.
|
||||
- 경험 후보:
|
||||
- blog seed가 직접 명시되지는 않았지만 구현 기록과 test edge가 충분하다는 lane-08 판정이 있다.
|
||||
- 의견/해석 후보:
|
||||
- liveness/readiness/startup은 모두 health endpoint지만 실패 시 orchestration action이 다르므로 분리해야 한다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. liveness/readiness/startup은 같은 "건강"이 아니다 — restart, traffic removal, startup delay라는 action이 다르다.
|
||||
2. Spring Actuator group split이 주는 구조 — dependency readiness와 process liveness를 나눈다.
|
||||
3. auth/exposure blocker를 분리하기 — probe가 있어도 network/auth 설정이 막으면 운영 계약이 완성되지 않는다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/runtime-health-lifecycle-contract.md` 후보:
|
||||
- ca-tmpl runtime health lifecycle implementation.
|
||||
- `wiki/concepts/spring-actuator-health-probes.md` 후보:
|
||||
- Spring Actuator health groups와 Kubernetes probes 일반 개념.
|
||||
- 필요한 추가 검증:
|
||||
- actuator exposure/auth, probe endpoint path, Kubernetes manifest 연결 여부.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-runtime-health-lifecycle-contract]] — probe group split과 startup guard 근거.
|
||||
- [[raw/blog-topics/spring-boot-startup-exit-code-propagation-2026-06-10]] — 인접한 startup failure topic.
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: actuator exposure/auth blocker 해소 여부.
|
||||
- 과장하면 안 되는 부분: Kubernetes end-to-end readiness 보장을 단정하지 않는다. 구현 기록과 남은 blocker를 분리한다.
|
||||
- 블로그로 쓰기 전에 필요한 canonical 정제: runtime health project 문서의 current state 갱신.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/runtime-container-health-migration.md` 에 Actuator health probe group split 글감으로 반영했다.
|
||||
- 다음 단계: target canonical이 아직 `draft` 이므로 `blogify` 전 review/verify가 필요하다. Kubernetes end-to-end readiness 보장을 단정하지 않는다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/feature-runtime-health-lifecycle-contract]]
|
||||
- 관련 error:
|
||||
- 관련 interview prep:
|
||||
- derived blog: 생성 전. 생성 시 `wiki/blog/spring-actuator-health-probe-group-split-YYYY-MM-DD.md` 후보
|
||||
-1
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/spring-async-taskdecorator-bounded-executor-saturation-shutdown-2026-06-13.md
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
---
|
||||
title: blog-topic / spring-async-taskdecorator-bounded-executor-saturation-shutdown-2026-06-13
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-background-job-async-contract]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, async, threadpooltaskexecutor, taskdecorator, mdc, graceful-shutdown, micrometer, clean-architecture]
|
||||
created: 2026-06-13
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: spring-async-taskdecorator-bounded-executor-saturation-shutdown-2026-06-13
|
||||
|
||||
> Layer: `raw/blog-topics/` — 채용공고가 아닌 작업·학습·트러블슈팅에서 나온 블로그 글감 원석. canonical 정제 전 raw 후보이며, `wiki/blog/` 직접 생성 근거가 아니다.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-background-job-async-contract]] — D5/D6/D7/D8 (executor + context propagation + saturation + graceful shutdown) 실 구현에서 추출.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- "요청 스레드 밖의 실패는 GlobalExceptionHandler 가 못 잡는다" 는 문제의식으로 배경 작업(@Async/scheduler) 운영 계약을 코드로 구현하면서, Spring Boot 의 기본 executor 가 운영에 부적합한 기본값(unbounded queue)을 갖는다는 점과 컨텍스트 전파/우아한 종료의 세부가 한데 모였다.
|
||||
|
||||
## 글감 코어 / Core idea
|
||||
|
||||
- **기본 executor 를 그대로 쓰면 안 되는 이유**: Spring Boot 가 자동 구성하는 `applicationTaskExecutor` 의 queue capacity 기본값은 `Integer.MAX_VALUE`(사실상 unbounded). JDK `ThreadPoolExecutor` 는 큐가 *가득 찰 때만* core→max 로 성장하므로(3단계 성장), unbounded 큐에서는 `maxPoolSize` 가 영원히 무효 — OOM 직전까지 큐만 쌓인다. 따라서 bounded queue 를 강제하고 `@ConditionalOnMissingBean(Executor.class)` 로 자동 구성을 back-off 시킨 뒤 직접 빈을 등록한다. `Integer.MAX_VALUE` 큐 용량은 "이름만 bounded 인 unbounded" 라 설정 검증에서 거부.
|
||||
- **TaskDecorator 1개로 컨텍스트 전파**: caller→worker 로 (1) MDC 맵 전체(`MDC.getCopyOfContextMap()` — request_id/trace_id/correlation_id/tenant_id + tracing bridge 가 채운 span_id 까지 한 번에), (2) 도메인 컨텍스트(별도 propagator seam 의 `wrap(Runnable)`)를 복사. **캡처 시점이 핵심**: `decorate()` 호출 시점(=submit time)에 스냅숏을 떠야 하며 run time 이 아니다. 그리고 **대칭 복원**: 작업 후 worker 의 이전 MDC 로 되돌려, 풀 재사용 스레드가 한 작업의 MDC 를 다음 작업으로 흘리지 않게 한다.
|
||||
- **SecurityContext 는 기본 전파하지 않는다**: `MODE_INHERITABLETHREADLOCAL` 은 풀 스레드 재사용 시 stale principal 위험. principal 이 필요한 use case 만 `DelegatingSecurityContextTaskExecutor` 로 명시적 opt-in. (registry 상 user_principal 은 `propagation: [none]`.)
|
||||
- **Saturation 을 침묵시키지 않는다**: AbortPolicy 를 감싸 거부 시 (1) 구조화 ERROR 로그(error.code=JOB_EXECUTOR_REJECTED) + (2) `executor.rejected.total{executor_name, policy}` 카운터 증가 후 (3) `RejectedExecutionException` 재던짐(AbortPolicy 시맨틱 보존). `executor.saturation` 게이지로 큐 점유율 관측.
|
||||
- **Graceful shutdown 예산 계층**: `setWaitForTasksToCompleteOnShutdown(true)` + `setAwaitTerminationSeconds(19)`. 19 = 컨테이너 app shutdown 예산 20s − 1s 정리 마진. 계층 부등식: executor await(≤19s) < app shutdown(20s) ≤ `timeout-per-shutdown-phase` < k8s `terminationGracePeriodSeconds`(기본 30s, 초과 시 SIGKILL).
|
||||
- **async 예외의 두 경로**: `submit()` 은 throwable 을 `Future` 에 가둬 `get()` 으로 표면화(삼켜지지 않음); `execute()` 는 worker 의 uncaught handler 로 간다 — 그래서 모든 작업을 감싸는 decorator 는 예외를 **재던져야** 하고 MDC 복원 `finally` 에서 삼키면 안 된다.
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: 운영 가능한 Spring async executor는 bounded queue, context propagation, rejection metric, graceful shutdown budget을 하나의 계약으로 묶어야 한다.
|
||||
- 예상 제목 후보:
|
||||
- `@Async`를 운영 계약으로 만들기
|
||||
- Spring `ThreadPoolTaskExecutor`에서 MDC, saturation, shutdown을 다루는 법
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- unbounded queue에서는 `ThreadPoolExecutor`의 maxPoolSize가 사실상 성장 조건을 만나기 어렵다.
|
||||
- `TaskDecorator`는 submit 시점의 MDC/context snapshot을 worker 실행으로 넘길 수 있다.
|
||||
- executor await time은 application/container shutdown budget보다 작아야 한다.
|
||||
- 의견/해석 후보:
|
||||
- background job 안정성은 비동기 실행 자체보다 실패, 포화, 종료를 관측 가능한 계약으로 만드는 데 달려 있다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. Spring Boot 기본 executor queue 설정과 maxPoolSize 함정을 설명한다.
|
||||
2. TaskDecorator로 MDC와 domain context를 복사하고 복원하는 흐름을 정리한다.
|
||||
3. SecurityContext는 기본 전파하지 않고 opt-in으로 다루는 이유를 적는다.
|
||||
4. rejection logging/metric과 graceful shutdown budget 계층을 하나의 운영 계약으로 묶는다.
|
||||
|
||||
## 왜 흥미로운가 / Why it matters
|
||||
|
||||
- "그냥 @Async 붙이면 된다" 와 운영 가능한 background 실행의 간극(기본값 함정 · 컨텍스트 전파 · saturation 가시성 · 종료 예산)을 구체 코드로 보여주는 좋은 사례. Clean Architecture 관점에서 executor 배선은 composition root(app-bootstrap) 가 소유하고, 도메인 컨텍스트 전파는 별도 seam 인터페이스로 분리한 점도 곁들일 수 있다.
|
||||
|
||||
## 확장 메모 / Notes
|
||||
|
||||
- 본문 작성 시 정량 근거(부하테스트로 core=10/max=50/queue=200 검증)는 아직 `planned` 임을 명시 — 수치는 trade-off 기본값이지 측정값이 아니다.
|
||||
- Observation **scope** 전파(worker 에서 만든 child span 의 부모 연결)는 `io.micrometer:context-propagation` + `ContextPropagatingTaskDecorator` 가 필요한 별도 업그레이드 — 본 구현은 MDC 문자열 복사(로그 연속성)까지만.
|
||||
|
||||
## 관련 / Related
|
||||
|
||||
- [[raw/branch-notes/feature-background-job-async-contract]]
|
||||
- [[raw/official-docs/jdk21-threadpoolexecutor-javadoc]] · [[raw/official-docs/spring-framework-threadpooltaskexecutor-javadoc]] · [[raw/official-docs/spring-executor-configuration-support-javadoc]] · [[raw/official-docs/kubernetes-pod-lifecycle-termination]]
|
||||
- [[raw/interviews/async-executor-saturation-context-propagation-2026-06-13]]
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/observability-log-metric-trace-runbook.md` 후보:
|
||||
- async executor MDC/context propagation, saturation metric, graceful shutdown 글감.
|
||||
- `wiki/projects/ca-tmpl/runtime-container-health-migration.md` 후보:
|
||||
- shutdown budget와 executor await hierarchy 글감.
|
||||
- 필요한 추가 검증:
|
||||
- current executor bean, TaskDecorator, rejection metric, shutdown budget test.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-background-job-async-contract]]
|
||||
- [[raw/official-docs/jdk21-threadpoolexecutor-javadoc]]
|
||||
- [[raw/official-docs/spring-framework-threadpooltaskexecutor-javadoc]]
|
||||
- [[raw/official-docs/spring-executor-configuration-support-javadoc]]
|
||||
- [[raw/official-docs/kubernetes-pod-lifecycle-termination]]
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: 부하테스트나 executor sizing 실측 여부.
|
||||
- 과장하면 안 되는 부분: core/max/queue 숫자를 측정 기반 튜닝값처럼 쓰지 않는다.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/observability-log-metric-trace-runbook.md` 와 `wiki/projects/ca-tmpl/runtime-container-health-migration.md` 에 async executor 운영 계약 글감으로 반영한다.
|
||||
- 다음 단계: blogify 전 MDC-only propagation과 Observation scope propagation을 분리한다.
|
||||
-1
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/spring-boot-3-configprops-record-multi-constructor-binding-2026-06-12.md
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
---
|
||||
title: Spring Boot 3 @ConfigurationProperties record 에 보조 생성자를 추가하면 바인딩이 깨지는 이유
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
created: 2026-06-12
|
||||
related_branches: [feature-domain-event-outbox-contract]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, spring-boot, configuration-properties, record, constructor-binding, java21]
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# Spring Boot 3 `@ConfigurationProperties` record 에 보조 생성자를 추가하면 바인딩이 깨지는 이유
|
||||
|
||||
## Parent
|
||||
|
||||
[[raw/branch-notes/feature-domain-event-outbox-contract]]
|
||||
|
||||
---
|
||||
|
||||
## 글감 씨앗
|
||||
|
||||
`OutboundHttpSettings` record 에 기존 호출부 호환을 위한 보조 6-arg 생성자를 추가했을 때, `ApplicationContextRunner` 로 바인딩을 테스트하자 `No default constructor found` 로 실패한 경험. 해결책은 `@ConstructorBinding` 을 canonical compact constructor 에 추가하는 것이었다.
|
||||
|
||||
---
|
||||
|
||||
## 블로그 글 아이디어
|
||||
|
||||
### 제목 후보
|
||||
|
||||
- "Spring Boot 3 `@ConfigurationProperties` 레코드에 보조 생성자를 추가하면 생기는 일"
|
||||
- "왜 Java record 에 생성자를 하나 더 추가했더니 Spring Boot 설정 바인딩이 깨졌나"
|
||||
|
||||
### 핵심 메시지
|
||||
|
||||
Spring Boot 3.x 는 record 에 생성자가 **딱 하나**일 때만 자동으로 constructor binding 경로를 선택한다. 생성자가 둘 이상이면 일반 JavaBean 경로(no-arg constructor 탐색)로 fallback 하기 때문에, 보조 생성자를 추가하는 순간 기존에 잘 돌던 바인딩이 깨진다.
|
||||
|
||||
### 커버할 내용
|
||||
|
||||
1. Spring Boot `@ConfigurationProperties` 에서 record 바인딩이 동작하는 원리 (single-constructor auto-detect)
|
||||
2. 보조 생성자 추가 시 발생하는 예외 메시지와 스택 트레이스 분석
|
||||
3. 해결책: `@ConstructorBinding` (from `org.springframework.boot.context.properties.bind`) 을 canonical compact constructor 에 명시
|
||||
4. Spring Boot 2.x vs 3.x import 경로 차이 (`@ConstructorBinding` deprecated 위치 변경)
|
||||
5. 실전 패턴: 기존 호출부 호환을 유지하면서 record 필드를 확장하는 방법 (보조 생성자 + `@ConstructorBinding`)
|
||||
|
||||
### 코드 예시
|
||||
|
||||
```java
|
||||
@ConfigurationProperties(prefix = "app.outbound.http")
|
||||
public record MySettings(
|
||||
Duration connectTimeout,
|
||||
Retry retry) {
|
||||
|
||||
@ConstructorBinding // 다중 생성자 record 필수!
|
||||
public MySettings { /* validation */ }
|
||||
|
||||
/** 보조 생성자: 기존 호출부 호환 */
|
||||
public MySettings(Duration connectTimeout) {
|
||||
this(connectTimeout, null);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 독자 대상
|
||||
|
||||
Java 21 + Spring Boot 3.x 를 사용하며 `@ConfigurationProperties` 를 record 로 작성하는 개발자.
|
||||
|
||||
---
|
||||
|
||||
## Claims To Verify
|
||||
|
||||
- Spring Boot 3.4 릴리즈 노트에 이 동작의 공식 문서 여부 확인 필요.
|
||||
- `@ConstructorBinding` import 경로 변경 이력 (2.x → 3.x) 공식 마이그레이션 가이드 인용 필요.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `troubleshooting`
|
||||
- 트리거 날짜: 2026-06-12
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-domain-event-outbox-contract]]
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: Spring Boot 3 record `@ConfigurationProperties`에 보조 생성자를 추가하면 constructor binding auto-detect가 깨질 수 있어 canonical constructor에 `@ConstructorBinding`을 명시해야 한다.
|
||||
- 예상 제목 후보:
|
||||
- Spring Boot 3 record configuration binding이 깨지는 이유
|
||||
- 보조 생성자와 `@ConstructorBinding`의 함정
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- multi-constructor record는 single-constructor auto-detect 경로를 벗어날 수 있다.
|
||||
- 의견/해석 후보:
|
||||
- backward-compatible constructor를 추가할 때 binding entrypoint를 명시하는 테스트가 필요하다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. record binding auto-detect와 multi-constructor fallback을 설명한다.
|
||||
2. `ApplicationContextRunner` failure로 원인을 좁힌다.
|
||||
3. `@ConstructorBinding` import 경로와 canonical constructor 명시 패턴을 정리한다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/config-and-adapter-templates.md` 후보:
|
||||
- configuration properties record binding troubleshooting 글감.
|
||||
- 필요한 추가 검증:
|
||||
- 공식 문서/마이그레이션 가이드 source 보강.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-domain-event-outbox-contract]]
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: Spring Boot 3.x 공식 문서의 정확한 constructor binding 문구.
|
||||
- 과장하면 안 되는 부분: 모든 record multi-constructor가 동일하게 실패한다고 단정하지 않는다.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/config-and-adapter-templates.md` 에 Spring Boot 3 record configuration binding 글감으로 반영한다.
|
||||
- 다음 단계: blogify 전 official doc 근거를 보강한다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/feature-domain-event-outbox-contract]]
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/spring-boot-serialization-contract-pins-2026-07-02.md
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
title: blog-topic / spring-boot-serialization-contract-pins
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-schema-serialization-contract]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, api-design, spring-boot, json, api-contract, static-analysis]
|
||||
created: 2026-07-02
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: spring-boot-serialization-contract-pins
|
||||
|
||||
> Layer: `raw/blog-topics/` — 작업 중 나온 블로그 글감 원석. 최종 블로그는 canonical 정제 후 `wiki/blog/`에서 작성한다.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-schema-serialization-contract]] — Jackson/BigDecimal/datetime serialization pin과 ArchUnit guard 구현에서 나온 글감.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-07-02
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-schema-serialization-contract]]
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: Spring Boot serialization을 framework default에 맡기지 않고 명시 pin, effective bean test, ArchUnit rule로 고정한 이유를 정리한다.
|
||||
- 예상 제목 후보:
|
||||
- Spring Boot serialization contract를 default 대신 pin으로 관리하기
|
||||
- BigDecimal과 datetime serialization을 테스트 가능한 계약으로 만들기
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- branch가 별도 blog candidate를 직접 남겼고 serialization pin 구현 결과를 기록했다 — 근거 후보: [[raw/branch-notes/feature-schema-serialization-contract]] line `:302`, D1-D4 `:130-135`, implementation `:244-250`.
|
||||
- 경험 후보:
|
||||
- BigDecimal double constructor 차단과 effective ObjectMapper test는 "설정값이 있다"가 아니라 "실제로 적용된다"를 확인하기 위한 장치다.
|
||||
- 의견/해석 후보:
|
||||
- serialization policy는 API compatibility의 일부라서 default drift를 방치하면 client contract가 흔들릴 수 있다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. serialization default는 API contract가 아니다 — framework upgrade와 설정 drift를 고려해야 한다.
|
||||
2. pin + effective bean test 조합 — yml 값과 실제 ObjectMapper 동작을 같이 확인한다.
|
||||
3. ArchUnit으로 금지 API를 막기 — `new BigDecimal(double)` 같은 실수를 compile/test 단계에서 잡는다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/api-evolution-and-schema.md` 후보:
|
||||
- ca-tmpl schema/serialization contract 구현 사실.
|
||||
- `wiki/concepts/api-evolution-and-schema.md` 후보:
|
||||
- serialization compatibility와 numeric precision 일반 개념.
|
||||
- 필요한 추가 검증:
|
||||
- ObjectMapper test, ArchUnit rule, BigDecimal/datetime sample output.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-schema-serialization-contract]] — serialization pin과 implementation 근거.
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: per-API money field 직렬화 예제가 실제로 존재하는지.
|
||||
- 과장하면 안 되는 부분: 모든 API serialization 문제가 해결됐다고 쓰지 않는다. branch에서 구현/검증한 pin과 guard 범위로 제한한다.
|
||||
- 블로그로 쓰기 전에 필요한 canonical 정제: project 문서의 verified 항목과 needs-confirmation 항목 분리.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/api-evolution-and-schema.md` 의 schema/serialization 섹션에 output serialization pin, effective `ObjectMapper` test, BigDecimal constructor guard 범위로 반영했다.
|
||||
- 다음 단계: source canonical이 `verified` 상태이므로 이후 `blogify` 대상으로 삼을 수 있다. 단 입력측 deser switch와 per-API money 직렬화 예제는 별도 owner/미구현 범위로 표시한다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/feature-schema-serialization-contract]]
|
||||
- 관련 error:
|
||||
- 관련 interview prep:
|
||||
- derived blog: 생성 전. 생성 시 `wiki/blog/spring-boot-serialization-contract-pins-YYYY-MM-DD.md` 후보
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/spring-boot-startup-exit-code-propagation-2026-06-10.md
|
||||
@@ -0,0 +1,115 @@
|
||||
---
|
||||
title: blog-topic / spring-boot-startup-exit-code-propagation-2026-06-10
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-migration-startup-contract]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, spring-boot, exit-code, startup, kubernetes, flyway, sysexits]
|
||||
created: 2026-06-10
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: spring-boot-startup-exit-code-propagation-2026-06-10
|
||||
|
||||
> Layer: `raw/blog-topics/` — 채용공고가 아닌 작업·학습·트러블슈팅에서 나온 블로그 글감 원석. canonical 정제 전 raw 후보이며, `wiki/blog/` 직접 생성 근거가 아니다.
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-migration-startup-contract]]
|
||||
|
||||
## 글감 한 줄
|
||||
|
||||
서버가 뜨기 전에 죽는 실패(env 누락 / migration 실패 / profile mismatch / required adapter disabled)에서 **원인별 JVM exit code** 를 안전하게 전파하는 Spring Boot 메커니즘과, 흔히 처방되는 `System.exit(SpringApplication.exit(run(...)))` 패턴이 장기 실행 서버에서는 오히려 버그인 이유.
|
||||
|
||||
## 핵심 포인트 (draft 후보)
|
||||
|
||||
1. **두 가지 메커니즘과 동작 시점**
|
||||
- `ExitCodeExceptionMapper` (bean) — context 가 active 일 때만 동작. context refresh 실패(env/profile/adapter 검증이 `SmartInitializingSingleton` 에서 throw)는 `context.isActive()==false` 라 mapper 가 호출되지 않음.
|
||||
- `ExitCodeGenerator` (예외가 직접 구현) — `SpringApplication.run()` 이 실패를 re-throw 하면, 부팅 스레드에 설치된 `SpringBootExceptionHandler`(uncaught exception handler)가 실패 예외 체인에서 `getExitCode()` 를 읽어 `System.exit(code)` 호출. **main() 을 건드리지 않아도** custom exit code 가 전파된다.
|
||||
|
||||
2. **`System.exit(SpringApplication.exit(run(...)))` 의 함정**
|
||||
- 많은 글이 "custom exit code 를 쓰려면 main 을 이렇게 감싸라"고 처방한다.
|
||||
- 그러나 `SpringApplication.exit(context, ...)` 의 구현은 `finally { close(context); }` — context 를 닫고, 정상 부팅이면 `ExitCodeGenerator` bean 이 없으니 **0 을 반환**한다.
|
||||
- 결과: web 서버처럼 계속 떠 있어야 하는 프로세스를 **부팅 직후 종료**시킨다. 이 패턴은 batch/CLI(러너 완료 후 종료)용이지 long-running server 용이 아니다.
|
||||
- 교훈: "startup 실패 exit code" 와 "정상 종료 exit code" 는 다른 문제다. 전자는 예외 + `ExitCodeGenerator` 로 충분.
|
||||
|
||||
3. **exit code 숫자 선택 — sysexits(3) 정합/불일치**
|
||||
- `78 EX_CONFIG`(env 누락/malformed), `70 EX_SOFTWARE`(migration 실패) 는 BSD sysexits 의미와 정합.
|
||||
- `71 EX_OSERR`("cannot fork/pipe"), `72 EX_OSFILE`("system file missing") 는 profile mismatch / adapter disabled 와 의미가 어긋남 → 외부 표준으로 방어 불가, **조직 internal convention** 으로만 성립. 글에서 "POSIX 표준" 이라 과장하지 말 것.
|
||||
- k8s 는 0–255 exit code 를 `lastState.terminated.exitCode` 에 보존하지만 숫자별 자동 분기는 없음 → 실질 discriminator 는 structured log(`startup.phase`/`error.code`).
|
||||
|
||||
4. **migration 을 readiness 이전에 — `FlywayMigrationStrategy` vs `ApplicationRunner`**
|
||||
- `FlywayMigrationStrategy` 는 context refresh 단계(Flyway bean 초기화)에 실행 → readiness(=ApplicationReadyEvent 이후 UP) **이전**에 완료/실패. 반쯤 migrate 된 schema 가 트래픽을 받지 못한다.
|
||||
- 같은 일을 `ApplicationRunner` 로 하면 ready 이후 실행되어 순서 보장이 깨진다.
|
||||
|
||||
## 왜 글로 쓸 만한가
|
||||
|
||||
- "startup exit code" 검색 시 나오는 다수 처방이 long-running 서버에 부적합하다는 점은 실제로 코드를 까봐야 드러난다 (`SpringApplication.exit` 의 `finally close`).
|
||||
- sysexits 를 빌려 쓰되 71/72 처럼 의미가 안 맞는 코드를 "표준" 이라 부르지 않는 정직한 컨벤션 설계 사례.
|
||||
|
||||
## 검증 상태
|
||||
|
||||
- `locally-verified`: 예외별 `getExitCode()` = 78/70/71/72 단위 테스트, structured log 필드 단위 테스트, refresh-time 전략 구조 테스트 (app-bootstrap, 전체 `check` green).
|
||||
- `planned`: 실제 k8s pod `lastState.terminated.exitCode` e2e 단언, testcontainers 기반 migration 실패 로그 단언.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-06-10
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-migration-startup-contract]]
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: startup failure exit code는 long-running server를 `SpringApplication.exit(run(...))`로 감싸는 문제가 아니라 실패 예외와 Boot exit-code propagation 경로를 이해하는 문제다.
|
||||
- 예상 제목 후보:
|
||||
- Spring Boot startup 실패 exit code를 안전하게 전파하기
|
||||
- `SpringApplication.exit(run(...))`가 서버에서 위험한 이유
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- `ExitCodeExceptionMapper`와 `ExitCodeGenerator`는 동작 시점이 다르다.
|
||||
- sysexits 숫자는 POSIX 표준이 아니라 BSD 관례/조직 convention으로 다뤄야 한다.
|
||||
- 의견/해석 후보:
|
||||
- startup failure exit code와 정상 종료 exit code는 다른 문제다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. startup failure의 exit code 전파 경로를 구분한다.
|
||||
2. `SpringApplication.exit(run(...))` 패턴이 long-running server를 닫는 함정을 설명한다.
|
||||
3. sysexits 관례와 ca-tmpl 내부 convention의 경계를 분리한다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/runtime-container-health-migration.md` 후보:
|
||||
- startup failure exit code propagation 글감.
|
||||
- 필요한 추가 검증:
|
||||
- 현재 ca-tmpl 코드의 exit code exception/test 존재 여부.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-migration-startup-contract]]
|
||||
- [[raw/official-docs/spring-boot-exit-code-generator-startup-failure]]
|
||||
- [[raw/official-docs/sysexits-bsd-exit-code-convention]]
|
||||
- [[raw/official-docs/kubernetes-exit-code-observability-termination]]
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: k8s pod termination exit code e2e 검증 여부.
|
||||
- 과장하면 안 되는 부분: sysexits를 POSIX 표준이라고 쓰지 않는다.
|
||||
|
||||
## Related
|
||||
|
||||
- [[raw/official-docs/spring-boot-exit-code-generator-startup-failure]]
|
||||
- [[raw/official-docs/sysexits-bsd-exit-code-convention]]
|
||||
- [[raw/official-docs/kubernetes-exit-code-observability-termination]]
|
||||
- [[raw/blog-topics/hikaricp-inter-knob-constraints-startup-guard-2026-06-09]] — 같은 `SmartInitializingSingleton` fail-fast startup-guard 패턴.
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/runtime-container-health-migration.md` 에 startup failure exit code propagation 글감으로 반영했다.
|
||||
- 다음 단계: target canonical이 아직 `draft` 이므로 `blogify` 전 sysexits 관례와 ca-tmpl 내부 convention 경계를 분리해 review한다.
|
||||
@@ -1 +0,0 @@
|
||||
../../vault/40-publish/blog-topics/spring-conditional-on-property-optional-adapter-template-2026-06-09.md
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
---
|
||||
title: blog-topic / spring-conditional-on-property-optional-adapter-template-2026-06-09
|
||||
source_type: blog-topic
|
||||
status: raw
|
||||
related_branches: [feature-integration-adapter-templates]
|
||||
related_projects: [ca-tmpl]
|
||||
tags: [blog-topic, ca-tmpl, spring, clean-architecture, adapter, template]
|
||||
created: 2026-06-09
|
||||
status_label: ready-for-canonical
|
||||
target_audience: backend-engineer
|
||||
inspiration_url:
|
||||
archive_url:
|
||||
---
|
||||
|
||||
# blog-topic: heavy SDK 없이 "선택형 어댑터 템플릿" 을 경량 skeleton 에 싣기
|
||||
|
||||
> Layer: `raw/blog-topics/` — 구현·설계·트러블슈팅 기반 글감(채용공고 아님).
|
||||
|
||||
## Parent / 부모
|
||||
|
||||
- [[raw/branch-notes/feature-integration-adapter-templates]]
|
||||
|
||||
## 글감 한 줄
|
||||
|
||||
Clean Architecture 템플릿에 Kafka/Redis/Slack/Email 같은 선택형 어댑터를 "기본 비활성 + 같은 방식으로 실패/관측" 하도록 싣되, 실 SDK 는 안 넣고 `@ConditionalOnProperty` + integration seam + disabled sentinel + ArchUnit 3계층으로 계약만 보장하는 패턴.
|
||||
|
||||
## 다룰 내용
|
||||
|
||||
1. 문제: 선택형 어댑터를 전부 기본 dependency 로 넣으면 skeleton 이 무거워지고, 빼면 "붙일 때 제각각" 실패한다.
|
||||
2. 결정: optional module/template 기본 + disabled-default. 대안 비교(Java SPI=on/off 표현 불가·DI 미통합, `@Profile`=boolean 시맨틱 부재, Feature flag(FF4J/Togglz)=runtime branching 이라 startup on/off 와 시맨틱 다름)를 왜 제쳤는지.
|
||||
3. 3계층 검출:
|
||||
- Layer 1 `@ConditionalOnProperty(matchIfMissing=false)` 로 bean-gating + 누락=disabled 명시.
|
||||
- Layer 2 ArchUnit 로 (a) application→optional adapter import 격리, (b) optional adapter `@Bean` 의 `@ConditionalOnProperty` gating 강제. 정적 검사의 한계도 함께.
|
||||
- Layer 3 disabled sentinel 이 `AdapterDisabledException` 으로 fail-fast.
|
||||
4. integration seam 패턴: `KafkaSender`/`RedisClient`/... interface 만 제공 → fork 프로젝트가 SDK + 구현 주입. 템플릿은 계약 owner, 소비자는 연동 owner.
|
||||
5. fail-open vs fail-closed: 알림/캐시는 부수효과라 fail-open(5xx 미승격), Redis unavailable=cache-miss, 로거 시그니처에서 payload 인자를 없애 PII 누출을 구조적으로 차단.
|
||||
6. 운영 계약 정합: error-codes/env-keys registry 에 row 추가, startup `REQUIRED_ADAPTER_DISABLED` 와 runtime `ADAPTER_DISABLED` 를 lifecycle 로 분리.
|
||||
7. 함정: B7 같은 "outbound 패키지 public method return-type" ArchUnit rule 이 `@Configuration` `@Bean` factory 를 과탐 → rule scoping(약화 아님, 정밀화). [[raw/errors/archunit-b7-configuration-bean-factory-return-type-2026-06-09]].
|
||||
|
||||
## 왜 쓸 만한가
|
||||
|
||||
- "라이브러리를 안 넣고도 계약을 강제" 하는 구체 사례 — 면접/포트폴리오에서 설계 판단(경량성 vs 계약 강제) 을 보여줄 수 있다.
|
||||
|
||||
## 트리거 / Trigger
|
||||
|
||||
- 트리거 유형: `branch-work`
|
||||
- 트리거 날짜: 2026-06-09
|
||||
- 트리거 연결 노트: [[raw/branch-notes/feature-integration-adapter-templates]]
|
||||
|
||||
## 글감 / Topic seed
|
||||
|
||||
- 한 문장 요지: optional adapter를 기본 비활성 seam으로 싣고, 실제 SDK는 fork/consumer가 붙이게 하되 실패 언어와 gating은 skeleton이 제공한다.
|
||||
- 예상 제목 후보:
|
||||
- 선택형 어댑터 템플릿을 가볍게 싣는 방법
|
||||
- `@ConditionalOnProperty`로 optional adapter 계약 만들기
|
||||
|
||||
## 핵심 주장 후보 / Claim candidates
|
||||
|
||||
- 사실 후보:
|
||||
- raw branch는 optional adapter template과 disabled sentinel, ArchUnit scope를 다룬다.
|
||||
- 의견/해석 후보:
|
||||
- 선택형 어댑터의 핵심은 SDK 포함 여부가 아니라 disabled 상태의 실패 방식과 관측 가능성이다.
|
||||
|
||||
## Outline seed
|
||||
|
||||
1. 선택형 adapter를 모두 dependency로 넣으면 skeleton이 무거워진다.
|
||||
2. `@ConditionalOnProperty`, integration seam, disabled sentinel의 역할을 나눈다.
|
||||
3. ArchUnit rule은 정적 검출 범위와 한계를 함께 적어야 한다.
|
||||
|
||||
## Canonical 전환 후보 / Canonical extraction candidates
|
||||
|
||||
- `wiki/projects/ca-tmpl/config-and-adapter-templates.md` 후보:
|
||||
- optional adapter template과 ConditionalOnProperty 기반 gating.
|
||||
- 필요한 추가 검증:
|
||||
- 실제 adapter template module, ArchUnit rule, disabled sentinel 구현 여부.
|
||||
|
||||
## Sources / 근거 후보
|
||||
|
||||
- [[raw/branch-notes/feature-integration-adapter-templates]]
|
||||
- [[raw/errors/archunit-b7-configuration-bean-factory-return-type-2026-06-09]]
|
||||
|
||||
## 미해결 / Unknown
|
||||
|
||||
- 아직 확인해야 할 사실: 현재 ca-tmpl 코드에 optional adapter template이 구현됐는지.
|
||||
- 과장하면 안 되는 부분: heavy SDK 없이 계약을 설계한 것과 실제 adapter 구현을 분리한다.
|
||||
|
||||
## Related / 관련
|
||||
|
||||
- 관련 branch: [[raw/branch-notes/feature-integration-adapter-templates]]
|
||||
|
||||
## Decision / 처리 결정
|
||||
|
||||
- 액션: `promote-to-canonical`
|
||||
- 이유: `wiki/projects/ca-tmpl/config-and-adapter-templates.md` 에 optional adapter template / `@ConditionalOnProperty` 글감으로 반영했다.
|
||||
- 다음 단계: target canonical이 아직 `draft` 이므로 `blogify` 전 실제 adapter template 구현·ArchUnit rule·disabled sentinel 존재 여부를 재검증한다.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user