@Transactional의 처리 과정과 self-invocation / readOnly의 이점

양성준·2025년 4월 11일

스프링

목록 보기
28/49

@Transactional의 구조적 흐름 (JPA)

// 명시적으로 코드를 작성한다면..
public class businessService {
	
    // 트랜잭션 매니저
    private final PlatformTransactionManager transactionManager;

	public void basicLogic(String userId) throws SQLException {
    
    // 트랜잭션 시작
    TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());

        try {
          // 비즈니스 로직
          bizLogic(userId);
          transactionManager.commit(status); // 성공시 커밋
          
        } catch (Exception e) {
        
          transactionManager.rollback(status); //실패시 롤백
          throw new IllegalStateException(e);
        }
  	}
    
    private void bizLogic(String userId) throws SQLException {
    	...
  	}
}

1. 서비스 메서드 호출과 프록시

  • @Transactional이 붙은 서비스 클래스는 Spring AOP에 의해 원본 객체가 아닌 프록시 객체로 감싸져 Bean으로 등록됨
    • 컨트롤러가 Service를 주입받을 때 원본이 아닌 프록시 객체를 주입받음

2. 프록시의 역할

  • 프록시 객체는 메서드 호출을 가로채서 내부적으로 TransactionInterceptor 에게 위임함
  • 프록시는 트랜잭션을 직접 관리하지 않고 "이 메서드에 트랜잭션이 필요하다"는 정보와 트랜잭션 속성을 전달

3. TransactionInterceptor의 역할

  • @Transactional 정보를 기반으로 트랜잭션 속성 분석
    • 전파 전략 (REQUIRED, REQUIRES_NEW 등)
    • readOnly 여부
      • readOnly = true를 설정하게 되면 JPA는 해당 트랜잭션 내에서 조회하는 Entity는 조회용임을 인식하고 변경 감지를 위한 Snapshot을 따로 보관하지 않으므로 메모리가 절약되는 성능상 이점이 있음
      • 트랜잭션 Commit 시 영속성 컨텍스트가 자동으로 flush 되지 않으므로 조회용으로 가져온 Entity의 예상치 못한 수정을 방지
  • 트랜잭션이 필요한 상황이면 PlatformTransactionManager를 통해 트랜잭션 시작 요청
  • 실제 메서드를 실행 (invocation.proceed())

4. TransactionManager (예: JpaTransactionManager)의 역할

  • 실제 트랜잭션 실행은 하지 않고, EntityManager의 생명주기만 관리해줌
  • EntityManagerFactory에서 EntityManager 생성
    • 현재 쓰레드에 EntityManager를 바인딩
  • 트랜잭션 시작 요청 시 → 내부의 EntityManager.getTransaction().begin() 호출
  • 트랜잭션 커밋/롤백 시 → EntityManager.getTransaction().commit()/rollback() 호출

5. EntityManager의 역할

  • 트랜잭션의 실질적인 실행 주체, 영속성 컨텍스트를 관리하는 핵심 객체 (엔티티의 생명주기 관리)
    • begin()
    • commit()
    • rollback()
  • JPA 구현체 (예: Hibernate)가 제공하는 기능

왜 Spring이 이런 과정으로 추상화했는가?

  • 직접 EntityManager로 트랜잭션 관리 시:
    • 예외 처리 코드 중복
    • 롤백 누락 위험
    • 전파 속성, readOnly 설정 등 복잡한 로직을 수동 처리해야 함
  • 그래서 Spring은 @Transactional과 TransactionManager를 통해 선언적 트랜잭션 처리 방식을 제공해 실수를 줄이고, 코드 가독성과 유지보수성을 향상시킴

전체 흐름 요약

[Controller]
  ↓ (Service 프록시 호출)
[프록시 객체 (@Transactional)]
  ↓
[TransactionInterceptor]
  ↓
[PlatformTransactionManager (JpaTransactionManager)]
  ↓
[EntityManagerFactory → EntityManager]
  ↓
[EntityManager.begin()/commit()/rollback()] ← 트랜잭션 실행

=> 트랜잭션이 프록시를 통해 동작을 하기 때문에, self-invocation 문제가 발생한다.

Self-invocation 문제

"Spring에서 트랜잭션은 프록시를 통해 동작하기 때문에,
같은 클래스 내부에서 자기 자신의 메서드를 호출(self-invocation)하면 프록시를 거치지 않고 직접 호출하게 되고,
이로 인해 @Transactional이 적용되지 않는 문제가 발생한다."

문제 상황

@Service
public class PaymentService {

