46 KiB
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 |
|
|
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 |
|
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):
/ingestreconcile 시 commitffb0e13코드를 직접 읽어 D1~D14 구현 사실을 확인 —TransactionPort(inWrite/inRead/inNew+ Runnable defaults),SpringTransactionPort(모드별 pre-builtTransactionTemplate, 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/위임).
부모 (필수)
- Parent project (canonical SSOT): raw/project-notes/ca-skeleton-operational-contract
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또는TransactionalUseCaseRunnerabstraction을 기본값으로 둠. - 2026-05-22: write use case는
transactionMode,idempotency,repositoryAccess를 명시해야 함. query use case는readOnlytransaction mode를 기본값으로 둠. - 2026-05-28 (implementation):
TransactionPort선택.TransactionalUseCaseRunner는 채택 안 함 (단일 abstraction 면 충분, 두 추상이 공존하면 사용 지침이 모호해짐). - 2026-05-28 (implementation):
TransactionPortAPI 는inWrite/inRead/inNew3 메서드.NESTED와NEVER는 API 에서 노출 안 함 (브랜치 노트 금지 사항). - 2026-05-28 (implementation):
Isolationenum 은READ_COMMITTED만 노출.REPEATABLE_READ,SERIALIZABLE은feature-transaction-concurrency-contract브랜치로 위임. - 2026-05-28 (implementation):
SpringTransactionPort는 모드별TransactionTemplate인스턴스 3개를 미리 빌드해서 보관.setReadOnly/setPropagationBehavior의 호출당 mutation 으로 인한 동시성 위험 차단. - 2026-05-28 (implementation):
Idempotencyenum 값은IDEMPOTENT/KEYED/NOT_IDEMPOTENT세 종.KEYED는 idempotency key 기반 dedup 필요 표시 (브랜치 노트의feature-rate-limit-idempotency-contract가 후속 운영). - 2026-05-28 (implementation):
application-coreGradle 의spring-tx의존성 제거.@Transactional어노테이션이 모듈 컴파일 classpath 자체에 없도록 belt+suspenders. - 2026-05-28 (D11 checked-exception wrapping):
TransactionPort는Supplier<T>/Runnable시그니처 유지 (checked exception 시그니처에 노출 안 함). SpringTransactionTemplate.execute(TransactionCallback<T>) throws TransactionException도 동일 제약 — 이는 TransactionPort 설계 결함이 아닌 Spring 공식 idiom. Wrapping 정책: 도메인 checked →DomainException extends RuntimeException,IOException→UncheckedIOException,SQLException→ SpringDataAccessException계층이 자동 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-core는org.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)사용은 후속 branchfeature-rate-limit-idempotency-contractmerge 전까지 금지. 이유: 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-contractmerge 시 제거).
근거 (필수, 최소 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 참조.
- 채택 결정 (TransactionPort / TransactionalUseCaseRunner abstraction):
- raw/company-tech-blogs/transaction-port-clean-ddd-spring-medium — UNIL의 동일 진화 경로 (2024-05)
- raw/company-tech-blogs/transaction-port-vassilis-soum-github-readme — TransactionPort 참고 구현
- 검토한 대안:
- 대안 1: @Transactional direct — raw/official-docs/at-transactional-spring-official, raw/company-tech-blogs/hexagonal-reflectoring-transactional-placement (Hexagonal 표준 다수파)
- 대안 2: TransactionTemplate programmatic — raw/official-docs/transaction-template-spring-official
- 대안 3: Functional Resource monad — raw/official-docs/functional-tx-arrow-kt-resource-docs (Arrow Kt)
- 대안 4: Custom TransactionInterceptor (AOP) — raw/company-tech-blogs/custom-transaction-interceptor-catnipcoder
- 보완 (대체 X): raw/company-tech-blogs/woowahan-hexagonal-multimodule — multi-module 분리
- 비교 핵심: ca-tmpl 결정은 "Spring 의존 숨김" 소수파. 다수파는
@Transactional직접 부착(testability 낮지만 boilerplate 최저). Functional monad는 testability 최고지만 팀 학습 비용 큼.
판정 기준
| 구분 | 기준 |
|---|---|
| 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-core 는 org.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-portfolio 을 app-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) — SpringTransactionPort 가 TransactionTemplate 기반으로 구현됨. 모드별 미리 빌드된 인스턴스를 사용하여 동시성 안전. 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_transport 에 org.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 (inNewpool-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 (inNewpool-sizing + loop forbidden) + MapStruct@Generatedexemption ArchUnit predicate 예시 (D9 of architecture-enforcement-rules).src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java—domain_is_pure에lombok..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 — customArchCondition으로 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.gradle—testCompileOnly 'org.springframework:spring-tx'추가 (violation fixture 의@Transactionalimport 만을 위해).CLAUDE.md(root) —apivsimplementation정책 추가 (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.java—inWrite/inRead/inNew(+ Runnable defaults).src/application-core/src/main/java/dev/caskeleton/application/transaction/TransactionMode.java—WRITE/READ_ONLY/REQUIRES_NEW.src/application-core/src/main/java/dev/caskeleton/application/transaction/Isolation.java—READ_COMMITTEDonly.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.java—IDEMPOTENT/KEYED/NOT_IDEMPOTENT.src/application-core/src/main/java/dev/caskeleton/application/capability/RepositoryAccess.java—NONE/READ_REPOSITORY/WRITE_REPOSITORY.src/application-core/build.gradle— dropspring-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-backedTransactionPort(pre-builtTransactionTemplateper mode,READ_COMMITTEDpinned).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— documentTransactionPortimplementation + 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.gradle—testImplementation 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 +@Transactionalmethod-level withTransactionPort.inRead/inWritecalls.TransactionPortinjected 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@TransactionalfromdeleteByAuthorId(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 inapplication-core,SpringTransactionPort, 3 new ArchUnit rules, sample-portfolio migration toTransactionPort.locally-verified: full./gradlew checkgreen; ArchUnit rules verified against the migrated reference implementation.documented-only:*Portoutbound naming rule,externalOutboundAlloweddependency-aware rule, REPEATABLE_READ / SERIALIZABLE isolation — explicitly deferred with rationale.planned:readOnlydriver 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']만 검사하므로 영향 없음. ArchUnitproduction_code_does_not_depend_on_sample_portfoliorule 은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 부재 검증.
묶음
- raw/company-tech-blogs/axonframework-transactionmanager-spring-adapter
- raw/company-tech-blogs/buckpal-archunit-lombok-allowlist-direct-transactional
- raw/company-tech-blogs/custom-transaction-interceptor-catnipcoder
- raw/company-tech-blogs/hexagonal-reflectoring-transactional-placement
- raw/company-tech-blogs/spring-modulith-archunit-generated-exemption-and-violations-as-data
- raw/company-tech-blogs/transaction-port-clean-ddd-spring-medium
- raw/company-tech-blogs/transaction-port-vassilis-soum-github-readme
- raw/company-tech-blogs/woowahan-hexagonal-multimodule
- raw/official-docs/arch-hexagonal-cockburn
- raw/official-docs/at-transactional-spring-official
- raw/official-docs/functional-tx-arrow-kt-resource-docs
- raw/official-docs/spring-transaction-synchronization-manager-javadoc
- raw/official-docs/spring-tx-management-reference
- raw/official-docs/spring-tx-propagation-required-new-nested-official
- raw/official-docs/transaction-template-spring-official
- raw/interviews/archunit-static-analysis-limits
- raw/interviews/transaction-port-vs-spring-transactional
- raw/blog-topics/archunit-violations-as-data-pattern-2026-05-28
- raw/blog-topics/transaction-port-abstraction-over-spring-transactional-2026-05-28
본 feature branch 는 leaf — 자식 자료 없음. Phase C2 실 코드 작성 단계에서 errors / interview prep / lectures 가 누적되면 본 섹션에서 그룹화.
근거 자료
- raw/official-docs/spring-tx-propagation-required-new-nested-official — PROPAGATION_REQUIRES_NEW 의 independent physical transaction + connection pool exhaustion / deadlock 경고 + NESTED savepoint 동작 (Spring 공식 문서 verbatim)
- raw/company-tech-blogs/spring-modulith-archunit-generated-exemption-and-violations-as-data — D1/D3 counter-evidence:
@ApplicationModuleListener가@Transactional(propagation = Propagation.REQUIRES_NEW)를 meta-annotation 으로 재노출 (SPRING-MOD-TX-C1). ca-tmpl forbidden 정책 재검토 증거로 기록 (D3 override 아님) - raw/company-tech-blogs/axonframework-transactionmanager-spring-adapter — Axon Framework
TransactionManagerinterface (executeInTransaction(Runnable)+fetchInTransaction(Supplier<T>)) +SpringTransactionManager(PlatformTransactionManager)어댑터 — D3 (TransactionPort 채택) 보강 증거 (company-case-study, Spring 공식 아님) - raw/company-tech-blogs/buckpal-archunit-lombok-allowlist-direct-transactional — D1/D3 CONTRARY evidence: Buckpal application service 가
@Transactional직접 클래스 부착 + transaction abstraction 부재 (BUCKPAL-TX-C1, BUCKPAL-TX-C2). ca-tmpl D3 (TransactionPort) 가 OSS 소수파 결정임을 뒷받침
오류 기록 (본 feature 작업 중 발생)
- raw/errors/archunit-test-scope-sample-ticket-inclusion-2026-05-28 — ArchUnit scope 가 production classpath 만 보는 함정과
testImplementation우회.
면접 준비 (이 작업에서 나올 수 있는 면접 질문)
- raw/interviews/transaction-port-vs-spring-transactional —
@Transactional직접 부착 다수파 vsTransactionPort추상화 소수파의 trade-off. - raw/interviews/archunit-static-analysis-limits — D14 (KEYED idempotency freeze) 의 custom
ArchCondition작성 + ArchUnit static analysis 한계 + violations-as-data 보완 (round 2).
Blog topics (이 작업에서 나올 수 있는 글감)
- raw/blog-topics/transaction-port-abstraction-over-spring-transactional-2026-05-28 — application 계층이
@Transactional을 직접 import 하지 않도록 TransactionPort 를 도입한 실제 ca-tmpl 사례 + ArchUnit fitness function 으로 강제한 방법. - raw/blog-topics/archunit-violations-as-data-pattern-2026-05-28 — D14 의 custom ArchCondition 을 negative test fixture 로 보증한 round 2 작업 글감.
진행 중 메모
- 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 =
*UseCasenaming (D1) — ArchUnitinbound_port_implementations_end_with_use_case으로 강제. @UseCaseCapabilitymandatory annotation (D3 / 판정 기준 Required fields) — ArchUnitinbound_port_implementations_declare_capability.TransactionPortabstraction withinWrite/inRead/inNew3 modes (D3) — Spring@Transactional직접 import 금지 (application_does_not_use_spring_transactional_annotation).READ_COMMITTEDonly isolation (TransactionPort Contract) —Isolationenum 단일 값.NESTED/NEVERpropagation forbidden —TransactionPortAPI 에서 노출 안 함.- Spring
TransactionTemplate기반 infrastructure (D5 의 cited alternative 채택) —SpringTransactionPort모드별 pre-built template. - sample-portfolio 의
@Transactional전체 제거 +TransactionPort사용으로 contract conformance 입증.
- inbound port =
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/SERIALIZABLEisolation (feature-transaction-concurrency-contract위임).- outbox/audit
REQUIRES_NEW동작 통합 테스트 (feature-domain-event-outbox-contract위임). externalOutboundAllowed의 dependency-aware ArchUnit rule (outbound port marker 정의 후).- Hibernate
readOnlyflush-mode statistics 측정 PoC (Testcontainers 환경 후).