14 KiB
14 KiB
title, source_type, status, related_branches, related_projects, tags, created, status_label, target_audience, inspiration_url, archive_url
| title | source_type | status | related_branches | related_projects | tags | created | status_label | target_audience | inspiration_url | archive_url | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| blog-topic / transaction-port-abstraction-over-spring-transactional-2026-05-28 | blog-topic | raw |
|
|
|
2026-05-28 | ready-for-canonical | backend-engineer |
blog-topic: transaction-port-abstraction-over-spring-transactional-2026-05-28
Layer:
raw/blog-topics/— 채용공고가 아닌 작업·학습·트러블슈팅에서 나온 블로그 글감 원석. canonical 정제 전 raw 후보이며,wiki/blog/직접 생성 근거가 아니다.
Parent / 부모
- raw/branch-notes/feature-application-port-usecase-contract —
TransactionPort도입 + ArchUnit fitness function +sample-ticket마이그레이션의 실 구현 (D3, Decisions 2026-05-28). - raw/project-notes/ca-skeleton-operational-contract — ca-tmpl skeleton 의 운영 계약 맥락.
트리거 / Trigger
- 트리거 유형:
branch-work - 트리거 날짜: 2026-05-28
- 트리거 연결 노트: raw/branch-notes/feature-application-port-usecase-contract — branch-note 의 D3 ("transaction boundary 는 application use case 책임이지만 Spring
@Transactional직접 import 는 금지하고TransactionPort또는TransactionalUseCaseRunnerabstraction 을 기본값으로") 를 실제 코드로 옮긴 작업이 글감의 핵심.
글감 / Topic seed
- 한 문장 요지: application 계층이
@Transactional을 직접 import 하지 않도록TransactionPort같은 추상화를 두는 선택은 Hexagonal 의 소수파 패턴이지만, ArchUnit fitness function 과 결합하면 framework leakage drift 를 실제로 막을 수 있다 — 추상은 그 자체가 아니라 enforce 되는 추상이 의미를 갖는다. - 떠오른 계기:
feature-application-port-usecase-contract작업에서TransactionPort를 정의하고 ArchUnit 의application_does_not_use_spring_transactional_annotation으로@Transactional직접 import 를 차단, 동시에sample-ticket의 기존@Transactional사용을 모두tx.inWrite/tx.inRead로 마이그레이션한 경험. - 예상 제목 후보:
- application 계층에서
@Transactional을 떼어내기 — TransactionPort 와 ArchUnit fitness function - Hexagonal 다수파 vs 소수파 — 트랜잭션 경계 추상화의 비용과 이득
@Transactional직접 부착 vs TransactionPort — ca-skeleton 사례
- application 계층에서
핵심 주장 후보 / Claim candidates
각 항목은 branch-note Decision ID 또는 외부 source claim ID 로 근거를 인용한다.
- 사실 후보:
- ca-tmpl 의
application-core모듈은 Gradle 의존성에서spring-tx를 제거 해서@Transactional어노테이션이 컴파일 classpath 에서 reach 불가능 — 근거:feature-application-port-usecase-contract.mdDecisions 2026-05-28 ("application-coreGradle 의spring-tx의존성 제거.@Transactional어노테이션이 모듈 컴파일 classpath 자체에 없도록 belt+suspenders") + 구현 결과 §application-core build.gradle. TransactionPort는inWrite/inRead/inNew3개 메서드만 노출하고NESTED/NEVERpropagation 은 의도적 미노출 — 근거: 동일 branch-note Decisions 2026-05-28 ("TransactionPortAPI 는inWrite/inRead/inNew3 메서드.NESTED와NEVER는 API 에서 노출 안 함") + §TransactionPort Contract 표.SpringTransactionPort는 mode 별로 미리 빌드된TransactionTemplate인스턴스 3개를 보관해서 호출 시점의 mutation 없이 동시성 안전 — 근거: 동일 Decisions ("모드별TransactionTemplate인스턴스 3개를 미리 빌드해서 보관.setReadOnly/setPropagationBehavior의 호출당 mutation 으로 인한 동시성 위험 차단").- ArchUnit
noClasses().that().resideInAPackage("..application..").should().dependOnClassesThat().haveFullyQualifiedName("org.springframework.transaction.annotation.Transactional")으로 application 의@Transactionalimport 를 자동 차단 — 근거: 동일 branch-note Claims to Verify 표의 "application package의 ArchUnit rule이org.springframework.transaction.annotation.Transactionalimport를 실제로 catch" 행 →actually-implemented(2026-05-28). Isolationenum 은READ_COMMITTED만 노출하고REPEATABLE_READ/SERIALIZABLE는feature-transaction-concurrency-contract로 위임 — 근거: 동일 Decisions ("Isolationenum 은READ_COMMITTED만 노출").- Spring
@TransactionalAOP proxy 의 self-invocation 함정: 같은 클래스 내this.otherMethod()호출 시 proxy 우회 — 근거:raw/official-docs/at-transactional-spring-official.md#AT-TX-C5. readOnly = true는 REQUIRED / REQUIRES_NEW propagation 에 한정 적용 — 근거:raw/official-docs/spring-tx-management-reference.md#SPRING-TX-MGR-C6. ca-tmpl 의TransactionPort.inRead도 이 제약을 따라 REQUIRED + readOnly 로 구현.
- ca-tmpl 의
- 경험 후보:
- 기존
sample-ticket의 aggregate Service (UserService/PostService) 의@Transactional(readOnly=true)class-level +@Transactionalmethod-level 패턴을tx.inRead(() -> ...)/tx.inWrite(() -> ...)로 일괄 마이그레이션. 동작 동등하지만 application 의 Spring 의존성 surface 가 감소 — 근거:feature-application-port-usecase-contract.md구현 결과 §sample-ticket. - ArchUnit rule 을 추가했지만
app-bootstrap의 test classpath 가sample-ticket을 포함하지 않아서 vacuously 통과한 사례.testImplementation project(':sample-ticket')으로 test-only 의존을 추가해 해결 — 근거: 파생 에러 raw/errors/archunit-test-scope-sample-ticket-inclusion-2026-05-28. Idempotencyenum 값을IDEMPOTENT/KEYED/NOT_IDEMPOTENT세 종으로 결정.KEYED는 idempotency key 기반 dedup 필요 표시이고 후속feature-rate-limit-idempotency-contract가 이어받음 — 근거:feature-application-port-usecase-contract.mdDecisions 2026-05-28.
- 기존
- 의견 / 해석 후보:
TransactionPort추상의 진짜 이득 은 testability 가 아니다 (@Transactional메서드도@SpringBootTest로 잘 테스트됨). Spring 의존을 단일 진입점 (SpringTransactionPort) 으로 좁힌다 — Spring 업그레이드 / multi-tenant / multi-DB 시 transaction 정책 변경 지점이 한 클래스로 집중됨.- boilerplate 증가는 사실 (메서드마다
tx.inWrite(() -> { ... })한 단). 단일 DB / 단일 transactionManager 환경의 작은 팀은 다수파 (@Transactional직접 부착, hexagonal-reflectoring) 가 reasonable. - 추상은 enforce 되는 추상이 의미를 갖는다. ArchUnit fitness function 없이
TransactionPort만 두면 컨벤션 에 그치지만, fitness function 이@Transactionalimport 를 실패 시키면 추상이 drift 방지 메커니즘 으로 작동. testImplementation project(':sample-ticket')으로 sample 을 test classpath 에만 두는 비대칭 의존은 production drift 차단 (production_code_does_not_depend_on_sample_ticket) 과 ArchUnit scope 확장을 양립 시키는 흥미로운 패턴 — 일반화 가능.
Outline seed
각 섹션 옆에
→ 핵심 메시지를 함께 명시한다.
- 동기 — hexagonal / Clean Architecture 를 "했다" 면서
@Transactional은 application 에 그대로 두는 일관성 누락 → 추상의 enforceable 형태가 없으면 컨벤션이 무너진다. - 다수파 입장의 합리성 —
@Transactional직접 부착 (raw/company-tech-blogs/hexagonal-reflectoring-transactional-placement) → 소수파 결정의 대가 를 인정하고 시작. - ca-tmpl 의 선택 —
TransactionPort(UNIL / Vassilis Soum 류) + ArchUnit fitness function 결합 → 두 요소가 함께 있을 때만 의미. - 구현 스케치 —
TransactionPort의 3 메서드 한정 API →NESTED/NEVER미노출이 컨벤션이 아니라 API 표현. - infrastructure 구현 —
SpringTransactionPort의 mode 별 pre-builtTransactionTemplate→setReadOnly/setPropagationBehavior의 호출당 mutation 회피. - belt+suspenders —
application-coreGradle 에서spring-tx제거 → 컴파일 classpath 에서 reach 불가능 + ArchUnit 양쪽으로 막음. - ArchUnit scope 의 함정 —
app-bootstraptest classpath 가sample-ticket을 안 보던 문제 →testImplementation project(':sample-ticket')의 비대칭 의존. - 마이그레이션 결과 —
sample-ticket의@Transactional0개.tx.inWrite/tx.inRead로 일괄 전환 → 동작 동등 + Spring 의존 surface 감소. - 한계 / 미해결 —
REQUIRES_NEW실 outbox 통합 검증 미수행,noRollbackFor/timeout미지원,externalOutboundAlloweddependency-aware rule 미작성 → 추상이 모든 Spring 표현력을 capture 하는 게 아니라는 정직함. - 정리 — 추상은 그 자체로 가치 있는 게 아니라, enforce 되는 추상이 의미를 갖는다 → fitness function 결합이 본 글의 진짜 thesis.
Canonical 전환 후보 / Canonical extraction candidates
wiki/projects/ca-tmpl/transaction-port-contract.md후보:TransactionPort의 3 메서드 +Isolation.READ_COMMITTED만 노출 +NESTED/NEVER차단.SpringTransactionPort의 mode 별 pre-built template 패턴.application-core의spring-tx제거 + ArchUnit fitness function 의 belt+suspenders.- sample-ticket 마이그레이션 결과 (Before / After 코드).
wiki/concepts/transaction-boundary-abstraction.md후보:- 다수파 (
@Transactional직접 부착) vs 소수파 (TransactionPort) 의 trade-off 표. TransactionTemplatevs@TransactionalAOP proxy 의 self-invocation 차이.- "추상은 enforce 되는 추상이 의미를 갖는다" 의 일반 원칙.
- 다수파 (
- 필요한 추가 검증:
REQUIRES_NEW의 실 outbox 동작 통합 테스트 (Testcontainers,feature-domain-event-outbox-contract로 위임).readOnly = true의 Hibernate flush-mode 측정 PoC (feature-application-port-usecase-contract.mdClaims to Verify 의planned).*Portoutbound naming rule +externalOutboundAlloweddependency-aware rule (outbound port marker 정의 필요).
Sources / 근거 후보
- raw/branch-notes/feature-application-port-usecase-contract — 결정 D1~D10 + 2026-05-28 구현 결과 + Claims to Verify status.
- raw/branch-notes/feature-architecture-enforcement-rules — 자매 결정 (D8: application
@Transactional직접 import 금지) 의 자동 검증 자매. - raw/errors/archunit-test-scope-sample-ticket-inclusion-2026-05-28 — ArchUnit scope 의 test-classpath 함정.
- raw/interviews/transaction-port-vs-spring-transactional — 같은 주제의 면접 질문 노트.
- raw/official-docs/at-transactional-spring-official —
@Transactional공식 + self-invocation 함정 (AT-TX-C1,AT-TX-C5). - raw/official-docs/spring-tx-management-reference — Spring transaction abstraction (
SPRING-TX-MGR-C3,SPRING-TX-MGR-C6). - raw/official-docs/transaction-template-spring-official —
TransactionTemplateprogrammatic 공식 (TX-TMPL-C1~TX-TMPL-C4). - raw/company-tech-blogs/transaction-port-clean-ddd-spring-medium — UNIL TransactionPort 진화 사례 (
UNIL-TX-C1,UNIL-TX-C2). - raw/company-tech-blogs/transaction-port-vassilis-soum-github-readme — Vassilis Soum 의 TransactionPort 참고 구현 (
VSOUM-TX-C1~VSOUM-TX-C4). - raw/company-tech-blogs/hexagonal-reflectoring-transactional-placement —
@Transactional직접 부착 다수파 (HEX-REFL-C1,HEX-REFL-C5). - raw/blog-topics/clean-architecture-boundary-enforcement-2026-05-28 — ArchUnit fitness function 의 자매 글감.
미해결 / Unknown
- 아직 확인해야 할 사실:
TransactionPort가noRollbackFor/timeout/transactionManager(multi-DB) 등 Spring 표현력 전체를 capture 가능한지 — 현재는 하지 않음 (의도적 한정). - 아직 확인해야 할 사실:
REQUIRES_NEW가 실제 outbox / audit row 시나리오에서 독립 commit 되는지 통합 테스트 미수행. - 아직 확인해야 할 사실:
TransactionTemplate기반 구현이 self-invocation 함정에서 완전히 자유로운지 (port 메서드가 다른 port 메서드를 호출하는 경우 PoC 필요). - 과장하면 안 되는 부분: 본 글의 모든 검증은
locally-verified이고 prod 운영 없음. - 과장하면 안 되는 부분:
TransactionPort가 다수파 (@Transactional직접) 보다 우월 하다는 식 금지. 이 맥락 (template repository, 격리 우선) 의 선택까지만. - 블로그로 쓰기 전에 필요한 canonical 정제:
wiki/projects/ca-tmpl/transaction-port-contract.md정제 + outbox 통합 검증 1 사례 추가 후expanded.
Decision / 처리 결정
- 액션:
promote-to-canonical - 이유:
wiki/projects/ca-tmpl/transaction-boundary-abstraction.md에 TransactionPort abstraction 글감으로 반영했다. - 다음 단계: source canonical이
verified상태이므로 이후blogify대상으로 삼을 수 있다. 단 REQUIRES_NEW/outbox 실 DB 통합 검증 부재와 소수파 선택이라는 경계를 유지한다.
Related / 관련
- 관련 branch: raw/branch-notes/feature-application-port-usecase-contract (본 결정의 SSOT), raw/branch-notes/feature-architecture-enforcement-rules (자매 —
@Transactional금지 rule), raw/branch-notes/feature-domain-event-outbox-contract (후속 —REQUIRES_NEW의 실 사용처). - 관련 errors: raw/errors/archunit-test-scope-sample-ticket-inclusion-2026-05-28.
- 관련 interview prep: raw/interviews/transaction-port-vs-spring-transactional (같은 주제의 자매), raw/interviews/clean-architecture-boundary-enforcement (ArchUnit 자매).
- 관련 blog topics: raw/blog-topics/clean-architecture-boundary-enforcement-2026-05-28 (자매 글감 — boundary 강제), raw/blog-topics/clean-architecture-module-blueprint-2026-05-28 (자매 글감 — module 분리), raw/blog-topics/post-implementation-knowledge-capture-workflow-2026-05-28 (캡처 워크플로우 자매).
- derived blog: 생성 전. 생성 시
wiki/blog/transaction-port-abstraction-over-spring-transactional-YYYY-MM-DD.md후보.