Domain 영속화 표준은 트랜잭션 경계를 UseCase 구현 메서드에 두기로 하면서 이렇게 적었다.

클래스가 package-private 이어도 UseCase 인터페이스 구현 메서드는 public 이라
@Transactional 프록시가 동작하고, UseCase 에 없는 package-private 메서드에는 걸리지 않는다.

뒤 절반이 「경계를 가시성으로 긋는다」는 뜻이다. 그게 성립하는지 쟀다.


주장 — package-private 메서드에는 @Transactional이 걸리지 않는다

@Transactional 은 public 메서드에만 걸리고 package-private 메서드에는 걸리지 않는다.

근거 종류는 1(기술 사실)이다.


반증 조건 — package-private에서 트랜잭션이 살아 있으면 반증이다

  • refuted — 두 환경 중 하나라도 package-private 메서드에서 isActualTransactionActive()가 true다
  • supported — 두 환경 모두 public은 true, package-private은 false다
  • inconclusive — public 대조군이 false다(프록시가 아예 안 걸린 것이다)

protected 결과는 기록만 하고 판정에 쓰지 않는다. 표준 글의 문장이 다루는 것은 public과 package-private 둘이다.


설계 — 자기 호출을 피하고, 호출 직전 상태까지 같이 잰다

package-private 클래스 하나에 public · protected · package-private 메서드를 두고 전부 @Transactional을 달았다. 메서드 안에서 TransactionSynchronizationManager.isActualTransactionActive()를 읽는다.

호출은 같은 패키지의 다른 빈이 한다. 같은 빈 안에서 부르면 프록시를 안 거쳐 무조건 false가 나오는데, 그건 가시성 때문이 아니다. 이걸 틀리면 실험이 통째로 무의미해진다.

프록시 방식(AopUtils.isCglibProxy)도 기록했다. 가시성 지원이 프록시 방식에 달려 있을 수 있어서다. DB는 트랜잭션 매니저가 필요해서 로컬 PostgreSQL 17에 붙였고 실험 전용 데이터베이스를 만들고 지웠다.


결과 — 셋 다 트랜잭션 안이었다

PROBE boot=3.5.16 proxy=cglib public=true protected=true package=true
                  outside_before_public=false outside_before_protected=false outside_before_package=false
PROBE boot=4.1.1  (같음)

판정은 refuted다. 두 Boot 버전 모두 CGLIB 프록시였고 package-private 메서드에서도 트랜잭션이 살아 있었다.


예상과 달랐던 점 — 착시를 먼저 의심하고 대조군을 더 넣었다

첫 결과가 주장과 정반대라 앞 호출의 트랜잭션이 남아 다음 메서드가 저절로 true로 보이는 착시를 의심했다. 그래서 호출자 쪽에서 호출 직전에 같은 값을 읽는 대조군(outside_before_*)을 넣고 다시 돌렸다.

호출 직전(호출자 쪽)   false
메서드 안             true

셋 다 호출 직전은 false다. 트랜잭션은 그 호출에서 새로 시작된 것이고, 프록시가 실제로 package-private 메서드를 가로챈다.

왜 걸리는지는 재지 않았다. 스프링의 어느 버전에서 무엇이 바뀌었는지는 측정한 것이 아니라 추측이라 적지 않는다. 측정된 것은 동작뿐이다.


이 결과가 표준에 남기는 것 — 경계를 가시성에 기대던 문장이 무너진다

규칙(트랜잭션 경계는 UseCase 구현 메서드에 둔다)은 이 실험이 판정하지 않는다. 깨진 것은 「UseCase에 없는 package-private 메서드에는 안 걸린다」는 안전장치 설명이다. 지금은 내부 보조 메서드에 @Transactional을 실수로 달아도 그대로 동작한다. 경계가 의도와 다른 자리에 생길 수 있다는 뜻이다.

규칙은 바로 고치지 않는다. 표준 글에 > 반증: 줄을 달고 kind: 모순 이슈에서 경계를 무엇으로 강제할지를 정한다. 가시성이 아니라면 ArchUnit 같은 검사가 후보다.


정리

  • public·protected·package-private 셋 다 트랜잭션 안이었다. 두 Boot 버전 모두 CGLIB
  • 호출 직전 상태는 전부 false였다. 앞 트랜잭션이 샌 착시가 아니다
  • 가시성으로 트랜잭션 경계를 그을 수 없다
  • 재지 않은 것 — 그렇게 동작하는 기전, JDK 동적 프록시일 때, 자기 호출일 때

자신만의 철학을 만들어가는 중입니다.
최상단으로 이동했습니다!
확대 이미지

댓글남기기