프록시 패턴 (proxy pattern)

Park Jong Ho·2023년 1월 1일
0

프록시 패턴

이번 포스트에서는 프록시 패턴에 대해서 알아보자. Proxy란 우리말로 대리인이라는 뜻을 지닌다. 대리인은 남의 일을 대신 수행하는 역할을 하는 사람인데, 프로그램에서도 똑같은 의미로 사용되고 있는 패턴이다.

이미지를 불러오는 작업을 하는 RealImageLoader라는 객체가 있다고 생각해보자.

final class RealImageLoader {
    func image(_ completion: (NSImage) -> Void) {
      //...
    }
}

그렇다면 클라이언트는 이미지를 불러오기 위해서 RealImageLoader의 인스턴스를 생성하고, image 함수를 호출해야 한다.

하지만 이미지는 용량이 매우 큰 데이터로, 우리는 캐시를 적절히 활용해 리소스를 아낄 수 있다. 예를 들어 url에서 이미지를 불러오는 경우, 해당 url 자체를 캐시키로 사용할 수도 있다.

따라서 이미지가 캐시에 이미 있는 경우 캐시에서 불러오고, 아니면 실제로 이미지를 요청하는 객체를 생성하고자 한다. 그러나 ImageLoader 및 RealImageLoader가 실제로 써드파티 라이브러리일 경우, 해당 코드를 수정하는 것은 일반적으로 불가능하다. (Swift Package일 경우 소스 코드가 오픈되있으므로 별도의 프레임워크로 만들어서 수정할 수 있을 것 같지만, 애초에 배포 형태가 프레임워크일 경우 소스 코드를 볼 수 없음)

따라서 기존의 코드를 수정하지 않고 실제 행동을 수행하기 전 별도의 액션을 추가로 실행하고 싶을 경우, 프록시 패턴을 활용할 수가 있다. 아래에서 Swift에서 프록시 패턴을 한 번 구현해보겠다.

프록시 패턴 구현

  • 이미지를 요청하는 클라이언트는, 기존의 RealImageLoader 대신 새롭게 정의된 ImageLoaderProtocol에 의존하도록 변경한다. 이렇게 의존 관계가 변경된 경우, 우리는 ImageLoaderProtocol을 구현하는 Proxy 객체를 RealImageLoader 대신 주입시킬 수 있다.
  • Proxy 객체는 이전에 Client가 사용하던 RealImageLoader의 인스턴스를 참조하고 있다.
  • Client가 이제 Image를 요청할 경우, 해당 요청은 ProxyImageLoader로 전달되고, ProxyImageLoader가 RealImageLoader에 대신 요청을 전달해 Client의 요청을 마무리한다.

요약하자면, Client가 새로운 인터페이스에 의존하게 함으로써 기존의 RealImageLoader 대신 새로운 ImageLoaderProtocol을 구현하는 Proxy 객체를 주입할 수 있다. 이 때 ProxyImageLoader는 RealImageLoader의 인스턴스를 가지고 있고, 만약 클라이언트의 요청이 올 경우, 별도의 작업을 하고 실제로 이미지를 요청할 수도 있다는 점이다.

그렇다면 코드로 캐쉬를 활용하는 ImageLoader를 구현해보도록 하자.

final class RealImageLoader {
    func image(_ completion: (NSImage) -> Void) {

    }
}

protocol ImageLoaderProtocol {
    func image(_ completion: (NSImage) -> Void)
}

extension RealImageLoader: ImageLoaderProtocol {

}

final class ProxyImageLoader: ImageLoaderProtocol {
    private let imageLoader: RealImageLoader = RealImageLoader()

    func image(_ completion: (NSImage) -> Void) {
        // 캐쉬에 데이터가 있으면 바로 클로저 호출...

        // 캐쉬에 데이터가 없으면 실제 로더에다가 이미지를 요청한다.
        imageLoader.image(completion)
    }
}

// 클라이언트는 이제 RealImageLoader 대신 ProxyImageLoader를 사용한다.
ProxyImageLoader().image { image in
  // 이미지를 화면에 표시한다.                        
}
  • ProxyImageLoader는 RealImageLoader를 프로퍼티로 참조하고 있다.
  • 이제 클라이언트는 RealImageLoader 대신 ProxyImageLoader를 사용하여, 캐쉬 처리와 같은 이점들을 가져갈 수 있다.

즉 프록시 패턴을 활용하면, 실제로 원하는 행동 전후로 추가적인 행동을 할 수 있다. 또한 더 이상 구체 타입에 의존하지 않고 추상 타입에 의존하기 때문에, 추후 클라이언트 테스트도 수월히 할 수 있을 것이다.

프록시 패턴의 장, 단점

프록시 패턴을 활용하면 다음과 같은 장점들을 얻을 수 있다.

  • 추가적인 코드의 변경 없이 필요한 기능을 추가할 수 있다.
    • 물론 기존의 구체 타입, 혹은 기존의 인터페이스를 사용하던 클라이언트가 새롭게 선언된 인터페이스를 사용해야 하므로, 이 정도 변경은 존재하는게 아닐까 싶다.
  • 접근 제어를 활용할 수 있다.
    • 클라이언트가 실제 서비스에 접근하기 전, 프록시 객체에 먼저 접근하므로 허용된 클라이언트만 실제 서비스를 활용할 수 있도록 할 수 있다.
  • 클라이언트의 요청을 로깅할 수 있다.
  • 클라이언트들 대신 실제 서비스 객체의 라이프사이클을 관리할 수 있다.
    • 예를 들어 실제 서비스 객체가 비용이 매우 큰 경우, 클라이언트가 오랫동안 사용하지 않으면 프록시 객체가 이를 해제할 수 있다.
  • 프록시 객체는 실제 서비스 객체가 준비되지 않았거나 사용할 수 없는 경우에도 작동한다.

그러나 다음과 같은 단점도 존재한다.

  • 다이렉트로 서비스 객체를 사용하는 대신 프록시 객체가 추가적으로 생성되야 하므로, 인스턴스를 빈번하게 생성해야 한다면 성능 저하가 일어날 수도 있다.
profile
iOS 개발자입니다.

0개의 댓글