Files
llm-wiki/raw/branch-notes/feature-application-port-usecase-contract.md
T

46 KiB
Raw Blame History

title, source_type, status, branch, related_projects, tags, created, target_merge, status_label, last_updated, last_reviewed, id, kind, project, work_item, inherits, refines, overrides, depends_on, contract_packet, parent_branch, contract_packet_sha256
title source_type status branch related_projects tags created target_merge status_label last_updated last_reviewed id kind project work_item inherits refines overrides depends_on contract_packet parent_branch contract_packet_sha256
branch / feature-application-port-usecase-contract branch-note verified feature-application-port-usecase-contract
ca-skeleton
branch
ca-skeleton
application
usecase
port
transaction-port
2026-05-22 actually-implemented 2026-05-28 2026-06-04 BR-CA-SKELETON-OPERATIONAL-CONTRACT-035 project-work-item ca-skeleton-operational-contract WI-CA-SKELETON-OPERATIONAL-CONTRACT-035
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-TRANSACTION-001@1
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-MODULE-LAYOUT-001@1
1 21500ddfbea7eff6f949e176ee85f1c73636fd011e0a969b1f59d242b6f68784

branch: feature-application-port-usecase-contract

Layer: raw/branch-notes/ — application layer의 use case, input port, output port, command/query 기준을 정의합니다.

Ground-truth 대조 (2026-06-04, ca-tmpl @ffb0e13): /ingest reconcile 시 commit ffb0e13 코드를 직접 읽어 D1~D14 구현 사실을 확인 — TransactionPort(inWrite/inRead/inNew + Runnable defaults), SpringTransactionPort(모드별 pre-built TransactionTemplate, READ_COMMITTED pin), Isolation 단일값, 6종 ArchUnit rule, violations-as-data fixture, sample 모듈(@ffb0e13 명칭 sample-ticket, 이후 sample-portfolio 로 rename) 의 @Transactional 전면 제거 모두 코드에 실재. 단위 테스트 + ArchUnit PASS(2026-06-04 재실행 exit 0). status: verified. 실 DB 통합/운영 검증은 미수행(planned/위임).

부모 (필수)

ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT 역할. 본 feature branch 는 project-note 의 운영 계약 중 해당 영역 (§<관련 섹션>) 의 결정/근거/금지 사항을 정제한다.

브랜치 계약 패킷

  • 생성 시 프로젝트 개정: 1
  • 패킷 스키마: contract_packet: 1
  • 완료 조건: application port와 transaction runner architecture 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

브랜치 지역 결정

기존 branch-local 결정은 아래 ## Decision Evidence Map / 결정-근거 매핑의 D-row가 소유하며 이 packet에서 복제하지 않는다.

Decision ID Decision Relation Supporting Claims Status

선언한 예외

Override ID Overrides Reason Approval Status

목표

실제 도메인이 들어오면 application layer가 가장 먼저 비대해집니다. use case가 DTO, JPA entity, HTTP request, external client를 직접 다루지 않도록 port 계약과 command/query 모델을 고정합니다.

  • 이슈:
  • PR:

범위

포함 범위

  • command/query 분리 기준.
  • inbound port naming.
  • outbound port naming.
  • use case transaction/capability/idempotency 선언 기준.
  • application result/error 변환 기준.

제외 범위

  • 특정 command bus framework.
  • CQRS 인프라 강제.
  • domain-specific workflow engine.

TODO

TODO drained — 결정은 아래 표/결정 사항 참조.

Work Item Contract

각 TODO는 아래 판정 단위로 재작성되어야 canonical 승급 가능합니다. TODO가 단순히 기준 작성으로 남아 있으면 이 branch는 완료로 보지 않습니다.

field required rule
Decision yes 구현자가 선택해야 하는 기본값
Allowed yes 허용되는 예외와 조건
Forbidden yes 절대 금지되는 구현/문서 상태
Required registry update conditional error/env/header/log/metric/capability 변경 시 필수
Required contract test yes 계약 위반 시 실패해야 하는 테스트
Failure condition yes review/build에서 실패로 판정할 상태
Canonical extraction target yes wiki/projects 승급 위치

