Spring

프록시의 한계와 해결방안(내부 호출)

쭈녁 2024. 1. 13. 17:39

 

 

Spring AOP의 특징

 

 

앞에서 알아본 바와 같이 Spring의 AOP는 프록시 기술을 기반으로 한다.

따라서 AOP를 적용하면 항상 프록시를 통해 Target의 메서드를 실행하게 된다. 

 

Spring은 또한 AOP를 적용하면 대상 객체 대신에 프록시를 스프링 빈으로 등록한다.

따라서 스프링은 의존관계 주입시에 항상 프록시 객체를 주입한다.

 

실제 객체를 직접 호출하는 경우는 없지만 대상 객체의 내부에서 메서드 호출이 발생하면 프록시를 거치지 않고 대상 객체를 직접 호출하는 문제가 발생한다.

 

 

코드 예시

 

Advicer 코드

@Aspect
@Slf4j
public class CallLogAspect {
	// 메서드를 실행하면 해당 메서드 시그니처(메서드 이름)을 로그로 남기는 코드
    @Before("execution(* hello.aop.internalcall..*.*(..))")
    public void log(JoinPoint joinPoint) {
        log.info("aop={}", joinPoint.getSignature());
    }
}

 

내부 호출 되는 서비스 단

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

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

 

테스트 코드

@SpringBootTest
@Slf4j
@Import(CallLogAspect.class)
class CallServiceV0Test {
    @Autowired
    CallServiceV0 callServiceV0;

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

/* ---->결과
	 h.aop.internalcall.aop.CallLogAspect     : aop=void hello.aop.internalcall.CallServiceV0.external()
	 hello.aop.internalcall.CallServiceV0     : call external
	 hello.aop.internalcall.CallServiceV0     : call internal
*/

 

external 안에서 호출되는 internal 코드에는 AOP가 적용되지 않은 것을 볼 수 있다.

 

이러한 내부 호출의 문제를 해결하기 위한 3가지 방법은 아래와 같다.

 

 

해결 방안

 

1. 자기 자신 의존 관계 부여 방법

  • 이 경우 생성자로 주입시 오류가 발생한다. 객체 자신을 생성하면서 주입해야 하기 때문에 순환 사이클이 만들어진다.
  • 반면에 수정자 주입은 스프링이 생성된 이후에 주입할 수 있기 때문에 오류가 발생하지 않는다.
  • 하지만 결국 자기 자신을 의존하기 때문에 순환 사이클이 발생할 가능성이 있다.

내부 호출 코드

@Component
@Slf4j
public class CallServiceV1 {
	//자기 자신을 의존하도록 맴버변수로 넣고
    private CallServiceV1 callServiceV1;

    //setter를 통한 의존관계 부여
    @Autowired
    public void setCallServiceV1(CallServiceV1 callServiceV1) {
        log.info("callServiceV1 Setter = {}",callServiceV1.getClass());
        this.callServiceV1 = callServiceV1;
    }

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

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

 

테스트 코드

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

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

/* 
1. 빈 컨테이너 등록 단계 : 빈 생성 후 setter를 통해 자기 자신에 대한 의존관계를 부여 받음
    hello.aop.internalcall.CallServiceV1     : callServiceV1 Setter = class hello.aop.internalcall.CallServiceV1$$SpringCGLIB$$0

2. 테스트 결과
    aop=void hello.aop.internalcall.CallServiceV1.external()
    hello.aop.internalcall.CallServiceV1     : call external
    h.aop.internalcall.aop.CallLogAspect     : aop=void hello.aop.internalcall.CallServiceV1.internal()
    hello.aop.internalcall.CallServiceV1     : call internal

*/

 

생성자에 넣은 setter로그가 확인됨 -> 이는 의존관계가 프록시 객체로 받아졌음을 의미함.

내부 호출도 AOP가 적용됨을 볼 수 있다

 

 

 

2. 지연 조회

  • 스프링 빈을 사용하는 시점에 조회하도록 설정 (지연 조회)하여 프록시 객체로 가져오도록 구성한다.
  • ObjectProvider(Provider) , ApplicationContext 를 사용.

내부 호출 코드

@Component
@Slf4j
@RequiredArgsConstructor
public class CallServiceV2 {
    //    private final ApplicationContext applicationContext;
    private final ObjectProvider<CallServiceV2> callServiceV2Provider;


    public void external() {
        log.info("call external");
//        applicationContext.getBean(CallServiceV2.class).internal();     //내부 메서드 호출(this.internal())
        callServiceV2Provider.getObject().internal();
    }

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

 

 

테스트 코드

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

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

/* --> 결과
h.aop.internalcall.aop.CallLogAspect     : aop=void hello.aop.internalcall.CallServiceV2.external()
hello.aop.internalcall.CallServiceV2     : call external
h.aop.internalcall.aop.CallLogAspect     : aop=void hello.aop.internalcall.CallServiceV2.internal()
hello.aop.internalcall.CallServiceV2     : call internal
*/

 

사용 시점에 ObjectProvider에서 CallServiceV2의 Bean을 가져와 쓰기 때문에 순환 사이클이 발생하지 않는다.

 

 

3. 구조의 변경

  • 내부에서 호출되던 메서드를 따로 빼내어 AOP가 적용되는 클래스로 빼고 의존관계를 부여한 후 호출한다.

서비스 코드

@Component
@Slf4j
@RequiredArgsConstructor
public class CallServiceV3 {
    private final InternalService internal;
    /**
     * 구조 분리
     */

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

 

내부에서 호출되던 코드의 분리

@Slf4j
@Component
public class InternalService {
    public void internal() {
        log.info("call internal");
    }
}

 

테스트 코드

@SpringBootTest
@Import(CallLogAspect.class)
class CallServiceV3Test {
    @Autowired
    CallServiceV3 callServiceV3;
    @Test
    void external() {
        callServiceV3.external();
    }
}
/* --> 결과
h.aop.internalcall.aop.CallLogAspect     : aop=void hello.aop.internalcall.CallServiceV2.external()
hello.aop.internalcall.CallServiceV2     : call external
h.aop.internalcall.aop.CallLogAspect     : aop=void hello.aop.internalcall.CallServiceV2.internal()
hello.aop.internalcall.CallServiceV2     : call internal
*/

 

내부에서 호출되던 메서드가 분리되었음으로 호출될 때 자기 자신에게 적용된 AOP를 적용받고 호출받는다.

 

 

코드 출처

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

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B3%A0%EA%B8%89%ED%8E%B8