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 동적 프록시일 때, 자기 호출일 때
자신만의 철학을 만들어가는 중입니다.
댓글남기기