본문 바로가기

Spring Boot

스프링 AOP의 내부호출 문제 및 해결법

728x90

내부 호출

스프링은 프록시 방식의 AOP를 사용합니다. 따라서, AOP를 적용하려면 항상 프록시를 통해서 대상 객체(Target)를 호출해야 합니다. 프록시를 거치지 않고 대상 객체를 직접 호출하면, AOP가 적용되지 않고 Advice도 호출되지 않습니다.

즉, 대상 객체의 내부에서 메서드 호출이 발생하면 프록시를 거치지 않게 됩니다. 코드를 통해 확인해 보겠습니다.


package hello.aop.internalcall;

@Slf4j
@Component
public class CallServiceV0 {

    public void external() {
        log.info("call external");
        internal(); //내부 메서드 호출(this.internal())
    }

    public void internal() {
        log.info("call internal");
    }
}



package hello.aop.internalcall.aop;

@Slf4j
@Aspect
public class CallLogAspect { // aspect

    @Before("execution(* hello.aop.internalcall..*.*(..))")
    public void doLog(JoinPoint joinPoint) {
        log.info("aop={}", joinPoint.getSignature());
    }
}

 

위 코드에서 hello.aop.internalcall 패키지 내부의 클래스(CallService0.class)에 advice를 적용하고 있습니다. 테스트 코드를 통해 어떤 메서드들에 advice가 적용되는지 확인해 보겠습니다.


@Import(CallLogAspect.class) // CallLogAspect를 스프링 빈으로 등록
@SpringBootTest(classes = CallServiceV0.class)
class CallServiceV0Test {
    @Autowired
    CallServiceV0 callServiceV0;

    @Test
    void external(){
        callServiceV0.external(); 
    }

    @Test
    void internal(){
        callServiceV0.internal();
    }
}

 

위 코드에서 CallService0 클래스의 external(), internal() 메서드를 각각 호출하고 있습니다. 위 테스트 코드를 실행해 보겠습니다.

external() 테스트 실행
internal() 클래스 실행

 

실행 결과를 보면 callServiceV0.external(), callServiceVO.internal()을 각각 실행할 때는 프록시를 호출해서 external 메서드와 internal 메서드에 CallLogAspect 어드바이스가 호출된 것을 확인할 수 있습니다.

그런데 여기서 문제는 첫 번째 그림의 callServiceV0.external() 안에서 internal() 을 호출할 때입니다. 이 때는 CallLogAspect 어드바이스가 호출되고 있지 않습니다. 이렇게 CallServiceVO의 external() 안에서 internal()을 호출하는 것이 바로 내부 호출입니다. 

결과적으로 이러한 내부 호출은 프록시를 거치지 않기 때문에 어드바이스도 적용할 수 없습니다.

 

 

 

 

이런 내부 호출을 해결할 수 있는 3가지 방법에 대해서 알려드리겠습니다.


방법 1 - 자기 자신 주입

아래 코드처럼 자기 자신을 의존관계로 주입 받아서 해결할 수 있습니다.


  

@Slf4j
@Component
public class CallServiceV1 {

    private CallServiceV1 callServiceV1;

   // 자기 자신은 꼭 setter로 주입해야 함!! 
   // 생성자 주입은 순환 사이클을 만드므로 실패함
    @Autowired
    public void setCallServiceV1(CallServiceV1 callServiceV1) {
        this.callServiceV1 = callServiceV1;
    }

    public void external() {
        log.info("call external");
        callServiceV1.internal(); //외부 메서드 호출
    }

    public void internal() {
        log.info("call internal");
    }
}

 

이렇게 주입받으면, 스프링에서 AOP가 적용된 대상을 의존 관계로 주입받기 때문에 그 대상은 실제 자신이 아닌 프록시 객체입니다.  따라서, callServiceV1.internal()을 호출하면 주입받은 callServiceV1은 프록시이므로 프록시를 통해 AOP를 적용할 수 있습니다.

 

아래 테스트를 실행하면 external, internal 모두에 advice가 적용됨을 확인할 수 있습니다.


      

@Import(CallLogAspect.class)
@SpringBootTest
class CallServiceV1Test {
    @Autowired
    CallServiceV1 callServiceV1; 

    @Test
    void external(){
        callServiceV1.external();
    }

}

// 스프링 2.6부터는 순환 참조 기본적으로 금지하므로
// application.properties에 spring.main.allow-circular-references=true 옵션 명시 필요

 

테스트 실행

 

호출 flow


방법 2 - 지연 조회

스프링 컨테이너에서 빈을 조회하는 것스프링 빈 생성 시점이 아니라 실제 객체를 사용하는 시점으로 지연하는 방법입니다. 이는 ObjectProvider라는 클래스를 통해 구현할 수 있습니다. 


@Slf4j
@Component
public class CallServiceV2 {

    private final ObjectProvider<CallServiceV2> callServiceProvider;

    public CallServiceV2(ObjectProvider<CallServiceV2> callServiceProvider) {
        this.callServiceProvider = callServiceProvider;
    }

    public void external() {
        log.info("call external");
        // getObject() 를 호출하는 시점에 스프링 컨테이너에서 빈을 조회
        CallServiceV2 callServiceV2 = callServiceProvider.getObject(); 
        callServiceV2.internal(); //외부 메서드 호출
    }

    public void internal() {
        log.info("call internal");
    }
}

 

 

아래 테스트 코드로 external, internal 모두에 Advice가 적용되는 것을 확인할 수 있습니다.


@Import(CallLogAspect.class)
@SpringBootTest
class CallServiceV2Test {
    @Autowired
    CallServiceV2 callServiceV2;

    @Test
    void external() {
        callServiceV2.external();
    }
}

 


방법 3 - 구조 변경

가장 나은 대안으로, 내부 호출이 발생하지 않도록 구조를 변경합니다. 즉, internal 메서드를 외부 클래스로 분리하여 구조를 변경하는 방법입니다.


@Slf4j
@Component
@RequiredArgsConstructor
public class CallServiceV3 {

    private final InternalService internalService;

    public void external() {
        log.info("call external");
        internalService.internal(); //외부 메서드 호출
    }

}


// InternalService라는 별도 클래스로 분리
@Slf4j
@Component
public class InternalService {

    public void internal() {
        log.info("call internal");
    }

}

 

 

 

아래 테스트 코드를 수행하면 external, internal 메서드 모두에 Advice가 적용된 것을 확인할 수 있습니다.


@Import(CallLogAspect.class)
@SpringBootTest(classes = CallServiceV3.class)
class CallServiceV3Test {
    @Autowired
    CallServiceV3 callServiceV3;

    @Test
    void external() {
        callServiceV3.external();
    }
}

 

즉, 내부 호출 자체가 사라지고 callService가 internalService를 호출하는 구조로 변경되었습니다. 

호출 flow

 

혹은, 구조 변경 없이, client 자체에서 external()과 internal()을 각각 호출하는 방식으로도 해결할 수 있습니다. 즉, 방법 1의 테스트 코드처럼 외부에서 external(), internal()을 호출하도록 하면 두 메서드에 모두 AOP가 적용됩니다.

 

소스코드

SpringAOP/advanced/src/main/java/hello/aop/internalcall at main · eunhwa99/SpringAOP (github.com)

 

SpringAOP/advanced/src/main/java/hello/aop/internalcall at main · eunhwa99/SpringAOP

SpringAOP 구현 코드. Contribute to eunhwa99/SpringAOP development by creating an account on GitHub.

github.com

 

 


[참고자료]

김영한, "스프링 핵심 원리 - 고급편 ", 인프런

 

728x90
반응형