본문 바로가기
Spring|Spring-boot/Spring AOP

Spring AOP 한계(2)

by oncerun 2022. 10. 14.
반응형

 

Spring은 프록시 기반의 AOP만을 지원하고 있다.  프록시 객체를 생성하는 방법은 크게 두 가지로 나뉜다.

 

  • JDK 동적 프록시
  • CGLIB

 

JDK 동적 프록시인 경우에는 인터페이스가 필수이며, 인터페이스 기반으로 프록시 객체를 생성한다.

CGLIB는 구체 클래스를 기반으로 생성한다. 

하지만 인터페이스가 있는 경우에는 JDK 동적프록시, CGLIB 둘 중 하나를 선택할 수 있다.

 

이는 spring 옵션 중 proxyTargetClass을 활용하여 선택할 수 있으며 false인 경우 JDK, true인 경우 CGLIB를 사용한다.

 

 

테스트 준비

  • Example은 구체 클래스
  • ExampleImpl은 ExampleInterface를 사용해 만든 구현체
  • ExampleInterface는 인터페이스이다. 

 

 

 

JDK 동적 프록시 한계

 

- 인터페이스 기반으로 만들어지는 프록시 객체이기 때문에 구체 클래스로 타입 캐스팅이 불가능하다.

 

JDK 동적 프록시를 테스트하기 위해 ExampleImpl을 프록시로 생성하여 타입 캐스팅을 진행하자.

@Test
void jdkProxy() {

    ExampleImpl target= new ExampleImpl();
    
    ProxyFactory proxyFactory = new ProxyFactory(target);
    
    ExampleInterface proxy = (ExampleInterface) proxyFactory.getProxy();
    
    Assertions.assertThrows(ClassCastException.class, () -> {
        ExampleImpl proxyImpl = (ExampleImpl) proxyFactory.getProxy();
    });
}

JDK 동적 프록시는 인터페이스 기반이기 때문에 ExampleInterface로 타입 캐스팅이 가능하다. 그럼 구현체로 타입 변환을 하면 어떻게 될까?

캐스팅을 못한다고 에러가 발생한다.  이는 당연한 것이다. 인터페이스 기반으로 프록시 객체가 생성됬기 때문에 

이 프록시 객체는 구현체에 대해서는 알 수가 없다.

 

이는 포인트 컷에서 타입으로 프록시 적용 객체를 선별할 때 영향을 미칠 수 있다.

 

이 코드를 CGLIB로 변경해보자.

 

 * options = proxyFactory.setProxyTargetClass(true);  true인 경우 CGLIB, false인 경우 JDK 동적 프록시

 

@Test
void cglibProxy() {

    ExampleImpl target= new ExampleImpl();

    ProxyFactory proxyFactory = new ProxyFactory(target);

    proxyFactory.setProxyTargetClass(true);

    ExampleInterface proxy = (ExampleInterface) proxyFactory.getProxy();

    ExampleImpl proxyImpl = (ExampleImpl) proxyFactory.getProxy();
}

이 코드는 에러 없이 잘 실행된다. 당연하게도 CGLIB는 구체 클래스 기반으로 프록시를 생성하기 때문이다. 이 구체 클래스는 그 상위 부모 타입으로 interface까지 가지고 있기에 캐스팅이 가능하다.

이러한 특성이 어디서 문제가 발생할까?

 

의존관계 주입

 

TIP  - 스프링 부트는 기본적으로 CGLIB를 기본정책으로 사용한다. 따라서 JDK 동적 프록시로 변경한 후 테스트를 진행

@SpringBootTest(properties = {"spring.aop.proxy-target-class=false"})

 

 

