Files
llm-wiki/wiki/concepts/transaction-boundary-abstraction.md
T

15 KiB

title, source_type, status, confidence, tags, related_projects, last_reviewed
title source_type status confidence tags related_projects last_reviewed
Transaction Boundary Abstraction (TransactionPort vs @Transactional) llm-generated draft medium
transaction
clean-architecture
spring
ca-skeleton
2026-05-22

Transaction Boundary Abstraction (TransactionPort vs @Transactional)

Layer: wiki/concepts/ — 일반 개념. 특정 프로젝트의 적용 사실은 wiki/projects/로 분리.

Summary

Transaction boundary abstraction은 application layer가 Spring transaction API(@Transactional, PlatformTransactionManager)를 직접 의존하지 않고, TransactionPort 또는 TransactionalUseCaseRunner 같은 port abstraction을 통해 트랜잭션 경계를 선언하는 패턴이다. Clean Architecture / Hexagonal에서 "application은 framework를 모른다"는 원칙을 트랜잭션 경계까지 일관되게 적용하기 위한 선택지 중 하나이며, 다수파인 @Transactional 직접 부착의 대안으로 testability와 framework lock-in 완화를 노린다.

Standard (공식 정의)

Spring Framework는 트랜잭션 경계 선언을 위해 세 가지 표준 메커니즘을 제공한다.

  • PlatformTransactionManager: 모든 트랜잭션 추상화의 SPI. JDBC, JPA, JTA 구현체가 존재.
  • 선언적 트랜잭션 (@Transactional): AOP proxy 기반. method/class 단위 attribute로 propagation, isolation, timeout, rollbackFor, readOnly 등을 선언.
  • 프로그래매틱 트랜잭션 (TransactionTemplate, TransactionManager): 명시적 코드로 트랜잭션 범위를 둘러쌈.

Propagation 7종 (Spring Propagation enum)

의미
REQUIRED (default) 기존 트랜잭션 참여, 없으면 새로 생성
SUPPORTS 있으면 참여, 없으면 non-transactional
MANDATORY 반드시 존재해야 함, 없으면 예외
REQUIRES_NEW 항상 새 물리 트랜잭션 (기존은 suspend)
NOT_SUPPORTED non-transactional로 실행 (기존은 suspend)
NEVER 트랜잭션 존재 시 예외
NESTED savepoint 기반 nested 트랜잭션 (JDBC 한정, JPA는 일반적으로 미지원)

Isolation 5종 (Spring Isolation enum)

DEFAULT, READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE. PostgreSQL은 READ_COMMITTED가 default, MySQL InnoDB는 REPEATABLE_READ가 default라서 vendor default 묵시 사용은 의미 차이를 만든다.

출처: raw/official-docs/at-transactional-spring-official, raw/official-docs/transaction-template-spring-official.

한계 / 주의점

트랜잭션 경계를 어떻게 선언할지에 대한 5가지 대안과 그 한계.

대안 1: @Transactional direct (다수파)

  • 장점: boilerplate 최저, Spring/Hexagonal 표준 다수파, IDE 가시성 좋음.
  • 한계:
    • AOP self-invocation 문제: 같은 클래스 내부 메서드 호출은 proxy를 거치지 않아 @Transactional이 무시됨. self-injection이나 별도 bean 분리 같은 우회가 필요.
    • Testability 낮음: application use case 단위 테스트에서 트랜잭션 경계를 검증하려면 Spring context 또는 @DataJpaTest 등 통합 환경이 필요.
    • Framework lock-in: application package가 org.springframework.transaction.annotation.Transactional을 직접 import → Clean Architecture 의존성 규칙 위반 (application은 framework를 모른다).
    • 선언과 실행 분리: annotation은 attribute 선언일 뿐 실제 실행은 proxy/interceptor가 담당. 디버깅 시 호출 경로 추적이 간접적.
  • 출처: raw/official-docs/at-transactional-spring-official, raw/company-tech-blogs/hexagonal-reflectoring-transactional-placement.

대안 2: TransactionTemplate programmatic

  • 장점: 명시적 코드, self-invocation 문제 없음, propagation/isolation을 객체로 다룸.
  • 한계:
    • Boilerplate 증가 — 매 use case마다 template.execute(status -> { ... }) 작성.
    • 여전히 org.springframework.transaction.support.TransactionTemplate를 application이 직접 import → framework lock-in은 그대로.
  • 출처: raw/official-docs/transaction-template-spring-official.

