스터디/Spring

[SPRING 고급] 프록시와 내부 호출, 프록시 기술 한계

혜유우 2025. 1. 8. 01:55
프록시와 내부 호출-문제

스프링은 프록시 방식의 AOP를 사용한다. 따라서 AOP를 적용하려면 항상 프록시를 통해서 대상 객체(target)을 호출해야 한다. 이렇게 해야 프록시에서 먼저 어드바이스를 호출하고 이후에 대상 객체를 호출한다.

만약 프록시를 거치지 않고 대상 객체를 직접 호출하게 되면 AOP가 적용되지 않고 어드바이스도 호출되지 않는다.

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

 

AOP proxy는 target.external()을 호출한다. 그런데 여기서 문제는 callServiceV0.external() 안엥서 internal()을 호출할 때 발생한다. 

이떄는 CallLogAspect 어드바이스가 호출되지 않는다. 자바 언어에서 메서드 앞에 별도의 참조가 없으면 this라는 뜻으로 자기 자신의 인스턴스를 가리킨다. 

결과적으로 자기 자신의 내부 메서드를 호출하는 this.internal()이 되는데, 여기서 this는 실제 대상 객체(target)의 인스턴스를 뜻한다.

이러한 내부 호출은 프록시를 거치지 않는다. 따라서 어드바시으도 적용할 수 없다.

 

프록시 방식의 AOP 한계

스프링은 프록시 방식의 AOP를 사용한다. 프록시 방식의 AOP는 메서드 내부 호출에 프록시를 적용할 수 없다. 

 

 

해결1. 자기 자신 주입

 

 

/**
* 참고: 생성자 주입은 순환 사이클을 만들기 때문에 실패한다. */
@Slf4j
 @Component
 public class CallServiceV1 {
      private CallServiceV1 callServiceV1;
     @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");
         }
  }

 

해결2. 지연 조회

생성자 주입이 실패하는 이유는 자기 자신을 생성하면서 주입해야 하기 떄문이다. 이 경우 수정자 주입을 사용하거나 지금부터 설명하는 지연조회를 사용하면 된다. ObjectProvider(provider), ApplicationContext를 사용하면 된다.

ApplicationContext는 너무 많은 기능을 제공한다. ObjectProvider는 객체를 스프링 컨테이너에서 조회하는 것을 스프링 빈 생성 시점이 아니라 실제 객체를 사용하는 시점으로 지연할 수 있다. callServiceProvider.getObject()를 호출하는 시점에 스프링 컨테이너에서 빈을 조회한다. 여기서는 자기 자신을 주입 받는 것이 아니기 때문에 순환 사이클이 발생하지 않는다.

 

 

해결3. 구조 변경 (권장)

클라이언트 -> external()

클라이언트 -> internal()

 

AOP는 주로 트랜잭션 적용이나 주요 컴포넌트의 로그 출력 기능에 사용된다. 인터페이스에 메서드가 나올 정도의 규모에 AOP를 적용하는 것이 적당하다. AOP는 public 메서드에만 적용한다. private 메서드처럼 작은 단위에는 AOP를 적용하지 않는다.

AOP 적용을 위해 private 메서드를 외부 클래스로 변경하고 public으로 변경하는 일은 거의 없다. 그러나 public 메서드에서 public 메서드를 내부 호출하는 경우에는 문제가 발생한다. 

 

📍JDK 동적 프록시 한계

인터페이스 기반으로 프록시를 생성하는 JDK 동적 프록시는 구체 클레스로 타입 캐스팅이 불가능한 한계가 있다. 

 

JDK Proxy를 대상 클래스인 MemberServiceImpl 타입으로 캐스팅 하려고 하니 예외가 발생

왜냐하면 JDK 동적 프록시는 인터페이스를 기반으로 프록시를 생성하기 때문이다. JDK Proxy는 MemberService 인터페이스를 기반으로 생성된 프로기이다. 따라서 JDK Proxy는 MemberService로 캐스팅은 가능하지만 MemberServiceImpl이 어떤 것인지 전혀 알지 못한다.

따라서 MemberServiceImpl 타입으로는 캐스팅이 불가능하다.

 

