Files
llm-wiki/vault/30-knowledge/projects/ca-tmpl/transaction-boundary-abstraction.md
T

14 KiB

title, source_type, status, confidence, tags, related_projects, last_reviewed
title source_type status confidence tags related_projects last_reviewed
ca-tmpl - Transaction Boundary Abstraction 결정 (TransactionPort) project verified high
ca-tmpl
transaction
application-layer
actually-implemented
ca-tmpl
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 에 실재.
  • SpringTransactionPortsrc/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.javaWRITE / READ_ONLY / REQUIRES_NEW 3값.
  • transaction/Isolation.javaREAD_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.javaIDEMPOTENT / KEYED / NOT_IDEMPOTENT. capability/RepositoryAccess.javaNONE / READ_REPOSITORY / WRITE_REPOSITORY.
  • application-core/build.gradlespring-tx 의존을 의도적으로 선언하지 않음 (주석으로 사유 명시). spring-boot-starter 는 유지(DI 목적, D13).

adapter-persistence

  • transaction/SpringTransactionPort.javaTransactionPort 의 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_caseCommandUseCase/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.javadeleteByAuthorId@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_NEWNESTED 의 차이, 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에 연결했다.

관련 개념

Sources

Cluster / 묶음