프록시 패턴을 이해하기 위한 예제 코드를 작성해보자.
프록시 패턴 - 적용 전
먼저 프록시 패턴을 도입하기 전 코드를 아주 단순하게 만들어보자.

Subject 인터페이스
public interface Subject {
String operation();
}
예제에서 Subject 인터페이스는 단순히 operation() 메서드 하나만 가지고 있다.
RealSubject
@Slf4j
public class RealSubject implements Subject {
@Override
public String operation() {
log.info("실제 객체 호출");
sleep(1000); // 1초 쉬기
return "data";
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
RealSubject는 Subject인터페이스를 구현했다. operation() 은 데이터 조회를 시뮬레이션 하기 위해 1초 쉬도록 했다. .
ProxyPatternClient
public class ProxyPatternClient {
private Subject subject;
public ProxyPatternClient(Subject subject) {
this.subject = subject;
}
public void execute() {
subject.operation();
}
}
Subject 인터페이스에 의존하고, Subject를 호출하는 클라이언트 코드이다. execute()를 실행하면 subject.operation()를 호출한다.
ProxyPatternTest
public class ProxyPatternTest {
@Test
void noProxyTest() {
RealSubject realSubject = new RealSubject();
ProxyPatternClient client = new ProxyPatternClient(realSubject);
client.execute();
client.execute();
client.execute();
}
}
테스트 코드에서는 client.execute()를 3번 호출한다. 데이터를 조회하는데 1초가 소모되므로 총 3초의 시간이 걸린다.
실행 결과
실제 객체 호출
실제 객체 호출
실제 객체 호출
client.execute()을 3번 호출하면 다음과 같이 처리된다.
- client -> realSubject 를 호출해서 값을 조회한다. (1초)
- client -> realSubject 를 호출해서 값을 조회한다. (1초)
- client -> realSubject 를 호출해서 값을 조회한다. (1초)
그런데 이 데이터가 한번 조회하면 변하지 않는 데이터라면 어딘가에 보관해두고 이미 조회한 데이터를 사용하는 것이 성능상 좋다. 이런 것을 캐시라고 한다. 프록시 패턴의 주요 기능은 접근 제어이다. 캐시도 접근 자체를 제어하는 기능 중 하나이다.
이미 개발된 로직을 전혀 수정하지 않고, 프록시 객체를 통해서 캐시를 적용해보자.
프록시 패턴 - 적용 후
프록시 패턴을 적용하자.

클라이언트가 proxy를 참조하고, proxy는 realSubject 참조하도록 proxy 끼워넣기
RealSubject
@Slf4j
public class RealSubject implements Subject {
@Override
public String operation() {
log.info("실제 객체 호출");
sleep(1000);
return "data";
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
CachedProxy
@Slf4j
public class CacheProxy implements Subject {
private Subject target; // RealSubject
private String cacheValue;
public CacheProxy(Subject target) {
this.target = target;
}
@Override
public String operation() {
log.info("프록시 호출");
if (cacheValue == null) {
// cachedValue가 없을 땐 realSubject의 operation호출
// "data"가 cachedValue에 들어감
cacheValue = target.operation();
}
// cachedValue있으면 바로 cachedValue 반환
return cacheValue;
}
}
앞서 설명한 것 처럼 프록시도 실제 객체와 그 모양이 같아야 하기 때문에 Subject 인터페이스를 구현해야 한다.
- private Subject target : 클라이언트가 프록시를 호출하면 프록시가 최종적으로 실제 객체를 호출해야 한다. 따라서 내부에 실제 객체(RealSubject)의 참조를 가지고 있어야 한다. 이렇게 프록시가 호출하는 대상을 target이라 한다.
- operation() : 구현한 코드를 보면 cacheValue에 값이 없으면 실제 객체(target)를 호출해서 값을 구한다. 그리고 구한 값을 cacheValue 에 저장하고 반환한다. 만약 cacheValue 에 값이 있으면 실제 객체를 전혀 호출하지 않고, 캐시 값을 그대로 반환. 따라서 처음 조회 이후에는 캐시(cachedValue)에서 매우 빠르게 데이터를 조회할 수 있다.
ProxyPatternClient
public class ProxyPatternClient {
private Subject subject;
public ProxyPatternClient(Subject subject) {
this.subject = subject; // CachedProxy 주입
}
public void execute() {
subject.operation();
}
}
ProxyPatternTest - cacheProxyTest() 추가
public class ProxyPatternTest {
@Test
void noProxyTest() {
RealSubject realSubject = new RealSubject();
ProxyPatternClient client = new ProxyPatternClient(realSubject);
client.execute();
client.execute();
client.execute();
}
// 추가된 테스트
@Test
void cacheProxyTest() {
Subject realSubject = new RealSubject();
Subject cacheProxy = new CacheProxy(realSubject);
ProxyPatternClient client = new ProxyPatternClient(cacheProxy);
client.execute();
client.execute();
client.execute();
}
}
cacheProxyTest()
client → cacheProxy → realSubject 런타임 객체 의존 관계가 완성된다.

cacheProxyTest()는 client.execute()을 총 3번 호출한다. 이번에는 클라이언트가 실제 realSubject를 호출하는 것이 아니라 cacheProxy를 호출하게 된다.
실행 결과
프록시 호출
실제 객체 호출
프록시 호출
프록시 호출
client.execute()을 3번 호출하면 다음과 같이 처리된다.
- client의 cacheProxy 호출 → cacheProxy에 캐시 값이 없다. realSubject를 호출, 결과를 캐시에 저장 (1초)
- client의 cacheProxy 호출 cacheProxy에 캐시 값이 있다. cacheProxy에서 즉시 반환 (0초)
- client의 cacheProxy 호출 cacheProxy에 캐시 값이 있다. cacheProxy에서 즉시 반환 (0초)
결과적으로 캐시 프록시를 도입하기 전에는 3초가 걸렸지만,
캐시 프록시 도입 이후에는 최초에 한번만 1초가 걸리고, 이후에는 거의 즉시 반환한다.
정리 프록시 패턴의 핵심은 RealSubject 코드와 클라이언트 코드를 전혀 변경하지 않고, 프록시를 도입해서 접근 제어를 했다는 점이다. 그리고 클라이언트 코드의 변경 없이 자유롭게 프록시를 넣고 뺄 수 있다.
실제 클라이언트 입장에서는 프록시 객체가 주입되었는지, 실제 객체가 주입되었는지 알지 못한다.
'Spring, JPA 🌱 > 김영한 스프링 고급편' 카테고리의 다른 글
[김영한 스프링 고급/AOP] 데코레이터 패턴 (0) | 2025.02.07 |
---|---|
[김영한 스프링 고급 / AOP] 프록시 (0) | 2025.02.07 |
프록시 패턴을 이해하기 위한 예제 코드를 작성해보자.
프록시 패턴 - 적용 전
먼저 프록시 패턴을 도입하기 전 코드를 아주 단순하게 만들어보자.

Subject 인터페이스
public interface Subject { String operation(); }
예제에서 Subject 인터페이스는 단순히 operation() 메서드 하나만 가지고 있다.
RealSubject
@Slf4j public class RealSubject implements Subject { @Override public String operation() { log.info("실제 객체 호출"); sleep(1000); // 1초 쉬기 return "data"; } private void sleep(int millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { e.printStackTrace(); } } }
RealSubject는 Subject인터페이스를 구현했다. operation() 은 데이터 조회를 시뮬레이션 하기 위해 1초 쉬도록 했다. .
ProxyPatternClient
public class ProxyPatternClient { private Subject subject; public ProxyPatternClient(Subject subject) { this.subject = subject; } public void execute() { subject.operation(); } }
Subject 인터페이스에 의존하고, Subject를 호출하는 클라이언트 코드이다. execute()를 실행하면 subject.operation()를 호출한다.
ProxyPatternTest
public class ProxyPatternTest { @Test void noProxyTest() { RealSubject realSubject = new RealSubject(); ProxyPatternClient client = new ProxyPatternClient(realSubject); client.execute(); client.execute(); client.execute(); } }
테스트 코드에서는 client.execute()를 3번 호출한다. 데이터를 조회하는데 1초가 소모되므로 총 3초의 시간이 걸린다.
실행 결과
실제 객체 호출 실제 객체 호출 실제 객체 호출
client.execute()을 3번 호출하면 다음과 같이 처리된다.
- client -> realSubject 를 호출해서 값을 조회한다. (1초)
- client -> realSubject 를 호출해서 값을 조회한다. (1초)
- client -> realSubject 를 호출해서 값을 조회한다. (1초)
그런데 이 데이터가 한번 조회하면 변하지 않는 데이터라면 어딘가에 보관해두고 이미 조회한 데이터를 사용하는 것이 성능상 좋다. 이런 것을 캐시라고 한다. 프록시 패턴의 주요 기능은 접근 제어이다. 캐시도 접근 자체를 제어하는 기능 중 하나이다.
이미 개발된 로직을 전혀 수정하지 않고, 프록시 객체를 통해서 캐시를 적용해보자.
프록시 패턴 - 적용 후
프록시 패턴을 적용하자.

클라이언트가 proxy를 참조하고, proxy는 realSubject 참조하도록 proxy 끼워넣기
RealSubject
@Slf4j public class RealSubject implements Subject { @Override public String operation() { log.info("실제 객체 호출"); sleep(1000); return "data"; } private void sleep(int millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { e.printStackTrace(); } } }
CachedProxy
@Slf4j public class CacheProxy implements Subject { private Subject target; // RealSubject private String cacheValue; public CacheProxy(Subject target) { this.target = target; } @Override public String operation() { log.info("프록시 호출"); if (cacheValue == null) { // cachedValue가 없을 땐 realSubject의 operation호출 // "data"가 cachedValue에 들어감 cacheValue = target.operation(); } // cachedValue있으면 바로 cachedValue 반환 return cacheValue; } }
앞서 설명한 것 처럼 프록시도 실제 객체와 그 모양이 같아야 하기 때문에 Subject 인터페이스를 구현해야 한다.
- private Subject target : 클라이언트가 프록시를 호출하면 프록시가 최종적으로 실제 객체를 호출해야 한다. 따라서 내부에 실제 객체(RealSubject)의 참조를 가지고 있어야 한다. 이렇게 프록시가 호출하는 대상을 target이라 한다.
- operation() : 구현한 코드를 보면 cacheValue에 값이 없으면 실제 객체(target)를 호출해서 값을 구한다. 그리고 구한 값을 cacheValue 에 저장하고 반환한다. 만약 cacheValue 에 값이 있으면 실제 객체를 전혀 호출하지 않고, 캐시 값을 그대로 반환. 따라서 처음 조회 이후에는 캐시(cachedValue)에서 매우 빠르게 데이터를 조회할 수 있다.
ProxyPatternClient
public class ProxyPatternClient { private Subject subject; public ProxyPatternClient(Subject subject) { this.subject = subject; // CachedProxy 주입 } public void execute() { subject.operation(); } }
ProxyPatternTest - cacheProxyTest() 추가
public class ProxyPatternTest { @Test void noProxyTest() { RealSubject realSubject = new RealSubject(); ProxyPatternClient client = new ProxyPatternClient(realSubject); client.execute(); client.execute(); client.execute(); } // 추가된 테스트 @Test void cacheProxyTest() { Subject realSubject = new RealSubject(); Subject cacheProxy = new CacheProxy(realSubject); ProxyPatternClient client = new ProxyPatternClient(cacheProxy); client.execute(); client.execute(); client.execute(); } }
cacheProxyTest()
client → cacheProxy → realSubject 런타임 객체 의존 관계가 완성된다.

cacheProxyTest()는 client.execute()을 총 3번 호출한다. 이번에는 클라이언트가 실제 realSubject를 호출하는 것이 아니라 cacheProxy를 호출하게 된다.
실행 결과
프록시 호출 실제 객체 호출 프록시 호출 프록시 호출
client.execute()을 3번 호출하면 다음과 같이 처리된다.
- client의 cacheProxy 호출 → cacheProxy에 캐시 값이 없다. realSubject를 호출, 결과를 캐시에 저장 (1초)
- client의 cacheProxy 호출 cacheProxy에 캐시 값이 있다. cacheProxy에서 즉시 반환 (0초)
- client의 cacheProxy 호출 cacheProxy에 캐시 값이 있다. cacheProxy에서 즉시 반환 (0초)
결과적으로 캐시 프록시를 도입하기 전에는 3초가 걸렸지만,
캐시 프록시 도입 이후에는 최초에 한번만 1초가 걸리고, 이후에는 거의 즉시 반환한다.
정리 프록시 패턴의 핵심은 RealSubject 코드와 클라이언트 코드를 전혀 변경하지 않고, 프록시를 도입해서 접근 제어를 했다는 점이다. 그리고 클라이언트 코드의 변경 없이 자유롭게 프록시를 넣고 뺄 수 있다.
실제 클라이언트 입장에서는 프록시 객체가 주입되었는지, 실제 객체가 주입되었는지 알지 못한다.
'Spring, JPA 🌱 > 김영한 스프링 고급편' 카테고리의 다른 글
[김영한 스프링 고급/AOP] 데코레이터 패턴 (0) | 2025.02.07 |
---|---|
[김영한 스프링 고급 / AOP] 프록시 (0) | 2025.02.07 |