동적 프록시 기술
프록시를 적용한 기존 코드에는 한 가지 문제점이 존재했다.
- 대상 클래스 수 만큼 로그 추적을 위한 프록시 클래스를 만들어야 한다는 점이다
- 로그 추적을 위한 프록시 클래스들의 소스코드는 거의 같은 모양을 하고 있다.
리플렉션
자바가 기본으로 제공하는 JDK 동적 프록시 기술이나 CGLIB 같은 프록시 생성 오픈소스 기술을 활용하면 프록시 객체를 동적으로 만들어낼 수 있다. 쉽게 이야기해서 프록시 클래스를 지금처럼 계속 만들지 않아도 된다는 것이다.
JDK 동적 프록시는 자바 reflection 기술을 바탕으로 동작한다. 리플렉션과 관련된 내용은 스킵한다.
JDK 동적 프록시
소개
앞선 예제에서 봤듯이 프록시의 로직은 같은데, 적용 대상만 차이가 있었다. 이 문제를 해결하는 것이 바로 동적 프록시 기술이다.
동적 프록시 기술을 사용하면 개발자가 직접 프록시 클래스를 만들지 않아도 된다. 이름 그대로 프록시 객체를 동적으로 런타임에 개발자 대신 만들어준다. 그리고 동적 프록시에 원하는 실행 로직을 지정할 수 있다.
주의
JDK 동적 프록시는 인터페이스를 기반으로 프록시를 동적으로 만들어준다. 따라서 인터페이스가 필수이다.
public interface AInterface {
String call();
}
@Slf4j
public class AImpl implements AInterface {
@Override
public String call() {
log.info("A 호출");
return "a";
}
}
public interface BInterface {
String call();
}
@Slf4j
public class BImpl implements BInterface {
@Override
public String call() {
log.info("B 호출");
return "b";
}
}
예제 코드
JDK 동적 프록시에 적용할 로직은 InvocationHandler
인터페이스를 구현해서 작성하면 된다.
JDK 동적 프록시가 제공하는 InvocationHandler
package java.lang.reflect;
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
제공되는 파라미터는 다음과 같다.
Object proxy
: 프록시 자신Method method
: 호출한 메서드Object[] args
: 메서드를 호출할 때 전달한 인수
@Slf4j
public class TimeInvocationHandler implements InvocationHandler {
private final Object target;
public TimeInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
log.info("TimeProxy 실행");
long startTime = System.currentTimeMillis();
Object result = method.invoke(target, args);
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeProxy 종료 resultTime={}", resultTime);
return result;
}
}
Object target
: 동적 프록시가 호출할 대상method.invoke(target, args)
: 리플렉션을 사용해서target
인스턴스의 메서드를 실행한다.args
는 메서드 호출시 넘겨줄 인수이다.
사용 예시
@Test
void dynamicA() {
AInterface target = new AImpl();
TimeInvocationHandler handler = new TimeInvocationHandler(target);
AInterface proxy = (AInterface) Proxy.newProxyInstance(
AInterface.class.getClassLoader(),
new Class[]{AInterface.class},
handler
);
proxy.call();
log.info("targetClass={}", target.getClass());
log.info("proxyClass={}", proxy.getClass());
}
new TimeInvocationHandler(target)
: 동적 프록시에 적용할 핸들러 로직이다.Proxy.newProxyInstance(AInterface.class.getClassLoader(), new Class[]{AInterface.class}, handler)
- 동적 프록시는
java.lang.reflect.Proxy
를 통해서 생성할 수 있다. - 클래스 로더 정보, 인터페이스, 그리고 핸들러 로직을 넣어주면 된다. 그러면 해당 인터페이스를 기반으로 동적 프록시를 생성하고 그 결과를 반환한다.
- 동적 프록시는
dynamicA() 출력 결과
TimeInvocationHandler - TimeProxy 실행
AImpl - A 호출
TimeInvocationHandler - TimeProxy 종료 resultTime=0
JdkDynamicProxyTest - targetClass=class hello.proxy.jdkdynamic.code.AImpl
JdkDynamicProxyTest - proxyClass=class com.sun.proxy.$Proxy1
우리는 프록시 클래스를 직접 만들지 않았다. 우리는 InvocationHandler
를 구현한 TimeInvocationHandler
라는 곳에 로직만 정의했고, JDK가 프록시를 만들어줬다.
실행 순서
- 클라이언트는 JDK 동적 프록시의
call()
을 실행한다. - JDK 동적 프록시는
InvocationHandler.invoke()
를 호출한다.TimeInvocationHandler
가 구현체로 있으므로TimeInvocationHandler.invoke()
가 호출된다. TimeInvocationHandler
가 내부 로직을 수행하고,method.invoke(target, args)
를 호출해서target
인 실제 객체(AImpl)를 호출한다.AImpl
인스턴스의call()
이 실행된다.AImpl
인스턴스의call()
의 실행이 끝나면TimeInvocationHandler
로 응답이 돌아온다. 시간 로그를 출력하고 결과를 반환한다.
JDK 프록시 객체는 InvocationHandler.invoke
를 호출하게 되고, 구체적인 구현체(우리가 등록해놓은 로직)가 동작한다.
JDK 동적 프록시 기술 덕분에 적용 대상 만큼 프록시 객체를 만들지 않아도 된다.
JDK 동적 프록시 - 적용1:
로그 추적기(V1) 예시에 적용해보자.
public class LogTraceBasicHandler implements InvocationHandler {
private final Object target;
private final LogTrace logTrace;
public LogTraceBasicHandler(Object target, LogTrace logTrace) {
this.target = target;
this.logTrace = logTrace;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
TraceStatus status = null;
try {
String message = method.getDeclaringClass().getSimpleName() + "."
+ method.getName() + "()";
status = logTrace.begin(message);
//로직 호출
Object result = method.invoke(target, args);
logTrace.end(status);
return result;
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
수동 빈 등록
@Configuration
public class DynamicProxyBasicConfig {
@Bean
public OrderControllerV1 orderControllerV1(LogTrace logTrace) {
OrderControllerV1 orderController = new OrderControllerV1Impl(orderServiceV1(logTrace));
OrderControllerV1 proxy = (OrderControllerV1) Proxy.newProxyInstance(
OrderControllerV1.class.getClassLoader(),
new Class[]{OrderControllerV1.class},
new LogTraceBasicHandler(orderController, logTrace)
);
return proxy;
}
@Bean
public OrderServiceV1 orderServiceV1(LogTrace logTrace) {
OrderServiceV1 orderService = new OrderServiceV1Impl(orderRepositoryV1(logTrace));
OrderServiceV1 proxy = (OrderServiceV1) Proxy.newProxyInstance(
OrderServiceV1.class.getClassLoader(),
new Class[]{OrderServiceV1.class},
new LogTraceBasicHandler(orderService, logTrace)
);
return proxy;
}
@Bean
public OrderRepositoryV1 orderRepositoryV1(LogTrace logTrace) {
OrderRepositoryV1 orderRepository = new OrderRepositoryV1Impl();
OrderRepositoryV1 proxy = (OrderRepositoryV1) Proxy.newProxyInstance(
OrderRepositoryV1.class.getClassLoader(),
new Class[]{OrderRepositoryV1.class},
new LogTraceBasicHandler(orderRepository, logTrace)
);
return proxy;
}
}
- JDK 동적 프록시 기술을 통해 생성된 프록시 객체를 빈으로 등록한다.
LogTraceBasicHandler
: 동적 프록시를 만들더라도LogTrace
를 출력하는 로직은 모두 같기 때문에 프록시는 모두LogTraceBasicHandler
를 사용한다.
[/JDK 동적 프록시 - 적용2](#JDK 동적 프록시 - 적용2)
위 예제는, no-log 메서드에서도 로그가 남는 문제가 존재한다. 이런 문제를 해결하기 위해 메서드 이름을 기준으로 특정 조건을 만족할 때만 로그를 남기는 기능을 개발해보자.
public class LogTraceFilterHandler implements InvocationHandler {
private final Object target;
private final LogTrace logTrace;
private final String[] patterns;
public LogTraceFilterHandler(Object target, LogTrace logTrace, String... patterns) {
this.target = target;
this.logTrace = logTrace;
this.patterns = patterns;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//메서드 이름 필터
String methodName = method.getName();
if (!PatternMatchUtils.simpleMatch(patterns, methodName)) {
return method.invoke(target, args);
}
TraceStatus status = null;
try {
String message = method.getDeclaringClass().getSimpleName() + "."
+ method.getName() + "()";
status = logTrace.begin(message);
//로직 호출
Object result = method.invoke(target, args);
logTrace.end(status);
return result;
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
patterns 정보를 추가로 받아, 매칭되는 메서드만 프록시 기능을 활용하도록 변경하였다.
- 조건과 맞지 않다고 호출 자체를 프록시 객체가 먹어버리면 안 된다. 반드시 원본 메서드를 호출해줘야 한다.
Config 클래스에서 프록시 객체 생성 시에, 핸들러를 생성하게 될 거고, 이 때 패턴 정보를 같이 넘겨주면 된다.
JDK 동적 프록시 - 한계
JDK 동적 프록시는 인터페이스가 필수이다.
그렇다면 V2 애플리케이션 처럼 인터페이스 없이 클래스만 있는 경우에는 어떻게 동적 프록시를 적용할 수 있을까?
이것은 일반적인 방법으로는 어렵고 CGLIB
라는 바이트코드를 조작하는 특별한 라이브러리를 사용해야 한다.
CGLIB
CGLIB: Code Generator Library
- CGLIB는 바이트코드를 조작해서 동적으로 클래스를 생성하는 기술을 제공하는 라이브러리이다.
- CGLIB를 사용하면 인터페이스가 없어도 구체 클래스만 가지고 동적 프록시를 만들어낼 수 있다.
- CGLIB는 원래는 외부 라이브러리인데, 스프링 프레임워크가 스프링 내부 소스 코드에 포함했다. 따라서 스프링을 사용한다면 별도의 외부 라이브러리를 추가하지 않아도 사용할 수 있다.
우리가 CGLIB를 직접 사용할 일은 거의 없고, 스프링의 ProxyFactory
기술이 CGLIB를 편리하게 사용하도록 도와주므로, 대략 개념만 잡으면 된다고 한다.
CGLIB - 예제 코드
인터페이스 + 구현 조합
public interface ServiceInterface {
void save();
void find();
}
@Slf4j
public class ServiceImpl implements ServiceInterface {
@Override
public void save() {
log.info("save 호출");
}
@Override
public void find() {
log.info("find 호출");
}
}
구체 클래스만 있는 경우
@Slf4j
public class ConcreteService {
public void call() {
log.info("ConcreteService 호출");
}
}
CGLIB 적용
JDK 동적 프록시에서 실행 로직을 위해 InvocationHandler
를 제공했듯이, CGLIB는 MethodInterceptor
를 제공한다.
MethodInterceptor - CGLIB 제공
package org.springframework.cglib.proxy;
public interface MethodInterceptor extends Callback {
Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy)
throws Throwable;
}
obj
: CGLIB가 적용된 객체method
: 호출된 메서드args
: 메서드를 호출하면서 전달된 인수proxy
: 메서드 호출에 사용
JDK 동적 프록시 메서드의 모양이랑 비교하면, MethodProxy
라는게 추가로 있는 모양인데, CGLIB가 적용된 obj
대신 MethodProxy 타입 proxy
를 사용하면 훨씬 실행속도가 빠르다고 한다.
@Slf4j
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 {
log.info("TimeProxy 실행");
long startTime = System.currentTimeMillis();
Object result = proxy.invoke(target, args);
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeProxy 종료 resultTime={}", resultTime);
return result;
}
}
- JDK 동적 프록시를 설명할 때 예제와 거의 코드 짜임새가 같다.
사용 예시
@Slf4j
public class CglibTest {
@Test
void cglib() {
ConcreteService target = new ConcreteService();
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(ConcreteService.class);
enhancer.setCallback(new TimeMethodInterceptor(target));
ConcreteService proxy = (ConcreteService) enhancer.create();
log.info("targetClass={}", target.getClass());
log.info("proxyClass={}", proxy.getClass());
proxy.call();
}
}
Enhancer
: CGLIB는Enhancer
를 사용해서 프록시를 생성한다.enhancer.setSuperclass(ConcreteService.class)
: CGLIB는 구체 클래스를 상속 받아서 프록시를 생성할 수 있다. 어떤 구체 클래스를 상속 받을지 지정한다.enhancer.setCallback(new TimeMethodInterceptor(target))
- 프록시에 적용할 실행 로직을 할당한다.
enhancer.create()
: 프록시를 생성한다. 앞서 설정한enhancer.setSuperclass(ConcreteService.class)
에서 지정한 클래스를 상속 받아서 프록시가 만들어진다.
JDK 동적 프록시는 인터페이스를 구현(implement)해서 프록시를 만든다. CGLIB는 구체 클래스를 상속(extends)해서 프록시를 만든다.
CGLIB가 동적으로 생성하는 클래스 이름은 다음과 같은 규칙으로 생성된다.대상클래스$$EnhancerByCGLIB$$임의코드
CGLIB 제약: 클래스 기반 프록시는 상속을 사용하기 때문에 몇가지 제약이 있다.
- 부모 클래스의 생성자를 체크해야 한다. CGLIB는 자식 클래스를 동적으로 생성하기 때문에 기본 생성자가 필요하다.
- 클래스에
final
키워드가 붙으면 상속이 불가능하다. CGLIB에서는 예외가 발생한다. - 메서드에
final
키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없다. CGLIB에서는 프록시 로직이 동작하지 않는다.
남은 문제
- 인터페이스가 있는 경우에는 JDK 동적 프록시를 적용하고, 그렇지 않은 경우에는 CGLIB를 적용하려면 어떻게 해야할까?
- 두 기술을 함께 사용할 때 부가 기능을 제공하기 위해서 JDK 동적 프록시가 제공하는
InvocationHandler
와 CGLIB가 제공하는MethodInterceptor
를 각각 중복으로 만들어서 관리해야 할까? - 특정 조건에 맞을 때 프록시 로직을 적용하는 기능도 공통으로 제공되었으면?
다음 챕터에서 동적 프록시 기술을 통해 위 문제를 해결한다.
'Spring > AOP' 카테고리의 다른 글
[Spring AOP] 빈 후처리기 (0) | 2025.07.06 |
---|---|
[Spring AOP] 스프링이 지원하는 프록시 (0) | 2025.07.06 |
[Spring AOP] 프록시 패턴과 데코레이터 패턴 (0) | 2025.07.06 |
[Spring AOP] 템플릿 메서드 패턴과 콜백 패턴 (0) | 2025.07.06 |
[Spring AOP] 쓰레드 로컬 - Thread Local (0) | 2025.07.06 |