Files
llm-wiki/wiki/projects/ca-tmpl/transaction-boundary-abstraction.md

143 lines
14 KiB
Markdown

---
title: ca-tmpl - Transaction Boundary Abstraction 결정 (TransactionPort)
source_type: project
status: verified
confidence: high
tags: [ca-tmpl, transaction, application-layer, actually-implemented]
related_projects: [ca-tmpl]
last_reviewed: 2026-07-02
---
# ca-tmpl - Transaction Boundary Abstraction 결정 (TransactionPort)
> Layer: `wiki/projects/` — 내 프로젝트 사실. 일반 개념은 [[wiki/concepts/transaction-boundary-abstraction]] 참조.
## 프로젝트 컨텍스트
- **프로젝트**: ca-tmpl — Clean Architecture 기반 백엔드 skeleton 템플릿.
- **목표**: application layer가 Spring transaction API(`@Transactional`, `PlatformTransactionManager`, `TransactionTemplate`)를 직접 import하지 않도록 `TransactionPort` abstraction을 도입.
- **이유**: Clean Architecture / Hexagonal 의존성 규칙("application은 framework를 모른다")을 트랜잭션 경계까지 일관되게 적용하기 위함. 부차적으로 use case 단위 테스트에서 Spring context 없이 트랜잭션 경계를 검증할 수 있도록 testability 확보.
- **진행 단계**: **Phase C2 (코드) 구현 + 로컬 검증 완료.** `feature-application-port-usecase-contract` 브랜치에서 contract type, Spring 구현체, ArchUnit fitness function, 단위 테스트, sample 모듈 마이그레이션까지 작성되어 코드 베이스에 존재한다. 운영 배포 / 통합(DB) 테스트 / 측정값은 아직 없다.
## Ground-truth 대조 (2026-06-04, ca-tmpl @ffb0e13 "트랜잭션 포트와 웹 설정")
`/home/donghyeon/workspace/ca-tmpl` 의 commit `ffb0e13` (본 브랜치 구현 커밋) 코드를 직접 읽어 검증한 사실:
- 패키지 root 는 `dev.caskeleton.*` (브랜치 노트의 이전 stale 값 `com.example.blog` 아님). 본 문서의 이전 "구현 없음" 서술이 stale 이었음 — 실제로는 구현 완료 상태.
- contract type 들은 `src/application-core/.../application/transaction|usecase|command|query|capability` 에 실재.
- `SpringTransactionPort``src/adapter-persistence/.../transaction/SpringTransactionPort.java` 에 실재 (`@Component`, `PlatformTransactionManager` 주입, 모드별 pre-built `TransactionTemplate` 3개).
- ffb0e13 시점의 reference sample 모듈명은 **`sample-ticket`** (`PostService` / `UserService`). 이후 커밋(현재 HEAD `db61075`)에서 **`sample-portfolio`** (`WorkLog*` use case) 로 rename 됨. 본 문서는 ffb0e13 기준 사실을 기록하되, 모듈 rename 은 후속 브랜치 사실로 본다.
- `./gradlew :application-core:test :adapter-persistence:test :app-bootstrap:test --tests '*CleanArchitectureTest' --tests '*ArchitectureViolationFixtureTest'` → 현재 checkout 기준 PASS (exit 0, 2026-06-04 재실행).
## 실제 구현 내용 (`actually-implemented`)
ca-tmpl @ffb0e13 코드에서 직접 확인한 산출물:
**application-core (contract types, `dev.caskeleton.application.*`)**
- `transaction/TransactionPort.java` — outbound port. `<T> T inWrite(Supplier<T>)` / `inRead(Supplier<T>)` / `inNew(Supplier<T>)` 3 메서드 + `Runnable` default 오버로드 3개. Javadoc 에 D11(`Supplier`/`Runnable` 만 받아 checked exception 차단 → 호출 측 `RuntimeException` wrap) + D12(`inNew` = REQUIRES_NEW = 새 physical JDBC connection, pool-sizing 공식 `hikari.maximumPoolSize >= (concurrent_threads * (1 + max_inNew_depth)) + 1`, loop 내 호출 forbidden) 명시.
- `transaction/TransactionMode.java``WRITE` / `READ_ONLY` / `REQUIRES_NEW` 3값.
- `transaction/Isolation.java``READ_COMMITTED` **단일 값만 노출** (REPEATABLE_READ / SERIALIZABLE 은 `feature-transaction-concurrency-contract` 로 위임, READ_UNCOMMITTED 는 forbidden).
- `usecase/UseCase.java` / `CommandUseCase.java` / `QueryUseCase.java` — inbound port base + command/query 분리.
- `command/Command.java` / `query/Query.java` — write/read intent marker.
- `capability/UseCaseCapability.java` — runtime-retained annotation (필수 필드). `capability/Idempotency.java``IDEMPOTENT` / `KEYED` / `NOT_IDEMPOTENT`. `capability/RepositoryAccess.java``NONE` / `READ_REPOSITORY` / `WRITE_REPOSITORY`.
- `application-core/build.gradle``spring-tx` 의존을 의도적으로 선언하지 않음 (주석으로 사유 명시). `spring-boot-starter` 는 유지(DI 목적, D13).
**adapter-persistence**
- `transaction/SpringTransactionPort.java``TransactionPort` 의 Spring 구현. 생성자에서 모드별 `TransactionTemplate` 3개(write / read / requiresNew)를 미리 빌드. 모두 `ISOLATION_READ_COMMITTED` pin. write=REQUIRED+readOnly false, read=REQUIRED+readOnly true, requiresNew=REQUIRES_NEW+readOnly false. 호출당 mutation 으로 인한 동시성 race 차단.
**app-bootstrap (ArchUnit fitness functions)**`architecture/CleanArchitectureTest.java` 에 다음 rule 실재:
- `application_does_not_use_spring_transactional_annotation` — application 패키지에서 `org.springframework.transaction.annotation.Transactional` 의존 금지 (D3).
- `inbound_port_implementations_end_with_use_case``CommandUseCase`/`QueryUseCase` 구현은 `UseCase` suffix 강제 (D1).
- `inbound_port_implementations_declare_capability` — 모든 use case 구현에 `@UseCaseCapability` 강제.
- `inbound_port_implementations_do_not_declare_keyed_idempotency` — custom `ArchCondition` 으로 `Idempotency.KEYED` 선언 차단 (D14 freeze, `feature-rate-limit-idempotency-contract` merge 시 제거 예정).
- `application_does_not_depend_on_application_context` (D11), `application_does_not_depend_on_adapters_or_transport` (+`org.springframework.web..` 추가), `domain_is_pure`.
- `ArchitectureViolationFixtureTest` + `architecture/violations/` 의 의도된 위반 fixture 클래스들 — violations-as-data 네거티브 테스트.
**sample 모듈 마이그레이션 (ffb0e13: `sample-ticket`)**
- `sample-ticket/.../application/PostService.java`, `UserService.java` — 기존 `@Transactional` 을 전부 제거하고 `tx.inWrite(...)` / `tx.inRead(...)` 호출로 교체. `TransactionPort` 를 생성자 주입.
- `sample-ticket/.../adapter/persistence/repository/PostRepositoryAdapter.java``deleteByAuthorId``@Transactional` 제거 (트랜잭션은 호출 측 use case 가 소유).
## 로컬/dev 검증 (`locally-verified`)
- 단위 테스트 PASS: `application-core` (`TransactionPortTest` Supplier/Runnable delegation, `UseCaseCapabilityTest`, `UseCaseContractTest`), `adapter-persistence` (`SpringTransactionPortTest` — 모드별 propagation / isolation / readOnly / rollback-on-exception 확인).
- ArchUnit fitness function PASS: `CleanArchitectureTest` (위 rule들) + `ArchitectureViolationFixtureTest` (각 rule 이 의도된 위반 fixture 를 실제로 잡아냄).
- `./gradlew check` green (브랜치 노트 기록: 25 actionable tasks). 2026-06-04 재실행 시 위 핵심 test task 들 exit 0 확인.
- 검증 범위는 JVM 단위 테스트 + 정적 분석까지. **실 DB 통합 테스트는 아직 없음** (아래 planned 참조).
## 운영 검증 (`prod-verified`)
**없음.** 운영 환경에 배포된 적이 없다. 측정값 / 인시던트 / 릴리즈 노트 어느 것도 없다.
## 문서/계획만 존재 (`documented-only` / `planned`)
다음 항목은 설계/문서/위임 상태이며 **면접에서 "구현했다 / 검증했다"고 말하면 안 된다**.
- **`TransactionalUseCaseRunner` 대안**: 검토 후 미채택. 단일 abstraction(`TransactionPort`)만 채택했으므로 코드에 존재하지 않는다 (`documented-only`).
- **`REPEATABLE_READ` / `SERIALIZABLE` isolation**: `Isolation` enum 에 노출하지 않음. `feature-transaction-concurrency-contract` 로 위임 (`documented-only`).
- **`inNew` (REQUIRES_NEW) 의 outbox/audit 실제 동작 통합 테스트**: `feature-domain-event-outbox-contract` 로 위임. `max_inNew_depth` 실측은 도메인 use case별 통합 테스트 필요 (`planned`).
- **`@UseCaseCapability(idempotency = KEYED)` 활성화**: `feature-rate-limit-idempotency-contract` merge 전까지 ArchUnit rule 로 freeze (`planned` / 의도적 차단).
- **`externalOutboundAllowed` 의 dependency-aware ArchUnit rule** 및 **`*Port` outbound naming rule**: outbound port marker 정의 후 추가 예정 (`documented-only`).
- **`readOnly = true` 의 driver flush-mode 변경 통합 검증**: Testcontainers 환경에서 Hibernate session statistics 측정 PoC 필요. 현재는 단위 테스트로 `TransactionTemplate.isReadOnly() == true` 만 확인 (`planned`).
## 면접에서 말할 수 있는 범위
### 자신 있게 답할 수 있는 질문
- 왜 application layer에서 Spring `@Transactional` 직접 부착을 금지했는가, 어떤 trade-off가 있는가. (실제 `TransactionPort` 로 구현 + ArchUnit 으로 강제까지 함.)
- `TransactionPort` 를 어떻게 설계했는가 — `inWrite`/`inRead`/`inNew` 3 메서드, `Supplier<T>`/`Runnable` 시그니처, `READ_COMMITTED` 단일 isolation, checked exception 을 노출하지 않는 이유(D11).
- `SpringTransactionPort` 가 모드별 `TransactionTemplate` 을 미리 빌드한 이유 (per-call mutation 의 동시성 race 차단).
- ArchUnit fitness function 으로 `org.springframework.transaction.annotation.Transactional` import 를 실제로 차단하고, violations-as-data 네거티브 fixture 로 rule 동작을 보증한 방법.
- AOP self-invocation 문제가 무엇이고 표준 우회가 무엇인지, `TransactionPort` abstraction 과 어떤 관계인지.
- `REQUIRES_NEW`(`inNew`)가 새 physical connection 을 잡아 pool 을 소모하는 비용 + loop 내 호출 anti-pattern.
### 적당히 답할 수 있는 질문
- `REQUIRES_NEW``NESTED` 의 차이, JPA 에서 `NESTED` 가 일반적으로 권장되지 않는 이유 (savepoint / JDBC 한정 / provider 의존). (단 ca-tmpl 은 `NESTED` 를 API 에 노출하지 않음 — 일반 개념 수준 답변.)
- Isolation level 4단계와 dirty/non-repeatable/phantom read 의 관계, vendor default 차이 (PostgreSQL `READ_COMMITTED` vs MySQL InnoDB `REPEATABLE_READ`).
- 단순 CRUD vs 도메인 복잡도가 큰 프로젝트에서 `TransactionPort` 도입 trade-off 가 어떻게 다른가.
### 답하면 안 되는 질문 (모른다고 해야 함)
- "`readOnly = true` 가 실제 driver flush mode 를 바꾸는 것을 측정했는가?" → **측정 안 함. 단위 테스트로 `isReadOnly()` flag 만 확인.**
- "`inNew` 의 outbox REQUIRES_NEW 동작을 실 DB 로 통합 검증했는가?" → **안 함. `feature-domain-event-outbox-contract` 로 위임.**
- "운영에서 어떤 인시던트나 사례가 있었는가? 성능/지연을 `@Transactional` 과 비교 측정했는가?" → **운영 배포 없음, 측정 없음.**
- "`KEYED` idempotency 를 실제로 적용했는가?" → **freeze 상태. ArchUnit rule 로 선언 자체를 차단 중.**
## 과장 금지 지점
- **"운영에서 검증했다 / prod 에서 돌고 있다" → 금지.** 로컬 단위 테스트 + 정적 분석까지가 검증 범위.
- **"실 DB 통합 테스트로 트랜잭션 전파를 검증했다" → 금지.** `SpringTransactionPortTest` 는 mock `PlatformTransactionManager` 로 template 설정값만 확인한다. 실 connection 동작은 미검증.
- **"UNIL 팀과 동일한 경로를 거쳤다" → 금지.** [[raw/company-tech-blogs/transaction-port-clean-ddd-spring-medium]](UNIL, 2024-05)는 동일 결론에 도달한 **별개 외부 사례**다.
- **"AOP `@Transactional` 은 self-invocation 때문에 깨진다" → 단정 금지.** 표준 우회로 다수 production 에서 잘 동작한다. 함정이지 치명적 결함이 아니다.
- **"`TransactionPort` 가 무조건 우월하다" → 금지.** 단순 CRUD + framework 교체 계획 없음 + Spring 숙련 팀이면 `@Transactional` 직접 부착이 합리적이다. Buckpal(hex-arch 공식 reference), Spring Modulith 등 OSS 다수파/공식 incubator 는 오히려 `@Transactional` 직접/meta-annotation 부착을 한다 — ca-tmpl 의 forbidden 정책은 소수파 자체 taste 임을 함께 인정.
### Blog-topic ingest: transaction boundary 묶음 (2026-07-02)
아래 raw seed들은 transaction boundary canonical에 연결했다.
- [[raw/blog-topics/transaction-port-abstraction-over-spring-transactional-2026-05-28]]: application 계층이 Spring `@Transactional`을 직접 import하지 않도록 `TransactionPort`와 ArchUnit fitness function을 결합한 이유를 다룬다. **주의**: `TransactionPort`가 다수파보다 우월하다고 쓰지 않고 ca-tmpl template repository 맥락의 선택으로 제한한다.
- [[raw/blog-topics/transaction-isolation-vendor-default-pin-2026-07-02]]: DB vendor default isolation 차이를 skeleton contract에서 명시 pin/test 대상으로 다루는 이유를 다룬다. **주의**: 모든 concurrency anomaly를 isolation pin으로 해결한다고 쓰지 않는다.
## 관련 개념
- [[wiki/concepts/transaction-boundary-abstraction]]
## Sources
- [[raw/project-notes/ca-skeleton-operational-contract]] — §14 Transaction/Concurrency, §19 Domain Application Readiness, §29 Topic 2 (TransactionPort 결정 사유)
- [[raw/branch-notes/feature-application-port-usecase-contract]] — TransactionPort interface spec, forbidden import 규칙, Decision Evidence Map (D1~D14), 구현 결과 (round 1 + round 2)
- [[raw/blog-topics/transaction-port-abstraction-over-spring-transactional-2026-05-28]] — TransactionPort abstraction 블로그 글감 raw seed
- [[raw/branch-notes/feature-transaction-concurrency-contract]] — isolation default, propagation default, idempotency / lock 분류
- [[raw/blog-topics/transaction-isolation-vendor-default-pin-2026-07-02]] — transaction isolation vendor default pin 블로그 글감 raw seed
- ca-tmpl @ffb0e13 코드 (ground-truth): `src/application-core/.../application/transaction|usecase|command|query|capability/*.java`, `src/adapter-persistence/.../transaction/SpringTransactionPort.java`, `src/app-bootstrap/.../architecture/CleanArchitectureTest.java`
## Cluster / 묶음
<!-- GENERATED: derived-blogs:start -->
- [[wiki/blog/ca-tmpl-transaction-boundary-abstraction-2026-07-02]]
<!-- GENERATED: derived-blogs:end -->