이번 포스트에서는 프록시 패턴에 대해서 알아보자. Proxy란 우리말로 대리인이라는 뜻을 지닌다. 대리인은 남의 일을 대신 수행하는 역할을 하는 사람인데, 프로그램에서도 똑같은 의미로 사용되고 있는 패턴이다.
이미지를 불러오는 작업을 하는 RealImageLoader라는 객체가 있다고 생각해보자.
final class RealImageLoader {
func image(_ completion: (NSImage) -> Void) {
//...
}
}
그렇다면 클라이언트는 이미지를 불러오기 위해서 RealImageLoader의 인스턴스를 생성하고, image 함수를 호출해야 한다.
하지만 이미지는 용량이 매우 큰 데이터로, 우리는 캐시를 적절히 활용해 리소스를 아낄 수 있다. 예를 들어 url에서 이미지를 불러오는 경우, 해당 url 자체를 캐시키로 사용할 수도 있다.
따라서 이미지가 캐시에 이미 있는 경우 캐시에서 불러오고, 아니면 실제로 이미지를 요청하는 객체를 생성하고자 한다. 그러나 ImageLoader 및 RealImageLoader가 실제로 써드파티 라이브러리일 경우, 해당 코드를 수정하는 것은 일반적으로 불가능하다. (Swift Package일 경우 소스 코드가 오픈되있으므로 별도의 프레임워크로 만들어서 수정할 수 있을 것 같지만, 애초에 배포 형태가 프레임워크일 경우 소스 코드를 볼 수 없음)
따라서 기존의 코드를 수정하지 않고 실제 행동을 수행하기 전 별도의 액션을 추가로 실행하고 싶을 경우, 프록시 패턴을 활용할 수가 있다. 아래에서 Swift에서 프록시 패턴을 한 번 구현해보겠다.
요약하자면, 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
// 이미지를 화면에 표시한다.
}
즉 프록시 패턴을 활용하면, 실제로 원하는 행동 전후로 추가적인 행동을 할 수 있다. 또한 더 이상 구체 타입에 의존하지 않고 추상 타입에 의존하기 때문에, 추후 클라이언트 테스트도 수월히 할 수 있을 것이다.
프록시 패턴을 활용하면 다음과 같은 장점들을 얻을 수 있다.
그러나 다음과 같은 단점도 존재한다.