본문 바로가기
Concepts/Design Pattern

구조 패턴 - 프록시(Proxy)

by ocwokocw 2021. 5. 20.

- 참조: GoF의 디자인패턴

- 참조: https://refactoring.guru/design-patterns/proxy

- 프록시 패턴

다른이름으로 대리자(Surrogate) 라고도 하며 다른 객체에 대한 접근을 제어하기 위한 역할을 한다.


- 문제점

많은 시스템의 리소스를 소비하는 객체가 있다고 가정하자. 해당 객체가 특정 시점이 되면 사용되겠지만 언제나 필요한건 아니다. 이때 흔히 알고 있는 Lazy loading 기법을 사용할 수 있다. 정말 필요한 시점에만 해당 객체를 생성하고, 해당 객체의 모든 클라이언트는 초기화 코드를 수행하면 된다. 하지만 이런 방식으로 구현하면 코드의 중복이 너무 많이 나타난다.

 

이런 코드를 해당 객체에 직접 넣는 방법도 있지만 만약 해당 클래스가 3rd-party 라이브러리라면 이 방법 또한 불가능하다.


- 해결책

프록시 패턴에서는 원본 서비스를 제공하는 객체와 동일한 인터페이스를 구현하는 새로운 프록시 클래스를 생성하여 이 문제를 해결한다. 그리고 새로 생성한 프록시 클래스를 원래 서비스를 사용하는 객체의 클라이언트에게 제공한다. 클라이언트가 요청을 보내면, 프록시 클래스는 실제 서비스 객체를 만들고, 요청을 위임한다.

 

이렇게 하면 뭐가 좋을까? 원본 서비스 클래스의 변경없이 전처리나 후처리 기능을 제공할 수 있다. 프록시는 원본 서비스 객체의 인터페이스를 동일하게 구현하기 때문에 클라이언트 입장에서는 원본 서비스를 사용하는것과 다를바가 없다.


- 실생활 예제

신용카드는 은행 계좌를 위한 프록시이며, 은행 계좌는 현금을 위한 프록시이다. 둘 다 결제행위를 할 수 있는 같은 인터페이스를 구현하고 있다. 소비자는 돈을 직접 들고다니지 않아도 되기 때문에 상당히 편리하다. 또한 가게의 주인도 보증금을 잃어버릴 위험이나 강도에 대한 위험요소를 감수하지 않아도 되기 때문에 편리하다.


- 시나리오

유투브의 비디오들을 제공하는 라이브러리가 있다. 하지만 한 가지 불편한점이 있는데, 만약 클라이언트 어플리케이션이 똑같은 비디오를 여러번 요청할때에도 해당 라이브러리는 과거에 요청한 정보를 캐싱하지 않고, 단순하게 요청 처리를 반복한다. 

 

프록시 클래스는 원래 비디오 라이브러리와 동일한 인터페이스를 구현하여 모든 일을 위임한다. 하지만 한 가지 다른점이 있는데, 어플리케이션에서 과거 다운로드 요청이 있었던 비디오 파일에 대해서는 캐싱된 결과를 반환한다.

위 다이어그램에서 Cached YouTubeClass 는 캐싱 기능이 추가된 Proxy 클래스이다. Proxy 클래스에서는 원본 서비스인 ThirdPartyYouTubeClass를 참조하고 있다. 


- 활용성

프록시 패턴은 아래와 같이 다양한 방식으로 사용될 수 있다.

  • 원격지 프록시(remote proxy): 서로 다른 주소공간에 존재하는 객체를 가리키는 대표 객체이며 로컬 환경에 위치한다.
  • 가상 프록시(virtual proxy): Lazy loading 개념으로, 요청이 필요한 때만 고비용 객체를 생성한다.
  • 보호용 프록시(protection proxy): 원격 객체에 대한 실제 접근을 제어한다. 객체 별로 접근 권한이 다를 때 유용하게 사용가능하다.
  • 스마트 참조자(smart reference): 원시 포인터의 대체용 객체로 실제 객체에 접근이 일어날 때 추가적인 행동을 수행한다.
  • 로깅 프록시(logging proxy): 원본 서비스에 접근하는 요청에 대한 기록을 남길 수 있다.
  • 캐싱 프록시(caching proxy): 위에서 언급한 예제로 어떤 요청에 대해 캐싱을 할 수 있다.

