Spring 실행 시점에서 Proxy와 Transaction은 어떻게 함께 동작하는가

donghyeoneom·2026년 2월 4일
post-thumbnail

Spring을 사용하다 보면 자연스럽게 이런 질문을 하게 됩니다.

  • 왜 내부 메서드 호출에서는 @Transactional이 동작하지 않는가
  • Service를 여러 개로 나눴는데 왜 전체 롤백이 되는가
  • Proxy와 Transaction은 정확히 어디에서 시작되는가

이 글에서는 실행 시점(Runtime)만을 기준으로
Spring의 Proxy와 Transaction이 어떻게 연결되어 동작하는지를 정리합니다.


1️⃣ 실행 시점의 전제 조건

실행 시점에 애플리케이션은 이미 다음 상태입니다.

  • 모든 Bean은 생성되어 있습니다
  • AOP 대상 Bean은 Proxy로 등록되어 있습니다
  • 트랜잭션 설정은 이미 확정되어 있습니다

즉,

실행 시점에는
Proxy와 Transaction이 새로 결정되지 않고,
이미 준비된 구조가 사용됩니다.


2️⃣ Proxy의 본질부터 정리합니다

Spring에서 Proxy의 역할은 명확합니다.

Proxy는 메서드 호출을 가로채서
부가 로직을 실행하기 위한 구조입니다.

Proxy는 다음 질문만 판단합니다.

  • 이 호출이 Proxy를 통해 들어왔는가
  • 들어왔다면 개입해야 하는가

👉 Proxy는 객체를 대신하는 입구일 뿐이며, 비즈니스 로직을 직접 수행하지 않습니다.


3️⃣ Proxy가 동작하기 위한 조건

Proxy는 항상 동작하지 않습니다.
다음 조건을 모두 만족해야 개입합니다.

  • 외부 Bean에서 호출됩니다
  • public 메서드입니다
  • Proxy를 거쳐 호출됩니다

👉이 중 하나라도 깨지면 Proxy는 존재하지 않는 것처럼 동작합니다.


4️⃣ 예시① 내부 메서드 호출에서 Proxy가 동작하지 않는 이유

@Service
class SampleService {

    fun outer() {
        inner()
    }

    @Transactional
    fun inner() {
        // DB 작업
    }
}

몇몇 개발자는 보통 이렇게 기대합니다.

inner()@Transactional이 있으니
outer()에서 호출해도 트랜잭션이 걸릴 것이다

하지만 실제로는 동작하지 않습니다.

실제 실행 흐름

외부 호출
 → SampleService Proxy
   → outer()
        → this.inner()  // Proxy 미통과

핵심은 이것입니다.

Proxy는 outer()까지만 개입합니다.
내부에서 호출되는 this.inner()
이미 Proxy를 벗어난 실제 객체 호출입니다.

그래서 inner()에 붙은 @Transactional은 실행될 기회조차 없습니다.

  • Proxy는 외부 진입 지점만 감쌉니다
  • 내부 호출은 Proxy 영역 밖입니다

5️⃣ Transaction의 본질을 정리합니다

Spring에서 Transaction은 다음을 의미합니다.

  • 하나의 DB Connection을 점유합니다
  • AutoCommit을 끕니다
  • Commit 또는 Rollback을 결정합니다

중요한 점은 이것입니다.

Transaction은 메서드에 붙는 것이 아니라, Proxy가 열고 닫습니다.


6️⃣ Transaction은 언제 시작되는가

Transaction은 다음 시점에 시작됩니다.

외부 호출
 → Service Proxy 진입
   → @Transactional 확인
     → Transaction 시작

즉,

  • DispatcherServlet ❌
  • Controller ❌
  • Service Proxy 진입 시점 ⭕
    입니다.

7️⃣ 예시 ② Service를 분리했는데 왜 전체 롤백이 되는가

BizService

@Service
@Transactional
class BizService(
    private val aJpaService: AJpaService,
    private val bJpaService: BJpaService
) {
    fun execute() {
        aJpaService.save()
        bJpaService.save() // 여기서 예외 발생
    }
}

JpaService

@Service
@Transactional
class AJpaService {
    fun save() { }
}

@Service
@Transactional
class BJpaService {
    fun save() {
        throw RuntimeException()
    }
}

실행 시점의 실제 판단

  1. BizService Proxy 진입
  2. 트랜잭션 시작
  3. AJpaService Proxy 호출 → 기존 트랜잭션 참여
  4. BJpaService Proxy 호출 → 기존 트랜잭션 참여
  5. 예외 발생
  6. 트랜잭션 rollback-only 마킹
  7. 전체 트랜잭션 롤백

Service가 여러 개인 것은 중요하지 않습니다.
트랜잭션은 하나입니다.

  • 트랜잭션 경계는 Proxy 개수가 아니라 외부 진입점입니다
  • 모든 하위 Service는 같은 트랜잭션에 참여합니다

8️⃣ Proxy와 Transaction의 관계를 한 번에 정리하면

구분ProxyTransaction
역할호출 가로채기DB 작업 단위
시작 시점외부 호출 시Proxy 진입 시
내부 호출개입 불가기존 TX 유지
Service 분리Proxy 체인TX 전파

9️⃣ 마무리

Proxy와 Transaction을 함께 이해하면
다음 질문들이 한 번에 해결됩니다.

  • 왜 내부 메서드 호출에서는 트랜잭션이 안 되는지
  • 왜 Service를 분리해도 전체 롤백이 되는지
  • 왜 트랜잭션 경계를 Service 진입점에 두는지

0개의 댓글