프록시의 사전적 정의는 대리자라는 뜻입니다. 소프트웨어에서 프록시는 클라이언트의 요청을 대신 받아 처리하고, 필요한 경우 실제 서버에 요청을 위임하는 대리 객체입니다. 따라서 클라이언트는 서버가 요청을 처리한 것인지 프록시가 요청을 처리한 것인지 알지 못합니다.
이번 포스트에는 프록시 패턴과 동적 프록시에 대해 알아보고, 다음 포스트는 프록시 패턴을 스프링에서 어떻게 사용하는지 알아보고자 합니다.
프록시 패턴의 구조
사진 출처: https://ko.wikipedia.org/wiki/프록시_패턴
Subject (인터페이스)
클라이언트가 사용하는 공통 인터페이스입니다. RealSubject와 Proxy 모두 이 인터페이스를 구현합니다.
RealSubject (실제 객체)
실제 비즈니스 로직을 수행하는 클래스입니다.
Proxy (대리자)
RealSubject에 대한 참조를 가지고 있으며, 클라이언트의 요청을 가로채서 추가 작업을 수행한 후 RealSubject에 위임합니다.
인터페이스를 통해서 프록시 패턴을 설명했지만, RealSubject를 Proxy가 상속받게 하여 프록시 패턴을 상속을 활용하여 만들 수도 있습니다.
프록시 장점
프록시를 사용하면 좋은점이 직접 서버를 호출하는것과 다르게 프록시를 사용하여 서버를 호출하면 추가적인 여러가지 일을 할 수 있다는 점입니다.
접근제어 및 캐싱
어떤 데이터 조회 작업이 반복적으로 발생하지만 결과가 자주 바뀌지 않는다면, 프록시가 이전 결과를 캐싱해두고 동일한 요청에 대해 빠르게 응답할 수 있습니다.
프록시 체인
하나의 실제 객체(RealSubject)를 감싸는 여러 개의 프록시들이 계층적으로 중첩되어 클라이언트 요청을 처리하는 구조를 말합니다.
예를 들면 Client → LoggingProxy → CachingProxy → TransactionProxy → RealService 순으로 요청을 전달할 수 있습니다.
LoggingProxy: 요청이 들어왔다는 로그 출력
CachingProxy: 결과가 캐시에 있는지 확인
TransactionProxy: 트랜잭션 시작
RealService: 실제 비즈니스 로직 실행
부가기능 추가
데코레이터 패턴과 같이 사용하여 여러 부가기능을 추가할 수 있습니다.
정적 프록시는 직접 코드를 작성해서 프록시 클래스를 만들어야 하지만, 동적 프록시는 런타임에 자동으로 프록시 객체를 생성해줍니다.
정적 프록시를 사용하면 프록시 적용 대상만큼 프록시 클래스를 만들어야합니다. 100개가 적용 대상이라면 프록시도 100개를 만들어야한다는 것입니다. 그래서 이러한 문제를 해결하기 위해 동적 프록시를 사용합니다.
JDK 동적 프록시는 인터페이스 기반의 프록시 객체를 런타임에 생성하는 방식입니다. 자바의 java.lang.reflect.Proxy 클래스를 활용하여 동작하며, 다음과 같은 특징이 있습니다
동적 프록시가 무엇인지 느낌이 잘 오지 않을테니 코드로 살펴보겠습니다.
Subject1, Subject2: Client에서 사용할 인터페이스
public interface Subject1 {
void call();
}
public interface Subject2 {
void call();
}
RealSubject1, RealSubject2: 프록시의 적용대상
@Slf4j
public class RealSubject1 implements Subject1 {
@Override
public void call() {
log.info("서버1 시작");
log.info("서버1 끝");
}
}
@Slf4j
public class RealSubject2 implements Subject2 {
@Override
public void call() {
log.info("서버2 시작");
log.info("서버2 끝");
}
}
DynamicProxyHandler: 동적 프록시를 만들어주는 핸들러
@Slf4j
public class DynamicProxyHandler implements InvocationHandler {
private Object target;
public DynamicProxyHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
log.info("동적 프록시 실행");
Object result = method.invoke(target, args);
log.info("동적 프록시 종료");
return result;
}
}
invoke()메서드:
Object proxy : 프록시 자신
Method method : 호출한 메서드
Object[] args : 메서드를 호출할 때 전달한 인수
이 핸들러는 여러 프록시 적용대상의 프록시를 만들어 주는 역할을 합니다.
테스트 코드
@Slf4j
public class JdkDynamicTest {
@Test
public void dynamicTest1() {
Subject1 subject1 = new RealSubject1();
DynamicProxyHandler handler1 = new DynamicProxyHandler(subject1);
Subject1 proxy1 = (Subject1) Proxy.newProxyInstance(subject1.getClass().getClassLoader(), new Class[]{Subject1.class}, handler1);
proxy1.call();
log.info("subject1 = {}", subject1.getClass());
log.info("proxy1 = {}", proxy1.getClass());
Subject2 subject2 = new RealSubject2();
DynamicProxyHandler handler2 = new DynamicProxyHandler(subject2);
Subject2 proxy2 = (Subject2) Proxy.newProxyInstance(subject2.getClass().getClassLoader(), new Class[]{Subject2.class}, handler2);
proxy2.call();
log.info("subject2 = {}", subject2.getClass());
log.info("proxy1 = {}", proxy2.getClass());
}
}
테스트 결과

