Spring 프레임워크를 사용하면서 @Transactional, @Async 과 같은 기능들을 자주 접하게 됩니다. 이런 기능들은 개발자가 직접 작성한 코드 바깥에서 실행되는 부가 기능을 자동으로 적용해주는 역할을 하고 개발자가 개발에만 집중 할 수 있게 도와준다.
그런데 이게 어떻게 가능한 걸까?
그 핵심에는 프록시(Proxy) 라는 기술이 있다. Spring은 개발자가 작성한 원본 객체를 직접 사용하는 것이 아니라, 그 객체를 감싸는 프록시 객체를 만들어서 그 안에 부가기능을 끼워 넣는 방식으로 동작합니다.
이때 Spring이 사용하는 두 가지 대표적인 프록시 기술이 바로 JDK 동적프록시, CGLIB(Code Generation Library) 입니다.
오늘은 이 2가지 기술에 대해서 간단하게 정리해보겠습니다.
JDK 동적 프록시
JDK 동적 프록시는 Java 표준에서 제공하는 프록시 생성 기술로, java.lang.reflect.Proxy와 InvocationHandler를 이용해 런타임에 인터페이스 기반의 프록시 객체를 생성합니다.
즉, 프록시 대상 클래스가 인터페이스를 구현하고 있을 경우 JDK 동적 프록시를 통해 해당 인터페이스를 구현한 프록시 객체를 만들 수 있습니다.
📌 핵심 개념
- 대상 객체는 반드시 인터페이스를 구현해야 함
- 런타임에 프록시 객체를 동적으로 생성 (컴파일 시점이 아님)
- 프록시 객체는 인터페이스 타입으로 취급됨
JDK 동적 프록시 예제
JDK 동적 프록시 예제를 할용해볼 A 인터페이스, 구현체를 만들어 줍니다.
// A 인페이스
public interface AInterface {
String call();
}
// A 구현체
public class AImpl implements AInterface {
@Override
public String call() {
System.out.println("A 호출");
return "A";
}
}
InvocationHandler 정의
import java.lang.reflect.InvocationHandler;
// InvocationHandler 구현
public class TimeInvocationHandler implements InvocationHandler {
private final Object target;
public TimeInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, java.lang.reflect.Method method, Object[] args) throws Throwable {
System.out.println("Time Proxy 호출");
long start = System.currentTimeMillis();
Object result = method.invoke(target, args);
long end = System.currentTimeMillis();
System.out.println("Method " + method.getName() + " executed in " + (end - start) + " ms");
System.out.println("Time Proxy 종료");
return result;
}
}
이 클래스는 JDK 동적 프록시를 만들기 위한 핵심 인터페이스인 InvocationHandler를 구현한 클래스입니다.
메서드 호출 시 부가적인 작업을 추가하고 싶을 때 사용됩니다.
TimeInvocationHandler는 실제 객체(target)의 메서드 실행 시간을 측정하고 로그를 출력하는 프록시 역할을 합니다.
private final Object target;
- 실제 로직을 수행할 대상 객체(Real Object)
- 프록시가 이 객체의 메서드를 호출합니다.
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
- 프록시 객체가 메서드를 호출할 때 자동으로 이 메서드가 실행됩니다.
- proxy: 생성된 프록시 객체
- method: 호출된 메서드 정보
- args: 메서드에 전달된 인자들
테스트 코드
AInterface target = new AImpl();
TimeInvocationHandler timeInvocationHandler = new TimeInvocationHandler(target);
AInterface proxy = (AInterface) Proxy.newProxyInstance(
AInterface.class.getClassLoader(),
new Class[]{AInterface.class},
timeInvocationHandler
);
proxy.call();
System.out.println("targetClass: " + target.getClass().getName());
System.out.println("proxyClass: " + proxy.getClass().getName());
AInterface target = new AImpl();
TimeInvocationHandler timeInvocationHandler = new TimeInvocationHandler(target);
- 실제 비즈니스 로직을 수행할 구현체 객체(target)를 생성 : 프록시가 내부적으로 위임받아 사용할 대상
- InvocationHandler 구현체인 TimeInvocationHandler를 생성하면서, 내부에 target을 주입
이 핸들러는 프록시 메서드가 호출될 때마다 타이밍 로직을 실행하는 책임을 가집니다.
AInterface proxy = (AInterface) Proxy.newProxyInstance(
AInterface.class.getClassLoader(),
new Class[]{AInterface.class},
timeInvocationHandler
);
JDK 동적 프록시 객체 생성
- AInterface.class.getClassLoader()
→ 프록시 클래스를 생성할 때 사용할 클래스 로더 지정 - new Class[]{AInterface.class}
→ 어떤 인터페이스를 구현한 프록시를 만들 것인지 지정 - timeInvocationHandler
→ 프록시 메서드 호출 시 실행할 로직을 가진 핸들러
💡 여기서 반환된 proxy는 실제로 AInterface를 구현한 프록시 객체입니다. 내부적으로는 jdk.proxy3.$Proxy12 같은 이름의 클래스가 런타임에 만들어집니다.
proxy.call()
- 이 줄은 실제 프록시의 call() 메서드를 호출합니다.
- 그런데 프록시는 실제 target.call()을 호출하는 것이 아니라 invoke() 메서드 안에서 실행됩니다.
- 즉, TimeInvocationHandler.invoke() → target.call() 순으로 실행됩니다.
// == 출력 결과 ==
// Time Proxy 호출
// A 호출
// Method call executed in 0 ms
// Time Proxy 종료
// targetClass: study.jdkdynamic.code.AImpl
// proxyClass: jdk.proxy3.$Proxy12
- "Time Proxy 호출"
→ 프록시의 메서드가 호출되었고, 타이밍을 측정하기 시작 - "A 호출"
→ 실제 AImpl.call()이 호출되어 출력 - "Method call executed in ... ms"
→ 실행 시간 측정 결과 출력 - 프록시 클래스는 $Proxy12
→ JVM이 런타임에 생성한 익명 프록시 클래스
JDK 동적 프록시 흐름