대안 3: Functional Resource monad (예: Arrow Kt Resource, transaction { })

  • 장점: testability 최고 (순수 함수 합성으로 검증 가능), 명시적 effect, type-level 보장.
  • 한계:
    • 팀 학습 비용 큼 — Kotlin/함수형 코드 스타일에 익숙하지 않은 팀에선 채택 장벽이 높다.
    • Java 위주 Spring 팀에선 패턴 매칭 / monad 사용이 자연스럽지 않음.
    • Spring의 propagation/isolation 기본 의미를 monad 위에 재구현해야 하는 경우 있음.
  • 출처: raw/official-docs/functional-tx-arrow-kt-resource-docs.

대안 4: Custom TransactionInterceptor (AOP)

  • 장점: 자체 annotation 정의 가능, 커스텀 정책 주입(예: capability 검증과 결합) 가능.
  • 한계:
    • AOP 자체의 self-invocation 문제 동일하게 잔존.
    • interceptor 구현 자체가 Spring AOP 의존을 가짐.
    • 표준 @Transactional 도구(@TransactionalEventListener 등) 호환성 추가 검증 필요.
  • 출처: raw/company-tech-blogs/custom-transaction-interceptor-catnipcoder.

대안 5: TransactionPort / TransactionalUseCaseRunner abstraction (소수파)

  • 장점:
    • Application package가 Spring transaction import 없이 트랜잭션 경계를 선언.
    • Test에서는 in-memory fake port로 트랜잭션 경계 검증 가능 → use case 단위 테스트가 Spring context 없이 성립.
    • Framework 교체(예: Spring → Micronaut) 시 application 코드 변경 최소화.
  • 한계:
    • 소수파 — 일반적 hexagonal 사례에서도 @Transactional을 application service에 부착하는 경우가 다수.
    • Port interface 추가, infrastructure 구현체 추가, propagation/isolation을 port 시그니처로 어떻게 표현할지 결정 비용.
    • Spring 도구(@TransactionalEventListener, JPA OSIV, AOP 기반 audit 등)와의 호환을 직접 챙겨야 함.
    • 단순 CRUD 위주 프로젝트에서는 over-engineering이 될 수 있음.
  • 출처: raw/company-tech-blogs/transaction-port-clean-ddd-spring-medium, raw/company-tech-blogs/transaction-port-vassilis-soum-github-readme.

공통 주의점

  • 묵시적 vendor default isolation: Isolation.DEFAULT로 두면 PostgreSQL은 READ_COMMITTED, MySQL InnoDB는 REPEATABLE_READ로 달라진다. multi-vendor 환경에서는 명시 선언이 안전.
  • NESTED는 JDBC savepoint 기반: JPA EntityManager는 일반적으로 nested 트랜잭션을 지원하지 않음 (provider 의존).
  • REQUIRES_NEW는 비싸다: 기존 트랜잭션을 suspend하고 새 connection을 잡는 비용이 있음. outbox/audit 같은 명시적 케이스에만 사용.

Claim-backed Knowledge

이 표는 일반 개념 지식이 어떤 raw 근거로 뒷받침되는지 명시한다. 프로젝트 구현 주장은 여기에 넣지 않는다 (project 문서 참조).

Knowledge Point Supporting Claims Confidence Notes
Spring 은 트랜잭션 경계 선언에 declarative(@Transactional) / programmatic(TransactionTemplate) / SPI(PlatformTransactionManager) 메커니즘을 제공 raw/official-docs/at-transactional-spring-official, raw/official-docs/transaction-template-spring-official high official-vendor-doc (Spring 공식)
@Transactional 은 AOP proxy 기반이라 self-invocation 시 무시될 수 있음 raw/official-docs/at-transactional-spring-official#AT-TX-C5 high 표준 우회(self-injection 등) 존재 — 치명적 결함 아님
Propagation 기본값은 REQUIRED, readOnly 는 REQUIRED/REQUIRES_NEW 한정 적용 raw/official-docs/spring-tx-management-reference#SPRING-TX-MGR-C3, #SPRING-TX-MGR-C6 high official-vendor-doc
REQUIRES_NEW 는 독립 physical transaction + 새 connection → pool 소모, exhaustion/deadlock 위험 raw/official-docs/spring-tx-propagation-required-new-nested-official#SPRING-PROP-C1~C4 high official-vendor-doc
closure-based transaction abstraction 은 enterprise OSS 선례 존재(Axon executeInTransaction/fetchInTransaction) raw/company-tech-blogs/axonframework-transactionmanager-spring-adapter#AXON-TX-C1~C3 medium company-case-study — 공식 best practice 아님
다수파 hexagonal 사례는 오히려 application service 에 @Transactional 직접 부착(abstraction 없음) raw/company-tech-blogs/buckpal-archunit-lombok-allowlist-direct-transactional#BUCKPAL-TX-C1~C2, raw/company-tech-blogs/hexagonal-reflectoring-transactional-placement#HEX-REFL-C1 medium engineering-blog/company-case-study — TransactionPort 가 소수파임을 보여주는 contrary evidence
Spring 공식 incubator(Modulith)는 @ApplicationModuleListener@Transactional(REQUIRES_NEW) 를 meta-annotation 재노출 raw/company-tech-blogs/spring-modulith-archunit-generated-exemption-and-violations-as-data#SPRING-MOD-TX-C1 medium abstraction-only forbidden 정책과 반대 방향