결정 사항

  • 2026-05-22: inbound port는 *UseCase, outbound port는 *Port를 기본 naming으로 둠.
  • 2026-05-22: command use case와 query use case를 기본 분리.
  • 2026-05-22: transaction boundary는 application use case 책임이지만 Spring @Transactional 직접 import는 금지하고 TransactionPort 또는 TransactionalUseCaseRunner abstraction을 기본값으로 둠.
  • 2026-05-22: write use case는 transactionMode, idempotency, repositoryAccess를 명시해야 함. query use case는 readOnly transaction mode를 기본값으로 둠.
  • 2026-05-28 (implementation): TransactionPort 선택. TransactionalUseCaseRunner 는 채택 안 함 (단일 abstraction 면 충분, 두 추상이 공존하면 사용 지침이 모호해짐).
  • 2026-05-28 (implementation): TransactionPort API 는 inWrite / inRead / inNew 3 메서드. NESTEDNEVER 는 API 에서 노출 안 함 (브랜치 노트 금지 사항).
  • 2026-05-28 (implementation): Isolation enum 은 READ_COMMITTED 만 노출. REPEATABLE_READ, SERIALIZABLEfeature-transaction-concurrency-contract 브랜치로 위임.
  • 2026-05-28 (implementation): SpringTransactionPort 는 모드별 TransactionTemplate 인스턴스 3개를 미리 빌드해서 보관. setReadOnly / setPropagationBehavior 의 호출당 mutation 으로 인한 동시성 위험 차단.
  • 2026-05-28 (implementation): Idempotency enum 값은 IDEMPOTENT / KEYED / NOT_IDEMPOTENT 세 종. KEYED 는 idempotency key 기반 dedup 필요 표시 (브랜치 노트의 feature-rate-limit-idempotency-contract 가 후속 운영).
  • 2026-05-28 (implementation): application-core Gradle 의 spring-tx 의존성 제거. @Transactional 어노테이션이 모듈 컴파일 classpath 자체에 없도록 belt+suspenders.
  • 2026-05-28 (D11 checked-exception wrapping): TransactionPortSupplier<T> / Runnable 시그니처 유지 (checked exception 시그니처에 노출 안 함). Spring TransactionTemplate.execute(TransactionCallback<T>) throws TransactionException 도 동일 제약 — 이는 TransactionPort 설계 결함이 아닌 Spring 공식 idiom. Wrapping 정책: 도메인 checked → DomainException extends RuntimeException, IOExceptionUncheckedIOException, SQLException → Spring DataAccessException 계층이 자동 wrap. 근거: raw/official-docs/transaction-template-spring-official#TX-TMPL-C2/C3 ("RuntimeException ... rollback ... propagated").
  • 2026-05-28 (D12 REQUIRES_NEW pool sizing): inNew 호출은 새 physical JDBC connection 획득 (outer transaction 의 connection 은 그대로 점유). Pool sizing 제약 — hikari.maximumPoolSize ≥ (concurrent_threads × (1 + max_inNew_depth)) + 1. Forbidden: inNew 를 loop 안에서 per-record 호출 (anti-pattern, pool exhaustion + deadlock 위험). 근거: raw/official-docs/spring-tx-propagation-required-new-nested-official#SPRING-PROP-C1~C4.
  • 2026-05-28 (D13 application 의 Spring DI 의존): application-coreorg.springframework.stereotype.{Service,Component} import 및 사용 허용 (DI 등록 목적). Spring core (spring-context / spring-beans) 의존은 유지하되 spring-tx / org.springframework.web / JPA annotation 은 forbidden 유지. 이유: Spring DI 없이 use case bean 등록을 매번 @Configuration 수동 작성하면 boilerplate 폭발.
  • 2026-05-28 (D14 KEYED idempotency freeze): @UseCaseCapability(idempotency = Idempotency.KEYED) 사용은 후속 branch feature-rate-limit-idempotency-contract merge 전까지 금지. 이유: key source (HTTP header / command field / domain ID) 와 storage backend (Redis / DB / in-memory) 와 TTL 정책이 미정인 상태에서 KEYED 를 달면 undefined behavior. 임시 ArchUnit rule: inbound_port_implementations_do_not_declare_keyed_idempotency (feature-rate-limit-idempotency-contract merge 시 제거).

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

본 branch의 결정 근거. 상세 비교는 §외부 근거 / 대안 조사 (있다면) 참조.