1. 클라이언트는 JDK 동적 프록시의 call() 호출
2. JDK 동적 프록시는 InvocationHandler.invoke()를 호출. TimeInvocationHandler가 구현체가 있으므로 TimeInvocationHandler.invoke()가 호출된다.
3. TimeInvocationHandler가 내부 로직을 실행하고, method.invoke(target, args)를 호출해서 target의 실체 객체 AImpl를 호출 한다
4. AImpl 인스턴스의 call()이 호출
5. AImpl 인스턴스의 call()이 호출이 끝나면 TimeInvocationHandler로 응답이 돌아오고 시간 로그를 출력 한 다음 결과를 반환
CGLIB
CGLIB는 클래스를 상속받아 런타임에 프록시 객체를 생성하는 기술로 JDK 동적 프록시와는 다르게 인터페이스가 없어도 사용 가능하며, 구현체 클래스만 있어도 프록시를 만들 수 있는 점이 큰 특징
Spring AOP에서는 프록시 대상 클래스에 인터페이스가 없다면 자동으로 CGLIB을 사용합니다.
📌 특징 정리
- 클래스 기반의 프록시 생성 (상속을 통해 생성)
- 인터페이스 없이도 동작 가능
- final 클래스나 final 메서드는 프록시 불가 (상속 불가)
CGLIB 예제
JDK 동적 프록시 예제를 할용해볼 A 인터페이스, 구현체를 만들어 줍니다.
public class ConcreteService {
public void call() {
System.out.println("ConcreteService 호출");
}
}
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class TimeMethodInterceptor implements MethodInterceptor {
private final Object target;
public TimeMethodInterceptor(Object target) {
this.target = target;
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("Time Proxy 호출");
long start = System.currentTimeMillis();
Object result = method.invoke(target, args);
long end = System.currentTimeMillis();
System.out.println("Method " + method.getName() + " executed in " + (end - start) + " ms");
System.out.println("Time Proxy 종료");
return result;
}
}
이 클래스는 CGLIB의 프록시 동작을 정의하는 클래스입니다.
CGLIB에서 프록시 메서드 호출 시 intercept() 메서드를 호출하여 공통 로직을 실행합니다.
- org.springframework.cglib.proxy.MethodInterceptor를 구현합니다.
- CGLIB는 인터페이스가 아니라 클래스 기반 프록시를 생성하므로, 구체 클래스 상속 + 메서드 오버라이드 방식을 사용합니다.
테스트 코드
ConcreteService target = new ConcreteService();
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(ConcreteService.class);
enhancer.setCallback(new TimeMethodInterceptor(target));
ConcreteService proxy = (ConcreteService) enhancer.create();
proxy.call();
System.out.println("targetClass = " + target.getClass());
System.out.println("proxyClass = " + proxy.getClass());
ConcreteService target = new ConcreteService();
- 실제 비즈니스 로직을 실행할 객체
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(ConcreteService.class);
enhancer.setCallback(new TimeMethodInterceptor(target));
ConcreteService proxy = (ConcreteService) enhancer.create();
- Enhancer: CGLIB에서 프록시를 생성하는 핵심 클래스입니다.
- setSuperclass: 프록시가 상속할 클래스 지정.
→ CGLIB은 클래스 기반 프록시이므로, 인터페이스가 아닌 구현 클래스를 상속 - setCallback: 메서드가 호출될 때 실행될 인터셉터 지정.
→ 앞서 작성한 TimeMethodInterceptor를 넣어줌. - enhancer.create() : 프록시 객체 생성
이 객체는 ConcreteService를 상속하고 있으며, 메서드 호출 시 TimeMethodInterceptor의 intercept()가 실행
Time Proxy 호출
ConcreteService 호출
Method call executed in 0 ms
Time Proxy 종료
targetClass = class study.cglib.code.ConcreteService
proxyClass = class study.cglib.code.ConcreteService$$EnhancerByCGLIB$$a73fc3a8
proxyClass: CGLIB이 자동으로 생성한 프록시 클래스.
→ 이름에서 $$EnhancerByCGLIB$$ 라는 패턴을 볼 수 있음
CGLIB 흐름