Project Application

내가 설명할 수 있어야 하는 것

  • transaction boundary abstraction 의 공식 정의 — Spring 의 declarative / programmatic / SPI 메커니즘과의 관계.
  • 어떤 문제를 해결하는가 — application 패키지의 framework lock-in 차단 + use case 단위 테스트의 Spring context 분리(testability).
  • 어떤 상황에서는 쓰면 안 되는가 — 단순 CRUD 위주 + framework 교체 계획 없음 + Spring 숙련 팀이면 @Transactional 직접 부착이 합리적. abstraction 은 over-engineering 이 될 수 있다.
  • 공식 문서가 말하지 않는 부분 — Spring 공식은 @Transactional/TransactionTemplate 을 권장하지 abstraction port 를 권장하지 않는다. port 화는 자체 taste.
  • 회사 기술 블로그 사례를 일반 법칙처럼 말하면 안 되는 지점 — UNIL / Axon / Buckpal / Modulith 는 case-study/engineering-blog 등급. 특히 Buckpal·Modulith 는 오히려 @Transactional 직접/meta 부착이라 abstraction-only 가 다수파라고 말하면 안 된다.
  • 내 프로젝트에서는 어떤 branch decision 으로 연결됐는가 — raw/branch-notes/feature-application-port-usecase-contract D3(TransactionPort 채택) / D11(callback 시그니처) / D12(inNew pool 비용). 구현 사실은 wiki/projects/ca-tmpl/transaction-boundary-abstraction.
  • 코드/운영에서 검증하려면 — ArchUnit 으로 application 패키지의 @Transactional import 차단을 확인, readOnly flush-mode 는 Hibernate session statistics 로 측정, REQUIRES_NEW 는 connection pool 사용량을 통합 테스트로 확인.

Interview Questions

  • 왜 application layer에서 Spring @Transactional 직접 import를 금지할 수 있는가? 어떤 trade-off가 있는가?
  • AOP self-invocation 문제는 무엇이고, TransactionPort abstraction은 이 문제를 어떻게 회피하는가?
  • REQUIRES_NEWNESTED의 차이는 무엇이며, 왜 NESTED는 JPA에서 일반적으로 권장되지 않는가?
  • Isolation level 4단계(READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE)와 phantom read / non-repeatable read / dirty read의 관계를 설명할 수 있는가?
  • TransactionPort 도입의 trade-off를 단순 CRUD 프로젝트와 도메인 복잡도가 큰 프로젝트로 나눠 어떻게 다르게 평가하는가?

Do Not Overclaim

  • "TransactionPort가 무조건 우월하다"고 말하지 않는다. 단순 CRUD가 대부분이고 framework 교체 계획이 없으며 팀이 Spring에 익숙하다면, @Transactional 직접 부착이 boilerplate / 가시성 / 표준 도구 호환성 측면에서 합리적인 선택이다. Hexagonal/Clean Architecture 사례 다수도 application service에 @Transactional을 부착한다.
  • UNIL 팀 사례를 "ca-tmpl이 영감을 받았다"고 단정하지 않는다. raw/company-tech-blogs/transaction-port-clean-ddd-spring-medium(UNIL, 2024-05)와 ca-tmpl은 동일한 evolution path(@Transactional → AOP → TransactionPort)를 거친 별개 사례로 다루며, 인용은 "동일한 결론에 도달한 외부 사례" 수준에서만 한다.
  • "AOP 기반 transaction은 항상 self-invocation 문제 때문에 깨진다"고 말하지 않는다. self-injection, public method 분리, 별도 bean 분리 같은 표준 우회가 존재하며, 다수 프로덕션에서 잘 동작한다. self-invocation은 "주의해야 할 함정"이지 "치명적 결함"이 아니다.
  • "Functional monad가 testability에서 항상 우월하다"고 말하지 않는다. test 친화성은 높지만 팀 역량 / 언어 / 기존 코드베이스에 따라 실제 도입 비용이 매우 크다.
  • 본 문서의 5종 비교는 외부 source를 기반으로 정리한 trade-off 표이며, 모든 항목이 자체 측정 결과는 아니다. status draft / confidence medium로 둔다.

Sources

공식 문서

사례 / 블로그 (공식 best practice 아님)

프로젝트 canonical / branch-notes