Source 정당화하는 결정
raw/company-tech-blogs/transaction-port-clean-ddd-spring-medium UNIL의 동일 진화 경로 (2024-05
raw/company-tech-blogs/transaction-port-vassilis-soum-github-readme TransactionPort 참고 구현
raw/official-docs/at-transactional-spring-official raw/company-tech-blogs/hexagonal-reflectoring-transactional-placement (Hexagonal 표준 다수파
raw/company-tech-blogs/hexagonal-reflectoring-transactional-placement Hexagonal 표준 다수파
raw/official-docs/transaction-template-spring-official
raw/official-docs/functional-tx-arrow-kt-resource-docs Arrow Kt
raw/company-tech-blogs/custom-transaction-interceptor-catnipcoder
raw/company-tech-blogs/woowahan-hexagonal-multimodule multi-module 분리
raw/official-docs/arch-hexagonal-cockburn primary port = use case interface 의 원형 (Cockburn alistair.cockburn.us — engineering-blog 등급, official-standard 아님). D1 의 *Port 명명과 D3 의 application↔외부 경계 abstraction 의 inside/outside asymmetry 사상 근거
raw/official-docs/spring-tx-management-reference Spring transaction abstraction (PlatformTransactionManager SPI) + propagation 기본값 + self-invocation 우회 + readOnly 적용 범위 (official-vendor-doc). D3 (TransactionPort abstraction 이 회피하려는 함정), D4 (@Transactional 다수파), D9 (readOnly transaction) 의 vendor 공식 근거
raw/official-docs/spring-transaction-synchronization-manager-javadoc registerSynchronization() 이 commit-bound domain event publish 의 공식 SPI 임을 정당화 (TSM-C3). per-thread 자원 격리 보장으로 multi-tenant 호환성 근거 제공 (TSM-C1, TSM-C4).
raw/official-docs/spring-tx-propagation-required-new-nested-official TransactionPort.inNew (PROPAGATION_REQUIRES_NEW) 의 independent physical transaction 보장 + connection pool exhaustion / deadlock 경고 (SPRING-PROP-C1~C4) + NESTED savepoint 동작 (SPRING-PROP-C5) — spring-tx-management-reference.md 가 직접 인용하지 않는 REQUIRES_NEW connection 동작 보강
raw/company-tech-blogs/axonframework-transactionmanager-spring-adapter D3 OSS PRECEDENT — Axon TransactionManager.executeInTransaction(Runnable) + fetchInTransaction(Supplier<T>) 가 ca-tmpl inWrite/inRead 와 closure 시그니처 1:1 매칭 (AXON-TX-C1~C3). 3.6k stars enterprise OSS — closure-based abstraction 패턴의 production precedent. 단 specific 3중 메소드 구조 / TransactionPort 명명은 ca-tmpl 자체
raw/company-tech-blogs/buckpal-archunit-lombok-allowlist-direct-transactional D3/D8 CONTRARY EVIDENCE — Buckpal (Hombergs 책 hex-arch 공식 reference, 2.5k stars) 의 application service 가 @Component @Transactional 직접 부착 (BUCKPAL-TX-C1, BUCKPAL-TX-C2). ca-tmpl 의 "Spring @Transactional import forbidden" 정책이 OSS best practice 가 아님을 명시
raw/company-tech-blogs/spring-modulith-archunit-generated-exemption-and-violations-as-data D3/D8 CONTRARY EVIDENCE — Spring 공식 incubator 가 @ApplicationModuleListener@Transactional(propagation = REQUIRES_NEW) 를 meta-annotation 재노출 (SPRING-MOD-TX-C1). ca-tmpl forbidden 정책과 Spring 팀 방향이 정면 충돌함을 명시. feature-domain-event-outbox-contract 입력으로 Event Publication Registry (SPRING-MOD-TX-C2) 활용 가능

외부 근거 / 대안 조사 (2026-05-22 — Topic 2)

본 branch의 TransactionPort abstraction 결정에 대한 외부 source. 5종 대안 비교는 (예정) wiki/concepts/transaction-boundary-abstraction.md 참조.

판정 기준

구분 기준
Decision application은 use case와 port를 통해서만 외부와 연결
Allowed read-only query use case는 readOnly transaction과 READ_REPOSITORY capability만 선언 가능
Forbidden use case method가 HTTP DTO, JPA entity, external client response를 직접 받음. application package가 Spring transaction annotation을 직접 import
Required fields command/query input, use case capability, transaction mode, idempotency 여부, repository access capability
Failure condition application package가 infrastructure 구현체나 presentation DTO를 import하면 실패

TransactionPort Contract

field default
abstraction name TransactionPort 또는 TransactionalUseCaseRunner
write mode required
query mode readOnly
propagation REQUIRES_NEW은 outbox/audit row 명시 선언 시만 허용. NESTED와 NEVER는 어떤 경우에도 forbidden (transaction-concurrency와 일관).
isolation READ_COMMITTED (transaction-concurrency SSOT 위임). 묵시적 vendor default 사용은 forbidden.
forbidden import org.springframework.transaction.annotation.Transactional in application package
callback signature Supplier<T> / Runnable (checked exception 노출 안 함 — Spring TransactionCallback 과 동일 제약). 호출 측에서 RuntimeException 으로 wrap.
inNew connection cost 호출당 새 physical JDBC connection 획득. Pool sizing: hikari.maximumPoolSize ≥ (concurrent_threads × (1 + max_inNew_depth)) + 1. Loop 안에서 호출 금지.

infrastructure가 Spring transaction implementation을 제공하고 application은 port만 호출합니다.

결정-근거 매핑

각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시. company-tech-blog 증거는 company-case-study 로 표기하며 공식 best practice 로 승격하지 않음.

Decision ID Decision Supporting Claims Evidence Strength Open Risk
D1 inbound port = *UseCase, outbound port = *Port naming convention raw/official-docs/arch-hexagonal-cockburn.md#HEX-COCKBURN-ORIG-C3 (port = plug-point for conversation with external agency), raw/official-docs/arch-hexagonal-cockburn.md#HEX-COCKBURN-ORIG-C4 (adapter converts port API to device signals) engineering-blog (Cockburn 개인 블로그 — official-standard 아님) HEX-COCKBURN-ORIG-C3/C4 Does not prove: *UseCase (inbound) 와 *Port (outbound) 의 specific suffix convention — Cockburn 은 "primary/secondary port" 일반 개념만 명시. *UseCase suffix 는 buckpal / ca-tmpl 자체 차용
D2 command use case 와 query use case 기본 분리 UNSUPPORTED_DECISION (cited raw 중 CQS/CQRS 분리 권고 직접 인용 없음) n/a Greg Young / Martin Fowler CQRS source 또는 Spring @Transactional(readOnly=true) 권고 source 보강 필요
D3 application use case 가 transaction boundary 의 owner — but Spring @Transactional 직접 import 금지, TransactionPort / TransactionalUseCaseRunner abstraction 사용 raw/company-tech-blogs/transaction-port-clean-ddd-spring-medium.md#UNIL-TX-C1, raw/company-tech-blogs/transaction-port-clean-ddd-spring-medium.md#UNIL-TX-C2, raw/company-tech-blogs/transaction-port-vassilis-soum-github-readme.md#VSOUM-TX-C1, raw/company-tech-blogs/transaction-port-vassilis-soum-github-readme.md#VSOUM-TX-C2 / OSS PRECEDENT (closure-based pattern): raw/company-tech-blogs/axonframework-transactionmanager-spring-adapter.md#AXON-TX-C1, #AXON-TX-C2 (Axon TransactionManager.executeInTransaction(Runnable) + fetchInTransaction(Supplier<T>) 시그니처는 ca-tmpl inWrite(Supplier) 와 1:1 매칭, 3.6k stars enterprise OSS), #AXON-TX-C3 (SpringTransactionManager(PlatformTransactionManager) adapter 구조 ca-tmpl SpringTransactionPort 와 동일) / CONTRARY EVIDENCE (다수파 = @Transactional 직접 부착): raw/company-tech-blogs/buckpal-archunit-lombok-allowlist-direct-transactional.md#BUCKPAL-TX-C1, #BUCKPAL-TX-C2 (Buckpal hex-arch 공식 reference 가 @Component @Transactional 직접 application service 부착, abstraction 없음) / CONTRARY (Spring Modulith): raw/company-tech-blogs/spring-modulith-archunit-generated-exemption-and-violations-as-data.md#SPRING-MOD-TX-C1 (@ApplicationModuleListener 가 meta-annotation 으로 @Transactional 재노출 — Spring 공식 incubator 가 forbidden 방향과 정반대) company-case-study + engineering-blog (TransactionPort pattern 자체) + company-case-study (Axon enterprise OSS precedent) + UNSUPPORTED_DECISION (CONTRARY for specific shape) (1) closure-based abstraction 패턴 자체는 강화됨 — Axon (3.6k stars) + Spanner SDK 가 동일 closure 시그니처 사용. (2) 그러나 TransactionPort literal 명칭 + inWrite/inRead/inNew 3중 메소드 구조는 OSS 1:1 매칭 없음 — Axon 은 TransactionManager + 2 메소드. ca-tmpl 의 specific shape 은 자체 결정. (3) forbidden 정책은 OSS 다수파 (Buckpal) 와 Spring 공식 incubator (Modulith) 양쪽과 충돌 — multi-Gradle-module Hexagonal context 에서 application 모듈 순수성을 위한 자체 stricter taste. UNSUPPORTED_DECISION(CONTRARY) for forbidden enforcement
D4 (대안 비교) @Transactional 직접 부착이 hexagonal 표준 다수파임을 인정 raw/official-docs/at-transactional-spring-official.md#AT-TX-C1, raw/official-docs/at-transactional-spring-official.md#AT-TX-C3, raw/official-docs/at-transactional-spring-official.md#AT-TX-C5, raw/company-tech-blogs/hexagonal-reflectoring-transactional-placement.md#HEX-REFL-C1, raw/company-tech-blogs/hexagonal-reflectoring-transactional-placement.md#HEX-REFL-C5 official-vendor-doc + engineering-blog (HEX-REFL-C5 는 negative claim — 저자가 명시적 정당화 없음) Spring 공식 권고 (AT-TX-C1) 와 ca-tmpl D3 결정 사이 분기점 — 채택 결정 정당화가 abstraction 의 testability 이득에 의존
D5 (대안 비교) TransactionTemplate programmatic 옵션 raw/official-docs/transaction-template-spring-official.md#TX-TMPL-C1, raw/official-docs/transaction-template-spring-official.md#TX-TMPL-C2, raw/official-docs/transaction-template-spring-official.md#TX-TMPL-C3, raw/official-docs/transaction-template-spring-official.md#TX-TMPL-C4 official-vendor-doc (Spring 팀 공식 programmatic 권장 도구) callback 접근법이 declarative 보다 우월하다는 뜻 아님 (TX-TMPL-C2) — application 이 import 해야 하는 부담 잔존
D6 (대안 비교) Functional Resource monad (Arrow Kt) raw/official-docs/functional-tx-arrow-kt-resource-docs.md#ARROW-RES-C1, raw/official-docs/functional-tx-arrow-kt-resource-docs.md#ARROW-RES-C5 official-vendor-doc (Arrow vendor 공식, JDBC/JPA 1:1 매퍼는 별도 — ARROW-RES-C1 Usage Boundaries 참조) Java 코드베이스 적용 어려움 — Kotlin coroutines 전제 (ARROW-RES-C2)
D7 (대안 비교) Custom TransactionInterceptor (AOP) raw/company-tech-blogs/custom-transaction-interceptor-catnipcoder.md#CATNIP-TXINT-C1, raw/company-tech-blogs/custom-transaction-interceptor-catnipcoder.md#CATNIP-TXINT-C2, raw/company-tech-blogs/custom-transaction-interceptor-catnipcoder.md#CATNIP-TXINT-C3, raw/company-tech-blogs/transaction-port-vassilis-soum-github-readme.md#VSOUM-TX-C3, raw/company-tech-blogs/transaction-port-vassilis-soum-github-readme.md#VSOUM-TX-C4 engineering-blog + company-case-study (개인 블로그 + GitHub README, Spring 공식 권장 패턴 아님) bean override (VSOUM-TX-C4) 활성화의 side-effect 부담. Spring internal API stability 미보장
D8 (보강) 우아한형제들 hexagonal multi-module 분리 사례 raw/company-tech-blogs/woowahan-hexagonal-multimodule.md#WW-HEX-C1, raw/company-tech-blogs/woowahan-hexagonal-multimodule.md#WW-HEX-C2, raw/company-tech-blogs/woowahan-hexagonal-multimodule.md#WW-HEX-C3, raw/company-tech-blogs/woowahan-hexagonal-multimodule.md#WW-HEX-C5 company-case-study (best practice 승격 금지 — WW-HEX-C5 는 negative: 우아한형제들 글이 transaction boundary 정책 직접 다루지 않음) 4-hexagon 구성은 우아한형제들 특정 사례 — ca-tmpl 의 module 분리에 1:1 mapping 보장 안 됨
D9 read-only query use case 는 readOnly transaction + READ_REPOSITORY capability 만 선언 가능 raw/official-docs/spring-tx-management-reference.md#SPRING-TX-MGR-C6 (readOnly 속성이 REQUIRED/REQUIRES_NEW propagation 한정 적용), raw/official-docs/spring-tx-management-reference.md#SPRING-TX-MGR-C3 (default propagation = REQUIRED — readOnly 적용 가능 전제) official-vendor-doc SPRING-TX-MGR-C6 Does not prove: driver level flush mode 변경의 구체 동작 — Spring Data JPA / Hibernate session statistics 측정은 별도 PoC 필요. READ_REPOSITORY capability 자체는 ca-tmpl 자체 contract
D10 application package 가 org.springframework.web / JPA entity / adapter implementation import 금지 (ArchUnit fitness function) UNSUPPORTED_DECISION (ArchUnit 의 정적 검사 가능 범위는 별도 source — 본 branch cited raw 에 ArchUnit 직접 인용 없음) n/a raw/official-docs/archunit-conditional-on-property-3-layer-pattern (AUCP-C1~C5) fetch + ArchUnit fitness function source 보강 필요
D11 TransactionPort 콜백 시그니처는 Supplier<T> / Runnable (checked exception 노출 안 함). 호출 측에서 RuntimeException 으로 wrap raw/official-docs/transaction-template-spring-official.md#TX-TMPL-C2, raw/official-docs/transaction-template-spring-official.md#TX-TMPL-C3 (Spring TransactionTemplate.execute(TransactionCallback) throws TransactionException 도 동일 제약 + "RuntimeException ... rollback ... propagated") official-vendor-doc TX-TMPL-C2/C3 Does not prove: Supplier<T>TransactionCallback<T> 와 동등하다는 명시적 진술 — Spring 공식이 별도 functional interface TransactionCallback 을 둔 것은 사실. ca-tmpl 의 Supplier<T> 채택은 boilerplate 감소를 위한 자체 결정
D12 inNew (PROPAGATION_REQUIRES_NEW) 호출은 새 physical JDBC connection 획득. Pool sizing 제약 명시 + loop 안 호출 금지 raw/official-docs/spring-tx-propagation-required-new-nested-official.md#SPRING-PROP-C1, raw/official-docs/spring-tx-propagation-required-new-nested-official.md#SPRING-PROP-C2, raw/official-docs/spring-tx-propagation-required-new-nested-official.md#SPRING-PROP-C3, raw/official-docs/spring-tx-propagation-required-new-nested-official.md#SPRING-PROP-C4 official-vendor-doc (Spring 공식 직접 인용 — "always uses an independent physical transaction" + "new database connection" + "exhaustion of the connection pool" + "Do not use ... unless your connection pool is appropriately sized") max_inNew_depth 의 실제 측정은 도메인 use case 별 통합 테스트 필요 — feature-domain-event-outbox-contract outbox 구현 단계에서 확정
D13 application-coreorg.springframework.stereotype.{Service,Component} 허용 (DI 등록 목적). spring-context / spring-beans 의존은 유지하되 spring-tx / web / JPA annotation 은 forbidden UNSUPPORTED_DECISION — Spring 공식이 "application layer 에서 @Service 허용 / 금지" 를 직접 진술한 source 없음. ca-tmpl 의 실용주의 자체 결정 project-decision 대안: @Configuration manual bean 등록 — boilerplate 폭발. 대안 채택 시 D13 retract 검토
D14 @UseCaseCapability(idempotency = Idempotency.KEYED) 사용은 feature-rate-limit-idempotency-contract merge 전까지 금지 project-decision — key source / storage / TTL 미정 시 undefined behavior 회피. 후속 branch 가 정의되기 전까지 임시 freeze project-decision freeze 자체는 ArchUnit rule (inbound_port_implementations_do_not_declare_keyed_idempotency) 로 강제. merge 시점에 rule 제거 + KEYED 활성

검증해야 할 주장

공식 문서나 사례는 근거지만, 내 프로젝트에서의 동작을 자동으로 보장하지 않는다.

Claim Why uncertain How to verify Status
TransactionPort abstraction 이 Spring @Transactional 의 propagation / isolation / rollback policy 전 표현력을 동등하게 capture 가능한지 UNIL-TX-C2 가 명시한 Runnable 시그니처는 단순 — REQUIRES_NEW, NESTED, noRollbackFor, timeout 등 전 옵션 표현 가능한지 미증명 port interface design 에 transaction options 별 method 정의 + 통합 테스트로 outbox/audit row REQUIRES_NEW 동작 확인 partially-implemented (2026-05-28 round 2) — inWrite / inRead / inNew 3 메서드 + Supplier<T> / Runnable 시그니처 (D11). NESTED / NEVER / noRollbackFor / timeout 는 의도적으로 미노출 (브랜치 노트 forbidden 와 일관). TransactionPort.inNew Javadoc 에 D12 의 pool-sizing 공식과 loop anti-pattern 명시 + application-core/CLAUDE.md / adapter-persistence/CLAUDE.md 에 cross-link. REQUIRES_NEW 의 실제 outbox 동작 통합 검증은 feature-domain-event-outbox-contract 로 위임.
application package 의 ArchUnit rule 이 org.springframework.transaction.annotation.Transactional import 를 실제로 catch AUCP-C1 의 PREDICATE/CONDITION 모델로 import 검사 가능 (bytecode 기반) — but rule wording 필요 ArchUnit rule noClasses().that().resideInAPackage("..application..").should().dependOnClassesThat().haveFullyQualifiedName("org.springframework.transaction.annotation.Transactional") 작성 + violating PR 통합 테스트 actually-implemented (2026-05-28) — app-bootstrap/src/test/.../CleanArchitectureTest#application_does_not_use_spring_transactional_annotation 으로 작성. sample-portfolioapp-bootstrap testImplementation 으로 추가해 ArchUnit scope 에 포함. 기존 sample-portfolio UserService / PostService 에서 @Transactional 제거 시 rule 통과 확인 (./gradlew :app-bootstrap:test --tests '*CleanArchitectureTest').
TransactionPort infrastructure 구현이 Spring TransactionTemplate (TX-TMPL-C3) 또는 @Transactional AOP proxy (AT-TX-C4) 중 어느 것으로 더 안전한지 두 옵션 모두 cited official-doc 에서 지원 — self-invocation 함정 (AT-TX-C5) 회피 차이 infrastructure adapter 두 버전 prototype + self-invocation 테스트 (port 메서드가 다른 port 메서드 호출) partially-implemented (2026-05-28) — SpringTransactionPortTransactionTemplate 기반으로 구현됨. 모드별 미리 빌드된 인스턴스를 사용하여 동시성 안전. self-invocation 테스트는 outbox 구현 단계로 위임.
readOnly = true transaction 이 실제로 driver 수준 flush mode 변경을 트리거 D9 가 UNSUPPORTED_DECISION — Spring Data JPA / Hibernate 별 동작 차이 Hibernate session statistics 로 flush count 측정 + readOnly true/false 비교 planned — DB 통합 테스트 환경 (Testcontainers) 후 별도 PoC. 현재는 단위 테스트로 TransactionTemplate.isReadOnly() == true 만 확인 (SpringTransactionPortTest).
use case naming convention (*UseCase / *Port) 이 팀 내 일관성으로 강제 가능 D1 이 UNSUPPORTED_DECISION — Spring 공식 권고 부재 ArchUnit class naming rule + checkstyle rule 작성 actually-implemented (2026-05-28) — inbound_port_implementations_end_with_use_case ArchUnit rule 작성. *Port outbound naming 은 별도 rule 으로 추후 추가 가능 (현재는 inbound 만).
outbound adapter 호출 use case 의 EXTERNAL_OUTBOUND_ALLOWED capability annotation 이 작동 capability annotation spec 자체가 ca-tmpl 자체 contract — 외부 source 무관 annotation + ArchUnit rule + capability registry SSOT 작성 후 통합 테스트 partially-implemented (2026-05-28) — @UseCaseCapability(externalOutboundAllowed = ...) 정의 + inbound_port_implementations_declare_capability rule 으로 capability annotation 자체는 mandatory. externalOutboundAllowed = true 가 없는 use case 가 outbound *Port 호출 시 실패시키는 dependency-aware rule 은 후속 (outbound port marker 가 먼저 필요).
presentation 분리 (UNIL-TX-C4) 가 application layer 에서 강제 가능 UNIL-TX-C4 의 "presentation" 경계가 모호 (HTTP 응답만? 이벤트 발행도?) use case 결과 type 을 domain object 로 강제 + presentation mapper 를 adapter layer 로 배치 + ArchUnit rule needs-confirmation — 현재 ArchUnit application_does_not_depend_on_adapters_or_transportorg.springframework.web.. 추가로 transport 의존 차단. event publication 경계는 feature-domain-event-outbox-contract 로 위임.

테스트 계약

  • application use case가 org.springframework.web, JPA entity, adapter implementation을 import하면 실패.
  • application use case가 org.springframework.transaction.annotation.Transactional을 직접 import하면 실패.
  • write use case에 transaction/capability 선언이 없으면 실패.
  • outbound adapter 호출 use case에 EXTERNAL_OUTBOUND_ALLOWED가 없으면 실패.

완료 후 wiki 추출 대상

  • wiki/projects/ca-skeleton-operational-contract.md의 application port/use case canonical section.

본 branch는 TransactionPort interface spec 자체가 Decisionized Work Items 등가. 별도 7-column 표는 작성하지 않음.

구현 결과

Files changed (round 2)

  • src/application-core/src/main/java/dev/caskeleton/application/transaction/TransactionPort.java — Javadoc 확장: D11 (Supplier/Runnable + RuntimeException wrap) + D12 (inNew pool-sizing 공식 + loop anti-pattern).
  • src/application-core/CLAUDE.md — D13 (Spring DI 허용 + spring-boot-starter 잔존 이유), D14 (KEYED idempotency freeze), D11 (ApplicationContext 금지), Lombok forbidden 명시, ArchUnit guardrail 목록 갱신.
  • src/adapter-persistence/CLAUDE.md — D12 (inNew pool-sizing + loop forbidden) + MapStruct @Generated exemption ArchUnit predicate 예시 (D9 of architecture-enforcement-rules).
  • src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.javadomain_is_purelombok.. forbidden 추가 (D3 of architecture-enforcement-rules). 새 rule 3종 추가: application_does_not_depend_on_application_context (D11), inbound_port_implementations_do_not_declare_keyed_idempotency (D14 — custom ArchCondition 으로 KEYED enum 값 catch).
  • src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/ArchitectureViolationFixtureTest.java — violations-as-data 네거티브 테스트 (Claims to Verify of architecture-enforcement-rules) — 6개 rule 의 실 동작을 fixture 로 보증.
  • src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/violations/ — 의도된 위반 fixture 클래스 6종 (domain 1 + application 5).
  • src/app-bootstrap/build.gradletestCompileOnly 'org.springframework:spring-tx' 추가 (violation fixture 의 @Transactional import 만을 위해).
  • CLAUDE.md (root) — api vs implementation 정책 추가 (D9 of skeleton-package-blueprint-contract).

Verification (round 2)

Command Result
cd src && ./gradlew check PASS — 25 actionable tasks.
cd src && ./gradlew :app-bootstrap:test --tests '*CleanArchitectureTest' PASS — 14 tests (9 originals + 5 round-1 = 14; this round added 2 rules and modified 1, no change in test count visible from @ArchTest count = 14).
cd src && ./gradlew :app-bootstrap:test --tests '*ArchitectureViolationFixtureTest' PASS — 6 negative tests (each rule catches its fixture violation).

구현 결과

Files changed

application-core (new contract types)

  • src/application-core/src/main/java/dev/caskeleton/application/usecase/UseCase.java — generic inbound port base.
  • src/application-core/src/main/java/dev/caskeleton/application/usecase/CommandUseCase.java — write inbound port (C extends Command).
  • src/application-core/src/main/java/dev/caskeleton/application/usecase/QueryUseCase.java — read inbound port (Q extends Query).
  • src/application-core/src/main/java/dev/caskeleton/application/command/Command.java — write-intent marker.
  • src/application-core/src/main/java/dev/caskeleton/application/query/Query.java — read-intent marker.
  • src/application-core/src/main/java/dev/caskeleton/application/transaction/TransactionPort.javainWrite / inRead / inNew (+ Runnable defaults).
  • src/application-core/src/main/java/dev/caskeleton/application/transaction/TransactionMode.javaWRITE / READ_ONLY / REQUIRES_NEW.
  • src/application-core/src/main/java/dev/caskeleton/application/transaction/Isolation.javaREAD_COMMITTED only.
  • src/application-core/src/main/java/dev/caskeleton/application/capability/UseCaseCapability.java — runtime-retained annotation, required fields.
  • src/application-core/src/main/java/dev/caskeleton/application/capability/Idempotency.javaIDEMPOTENT / KEYED / NOT_IDEMPOTENT.
  • src/application-core/src/main/java/dev/caskeleton/application/capability/RepositoryAccess.javaNONE / READ_REPOSITORY / WRITE_REPOSITORY.
  • src/application-core/build.gradle — drop spring-tx; comment explains why.
  • src/application-core/CLAUDE.md — document the contract surface, allowed transactional shapes, ArchUnit guardrails.

application-core (unit tests)

  • src/application-core/src/test/java/dev/caskeleton/application/capability/UseCaseCapabilityTest.java — 3 tests.
  • src/application-core/src/test/java/dev/caskeleton/application/transaction/TransactionPortTest.java — 4 tests (Supplier + Runnable delegation per mode).
  • src/application-core/src/test/java/dev/caskeleton/application/usecase/UseCaseContractTest.java — 2 tests (Command/Query use case wiring).

adapter-persistence

  • src/adapter-persistence/src/main/java/dev/caskeleton/adapter/persistence/transaction/SpringTransactionPort.java — Spring-backed TransactionPort (pre-built TransactionTemplate per mode, READ_COMMITTED pinned).
  • src/adapter-persistence/src/test/java/dev/caskeleton/adapter/persistence/transaction/SpringTransactionPortTest.java — 4 tests (propagation / isolation / readOnly / rollback-on-exception).
  • src/adapter-persistence/CLAUDE.md — document TransactionPort implementation + repository-adapter forbidden @Transactional.

app-bootstrap (ArchUnit fitness functions)

  • src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java — added 3 new rules (application_does_not_use_spring_transactional_annotation, inbound_port_implementations_end_with_use_case, inbound_port_implementations_declare_capability) + org.springframework.web.. added to existing application-forbid list.
  • src/app-bootstrap/build.gradletestImplementation project(':sample-portfolio') so ArchUnit can analyse the template's reference implementation. Production scope unaffected.

sample-portfolio (migration to TransactionPort)

  • src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/UserService.java — replaced @Transactional(readOnly=true) class-level + @Transactional method-level with TransactionPort.inRead / inWrite calls. TransactionPort injected via constructor.
  • src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/PostService.java — same migration pattern.
  • src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/adapter/persistence/repository/PostRepositoryAdapter.java — removed @Transactional from deleteByAuthorId (caller owns the transaction now).

Verification commands

Command Result
cd src && ./gradlew :application-core:test PASS — 9 tests (3 + 4 + 2).
cd src && ./gradlew :adapter-persistence:test PASS — 4 tests.
cd src && ./gradlew :app-bootstrap:test --tests '*CleanArchitectureTest' PASS — 12 tests (9 original + 3 new).
cd src && ./gradlew check PASS — 25 actionable tasks.
cd src && ./gradlew verifyCleanArchitectureDependencies PASS.

Evidence labels

  • actually-implemented: contract types in application-core, SpringTransactionPort, 3 new ArchUnit rules, sample-portfolio migration to TransactionPort.
  • locally-verified: full ./gradlew check green; ArchUnit rules verified against the migrated reference implementation.
  • documented-only: *Port outbound naming rule, externalOutboundAllowed dependency-aware rule, REPEATABLE_READ / SERIALIZABLE isolation — explicitly deferred with rationale.
  • planned: readOnly driver flush-mode integration test (needs Testcontainers).

마주친 문제

  • ArchUnit @AnalyzeClasses(packages = "dev.caskeleton")app-bootstrap 의 컴파일 classpath 만 본다는 점을 발견. sample-portfolio 은 production 의존 매트릭스 상 app-bootstrap 가 import 하지 않으므로 ArchUnit scope 에 안 잡혀서 새 rule 이 vacuously 통과. → testImplementation project(':sample-portfolio') 추가로 test-scope only inclusion. production dependency check (verifyCleanArchitectureDependencies) 는 ['api', 'implementation', 'compileOnly', 'runtimeOnly'] 만 검사하므로 영향 없음. ArchUnit production_code_does_not_depend_on_sample_portfolio rule 은 ImportOption.DoNotIncludeTests 로 test 클래스 제외하므로 여전히 production drift 만 catch. (raw/errors/archunit-test-scope-sample-portfolio-inclusion-2026-05-28.md 참조)
  • 초기에 IDE diagnostics 가 stale 상태로 Transactional cannot be resolved 오류를 표시. Edit 직후 IDE refresh 가 따라잡기 전 noise 임을 확인 후 무시. 실제 grep -n Transactional 로 import 부재 검증.

묶음

본 feature branch 는 leaf — 자식 자료 없음. Phase C2 실 코드 작성 단계에서 errors / interview prep / lectures 가 누적되면 본 섹션에서 그룹화.

근거 자료

오류 기록 (본 feature 작업 중 발생)

면접 준비 (이 작업에서 나올 수 있는 면접 질문)

Blog topics (이 작업에서 나올 수 있는 글감)

진행 중 메모

  • port naming·transaction boundary 계약과 구현 결과는 위 판정 기준 및 구현 결과 절에서 추적한다.

구현 가이드

  • inbound port는 use case capability를, outbound port는 외부 기술 의존을 추상화한다.
  • transaction 시작·종료는 application boundary가 TransactionPort를 통해 요청하고 domain은 framework annotation을 알지 않는다.
  • read-only와 write use case fixture를 분리해 dependency direction을 architecture test로 검증한다.

엣지·실패·의존

  • adapter가 application 구현체를 우회하거나 domain이 transaction API를 직접 참조하면 경계가 무너진다.
  • query bypass·transaction concurrency·module layout 계약이 본 port 규칙을 소비한다.

관련 일일 노트

  • 별도 일일 노트 없음.

완료 후 정리

머지/종료 시점에 채움.

  • PR 링크:
  • 리뷰 메모:
  • 머지 결과 / 배포 환경:
  • wiki 추출 대상 (verified만, wiki/projects/로만 추출):
    • actually-implemented 항목:
      • inbound port = *UseCase naming (D1) — ArchUnit inbound_port_implementations_end_with_use_case 으로 강제.
      • @UseCaseCapability mandatory annotation (D3 / 판정 기준 Required fields) — ArchUnit inbound_port_implementations_declare_capability.
      • TransactionPort abstraction with inWrite / inRead / inNew 3 modes (D3) — Spring @Transactional 직접 import 금지 (application_does_not_use_spring_transactional_annotation).
      • READ_COMMITTED only isolation (TransactionPort Contract) — Isolation enum 단일 값.
      • NESTED / NEVER propagation forbidden — TransactionPort API 에서 노출 안 함.
      • Spring TransactionTemplate 기반 infrastructure (D5 의 cited alternative 채택) — SpringTransactionPort 모드별 pre-built template.
      • sample-portfolio 의 @Transactional 전체 제거 + TransactionPort 사용으로 contract conformance 입증.
    • locally-verified 항목:
      • ./gradlew check 통과 (25 tasks, 12 ArchUnit + 9 application + 4 adapter-persistence + 9 adapter-web + 6 bootstrap settings).
    • prod-verified 항목: 없음 — 운영 환경 배포 없음.
  • 추출하지 않을 항목 (planned / documented-only / abandoned):
    • TransactionalUseCaseRunner 대안 (Decision 2026-05-28 으로 TransactionPort 단일 abstraction 채택).
    • REPEATABLE_READ / SERIALIZABLE isolation (feature-transaction-concurrency-contract 위임).
    • outbox/audit REQUIRES_NEW 동작 통합 테스트 (feature-domain-event-outbox-contract 위임).
    • externalOutboundAllowed 의 dependency-aware ArchUnit rule (outbound port marker 정의 후).
    • Hibernate readOnly flush-mode statistics 측정 PoC (Testcontainers 환경 후).