    @Transactional
    public void processPayment(Payment payment) {
        savePayment(payment);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW) // 별도의 새 트랜잭션을 생성
    public void savePayment(Payment payment) {
    }
}
  • savePayment 메서드는 외부에서 호출하면 새 트랜잭션에서 실행되지만, processPayment에서 호출하면 새 트랜잭션이 생성되지 않음
    • 프록시가 아닌 this.savePayment()로 직접 호출되기 때문 -> AOP가 적용되지 않아 @Transactional이 무시됨
    • @Transactional은 프록시 객체가 해당 메서드를 가로챌 때만 트랜잭션을 적용할 수 있다.
    • 같은 클래스 내부에서의 메서드 호출은 프록시를 통하지 않기 때문에 트랜잭션이 적용되지 않습니다.
    • @Service가 붙었다고 해서 무조건 프록시가 생성되는 건 아니다. AOP가 적용되는 애노테이션(예: @Transactional, @Async, @Cacheable) 등이 있어야 Spring이 프록시 객체를 생성해서 Bean으로 등록한다.
    • 이때 등록된 Bean은 원본 클래스가 아니라 프록시 객체!
    • 하지만 같은 클래스 내부에서의 메서드 호출은 this.메서드() 형태로 호출되기 때문에 프록시를 거치지 않음 →
      따라서 @Transactional과 같은 AOP가 동작하지 않는다.

해결 방법

서비스 클래스 분리 방식 (가장 많이 사용)

// 결제 저장을 담당하는 서비스
@Service
public class PaymentSaveService {

    @Autowired
    private PaymentRepository paymentRepository;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void savePayment(Payment payment) {
        // 새 트랜잭션에서 실행됨
        paymentRepository.save(payment);
    }
}

// 결제 처리를 담당하는 서비스
@Service
public class PaymentProcessService {

    @Autowired
    private PaymentSaveService paymentSaveService; // 분리된 서비스 주입

    @Transactional
    public void processPayment(Payment payment) {
        // 비즈니스 로직 처리...

        // 분리된 서비스의 메서드 호출 - 프록시를 통해 호출되므로 트랜잭션 적용됨
        paymentSaveService.savePayment(payment);

        // 추가 로직...
    }
}

=> 여기서는 프록시 객체가 의존성 주입될 때 주입되어 트랜잭션을 관리함 -> processPayment에서 savePayment를 호출해도 프록시를 통해 호출되어 @Transactional이 적용된다.

  • 명확한 책임 분리: 각 서비스 클래스가 더 명확한 역할과 책임을 갖게 된다.
  • 코드 가독성 향상: 비즈니스 로직이 단일 목적을 가진 클래스로 분리되어 코드가 더 읽기 쉬워진다.
  • 테스트 용이성: 분리된 각 서비스를 독립적으로 테스트하기 쉽다.
  • 유지보수 용이: 기능 확장이나 변경 시 관련된 클래스만 수정하면 된다.
  • AOP 동작의 자연스러운 활용: Spring의 프록시 메커니즘을 그대로 활용하므로 추가 설정이나 특별한 코드가 필요 없음!

그 외의 self-injection, AopContext 사용 방식도 있지만, 잘 사용하지 않는다.

@Service
public class PaymentService {

    @Autowired
    private PaymentService self; // 자기 자신의 프록시 주입

    @Transactional
    public void processPayment(Payment payment) {
        // 프록시를 통해 호출하여 트랜잭션 적용
        self.savePayment(payment);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void savePayment(Payment payment) {
        // 새 트랜잭션에서 실행됨
    }
}
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Service
public class PaymentService {

    @Transactional
    public void processPayment(Payment payment) {
        // 현재 프록시 객체를 가져와서 savePayment 메서드 호출
        ((PaymentService) AopContext.currentProxy()).savePayment(payment);

        // 다른 비즈니스 로직...
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void savePayment(Payment payment) {
        // 새 트랜잭션에서 실행됨
        // 데이터베이스 저장 로직...
    }
}

=> 이 모든것들이, AOP가 적용된 객체가 생성될 때 먼저 proxy 객체를 만들어서 실제 객체를 lazyLoading을 해주기 때문에,
proxy 객체가 존재하여 가능한 것

  • Spring AOP는 프록시 객체를 먼저 생성해서 Bean으로 등록하고,
  • 그 프록시는 내부에 실제 객체를 들고 있으며,
  • PaymentService self나 AopContext.currentProxy()를 통해 현재 프록시를 직접 참조할 수 있고,
  • 이를 통해 내부 호출에서도 프록시를 "우회적으로" 사용할 수 있게 되는 것이다
profile
백엔드 개발자

0개의 댓글