14 KiB
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 |
|
|
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하지 않도록TransactionPortabstraction을 도입. - 이유: 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-builtTransactionTemplate3개).- ffb0e13 시점의 reference sample 모듈명은
sample-ticket(PostService/UserService). 이후 커밋(현재 HEADdb61075)에서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 메서드 +Runnabledefault 오버로드 3개. Javadoc 에 D11(Supplier/Runnable만 받아 checked exception 차단 → 호출 측RuntimeExceptionwrap) + 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_NEW3값.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 구현. 생성자에서 모드별TransactionTemplate3개(write / read / requiresNew)를 미리 빌드. 모두ISOLATION_READ_COMMITTEDpin. 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구현은UseCasesuffix 강제 (D1).inbound_port_implementations_declare_capability— 모든 use case 구현에@UseCaseCapability강제.inbound_port_implementations_do_not_declare_keyed_idempotency— customArchCondition으로Idempotency.KEYED선언 차단 (D14 freeze,feature-rate-limit-idempotency-contractmerge 시 제거 예정).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(TransactionPortTestSupplier/Runnable delegation,UseCaseCapabilityTest,UseCaseContractTest),adapter-persistence(SpringTransactionPortTest— 모드별 propagation / isolation / readOnly / rollback-on-exception 확인). - ArchUnit fitness function PASS:
CleanArchitectureTest(위 rule들) +ArchitectureViolationFixtureTest(각 rule 이 의도된 위반 fixture 를 실제로 잡아냄). ./gradlew checkgreen (브랜치 노트 기록: 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/SERIALIZABLEisolation:Isolationenum 에 노출하지 않음.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-contractmerge 전까지 ArchUnit rule 로 freeze (planned/ 의도적 차단).externalOutboundAllowed의 dependency-aware ArchUnit rule 및*Portoutbound 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/inNew3 메서드,Supplier<T>/Runnable시그니처,READ_COMMITTED단일 isolation, checked exception 을 노출하지 않는 이유(D11).SpringTransactionPort가 모드별TransactionTemplate을 미리 빌드한 이유 (per-call mutation 의 동시성 race 차단).- ArchUnit fitness function 으로
org.springframework.transaction.annotation.Transactionalimport 를 실제로 차단하고, violations-as-data 네거티브 fixture 로 rule 동작을 보증한 방법. - AOP self-invocation 문제가 무엇이고 표준 우회가 무엇인지,
TransactionPortabstraction 과 어떤 관계인지. 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_COMMITTEDvs MySQL InnoDBREPEATABLE_READ). - 단순 CRUD vs 도메인 복잡도가 큰 프로젝트에서
TransactionPort도입 trade-off 가 어떻게 다른가.
답하면 안 되는 질문 (모른다고 해야 함)
- "
readOnly = true가 실제 driver flush mode 를 바꾸는 것을 측정했는가?" → 측정 안 함. 단위 테스트로isReadOnly()flag 만 확인. - "
inNew의 outbox REQUIRES_NEW 동작을 실 DB 로 통합 검증했는가?" → 안 함.feature-domain-event-outbox-contract로 위임. - "운영에서 어떤 인시던트나 사례가 있었는가? 성능/지연을
@Transactional과 비교 측정했는가?" → 운영 배포 없음, 측정 없음. - "
KEYEDidempotency 를 실제로 적용했는가?" → freeze 상태. ArchUnit rule 로 선언 자체를 차단 중.
과장 금지 지점
- "운영에서 검증했다 / prod 에서 돌고 있다" → 금지. 로컬 단위 테스트 + 정적 분석까지가 검증 범위.
- "실 DB 통합 테스트로 트랜잭션 전파를 검증했다" → 금지.
SpringTransactionPortTest는 mockPlatformTransactionManager로 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으로 해결한다고 쓰지 않는다.
관련 개념
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