[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 컨테이너 초기화
- Spring Boot 애플리케이션이 실행되면서 ApplicationContext가 초기화 됨
- @ComponentScan에 의해 @Service가 붙은 OrderService가 빈으로 등록될 대상이 됨.
- @Transactional이 붙어 있기 때문에 AOP 프록시 생성이 필요함.
2️⃣ AOP 프록시 생성 과정
Spring AOP는 프록시를 생성하기 위해 두 가지 방식을 사용합니다.
- JDK 동적 프록시 -> 인터에피스가 있는 경우
- CGLIB 프록시 -> 인터에피스가 없는 경우(클래스 상송 방식)
OrderService는 인터페이스를 구현하지 않았으므로 CGLIB을 사용하여 프록시 객체를 생성합니다.
[프록시 생성 과정]
- Spring의 BeanPostProcessor 가 실행되면서 @Transactional이 붙은 클래스를 감지.
- CGLIB을 이용하여 OrderService$$EnhancerBySpringCGLIB라는 동적 프록시 클래스를 생성
- 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. 해결 방법: 클래스 분리
'Back-End > Spring' 카테고리의 다른 글
| Spring의 @Configuration (0) | 2025.04.15 |
|---|---|
| Spring은 어떻게 객체를 관리할까? (0) | 2025.02.25 |
| Self-Invocation(자기호출) 문제 (0) | 2025.02.25 |
| SpringSecurity Authentication 객체와 Principal 객체 (0) | 2025.01.24 |
| SpringSecurity + JWT를 활용한 로그인 (0) | 2025.01.23 |