Backend Development

Spring AOP와 @Transactional: private 메서드에서의 동작 방식

Kun Woo Kim 2025. 5. 29. 00:37
반응형

서론

Spring Framework에서 @Transactional 애너테이션은 데이터베이스 트랜잭션 관리를 위한 핵심 기능입니다. 그러나 이 애너테이션이 private 메서드에서 제대로 동작하지 않는 경우가 있습니다. 이는 Spring AOP의 동작 방식과 밀접한 관련이 있습니다. 본 글에서는 Spring AOP의 동작 원리와 @Transactional이 private 메서드에서 동작하지 않는 이유, 그리고 이를 해결하는 방법에 대해 설명하겠습니다.


Spring AOP의 동작 원리

1. AOP 프록시 생성 방식

Spring AOP는 런타임에 동작하며, JDK Dynamic Proxy와 CGLIB 두 가지 방식으로 프록시를 생성합니다.

JDK Dynamic Proxy

  • 인터페이스를 구현한 클래스에 적용
  • public 메서드만 AOP 적용 가능
  • 인터페이스 기반으로 프록시 생성

CGLIB

  • 인터페이스를 구현하지 않은 클래스에 적용
  • private을 제외한 모든 메서드에 AOP 적용 가능
  • 클래스 상속을 통한 프록시 생성

2. 프록시 생성 시점

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

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

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

Self-Invocation 문제

1. 문제 발생 원인

  • 같은 클래스 내부에서 메서드 호출 시 프록시를 거치지 않음
  • AOP 어드바이스가 적용되지 않음
  • 트랜잭션 관리가 제대로 동작하지 않음

2. 테스트 코드 예시

@SpringBootTest
class SelfInvocationTest {
    @Autowired
    private SelfInvocation selfInvocation;

    @Test
    void outerSaveWithPublic() {
        Member member = new Member("test");
        try {
            selfInvocation.outerSaveWithPublic(member);
        } catch (RuntimeException e) {
            log.info("catch exception");
        }

        List<Member> members = memberRepository.findAll();
        // 트랜잭션이 정상 동작하지 않아 롤백되지 않음
        assertThat(members).hasSize(1);
    }
}

해결 방법

1. 자기 자신을 프록시로 주입

@Service
public class SelfInvocation {
    private final MemberRepository memberRepository;
    private final SelfInvocation selfInvocation;  // 자기 자신 주입

    public void outerSaveWithPublic(Member member) {
        selfInvocation.saveWithPublic(member);  // 프록시를 통한 호출
    }
}

주의: 순환 의존성 문제가 발생할 수 있어 권장되지 않습니다.

2. 클래스 분리

@Service
public class OuterTransactionService {
    private final InnerTransactionService innerTransactionService;

    @Transactional
    public void outer() {
        innerTransactionService.inner();
    }
}

@Service
public class InnerTransactionService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void inner() {
        // 내부 로직
    }
}

3. AspectJ 사용

  • 컴파일 시점에 위빙(Weaving) 수행
  • 동일 클래스 내 메서드 호출에도 AOP 적용 가능
  • 설정이 복잡할 수 있음

트랜잭션 전파 속성과 Self-Invocation

1. 문제 상황

@Service
public class TransactionService {
    @Transactional
    public void outer() {
        inner();  // REQUIRES_NEW 속성이 무시됨
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void inner() {
        // 내부 로직
    }
}

2. 해결 방법

@Service
public class OuterTransactionService {
    private final InnerTransactionService innerTransactionService;

    @Transactional
    public void outer() {
        innerTransactionService.inner();  // 별도 클래스로 분리
    }
}

@Service
public class InnerTransactionService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void inner() {
        // 내부 로직
    }
}

결론

Spring AOP는 프록시 기반으로 동작하기 때문에, 같은 클래스 내부에서의 메서드 호출에는 AOP 어드바이스가 적용되지 않습니다. 이를 해결하기 위해서는 클래스를 분리하거나 AspectJ를 사용하는 것이 좋습니다.

인사이트: Spring AOP의 동작 방식을 이해하면 트랜잭션 관리뿐만 아니라 다른 AOP 기반 기능들도 더 효과적으로 활용할 수 있습니다. 특히 Self-Invocation 문제는 Spring 애플리케이션에서 자주 발생하는 이슈이므로, 미리 대비하는 것이 중요합니다.


참고 자료

반응형