본문 바로가기

Back-End/Spring

Spring AOP Proxy - SelfInvocation 문제

 

[Spring 애플리케이션이 실행되면서 AOP 프록시가 생성되고 동작하는 과정]

1. AOP 적용 대상 클래스(OrderService)

@Service
public class OrderService {

    public void processOrder() {
        System.out.println("Processing order...");
        completeOrder();
    }

    @Transactional
    public void completeOrder() {
        System.out.println("Order completed!");
        printTransactionStatus("completeOrder");
    }
    
    private void printTransactionStatus(String methodName) {
        boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
        System.out.println("[" + methodName + "] Transaction Active: " + isActive);
    }
}

 

위 코드에서 AOP가 적용될 대상은 @Transactional이 붙은 메서드입니다.

Spring이 이 클래스를 빈으로 등록할 때, 프로시 객체를 생성하여 원본 객체를 감싸게 됩니다.


2. Spring 애플리케이션 실행 시 프록시 생성 과정

1️⃣ Spring 컨테이너 초기화

  1. Spring Boot 애플리케이션이 실행되면서 ApplicationContext가 초기화 됨
  2. @ComponentScan에 의해 @Service가 붙은 OrderService가 빈으로 등록될 대상이 됨.
  3. @Transactional이 붙어 있기 때문에 AOP 프록시 생성이 필요함.

2️⃣ AOP 프록시 생성 과정

Spring AOP는 프록시를 생성하기 위해 두 가지 방식을 사용합니다.

  • JDK 동적 프록시 -> 인터에피스가 있는 경우
  • CGLIB 프록시 -> 인터에피스가 없는 경우(클래스 상송 방식)

OrderService는 인터페이스를 구현하지 않았으므로 CGLIB을 사용하여 프록시 객체를 생성합니다.

 

[프록시 생성 과정]

  1. Spring의 BeanPostProcessor 가 실행되면서 @Transactional이 붙은 클래스를 감지.
  2. CGLIB을 이용하여 OrderService$$EnhancerBySpringCGLIB라는 동적 프록시 클래스를 생성
  3. Spring컨테이너에 저장되는 객체는 원본이 아닌 프록시 객체(OrderService$$EnhancerBySpringCGLIB)

[프록시 객체의 역할]

  • 메서드가 호출되면 AOP 어드바이를 실행한 후, 원본 메서드를 실행

3. AOP 프록시가 적용된 상태에서 메서드 실행

이제 OrderService의 메서드를 실행해 보겠습니다.

@Aspect
@Component
public class TransactionLoggerAspect {

    @Before("@annotation(org.springframework.transaction.annotation.Transactional)")
    public void beforeTransaction(JoinPoint joinPoint) {
        System.out.println("Transaction Start");
    }

    @After("@annotation(org.springframework.transaction.annotation.Transactional)")
    public void afterTransaction(JoinPoint joinPoint) {
        System.out.println("Transaction End");
    }
}
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

OrderService orderService = context.getBean(OrderService.class);
System.out.println("OrderService의 실제 클래스: " + orderService.getClass().getName());

orderService.processOrder();

 

[출력 결과]

OrderService의 실제 클래스: com.example.service.OrderService$$EnhancerBySpringCGLIB
Processing order...
Order Complete...
[completeOrder] Transaction Active : false

 

어떤 일이 벌어졌을까?

Spring AOP가 동작하는 흐름을 정리해보겠습니다.


4. 메서드 호출 시 프록시 동작 과정

1️⃣ orderService.processOrder() 호출

실제 OrderService가 아니라 OrderService$$EnhancerBySpringCGLIB 프록시 객체가 호출됨.

 

2️⃣ 프록시가 AOP 어드바이스 실행

@Transactional이 적용된 메서드이므로 트랜잭션 관련 AOP 로직이 먼저 실행됨.

TransactionAspect에서 “Transaction Start”를 출력.

 

3️⃣ 원본 메서드 실행 (processOrder())

System.out.println("Processing order..."); 실행.

 

4️⃣ Self-Invocation 문제 발생

processOrder() 내부에서 completeOrder()를 직접 호출.

하지만 같은 클래스 내부의 호출이므로 프록시를 거치지 않고 원본 객체의 completeOrder()가 실행됨.

결과적으로 @Transactional이 적용되지 않음.

트랜잭션 없이 실행되므로 Transaction Start와 Transaction End가 찍히지 않음. 그리고 Transaction Active : false


5. Self-Invocation 문제 해결

위 문제를 해결하려면 프록시 객체를 거쳐서 메서드를 호출해야 합니다.

 

[해결 방법 : 자기 호출 또는 클래스 분리]

 

여기서는 클래스 분리 방법을 사용하겠습니다.
왜냐하면 Spring 컨테이너에 직접 접근하는 코드가 추가되므로 클래스의 결합도가 높아지고 테스트가 어려워질 수 있다고 판단되었습니다.

 

[기존 코드 - self-Invocation 문제 발생]

@Service
public class OrderService {

    public void processOrder() {
        System.out.println("Processing order...");
        completeOrder(); // Self-Invocation 문제 발생 (AOP 미적용)
    }

    @Transactional
    public void completeOrder() {
        System.out.println("Order completed!");
        printTransactionStatus("completeOrder");
    }
    
     private void printTransactionStatus(String methodName) {
        boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
        System.out.println("[" + methodName + "] Transaction Active : " + isActive);
    }
}
    • Self-Invocation 문제 발생 → completeOrder()는 프록시를 거치지 않고 실행됨.
    • 해결 방법 → completeOrder()를 다른 클래스로 분리하여 프록시를 거치도록 수정.

[해결 방법 - 트랜잭션이 필요한 메서드를 별도 클래스로 분리]

@Service
@RequiredConstructor
public class OrderService {

    private final OrderTransactionService orderTransactionService;

    public void processOrder() {
        System.out.println("Processing order...");
        orderTransactionService.completeOrder(); // 외부 클래스를 통해 호출 (AOP 적용됨)
    }
}

@Service
public class OrderTransactionService {

    @Transactional
    public void completeOrder() {
        System.out.println("Order completed!");
        printTransactionStatus2("completeOrder");
    }
    
     private void printTransactionStatus2(String methodName) {
        boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
        System.out.println("[" + methodName + "] Transaction Active : " + isActive);
    }
}
    • completeOrder()를 OrderTransactionService라는 별도 클래스로 이동
    • 이제 processOrder()가 OrderTransactionService.completeOrder()를 호출하면 프록시를 거쳐 @Transactional이 정상적으로 적용됩니다

[정상적인 결과]

OrderService의 실제 클래스: com.example.service.OrderService$$EnhancerBySpringCGLIB
Processing order...
Transaction Start 
Order Complete...
[completeOrder] Transaction Active : true
Transaction End

전체 과정 요약

Spring AOP 프록시 생성

 

1. Spring 컨테이너가 @Service가 붙은 OrderService를 빈으로 등록할 때 감지

2. @Transactional이 붙어 있어 CGLIB 프록시(OrderService$$EnhancerBySpringCGLIB)를 생성

3. Spring 컨테이너에 저장되는 객체는 원본이 아니라 프록시 객체

 

AOP 적용 메서드 호출 흐름

 

1. orderService.processOrder() 호출

2. 프록시 객체가 AOP 어드바이스 실행 (Transaction Start)

3. 원본 processOrder() 실행

4. Self-Invocation 발생 시 프록시를 거치지 않아 AOP 미적용 문제 발생

5. 해결 방법: 클래스 분리