2 분 소요

 인프런 스프링 핵심 원리 - 고급편을 학습하고 정리한 내용 입니다.

프록시 기술과 한계 - 타입 캐스팅

JDK 동적 프록시와 CGLIB를 사용해서 AOP 프록시를 만드는 방법에는 각각 장단점이 있다.

JDK 동적 프록시는 인터페이스가 필수이고, 인터페이스를 기반으로 프록시를 생성한다.
CGLIB는 구체 클래스를 기반으로 프록시를 생성한다.

물론 인터페이스가 없고 구체 클래스만 있는 경우에는 CGLIB를 사용해야 한다. 그런데 인터페이스가 있는 경우에는 JDK 동적 프록시나 CGLIB 둘중에 하나를 선택할 수 있다.

스프링이 프록시를 만들때 제공하는 ProxyFactoryproxyTargetClass옵션에 따라 둘중 하나를 선택해서 프록시를 만들 수 있다.

  • proxyTargetClass=false JDK 동적 프록시를 사용해서 인터페이스 기반 프록시 생성
  • proxyTargetClass=true CGLIB를 사용해서 구체 클래스 기반 프록시 생성
  • 참고로 옵션과 무관하게 인터페이스가 없으면 JDK 동적 프록시를 적용할 수 없으므로 CGLIB를 사용한다.

JDK 동적 프록시 한계

인터페이스 기반으로 프록시를 생성하는 JDK 동적 프록시는 구체 클래스로 타입 캐스팅이 불가능한 한계가 있다. 어떤 한계인지 코드를 통해서 알아보자.

ProxyCastingTest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Slf4j  
public class ProxyCastingTest {  
  
    @Test  
    void jdkProxy() {  
        MemberServiceImpl target = new MemberServiceImpl();  
  
        ProxyFactory proxyFactory = new ProxyFactory(target);  
        proxyFactory.setProxyTargetClass(false); // JDK 동적 프록시  
  
        //프록시를 인터페이스로 캐스팅 -> 성공  
        MemberService memberServiceProxy = (MemberService) proxyFactory.getProxy();  
  
        log.info("proxy class={}", memberServiceProxy.getClass());  
  
        // JDK 동적 프록시를 구현 클래스로 캐스팅하면 실패함. ClassCastException 예외 발생  
        assertThatThrownBy(()-> {  
            MemberServiceImpl castingMemberService = (MemberServiceImpl) memberServiceProxy;  
        }).isInstanceOf(ClassCastException.class);  
    }  
}

여기서는 MemberServiceImpl타입을 기반으로 JDK 동적 프록시를 생성했다. MemberServiceImpl 타입은 MemberService 인터페이스를 구현한다. 따라서 JDK 동적 프록시는 MeberService 인터페이스 기반으로 프록시를 생성한다. 이 프록시를 JDK Proxy라고 하자. 여기서 memberServiceProxy가 바로 JDK Proxy이다.

그런데 여기서 JDK Proxy를 대상 클래스인 MemberServiceImpl 타입으로 캐스팅 하려고 하니 예외가 발생한다.

왜냐하면 JDK 동적 프록시는 인터페이스를 기반으로 프록시를 생성하기 때문이다. JDK ProxyMemberService인터페이스를 기반으로 생성된 프록시이다. 따라서 JDK ProxyMemberService로 캐스팅은 가능하지만 MemberServiceImpl이 어떤 것인지는 전혀 알 수가 없다. 따라서 MemberServiceImpl 타입으로는 캐스팅이 불가능하다. 캐스팅을 시도하면 ClassCastException.class 예외가 발생한다.

CGLIB 사용

ProxyCastingTest - 추가

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test  
void cglibProxy() {  
    MemberServiceImpl target = new MemberServiceImpl();  
    ProxyFactory proxyFactory = new ProxyFactory(target);  
    proxyFactory.setProxyTargetClass(true); // CGLIB 사용  
  
    // 프록시를 인터페이스로 캐스팅 -> 성공  
    MemberService memberServiceProxy = (MemberService) proxyFactory.getProxy();  
    log.info("proxy class={}", memberServiceProxy.getClass());  
  
    //CGLIB 프록시를 구현 클래스로 캐스팅 시도 -> 성공  
    MemberServiceImpl castingMemberService = (MemberServiceImpl) memberServiceProxy;  
}

MemberServiceImpl 타입을 기반으로 CGLIB 프록시를 생성했다. MemberServiceImpl타입은 MemberService인터페이스를 구현했다. CGLIB는 구체 클래스를 기반으로 프록시를 생성한다. 따라서 CGLIB는 MemberServiceImpl 구체 클래스를 기반으로 프록시를 생성한다. 이 프록시를 CGLIB Proxy라 하자. 여기서 memberServiceProxy가 바로 CGLIB Proxy 이다.

여기에서 CGLIB Proxy를 대상 클래스인 MemberServiceImpl타입으로 캐스팅하면 성공한다.

왜냐하면 CGLIB는 구체 클래스를 기반으로 프록시를 생성하기 때문이다. CGLIB Proxy는 MemberServiceImpl구체 클래스를 기반으로 생성된 프록시이다. 따라서 CGLIB Proxy는 MemberServiceImpl은 물론이고, MemberServiceImpl이 구현한 인터페이스인 MemberService로도 캐스팅 가능하다.

정리

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

그런데 프록시를 캐스팅 할 일이 많지 않을 것 같은데 왜 이 이야기를 하는 것일까? 진짜 문제는 의존관계 주입시에 발생한다.

댓글남기기