private 메서드에 @Transactional 선언하면 트랜잭션이 동작할까?

시훈·2025년 4월 22일

Spring에서 @Transactional을 선언해도 private 메서드에는 트랜잭션이 동작하지 않는다. 그 이유와 원리를 함께 알아보자.


🧠 핵심 개념 요약

  • @TransactionalSpring AOP 기반으로 동작한다.
  • AOP는 프록시 객체를 통해 메서드를 감싸 트랜잭션을 적용한다.
  • private 메서드는 프록시가 호출할 수 없으므로 트랜잭션이 적용되지 않는다.

🧪 실험 코드 예시

@Slf4j
@RequiredArgsConstructor
@Service
public class SelfInvocation {
    private final MemberRepository memberRepository;

    public void outerSaveWithPrivate(Member member) {
        savePrivate(member); // 직접 호출 (프록시 우회)
    }

    public void outerSaveWithPublic(Member member) {
        savePublic(member); // 직접 호출 (프록시 우회)
    }

    @Transactional
    private void savePrivate(Member member) {
        log.info("call savePrivate");
        memberRepository.save(member);
        throw new RuntimeException("rollback test");
    }

    @Transactional
    public void savePublic(Member member) {
        log.info("call savePublic");
        memberRepository.save(member);
        throw new RuntimeException("rollback test");
    }
}
@SpringBootTest
class SelfInvocationTest {

    @Autowired
    private SelfInvocation selfInvocation;
    @Autowired
    private MemberRepository memberRepository;

    @AfterEach
    void tearDown() {
        memberRepository.deleteAll();
    }

    @Test
    void privateMethodTransactionFail() {
        try {
            selfInvocation.outerSaveWithPrivate(new Member("test"));
        } catch (Exception e) {}

        // 트랜잭션이 동작하지 않아 롤백되지 않음
        assertThat(memberRepository.findAll()).hasSize(1);
    }

    @Test
    void directPublicMethodTransactionSuccess() {
        try {
            selfInvocation.savePublic(new Member("test"));
        } catch (Exception e) {}

        // 프록시를 통해 호출된 public 메서드는 정상적으로 트랜잭션 적용됨
        assertThat(memberRepository.findAll()).isEmpty();
    }
}

⚠️ 왜 private 메서드는 안될까?

  • Spring AOP는 기본적으로 JDK 동적 프록시 또는 CGLIB을 사용한다.
  • JDK 프록시는 인터페이스 기반으로 public 메서드만 프록싱 가능하다.
  • CGLIB은 클래스 기반 프록시이지만 private 메서드는 상속으로 오버라이드할 수 없어 프록시가 불가능하다.

결국, private 메서드는 AOP 프록시가 개입할 수 없어 트랜잭션이 적용되지 않는다.


🔁 self-invocation 문제

  • 동일 클래스 내에서 this.메서드() 형식으로 호출하는 경우도 프록시를 우회하게 되어 트랜잭션이 적용되지 않는다.

예시

public void outer() {
    this.innerTransactional(); // 트랜잭션 적용 안됨
}

@Transactional
public void innerTransactional() { ... }

🛠 해결 방법

1️⃣ 메서드를 다른 클래스로 분리하기

@Service
public class InnerService {
    @Transactional
    public void txMethod() { ... }
}

@Service
public class OuterService {
    private final InnerService innerService;

    public void call() {
        innerService.txMethod(); // 트랜잭션 정상 적용
    }
}

2️⃣ AopContext.currentProxy() 사용 (비추천)

public void outer() {
    ((MyService) AopContext.currentProxy()).inner();
}
  • @EnableAspectJAutoProxy(exposeProxy = true) 필요
  • Spring AOP에 강하게 결합되어 유지보수 측면에서 권장되지 않음

3️⃣ AspectJ 컴파일 타임 위빙 사용

  • AspectJ를 사용하면 this 호출에도 AOP 적용 가능
  • 설정 복잡도와 진입 장벽이 높아 일반적인 경우에는 잘 사용하지 않음

✅ 결론

조건트랜잭션 적용 여부
public 메서드 + 외부 호출✅ 적용됨
public 메서드 + 내부 호출 (this.메서드())❌ 적용 안됨
private 메서드❌ 적용 안됨

Spring AOP 기반의 @Transactional은 오직 프록시를 통한 public 메서드 호출에서만 작동한다.
프록시를 우회하는 private, this.메서드() 호출에서는 트랜잭션이 동작하지 않으므로 주의해야 한다.

profile
Backend Developer / Cloud Engineer

0개의 댓글