
Spring을 사용하다 보면 자연스럽게 이런 질문을 하게 됩니다.
- 왜 내부 메서드 호출에서는 @Transactional이 동작하지 않는가
- Service를 여러 개로 나눴는데 왜 전체 롤백이 되는가
- Proxy와 Transaction은 정확히 어디에서 시작되는가
이 글에서는 실행 시점(Runtime)만을 기준으로
Spring의 Proxy와 Transaction이 어떻게 연결되어 동작하는지를 정리합니다.
실행 시점에 애플리케이션은 이미 다음 상태입니다.
즉,
실행 시점에는
Proxy와 Transaction이 새로 결정되지 않고,
이미 준비된 구조가 사용됩니다.
Spring에서 Proxy의 역할은 명확합니다.
Proxy는 메서드 호출을 가로채서
부가 로직을 실행하기 위한 구조입니다.
Proxy는 다음 질문만 판단합니다.
👉 Proxy는 객체를 대신하는 입구일 뿐이며, 비즈니스 로직을 직접 수행하지 않습니다.
Proxy는 항상 동작하지 않습니다.
다음 조건을 모두 만족해야 개입합니다.
👉이 중 하나라도 깨지면 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은 실행될 기회조차 없습니다.

Spring에서 Transaction은 다음을 의미합니다.
중요한 점은 이것입니다.
Transaction은 메서드에 붙는 것이 아니라, Proxy가 열고 닫습니다.
Transaction은 다음 시점에 시작됩니다.
외부 호출
→ Service Proxy 진입
→ @Transactional 확인
→ Transaction 시작
즉,
@Service
@Transactional
class BizService(
private val aJpaService: AJpaService,
private val bJpaService: BJpaService
) {
fun execute() {
aJpaService.save()
bJpaService.save() // 여기서 예외 발생
}
}
@Service
@Transactional
class AJpaService {
fun save() { }
}
@Service
@Transactional
class BJpaService {
fun save() {
throw RuntimeException()
}
}
BizService Proxy 진입AJpaService Proxy 호출 → 기존 트랜잭션 참여BJpaService Proxy 호출 → 기존 트랜잭션 참여rollback-only 마킹Service가 여러 개인 것은 중요하지 않습니다.
트랜잭션은 하나입니다.

| 구분 | Proxy | Transaction |
|---|---|---|
| 역할 | 호출 가로채기 | DB 작업 단위 |
| 시작 시점 | 외부 호출 시 | Proxy 진입 시 |
| 내부 호출 | 개입 불가 | 기존 TX 유지 |
| Service 분리 | Proxy 체인 | TX 전파 |
Proxy와 Transaction을 함께 이해하면
다음 질문들이 한 번에 해결됩니다.