클래스 기반 프록시는 상속을 사용하기 때문에 몇가지 제약사항이 있다.
- 부모 클래스의 생성자를 체크해야 한다 -> CGLIB는 자식 클래스를 동적으로 생성하기 때문에 생성자가 필요하다
- 클래스에 'final' 키워드가 붙으면 상속이 불가능하다 -> CGLIB에서는 예외 발생
- 메서드에 'final' 키워드가 붙으면 오버라이딩이 불가능 하다 -> CGLIB에서는 프록시가 동작하지 않음
JDK 동적 프록시, CGLIB 비교
| JDK 동적 프록시 | CGLIB | |
| 대상 | 인터페이스 | 클래스(상속) |
| 프록시 방식 | InvocationHandler | MethodInterceptor |
| 제약 | 인터페이스 필수 | final 클래스/메서드 사용 불가능 |
| 성능 | 비교적 빠름(단순 구조) | 약간 느릴 수 있음 |
Spring에서는 기본적으로 JDK 동적 프록시를 사용하지만, 대상 클래스에 인터페이스가 없으면 자동으로 CGLIB 프록시로 전환됩니다.
즉, 개발자가 별도로 선택하지 않아도 Spring이 상황에 맞는 프록시 기술을 선택해줍니다.
📌 정리
이번 글을 통해 Spring AOP, 의존성 주입 등 다양한 내부동작에서 이 프록시 기술들이 어떤식으로 활용되는지 이해했다.
Spring을 사용하면서 자동으로 적용해주는 것들이 많아 그냥 저냥 넘어갔던 기능들이 이제 하나, 둘 보이는 것 같다.
근데 공부하며 공부할 수록 Spring이 어려워지는건 기분탓인가 🥲
'개발' 카테고리의 다른 글
| Spring AOP의 시작, ProxyFactory 이해하기 (2) | 2025.05.27 |
|---|---|
| 리플렉션을 쓰기 전에 생각해야 하는 것들 (0) | 2025.05.24 |
| 객체에 기능을 동적으로 추가하는 방법: 데코레이터 패턴 (0) | 2025.05.22 |