426 lines
46 KiB
Markdown
426 lines
46 KiB
Markdown
---
|
||
title: branch / feature-application-port-usecase-contract
|
||
source_type: branch-note
|
||
status: verified
|
||
branch: feature-application-port-usecase-contract
|
||
related_projects: [ca-skeleton]
|
||
tags: [branch, ca-skeleton, application, usecase, port, transaction-port]
|
||
created: 2026-05-22
|
||
target_merge:
|
||
status_label: actually-implemented
|
||
last_updated: 2026-05-28
|
||
last_reviewed: 2026-06-04
|
||
id: BR-CA-SKELETON-OPERATIONAL-CONTRACT-035
|
||
kind: project-work-item
|
||
project: ca-skeleton-operational-contract
|
||
work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-035
|
||
inherits: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-TRANSACTION-001@1, DEC-CA-SKELETON-OPERATIONAL-CONTRACT-MODULE-LAYOUT-001@1]
|
||
refines: []
|
||
overrides: []
|
||
depends_on: []
|
||
contract_packet: 1
|
||
parent_branch:
|
||
contract_packet_sha256: 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/위임).
|
||
|
||
<!-- section-id: branch-parent -->
|
||
## 부모 (필수)
|
||
|
||
- **Parent project (canonical SSOT)**: [[raw/project-notes/ca-skeleton-operational-contract]]
|
||
|
||
> ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT 역할. 본 feature branch 는 project-note 의 운영 계약 중 해당 영역 (§<관련 섹션>) 의 결정/근거/금지 사항을 정제한다.
|
||
|
||
<!-- GENERATED: branch-contract:start -->
|
||
<!-- section-id: branch-contract-packet -->
|
||
## 브랜치 계약 패킷
|
||
|
||
- **생성 시 프로젝트 개정**: `1`
|
||
- **패킷 스키마**: `contract_packet: 1`
|
||
- **완료 조건**: application port와 transaction runner architecture test가 통과한다
|
||
|
||
<!-- section-id: inherited-project-decisions -->
|
||
### 상속한 프로젝트 결정
|
||
|
||
| 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]] |
|
||
|
||
<!-- section-id: branch-local-decisions -->
|
||
### 브랜치 지역 결정
|
||
|
||
> 기존 branch-local 결정은 아래 `## Decision Evidence Map / 결정-근거 매핑`의 D-row가 소유하며 이 packet에서 복제하지 않는다.
|
||
|
||
| Decision ID | Decision | Relation | Supporting Claims | Status |
|
||
|---|---|---|---|---|
|
||
|
||
<!-- section-id: declared-overrides -->
|
||
### 선언한 예외
|
||
|
||
| Override ID | Overrides | Reason | Approval | Status |
|
||
|---|---|---|---|---|
|
||
<!-- GENERATED: branch-contract:end -->
|
||
|
||
<!-- section-id: branch-goal -->
|
||
## 목표
|
||
|
||
실제 도메인이 들어오면 application layer가 가장 먼저 비대해집니다. use case가 DTO, JPA entity, HTTP request, external client를 직접 다루지 않도록 port 계약과 command/query 모델을 고정합니다.
|
||
|
||
- 이슈:
|
||
- PR:
|
||
|
||
<!-- section-id: branch-scope -->
|
||
## 범위
|
||
|
||
### 포함 범위
|
||
|
||
- 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 메서드. `NESTED` 와 `NEVER` 는 API 에서 노출 안 함 (브랜치 노트 금지 사항).
|
||
- 2026-05-28 (implementation): `Isolation` enum 은 `READ_COMMITTED` 만 노출. `REPEATABLE_READ`, `SERIALIZABLE` 은 `feature-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): `TransactionPort` 는 `Supplier<T>` / `Runnable` 시그니처 유지 (checked exception 시그니처에 노출 안 함). Spring `TransactionTemplate.execute(TransactionCallback<T>) throws TransactionException` 도 동일 제약 — 이는 TransactionPort 설계 결함이 아닌 Spring 공식 idiom. Wrapping 정책: 도메인 checked → `DomainException extends RuntimeException`, `IOException` → `UncheckedIOException`, `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-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)` 사용은 후속 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` 참조.
|
||
|
||
- **채택 결정 (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 (`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.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 — 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.gradle` — `testCompileOnly '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.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_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.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` — 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.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 + `@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 부재 검증.
|
||
|
||
## 묶음
|
||
|
||
<!-- GENERATED: sources:start -->
|
||
- [[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]]
|
||
<!-- GENERATED: sources:end -->
|
||
|
||
<!-- GENERATED: interviews:start -->
|
||
- [[raw/interviews/archunit-static-analysis-limits]]
|
||
- [[raw/interviews/transaction-port-vs-spring-transactional]]
|
||
<!-- GENERATED: interviews:end -->
|
||
|
||
<!-- GENERATED: errors:start -->
|
||
- [[raw/errors/archunit-test-scope-sample-ticket-inclusion-2026-05-28]]
|
||
<!-- GENERATED: errors:end -->
|
||
|
||
<!-- GENERATED: blog-topics:start -->
|
||
- [[raw/blog-topics/archunit-violations-as-data-pattern-2026-05-28]]
|
||
- [[raw/blog-topics/transaction-port-abstraction-over-spring-transactional-2026-05-28]]
|
||
<!-- GENERATED: blog-topics:end -->
|
||
|
||
> 본 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 `TransactionManager` interface (`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` 직접 부착 다수파 vs `TransactionPort` 추상화 소수파의 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 = `*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 환경 후).
|