Spring @Transactional 과 프록시(JDK 동적 프록시 / 클래스 프록시)

풀어갈 나의 이야기·2026년 1월 29일

Spring Framework

목록 보기
7/7

1. 목적

이 문서는 스프링 @Transactional이 어떤 방식으로 동작하는지, 그리고 JDK 동적 프록시와 클래스 프록시(CGLIB)가 어떤 차이를 가지며 어떤 경우 트랜잭션이 적용되지 않는지 정리한다.


2. @Transactional 동작 원리(요약)

스프링 트랜잭션은 보통 “프록시 기반 AOP”로 구현된다.

  • 외부에서 메서드 호출이 들어오면 프록시가 호출을 가로챈다(intercept)
  • 트랜잭션 시작
  • 실제 대상 메서드 호출
  • 정상 종료 시 커밋, 예외 시 롤백
  • 트랜잭션 종료

핵심

  • 트랜잭션이 적용되려면 “프록시를 통해 호출되어야 한다”

3. 프록시 종류

3.1 JDK 동적 프록시(JDK Dynamic Proxy)

  • 인터페이스 기반 프록시
  • 런타임에 “인터페이스를 구현한 프록시 객체”를 생성한다(java.lang.reflect.Proxy)
  • 전제: 대상 빈이 하나 이상의 인터페이스를 구현해야 한다

특징

  • 프록시는 “구현 클래스 타입”이 아니라 “인터페이스 타입”으로 노출된다
  • 인터페이스에 선언된 메서드 호출을 중심으로 가로챈다

3.2 클래스 프록시(Class-based Proxy, CGLIB 계열)

  • 상속 기반 프록시
  • 대상 클래스를 상속한 서브클래스를 생성하고 메서드를 오버라이드해서 가로챈다

특징

  • 인터페이스가 없어도 적용 가능
  • 상속/오버라이드 제약을 그대로 받는다

중요 제약

  • final class는 상속 불가
  • final method는 오버라이드 불가
  • private method는 오버라이드 불가
  • static method는 프록시 가로채기 대상이 아님

4. 트랜잭션이 “안 되는” 대표 케이스

4.1 self-invocation(자기 호출)

가장 흔한 원인이다.

  • 프록시는 “외부에서 프록시를 통해 들어오는 호출”만 가로챈다
  • 같은 클래스 내부에서 다른 메서드를 호출하면 this.inner() 호출이 되어 프록시를 거치지 않는다
  • 내부 메서드에 선언한 @Transactional 옵션(전파, readOnly 등)이 적용되지 않을 수 있다

특히 문제가 되는 상황

  • 내부 메서드에 REQUIRES_NEW를 기대했으나 새 트랜잭션이 열리지 않음
  • 내부 메서드에 readOnly, 격리 수준 변경을 기대했으나 적용되지 않음

참고

  • 외부 진입 메서드가 이미 트랜잭션을 열어둔 상태라면 내부 호출 로직은 그 트랜잭션 안에서 실행될 수 있다.
  • 문제는 “내부 메서드에 선언한 트랜잭션 속성”이 별도로 적용되지 않는 점이다.

4.2 private 메서드에 @Transactional

  • 프록시가 가로채려면 인터셉트 지점이 필요하다
  • private는 오버라이드/가로채기가 불가능하여 적용되지 않는다

4.3 final 클래스/메서드(클래스 프록시 사용 시)

  • 클래스 프록시는 상속 기반이므로 final이 있으면 가로채기 어렵다
  • 트랜잭션 적용이 예상과 다르게 동작할 수 있다

4.4 스프링 컨테이너 밖에서 생성된 객체

  • new로 생성한 객체는 프록시가 아니므로 @Transactional이 동작하지 않는다

5. 롤백이 기대와 다른 대표 케이스(“트랜잭션은 걸렸지만 결과가 다름”)

5.1 기본 롤백 규칙

  • 기본적으로 RuntimeExceptionError에서 롤백한다
  • Checked Exception은 기본 롤백 대상이 아닐 수 있다

필요 시

  • rollbackFor = Exception.class 등으로 명시한다

5.2 예외를 잡아먹는 경우

  • try/catch로 예외를 처리하고 다시 던지지 않으면 롤백 트리거가 발생하지 않는다
  • 롤백을 원하면 예외를 재던지거나, 명시적으로 롤백 처리를 해야 한다

6. 문제 해결 가이드(권장 순)

6.1 self-invocation 해결(권장)

1) 트랜잭션 경계를 “빈 간 호출”로 만들기

  • 내부 메서드를 별도 서비스(별도 빈)로 분리
  • 외부 호출이 반드시 프록시를 통과하게 만들어 트랜잭션 속성을 정상 적용

2) TransactionTemplate 사용

  • AOP 대신 코드로 트랜잭션 경계를 명시
  • private 메서드, 내부 호출 구조에서도 안정적으로 적용 가능

3) 프록시를 통한 자기 자신 호출

  • 기술적으로 가능하나 구조를 복잡하게 만들 수 있어 우선순위가 낮다

6.2 트랜잭션이 필요한 초기화/워밍업 작업

  • @PostConstruct에 트랜잭션을 넣기보다, 애플리케이션 기동 이후 훅(예: ApplicationReadyEvent) 또는 TransactionTemplate 기반 실행을 고려한다

7. 요약

  • @Transactional은 프록시를 통해 메서드 호출을 가로채는 방식으로 트랜잭션을 적용한다
  • 프록시는 JDK 동적 프록시(인터페이스 기반) 또는 클래스 프록시(CGLIB, 상속 기반)로 생성된다
  • 트랜잭션이 적용되지 않는 가장 흔한 원인은 self-invocation(자기 호출)과 private/final 제약, 컨테이너 밖 객체 생성이다
  • 롤백은 기본 규칙(RuntimeException/Error)과 예외 처리 방식(try/catch)에 의해 기대와 다르게 보일 수 있다
profile
깨끗한 스케치북 일수록 우아한 그림이 그려지법, 읽기 쉽고, 짧은 코드가 더 아름다운 법, 개발이란 구현할 프로그래밍이 아닌 풀어갈 이야기로 써내려가는것.

0개의 댓글