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

[스프링 AOP] 프록시 타입 캐스팅 한계

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

[스프링 AOP] 프록시 타입 캐스팅 한계

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

 

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

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

www.inflearn.com

 

프록시 타입 캐스팅에 관련된 이야기를 하려고 합니다.
프록시를 타입 캐스팅할 경우가 많지 않을것 같은데…? 왜 이런 이야기를 하는 걸까요?
문제는 의존 관계 주입시 발생하기 때문입니다.

스프링 AOP 프록시

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

프록시를 만드는 방법에는 2가지가 있습니다.

  • JDK 동적 프록시
  • CGLIB 프록시

 

JDK 동적 프록시
인터페이스가 필수이고, 인터페이스를 기반으로 프록시를 생성

CGLIB 프록시
구체클래스 기반으로 프록시를 생성

 

스프링이 프록시를 생성할때 ProxyFactoryproxyTargetClass 옵션에 따라 둘중 하나를 선택 할수 있습니다.

  • proxyTargetClass=false JDK 동적 프록시 사용
  • proxyTargetClass=ture CGLIB 사용
  • 옵션과 무관하게 인터페이스가 없으면 CGLIB 사용

JDK 동적 프록시 타입 변환

JDK 동적 프록시는 인터페이스를 기반으로 프록시를 생성하기 때문에 구체 클래스로 타입 캐스팅이 불가능한 한계가 존재합니다.
설명만 들으면 고개가 갸우뚱🤨 할 수 있으니 코드를 통해서 한번 알아봅시다.

먼저 PaymentService 인터페이스가 존재하고, 해당 인터페이스를 상속받은 PaymentServiceImpl이 있다고 가정 합니다.

 

MyProxyCastingTest

@Slf4j
public class MyProxyCastingTest {
    @Test
    void jdkProxy() {
        PaymentServiceImpl target = new PaymentServiceImpl();
        ProxyFactory proxyFactory = new ProxyFactory(target);
        proxyFactory.setProxyTargetClass(false); // JDK 동적 프록시

        // 프록시를 인터페이스로 캐스팅 성공
        PaymentService paymentServiceProxy = (PaymentService) proxyFactory.getProxy();
        log.info("proxy class={}", paymentServiceProxy.getClass());

        // 프록시를 구체클래스로 캐스팅 실패
        Assertions.assertThrows(ClassCastException.class, () -> {
            PaymentServiceImpl paymentServiceImplProxy = (PaymentServiceImpl) proxyFactory.getProxy();
        });
    }
}

 

 

paymentServiceProxy는 인터페이스(paymentService)를 상속받아 프록시가 생성 됩니다.
paymentServiceProxypaymentServiceImpl이 무엇인지 전혀 알지 못합니다...!
따라서 타입 캐스팅을 시도하면 예외(ClassCastException.class)가 발생합니다.

CGLIB 프록시 타입 변환

이번에는 CGLIB를 사용하여 프록시를 생성해봅시다.
CGLIB는 구체 클래스를 기반으로 프록시를 생성합니다.!!!

 

MyProxyCastingTest

@Slf4j
public class MyProxyCastingTest {

    @Test
    void cglibProxy() {
        PaymentServiceImpl target = new PaymentServiceImpl();
        ProxyFactory proxyFactory = new ProxyFactory(target);
        proxyFactory.setProxyTargetClass(true); // CGLIB 프록시

        PaymentService paymentServiceProxy = (PaymentService) proxyFactory.getProxy();
        log.info("casting1 proxy class={}", paymentServiceProxy.getClass());

        // CGLIB 프록시를 구현 클래스로 캐스팅 시도
        PaymentServiceImpl paymentServiceImplProxy = (PaymentServiceImpl) proxyFactory.getProxy();
        log.info("casting2 proxy class={}", paymentServiceImplProxy.getClass());
    }
}

 

다시 말씀 드립니다..! CGLIB는 구체 클래스를 기반으로 프록시를 생성합니다.
따라서 PaymentServiceImpl을 기반으로 프록시를 생성합니다.
구체 클래스를 기반으로 프록시를 생성하기 때문에 타입 캐스팅이 가능합니다.^^

