Files
llm-wiki/raw/branch-notes/feature-application-query-bypass-contract.md

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
ca-skeleton
wiki/projects/ca-tmpl/clean-architecture-package-layout
wiki/projects/ca-tmpl/data-layer-persistence-cache-outbound
branch
ca-skeleton
application
query
cqrs
read-model
2026-06-04 review BR-CA-SKELETON-OPERATIONAL-CONTRACT-047 project-work-item ca-skeleton-operational-contract WI-CA-SKELETON-OPERATIONAL-CONTRACT-047
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-TRANSACTION-001@1
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-MODULE-LAYOUT-001@1
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-MAPPING-001@1
WI-CA-SKELETON-OPERATIONAL-CONTRACT-035
WI-CA-SKELETON-OPERATIONAL-CONTRACT-012
WI-CA-SKELETON-OPERATIONAL-CONTRACT-024
WI-CA-SKELETON-OPERATIONAL-CONTRACT-007
WI-CA-SKELETON-OPERATIONAL-CONTRACT-006
1 4c05fbdad06f0558c14b9c975d41ed0a9d49cce1c82ee4e842bc88c190ccb22d

branch: feature-application-query-bypass-contract

Layer: raw/branch-notes/ — 단일 브랜치의 TODO·결정·진행 기록. 머지/종료 후 verified 결과는 /ingestwiki/projects/에 추출. 원본은 raw에 영구 보관. status_label: in-progress | review | merged | abandoned 계층 표기: "root branch" 라는 별도 개념은 없음. project 의 직접 자식 branch 는 parent_branch:비워두고 related_projects 만 채움. 다른 branch 의 자식이면 parent_branch: <부모 branch 이름> 명시 + ## Parent 섹션의 부모 wikilink 필수.

부모 (필수)

ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT 역할. 본 feature branch 는 project-note 운영 계약 중 application read/query 경로 영역의 결정/근거/금지 사항을 정제한다.

선택 (관련 형제 branch):

브랜치 계약 패킷

  • 생성 시 프로젝트 개정: 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-contract SSOT.
  • 캐시 일관성/캐시 우회 — feature-cache-consistency-contract SSOT (본 branch 는 모델/데이터소스 우회만).
  • idempotency key 정책 — feature-rate-limit-idempotency-contract.
  • 특정 CQRS 프레임워크(Axon 등) 강제 — 채택은 조사 결과에 따르되, 프레임워크 lock-in 은 비목표.

근거 (필수, 최소 1개+)

