CGLIB
1. 개요
1. 개요
CGLIB는 자바 바이트코드 조작 프레임워크이다. 이 라이브러리는 런타임 시 동적으로 프록시 클래스를 생성하는 데 주로 사용된다. 스프링 프레임워크와 하이버네이트 같은 유명한 자바 프레임워크에서 내부적으로 활용되며, 관점 지향 프로그래밍을 구현하거나 데이터 접근 계층에서 지연 로딩을 구현하는 등의 목적으로 널리 쓰인다.
이 라이브러리는 2002년에 스프링 프레임워크의 창시자인 로드 존슨에 의해 개발되었다. CGLIB는 자바 다이나믹 프록시가 인터페이스 기반 프록시만 생성할 수 있는 한계를 극복하기 위해 만들어졌다. CGLIB는 상속을 이용하여 대상 클래스의 서브클래스를 바이트코드 수준에서 생성함으로써, 인터페이스가 없는 일반 클래스에 대해서도 프록시를 생성할 수 있는 능력을 제공한다.
2. 동작 원리
2. 동작 원리
CGLIB는 자바의 바이트코드를 직접 조작하여 런타임 시 새로운 클래스를 동적으로 생성하는 방식으로 동작한다. 이는 JDK Dynamic Proxy가 인터페이스 기반으로 프록시를 생성하는 것과 달리, 구체 클래스에 대한 프록시를 생성할 수 있게 해주는 핵심 메커니즘이다.
CGLIB는 ASM이라는 저수준 바이트코드 조작 라이브러리를 기반으로 구축되어 있다. 이는 리플렉션을 사용하는 대신, 대상 클래스의 바이트코드를 분석하고 수정하여 새로운 서브클래스를 생성한다. 구체적으로, Enhancer라는 핵심 클래스를 사용하여 프록시를 생성하며, 사용자는 MethodInterceptor를 구현하여 원본 메서드 호출 전후에 실행될 로직을 정의한다.
생성된 프록시 클래스는 원본 클래스를 상속받으며, 대상 메서드들을 오버라이드한다. 메서드가 호출되면, CGLIB에 의해 주입된 MethodInterceptor의 intercept 메서드로 제어권이 넘어가 사용자가 정의한 부가 기능이 실행된 후, 필요에 따라 원본 메서드가 호출된다. 이 과정은 AOP의 메서드 가로채기 개념을 구현하는 데 적합하다.
이러한 바이트코드 생성 방식은 런타임 성능 측면에서 리플렉션을 사용하는 방식보다 일반적으로 빠르다고 평가받는다. 또한 상속을 사용하기 때문에 final 클래스나 final 메서드에는 프록시를 생성할 수 없다는 제약이 따른다. CGLIB의 동작 원리는 스프링 프레임워크와 하이버네이트 같은 많은 자바 프레임워크에서 투명한 프록시 생성을 위한 기반 기술로 채택되었다.
3. 주요 특징
3. 주요 특징
CGLIB는 자바 바이트코드를 직접 조작하여 런타임에 새로운 클래스를 생성한다는 점이 가장 큰 특징이다. 이는 JDK Dynamic Proxy가 인터페이스 기반으로만 프록시를 생성할 수 있는 것과 달리, 구체 클래스(Concrete Class)에 대해서도 프록시를 생성할 수 있게 해준다. 따라서 대상 클래스가 별도의 인터페이스를 구현하지 않았더라도 프록시 기능을 적용하는 것이 가능하다.
이 프레임워크는 메서드 오버라이딩을 통해 프록시 로직을 구현한다. CGLIB가 생성한 프록시 클래스는 대상 클래스를 상속받고, 특정 메서드를 재정의하여 부가 기능(로깅, 트랜잭션 등)을 삽입한다. 이러한 방식은 Aspect-Oriented Programming의 개념을 구현하는 데 매우 효과적이며, Spring Framework의 AOP 모듈에서 널리 사용되는 핵심 기술이다.
또한 CGLIB는 성능 측면에서 고려할 만한 특징을 가진다. 초기 클래스 생성 시에는 리플렉션과 바이트코드 조작으로 인한 오버헤드가 존재하지만, 일단 클래스가 생성된 후에는 일반적인 메서드 호출과 유사한 성능을 보인다. 이러한 특성 덕분에 Hibernate 같은 ORM 프레임워크에서 지연 로딩을 구현하는 데에도 활용된다.
4. JDK Dynamic Proxy와의 비교
4. JDK Dynamic Proxy와의 비교
CGLIB는 JDK Dynamic Proxy와 함께 자바에서 동적 프록시를 생성하는 대표적인 두 기술 중 하나이다. 두 기술은 모두 런타임에 프록시 객체를 생성하여 AOP나 트랜잭션 관리와 같은 횡단 관심사를 처리하는 데 사용되지만, 그 구현 방식과 적용 대상에서 근본적인 차이를 보인다.
가장 핵심적인 차이는 프록시를 생성할 수 있는 대상에 있다. JDK Dynamic Proxy는 인터페이스 기반으로 동작한다. 즉, 프록시를 생성하려면 대상 객체가 최소한 하나의 인터페이스를 구현하고 있어야 한다. 반면 CGLIB는 상속을 기반으로 한다. 대상 클래스의 바이트코드를 직접 조작하여 서브클래스를 생성하는 방식으로 작동하기 때문에, 인터페이스가 없는 일반 클래스에도 프록시를 적용할 수 있다. 이는 Spring Framework가 기본적으로 인터페이스 기반 프록시를 사용하지만, 대상에 인터페이스가 없을 경우 CGLIB로 자동 전환하는 이유이기도 하다.
성능 측면에서 초기 CGLIB는 리플렉션을 사용하는 JDK Dynamic Proxy에 비해 더 빠른 경우가 많았다. 그러나 최신 JVM에서는 리플렉션 성능이 크게 개선되어 그 차이가 미미해졌다. 대신 CGLIB는 대상 클래스에 기본 생성자가 필요하며, final로 선언된 클래스나 메서드는 프록시로 감쌀 수 없다는 제약이 있다. JDK Dynamic Proxy는 InvocationHandler를, CGLIB는 MethodInterceptor를 사용하여 메서드 호출을 가로채는 점도 구현 상의 차이점이다.
5. 주요 사용 사례
5. 주요 사용 사례
CGLIB는 주로 스프링 프레임워크와 하이버네이트 같은 주요 자바 프레임워크에서 광범위하게 사용된다. 이 프레임워크들은 CGLIB를 활용하여 복잡한 기능을 투명하게 구현한다.
가장 대표적인 사용 사례는 관점 지향 프로그래밍의 구현이다. 스프링 AOP는 대상 객체가 인터페이스를 구현하지 않은 경우, 즉 프록시 패턴을 적용하기 위해 JDK Dynamic Proxy를 사용할 수 없는 경우에 CGLIB를 통해 런타임에 서브클래싱을 수행하여 프록시 객체를 생성한다. 이를 통해 트랜잭션 관리, 보안, 로깅과 같은 횡단 관심사를 비즈니스 로직에서 분리하여 적용할 수 있다.
또 다른 핵심 사용 사례는 객체 관계 매핑 프레임워크에서의 지연 로딩 구현이다. 하이버네이트는 엔티티 클래스를 상속받은 프록시 객체를 CGLIB로 동적으로 생성하여, 데이터베이스에서 실제 객체를 처음 필요로 할 때까지 조회를 미루는 지연 로딩 메커니즘을 제공한다. 이는 애플리케이션의 성능을 최적화하는 데 중요한 역할을 한다.
이 외에도 Mockito 같은 테스트 더블 라이브러리에서 모의 객체를 생성하거나, 라이브러리나 애플리케이션이 런타임에 클래스의 행동을 변경해야 할 때 CGLIB가 활용된다.
6. 제한 사항
6. 제한 사항
CGLIB는 강력한 기능을 제공하지만 몇 가지 명확한 제한 사항을 가지고 있다. 가장 큰 제한은 대상 클래스가 기본 생성자를 가져야 한다는 점이다. CGLIB는 상속을 통해 프록시 클래스를 생성하기 때문에, 자식 클래스의 인스턴스를 만들려면 부모 클래스의 기본 생성자를 호출해야 한다. 따라서 기본 생성자가 private으로 선언되거나 존재하지 않는 클래스에는 프록시를 생성할 수 없다. 또한 final로 선언된 메서드는 오버라이드가 불가능하므로, 이러한 메서드에는 어드바이스를 적용할 수 없다.
성능과 관련된 제한도 존재한다. CGLIB를 이용한 프록시 생성은 JDK Dynamic Proxy에 비해 상대적으로 무겁고 시간이 더 소요되는 작업이다. 프록시 클래스 생성 시 바이트코드를 조작하고 새로운 .class 파일을 생성하는 과정이 필요하기 때문이다. 그러나 한 번 생성된 프록시 클래스는 캐싱되어 재사용되므로, 런타임 성능 자체는 일반적으로 우수한 편이다. 또한 CGLIB는 스프링 프레임워크와 같은 컨테이너 내에서 주로 사용되며, 이러한 환경에서는 생성 비용이 애플리케이션 시작 시간에 한 번 발생하므로 큰 문제가 되지 않는 경우가 많다.
마지막으로, CGLIB는 자바 리플렉션 API보다 저수준의 바이트코드 조작을 수행하기 때문에, 복잡한 상속 구조를 가진 클래스에 적용할 때 예기치 않은 동작이 발생할 가능성이 있다. Aspect-Oriented Programming을 구현하는 데 널리 사용되지만, 이러한 제한 사항을 인지하고 대상 클래스의 설계를 고려하는 것이 중요하다.
