스프링 핵심 원리(고급편)

Ch12. 스프링 AOP(실전 예제) - 프록시 기술과 한계(JDK 동적 프록시)

webmaster 2022. 4. 22. 13:45
728x90

타입 캐스팅

JDK 동적 프록시와 CGLIB를 사용해서 AOP 프록시를 만드는 방법에는 각각 장단점이 있다. JDK 동적 프록시는 인터페이스가 필수이고, 인터페이스를 기반으로 프록시를 생성한다. CGLIB는 구체 클래스를 기반으로 프록시를 생성한다

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

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

JDK 동적 프록시 한계

@Test
public void jdkProxy() {
    MemberServiceImpl target = new MemberServiceImpl();
    ProxyFactory proxyFactory = new ProxyFactory(target);
    proxyFactory.setProxyTargetClass(false); //생략시 jdk 동작 프록시

    //프록시를 인터페이스로 캐스팅 성공
    MemberService memberServiceProxy = (MemberService) proxyFactory.getProxy();
    //JDK 동적 프록시를 구현 클래스로 캐스팅 시도 실패, ClassCastException 예외 발생
    Assertions.assertThrows(
        ClassCastException.class, () -> {
            MemberServiceImpl castingMemberService = (MemberServiceImpl) memberServiceProxy;
        });

}

JDK 동적프록시 한계 ( 인터페이스 기반 프록시를 만들기 때문에 구현체로 Casting X)
구현체로 타입 변환 X

  • 여기서는 MemberServiceImpl 타입을 기반으로 JDK 동적 프록시를 생성했다. MemberServiceImpl 타입은 MemberService 인터페이스를 구현한다.
  • 따라서 JDK 동적 프록시는 MemberService 인터페이스를 기반으로 프록시를 생성한다. 
  • 그런데 여기에서 JDK Proxy를 대상 클래스인 MemberServiceImpl 타입으로 캐스팅하려고 하니 예외가 발생한다. 왜냐하면 JDK 동적 프록시는 인터페이스를 기반으로 프록시를 생성하기 때문이다.
  • JDK Proxy는 MemberService 인터페이스를 기반으로 생성된 프록시이다. 따라서 JDK Proxy는 MemberService로 캐스팅은 가능하지만 MemberServiceImpl 이 어떤 것인지 전혀 알지 못한다.
  • 따라서 MemberServiceImpl 타입으로는 캐스팅이 불가능하다. 캐스팅을 시도하면 ClassCastException.class 예외가 발생한다.

CGLIB

@Test
public 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;
}

CGLIB 같은 경우 자식객체로 만들어져서 동작하기 때문에 Casting 가능
부모 객체는 물론 부모의 부모 객체로 까지 casting이 가능하다.

  • MemberServiceImpl 타입을 기반으로 CGLIB 프록시를 생성했다. MemberServiceImpl 타입은 MemberService 인터페이스를 구현했다.
  • CGLIB는 구체 클래스를 기반으로 프록시를 생성한다. 따라서 CGLIB는 MemberServiceImpl 구체 클래스를 기반으로 프록시를 생성한다
  • 여기에서 CGLIB Proxy를 대상 클래스인 MemberServiceImpl 타입으로 캐스팅하면 성공한다. 왜냐하면 CGLIB는 구체 클래스를 기반으로 프록시를 생성하기 때문이다.
  • CGLIB Proxy는 MemberServiceImpl 구체 클래스를 기반으로 생성된 프록시이다. 따라서 CGLIB Proxy는 MemberServiceImpl 은 물론이고, MemberServiceImpl 이 구현한 인터페이스인 MemberService 로도 캐스팅할 수 있다.

의존관계 주입

ProxyDIAspect

@Slf4j
@Aspect
public class ProxyDIAspect {

    @Before("execution(* hello.aop..*.*(..))")
    public void doTree(JoinPoint joinPoint){
        log.info("[proxyDIAdvice] {}", joinPoint.getSignature());
    }
}

Test

@Slf4j
@Import(ProxyDIAspect.class)
//@SpringBootTest(properties = {"spring.aop.proxy-target-class=false"})
@SpringBootTest(properties = {"spring.aop.proxy-target-class=true"})
public class ProxyDITest {

    @Autowired
    MemberService memberService;

    @Autowired
    MemberServiceImpl memberServiceImpl;

    @Test
    public void go(){
        log.info("memberService class={}", memberService.getClass());
        log.info("memberServiceImpl class={}", memberServiceImpl.getClass());
        memberService.hello("hello");
    }

}
  • properties = {"spring.aop.proxy-target-class=false"} : application.properties 에 설정하는 대신에 해당 테스트에서만 설정을 임시로 적용한다. 이렇게 하면 각 테스트마다 다른 설정을 손쉽게 적용할 수 있다.
  • spring.aop.proxy-target-class=false : 스프링이 AOP 프록시를 생성할 때 JDK 동적 프록시를 우선 생성한다. 물론 인터페이스가 없다면 CGLIB를 사용한다
  • JDK 동적 프록시에 구체 클래스 타입 주입
    • 타입과 관련된 예외가 발생한다. 자세히 읽어보면 memberServiceImpl에 주입되길 기대하는 타입은 hello.aop.member.MemberServiceImpl이지만 실제 넘어온 타입은 com.sun.proxy.$Proxy54이다. 따라서 타입 예외가 발생한다고 한다

JDK 동적 프록시 적용시 자식타입으로 케스팅 X

  • @Autowired MemberServiceImpl memberServiceImpl : JDK Proxy는 MemberService 인터페이스를 기반으로 만들어진다. 따라서 MemberServiceImpl 타입이 뭔지 전혀 모른다. 그래서 해당 타입에 주입할 수 없다.
    • MemberServiceImpl = JDK Proxy 가 성립하지 않는다
  • CGLIB 프록시

CGLIB 같은 경우 구현 클래스를 상속받아 프록시를 만들기 떄문에 부모, 부모의 부모 까지 모두 의존관계주입이 가능하다.

  • @Autowired MemberService memberService : CGLIB Proxy는 MemberServiceImpl 구체 클래스를 기반으로 만들어진다. MemberServiceImpl 은 MemberService 인터페이스를 구현했기 때문에 해당 타입으로 캐스팅할 수 있다.
    • MemberService = CGLIB Proxy 가 성립한다.
  • @Autowired MemberServiceImpl memberServiceImpl : CGLIB Proxy는 MemberServiceImpl 구체 클래스를 기반으로 만들어진다. 따라서 해당 타입으로 캐스팅 할 수 있다.
    • MemberServiceImpl = CGLIB Proxy 가 성립한다.
728x90