58 KiB
title, source_type, status, branch, parent_branch, related_projects, governing_docs, tags, created, target_merge, status_label, id, kind, project, work_item, inherits, refines, overrides, depends_on, contract_packet, contract_packet_sha256
| title | source_type | status | branch | parent_branch | related_projects | governing_docs | tags | created | target_merge | status_label | id | kind | project | work_item | inherits | refines | overrides | depends_on | contract_packet | contract_packet_sha256 | |||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| branch / feature-application-query-bypass-contract | branch-note | raw | feature-application-query-bypass-contract |
|
|
|
2026-06-04 | review | BR-CA-SKELETON-OPERATIONAL-CONTRACT-047 | project-work-item | ca-skeleton-operational-contract | WI-CA-SKELETON-OPERATIONAL-CONTRACT-047 |
|
|
1 | 4c05fbdad06f0558c14b9c975d41ed0a9d49cce1c82ee4e842bc88c190ccb22d |
branch: feature-application-query-bypass-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 project (canonical SSOT): raw/project-notes/ca-skeleton-operational-contract
ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT 역할. 본 feature branch 는 project-note 운영 계약 중 application read/query 경로 영역의 결정/근거/금지 사항을 정제한다.
선택 (관련 형제 branch):
- raw/branch-notes/feature-application-port-usecase-contract — command/query use case 분리,
QueryUseCase(READ_ONLY+READ_REPOSITORY강제),TransactionPort.inRead를 고정한 직접 선행 계약. 본 branch 가 우회를 논하는 "기존 표준 경로" 가 이 branch 의 산출물. - raw/branch-notes/feature-transaction-concurrency-contract — isolation/propagation SSOT (read tx 의 격리 수준 위임처).
- raw/branch-notes/feature-cache-consistency-contract — read 경로의 cache bypass(strict consistency) 와 인접. 본 branch 는 데이터소스/모델 우회, cache 계약은 캐시 우회.
브랜치 계약 패킷
- 생성 시 프로젝트 개정:
1 - 패킷 스키마:
contract_packet: 1 - 완료 조건: query bypass의 허용 경계·mapping·transaction 영향과 검증 test가 명시된다
상속한 프로젝트 결정
| Decision Ref | Project Summary | Branch Application | Source |
|---|---|---|---|
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-TRANSACTION-001@1 |
application은 Spring transaction annotation 대신 TransactionPort 또는 TransactionalUseCaseRunner를 사용한다 | Work Item 완료 조건에 적용 | raw/project-notes/ca-skeleton-operational-contract |
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-MODULE-LAYOUT-001@1 |
Gradle multi-module에서 domain-core·application-core·adapter-*·shared-contract·app-bootstrap·sample-portfolio 책임을 분리한다 | Work Item 완료 조건에 적용 | raw/project-notes/ca-skeleton-operational-contract |
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-MAPPING-001@1 |
수기 mapper와 record canonical constructor가 default이며 MapStruct는 optional profile이다 | Work Item 완료 조건에 적용 | raw/project-notes/ca-skeleton-operational-contract |
브랜치 지역 결정
기존 branch-local 결정은 아래
## Decision Evidence Map / 결정-근거 매핑의 D-row가 소유하며 이 packet에서 복제하지 않는다.
| Decision ID | Decision | Relation | Supporting Claims | Status |
|---|
선언한 예외
| Override ID | Overrides | Reason | Approval | Status |
|---|
목표
선행 계약 feature-application-port-usecase-contract 는 모든 읽기를 QueryUseCase → repository port(READ_REPOSITORY) → TransactionPort.inRead 경로로 강제하고, 그 port 가 도메인 aggregate 또는 projection 을 반환하도록 고정했다 (QueryUseCase Javadoc: "projection or domain object"). 그 branch 는 의도적으로 "CQRS 인프라 강제" 를 out-of-scope 로 미뤘다.
이 branch 는 그 미뤄둔 read-side 질문을 스켈레톤 기본 계약으로 확정한다: 읽기 경로가 표준 write-side 스택(도메인 aggregate / repository port / use-case / transaction)을 언제·어떻게 우회(bypass)해도 되는가. 도메인-특화 답이 아니라, 재사용 가능한 clean-architecture 스켈레톤이 보편적으로 가져갈 기본값 + opt-in 상향을 정하는 것이 목표다.
결정 방식 (사용자 지시 2026-06-04): bypass 의 구체 범위를 사전에 못박지 않는다. 외부 조사(
wiki-decision-researcher)로 기존 through-aggregate 방식 대비 clean-architecture 스켈레톤이 보편적으로 채택해야 할 방식을 도출하고, 그것이 진짜 선택인 지점만 대안과 함께 결정으로 남긴다. 따라서 아래 §결정/§Decision Evidence Map 의 셀은 조사 완료 전까지RESEARCH_PENDING으로 둔다 — 추측 금지(CLAUDE.md §11).
- 이슈:
- PR:
범위
⚠️ 아래 In/Out scope 의 경계선 자체가 조사로 확정될 결정이다(예: "use-case 우회 허용" 이 in 인지 out 인지). 현재는 조사 대상 축을 나열하며, 조사 후 D-결정에 따라 확정한다.
포함 범위 (조사로 확정할 축)
- 읽기 모델 우회 축: 읽기가 도메인 aggregate 로딩을 건너뛰고 전용 read port 로 projection(native/JPQL DTO)을 반환할지 — through-aggregate vs read-model/projection vs 별도 read store.
- 읽기 경로 ceremony 축: 단순 조회가 application use-case 를 거쳐야 하는지, thin read path(adapter-web → query service/read port 직접)를 허용할지.
- 읽기 트랜잭션 축: 읽기가
TransactionPort.inRead경계를 항상 거쳐야 하는지, no-tx read 를 허용할 조건이 있는지. - 위 축들의 정적 강제(ArchUnit) 가능성 및
RepositoryAccess/@UseCaseCapability계약과의 정합.
제외 범위
의도적으로 제외. 면접 등에서 "이건 범위에 없었습니다" 근거.
- 특정 도메인의 구체 read model 스키마/쿼리 (도메인-특화 — skeleton 범위 밖).
- 격리 수준(REPEATABLE_READ/SERIALIZABLE) —
feature-transaction-concurrency-contractSSOT. - 캐시 일관성/캐시 우회 —
feature-cache-consistency-contractSSOT (본 branch 는 모델/데이터소스 우회만). - idempotency key 정책 —
feature-rate-limit-idempotency-contract. - 특정 CQRS 프레임워크(Axon 등) 강제 — 채택은 조사 결과에 따르되, 프레임워크 lock-in 은 비목표.
근거 (필수, 최소 1개+)
이 branch의 결정 근거.
wiki-decision-researcher2개 lane(R1: 읽기 모델 우회 전략, R2: 읽기 경로 ceremony) 의 조사 산출물. company-tech-blog 는company-case-study/engineering-blog로만 취급 — 공식 best practice 승격 금지(CLAUDE.md §5).
| Source | 등급 | 정당화하는 결정 |
|---|---|---|
| raw/official-docs/cqrs-pattern-azure-architecture-center | official-vendor-doc | D1 (single-store CQRS = "foundational level"), D2 (separate-store = "advanced", escalation) |
| raw/official-docs/spring-data-jpa-projections-spring-official | official-vendor-doc | D1 (closed projection = column-subset 최적화 메커니즘) |
| raw/official-docs/spring-data-jpa-transactionality-spring-official | official-vendor-doc | D4 (CrudRepository readOnly tx 기본 + "unit of work" 권고) |
| raw/official-docs/spring-tx-management-reference | official-vendor-doc | D4 (readOnly 속성 적용 범위 SPRING-TX-MGR-C6) |
| raw/official-docs/cqrs-fowler-bliki | engineering-blog | D1/D3 (CQRS 분리 개념 + "be very cautious"/"significant complexity" 경고 → Alt 3 기각 근거) |
| raw/official-docs/domain-vaughn-vernon-aggregate-root | engineering-blog | D1 (small aggregate 가정 — through-aggregate fallback 조건) |
| raw/company-tech-blogs/cqrs-lite-clean-architecture-read-path-bypass-wakita | engineering-blog | D1 (read port = application-layer port, no domain type, logical split) |
| raw/company-tech-blogs/explicit-architecture-ddd-hexagonal-cqrs-hgraca | engineering-blog | D3 (query side 가 Application Service 없이 optimized query + DTO 반환 가능) |
| raw/company-tech-blogs/read-only-tx-hibernate-optimization-vladmihalcea | engineering-blog | D4 (readOnly 이득은 entity 多일 때 — trivial read 의 no-tx 비용 근거) |
| raw/company-tech-blogs/netflix-tudum-cqrs-separate-read-store-evolution | company-case-study (medium — 2차 출처) | D2 (separate read store 의 운영 friction) |
TODO
각 항목 옆에 증거 등급 표기.
- D1: read projection port 계약 정의 —
QueryUseCase가 도메인 aggregate 대신 application-layer projection DTO 를 반환하도록 read port 분리 — 등급:locally-verified(sample-portfolio 시연:WorkLogSummaryQueryPort+WorkLogSummaryrecord +ListRecentWorkLogSummariesUseCase+ 영속WorkLogSummaryQueryAdapter(JPQLSELECT new→WorkLogSummaryRow→ ULID 변환)) - D1: projection DTO 가 도메인 type / JPA entity / web DTO 가 아님을 강제하는 ArchUnit rule — 등급:
locally-verified(query_ports_do_not_leak_domain_jpa_or_web_types, customArchCondition<JavaMethod>가JavaType.getAllInvolvedRawTypes()로 generic type argument 까지 검사 — raw/generic/over-block 3 fixture 로 역검증) - D3:
QueryUseCase경유를 default 로 유지(Strict). thin read path 폐기 — 등급:actually-implemented(코드: 모든 read 가QueryUseCasebean 경유; 신규 rule 불필요 — 선행 계약 capability fitness function 재사용. application-core/CLAUDE.md §Read/query path 문서화) - D4:
TransactionPort.inReaddefault 유지. no-tx bypass opt-in 조건(OSIV=false + projection-only + no lazy) 명문화 — 등급:actually-implemented(코드:ListRecentWorkLogSummariesUseCase가tx.inRead경유 + testinReadCalled검증; CLAUDE.md 에 opt-in 선결조건 문서화) - D5: projection read 의 capability 표기 =
READ_REPOSITORY재사용 — 등급:actually-implemented(코드: 영속-backed projection use case 가repositoryAccess = READ_REPOSITORY; 신규 enum 없음) - D2: Full CQRS separate read store 는 본 branch out-of-scope — escalation trigger 만 문서화하고 별도 branch 로 위임 — 등급:
documented-only(코드 없음, 의도적)
진행 중 메모
- ca-tmpl 현황(2026-06-04 ground-truth): 읽기는 이미
QueryUseCase(GetRepoStatsUseCase/ListWorkLogsUseCase/GetWorkLogUseCase) 경유 = Strict baseline 실재.GetRepoStatsUseCase는 dedicatedRepoStatsPort.fetch()를 쓰지만 반환이 도메인 typeRepoStats이고 capability 가RepositoryAccess.NONE으로 선언됨 — projection-as-application-DTO 와 read-projection capability 어휘가 아직 없음(= 본 branch 가 채울 gap, D1/D5). - OSIV:
application-test.yml=open-in-view:false,application.yml=${DB_OPEN_IN_VIEW}(env). test 는 OSIV off → no-tx bypass(D4) 의 안전 전제 일부 충족하나, lazy access 가 tx 밖이면LazyInitializationException→ projection-only 조건이 그래서 필수. - web→application 경계 rule 은 "web 이 persistence/outbound adapter 의존 금지"만 있고 "web 은 QueryUseCase 만 호출" rule 은 없음 → thin read path(D3) 는 기존 rule 과 충돌하진 않으나 mandatory
@UseCaseCapabilityrule 을 우회하게 됨(D3 Open Risk).
결정 사항
핵심 헤드라인: "query bypass" = 도메인 aggregate 우회(projection read port) 를 skeleton 이 능력으로 제공한다 — 우회하는 건 도메인 모델뿐. 단 projection 은 강제 디폴트가 아니라 read 마다의 선택이고, 코어가 강제하는 건 purity 가드레일(read port 가 도메인/JPA/web 타입을 누출하지 않음)뿐이다(아래 D1 의 코어 vs 선택 분할). use-case ceremony 는 Strict 로 확정(읽기는 무조건
QueryUseCase경유, thin-path 폐기), transaction 은 defaultinRead유지(no-tx 만 opt-in). separate read store(Full CQRS)는 out-of-scope escalation.
- 2026-06-04 (D1, 2026-06-05 코어/선택 분할): CQRS-lite(single store) projection read 를 능력으로 제공한다 —
QueryUseCase가 도메인 aggregate 를 재구성하지 않고 dedicated read/query port 로 application-layer projection DTO 를 반환(Spring Data closed projection /SELECT new/ JdbcTemplate). / 코어 vs 선택 분할 (보편 핵심 원칙):- 코어로 강제 (모든 프로젝트 동일) = purity 가드레일 — read/query port 의 반환 type(generic argument 포함)이 domain/JPA/web 타입을 누출하지 않는다는 ArchUnit rule + read port 추상화의 모양. 이건 projection 을 쓸 때 깨끗함을 보장하는 가드레일이지, projection 을 쓰라는 강제가 아니다.
- 프로젝트 선택 (강제 금지) = "projection 이냐 through-aggregate 냐". projection 은 권장이자 제공된 능력일 뿐 강제 디폴트가 아니다. 단순 읽기는 Alt1 through-aggregate(기존 repository port 로 도메인 aggregate 반환)가 정당한 동급 선택 — read shape = write aggregate 와 동일 and aggregate 가 단일 트랜잭션 불변식 경계 내 최소 크기일 때.
- 시연 위치 = projection 사용 예시는
sample-portfolio에 둔다(교육용,production_code_does_not_depend_on_sample_portfolio로 격리). 코어 enforcement 에 "projection 기본" 을 박지 않는다. / 이유: aggregate hydration overhead 제거 + read shape 독립 진화 + hexagonal purity 유지는 원할 때 얻는 이득이지 모든 도메인에 강제할 보편 사실이 아님(작은 CRUD 는 through-aggregate 가 더 단순). / 대안: Alt1 through-aggregate(위), Alt3 separate store(D2). / 근거: [raw/official-docs/cqrs-pattern-azure-architecture-center], [raw/official-docs/spring-data-jpa-projections-spring-official], [raw/company-tech-blogs/cqrs-lite-clean-architecture-read-path-bypass-wakita].
- 2026-06-04 (D2): Full CQRS(별도 물리 read store)는 본 branch out-of-scope — escalation-only. / 이유: 단일 RDBMS skeleton 가정 위반 + eventual consistency + 운영 인프라(Kafka/CDC) 부담 + Fowler/Azure 의 "단순 도메인엔 부적합" 경고. / escalation trigger(별도 branch 결정, 정성): ① read/write 부하가 명확히 비대칭이어 단일 DB write-path 가 read latency SLA 미충족(정량 임계는 cited source 없음 → PoC 측정으로만 확정,
UNSUPPORTED_IMPL_DECISION) and ② denormalized shape 가 single-DB column-subset SELECT 로 불가 and ③ 도메인이 수초 stale read 허용. / 근거: [raw/official-docs/cqrs-pattern-azure-architecture-center], [raw/official-docs/cqrs-fowler-bliki], raw/company-tech-blogs/netflix-tudum-cqrs-separate-read-store-evolution(NETFLIX-TUDUM-C2/C3, medium). - 2026-06-04 (D3, 2026-06-05 Strict 확정): use-case layer ceremony = Strict (단일 계약, opt-in 없음) — 모든 읽기는
QueryUseCasebean 경유. thin read path(web→read port 직접)는 폐기. / 이유 (2026-06-05 재결정, §Audit & Findings 참조): 선행 계약은 capability 선언을 use-case 모양에 결합(@UseCaseCapability는 use-case 구현체에만)했다. thin-path 는 use-case 가 아니므로 capability 를 달 곳이 없어inbound_port_implementations_declare_capabilityfitness function 에 안 잡힌다 — 즉 thin-path 는 본 스켈레톤의 핵심 가치(아키텍처의 기계 강제)를 코드리뷰 신뢰로 격하시킨다. modest 한 ceremony 절감을 위해 기계 강제력을 포기할 가치가 없다고 판단 → thin-path 제거. capability 를 use-case 에서 분리하는 수술(port-level capability)은 thin-path 의 실익 증거가 생길 때 후속 계약으로 위임(현재 미생성). / 기각된 대안: Alt2 thin-by-default(HGRACA-CQRS-C1 의 "query side 는 Application Service 없이 가능" 학파 — "domain logic 없음" 의 정적 강제 불가로 기각), Alt3 query-handler(별도 infra 전제 → 기각). / 근거: raw/official-docs/cqrs-fowler-bliki(CQRS-FOWLER-C5, "CQRS 복잡도에 매우 신중하라" → 보수적 Strict 지지), raw/company-tech-blogs/explicit-architecture-ddd-hexagonal-cqrs-hgraca(HGRACA-CQRS-C1, 기각된 thin-by-default 학파의 출처). - 2026-06-04 (D4): transaction boundary = default
TransactionPort.inRead유지. no-tx(autocommit) read 는 opt-in —spring.jpa.open-in-view=falseand projection-only(lazy 접근 없음) and 단일 statement 일 때만. / 이유: Spring 권고는 "unit of work 시작 시 tx 경계 선언"(SPRING-DATA-TX-C3)이나 readOnly 이득은 entity 多 read 에서 큼(VM-READTX-C3) → trivial projection read 의 tx 비용 회피 여지. / 대안: 전면 no-tx(기각 — OSIV/ lazy 위험), CrudRepository 자체 readOnly tx 의존(부분 허용). / 근거: [raw/official-docs/spring-data-jpa-transactionality-spring-official], [raw/company-tech-blogs/read-only-tx-hibernate-optimization-vladmihalcea], [raw/official-docs/spring-tx-management-reference]. - 2026-06-04 (D5, 2026-06-05 해소): projection read 의 capability 어휘 =
READ_REPOSITORY재사용, 신규 enum 불필요. / 근거 (code-grounded, ca-tmplRepositoryAccess.java확인):RepositoryAccess는 repository 접근 수준 축(NONE/READ_REPOSITORY/WRITE_REPOSITORY)이고, "aggregate 냐 projection 이냐"는 반환 모양 축이라 서로 직교한다. repository-backed projection read 는 repository 의 read 메서드를 호출하므로 그대로READ_REPOSITORY다 — projection 이라는 사실은 capability 에 영향을 주지 않는다. 반환 모양 purity(projection ≠ domain/JPA/web)는 capability enum 이 아니라 D1 의 반환타입 ArchUnit rule 이 담당한다. 두 축을 혼동한 게READ_PROJECTION신설 논쟁의 정체였음(§Audit & Findings). /GetRepoStatsUseCase의NONE선언은 gap 이 아니라 올바른 분류 — 그건 outbound HTTP read(RepoStatsPort)라 repository 를 안 건드린다. repository projection read 로 이관하는 경우에만READ_REPOSITORY로 선언.
결정-근거 매핑
company-tech-blog 증거는
company-case-study/engineering-blog로 표기(공식 best practice 승격 금지).선택 조건= 이 조건일 때 이 결정 / 다른 조건이면 어떤 대안.
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|---|
| D1 | CQRS-lite projection read 를 능력으로 제공: QueryUseCase → dedicated read/query port → application-layer projection DTO(aggregate 우회), 동일 RDBMS. 코어 강제 = purity 가드레일만(read port 가 domain/JPA/web 누출 금지); projection 사용 자체는 프로젝트 선택(강제 디폴트 아님), 시연은 sample |
선택 가이드: projection = read shape 이 write 와 다르거나 hydration 비용을 피하고 싶을 때(권장). Alt1 through-aggregate(기존 repository port 로 도메인 aggregate 반환) = 동급 선택 = read shape = write aggregate 와 동일 and aggregate 가 단일 트랜잭션 불변식 경계 내 최소 크기(lazy collection 없음) — 단순 CRUD 의 기본; Alt3 별도 store = D2 trigger. — UNSUPPORTED_IMPL_DECISION: "필드 N개 이하" 같은 정량 임계는 cited source 없음(Vernon 은 정성 원칙만, VERNON-AGG-C3 가 정량 임계 부재 명시) → 정성 기준만 사용, 숫자 휴리스틱 금지 | raw/official-docs/cqrs-pattern-azure-architecture-center.md#AZURE-CQRS-C2, raw/official-docs/spring-data-jpa-projections-spring-official.md#SPRING-PROJ-C2(주의: "can optimize" — column-subset SELECT 보장 아님, Claims To Verify #1), raw/company-tech-blogs/cqrs-lite-clean-architecture-read-path-bypass-wakita.md#WAKITA-CQRS-C2, #WAKITA-CQRS-C3 |
official-vendor-doc(Azure, Spring) + engineering-blog(Wakita) |
closed projection 이 Hibernate 6 에서 실제 column-subset SELECT 를 생성하는지 통합 테스트 미검증(Claims#1). Wakita 는 Kotlin+jOOQ → Spring Data JPA 전이성 보강 필요 |
| D2 | Full CQRS(별도 물리 read store)는 out-of-scope escalation — trigger 문서화 후 별도 branch 위임 | escalation = read/write 부하가 명확히 비대칭이어 단일 DB write-path 가 read latency SLA 를 못 맞추는 시점 and single-DB projection 불가(denormalized) and eventual consistency 허용; 아니면 D1. — UNSUPPORTED_IMPL_DECISION: 정량 임계(QPS 배수 등)는 cited source 없음(Azure/Fowler/Netflix 모두 비율 미명시) → PoC 측정값으로만 확정, 숫자 threshold 단정 금지 | raw/official-docs/cqrs-pattern-azure-architecture-center.md#AZURE-CQRS-C4, #AZURE-CQRS-C6, #AZURE-CQRS-C7, raw/official-docs/cqrs-fowler-bliki.md#CQRS-FOWLER-C6, raw/company-tech-blogs/netflix-tudum-cqrs-separate-read-store-evolution.md#NETFLIX-TUDUM-C3 |
official-vendor-doc(Azure) + company-case-study(Netflix, medium — 2차 출처) |
NETFLIX-TUDUM 은 netflixtechblog SSL 오류로 ByteByteGo 2차 출처 — 직접 재검증 권장 |
| D3 | use-case ceremony = Strict 단일 계약 — 모든 읽기 QueryUseCase 경유, thin read path 폐기 |
무조건 Strict. thin-path 같은 use-case 우회 읽기는 없음(capability 선언이 use-case 모양에 결합돼 정적 강제 불가 → 폐기). port-level capability 분리 수술은 thin-path 실익 증거 생길 때 후속 계약 위임 | raw/official-docs/cqrs-fowler-bliki.md#CQRS-FOWLER-C5 (CQRS 복잡도 신중론 → 보수적 Strict 지지) / raw/company-tech-blogs/explicit-architecture-ddd-hexagonal-cqrs-hgraca.md#HGRACA-CQRS-C1 (기각된 thin-by-default 학파) |
engineering-blog(Fowler caution; Graça = 기각 대안) |
해소됨(2026-06-05): thin-path 폐기로 "capability rule 우회" 구멍이 사라짐. 잔여 trade-off: 단순 조회도 QueryUseCase ceremony 부담을 짐 — 스켈레톤의 기계 강제 가치를 위해 의도적으로 수용 |
| D4 | transaction default TransactionPort.inRead; no-tx read 는 opt-in |
no-tx = open-in-view=false + projection-only(lazy 없음) + 단일 statement; 아니면 inRead |
raw/official-docs/spring-data-jpa-transactionality-spring-official.md#SPRING-DATA-TX-C1, #SPRING-DATA-TX-C3, raw/company-tech-blogs/read-only-tx-hibernate-optimization-vladmihalcea.md#VM-READTX-C3, raw/official-docs/spring-tx-management-reference.md#SPRING-TX-MGR-C6 |
official-vendor-doc(Spring) + engineering-blog(Vlad) |
trivial read 에서 no-tx 가 inRead 대비 실측 이득이 있는지 PoC 미수행(UNVERIFIED). prod OSIV(${DB_OPEN_IN_VIEW}) 가 false 로 운영되는지 확인 필요 |
| D5 | projection read 의 capability = READ_REPOSITORY 재사용, 신규 enum 불필요 (해소) |
repository-backed projection read 는 항상 READ_REPOSITORY. outbound HTTP read 는 NONE(repository 미접근). 신규 READ_PROJECTION 없음 |
(code-grounded) ca-tmpl RepositoryAccess.java = {NONE, READ_REPOSITORY, WRITE_REPOSITORY} — 접근 수준 축 |
project-decision (code 확인) |
해소됨(2026-06-05): RepositoryAccess(접근 수준)와 반환 모양(aggregate/projection)은 직교 — 혼동이 논쟁의 정체였음. 반환 purity 는 D1 rule 이 담당. GetRepoStatsUseCase 의 NONE 은 outbound HTTP 라 올바른 분류(gap 아님) |
구현 가이드
결정 (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 (필수 준수):
- R1. Reference 필수 — 각 sub-section / row / cell 은 본 branch 의
Decision ID(예: D1, D2) + 그 결정의Supporting Claim ID(예:RAW-SLUG-C1) 를 reference. 근거 없는 결정 금지 — 모든 구현 detail 은 결정 + 근거의 도출 이어야 함.- R2. UNSUPPORTED_IMPL_DECISION 명시 — 근거 raw 가 원칙 만 권고하고 detail (메커니즘 선택 / 클래스/rule 명명 / glob 패턴 / algorithm / factory API 모양 등) 은 권고하지 않는 cell 은
UNSUPPORTED_IMPL_DECISION라벨 + 사용자 trade-off 근거 한 줄. 이게 근거 있는 결정 vs 사용자 임의 trade-off 의 경계.- R3. OUT_OF_BRANCH_SCOPE 정제 — 본 branch 결정 범위 밖 cell 은 §구현 가이드에 남기지 않음. 별도 branch 또는 canonical SSOT 로 이관 (이관 history 는 별도 § "Audit & Findings" 등에 보존). 도메인 특화 detail (ca-tmpl skeleton 범위 밖) 도 동일하게 정제.
각 sub-section 의 권장 헤더 패턴:
### N. <sub-section 제목> > **Trace**: <In-scope row 들 + Decision ID + Supporting Claim ID 의 매핑 (한 줄/한 단락)> > > - **UNSUPPORTED_IMPL_DECISION**: <근거 없는 사용자 임의 결정 항목들 + 각각의 trade-off 근거 한 줄> <표 또는 명확한 구조 — 자유 텍스트 = 모호함 = 되묻기 원인>
1. Read/query port 분리 + projection DTO 반환 (D1)
Trace: D1 (AZURE-CQRS-C2 single-store CQRS, SPRING-PROJ-C2 closed projection, WAKITA-CQRS-C3 no-domain-type port). ca-tmpl anchor: 기존
dev.caskeleton.application.usecase.QueryUseCase<Q,R>+application.query.Querymarker + sample 의RepoStatsPort(precursor).
UNSUPPORTED_IMPL_DECISION: read port 인터페이스 명명/패키지 (
*QueryPortvs*ReadPort,application.<domain>.portvsapplication.query.port) — 근거 raw 는 "application-layer port" 원칙만 권고(WAKITA-CQRS-C3), 구체 suffix/패키지는 미권고. trade-off: outbound port 기존*Port컨벤션과 충돌 회피 위해*QueryPort제안(임의).UNSUPPORTED_IMPL_DECISION: projection DTO 의 배치 layer — application 패키지에 record 로 둘지(반환 type 이 web/JPA 가 아니어야 하므로 application 이 자연) — 근거 원칙(no domain/web/JPA type)에서 도출되나 "record in application.query.result" 같은 구체 위치는 임의.
⚠️ 코어 vs 선택 분할 (구현 시 반드시 지킬 것): 아래 표에서 코어(모든 프로젝트 동일 강제)는 「정적 강제」 두 행 = read/query port 의 purity 가드레일뿐이다. 「반환 type / 조회 메커니즘」은 projection 경로를 택했을 때 의 명세지 모든 읽기에 projection 을 강제 하는 게 아니다. 단순 읽기는 기존 repository port 로 도메인 aggregate 를 반환(through-aggregate) 해도 되며 그 경로는 본 purity rule 대상이 아니다(아래 행). projection 사용 예시는
sample-portfolio에서 시연하고 코어 enforcement 에 "projection 기본" 을 박지 않는다.
| 항목 | 명세 | 근거/라벨 |
|---|---|---|
| 코어/선택 구분 | 코어 강제 = 「정적 강제」 행(purity rule). 프로젝트 선택 = projection 경로를 쓸지 vs through-aggregate(아래) | D1 (코어/선택 분할, 2026-06-05) |
| 반환 type (projection 경로) | 도메인 aggregate ❌ / web DTO ❌ / JPA entity ❌ → application-layer projection DTO(record 권장) | D1 / WAKITA-CQRS-C3 |
| through-aggregate 경로 (선택) | 단순 읽기: 기존 repository port → 도메인 aggregate 반환. read/query port 가 아니므로 purity rule 비대상. read shape=write and 최소 aggregate 일 때 동급 선택(Alt1) | D1 / VERNON-AGG-C3 (small aggregate) |
| 조회 메커니즘 | Spring Data closed interface projection 또는 SELECT new <AppDto>(...) JPQL 또는 JdbcTemplate RowMapper. 단, closed projection 의 column-subset SELECT 생성은 Claims#1 미검증(SPRING-PROJ-C2 는 "can optimize" 만 명시) → 검증 전까지 SELECT new/JdbcTemplate 를 1순위로 선호 |
D1 / SPRING-PROJ-C2 |
| nested join 주의 | closed projection 의 nested property 는 full join materialize(SPRING-PROJ-C6) → 다중 join 조회는 SELECT new/JdbcTemplate 선호 |
D1 / SPRING-PROJ-C6 |
| 정적 강제 (의도) | read port 메서드의 반환 type 이 ..domain.. / ..adapter.. / jakarta.persistence.. / org.springframework.web.. 에 속하지 않아야 함 — 직접 반환 type 뿐 아니라 generic type argument(List<DomainType>)까지 차단. violations-as-data negative fixture(도메인 type 반환 read port)로 rule 이 실제로 잡는지 역검증 |
D1 / WAKITA-CQRS-C3 (no-domain-type port 원칙) |
| 정적 강제 (ArchUnit 구체 API) | UNSUPPORTED_IMPL_DECISION / Claims#2 — 정확한 ArchUnit 구성은 구현 시 사용 중인 ArchUnit 버전 Javadoc 으로 확정. 후보: (a) 직접 반환 type 은 methods()...should().haveRawReturnType(DescribedPredicate) 계열(predicate overload 존재 여부·이름은 버전 의존 → copy 전 확인 필수), (b) List<DomainType> 등 generic type argument 누출은 raw-type 검사로 못 잡으므로 custom ArchCondition<JavaMethod> 가 메서드 반환의 type parameter 까지 들여다봐야 함. 즉 (a) 단독으로는 불충분 — 이 한계 자체가 trade-off 근거 |
D1. UNSUPPORTED_IMPL_DECISION: 근거 raw 는 "domain type 미노출" 원칙(WAKITA-CQRS-C3)만 권고하고 정적 강제의 구체 API 는 미권고 → 위 (a)/(b) 조합은 구현 fixture 로 확정, 노트의 DSL 을 그대로 copy 하지 말 것 |
| 기존 자산 정합 | GetRepoStatsUseCase 는 D1 패턴의 precursor지만 RepoStats(도메인 type) 반환 → D1 적용 시 projection DTO 로 이관 후보(planned) |
ca-tmpl ground-truth |
2. use-case ceremony = Strict 확정 (D3)
Trace: D3 (CQRS-FOWLER-C5 CQRS 복잡도 신중론 → 보수적 Strict 지지). ca-tmpl anchor: 선행 계약의
inbound_port_implementations_declare_capability/_end_with_use_case/_declare_capabilityrule (actually-implemented) —@UseCaseCapability는 use-case 구현체에만 부착되고 그 rule 들이 use-case 구현체를 대상으로 capability 를 강제한다.
- 결정 (2026-06-05): 읽기 경로는 단일 경로 = Strict. thin read path(web→read port 직접)는 폐기. 근거: capability 선언이 use-case 모양에 결합돼 있어, use-case 가 아닌 thin-path read 는 capability fitness function 에 안 잡힌다(정적 강제 불가). 스켈레톤의 핵심 가치는 기계 강제 이므로 ceremony 절감을 위해 이를 포기하지 않는다. capability 를 use-case 에서 분리(port-level capability + rule)하는 수술은 후속 계약으로 위임(thin-path 실익 증거가 생길 때) — 현재 미생성.
| 경로 | 허용 | capability 선언 | 비고 |
|---|---|---|---|
| Strict (유일 경로) | QueryUseCase 구현 → read/query port |
@UseCaseCapability(transactionMode=READ_ONLY, repositoryAccess=READ_REPOSITORY) mandatory (repository projection read). outbound HTTP read 는 repositoryAccess=NONE |
선행 계약 rule 그대로 — 무변경 |
| 폐기 — web 이 read port 직접 호출하는 경로 없음 | — | use-case⇄capability 결합이 풀리는 후속 계약 전까지 열지 않음 |
F4 (해소): 이전엔 thin read path 가
inbound_port_implementations_declare_capability를 우회하는 구멍이었고 D5 결정에 종속됐다. thin-path 폐기로 구멍이 제거됐다 — 모든 읽기가QueryUseCase이므로 capability 가 항상 선언·강제된다. capability 어휘(D5)는READ_REPOSITORY재사용으로 해소(신규 enum 불필요) → 본 §는 선행 계약 rule 을 그대로 쓰며 신규 ArchUnit rule 이 필요 없다.
3. read transaction 정책 (D4)
Trace: D4 (SPRING-DATA-TX-C1 CrudRepository readOnly 기본, SPRING-DATA-TX-C3 unit-of-work 권고, VM-READTX-C3 readOnly 이득=entity 多). ca-tmpl anchor:
TransactionPort.inRead(application-core) +SpringTransactionPort(READ_COMMITTED pinned) + OSIVapplication-test.yml=false/application.yml=${DB_OPEN_IN_VIEW}.
- UNSUPPORTED_IMPL_DECISION: no-tx opt-in 의 강제 방식 — 근거는 정책 권고만, "어떻게 막을지"(ArchUnit? 문서?) 미권고. trade-off: no-tx 조회가 lazy 를 건드리면 OSIV=false 에서
LazyInitializationException→ projection-only + 단일 statement 를 전제로만 허용, 정적 강제 대신 read port 가 도메인 entity 를 반환 안 한다는 D1 rule 로 간접 보증.- UNSUPPORTED_IMPL_DECISION: no-tx 의 latency/connection 이득 자체 — VM-READTX-C3 는 entity 多 bulk read 의 메모리 절약만 지지하며 trivial single-statement read 의 connection/latency 이득 은 인용 범위 밖이다(역방향 추론). no-tx opt-in 의 정당화는 Claims#4 의 JMH/부하 PoC 결과로만 확정 — PoC 전까지 "no-tx 가 더 빠르다" 단정 금지.
| read 형태 | tx 정책 | 조건 |
|---|---|---|
| lazy 연관 접근 있는 read | 반드시 TransactionPort.inRead |
OSIV=false 에서 tx 밖 lazy = 예외 |
| projection-only 단일 statement read | inRead default, no-tx opt-in 허용 | 선결 조건: 배포 env/env-keys.yaml 의 DB_OPEN_IN_VIEW 기본값 = false 확인 필수(Claims#5). 미확인 시 no-tx opt-in 은 Disabled — application.yml 이 env 위임(${DB_OPEN_IN_VIEW})이라 prod 값 미확정이면 tx 밖 lazy 안전 전제가 깨짐 |
OUT_OF_BRANCH_SCOPE 정제(R3): 격리 수준(REPEATABLE_READ 등)은
feature-transaction-concurrency-contract, 캐시 우회는feature-cache-consistency-contract, idempotency 는 raw/branch-notes/feature-rate-limit-idempotency-contract SSOT — 본 §에 detail 남기지 않음(링크만). Full CQRS read-store 구현(D2)도 별도 branch.
엣지·실패·의존
- 실패·엣지 경로:
- lazy + no-tx: projection-only 가 아닌데 no-tx(D4 opt-in)로 조회 후 lazy 연관 접근 → OSIV=false 에서
LazyInitializationException. 기대 동작: read port 가 도메인 entity 를 반환 안 함(D1)으로 구조적 차단, 위반 시 ArchUnit 실패. - closed projection nested join: nested property 포함 closed projection 은 full join materialize(SPRING-PROJ-C6) → 의도와 다른 over-fetch. 기대 동작: 다중 join 은
SELECT new/JdbcTemplate 로 명시. thin path 남용(해소, 2026-06-05): thin-path 자체를 폐기(D3 Strict 확정) → use-case 우회 read 경로가 없으므로@UseCaseCapability선언을 우회하는 read 가 구조적으로 불가능. 모든 읽기는QueryUseCase이고 선행 계약 rule 이 capability 를 강제.- capability 표기(D5 해소): repository projection read 는
READ_REPOSITORY(접근 수준 축), outbound HTTP read 는NONE. 반환 모양(projection)은 capability 와 직교 — purity 는 D1 반환타입 rule 이 담당. fitness function 은 기존 그대로 권한 상향(read→write)을 잡는다.
- lazy + no-tx: projection-only 가 아닌데 no-tx(D4 opt-in)로 조회 후 lazy 연관 접근 → OSIV=false 에서
- 다른 계약 의존:
- raw/branch-notes/feature-application-port-usecase-contract 의
D9(QueryUseCase+READ_REPOSITORY+inRead) /D1(*UseCase명명) / 판정기준(mandatory@UseCaseCapability) — 본 branch 는 그 계약의 read 경로를 확장(projection 반환 허용)할 뿐, ceremony 는 그대로 Strict 유지(thin-path 폐기로 완화 없음). 그 계약의 capability enum/rule 이 바뀌면 D1 영향. 또한 같은 계약의D12(HikariCP pool sizing SSOT,inNew전용)에 readinReadconnection 점유의 pool 영향도 위임 — read no-tx(D4) 가 pool 경합을 바꾸면 D12 재검토. / 후속 위임: capability 를 use-case 모양에서 분리(port-level capability)하는 수술은 thin-path 실익 증거가 생길 때 별도 계약(미생성)이 이 계약의 capability 메커니즘을 확장 — 본 branch 는 그 수술을 하지 않기로 결정(D3). - raw/branch-notes/feature-transaction-concurrency-contract 의
D3(isolation default=READ_COMMITTED) — D4 의 read tx 격리는 여기에 위임. 그D3가 바뀌면 D4 opt-in 조건 재검토 필요. - raw/branch-notes/feature-cache-consistency-contract — read 의 캐시 우회는 거기 SSOT. 본 branch 는 모델/데이터소스 우회만(경계 충돌 주의).
- raw/branch-notes/feature-outbound-http-client-baseline — outbound HTTP read(
RepoStatsPort같은 external read adapter)의 RestClient/Resilience4j/timeout 계약은 거기 SSOT. 본 branch 는 그 read 의 capability 표기(NONE— repository 미접근, D5 해소)만 확인하고 HTTP 계약은 위임. - raw/branch-notes/feature-persistence-failure-baseline — read projection query 의 오류 분류(SQLState
57014query canceled /08*connection 등)는 거기 SSOT. 본 branch read 경로도 동일 오류 경로 사용 → 위임. - (escalation 시) D2 → 별도
feature-cqrs-read-store-contract(미생성) 가 separate read store + 동기화 pipeline 소유.
- raw/branch-notes/feature-application-port-usecase-contract 의
검증해야 할 주장
공식 문서나 사례는 근거지만, 내 프로젝트에서의 동작을 자동으로 보장하지 않는다. 구현 전/중/후에 실제로 검증해야 하는 주장을 분리한다.
| Claim | Why uncertain | How to verify | Status |
|---|---|---|---|
| Spring Data closed projection 이 Hibernate 6(ca-tmpl) 에서 실제로 column-subset SELECT 를 생성한다 | SPRING-PROJ-C2 는 "optimize" 만 명시, JPA provider 별 동작 보장 아님(SPRING-PROJ-C4) | Testcontainers + Hibernate SQL 로그로 SELECT 컬럼 목록 확인 (projection vs entity 비교) | planned |
| read/query port 반환 type 이 도메인/web/JPA type 이 아님을 ArchUnit 으로 정적 강제 가능 | 메서드 반환 type 의존성 검사 rule wording 미작성 | methods().that().areDeclaredInClassesThat().haveSimpleNameEndingWith("QueryPort").should().notHaveRawReturnType(...) 류 rule + violation fixture |
planned |
| thin-path 자체를 폐기 → 검증 대상 아님 | (해당 없음 — thin-path 경로 제거) | obsolete |
|
trivial projection read 에서 no-tx 가 inRead 대비 실측 이득(connection 점유/latency)이 있다(D4 opt-in 정당화) |
VM-READTX-C3 는 메모리 절약만 — connection/latency 정량 미증명(역명제 비함의) | JMH/부하 테스트로 no-tx vs inRead 단일 row SELECT 비교 | planned |
ca-tmpl prod 의 ${DB_OPEN_IN_VIEW} 가 실제 false 로 운영된다(D4 안전 전제) |
application.yml 은 env 위임 — 실제 값 미확인(test 만 false 확인됨) |
배포 env/env-keys.yaml registry 의 DB_OPEN_IN_VIEW 기본값 확인 |
needs-confirmation |
GetRepoStatsUseCase(RepoStats 도메인 type 반환)를 D1 projection-DTO 패턴으로 이관 가능 |
도메인 type 반환을 application projection record 로 바꾸는 작업 — 단 이건 outbound HTTP read 라 capability 는 NONE 유지(repository 미접근, D5 해소) |
RepoStats → application projection record 이관 PoC. capability 는 NONE 그대로 |
planned |
| Netflix Tudum separate-store friction 근거(D2) | netflixtechblog SSL 오류로 2차 출처(ByteByteGo) 의존 — 1차 미확인 | 원 netflixtechblog 글 직접 재fetch 또는 InfoQ 교차확인 | needs-confirmation |
관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
/coverage가 채우는 생성물 — 손으로 유지하지 않는다. governing 문서(frontmattergoverning_docs)가 요구하는 관심사를 이 브랜치가 빠짐없이 덮는지의 결과. 기준:rules/coverage-gate.md. 상태:covered-here(이 브랜치 결정) /delegated(다른 owner 브랜치) /missing(아무도 안 맡음 → Blocking).
마지막 감사: 2026-06-04 (coverage-auditor) → Covered (Blocking 0 / Should-fix 3 → 위임 링크 추가로 해소 / Advisory 1). governing_docs:
clean-architecture-package-layout+data-layer-persistence-cache-outbound.
| 관심사 | 상태 | owner | 심각도 | 근거 |
|---|---|---|---|---|
| Application layer 가 adapter/transport/JPA 에 의존하지 않음 (ArchUnit isolation) | delegated | raw/branch-notes/feature-application-port-usecase-contract | OK | application_does_not_depend_on_adapters_or_transport actually-implemented. §Edge 위임 링크 |
QueryUseCase 의 @UseCaseCapability mandatory 선언 |
delegated | raw/branch-notes/feature-application-port-usecase-contract | OK | inbound_port_implementations_declare_capability. D3 Open Risk + §Edge 링크 |
QueryUseCase 명명(*UseCase suffix) |
delegated | raw/branch-notes/feature-application-port-usecase-contract | OK | inbound_port_implementations_end_with_use_case. §Edge 링크 |
| thin read path 가 capability rule 을 우회하는 문제 | covered-here | — | — | D3 (2026-06-05 Strict 확정 = thin-path 폐기) → 우회 경로 자체가 제거됨. 모든 읽기 QueryUseCase 경유 |
| Read/query port 의 application 패키지 배치(도메인·어댑터 아님) | covered-here | — | — | D1 (hexagonal purity, 도메인 type 미노출) |
| Read port 반환 type 의 domain/JPA/web 누출 방지 ArchUnit rule | covered-here | — | — | D1 §구현 가이드 §1 (DSL skeleton, planned — Claims#2) |
| Full CQRS 별도 read store 모듈 경계 | covered-here | — | — | D2 (escalation trigger 문서화, 별도 branch 위임) |
| Read-side 영속성 모델: aggregate vs projection | covered-here | — | — | D1 (코어=purity 가드레일 강제 / projection vs through-aggregate=프로젝트 선택, 2026-06-05 분할) |
| OSIV off 가 read transaction 경계에 미치는 영향 | covered-here | — | — | D4 (OSIV=false 전제 no-tx opt-in; prod env gap Claims#5) |
| Cache bypass(strict consistency read) | delegated | raw/branch-notes/feature-cache-consistency-contract | OK | §Out of scope + §Edge 위임 링크 |
| Read 격리 수준(REPEATABLE_READ 등) | delegated | raw/branch-notes/feature-transaction-concurrency-contract (D3) | OK | §Out of scope + §Edge 위임 링크 |
읽기용 Outbound HTTP 경로(RepoStatsPort 등 external read) |
delegated | raw/branch-notes/feature-outbound-http-client-baseline | OK (해소) | §Edge 위임 링크 추가됨. D5 는 capability 어휘만 |
| Read-path connection pool 영향(HikariCP + no-tx) | delegated | raw/branch-notes/feature-application-port-usecase-contract (D12) | OK (해소) | §Edge 위임 링크 추가됨 |
| Read-path 오류 분류(SQLState 57014/08* 등) | delegated | raw/branch-notes/feature-persistence-failure-baseline | OK (해소) | §Edge 위임 링크 추가됨 |
Capability 어휘 확장(READ_PROJECTION 신설 여부) |
covered-here | — | — | D5 (2026-06-05 해소: 신규 enum 불필요 — projection read = READ_REPOSITORY, 접근 수준과 반환 모양은 직교) |
| Read replica lag/라우팅 정책 | missing | (없음) | ⚪ Advisory | data-layer doc 이 documented-only 로만 명시. projection query ≠ replica routing — 본 branch 범위 밖, 프로젝트 레벨 gap(비-Blocking) |
감사 이력
branch-spec / depth / coverage 게이트가 남긴 감사 흔적. 위임 결정의 audit trail 과 깊이 보강 이력을 한 곳에 모은다(§Coverage 표·§Edge prose 와 중복이 아니라 왜 그렇게 분류·수정했는지 의 근거).
위임 audit trail (coverage)
본 branch 는 governing_docs(clean-architecture-package-layout + data-layer-persistence-cache-outbound)가 요구하는 관심사 중 다음을 명시적으로 다른 owner branch 에 위임한다. 모든 위임처는 raw/branch-notes/ 에 실재하며 §Edge 에 wikilink 가 있다(2026-06-05 재검증).
| 위임 관심사 | owner branch | 위임 근거 |
|---|---|---|
Application layer isolation / @UseCaseCapability mandatory / *UseCase 명명 |
feature-application-port-usecase-contract |
본 branch 의 read 경로가 그 계약의 확장(projection 반환)일 뿐 ceremony 는 Strict 유지(thin-path 폐기). 계약 rule 자체는 그 branch 소유 |
| Read-path connection pool 영향(HikariCP + no-tx) | raw/branch-notes/feature-application-port-usecase-contract (D12) | inNew pool-sizing SSOT. read no-tx(D4) 가 pool 경합을 바꾸면 D12 재검토 — 위임이되 역영향 경로 명시 |
| Read 격리 수준(REPEATABLE_READ 등) | raw/branch-notes/feature-transaction-concurrency-contract (D3) | isolation SSOT. D4 의 read tx 격리는 여기 위임 |
| Cache bypass(strict consistency) | feature-cache-consistency-contract |
본 branch 는 모델/데이터소스 우회만, 캐시 우회는 거기 SSOT |
읽기용 Outbound HTTP(RepoStatsPort external read) |
raw/branch-notes/feature-outbound-http-client-baseline | RestClient/Resilience4j/timeout SSOT. 본 branch 는 capability 어휘(D5)만 |
| Read-path 오류 분류(SQLState 57014/08*) | feature-persistence-failure-baseline |
SQLState classifier SSOT |
미할당 project-level gap (Advisory, 비-Blocking)
- Read replica lag / 라우팅 정책:
data-layer-persistence-cache-outbound가documented-only로만 명시, ca-tmplsrc/에 관련 코드 0건, 어느 branch 도 소유 안 함. projection query ≠ replica routing 이므로 본 branch 범위 밖. replica routing 이 운영상 필요해지면 별도feature-read-replica-routing-contract신설·할당 권고. 그 전까지 Advisory 로 유지.
깊이 게이트 보강 이력 (2026-06-05, depth-auditor 후속)
- (Blocking 해소) §구현 가이드 §1 「정적 강제」: ArchUnit DSL 을 copy 가능한 구체 호출로 제시하던 것을 의도(intent) 와 구체 API(UNSUPPORTED_IMPL_DECISION/Claims#2) 로 분리.
notHaveRawReturnType등 predicate overload 는 ArchUnit 버전 의존 + raw-type 만 검사해List<DomainType>generic 누출을 못 잡으므로 customArchCondition<JavaMethod>가 필요함을 명시 — 노트의 DSL 을 그대로 copy 금지. - (Should-fix 해소) §1 조회 메커니즘: closed projection 의 column-subset SELECT 가 Claims#1 미검증임을 명시하고
SELECT new/JdbcTemplate 1순위 선호로 보강. - (Should-fix 해소) §3: no-tx 의 latency/connection 이득이 VM-READTX-C3 직접 지지 범위 밖(역방향 추론)임을 UNSUPPORTED_IMPL_DECISION 으로 추가, Claims#4 PoC 종속.
- (Should-fix 해소) §3 no-tx opt-in 행: prod
DB_OPEN_IN_VIEW=false확인을 선결 조건 으로 승격(미확인=Disabled), Claims#5 연결.
계약 정합 재결정 (2026-06-05): thin-path 폐기 + D5 해소
사용자와의 설계 검토에서 "선행 계약(
feature-application-port-usecase-contract)이 너무 강한 강제성을 두어 후속 계약의 선택 폭이 좁아지는 것 아닌가"라는 비판을 검토한 결과. 근본 원인 진단: 선행 계약은 capability 선언을 use-case 모양에 결합(@UseCaseCapability는 use-case 구현체에만 부착)했다. 따라서 use-case 가 아닌 thin-path read 는 capability 를 달 곳이 없어 fitness function 에 안 잡힌다 — 이게 thin-path 를 막던(=D5 종속) 진짜 원인이었고, "enum 어휘(READ_PROJECTION) 부재"는 표면 증상이었다.
- D3 → Strict 단일 계약으로 확정 (thin-path 폐기). 대안이었던 "capability 를 use-case 에서 분리(port-level capability + rule)"하는 foundation 수술은 하지 않기로 결정. 이유: thin-path 의 ceremony 절감은 modest 한데, 그걸 위해 스켈레톤의 핵심 가치인 아키텍처 기계 강제 를 코드리뷰 신뢰로 격하시키는 비용이 크다. thin-path 실익 증거가 생기면 그때 후속 계약(D14 의 freeze-with-guard 패턴처럼)으로 foundation 의 capability 메커니즘을 확장. → foundation 무변경.
- D5 → 해소 (신규 enum 불필요).
ca-tmpl/.../capability/RepositoryAccess.java확인 결과{NONE, READ_REPOSITORY, WRITE_REPOSITORY}= repository 접근 수준 축. "aggregate/projection"은 반환 모양 축이라 직교 → repository projection read 는READ_REPOSITORY, outbound HTTP read 는NONE(올바른 분류, gap 아님). 반환 모양 purity 는 D1 의 반환타입 rule 이 담당.READ_PROJECTION논쟁은 두 축의 혼동이었음. - 영향 정리: §결정 D3/D5, §Decision Evidence Map D3/D5, §구현 가이드 §2(thin-path row 제거 + F4 해소), §Edge(thin path 남용·capability 모호 항목 해소), Claims(thin-path domain-logic claim →
obsolete; GetRepoStatsUseCase 이관 claim → capabilityNONE유지로 명확화), §Coverage(thin-path·READ_PROJECTION row 해소) 일괄 갱신. D1·D2·D4 는 무영향.
D1 코어/선택 분할 명문화 (2026-06-05)
"스켈레톤은 보편적으로 모두가 같게 쓰는 것만 코어에 강제해야 한다(복사되는 물건이라 안 쓰는 코드 = 지울 수 없는 인지 비용)"는 원칙을 D1 에 적용. 구현 착수 전, projection 이 강제 디폴트 로 코어에 박히는 것을 방지하기 위함.
- 분할 결정: D1 의 산출물 중 코어(모든 프로젝트 동일 강제) = read/query port 의 purity 가드레일(반환 type 이 domain/JPA/web 누출 금지 ArchUnit rule + read port 추상화 모양)뿐이다. "projection 을 기본으로 써라"는 코어에 강제하지 않는다 — projection vs through-aggregate 는 프로젝트 선택(단순 CRUD 는 through-aggregate via 기존 repository port 가 동급·기본). projection 사용 예시는
sample-portfolio에서 시연(production_code_does_not_depend_on_sample_portfolio로 격리). - 근거: purity 가드레일은 read port 를 쓸 때 깨끗함을 보장하는 보편 불변식(도메인 무관) → 코어 적합. 반면 projection 채택은 read 최적화라 상황적(read shape ≠ write 이거나 hydration 비용 회피 시 이득) → 강제 시 작은 CRUD 에 불필요한 over-engineering. ca-tmpl
RepositoryAccess(접근 수준)와 직교한 반환 모양 축이므로 capability 강제와도 무관. - 구현 지침: read-port 추상화 + purity rule 은 코어(
application-core+ ArchUnit)에. projection record/조회 메커니즘 예시는 sample. 모든 읽기에 projection port 를 만들지 말 것 — 능력·가드레일만 코어, 사용은 read 마다 선택. - 영향: §목표 헤드라인, §결정 D1, §Decision Evidence Map D1, §구현 가이드 §1(코어/선택 구분 행 + through-aggregate 경로 행 추가), §Coverage(aggregate vs projection row) 갱신. D2·D3·D4·D5 무영향.
마주친 문제
짧은 메모만. 깊이 있는 트러블슈팅은
raw/errors/로 분리하고 아래 Cluster에 연결.
- 이슈 1
- 원인:
- 시도:
- 해결: (또는 미해결이면
needs-confirmation) - 별도 에러 노트로 분리됨:
[[raw/errors/<...>]](생성 시)
묶음 (이 branch에서 파생된 자료)
- raw/company-tech-blogs/cqrs-lite-clean-architecture-read-path-bypass-wakita
- raw/company-tech-blogs/explicit-architecture-ddd-hexagonal-cqrs-hgraca
- raw/company-tech-blogs/netflix-tudum-cqrs-separate-read-store-evolution
- raw/company-tech-blogs/read-only-tx-hibernate-optimization-vladmihalcea
- raw/official-docs/cqrs-pattern-azure-architecture-center
- raw/official-docs/spring-data-jpa-projections-spring-official
- raw/official-docs/spring-data-jpa-transactionality-spring-official
이 branch는 단일 노트가 아니라 작업 묶음의 entry point. 이 branch에서 파생된 모든 raw 노트를 카테고리별로 명시. 자식 노트가 forward link만 박아도 Obsidian backlink로 자동 발견되지만, 읽기 흐름과 분류를 위해 hub가 명시적으로 그룹화한다.
Sub-branches (세부 작업)
[[raw/branch-notes/<sub-branch-1>]]— <한 줄 요약>[[raw/branch-notes/<sub-branch-2>]]— <한 줄 요약>
오류 기록 (이 branch 작업 중 발생)
- (없음 — 구현 중 에러 없음. JPQL
SELECT new/ generic-arg ArchUnit API 모두 1차 통과)
면접 준비 (이 작업에서 나올 수 있는 면접 질문)
- (별도 노트 미생성 — 면접 각도는 아래 blog-topic 의 "type erasure 가 정적 분석 사각지대를 만든다" 로 충분히 커버. 필요 시 분리)
강의 (이 작업을 위해 학습한 강의)
- (없음)
job-posting tie-ins (이 작업에서 파생된 글감)
- raw/blog-topics/archunit-generic-return-type-purity-query-port-2026-06-05 — D1 purity rule 의 generic type argument 검사 기법(
JavaType.getAllInvolvedRawTypes()) 단독 추출 - derived blog: 생성 전. 생성 시
wiki/blog/<slug>-YYYY-MM-DD.md후보
관련 일일 노트
이 브랜치를 작업한 날짜들. 양방향 nav 유지.
[[raw/daily-notes/YYYY-MM-DD]][[raw/daily-notes/YYYY-MM-DD]]
완료 후 정리
머지/종료 시점에 채움.
/ingest가 이 섹션을 기준으로 wiki/projects/에 추출.
- PR 링크: (미생성 — ca-tmpl 작업 브랜치
feature/business-rule-validation-contract위에서 구현) - 리뷰 메모: 2026-06-05 구현 완료. D1(core+demo)/D3/D4/D5 코드화, D2 documented-only 유지.
- 머지 결과 / 배포 환경: 로컬 검증 완료(locally-verified). prod 미배포.
- 구현 산출물 (ca-tmpl, 2026-06-05):
- D1 core (purity guardrail) —
src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java:query_ports_do_not_leak_domain_jpa_or_web_typesArchUnit rule + customArchCondition<JavaMethod>notLeakDomainJpaOrWebThroughReturnType(...)(사용 API:JavaMethod.getReturnType().getAllInvolvedRawTypes()→ generic type argument 의 erasure 까지 평탄화. Claims#2 의 "raw-type 검사로는List<DomainType>못 잡음" 을 custom condition 으로 해소). 타겟:..application..+ simple name*QueryPort. 금지 패키지:..domain.. / ..adapter.. / jakarta.persistence.. / javax.persistence.. / org.springframework.web.. / org.hibernate... - D1 fixtures (violations-as-data + over-block) —
.../violations/application/RawLeakQueryPort.java(raw leak),.../violations/application/GenericLeakQueryPort.java(generic-only leak — generic 검사 증명),.../allowed/application/CleanProjectionQueryPort.java(over-block guard) +ArchitectureViolationFixtureTest의 3 isolated 테스트. - D1 demo (sample-portfolio, projection read 경로) —
application/query/WorkLogSummary.java(projection record),application/query/ListRecentWorkLogSummariesQuery.java,application/port/WorkLogSummaryQueryPort.java,application/worklog/ListRecentWorkLogSummariesUseCase.java, 영속adapter/persistence/repository/WorkLogSummaryRow.java+WorkLogJpaRepository.findRecentSummaryRows(JPQLSELECT newcolumn-subset) +WorkLogSummaryQueryAdapter.java(UUID→ULID 매핑). production 코어에 "projection 기본" 미강제 — 코어는 purity rule 만, 사용 시연은 sample 격리(production_code_does_not_depend_on_sample_portfolio). - D3/D4/D5 문서화 —
src/application-core/CLAUDE.md§Read/query path(through-aggregate vs projection 표 + D1~D5) + §ArchUnit guardrails 에 신규 rule 등재.
- D1 core (purity guardrail) —
- 검증 결과 (2026-06-05, cd src):
./gradlew :sample-portfolio:test→ BUILD SUCCESSFUL (신규ListRecentWorkLogSummariesUseCaseTest2/2,WorkLogSummaryQueryAdapterTest2/2;@SpringBootTest컨텍스트 부팅 = JPQLSELECT new시동시 검증 통과)./gradlew :app-bootstrap:test→ BUILD SUCCESSFUL (CleanArchitectureTest36/36 — 신규 rule 포함;ArchitectureViolationFixtureTest30/30 — 신규 D1 3 테스트 포함)./gradlew verifyCleanArchitectureDependencies→ BUILD SUCCESSFUL
- 미해소 Claims (구현으로 닫지 않음, 의도적): Claims#1(closed projection column-subset —
SELECT new채택으로 회피, PoC 불요), Claims#4(no-tx latency PoC —inReaddefault 유지로 미수행), Claims#5(prodDB_OPEN_IN_VIEW=false— no-tx opt-in Disabled 전제로 유지). D2 escalation 정량 임계는UNSUPPORTED_IMPL_DECISION유지. - wiki 추출 대상 (verified만,
wiki/projects/로만 추출):actually-implemented항목: D3(Strict ceremony), D4(inRead default), D5(READ_REPOSITORY재사용)locally-verified항목: D1(purity guardrail rule + projection demo)prod-verified항목: (없음 — prod 미배포)
- 추출하지 않을 항목 (planned / documented-only / abandoned): D2(documented-only, separate read store)