앞에서 프록시 패턴에서 프록시는 상속을 통해서도 생성가능하다고 설명드렸습니다. 하지만 JDK동적 프록시는 인터페이스를 통한 프록시만 생성할 수 있고, 상속을 통한 프록시는 생성하지 못합니다. 이는 CGLIB을 활용하여 해결가능합니다.
CGLIB은 바이트코드를 조작하여 구체 클래스의 서브클래스를 런타임에 생성하므로, 인터페이스가 없어도 프록시 객체를 만들 수 있습니다.
RealSubject1, RealSubject2: 프록시의 적용대상
@Slf4j
public class RealSubject1{
@Override
public void call() {
log.info("서버1 시작");
log.info("서버1 끝");
}
}
@Slf4j
public class RealSubject2{
@Override
public void call() {
log.info("서버2 시작");
log.info("서버2 끝");
}
}
CglibProxyHandler: 동적 프록시를 만들어주는 핸들러
@Slf4j
public class CglibProxyHandler implements MethodInterceptor {
private Object target;
public CglibProxyHandler(Object target) {
this.target = target;
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
log.info("동적 프록시 실행");
Object result = proxy.invoke(target, args);
log.info("동적 프록시 종료");
return result;
}
}
Object proxy : 프록시 자신Method method : 호출한 메서드Object[] args : 메서드를 호출할 때 전달한 인수proxy : 메서드 호출에 사용테스트 코드
@Slf4j
public class CglibTest {
@Test
public void cglibTest() {
RealSubject1 subject1 = new RealSubject1();
Enhancer enhancer1 = new Enhancer();
enhancer1.setSuperclass(RealSubject1.class);
enhancer1.setCallback(new CglibProxyHandler(subject1));
RealSubject1 proxy1 = (RealSubject1) enhancer1.create();
proxy1.call();
log.info("subject1 = {}", subject1.getClass());
log.info("proxy1 = {}", proxy1.getClass());
RealSubject2 subject2 = new RealSubject2();
Enhancer enhancer2 = new Enhancer();
enhancer2.setSuperclass(RealSubject2.class);
enhancer2.setCallback(new CglibProxyHandler(subject2));
RealSubject2 proxy2 = (RealSubject2) enhancer2.create();
proxy2.call();
log.info("subject2 = {}", subject2.getClass());
log.info("proxy2 = {}", proxy2.getClass());
}
}
테스트 결과

이번 포스트에서는 프록시의 기본 개념과 프록시 패턴, 그리고 자바에서 제공하는 두 가지 동적 프록시 방식인 JDK 동적 프록시와 CGLIB 프록시에 대해 알아보았습니다.
| 구분 | JDK Dynamic Proxy | CGLIB Proxy |
|---|---|---|
| 기반 | 인터페이스 기반 | 클래스 상속 기반 |
| 사용 조건 | 인터페이스 필요 | 인터페이스 없어도 가능 |
| 생성 방식 | Proxy.newProxyInstance() | Enhancer.create() |
| 사용 예 | 스프링 AOP (인터페이스 기반) | 스프링 AOP (클래스 기반) |
코드를 짜다보면 상속기반 프록시와 인터페이스 기반 프록시를 혼용해서 사용해야할 때가 있습니다. 그러면 이럴 때마다 JDK와 CGLIB을 각각 따로 구현 해야할까요?
이에 대한 해결책으로 스프링은 상황에 맞게 JDK와 CGLIB을 통합해서 사용할 수 있는 ProxyFactory를 제공합니다.
다음 포스트에서는 프록시가 스프링에서 어떻게 활용되는지 알아보겠습니다.
출처
책: 『java언어로 배우는 디자인 패턴 입문』을 참고했습니다.
강의: 인프런의 김영한님의 『스프링 핵심 원리-고급편』을 듣고 작성했습니다.