특정 클래스에 AOP를 적용했을 때 아래와 같은 구조에서 발생 가능한 문제입니다.
@Slf4j
@Component
public class CallServiceV0 {
public void external() {
log.info("called external!!");
internal();
}
public void internal() {
log.info("called internal!!");
}
}
위 클래스의 모든 메서드를 JoinPoint로 하는 상황입니다.
문제는 external
메서드에서 internal
메서드를 호출할 때 발생합니다.
external
호출시 정의한 Advice
로직이 정상적으로 적용됩니다.
하지만 external
에서 internal
을 호출하면 Advice
로직이 적용되지 않습니다.
이는 Spring AOP
가 프록시 패턴으로 동작하기 때문에 생기는 문제입니다.
프록시 패턴이기 때문에
external
을 호출할 때 real-subject
가 호출되기 전에 프록시 객체
를 호출합니다.
프록시 객체
는 자신의 로직을 수행하고 real-subject
를 호출합니다.
결국 internal
을 호출하는 것은 프록시가 아닌 real-subject
인 것 입니다.
아래와 같이 메서드 호출에 앞서 메서드 시그니처를 로그로 찍는 Aspect
클래스를 정의합니다.
@Slf4j
@Component
@Aspect
public class CallLogAspect {
@Before("execution(* com.aop.lab3.internalcall.*..*(..))")
public void doLog(JoinPoint joinPoint) {
log.info("call logAop: {}", joinPoint.getSignature());
}
}
com.aop.lab3.internalcall
이하 모든 메서드에 AOP가 적용되야 합니다.
위 AOP를 적용할 클래스를 정의합니다.
package com.aop.lab3.internalcall;
@Slf4j
@Component
public class CallServiceV0 {
public void external() {
log.info("called external!!");
internal();
}
public void internal() {
log.info("called internal!!");
}
}
위 메서드를 호출할 테스트 클래스를 작성하고 테스트를 수행합니다.
@Import({CallLogAspect.class})
@Slf4j
@SpringBootTest
class CallServiceV0Test {
@Autowired
CallServiceV0 callServiceV0;
@DisplayName("external 호출 테스트")
@Test
void externalCallTest() {
callServiceV0.external();
}
}
테스트 결과 external
메서드 호출에 앞서 AOP가 적용되었지만 내부 호출된 internal
메서드에는 AOP가 적용되지 않았습니다.
실무에서 AOP를 도입할 때 충분히 만날 수 있는 문제이고 추적하며 원인을 찾는 것이 쉽지 않다고 합니다.
위 문제를 피할 수 있는 방법 두 가지가 있습니다.
자기 자신을 주입받아서 내부 호출되는 메서드를 마치 외부에서 호출되는 것 처럼 처리하는 방법입니다.
자기 자신을 주입받을 때 주의해야 합니다.
보통의 DI처럼 권장되는 대로 생성자 주입을 한다면 순환참조 예외를 만나게 됩니다.
컴포넌트 스캔으로 등록된 빈을 런타임에 주입받도록 하기 위해 Setter를 이용해서 의존성을 주입받도록 합니다.
@Slf4j
@Component
public class CallServiceV1 {
private CallServiceV1 callServiceV1; // 자기 자신 주입
@Autowired
public void setCallServiceV1(CallServiceV1 callServiceV1) {
log.info("setCallServiceV1={}", callServiceV1.getClass());
this.callServiceV1 = callServiceV1;
}
public void external() {
log.info("called external!!");
callServiceV1.internal(); // 자기 자신을 주입받아 메서드 호출
}
public void internal() {
log.info("called internal!!");
}
}
CallServiceV1
을 주입받으면 프록시 객체가 주입되므로 AOP가 적용된 internal
메서드 호출의 결과를 볼 수 있을 것 입니다.
동일한 테스트를 수행했을 때 internal
메서드에도 메서드 시그니처 로그가 남겨진 것을 확인할 수 있습니다.
자기 자신을 주입받는 방법보다는 좋다고 생각되는 방법입니다.
ObjectProvider<T>
를 이용해서 스프링 컨테이너에 등록된 Bean을 조회하고, 조회된 Bean을 통해 메서드를 호출하는 것으로 내부 메서드 호출 문제를 해결하는 방식입니다.
@Slf4j
@Component
public class CallServiceV2 {
private final ObjectProvider<CallServiceV2> callServiceV2Provider;
public CallServiceV2(ObjectProvider<CallServiceV2> callServiceV2Provider) {
this.callServiceV2Provider = callServiceV2Provider;
}
public void external() {
log.info("called external!!");
CallServiceV2 callServiceV2 = callServiceV2Provider.getObject();
callServiceV2.internal();
}
public void internal() {
log.info("called internal!!");
}
}
이 방식은 external
메서드가 호출되는 시점에 Bean을 조회하게 됩니다. (지연)
문제를 해결하는 원리는 자기 자신을 주입받는 것과 동일하다고 생각됩니다.
내부 호출을 마치 외부 호출처럼 !
테스트 결과 internal
메서드도 AOP가 적용되는 것을 확인할 수 있습니다.
아래 강의를 100% 참고하여 정리한 내용입니다.
강의자료를 그대로 가져온 것은 아니니 보다 정확한 정보를 원한다면 강의를 들어주세요. (강추!)
인프런 - 스프링 핵심 원리 고급편 (김영한 님)