프록시는 다른 객체에 대한 대체 또는 자리표시자를 제공할 수 있는 구조 디자인 패턴이다. 프록시는 원래 객체에 대한 접근을 제어하므로, 요청이 원래 객체에 전달되기 전 또는 후에 무언가를 수행할 수 있도록 한다.
객체에 대한 접근을 제어하는 이유가 무엇일까? 방대한 양의 시스템 자원을 소비하는 객체가 있다. 이 객체는 항상 필요하지 않다. 필요할 때만 객체를 만들어서 지연된 초기화를 구현할 수 있으나 객체의 모든 클라이언트 코드들이 지연된 초기화 코드를 실행해야 한다. 이는 많은 코드 중복을 일으킨다.
프록시 패턴은 원래 서비스 객체와 같은 인터페이스로 새 프록시 클래스를 생성한다. 생성하면 프록시 객체를 원래 객체의 모든 클라이언트에 전달하도록 업데이트 할 수 있다. 클라이언트로부터 요청을 받으면 이 프록시는 실제 서비스 객체를 생성하고 모든 작업을 이 객체에 위임한다.
클래스의 메인 로직 이전이나 이후에 무언가를 실행해야 하는 경우 프록시 패턴은 원래 클래스와 같은 인터페이스를 구현하므로 실제 서비스 객체를 기대하는 모든 클라이언트에 전달될 수 있다.
신용 카드는 은행 계좌의 프록시이며, 은행 계좌는 현금의 프록시이다. 둘 다 같은 인터페이스를 구현하며 둘 다 결제에 사용될 수 있다.
아래 예제는 제삼자 유튜브 통합 라이브러리에서 지연된 초기화 및 캐싱을 프록시 패턴으로 해결하는 방법을 보여준다.
이 라이브러리는 동영상 다운로드 클래스를 제공하나 매우 비효율적이다. 클라이언트 앱이 같은 동영상을 여러 번 요청하면 라이브러리는 처음 다운로드한 파일을 캐싱하고 재사용하는 대신 계속해서 같은 비디오를 다운로드하기 때문이다.
따라서 프록시 클래스는 원래 다운로드 클래스와 같은 인터페이스를 구현하고 이 다운로드 클래스에 모든 작업을 위힘하나, 앱이 같은 비디오를 두 번 이상 요청하면 이미 다운로드한 파일을 추적해 캐시된 결과를 반환한다.
import java.util.HashMap;
public interface ThirdPartyYouTubeLib {
HashMap<String, Video> popularVideos();
Video getVideo(String VideoId);
}
import java.util.HashMap;
public class ThirdPartyYoutubeClass implements ThirdPartyYouTubeLib{
@Override
public HashMap<String, Video> popularVideos() {
connectToServer("http://www.youtube.com");
return getRandomVideos();
}
@Override
public Video getVideo(String videoId) {
connectToServer("http://www.youtube.com/" + videoId);
return getSomeVideo(videoId);
}
//Fake methods to simulate network activity. They as slow as a real life.
private int random(int min, int max){
return min + (int) (Math.random() * ((max - min) + 1));
}
private void experienceNetworkLatency(){
int randomLatency = random(5, 10);
for (int i = 0; i < randomLatency; i++) {
try{
Thread.sleep(100);
} catch (InterruptedException ex){
ex.printStackTrace();
}
}
}
private void connectToServer(String server){
System.out.print("Connecting to " + server + " ... ");
experienceNetworkLatency();
System.out.print("Connected!" + "\n");
}
private HashMap<String, Video> getRandomVideos(){
System.out.print("Downloading populars...");
experienceNetworkLatency();
HashMap<String, Video> hmap = new HashMap<String, Video>();
hmap.put("catzzzzzzzzz", new Video("sadgahasgdas", "Catzzzz.avi"));
hmap.put("mkafksangasj", new Video("mkafksangasj", "Dog play with ball.mp4"));
hmap.put("dancesvideoo", new Video("asdfas3ffasd", "Dancing video.mpq"));
hmap.put("dlsdk5jfslaf", new Video("dlsdk5jfslaf", "Barcelona vs RealM.mov"));
hmap.put("3sdfgsd1j333", new Video("3sdfgsd1j333", "Programing lesson#1.avi"));
System.out.print("Done!" + "\n");
return hmap;
}
private Video getSomeVideo(String videoId){
System.out.print("Downloading video ..");
experienceNetworkLatency();
Video video = new Video(videoId, "some video title");
System.out.print("Done!" + "\n");
return video;
}
}
import java.util.HashMap;
public class YoutubeCacheProxy implements ThirdPartyYouTubeLib{
private ThirdPartyYouTubeLib youtubeService;
private HashMap<String, Video> cachePopular = new HashMap<String, Video>();
private HashMap<String, Video> cacheAll = new HashMap<String, Video>();
public YoutubeCacheProxy(){
this.youtubeService = new ThirdPartyYoutubeClass();
}
@Override
public HashMap<String, Video> popularVideos() {
if(cachePopular.isEmpty()){
cachePopular = youtubeService.popularVideos();
} else {
System.out.println("Retrieved list from cache.");
}
return cachePopular;
}
@Override
public Video getVideo(String videoId) {
Video video = cacheAll.get(videoId);
if(video == null){
video = youtubeService.getVideo(videoId);
cacheAll.put(videoId, video);
} else{
System.out.println("Retrieved video '" + videoId + "' from cache.");
}
return video;
}
public void reset(){
cachePopular.clear();
cacheAll.clear();
}
}
import java.util.HashMap;
public class YoutubeDownloader {
private ThirdPartyYouTubeLib api;
public YoutubeDownloader(ThirdPartyYouTubeLib api){
this.api = api;
}
public void renderVideoPage(String videoId){
Video video = api.getVideo(videoId);
System.out.println("\n-------------------------------");
System.out.println("Video page (imagine fancy HTML)");
System.out.println("ID: " + video.id);
System.out.println("Title: " + video.title);
System.out.println("Video: " + video.data);
System.out.println("-------------------------------\n");
}
public void renderPopularVideos(){
HashMap<String, Video> list = api.popularVideos();
System.out.println("\n------------------------------");
System.out.println("Most popular videos on YouTube (imagine fancy HTML)");
for(Video video: list.values()){
System.out.println("ID: " + video.id + " / Title: " + video.title);
}
System.out.println("-------------------------------\n");
}
}
public class Demo {
public static void main(String[] args) {
YoutubeDownloader naiveDownloader = new YoutubeDownloader(new ThirdPartyYoutubeClass());
YoutubeDownloader smartDownloader = new YoutubeDownloader((new YoutubeCacheProxy()));
long naive = test(naiveDownloader);
long smart = test(smartDownloader);
System.out.print("Time saved by caching proxy: " +(naive - smart) + "ms");
}
private static long test(YoutubeDownloader downloader){
long startTime = System.currentTimeMillis();
// User behavior in our app:
downloader.renderPopularVideos();
downloader.renderVideoPage("catzzzzzzzzz");
downloader.renderPopularVideos();
downloader.renderVideoPage("dancesvideoo");
// Users might visit the same page quite often.
downloader.renderVideoPage("catzzzzzzzzz");
downloader.renderVideoPage("someothervid");
long estimatedTime = System.currentTimeMillis() - startTime;
System.out.print("Time elapsed: " + estimatedTime + "ms\n");
return estimatedTime;
}
}
Time elapsed: 6121ms
Time saved by caching proxy: 2370ms
비디오 다운로드 클래스만 사용할 때 라이브러리는 처음 다운로드한 파일을 캐싱하고 재사용하는 대신 계속해서 같은 비디오를 다운로드한다.
그러나 프록시 패턴 즉, 프록시 클래스를 사용하면 앱이 같은 비디오를 두 번 이상 요청하면 이미 다운로드한 파일을 추적해 캐시된 결과를 반환한다. 따라서 실행 결과가 증명하듯이 속도 차이가 현저하다.
디자인 패턴에 뛰어들기
스프링 입문을 위한 자바 객체 지향의 원리와 이해