JDK, CGLIB 프록시 정리

JDK 동적 프록시는 대상 객체인 구체 클래스로 캐스팅 할 수 없다.
CGLIB 프록시는 대상 객체인 구체 클래스에 캐스팅 할 수 있다.

타입 캐스팅 문제로 인해 의존관계를 주입할때 문제가 발생합니다..

어떤 문제가 발생하는지 알아봅시다.👀

JDK 동적 프록시 의존관계 주입

JDK 동적 프록시에 구체 클래스 타입을 주입할 때 어떤 문제가 발생하는지 지금부터 확인해봅시다..!

먼저 간단하게 Aspect 하나를 만들어 보자.

 

MyProxyDIAspect

@Slf4j
@Aspect
public class MyProxyDIAspect {

    @Before("execution(* spring.advanced.advancedspring..*.*(..))")
    public void doLog(JoinPoint joinPoint) {
        log.info("[proxyDIAdvice] {}", joinPoint.getSignature());
    }
}

 

MyProxyDITest

@Slf4j
@SpringBootTest(properties = "spring.aop.proxy-target-class=false") // JDK 동적 프록시
@Import(MyProxyDIAspect.class)
public class MyProxyDITest {

    @Autowired
    PaymentService paymentService;
    @Autowired
    PaymentServiceImpl paymentServiceImpl;

    @Test
    void test() {
        log.info("paymentService class={}", paymentService.getClass());
        log.info("paymentServiceImpl class={}", paymentServiceImpl.getClass());
        paymentServiceImpl.payment(100);
    }
}

 

위 코드를 실행하면 아래와 같은 오류메시지가 발생합니다.

BeanNotOfRequiredTypeException: Bean named 'paymentServiceImpl' is expected to be of
type 'spring.advanced.advancedspring.practice.payment.PaymentServiceImpl' but was
actually of type 'com.sun.proxy.$Proxy60'

 

자세히 보면 paymentServiceImpl에 주입되기를 기대하는 타입은 spring.advanced.advancedspring.practice.payment.PaymentServiceImpl이지만, 실제로는 com.sun.proxy.$Proxy60라서 예외가 발생했다고 합니다.

 


아래 코드를 실행하면 다음과 같은 결과가 나옵니다.

@Slf4j
@SpringBootTest(properties = "spring.aop.proxy-target-class=false") // JDK
@Import(MyProxyDIAspect.class)
public class MyProxyDITest {

    @Autowired
    PaymentService paymentService;

    @Test
    void test() {
        log.info("paymentService class={}", paymentService.getClass());
    }
}

 

결과

MyProxyDITest     : paymentService class=class com.sun.proxy.$Proxy60

paymentService의 프록시로 Proxy60객체가 생성되었습니다!!!

 

앞에서 언급드린 내용이 기억 나시나요?
JDK Proxy는 인터페이스(PaymentService)를 기반으로 생성됩니다.
따라서 PaymentServiceImpl이 뭔지 알지 못합니다.
타입 캐스팅이 불가해요....ㅠ0ㅠ


그래서 주입 할 수 없습니다. 😿

CGLIB 프록시 의존관계 주입

이번에는 CGLIB 프록시를 생성하여 테스트 해봅시다.

MyProxyDITest

@Slf4j
@SpringBootTest(properties = "spring.aop.proxy-target-class=true") // CGLIB
@Import(MyProxyDIAspect.class)
public class MyProxyDITest {

    @Autowired
    PaymentService paymentService;
    @Autowired
    PaymentServiceImpl paymentServiceImpl;

    @Test
    void test() {
        log.info("paymentService class={}", paymentService.getClass());
        log.info("paymentServiceImpl class={}", paymentServiceImpl.getClass());
        paymentServiceImpl.payment(100);
    }
}

 

CGLIB Proxy는 구체클래스(paymentServiceImpl)를 기반으로 생성됩니다.
따라서 paymentServiceImpl가 무엇인지 알고 있습니다.
그래서 주입이 가능합니다.

 

