[Spring] Spring AOP 내부 메서드 호출시 문제해결

Kim Dae Hyun·2022년 3월 1일
4

Spring-AOP

목록 보기
6/6

특정 클래스에 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인 것 입니다.


📌 내부 메서드 호출시 AOP가 적용되지 않는 문제 테스트

아래와 같이 메서드 호출에 앞서 메서드 시그니처를 로그로 찍는 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 메서드에도 메서드 시그니처 로그가 남겨진 것을 확인할 수 있습니다.


📌 직접 등록된 Bean을 조회 후 사용하는 방법 (지연조회) 👍

자기 자신을 주입받는 방법보다는 좋다고 생각되는 방법입니다.

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% 참고하여 정리한 내용입니다.
강의자료를 그대로 가져온 것은 아니니 보다 정확한 정보를 원한다면 강의를 들어주세요. (강추!)
인프런 - 스프링 핵심 원리 고급편 (김영한 님)

profile
좀 더 천천히 까먹기 위해 기록합니다. 🧐

0개의 댓글