실전 고수 팁 — 실무 AOP 트러블슈팅: 내부 메서드 호출 문제
내부 메서드 호출(Self-Invocation) 시 AOP가 무시되는 치명적 버그
스프링의 프록시(Proxy) 기반 AOP 시스템을 처음 설계할 때 시니어 개발자들도 종종 놓치는 버그가 바로 **"단일 인스턴스 내부에서 자기 자신의 다른 메서드를 직접 호출할 때 AOP 로직이 완전히 증발해버리는 현상"**입니다.
@Service
public class PaymentService {
// AOP 적용 대상 (@Transactional, @Cacheable 등)
@Transactional
public void payInternal() {
// 결제 DB 반영 등 반드시 트랜잭션이 필요한 중요 비즈니스
}
public void checkout() {
// 내부 다른 메서드 직접 호출 (Self-Invocation)
this.payInternal();
}
}
외부 컨트롤러가 paymentService.checkout()을 찔러 넣을 때, checkout() 내부에 위치한 this.payInternal() 호출 구문은 외부 스프링 프록시 객체의 껍질을 전혀 경유하지 않습니다. 그 이유는 이미 실행 위치가 프록시 내부 깊숙한 "순수 Target 원본 객체(this)" 컨텍스트 안으로 진입했기 때문입니다.
결과적으로 가로채기가 통째로 무산되며, @Transactional AOP는 발동을 실패하여 로그인 쿼리 도중 장애가 발생해도 트랜잭션 롤백이 되지 않는 극한의 대형 데이터 장애로 직결됩니다.
💡 실무 해결책 2가지
-
Service 분리 설계 (가장 권장되는 정석 아키텍처)
- 내부 클래스 호출이 일어나는 강결합 로직 자체를 구조적으로 해체해, 아예 완벽히 분리된 "다른 빈(Bean)" 객체의 메서드를 간접 호출하도록 철저히 리팩토링합니다. 통상
PaymentProcessor,PaymentValidator,PaymentCoreService등 Facade 패턴으로 책임을 영구 이관합니다.
- 내부 클래스 호출이 일어나는 강결합 로직 자체를 구조적으로 해체해, 아예 완벽히 분리된 "다른 빈(Bean)" 객체의 메서드를 간접 호출하도록 철저히 리팩토링합니다. 통상
-
자기 자신을 순환 주입 (긴급 우회 패치 기법)
- 자기 자신 타입의 빈을 자기 자신에게 주입(
@Lazy필요)받아, 강제로 프록시 빈을 타고 우회 호출되게 만듭니다.
- 자기 자신 타입의 빈을 자기 자신에게 주입(
@Service
public class PaymentService {
private PaymentService self;
// 순환 참조(Circular Dependency) 방어 목적으로 반드시 @Lazy 적용 필요
@Autowired
public void setPaymentService(@Lazy PaymentService self) {
this.self = self;
}
@Transactional
public void payInternal() { /* ... */ }
public void checkout() {
// 순수 this.payInternal()을 버리고 프록시 객체인 self 경유를 강제함
self.payInternal();
}
}
팁
자기 자신의 프록시 우회 주입 기법은 스프링 컨테이너의 순환 참조 복잡도를 극심하게 높이기 때문에 최후의 비상 수단으로만 한정해야 하며, 장기적으로는 반드시 1번 방법인 독립 설계 리팩토링을 수행해야 건강한 아키텍처를 사수할 수 있습니다.