CGLIB Proxy를 대상 클래스인 MemberServiceImpl 타입으로 캐스팅하며녀 성공한다. CGLIB는 구체 클레스를 기반으로 프록시를 생성하기 때문이다. 

JDK 동적 프록시는 대상 객체인 MemberServiceImpl로 캐스팅 할 수 없다.

CGLIB 프록시는 대상 객체인 MemberServiceImpl로 캐스팅 할 수 있다.

 

📍의존관계 주입 문제(JDK Proxy)

1) JDK Proxy

  • @Autowired MemberService memberService: 이 부분은 문제가 없다. JDK Proxy는 MemberService 인터페이스를 기반으로 만들어진다. 따라서 해당 타입으로 캐스팅 할 수 있다. MemberService=JDK Proxy가 성립
  • @Autowired MemberServiceImpl memberServiceImpl: 문제는 여기다. JDK proxy는 MemberService 인터페이스를 기반으로 만들어진다. 따라서 MemberServiceImpl 타입이 뭔지 전혀 모른다. 그래서 해당 타입에 주입할 수 없다. MemberServiceImpl=JDK Proxy가 성립하지 않는다.

2) CGLIB Proxy

  • @Autowired MemberService memberService: CGLIB Proxy는 MemberServiceImpl 구레 클레스를 기반으로 만들어진다. MemberService=CGLIB Proxy가 성립
  • @Autowired MemberServiceImpl memberServiceImpl: CGLIB Proxy는 MemberServiceImpl 구체 클레스를 기반으로 만들어진다. MemberServiceImpl=CGLIB Proxy가 성립

JDK 동적 프록시는 대상 객체인 MemberServiceImpl 타입에 의존관계를 주입할 수 없다

CGLIB 프록시는 대상 객체인 MemberServiceImpl 타입에 의존관계 주입을 할 수 있다. 

 

실제로 개발할 때는 인터페이스가 있으면 인터페이스를 기반으로 의존관계 주입을 받는 것이 맞다.

하지만 여러 이유로 AOP 프록시가 적용된 구체 클래스를 직접 의존관계 주입 받아야 하는 경우가 있을 수 있다. 이때는 CGLIB를 통해 구체 클래스 기반으로 AOP 프록시를 적용하면 된다.

 

📍CGLIB 구체 클래스 기반 프록시 문제점

1. 대상 클래스에 기본 생성자 필수

CGLIB를 사용할 때 CGLIB가 만드는 프록시의 생성자는 우리가 호출하는 것이 아니다. CGLIB 프록시는 대상 클래스를 상속 받고, 생성자에서 대상 클래스의 기본 생성자를 호출한다. 따라서 대상 클래스에 기본 생성자를 만들어야 한다.

 

2. 생성자 2번 호출 문제

CGLIB는 구체 클래스로 상속 받는다. 자바 언어에서 상속을 받으면 자식 클래스의 생성자를 호출할 때 부모 클래스의 생성자도 호출해야 한다.

1) 실제 target의 객체를 생성할 때

2) 프록시 객체를 생성할 때 부모 클래스의 생성자 호출

 

3. final 키워드 클래스 메서드 사용 불가

final 키워드가 클래스에 있으면 상속이 불가능하고, 메서드에 있으면 오버라이딩이 불가능하다.

CGLIB는 상속을 기반으로 하기 때문에 두 경우 프록시가 생성되지 않거나 정상 동작하지 않는다.

 

📍스프링의 해결책

스프링 3.2, CGLIB를 스프링 내부에 함께 패키징

CGLIB 기본 생성자 필수 문제 해결

생성자 2번 호출 문제

스프링부트 2.0-CGLIB 기본 사용

스프링부트 2.0 버전부터 CGLIB를 기본으로 사용하도록 했다. 스프링부트는 별도의 설정이 없다면 AOP를 적용할 때 기본적으로 proxyTargetClass=true로 설정해서 사용한다. 따라서 인터페이스가 있어도 JDK 동적 프록시를 사용하는 것이 아니라 항상 CGLIB를 사용해서 구체클래스를 기반으로 프록시를 생성한다.