- Java 예제

먼저 유투브 video의 메타정보를 담고 있는 YouTubeVideo 클래스를 정의하였다. 별다른 특이사항은 없고, Video의 Id, 제목, 내용등을 설정할 수 있다.

 

public class YouTubeVideo{

	private String videoId;
	private String subject;
	private String content; 
	
	public YouTubeVideo(String videoId, String subject, String content) {
		super();
		this.videoId = videoId;
		this.subject = subject;
		this.content = content;
	}

	public String getVideoId() {
		return videoId;
	}

	public String getSubject() {
		return subject;
	}

	public void setSubject(String subject) {
		this.subject = subject;
	}

	public String getContent() {
		return content;
	}

	public void setContent(String content) {
		this.content = content;
	}

	@Override
	public String toString() {
		return "YouTubeVideo [videoId=" + videoId + ", subject=" + subject + ", content=" + content + "]";
	}
}

 

그리고 원본 Service의 인터페이스에는 Video의 목록을 반환하는 기능과 해당 video 상세 정보 조회 그리고 video를 다운로드 받는 기능이 있다.

 

public interface ThirdPartyYouTubeLib {

	List<YouTubeVideo> listVideos();
	
	Optional<YouTubeVideo> getVideoInfo(String id);
	
	void downloadVideo(String id);
}

 

원본 서비스의 기능을 구현해보자. 만약 Third-party 라이브러리 형태로 사용하는 경우 .jar 를 import 하여 사용하고, 기능을 직접 구현할 일은 없겠지만 원활한 Test를 위해서 클래스를 만들어보았다.

 

public class ThirdPartyYouTubeClass implements ThirdPartyYouTubeLib {

	public ThirdPartyYouTubeClass() {
		super();
	}

	@Override
	public List<YouTubeVideo> listVideos() {
		return loadRemoteVideoInfos();
	}

	private List<YouTubeVideo> loadRemoteVideoInfos() {
		return Arrays.asList(
				new YouTubeVideo("1", "Subject 1", "Content 1"),
				new YouTubeVideo("2", "Subject 2", "Content 2"),
				new YouTubeVideo("3", "Subject 3", "Content 3"),
				new YouTubeVideo("4", "Subject 4", "Content 4"));
	}
	
	@Override
	public Optional<YouTubeVideo> getVideoInfo(String id) {
		
		return loadRemoteVideoInfo(id);
	}

	private Optional<YouTubeVideo> loadRemoteVideoInfo(String id) {
		
		if(Arrays.asList("1", "2", "3", "4").contains(id)) {
			return Optional.ofNullable(
				new YouTubeVideo(id, "Subject " + id, "Content " + id));
		}
		
		return Optional.empty();
	}
	
	@Override
	public void downloadVideo(String id) {
		System.out.println("Download video");
	}
}

 

원본 서비스를 참조하는 Proxy 객체를 구현할 차례이다. youTubeService 속성명으로 원본 서비스를 참조하고 있다.

Video의 목록을 불러오는 메소드나 Video 정보를 불러오는 메소드에서는 기존 cache 데이터가 있는지 확인 후, 없으면 원번 서비스의 메소드를 호출한다.

 

public class CachedYouTubeClass implements ThirdPartyYouTubeLib {

	private ThirdPartyYouTubeLib youTubeService;
	private List<YouTubeVideo> videoListCache;
	private Optional<YouTubeVideo> videoCache;
	
	public CachedYouTubeClass(ThirdPartyYouTubeLib youTubeService) {
		super();
		this.youTubeService = youTubeService;
	}
	
	@Override
	public List<YouTubeVideo> listVideos() {
		
		if(videoListCache == null) {
			videoListCache = youTubeService.listVideos();
		}
		
		return videoListCache;
	}

	@Override
	public Optional<YouTubeVideo> getVideoInfo(String id) {
		
		if(videoCache == null || 
			!videoCache.filter(video -> id.equals(video.getVideoId())).isPresent()) {
			System.out.println("load video info from server!");
			videoCache = youTubeService.getVideoInfo(id);
		}
		else {
			System.out.println("load video info from cache!");
		}
		
		return videoCache;
	}

