KKanging

JDK 프록시 , CGLIB 개념과 Spring AOP 가 CGLIB 를 채택한 이유 본문

백엔드/Spring framework

JDK 프록시 , CGLIB 개념과 Spring AOP 가 CGLIB 를 채택한 이유

천방지축 개발자 2024. 9. 27. 01:29

AOP 적용 시점

위빙이란

AOP 적용 시점을 알기 위해서는 위빙(Weaving)이라는 단어를 알아야 한다.

우리가 분리한 핵심 로직과 부가 기능 로직이 결합되는 시점을 의미한다.

따라서 그 시점에 따라 다음과 같이 구분된다.

AOP 적용 시점

  1. 컴파일 타임 위빙(Compile-Time Weaving)
  2. 컴파일 타임 위빙(Compile-Time Weaving)
  3. 런타임 위빙(Runtime Weaving)

자바에서 사용하는 대표적인 AOP 기법

Spring AOP는 2가지 기법을 채택하였다.

JDK Dynamic 프록시

JDK Dynamic 프록시는 Reflection API 의 Proxy를 사용해서 구현한다.

프록시 팩토리에 의해 런타임 시 다이나믹하게 만들어지는 오브젝트

Target의 인터페이스를 상속하는 구조로 사용된다

따라서 client에서 Target 을 호출할 때 구현체를 호출해서는 안되고 Target의 인터페이스가 존재해야만 하는 제약 사항이 있다.

JDK 프록시는 InvocationHandler를 상속해서 invoke를 직접 재정의를 해야하는데, 이는 Target의 메서드 처리를 대체 된 proxy가 Invocation Handler의 invoke 메서드에 위임해서 Target을 호출하기 때문이다.

그래서 invoke를 재정의할 때 target을 호출한다.

CGLIB

CGLIB는 JDK proxy 와 조금 다르게 동작한다.

우선 Reflection 기술이 사용되지 않는다.

따라서 Proxy를 생성하는 역할이 필요한데 이는 Enhancer 라는 CGLIB의 모듈이 이를 처리한다.

그리고 CGLIB는 또 다른 점이 있는데 Target 클래스를 상속 받는 프록시 객체를 만든다는 점이다.

이는 interface를 만들어야하는 작업이 강제되지 않는 다는 점이 특징이다.

또한 상속이기 때문에 final이 붙은 메서드는 AOP 적용이 안된다.

JDK 프록시 처럼 MethodIntercepter 를 재정의 해야한다.

인터셉터가 해당 target의 메서드를 가로채어 부가기능을 수행한다.

JDK Dynamic Proxy vs CGLIB Proxy

공통점

  • 둘 다 proxy를 만들어서 부가 기능을 결합함

차이점

  • JDK는 Reflection API를 호출하여 proxy 객체를 생성하여 런타임 시점이지만 CGLIB는 클래스가 로드 될 때 Proxy 객체를 생성한다.
  • JDK 는 interface 가 필요하다. 대신 CGLIB는 상속을 사용해서 필요없다.
  • JDK 는 client에서 target의 interface가 아닌 구현을 호출한다면 runtime error 가 발생한다.
  • Target의 final 메서드는 CGLIB를 적용할 수 없다.
  • 성능의 차이는 3배 정도 차이가 난다 (CGLIB 승)

 

Spring AOP 동작원리

Spring AOP가 동작하는 원리

이제 Spring AOP 의 핵심 원리인 proxy 를 지원해주는 기능 2가지를 알아 보았다.

실제로 Spring AOP는 2가지를 모두 지원하고 target에 interface가 있다면 Jdk proxy 를 interface 가 없다면 CGLIB를 통해서 AOP를 적용한다.

실제로 Spring 이 AOP를 적용하는 시점은 Bean 이 생성 될 때 BeanPostProcessor 의 AOP 관련 구현체에 의해 프록시가 생성되고 DI 컨테이너에는 해당 프록시 객체가 저장된다.

따라서 우리는 Proxy를 생성하는 ProxyFactory를 구현 안해도 되면 Advice 만 구현하면 된다.

Spring AOP 를 사용할 때 Proxy 를 쓴 적이 없다?

AOP 기능을 많이 사용하거나 이미 구현된 AOP 기능을 많이 사용할 것이다.

하지만 Spring AOP가 JDK 방식과 CGLIB 2개 방식을 채택한다고 하는데, JDK 방식의 에러인 구현을 의존했을 때 발생하는 에러가 경험한 적 없을까

이유는 Spring AOP는 2가지 방식을 채택하지만 Spring Boot 는 CGLIB를 디폴트로 설정한다. 따라서 우리는 Spring Boot 프로젝트에서 JDK 프록시 를 경험한 적이 없을 것이다.

참고

왜 SpringBoot 는 CGLIB 방식을 디폴트로 채택했을까?

여기에는 다양한 이유가 있다.

우선 CGLIB는 클래스 로딩 시점에 proxy가 생성 되어 Reflection API를 사용하지 않는다.

이는 큰 특징인데 JVM에서 JIT compiler 가 수행하는 최적화는 Reflection 기능이 있는 class 는 수행하지 않기 때문이다.

또한 Proxy 생성 방법에도 이유가 있는데 JDK 프록시 기반은 인터페이스가 꼭 필요하다.

하지만 여기까진 괜찮지만

위 이미지를 보면 구현에 의존하는 DI를 수행하면 에러가 난다.

이유는 Target 의 구현은 Proxy와 상속 관계가 아니기 때문이다.

따라서 이러한 관점에서 프로그램의 자유도를 낮춘다고 생각해서 디폴트를 CGLIB를 채택했다고 추론이 가능하다.

왜 Spring AOP는 AspectJ를 사용하지 않을까

Spring AOP는 Spring Bean 에 AOP를 적용하기 위해 만들어졌다.

따라서 AspectJ와는 목적이 조금은 다르다.

AspectJ는 모든 Java 코드에 적용이 가능하다 ( 심지어 클래스 멤버 변수까지도)

그리고 컴파일시 위빙도 가능하여 Spring AOP보다 훨씬 더 좋은 성능을 내기도 한다.

그런데 왜 SpringBoot는 사용하지 않을까

우선 다음과 같은 이유가 있다.

  1. Bean에 AOP를 적용하는 것이 목적이기 때문

보통 우리가 유지보수하는 로직은 Component 안에 존재한다.

AOP를 적용해야할 코드는 Bean에 대다수가 존재하기 때문에 AOP를 모든 코드에 적용할 일이 그렇게 많지 않다는 뜻이다.

  1. AspectJ의 호환성 및 설정 러닝커브

Spring AOP의 사용법이나 설정은 Spring 개발자라면 쉽다. 하지만 AspectJ는 사용법이 복잡하고 컴파일 위빙 같은건 다른 라이브러리들과 호환성 문제가 있을 수 있다.

위와 같은 이유들 때문에 아마 Spring 에서는 AspectJ를 사용하지 않는거 같다.

참고자료

JDK Dynamic Proxy vs CGLIB Proxy (tistory.com)

자바 AOP의 모든 것(Spring AOP & AspectJ) (tistory.com)

스프링 AOP와 AspectJ의 차이점 및 실무 적용 가이드 (f-lab.kr)