0+ 스프링/0+ 스프링 AOP

[스프링 AOP] 내부 호출 문제 해결

힘들면힘을내는쿼카 2023. 2. 7. 16:32
728x90
반응형

[스프링 AOP] 내부 호출 문제 해결

스프링 핵심 원리 - 고급편 - 인프런 | 강의
이 글은 인프런에서 스프링 핵심 원리 - 고급편 강의를 참고하여 작성했습니다.

 

스프링 핵심 원리 - 고급편 - 인프런 | 강의

스프링의 핵심 원리와 고급 기술들을 깊이있게 학습하고, 스프링을 자신있게 사용할 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

 

 

스프링은 프록시 방식의 AOP를 사용합니다.
따라서 AOP를 적용하기 위해서는 항상 프록시 객체를 통해서 대상 객체(Target)를 호출해야 합니다.

 

AOP를 적용하면 스프링은 대상 객체 대신에 프록시를 빈으로 등록합니다.
(스프링은 의존관계 주입시에 항상 프록시 객체를 주입하기 때문에 대상 객체를 직접 호출하는 문제는 일반적으로 발생하지 않습니다.)

 

하지만, 대상 객체(Target)의 내부에서 메소드 호출이 발생하면
프록시를 거치지 않고 대상 객체를 직접 호출하는 문제가 발생
하게 됩니다.

 

 

무슨말인지 잘 모르겠다고요? 🤷🏻‍♂️

코드로 보여드리겠습니다.

 

AOP 내부 호출 문제 발생 상황

MyLogAspect

package spring.advanced.advancedspring.practice.internalcall.aop;

@Slf4j
@Aspect
public class MyLogAspect {

    @Before("execution(* spring.advanced.advancedspring.practice.internalcall..*.*(..))")
    public void doLog(JoinPoint joinPoint) throws Throwable {
        log.info("[AOP 적용] {}", joinPoint.getSignature());
    }
}

 

MyCallServiceV0

package spring.advanced.advancedspring.practice.internalcall;

@Slf4j
@Component
public class MyCallServiceV0 {
    public void external() {
        log.info("external 호출");
        internal(); // 내부 메소드 호출
    }

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

 

external()MyCallServiceV0의 내부 메소드인 internal()을 호출하는 메소드 입니다.

 

MyCallServiceV0Test

@Slf4j
@SpringBootTest
@Import(MyLogAspect.class)
class MyCallServiceV0Test {

    @Autowired
    MyCallServiceV0 myCallServiceV0;

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

 

external 결과

[AOP 적용] void spring.advanced.advancedspring.practice.internalcall.MyCallServiceV0.external()
external 호출
internal 호출

테스트 결과를 보면 이상합니다.
external()에는 AOP가 잘 적용되었는데, internal()에는 AOP가 적용되지 않았습니다.

 

이것이 앞에서 말씀드린
대상 객체(myCallServiceV0)의 내부에서 메소드 호출(internal())이 발생하면 프록시를 거치지 않고 대상 객체를 직접 호출하는 문제가 발생 한 경우 입니다.

 

internal() 메소드에 문제가 있는걸 까요? 🤔

 

MyCallServiceV0Test

@Slf4j
@SpringBootTest
@Import(MyLogAspect.class)
class MyCallServiceV0Test {

    @Autowired
    MyCallServiceV0 myCallServiceV0;

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

 

internal 결과

[AOP 적용] void spring.advanced.advancedspring.practice.internalcall.MyCallServiceV0.internal()
internal 호출

 

internal() 메소드는 문제가 없습니다…!!

 

🤔 그렇다면 왜?????????? 이러한 문제가 생길까요?

 

AOP 내부 호출 문제 발생 이유

 

1. 프록시 호출
2. MyLogAspect external() 호출
3. MyCallServiceV0 external() 호출
4. MyCallServiceV0 internal() 호출

 

3 -> 4 과정에서 프록시를 통해서 호출하는 것이 아니라
자기 자신이 내부 메소드를 호출하기 때문에 AOP가 적용되지 않습니다.

 

참고
Java 에서 메소드 앞에 별도의 참조가 없으면 this라는 뜻으로 자기 자신의 인스턴스를 가리킵니다.
결과적으로 자기 자신의 내부 메소드를 호출하는 this.internal()이 호출되는데,
this는 실제 대상 객체(MyCallServiceV0)를 의미하게 됩니다.
그래서 프록시를 거치지 않게 되고 어드바이스를 적용할수 없습니다.

 

프록시 방식의 AOP를 사용하는 스프링 방식의 AOP는 내부 호출에 프록시를 적용할 수 없습니다.

 

AOP 내부 호출 문제 해결

그렇다면 어떻게 문제를 해결할 수 있을까요?