	@Override
	public void downloadVideo(String id) {
		youTubeService.downloadVideo(id);
	}

}

 

Proxy 객체를 사용하는 클라이언트를 구현해보자. 생성자를 살펴보면 Proxy 객체와 원본 서비스가 구현하고 있는 Interface 형으로 생성자 인자를 받는다.

 

클라이언트 객체 생성시 Proxy 객체를 사용하지 않는다면 원본 서비스를 생성자 인자로 전달할 것이고, Caching을 위해 Proxy 객체를 사용한다면 Proxy 객체를 생성자 인자로 넘길것이다.

 

public class YouTubeManager {

	private ThirdPartyYouTubeLib thirdPartyYouTubeLib;
	
	public YouTubeManager(ThirdPartyYouTubeLib thirdPartyYouTubeLib) {
		this.thirdPartyYouTubeLib = thirdPartyYouTubeLib;
	}
	
	public void renderVideoPanel() {
		
		List<YouTubeVideo> youTubeVideos = thirdPartyYouTubeLib.listVideos();
		
		youTubeVideos.stream()
			.forEach(System.out::println);
	}
	
	public void renderVideoPage(String id) {
		
		Optional<YouTubeVideo> videoInfo = thirdPartyYouTubeLib.getVideoInfo(id);
		
		videoInfo.ifPresent(System.out::println);
	}
	
	public void watchVideoClip(String id) {
		
		thirdPartyYouTubeLib.downloadVideo(id);
	}
}

 

위의 클래스들을 이용하여 Test 메소드를 작성하였다. Proxy 객체에서 원본 서비스를 참조하고 있으므로, 원본 서비스를 먼저 생성한 후, 이를 Proxy 객체의 생성자 인자로 넘겼다. 또한 클라이언트 객체는 Proxy 객체를 사용하므로 해당 객체를 생성자 인자로 넘겼다.

 

실행결과를 보면 동일한 video id 의 정보조회시 cache 로 부터 조회한것을 알 수 있다.

 

public static void main(String[] args) {
		
		ThirdPartyYouTubeClass originYouTubeService = new ThirdPartyYouTubeClass();
		CachedYouTubeClass proxyYouTube = new CachedYouTubeClass(originYouTubeService);
		
		YouTubeManager youTubeManager = new YouTubeManager(proxyYouTube);
		
		System.out.println("========Draw render list panel");
		youTubeManager.renderVideoPanel();
		
		System.out.println();
		System.out.println("========Render video page");
		youTubeManager.renderVideoPage("1");
		youTubeManager.renderVideoPage("1");
		youTubeManager.renderVideoPage("1");
		youTubeManager.renderVideoPage("2");
		youTubeManager.renderVideoPage("3");
		youTubeManager.renderVideoPage("4");
		youTubeManager.renderVideoPage("1");
		youTubeManager.renderVideoPage("1");
	}

실행결과
========Draw render list panel
YouTubeVideo [videoId=1, subject=Subject 1, content=Content 1]
YouTubeVideo [videoId=2, subject=Subject 2, content=Content 2]
YouTubeVideo [videoId=3, subject=Subject 3, content=Content 3]
YouTubeVideo [videoId=4, subject=Subject 4, content=Content 4]

========Render video page
load video info from server!
YouTubeVideo [videoId=1, subject=Subject 1, content=Content 1]
load video info from cache!
YouTubeVideo [videoId=1, subject=Subject 1, content=Content 1]
load video info from cache!
YouTubeVideo [videoId=1, subject=Subject 1, content=Content 1]
load video info from server!
YouTubeVideo [videoId=2, subject=Subject 2, content=Content 2]
load video info from server!
YouTubeVideo [videoId=3, subject=Subject 3, content=Content 3]
load video info from server!
YouTubeVideo [videoId=4, subject=Subject 4, content=Content 4]
load video info from server!
YouTubeVideo [videoId=1, subject=Subject 1, content=Content 1]
load video info from cache!
YouTubeVideo [videoId=1, subject=Subject 1, content=Content 1]

댓글