[Spring] Private 메서드에 @Transactional 선언 시 트랜잭션이 동작하는가?

2025. 1. 16. 00:51·Back-end/Spring
반응형
매일메일을 통한
하루하루 CS 공부하기

 

 

기본적으로 @Transactional, @Cacheable, @Async 등의 AOP 어노테이션은 런타임에 동작하는 Spring AOP를 기반으로 동작한다.

 

Spring AOP가 제공하는 JDK Dynamic Proxy, CGLIB 방식 모두 타깃이 구현하는 인터페이스나 구체 클래스를 대상으로 프록시를 만들어서 타깃 클래스의 메서드 수행 전후에 횡단 관심사에 대한 처리를 할 수 있다.

 

Spring은 빈 생성시, 해당 빈에 AOP 애너테이션이 있는지 검사하고, 있다면 프록시 객체를 생성하여 빈을 대체한다.

 

AOP 적용 대상인 클래스의 경우, 즉, @Transactional과 같은 AOP 애너테이션이 하나라도 선언된 클래스는 프록시로 감싸진다.

 

  • JDK Dynamic Proxy
    • 타깃 클래스가 구현하는 인터페이스를 기준으로 프록시를 생성하여public 메서드만 AOP 적용 가능
  • CGLIB 방식
    • 인터페이스를 구현하지 않는 클래스를 상속하여 프록시를 생성하고, private을 제외한 public, protected, package-private 메서드에 AOP 적용 가능.

 

@Slf4j
@RequiredArgsConstructor  
@Service  
public class SelfInvocation {  
  
    private final MemberRepository memberRepository;  
  
    public void outerSaveWithPublic(Member member) {  
        saveWithPublic(member);  
    }  
  
    @Transactional  
    public void saveWithPublic(Member member) {  
        log.info("call saveWithPublic");  
        memberRepository.save(member);  
        throw new RuntimeException("rollback test");  
    }  
  
    public void outerSaveWithPrivate(Member member) {  
        saveWithPrivate(member);  
    }  
  
    @Transactional  
    private void saveWithPrivate(Member member) {  
        log.info("call saveWithPrivate");  
        memberRepository.save(member);  
        throw new RuntimeException("rollback test");  
    }  
}

public interface MemberRepository extends JpaRepository<Member, Long> {  
}


@SpringBootTest  
class SelfInvocationTest {  
  
    private static final Logger log = LoggerFactory.getLogger(SelfInvocationTest.class);  
  
    @Autowired  
    private SelfInvocation selfInvocation;  
  
    @Autowired  
    private MemberRepository memberRepository;  
  
    @AfterEach  
    void tearDown() {  
        memberRepository.deleteAllInBatch();  
    }  
  
    @Test  
    void aopProxyTest() {  
        // @Transactional 애너테이션을 가지고 있으므로, 빈이 Proxy 객체로 대체되어 주입된다.  
        assertThat(AopUtils.isAopProxy(selfInvocation)).isTrue();  
        // interface를 구현하지 않은 클래스이므로 CGLIB Proxy가 생성된다.  
        assertThat(AopUtils.isCglibProxy(selfInvocation)).isTrue();  
    }  
  
    @Test  
    void outerSaveWithPublic() {  
        Member member = new Member("test");  
  
        try {  
            selfInvocation.outerSaveWithPublic(member);  
        } catch (RuntimeException e) {  
            log.info("catch exception");  
        }  
  
        List<Member> members = memberRepository.findAll();  
        // self invocation 문제로 인해 트랜잭션이 정상 동작하지 않음.  
        // 예외 발생으로 인한 롤백이 동작하지 않고 남아있음.
    
      assertThat(members).hasSize(1);  
    }  
  
    @Test  
    void outerSaveWithPrivate() {  
        try {  
            selfInvocation.outerSaveWithPrivate(new Member("test"));  
        } catch (RuntimeException e) {  
            log.info("catch exception");  
        }  
  
        List<Member> members = memberRepository.findAll();  
  
        // self invocation 문제로 인해 트랜잭션이 정상 동작하지 않음.  
        // 예외 발생으로 인한 롤백이 동작하지 않고 남아있음.        assertThat(members).hasSize(1);  
    }  
  
    @Test  
    void saveWithPublic() {  
        Member member = new Member("test");  
  
        try {  
            selfInvocation.saveWithPublic(member);  
        } catch (RuntimeException e) {  
            log.info("catch exception");  
        }  
  
        List<Member> members = memberRepository.findAll();  
  
        // 외부에서 프록시 객체를 통해 메서드가 호출되었기 때문에 트랜잭션 정상 동작, 롤백 성공.  
        assertThat(members).hasSize(0);  
    }  
}

 

 

Spring AOP는 외부에서 프록시 객체를 통해 메서드가 호출될 때만 AOP 어드바이스(트랜잭션 관리)를 적용한다.

 

같은 클래스 내에서 메서드를 호출하면, 프록시를 거치지 않고 직접 호출되므로 트랜잭션 어드바이스가 적용되지 않는다.

이를 해결하기 위해서는 별도의 클래스로 분리하거나, AspectJ를 이용하는 방법이 있다.

 

별도의 클래스로 분리

@Slf4j  
@RequiredArgsConstructor  
@Service  
public class TransactionService {  
  
    @Transactional  
    public void outer() {  
        log.info("call outer");  
        logCurrentTransactionName();  
        logActualTransactionActive();  
        inner();  
    }  
  