정리
JDK 동적 프록시는 대상 객체인 paymentServiceImpl타입에 의존관계를 주입할 수 없다.
CGLIB 프록시는 대상 객체인 paymentServiceImpl타입에 의존관계 주입을 할 수 있다.

 

잠깐만!?
그러면 CGLIB만 사용하면 되는거 아닌가? 🤷🏽

CGLIB의 단점

스프링에서 CGLIB는 구체 클래스를 상속 받아 AOP 프록시를 생성할 때 사용합니다.
하지만, CGLIB는 구체 클래스를 상속받기 때문에 다음과 같은 문제가 있습니다.

  • 대상 클래스에 기본 생성자 필수
  • 생성자 2번 호출
  • final 키워드 클래스, 메소드 사용 불가

 

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

CGLIB는 구체 클래스를 상속 받습니다.
Java 에서 상속을 받으면 자식 클래스의 생성자를 호출할 때, 자식 클래스의 생성자에서 부모 클래스의 생성자도 호출해야 합니다.(만약 생략 되어 있다면 자식 클래스의 생성자 첫줄에 부모 클래스의 기본생성자를 호출하는 super()가 자동으로 들어 갑니다.^^) 이것은 java 문법 입니다.

public class Child extends Parent {
    // 자식 생성자 호출시 부모 기본생성자 호출
    public Child() {
        super(); // 일반적으로 생략되어 있음
    }
}

 

생성자 2번 호출

  • 실제 target의 객체를 생성할 때 생성자 호출
  • 프록시 객체를 생성할 때 부모 클래스의 생성자 호출 1번
    이렇게 총 2번 호출하게 됩니다.

다시 이야기하면
실제 대상(paymentServiceImpl)의 객체를 생성할 때 생성자를 호출 합니다.
구체 클래스(paymentServiceImpl)를 상속받은 CGLIB Proxy 객체를 생성 할때 CGLIB Proxy 객체의 부모 클래스(paymentServiceImpl)의 생성자를 호출합니다.

 

final 키워드 클래스, 메소드 사용 불가

final 키워드가 클래스에 있으면 상속이 불가능하고, 메소드에 있으면 오버라이딩이 불가능 합니다.
CGLIB는 상속을 기반으로 하기 때문에 두 경우 프록시가 생성되지 않거나 정상적으로 동작하지 않습니다.

스프링의 해결책

스프링은 AOP 프록시 생성을 편리하게 제공하기 위해 오랜시간동안 고민하고 문제를 해결해왔습니다.

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

스프링 4.0부터 CGLIB의 기본 생성자가 필수인 문제가 해결되었습니다.
objenesis 라는 특별한 라이브러리를 사용해서 기본 생성자 없이 객체 생성이 가능합니다.
참고로 이 라이브러리는 생성자 호출 없이 객체를 생성할 수 있게 해준다.

참고: http://objenesis.org/

 

생성자 2번 호출 문제 해결

스프링 4.0부터 CGLIB의 생성자 2번 호출 문제가 해결되었습니다.
이것도 역시 objenesis 라는 특별한 라이브러리 덕분에 가능해졌습니다.^^
이제 생성자가 1번만 호출된다.

 

final 키워드 클래스, 메소드 사용 불가

CGLIB의 남은 문제라면 final 클래스final 메서드가 있는데, AOP를 적용할 대상에는 final 클래스final 메서드를 잘 사용하지는 않으므로 이 부분은 크게 문제가 되지는 않습니다.

 

정리

스프링은 최종적으로 스프링 부트 2.0에서 CGLIB를 기본으로 사용하도록 결정했습니다.
CGLIB를 사용하면 JDK 동적 프록시에서 동작하지 않는 구체 클래스 주입이 가능하기 때문입니다.
(물론 우리에게 선택권을 열어줍니다. spring.aop.proxy-target-class=false)

 

 

 

728x90
반응형

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

[스프링 AOP] 내부 호출 문제 해결  (0) 2023.02.07
[스프링 AOP] 포인트컷 지시자  (0) 2023.02.03
[스프링 AOP] 실습  (0) 2023.01.31
[스프링 AOP] 개념  (0) 2023.01.30