CGLIB Enhancer
🧠 CGLIB Enhancer
완전 정복: 동적 프록시를 생성하는 마법의 클래스
📌 들어가며
Spring AOP 또는 고급 프록시 프레임워크에서 흔히 등장하는 net.sf.cglib.proxy.Enhancer
.
이 클래스는 런타임에 기존 클래스(프록시 패턴에서의 타겟 클래스[Target Class])를 확장(상속)하여 새로운 프록시 클래스를 생성하는 핵심 클래스입니다.
본 글에서는 CGLIB의 Enhancer
가 어떤 역할을 하는지, 내부적으로 어떻게 동작하는지, 그리고 실무에서 어떻게 사용되는지를 심층적으로 분석해보겠습니다.
✅ 기본 역할: 클래스를 "향상(Enhance)" 시킨다
Enhancer
는 CGLIB에서 프록시 객체를 만들기 위한 중앙 클래스입니다.
📘 정의: Enhancer
는 런타임에 특정 클래스를 상속한 새로운 서브클래스를 바이트코드로 생성하고, 여기에 메서드 인터셉션 로직을 삽입하여 프록시 객체를 생성하는 클래스입니다.
※ 샘플 코드를 테스트하기 위해서는 CGLib 3.3.0 버전이 필요합니다.
메이븐 센트럴 리포지토리에서 CGLib 3.3.0 버전을 로컬 리포지토리에 다운로드합니다.
(만약 기존의 메이븐 프로젝트가 있다면, 다음 디펜던시를 pom.xml에 추가하여 로컬 리포지토리에 다운로드합니다)
자바 프로젝트에서 CGLib 3.3.0 라이브러리를 사용하기 위해서는 STS4에서 다음과 같이 설정합니다.
CGLib 라이브러리는 asm 라이브러리가 필요하기 때문에 이들을 로컬 리포지토리에 다운로드한 두 라이브러리를 Add External JARs... 를 선택하여 추가합니다.
🚫 Java 9 이상에서의 예외
Java 9 이상에서는 기본 모듈 시스템(JPMS)으로 인해 java.lang.ClassLoader#defineClass()는 디폴트로 리플렉션으로 호출할 수 없습니다. 그래서 다음과 같은 예외가 발생합니다.
java.lang.reflect.InaccessibleObjectException:
Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(...) accessible
🔧 해결 방법:
그래서 VM argument 설정에 다음 아규먼트를 설정해야 합니다.
--add-opens java.base/java.lang=ALL-UNNAMED
🧱 기본 사용 예제
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MyService.class);
enhancer.setCallback(new MyInterceptor());
MyService proxy = (MyService) enhancer.create();
이 코드는 다음과 같은 작업을 수행합니다:
MyService
클래스를 상속하는 새로운 프록시 클래스의 바이트코드를 생성- 메서드 호출 시
MyInterceptor.intercept()
를 호출하도록 설정 - 최종적으로 실제 인스턴스(proxy 객체) 를 생성하여 리턴
🔍 핵심 메서드 설명
메서드 | 설명 |
---|---|
setSuperclass(Class<?> clazz) |
프록시 대상이 될 클래스를 지정 (프록시는 이 클래스를 extends ) |
setCallback(Callback callback) |
메서드 호출을 가로챌 인터셉터를 지정 (MethodInterceptor ) |
create() |
위 설정을 기반으로 새로운 서브클래스 바이트코드를 생성하고 인스턴스를 반환 |
🧠 내부 작동 원리
1. Enhancer.create()
호출 시
AbstractClassGenerator.generate(ClassLoader)
호출- 내부에서 ASM을 사용하여 프록시 서브클래스의 바이트코드를 생성
- 클래스 이름 예:
MyService$$EnhancerByCGLIB$$abc123
- 생성된 바이트코드는 defineClass() 를 통해 JVM에 로드됨
2. 메서드 실행 시
- 생성된 프록시 클래스는
MethodInterceptor.intercept()
를 호출 - 내부적으로
proxy.invokeSuper(obj, args)
를 통해 실제 메서드를 호출
⚠️ 주의 사항
제한 사항 | 설명 |
---|---|
final 클래스 ❌ | 상속 불가 → CGLIB 프록시 생성 불가 |
final 메서드 ❌ | 오버라이드 불가 → 메서드 가로채기 불가 |
기본 생성자 필요 | Enhancer가 newInstance() 로 객체 생성하므로 필요 |
Java 9+ 모듈 접근 제한 | --add-opens java.base/java.lang=ALL-UNNAMED 필요 |
🧪 실무 예: 시간 측정 프록시
public class TimeLoggingInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
long start = System.nanoTime();
Object result = proxy.invokeSuper(obj, args);
long end = System.nanoTime();
System.out.println(method.getName() + " 실행 시간: " + (end - start) + "ns");
return result;
}
}
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MyService.class);
enhancer.setCallback(new TimeLoggingInterceptor());
MyService proxy = (MyService) enhancer.create();
🔧 CGLIB vs JDK Proxy
구분 | CGLIB Enhancer | JDK Proxy |
---|---|---|
대상 | 일반 클래스 (non-final) | 인터페이스 |
구현 | 바이트코드 생성 (ASM) | 리플렉션 기반 |
성능 | 상대적으로 빠름 | 느릴 수 있음 |
예외처리 | final 클래스는 불가 | 인터페이스만 되므로 제한적 |
📦 Spring AOP에서의 Enhancer 사용
Spring AOP에서 @EnableAspectJAutoProxy(proxyTargetClass = true)
를 설정하면 내부적으로 CGLIB의 Enhancer
가 사용됩니다. 이를 통해 인터페이스 없이도 POJO 클래스에 프록시를 적용할 수 있습니다.
@EnableAspectJAutoProxy(proxyTargetClass = true)
✅ 결론
Enhancer
는 CGLIB 기반 프록시 생성을 위한 강력한 도구입니다.
클래스를 기반으로 프록시를 생성해야 하거나, 인터페이스가 없는 객체에 부가기능을 삽입해야 하는 경우 매우 유용합니다.
Spring 내부 AOP뿐만 아니라, 다양한 범용 프록시 구현에서 활용할 수 있는 핵심 컴포넌트입니다.
📘 요약 정리
항목 | 내용 |
---|---|
핵심 역할 | 클래스를 상속한 프록시 생성 |
주요 메서드 | setSuperclass() , setCallback() , create() |
내부 기술 | ASM 기반 바이트코드 조작 |
주의 사항 | final 클래스/메서드 불가, 기본 생성자 필요 |
활용 분야 | AOP, 로깅, 트랜잭션, 동적 기능 삽입 |
🧠 CGLIB Enhancer
내부 동작 심층 분석
🔬 ASM 기반 바이트코드 생성 + defineClass()
호출까지
📌 전제 개념: Enhancer.create()
는 무슨 일을 하나?
CGLIB의 Enhancer.create()
는 단순히 객체를 생성하는 것이 아니라 다음과 같은 JVM 레벨의 작업을 수행합니다:
- 프록시 클래스 바이트코드 생성 (ASM 사용)
- 동적으로 JVM에 클래스 로드 (
defineClass()
호출) - 프록시 인스턴스 생성 (
newInstance()
)
📦 주요 클래스 흐름
순서 | 클래스 | 메서드 | 역할 |
---|---|---|---|
1 | Enhancer |
create() |
프록시 생성 시작점 |
2 | AbstractClassGenerator |
create(ClassLoader) |
바이트코드 생성 및 클래스 정의 |
3 | DefaultGeneratorStrategy |
generate(ClassGenerator) |
실제 바이트코드 생성 수행 |
4 | ReflectUtils |
defineClass(...) |
바이트코드를 JVM에 로드 |
🔍 핵심 코드 흐름 추적
// Enhancer.java
public Object create() {
this.classOnly = false;
this.argumentTypes = null;
this.arguments = null;
return super.create(this.getClassLoader());
}
// AbstractClassGenerator.java
protected Object create(ClassLoader loader) {
Object key = KEY_FACTORY.newInstance(...); // 프록시 고유 키
return super.create(key); // 캐싱된 바이트코드가 있으면 재사용
}
// AbstractClassGenerator.java
protected Class generate(ClassLoaderData data) {
byte[] classBytes = strategy.generate(this); // → DefaultGeneratorStrategy 사용
return ReflectUtils.defineClass(className, classBytes, loader);
}
💣 ReflectUtils.defineClass(...)
내부
public static Class defineClass(String name, byte[] b, ClassLoader loader) {
Method method = ClassLoader.class.getDeclaredMethod(
"defineClass", String.class, byte[].class, int.class, int.class, ProtectionDomain.class);
method.setAccessible(true); // 🔥 Java 9+에서는 여기서 예외 발생 가능
return (Class) method.invoke(loader, name, b, 0, b.length, protectionDomain);
}
✅ 여기서 호출되는 메서드:
protected final Class<?> ClassLoader.defineClass(String name, byte[] b, int off, int len, ProtectionDomain domain)
🧪 디버깅 팁: defineClass 트래킹
▶ IntelliJ or STS에서 ReflectUtils.defineClass()
브레이크포인트 설정
ReflectUtils.java
열기 (net.sf.cglib.core
)defineClass(...)
메서드에 브레이크포인트 설정Enhancer.create()
호출 → 디버거 진입classBytes
를 디버그 창에서 Base64 인코딩하여 저장하면 생성된 프록시 클래스 바이트코드를 확인 가능
📦 실제 생성되는 프록시 클래스 이름 예
com.intheeast.MyService$$EnhancerByCGLIB$$a1b2c3d4
- 기본 클래스 이름 뒤에 붙는
$$EnhancerByCGLIB$$[해시]
는 CGLIB이 만든 프록시 클래스라는 의미
🔍 바이트코드 확인하기 (선택 사항)
프록시 클래스가 어떻게 생겼는지 보려면 javap -c
명령으로 확인할 수 있습니다.
classBytes
를.class
파일로 저장javap -c MyService$$EnhancerByCGLIB$$abc.class
✅ 요약
단계 | 동작 내용 |
---|---|
Enhancer.create() |
프록시 인스턴스 생성 시작 |
generate() |
ASM 기반으로 바이트코드 생성 |
defineClass() |
리플렉션으로 클래스 JVM에 로딩 |
MethodInterceptor.intercept() |
메서드 호출 시 실행되는 프록시 콜백 |
Java 9+ | --add-opens 없으면 InaccessibleObjectException 발생 |
GitHub: https://github.com/nomadinsunda/JavaLabs/tree/main/src/com/intheeast/cglibdemo
JavaLabs/src/com/intheeast/cglibdemo at main · nomadinsunda/JavaLabs
Contribute to nomadinsunda/JavaLabs development by creating an account on GitHub.
github.com