  • 자기 자신 의존관계 주입
  • 지연 조회
  • 구조 변경(권장)

 

자기 자신 의존관계 주입

제일 간단한 방법은 자기 자신을 의존관계 주입을 받는 것 입니다.

@Slf4j
@Component
public class MyCallServiceV0 {

    private MyCallServiceV0 myCallServiceV0;

    public MyCallServiceV0(MyCallServiceV0 myCallServiceV0) {
        this.myCallServiceV0 = myCallServiceV0;
    }

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

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

 

하지만 위와 같은 방법은 생성자 주입은 순환 사이클을 만들기 때문에 실패합니다.


그러면 생성자 주입 말고 수정자를 통해서 주입 받으면 되지 않을까?

@Slf4j
@Component
public class MyCallServiceV0 {

    private MyCallServiceV0 myCallServiceV0;
	
    // 수정자 주입
    @Autowired
    public void setMyCallService(MyCallServiceV0 myCallServiceV0) {
        this.myCallServiceV0 = myCallServiceV0;
    }

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

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

 

스프링 부트 2.6 부터는 순환 참조를 기본적으로 금지하도록 정책이 변경 되었습니다.
따라서 위와 같은 방법은 권장하지 않는 방식 입니다.

 

지연 조회

지연 조회를 사용하는 방법도 있습니다.
ObjectProvider(Provider) 사용

/**
* ObjectProvider(Provider), ApplicationContext를 사용해서 지연(LAZY) 조회 
*/ 
@Slf4j
@Component
@RequiredArgsConstructor
public class MyCallServiceV1 {

    private final ObjectProvider<MyCallServiceV1> myCallServiceV1Provider;

    public void external() {
        log.info("external 호출");
        // 지연 조회
        MyCallServiceV1 myCallServiceV1 = myCallServiceV1Provider.getObject();
        myCallServiceV1.internal(); // 외부 메소드 호출
    }

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

 

ApplicationContext 사용

@Slf4j
@Component
@RequiredArgsConstructor
public class MyCallServiceV1 {

    private final ApplicationContext applicationContext;

    public void external() {
        log.info("external 호출");
        // 지연 조회
        MyCallServiceV1 myCallServiceV1 = applicationContext.getBean(MyCallServiceV1.class);
        myCallServiceV1.internal(); // 외부 메소드 호출
    }

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

 

결과

[AOP 적용] void spring.advanced.advancedspring.practice.internalcall.MyCallServiceV1.external()
external 호출
[AOP 적용] void spring.advanced.advancedspring.practice.internalcall.MyCallServiceV1.internal()
internal 호출

myCallServiceV1.internal() 호출시에도 스프링 AOP가 잘 동작하는 것을 확인 할 수 있습니다.
하지만 ApplicationContext를 사용하거나 ObjectProvider<T>를 사용하는 것처럼 조금 어색한 모습이 있습니다.

 

구조 변경

가장 나은 대안은 내부 호출이 발생하지 않도록 구조를 변경하는 것이라고 생각합니다.
(지금 내부 호출 때문에 스프링 AOP가 적용되지 않았기 때문입니다.)

MyCallInternalService

@Slf4j
@Component
public class MyCallInternalService {

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

 

MyCallExternalService

@Slf4j
@Component
@RequiredArgsConstructor
public class MyCallExternalService {

    private final MyCallInternalService myCallInternalService;

    public void external() {
        log.info("external 호출");
        myCallInternalService.internal();
    }
}

 

결과

[AOP 적용] void spring.advanced.advancedspring.practice.internalcall.MyCallExternalService.external()
external 호출
[AOP 적용] void spring.advanced.advancedspring.practice.internalcall.MyCallInternalService.internal()
internal 호출

 

내부 호출 자체가 사라지고,
MyCallExternalService -> MyCallInternalService를 호출하는 구조로 변경 했습니다.
이러한 구조 변경 덕분에 자연스럽게 AOP가 호출 됩니다..!

 

참고
AOP는 주로 트랜잭션 적용이나 주요 컴포넌트의 로그 출력 기능에 사용됩니다.
인터페이스에 메소드가 나올 정도의 규모에 AOP를 적용하는 것이 적절합니다.
(일반적으로 private 메소드 같은 작은 단위에는 AOP를 적용하지 않습니다.)

 

정리

AOP가 잘 적용되지 않으면 내부 호출을 의심해봅시다.! 👀

 

 

 

728x90
반응형

'0+ 스프링 > 0+ 스프링 AOP' 카테고리의 다른 글

[스프링 AOP] 프록시 타입 캐스팅 한계  (0) 2023.02.07
[스프링 AOP] 포인트컷 지시자  (0) 2023.02.03
[스프링 AOP] 실습  (0) 2023.01.31
[스프링 AOP] 개념  (0) 2023.01.30