JDK Dynamic Proxy에서 내부 메서드 호출이 안 되는 이유
Spring의 @Transactional이 같은 클래스 내에서 메서드를 호출할 때 동작하지 않는 이유는 JDK Dynamic Proxy가 메서드 호출을 가로채지 못하기 때문이다.
JDK Dynamic Proxy의 특징
JDK Dynamic Proxy는 인터페이스 기반의 프록시를 생성한다.
즉, 프록시는 인터페이스를 구현하는 객체로 생성되며, 원본 객체의 메서드 실행을 가로채 트랜잭션을 관리한다.
하지만!
JDK Dynamic Proxy는 인터페이스를 통해 접근할 때만 프록시를 거치며, 같은 클래스 내부에서 메서드를 호출하면 프록시를 거치지 않음.
왜 내부 호출이 트랜잭션을 적용하지 못하는가?
외부에서 호출할 때는 프록시가 적용됨
public interface ProductService {
void createProduct();
}
@Service
public class ProductServiceImpl implements ProductService {
@Override
@Transactional
public void createProduct() {
System.out.println("상품 생성");
}
}
📌 실제 객체 생성 과정 (JDK Dynamic Proxy 사용):
ProductService target = new ProductServiceImpl();
ProductService proxy = (ProductService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
new Class[]{ProductService.class},
new TransactionHandler(target) // 트랜잭션을 관리하는 핸들러
);
트랜잭션이 적용되는 과정
1. 클라이언트가 proxy.createProduct() 호출.
2. TransactionHandler가 createProduct() 실행을 가로채서 트랜잭션을 시작.
3. target.createProduct()를 실행.
4. 실행이 끝나면 트랜잭션을 commit() 또는 rollback().
즉, 외부에서 호출하면 JDK Dynamic Proxy가 동작하여 트랜잭션이 적용됨.
내부 호출 시 프록시가 적용되지 않음
@Service
public class ProductServiceImpl implements ProductService {
@Override
@Transactional
public void createProduct() {
System.out.println("상품 생성");
logTransaction(); // 내부 메서드 호출 (프록시 적용 안 됨)
}
@Transactional
public void logTransaction() {
System.out.println("트랜잭션 로그 저장");
}
}
내부 호출 흐름
productService.createProduct(); // 프록시 적용 (트랜잭션 관리)
├── createProduct() 호출
├── logTransaction() 호출 (내부 호출, 프록시를 거치지 않음)
결과적으로 logTransaction()은 프록시를 거치지 않고 직접 실행되므로 @Transactional이 적용되지 않음.
내부 호출에서 프록시가 적용되지 않는 이유
JDK Dynamic Proxy는 인터페이스를 기반으로 프록시 객체를 생성한다
• 프록시는 인터페이스를 통해 호출될 때만 실행을 가로챌 수 있음.
• 내부에서 this.logTransaction()을 호출하면, 인터페이스를 거치지 않고 직접 원본 객체를 실행하게 됨.
같은 클래스 내에서 this를 통해 호출하면 프록시를 거치지 않음
• Spring이 프록시 객체를 생성하는 방식:
ProductService proxy = new JdkDynamicProxy(new ProductServiceImpl());
• 이때, this.logTransaction();을 호출하면 프록시가 아니라 원본 객체(ProductServiceImpl)의 메서드를 직접 실행.
해결 방법
ApplicationContext를 사용하여 프록시를 통해 호출
@Autowired
private ApplicationContext applicationContext;
public void createProduct() {
System.out.println("상품 생성");
applicationContext.getBean(ProductService.class).logTransaction(); // 프록시를 통해 호출
}
• applicationContext.getBean(ProductService.class)를 사용하면 Spring이 관리하는 프록시 객체를 통해 실행되므로 트랜잭션이 정상적으로 적용됨.
CGLIB Proxy 사용 (proxyTargetClass = true)
JDK Dynamic Proxy는 인터페이스 기반이지만,
CGLIB은 클래스 기반의 프록시를 생성하여 내부 호출도 감지할 수 있음.
설정 방법
@EnableTransactionManagement(proxyTargetClass = true) // CGLIB 프록시 사용
public class AppConfig {
}
CGLIB을 사용하면 내부 호출도 트랜잭션이 적용됨
@Service
@Transactional
public class ProductService {
public void createProduct() {
System.out.println("상품 생성");
logTransaction(); // CGLIB에서는 트랜잭션이 적용됨!
}
@Transactional
public void logTransaction() {
System.out.println("트랜잭션 로그 저장");
}
}
• CGLIB은 클래스를 상속받아 프록시를 생성하므로, 내부 호출(this.logTransaction())도 프록시가 감지할 수 있음.
구분JDK Dynamic ProxyCGLIB Proxy
기반 | 인터페이스 기반 프록시 | 클래스 기반 프록시 |
내부 메서드 호출 | 프록시를 거치지 않음 | 프록시를 거쳐 트랜잭션 적용됨 |
프록시 생성 방식 | Proxy.newProxyInstance() | Enhancer.setSuperclass() |
설정 방법 | 기본적으로 사용됨 | @EnableTransactionManagement(proxyTargetClass = true) 필요 |
JDK Dynamic Proxy는 인터페이스 기반이므로, 내부 메서드 호출 시 트랜잭션이 적용되지 않는다.
다르게 말하면 내부 호출된 메서드에서 엔티티를 변경하거나 저장하려고 해도 영속성 컨텍스트가 활성화되지 않을 수 있으며, 트랜잭션이 관리되지 않아 예상치 못한 동작이 발생할 수 있다.
해결 방법
1. ApplicationContext.getBean()을 사용하여 프록시 객체를 통해 호출.
2. proxyTargetClass = true로 CGLIB을 사용하여 클래스 기반 프록시를 활성화.
'Spring' 카테고리의 다른 글
[Lombok] @Builder를 클래스가 아닌 메서드에 적용하는 이유 (0) | 2025.03.11 |
---|---|
[Spring AOP] @Transactional의 Proxy 작동 방식 (0) | 2025.03.11 |
[Lombok] @Builder, DTO 접근제한자 적절한 설정 방법 (0) | 2025.03.11 |
[Lombok] @Builder를 클래스가 아닌 메서드에 적용하는 이유 (0) | 2025.03.11 |
[Spring/Serialization-Deserialization] Date, LocalDate, LocalDateTime 형변환 방법, 직렬화/역직렬화 문제 해결하기 (0) | 2025.03.07 |