전역적으로 간단한 Aspect하나와 ExampleImpl, ExampleInterface 타입으로 주입받아 테스트를 진행한다.

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


    @Autowired
    ExampleInterface exampleInterface;

    @Autowired
    ExampleImpl example;


    @Test
    void test() {
        log.info("[{}]", exampleInterface.getClass());
        log.info("[{}]", example.getClass());
    }

}
  • 컴포넌트 스캔으로 ExampleImpl을 빈으로 등록한다. (@Component추가)
    @Component
    public class ExampleImpl implements ExampleInterface{}
  • Spring-boot의 테스트용 설정으로 JDK 동적 프록시로 생성하도록 옵션 변경
  • ExampleImpl의 메서드에 어드바이스를 추가하기 위해 ProxyDIAspect추가 후 빈등 록

 

[에러 로그]

Unsatisfied dependency expressed through field 'example'; 

nested exception is org.springframework.beans.factory.BeanNotOfRequiredTypeException: 

Bean named 'exampleImpl' is expected to be of type 'hello.aop.example.ExampleImpl'

but was actually of type 'com.sun.proxy.$Proxy50'

 

이 오류는 스프링 빈 주입 시 발생한 오류이다. exampleImpl을 기대했지만 실제로는 프록시가 들어왔다고 설명하고 있다.

 

흐름을 다시 한번 생각해보면 스프링 빈등 록 후 Aspect 적용 빈이면 Proxy객체를 만드는데 JDK 동적프록시로 생성한다.

생성된 JDK 동적 프록시는 인터페이스 기반이니까 ExamplInterface타입으로 생성될 것이다. 그리고 스프링 컨테이너에 저장된다. 

 

인터페이스로 주입받는 경우에는 아무런 문제가 없지만 실제로 구현 클래스로 주입을 받을 때 오류가 발생한다.

@Autowired
ExampleImpl example;

 

그럼 CGLIB는?

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


    @Autowired
    ExampleInterface exampleInterface;

    @Autowired
    ExampleImpl example;


    @Test
    void test() {
        log.info("[{}]", exampleInterface.getClass());
        log.info("[{}]", example.getClass());
    }

}

문제없이 컴파일 후 실행이 잘되었다.

 

JDK 동적 프록시에 대한 한계는 구체 클래스를 알 수 없기 때문에 DI에서 프록시 객체를 주입 시 문제가 발생한다는 것이다. 사실 OCP를 지키기 위해 클라이언트는 인터페이스에만 의존하여 주입받는 경우가 많지만 특별한 경우, 테스트인 경우 구체 클래스를 주입받아야 하는 경우가 존재한다. 

이때는 JDK 동적 프록시에서 CGLIB로 변경하면 된다.

 

 * 스프링 부트 2.0 버전부터 CGLIB를 기본으로 사용하도록 했다.

과거 AOP를 위한 스프링의 노력으로 spring4.0부터 objenesis라이브러리를 통해 상속의 단점을 해결했다. 

구체 클래스의 기본 생성자 필수, 생성자 호출 중복을 해결했기 때문이다.

다만 final 키워드는 상속이 불가능하기 때문에 CGLIB를 사용할 수 없다. 하지만 개발에 있어서 final로 클래스를 생성하는 경우는 매우 드물기 때문에 문제없이 잘 사용할 수 있다.

 

 

@Transactional

(22-10-14 추가)

 

과거에는 CGLIB 방식을 사용하면 인터페이스에 적용된 @Transactional을 인식하지 못했다. 

따라서 트랜잭션이 적용되지 않는 버그가 있었는데, 이를 스프링 5.0 부터는 이 부분을 개선해서 인터페이스에 있는 @Transactional도 인식한다. 

하지만 다른 AOP 방식에서는 적용되지 않을 수 있기 때문에 공식 매뉴얼의 가이드에서는 가급적 구체 클래스에 @Transcational을 사용하도록 권고하고 있다.

 

 

 

 

 

 

 

 

반응형

'Spring|Spring-boot > Spring AOP' 카테고리의 다른 글

Spring AOP 한계  (0) 2022.01.31
Spring AOP 매개변수 활용  (0) 2022.01.30
Spring AOP @target, @within  (0) 2022.01.29
Pointcut - within, args  (0) 2022.01.29
Pointcut - execution  (0) 2022.01.29

댓글