동적 프록시가 뭘까요?
이번글은 동적 프록시에 대해서 정리했습니다.
참고 강의 : 김영한의 스프링 핵심 원리 - 고급편
동적 프록시
동적 프록시는 프록시 객체를 런타임에 자동으로 생성해주는 기술입니다.
개발자가 직접 프록시 클래스를 작성하지 않아도, JVM이 런타임 시점에 프록시 클래스를 생성하여 제공합니다.
동적 프록시를 사용하는 이유가 뭔가요? 직접 만들어도 되는거 아닌가요?
동적 프록시는 런타임에 프록시 클래스를 메모리에서 생성하기 때문에 불필요한 클래스 파일이 소스코드나 파일시스템에 남지 않습니다.
또한, 반복적인 코드 생성을 자동화해 중복 작업을 최소화하고, 개발자는 핵심 로직에 집중할 수 있게 해줍니다.
동적 프록시는 인터페이스를 기반으로 프록시를 동적으로 만들어주기 때문에, 인터페이스를 필수로 사용해야 합니다.
동적 프록시 적용
간단한 인터페이스에 동적 프록시를 적용해보겠습니다.
AInterface
public interface AInterface {
String call();
}
인터페이스의 구현체는 다음과 같습니다.
@Slf4j
public class AImpl implements AInterface{
@Override
public String call() {
log.info("A 호출");
return "a";
}
}
동적 프록시에 적용할 로직은 InvocationHandler를 구현해서 작성해야 합니다. 코드는 다음과 같습니다.
구현해야 하는 메서드는 invoke() 메서드 입니다. 또한, 로직이 실행되는 시간을 출력하는 기능을 추가하고자 합니다.
@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;
}
}
invoke 메서드에 전달되는 파라미터는 다음과 같습니다.
- Object proxy : 프록시 자신
- Method method : 호출한 메서드
- Object[] args : 메서드를 호출할 때 전달할 인수
method.invoke(target, args)는 리플렉션을 사용해서 target 인스턴스의 메서드를 실행합니다.
args는 메서드 호출시 전달해줍니다.
A 인터페이스에 동적 프록시를 적용한 테스트 코드는 다음과 같습니다.
@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); 로 동적 프록시에 사용될 handler 인스턴스를 생성합니다.
Proxy.newProxyInstance(AInterface.class.getClassLoader(), new Class[]{AInterface.class}, handler);로 동적 프록시를 만들어 줍니다. 생성시 필요한 정보는 클래스 로더 정보, 인터페이스와 핸들러 로직을 넣어주어야 합니다.
proxy.call(); call 메서드를 호출하면, proxy 객체 생성시 넣어주었떤 handler가 호출됩니다.
출력 결과는 다음과 같습니다.
AInterface 구현체인 AImpl 인스턴스의 call() 메서드를 호출하지 않았지만, 동적 프록시 기술로 인해 호출된 것을 확인할 수 있습니다.
proxyClass=class com.sun.proxy.$Proxy12 부분이 동적으로 생성된 프록시 클래스 정보입니다.
전체 흐름은 어떻게 될까요?
동적 프록시를 사용해서 테스트한 코드의 흐름은 다음과 같습니다.
1. proxy.call()을 통해 동적 프록시의 call()메서드를 호출합니다.
2. 동적 프록시는 InvacationHandler.invoke()를 호출합니다. TimeInvocationHanlder가 구현체 이므로 구현체의 invoke()가 호출됩니다.
3. TimeInvocationHandler의 invoke 메서드의 내부 로직인 method.invoke(target,args)를 호출합니다. 여기서 target은 AImpl 클래스의 인스턴스를 의미합니다.
4. AImpl 인스턴스의 call() 메서드가 호출되고, TimeInvocationHandler로 응답을 전달합니다.
5. 시간 로그를 출력 후, 결과를 반환합니다.
그림으로 나타내면 다음과 같습니다.
AImpl에 대한 프록시 객체를 직접 만들어주지 않고, JDK 동적 프록시를 사용해서 프록시 객체를 동적으로 만들었습니다.
또한, TimeInvocationHandler는 공통으로 사용했습니다.
이는 JDK 동적 프록시 기술 덕분에 적용 대상 만큼 프록시 객체를 만들지 않아도 되고, 그리고 같은 부가 기능 로직을 한번만
개발해서 공통으로 적용할 수 있음을 의미합니다. 즉, 프록시 클래스를 직접 만들어야 하는 문제도 해결하고, 부가 기능 로직도 하나의 클래스에 모아서 단일 책임 원칙(SRP)도 지킬 수 있게 됩니다.
여러개의 인터페이스에 동적 프록시 적용하기 전과 후에는 어떤 차이가 있나요?
AInterface, BInterface, CInterface가 있다고 가정합니다.
동적 프록시를 적용하기 전엔 각 인터페이스에 대한 프록시 객체를 직접 생성해 주어야 합니다.
동적 프록시를 사용한 경우엔 다음과 같이 됩니다.
직접 Proxy 객체를 만들어 주지 않아도 됩니다.
정리
- 동적 프록시는 개발자가 프록시 클래스를 직접 작성하지 않고 런타임에 JVM이 자동으로 프록시 객체를 생성해주는 기술입니다.
- 동적 프록시를 사용해 실행할 부가 기능(로깅, 성능 측정 등)을 한 번만 작성하여 재사용할 수 있게 합니다. 이를 통해 코드 중복을 피하고 유지보수 및 확장성을 높일 수 있습니다.
'Spring' 카테고리의 다른 글
프록시 팩토리 (0) | 2025.03.11 |
---|---|
CGLIB (0) | 2025.03.10 |
트랜잭션 AOP 주의사항 (0) | 2025.03.01 |
Thread Local (0) | 2025.02.28 |
@Transactional 원리 (0) | 2025.02.25 |