CGLIB
CGLIB(Code Generator Library): 코드 생성 라이브러리로서 런타임에 동적으로 자바 클래스의 프록시를 생성해주는 기능을 제공한다. 인터페이스가 아닌 클래스에 대해서 동적 프록시를 생성할 수 있다.
CGLIB는 Spring AOP를 공부하면서 들었던 개념이었다. Spirng에서 AOP를 적용할 때 런타임시 프록시를 생성한다. 이때 인터페이스 기반은 JDK Dynamic Proxy로 클래스 기반은 CGLIB를 사용한다. Spring Boot에서는 CGLIB를 사용하고 있다.
CGLIB는 타겟에 대한 정보를 직접적으로 제공 받아 바이트 코드를 조작하여 프록시를 생성한다. 때문에 리플렉션을 사용하는 JDK Dynamic Proxy에 비해 성능이 좋다. 또한 CGLIB는 메소드가 처음 호출 되었을 때 동적으로 타겟 클래스의 바이트 코드를 조작하고, 이후 호출 시엔 조작된 바이트 코드를 재사용한다.
JDK Dynamic Proxy: Java의 리플렉션 패키지에 존재하는 Proxy라는 클래스를 통해 생성된 프록시 객체를 의미한다.
장점:
- 인터페이스 없이 단순 클래스만으로 프록시 객체를 동적으로 생성이 가능하다.
- 리플렉션이 아닌 바이트 조작을 사용하며, 타겟에 대한 정보를 알고 있기 때문에 JDK Dynamic Proxy에 비해 성능이 좋다.
단점:
- 의존성을 추가해야한다.(Spring 3.2 이후 버전의 경우 Spring Core 패키지에 포함됨)
- default 생성자가 필요하다.(objenesis 라이브러리르 통해 해결됨)
- 타겟의 생성자가 두 번 호출된다.(objenesis 라이브러리르 통해 해결됨)
CGLIB의 주요 구성 요소
CGLIB는 프록시 생성과 관련된 모듈은 위의 그림과 같이 Enhancer 클래스, Callback 인터페이스 그리고 CallbackFilter 인터페이스이다. CGLIB를 사용하면 인터페이스가 아닌 타겟 클래스에 대해서도 프록시 객체를 만들어 줄 수 있고, 이 과정에서 Enhancer라는 클래스를 활용한다.
프록시 객체 생성 과정
implementation group: 'cglib', name: 'cglib', version: '3.2.4'
의존성 추가 후, 아래와 같이 Enhancer를 사용하여 프록시 객체를 생성할 수 있다.
public class Main {
public static void main(String[] args) throws IOException {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Jayon.class); // 타켓 클래스
enhancer.setCallback(new MyMethodInterceptor(new Jayon())); //handler
Jayon jayon = (Jayon) enhancer.create(); // proxy 생성
jayon.speak("CGLIB");
}
}
class MyMethodInterceptor implements MethodInterceptor {
private final Person target;
public MyMethodInterceptor(Person target) {
this.target = target;
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("BEFORE");
method.invoke(target, args);
System.out.println("AFTER");
return null;
}
}
이 과정에서 CGLIB는 타켓 클래스에 포함된 모든 메소드를 재정의하고, 타겟 클래스에 대한 바이트 코드를 조작하여 프록시를 생성한다. 따라서 CGLIB를 적용할 클래스는 final 메소드가 들어있거나, final 클래스면 안된다. 또한 private 접근자로 된 메소드도 상속이 불가하므로 적용되지 않는다.
프록시 기술과 한계
1. JDK 동적 프록시의 한계 - 타입 캐스팅
위에 서술했다시피 JDK 동적 프록시는 인터페이스 기반으로 프록시 객체를 만든다. 따라서 프록시 객체는 타겟 객체의 구현제 클래스에 대해 전혀 알지 못한다. 따라서 타겟 객체의 구현 클래스로 타입 캐스팅이 불가능하다.
반면 CGLIB 프록시는 어떤 타입으로든 타입 캐스팅이 가능하다. GGLIB 객체는 구체 클래스를 상속받아 만들었기 때문이다.
CGLIB 프록시는 구체인 MemberServiceImpl을 상속받아 프록시 객체를 만들었다. 그리고 그 슈퍼 클래스는 MemberService를 구현하고 있다. 따라서 CGLIB 프록시는 모두 타입 캐스팅이 가능하다.
2. JDK 동적 프록시의 한계 - 의존관계 주입
종합해보자면, JDK 동적 프록시는 구체 클래스로 타입 캐스팅이 불가능하며 이런 이유 때문에 구체 클래스 타입으로 의존관계 주입이 불가능하다. DI는 주로 인터페이스 기반으로 주입을 받기 때문에 실제 구체 클래스를 선언하는 경우가 없어 큰 문제가 없을 가능성도 있다. 하지만, 테스트 또는 여러 이류로 AOP가 적용된 프록시가 구체 클래스를 직접 의존관계를 받아야 하는 경우도 있다.
3. CGLIB의 한계 - 기본 생성자 필수
자바 언어에서 상속을 받으면 자식 클래스를 생성할 때, 부모 클래스의 생성자도 같이 호출해야 한다. CGLIB 프록시는 대상 클래스를 상속 받고, 생성자에서 대상 클래스의 기본 생성자를 호출한다. 따라서 대상 클래스에 기본 생성자를 만들어야 한다.
4. CGLIB의 한계 - 생성자 2번 호출 문제
실제 target 객체를 생성할 때 1번 호출, 프록시 객체를 생성할 때, 부모 클래스의 생성자가 호출되어 1번 호출되어 생성자가 총 2번이 호출된다.
5. CGLIB의 한계- Final 키워드 클래스, 메서드 사용 불가
final 키워드가 클래스에 있으면 상속이 불가능하고, 메서드에 있으면 오버라이딩이 불가능하다. CGLIB는 상속을 기반으로 하기 떄문에 이런 경우 프록시가 생성되지 않거나 동작하지 않는다.
6. CGLIB 동적 프록시 한계 극복
CGLIB의 한계로 다룬 3번 문제 4번 문제는 스프링이 objenesis라는 특별한 라이브러리를 사용해서 기본 생성자 없이 객체 생성이 가능하도록 해결되었다.
Reference
https://steady-coding.tistory.com/608
https://ojt90902.tistory.com/721
'Spring > 개념' 카테고리의 다른 글
[Spring] Spring MVC와 스프링의 동작 원리 (0) | 2023.07.08 |
---|---|
[Spring] Annotation (0) | 2023.06.18 |
[Web] HTTP 웹 기본 (0) | 2023.06.04 |
[Web] 서블릿(servlet)이란? (0) | 2023.05.23 |
[Spring] 스프링 PSA (0) | 2023.05.11 |