    @Transactional(propagation = Propagation.REQUIRES_NEW)  
    public void inner() {  
        log.info("call inner");  
        logCurrentTransactionName();  
        logActualTransactionActive();  
    }  
  
    private void logActualTransactionActive() {  
        boolean actualTransactionActive = TransactionSynchronizationManager.isActualTransactionActive();  
        log.info("actualTransactionActive = {}", actualTransactionActive);  
    }  
  
    private void logCurrentTransactionName() {  
        String currentTransactionName = TransactionSynchronizationManager.getCurrentTransactionName();  
        log.info("currentTransactionName = {}", currentTransactionName);  
    }  
}

// 로그
// call outer  
// currentTransactionName = server.transaction.TransactionService.outer  
// actualTransactionActive = true  
// call inner  
// currentTransactionName = server.transaction.TransactionService.outer  
// actualTransactionActive = true

 

코드 상에서, outer가 inner 메서드를 호출하는데, outer의 propagation 속성은 REQUIRED, inner는 REQUIRES_NEW로 서로 다른 트랜잭션으로 분리되어야 한다.

 

하지만, 로그를 보면 동일한 outer의 트랜잭션에 속해있다.

 

이처럼 트랜잭션 전파 속성이 다른 두 메서드가 동일한 클래스 내부에서 self invocation 호출하면 의도대로 동작하지 않다.

 

이 때 outer와 inner 메서드를 각각 다른 클래스로 분리하여 호출해야 한다

 

// OuterTransactionService
@Slf4j  
@RequiredArgsConstructor  
@Service  
public class OuterTransactionService {  
  
    private final InnerTransactionService innerTransactionService;  
  
    @Transactional  
    public void outer() {  
        log.info("call outer");  
        logCurrentTransactionName();  
        logActualTransactionActive();  
        innerTransactionService.inner();  
    }  
  
    private void logActualTransactionActive() {  
        boolean actualTransactionActive = TransactionSynchronizationManager.isActualTransactionActive();  
        log.info("actualTransactionActive = {}", actualTransactionActive);  
    }  
  
    private void logCurrentTransactionName() {  
        String currentTransactionName = TransactionSynchronizationManager.getCurrentTransactionName();  
        log.info("currentTransactionName = {}", currentTransactionName);  
    }  
}

// InnerTransactionService
@Slf4j  
@RequiredArgsConstructor  
@Service  
public class InnerTransactionService {  
  
    @Transactional(propagation = Propagation.REQUIRES_NEW)  
    public void inner() {  
        log.info("call inner");  
        logCurrentTransactionName();  
        logActualTransactionActive();  
    }  
  
    private void logActualTransactionActive() {  
        boolean actualTransactionActive = TransactionSynchronizationManager.isActualTransactionActive();  
        log.info("actualTransactionActive = {}", actualTransactionActive);  
    }  
  
    private void logCurrentTransactionName() {  
        String currentTransactionName = TransactionSynchronizationManager.getCurrentTransactionName();  
        log.info("currentTransactionName = {}", currentTransactionName);  
    }  
}

// 로그
// call outer  
// currentTransactionName = server.transaction.OuterTransactionService.outer  
// actualTransactionActive = true  
// call inner  
// currentTransactionName = server.transaction.InnerTransactionService.inner  
// actualTransactionActive = true
반응형

'Back-end > Spring' 카테고리의 다른 글

[Spring] JPA 벌크 연산이란?  (0) 2025.02.01
[Spring] Spring Batch를 활용한 배치 프로세싱  (3) 2025.01.20
[Spring] Spring Data JPA에서 새로운 Entity인지 판단하는 방법?  (1) 2024.12.08
[Spring] AWS SageMaker 엔드포인트 연결 시도하기  (0) 2024.11.23
[Spring] self-hosted Runner를 통한 CI/CD 배포  (0) 2024.11.21
'Back-end/Spring' 카테고리의 다른 글
  • [Spring] JPA 벌크 연산이란?
  • [Spring] Spring Batch를 활용한 배치 프로세싱
  • [Spring] Spring Data JPA에서 새로운 Entity인지 판단하는 방법?
  • [Spring] AWS SageMaker 엔드포인트 연결 시도하기
류건
류건
개발 일지
  • 류건
    건's Dev
    류건
  • 전체
    오늘
    어제
    • 분류 전체보기 (94)
      • Back-end (55)
        • Spring (30)
        • Nest.js (3)
        • Next.js (2)
        • Node.js (3)
      • Infra & Cloud (20)
        • Cloud Computing (6)
        • Docker (3)
        • AWS (7)
      • Java (2)
      • Computer Science (12)
        • Computer Network (0)
        • Operating System (0)
        • 정보 보호와 시스템 보안 (12)
      • 회고록 (1)
        • 우아한테크코스 (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    Docker
    EC2
    public key
    어노테이션
    보안
    WebClient
    Spring
    https
    db
    오블완
    CD
    Spring Boot
    티스토리챌린지
    Kafka
    JPA
    Nest.js
    정보보호
    고가용성
    Lock
    JWT
    Github Actions
    ssl
    nginx
    Webflux
    aws
    express.js
    CI/CD
    ddl-auto
    CORS
    node.js
  • 최근 댓글

  • 최근 글

  • 반응형
  • hELLO· Designed By정상우.v4.10.0
류건
[Spring] Private 메서드에 @Transactional 선언 시 트랜잭션이 동작하는가?
상단으로

티스토리툴바