이 branch의 결정 근거. wiki-decision-researcher 2개 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 + WorkLogSummary record + ListRecentWorkLogSummariesUseCase + 영속 WorkLogSummaryQueryAdapter(JPQL SELECT newWorkLogSummaryRow → ULID 변환))
  • D1: projection DTO 가 도메인 type / JPA entity / web DTO 가 아님을 강제하는 ArchUnit rule — 등급: locally-verified (query_ports_do_not_leak_domain_jpa_or_web_types, custom ArchCondition<JavaMethod>JavaType.getAllInvolvedRawTypes()generic type argument 까지 검사 — raw/generic/over-block 3 fixture 로 역검증)
  • D3: QueryUseCase 경유를 default 로 유지(Strict). thin read path 폐기 — 등급: actually-implemented (코드: 모든 read 가 QueryUseCase bean 경유; 신규 rule 불필요 — 선행 계약 capability fitness function 재사용. application-core/CLAUDE.md §Read/query path 문서화)
  • D4: TransactionPort.inRead default 유지. no-tx bypass opt-in 조건(OSIV=false + projection-only + no lazy) 명문화 — 등급: actually-implemented (코드: ListRecentWorkLogSummariesUseCasetx.inRead 경유 + test inReadCalled 검증; 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 는 dedicated RepoStatsPort.fetch() 를 쓰지만 반환이 도메인 type RepoStats 이고 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 @UseCaseCapability rule 을 우회하게 됨(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 은 default inRead 유지(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 없음) — 모든 읽기는 QueryUseCase bean 경유. 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_capability fitness 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-inspring.jpa.open-in-view=false and 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-tmpl RepositoryAccess.java 확인): RepositoryAccessrepository 접근 수준 축(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). / GetRepoStatsUseCaseNONE 선언은 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 이 담당. GetRepoStatsUseCaseNONE 은 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 (필수 준수):

  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 의 권장 헤더 패턴:

### 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.Query marker + sample 의 RepoStatsPort(precursor).

  • UNSUPPORTED_IMPL_DECISION: read port 인터페이스 명명/패키지 (*QueryPort vs *ReadPort, application.<domain>.port vs application.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_capability rule (actually-implemented) — @UseCaseCapabilityuse-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 그대로 — 무변경
thin read path 폐기 — 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) + OSIV application-test.yml=false/application.yml=${DB_OPEN_IN_VIEW}.

  • UNSUPPORTED_IMPL_DECISION: no-tx opt-in 의 강제 방식 — 근거는 정책 권고만, "어떻게 막을지"(ArchUnit? 문서?) 미권고. trade-off: no-tx 조회가 lazy 를 건드리면 OSIV=false 에서 LazyInitializationExceptionprojection-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.yamlDB_OPEN_IN_VIEW 기본값 = false 확인 필수(Claims#5). 미확인 시 no-tx opt-in 은 Disabledapplication.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)을 잡는다.
  • 다른 계약 의존:
    • raw/branch-notes/feature-application-port-usecase-contractD9(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 전용)에 read inRead connection 점유의 pool 영향도 위임 — read no-tx(D4) 가 pool 경합을 바꾸면 D12 재검토. / 후속 위임: capability 를 use-case 모양에서 분리(port-level capability)하는 수술은 thin-path 실익 증거가 생길 때 별도 계약(미생성)이 이 계약의 capability 메커니즘을 확장 — 본 branch 는 그 수술을 하지 않기로 결정(D3).
    • raw/branch-notes/feature-transaction-concurrency-contractD3(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 57014 query canceled / 08* connection 등)는 거기 SSOT. 본 branch read 경로도 동일 오류 경로 사용 → 위임.
    • (escalation 시) D2 → 별도 feature-cqrs-read-store-contract(미생성) 가 separate read store + 동기화 pipeline 소유.

검증해야 할 주장

공식 문서나 사례는 근거지만, 내 프로젝트에서의 동작을 자동으로 보장하지 않는다. 구현 전/중/후에 실제로 검증해야 하는 주장을 분리한다.

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 read path 의 "domain logic 없음" 을 자동 강제할 수 없다 (D3 Strict 확정으로 무효화, 2026-06-05) 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 문서(frontmatter governing_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-outbounddocumented-only 로만 명시, ca-tmpl src/ 에 관련 코드 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 누출을 못 잡으므로 custom ArchCondition<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 → capability NONE 유지로 명확화), §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에서 파생된 자료)

이 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 (이 작업에서 파생된 글감)

관련 일일 노트

이 브랜치를 작업한 날짜들. 양방향 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_types ArchUnit rule + custom ArchCondition<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(JPQL SELECT new column-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 등재.
  • 검증 결과 (2026-06-05, cd src):
    • ./gradlew :sample-portfolio:test → BUILD SUCCESSFUL (신규 ListRecentWorkLogSummariesUseCaseTest 2/2, WorkLogSummaryQueryAdapterTest 2/2; @SpringBootTest 컨텍스트 부팅 = JPQL SELECT new 시동시 검증 통과)
    • ./gradlew :app-bootstrap:test → BUILD SUCCESSFUL (CleanArchitectureTest 36/36 — 신규 rule 포함; ArchitectureViolationFixtureTest 30/30 — 신규 D1 3 테스트 포함)
    • ./gradlew verifyCleanArchitectureDependencies → BUILD SUCCESSFUL
  • 미해소 Claims (구현으로 닫지 않음, 의도적): Claims#1(closed projection column-subset — SELECT new 채택으로 회피, PoC 불요), Claims#4(no-tx latency PoC — inRead default 유지로 미수행), Claims#5(prod DB_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)