[24] 디자인 패턴 목록 - 구조 패턴 - 프록시 패턴(Proxy Patten)
출처 : 디자인 패턴에 뛰어들기 - 알렉산더 슈베츠 도서
프록시(Proxy)
다른 객체에 대한 대체 또는 자리표시자를 제공할 수 있는 구조 디자인 패턴이다.
- 원래 객체에 대한 접근을 제어한다.
- 이로 인해서 당신의 요청이 원래 객체에 전달되기 전 또는 후에 무언가를 수행할 수 있도록 한다.
문제
객체에 대한 접근을 제한하는 이유는 무엇인지, 이 질문에 답하기 위하여 방대한 양의 시스템 자원을 소비하는 거대한 객체가 있다고 가정해본다.
이 객체는 필요할 때가 있기는 하지만, 항상 필요한 것은 아니다.
데이터베이스 쿼리들은 정말 느릴 수 있다는 예시이다.
실제로 필요할 때만 이 객체를 만들어서 지연된 초기화를 구현할 수 있다.
그러면 객체의 모든 클라이언트들은 어떤 지연된 초기화 코드를 실행해야 한다.
- 불행히도 이것은 아마도 많은 코드 중복을 초래할 것이다.
이상적인 상황에서는 이 코드를 객체의 클래스에 직접 넣을 수 있겠지만,
그게 항상 가능한 것은 아니다.
- 예를 들어, 그 클래스가 폐쇄된 타사 라이브러리의 일부일 수 있다.
해결책
프록시 패턴은 원래 서비스 객체와 같은 인터페이스로 새 프록시 클래스를 생성하라고 제안해야 한다.
- 프록시 객체를 원래 객체의 모든 클라이언트들에 전달하도록 앱을 업데이트할 수 있다.
- 클라이언트로부터 요청을 받으면 이 프록시는 실제 서비스 객체를 생성하고 모든 작업을 이 객체에 위임한다.
프록시는 데이터베이스 객체로 자신을 변장한다는 예시이다.
- 프록시는 지연된 초기화 및 결괏값 캐싱을 클라이언트와 실제 데이터베이스 객체가 알지 못하는 상태에서 처리할 수 있다.
그러나 이것들은 무슨 소용이 있는지 궁금하실 것이다.
클래스의 메인 로직 이전이나 이후에 무언가를 실행해야 하는 경우
프록시는 해당 클래스를 변경하지 않고도 이 무언가를 수행할 수 있도록 한다.
프록시는 원래 클래스와 같은 인터페이스를 구현하므로 실제 서비스 객체를 기대하는 모든 클라이언트에 전달될 수 있다.
실제상황 적용
신용 카드는 현금과 마찬가지로 결제에 사용할 수 있다는 예시이다.
신용 카드는 은행 계좌의 프록시이며, 은행 계좌는 현금의 프록시인 것을 알 수 있다.
- 둘 다 같은 인터페이스를 구현하며 둘 다 결제에 사용될 수 있다.
- 신용 카드를 사용하는 소비자는 많은 현금을 가지고 다닐 필요가 없어서 기분이 좋을 것이다.
- 또한 상점 주인은 거래 수입을 은행에 가는 길에 강도를 당하거나 잃어버릴 위험 없이 계좌에 전자적으로 입금이 되기 때문에 기분이 좋을 것이다.
구조
1. 서비스 인터페이스는 서비스의 인터페이스를 선언한다.
- 프록시가 서비스 객체로 위장할 수 있으려면 이 인터페이스를 따라야 합니다.
- 프록시가 서비스 객체로 위장할 수 있으려면 이 인터페이스를 따라야 합니다.
2. 서비스는 어떤 유용한 비즈니스 로직을 제공하는 클래스이다.
3. 프록시 클래스에는 서비스 객체를 가리키는 참조 필드가 있다.
- 프록시가 요청의 처리(예: 초기화 지연, 로깅, 액세스 제어, 캐싱 등)를 완료하면, 그 후 처리된 요청을 서비스 객체에 전달한다.
- 일반적으로 프록시들은 서비스 객체들의 전체 수명 주기를 관리한다.
4. 클라이언트는 같은 인터페이스를 통해 서비스들 및 프록시들과 함께 작동해야 한다.
- 그러면 서비스 객체를 기대하는 모든 코드에 프록시를 전달할 수 있기 때문이다.
의사코드
이 예시는 프록시 패턴이 제삼자 유튜브 통합 라이브러리에 지연된 초기화 및 캐싱을 도입하는 데 어떻게 도움이 되는지 보여주고 있다.
프록시를 사용하여 서비스의 결과를 캐싱하는 예시이다.
이 라이브러리는 비디오 다운로드 클래스를 제공하나 매우 비효율적이다.
- 왜냐하면 클라이언트 앱이 같은 비디오를 여러 번 요청하면
라이브러리는 처음 다운로드한 파일을 캐싱하고,
재사용하는 대신 계속해서 같은 비디오를 다운로드하기 때문이다.
프록시 클래스는 원래 다운로더와 같은 인터페이스를 구현하고 이 다운로더에 모든 작업을 위임한다.
- 하지만, 앱이 같은 비디오를 두 번 이상 요청하면 이미 다운로드한 파일을 추적한 후 캐시 된 결과를 반환한다.
// 원격 서비스의 인터페이스.
interface ThirdPartyYouTubeLib is
method listVideos()
method getVideoInfo(id)
method downloadVideo(id)
// 서비스 연결자의 구상 구현.
// 이 클래스의 메서드들은 유튜브에서 정보를 요청할 수 있다.
// 해당 요청의 속도는 사용자와 유튜브의 인터넷 연결 속도에 따라 다를 것이다.
// 앱이 많은 요청을 동시에 처리하면 속도가 느려질 것이다.
// 이는 요청들이 모두 같은 정보를 요청하더라도 마찬가지이다.
class ThirdPartyYouTubeClass implements ThirdPartyYouTubeLib is
method listVideos() is
// 유튜브에 API 요청을 보낸다.
method getVideoInfo(id) is
// 어떤 비디오에 대한 메타데이터를 가져온다.
method downloadVideo(id) is
// 유튜브에서 동영상 파일을 다운로드한다.
// 일부 대역폭을 절약하기 위해 요청 결과를 캐시하고 일정 기간 보관할 수 있다.
// 그러나 이러한 코드를 서비스 클래스에 직접 넣는 것은 불가능할 수 있다.
// 예를 들어, 타사 라이브러리의 일부로 제공되었거나 `final`로 정의된 경우에는
// 서비스 클래스와 같은 인터페이스를 구현하는 새 프록시 클래스에 캐싱 코드를 넣는
// 이유가 바로 그 때문이다.
// 이 클래스는 실제 요청을 보내야 하는 경우에만 서비스 객체에 위임한다.
class CachedYouTubeClass implements ThirdPartyYouTubeLib is
private field service: ThirdPartyYouTubeLib
private field listCache, videoCache
field needReset
constructor CachedYouTubeClass(service: ThirdPartyYouTubeLib) is
this.service = service
method listVideos() is
if (listCache == null || needReset)
listCache = service.listVideos()
return listCache
method getVideoInfo(id) is
if (videoCache == null || needReset)
videoCache = service.getVideoInfo(id)
return videoCache
method downloadVideo(id) is
if (!downloadExists(id) || needReset)
service.downloadVideo(id)
// 서비스 객체와 직접 작업하던 그래픽 사용자 인터페이스 클래스는 서비스 객체와
// 인터페이스를 통해 작업하는 한 변경되지 않는다.
// 둘 다 같은 인터페이스를 구현하므로 실제 서비스 객체 대신
// 프록시 객체를 안전하게 전달할 수 있다.
class YouTubeManager is
protected field service: ThirdPartyYouTubeLib
constructor YouTubeManager(service: ThirdPartyYouTubeLib) is
this.service = service
method renderVideoPage(id) is
info = service.getVideoInfo(id)
// 비디오 페이지를 렌더링하세요.
method renderListPanel() is
list = service.listVideos()
// 비디오 섬네일 리스트를 렌더링하세요.
method reactOnUserInput() is
renderVideoPage()
renderListPanel()
// 앱은 언제든지 프록시를 설정할 수 있다.
class Application is
method init() is
aYouTubeService = new ThirdPartyYouTubeClass()
aYouTubeProxy = new CachedYouTubeClass(aYouTubeService)
manager = new YouTubeManager(aYouTubeProxy)
manager.reactOnUserInput()
적용
프록시 패턴을 활용하는 방법들은 수십 가지가 있으며, 패턴이 가장 많이 사용되는 용도들을 살펴본다.
지연된 초기화(가상 프록시). 이것은 어쩌다 필요한 무거운 서비스 객체가 항상 가동되어 있어 시스템 자원들을 낭비하는 경우이다.
- 앱이 시작될 때 객체를 생성하는 대신, 객체 초기화를 실제로 초기화가 필요한 시점까지 지연할 수 있다.
접근 제어 (보호 프록시). 당신이 특정 클라이언트들만 서비스 객체를 사용할 수 있도록 하려는 경우에 사용할 수 있다.
- 예를 들어, 당신의 객체들이 운영 체제의 중요한 부분이고 클라이언트들이 다양한 실행된 응용 프로그램(악의적인 응용 프로그램 포함)인 경우이다.
- 이 프록시는 클라이언트의 자격 증명이 어떤 정해진 기준과 일치하는 경우에만 서비스 객체에 요청을 전달할 수 있다.
원격 서비스의 로컬 실행 (원격 프록시). 서비스 객체가 원격 서버에 있는 경우이다.
- 이 경우 프록시는 네트워크를 통해 클라이언트 요청을 전달하여 네트워크와의 작업의 모든 복잡한 세부 사항을 처리한다.
요청들의 로깅(로깅 프록시). 서비스 객체에 대한 요청들의 기록을 유지하려는 경우이다.
- 프록시는 각 요청을 서비스에 전달하기 전에 로깅(기록)할 수 있다.
요청 결과들의 캐싱(캐싱 프록시). 이것은 클라이언트 요청들의 결과들을 캐시하고 이 캐시들의 수명 주기를 관리해야 할 때, 특히 결과들이 상당히 큰 경우에 사용된다.
- 프록시는 항상 같은 결과를 생성하는 반복 요청들에 대해 캐싱을 구현할 수 있다.
- 프록시는 요청들의 매개변수들을 캐시 키들로 사용할 수 있다.
스마트 참조. 이것은 사용하는 클라이언트들이 없어 거대한 객체를 해제할 수 있어야 할 때 사용된다.
- 프록시는 서비스 객체 또는 그 결과에 대한 참조를 얻은 클라이언트들을 추적할 수 있다.
- 때때로 프록시는 클라이언트들을 점검하여 클라이언트들이 여전히 활성 상태인지를 확인할 수 있다.
- 클라이언트 리스트가 비어 있으면 프록시는 해당 서비스 객체를 닫고 그에 해당하는 시스템 자원을 확보할 수 있다.
- 또 프록시는 클라이언트가 서비스 객체를 수정했는지도 추적할 수 있다.
- 변경되지 않은 객체는 다른 클라이언트들이 재사용할 수 있다.
구현방법
1.기존 서비스 인터페이스가 없는 경우, 서비스 인터페이스를 하나 생성하여 프록시와 서비스 객체 간의 상호 교환을 가능하게 만드세요.
- 서비스 클래스에서 인터페이스를 추출하는 것이 항상 가능한 것은 아니다.
- 왜냐하면 그 인터페이스를 사용하려면 서비스의 모든 클라이언트를 변경해야 하기 때문이다.
- 대신 프록시를 서비스 클래스의 자식 클래스로 만들 수 있으며, 이렇게 하면 서비스의 인터페이스를 상속하게 할 수 있다.
2.프록시 클래스를 만드세요. 이 클래스에는 서비스에 대한 참조를 저장하기 위한 필드가 있어야 한다.
- 일반적으로 프록시들은 서비스들의 전체 수명 주기를 생성하고 관리한다.
- 또 드물지만, 클라이언트가 서비스를 프록시의 생성자에 전달하는 방식으로 서비스가 프록시에 전달되기도 한다
3. 목적에 따라 프록시 메서드들을 구현하세요.
- 대부분의 경우 프록시는 일부 작업을 수행한 후에 그 작업을 서비스 객체에 위임해야 한다.
- 대부분의 경우 프록시는 일부 작업을 수행한 후에 그 작업을 서비스 객체에 위임해야 한다.
4. 클라이언트가 프록시를 받을지 실제 서비스를 받을지를 결정하는 생성 메서드를 도입하는 것을 고려하세요.
- 이 메서드는 프록시 클래스의 간단한 정적 메서드이거나 완전한 팩토리 메서드일 수도 있다.
- 이 메서드는 프록시 클래스의 간단한 정적 메서드이거나 완전한 팩토리 메서드일 수도 있다.
5. 서비스 객체에 대해 지연된 초기화 구현을 고려하세요.
장단점
장점
클라이언트들이 알지 못하는 상태에서 서비스 객체를 제어할 수 있다.
클라이언트들이 신경 쓰지 않을 때 서비스 객체의 수명 주기를 관리할 수 있다.
프록시는 서비스 객체가 준비되지 않았거나 사용할 수 없는 경우에도 작동한다.
개방/폐쇄 원칙. 서비스나 클라이언트들을 변경하지 않고도 새 프록시들을 도입할 수 있다.
단점
새로운 클래스들을 많이 도입해야 하므로 코드가 복잡해질 수 있다.
서비스의 응답이 늦어질 수 있다.
다른 패턴과의 관계
어댑터는 다른 인터페이스를, 프록시는 같은 인터페이스를, 데코레이터는 향상된 인터페이스를 래핑된 객체에 제공한다.
퍼사드 패턴은 복잡한 객체 또는 시스템을 보호하고 자체적으로 초기화한다는 점에서 프록시와 유사하다.
- 퍼사드 패턴과 달리 프록시는 자신의 서비스 객체와 같은 인터페이스를 가지므로 이들은 서로 상호 교환이 가능하다.
- 퍼사드 패턴과 달리 프록시는 자신의 서비스 객체와 같은 인터페이스를 가지므로 이들은 서로 상호 교환이 가능하다.
데코레이터와 프록시의 구조는 비슷하나 이들의 의도는 매우 다르다.
- 두 패턴 모두 한 객체가 일부 작업을 다른 객체에 위임해야 하는 합성 원칙을 기반으로 한다.
- 이 두 패턴의 차이점은 프록시는 일반적으로 자체적으로 자신의 서비스 객체의 수명 주기를 관리하는 반면 데코레이터의 합성은 항상 클라이언트에 의해 제어된다는 점이다.