본문 바로가기

CS/디자인패턴

[디자인패턴] Proxy 패턴

프록시 패턴을 설명하는 그림

설명

 proxy라는 단어는 "대리" 라는 의미를 가지고 있다. 이름에서 알 수 있듯이 proxy 패턴은 원 객체에 대한 접근을 제어하기 위해 대리자 역할을 수행한다. 프록시 객체는 원 객체에 대한 접근을 조절하거나 생성 및 소멸을 책임질 수 있으며, 원 객체에게 전달될 요청을 받아 넘기기 전에 해당 데이터를 이용하여 여러가지 동작을 추가적으로 취할 수 있으므로 간단한 로깅부터 시작하여 접근 제어, 캐싱 등 사용 방법이 매우 다양할 수 있는 것이 특징이다.

 gof 책에서는 다음과 같은 활용 예시를 제시하고 있다.

  1. 원격지 프록시(remote proxy): 서로 다른 주소 공간에 존재하는 객체를 가리키는 객체로, 프록시 객체 자체는 로컬에 존재한다. 로컬의 프록시 객체는 네트워크 통신을 통해 원격의 실제 객체와 상호작용할 수 있는데, 이 과정에서 통신 프로토콜을 맞추거나 마샬링(표현 방식을 전송 또는 저장에 적합한 형태로 변경하는 것)이 요구될 수 있다. 원격지 프록시는 이러한 작업을 알아서 처리해주므로, 클라이언트 수준에서는 어떻게 상호 통신이 가능한지 알 필요가 없다.
    1. 요청 메시지와 파라미터를 통신 프로토콜에 맞게 인코딩하여 전달하는 역할을 수행한다.
    2. 투명성을 장점으로 지닌다. 원격지 프록시는 객체가 다른 주소 공간에 있다는 사실을 숨기고, 알아서 처리한다.
  2. 가상 프록시(virtual proxy) : 요청이 있을 때 필요한 고비용 객체를 생성한다.
    1. 실제 대상에 대한 추가적인 정보( 리소스 URI, 이미지 크기 등 ) 를 가지고 있으며, 해당 정보를 기반으로 원 객체에 대한 접근을 지연하여 객체에 대한 비용을 프로그램 시작 시점에 일괄 지불하는게 아니라 해당 객체가 실제로 필요한 시점에 분산하여 지불할 수 있도록 한다. ( 초기 로딩 시간 단축 가능 )
    2. 최적화가 장점이다. 요구에 따라 필요한 시점에 객체를 생성 및 제공할 수 있다.
  3. 보호용 프록시(protection proxy): 객체마다 엑세스 권한에 따라 객체와 통신하고 결과를 반환한다. 특정 객체가 일부 리소스에 대한 액세스 권한이 없을 때 액세스 권한이 있는 응용 프로그램의 개체와 통신하고 결과를 반환한다. 
    1. 요청한 대상이 실제로 요청할 수 있는 권한이 있는지 확인한다.
    2. 보안 측면의 장점이 존재한다.
  4. 스마트 프록시/레퍼런스(smart proxy): 위 경우에 포함되지 않는 프록시 유형. 객체에 대한 접근이 수행될 때 추가적인 동작을 수행한다. 로깅, 스마트 포인터, locking 등의 동작이 가능하다.

  요점은 프록시 객체가 원 객체를 대신하여 동작하면서 자신에게 요구되는 추가 로직을 수행한다는 것이다.

활용 예시

 게임에 대해 실제 사용 예시를 생각해보자. 심리스(seamless) 오픈월드 게임은 많은 그래픽, 사운드 데이터를 가지고 있다. 만약 오픈월드 게임에서 수많은 NPC, 몬스터 등의 데이터를 실행 시점에 로딩해야 한다면 데이터를 가져오는데 너무 긴 시간이 필요할 것이며, 크로스 플랫폼에서는 메모리 용량에 한계가 발생하므로 사전 로딩도 불가능하다.

 이때 인게임에서 사용자가 볼 수 있는 세계는 화면 내로 제한되므로, 화면 안에서 보여줄 수 있는 범위 안의 데이터에 대해서만 로딩을 수행하더라도 큰 문제는 없다. 예를 들어 사용자를 기준으로 화면 안에 표현될 수 있는 적당히 큰 가상의 원을 설정하고, 해당 원 안에 대상이 존재하는 경우에만 모델, 사운드 등의 데이터를 로딩하는 것이다. 이렇게 설계하는 경우 NPC와 몬스터 등 오브젝트들은 원에 포함되지 않을 때는 프록시 객체로서 (좌표, 데이터 소스) 정보만 포함하고 있다가 원 안에 포함되는 시점부터 데이터를 로딩하고, 원에서 빠져나갈 때는 메모리 상태에 따라 자원을 할당해제하는 등의 최적화가 가능할 것이다.

 현실에서 맨눈으로 봤을 때 멀리 있는 요소는 가까이 있는 요소보다 상대적으로 뚜렷하게 보이지 않는다. 따라서 게임 내에서도 원 내에 존재하는 모든 오브젝트를 명확하게 보이게 만들 필요는 없으며, 이런 특성과 관련된 추가적인 최적화를 고려할 수 있다. 오브젝트들에 대해 찰흙같이 간단한 모델과 명확한 모델을 몇 단계로 만들어두고 사용자 중심의 동심원에 가까워질 수록 선명도가 높은 모델로 변경하는 방식을 채택한다면 멀리 있을 수록 선명하게 보이지 않는다는 현실적인 특징을 만족하면서 메모리 자원을 아낄 수 있다. 프록시 객체는 사용자와의 거리에 따라 점점 선명한 모델을 보여주는 방식으로 동작할 수 있을 것 같다.

 확실히 게임 분야가 다른 분야보다 성능 및 최적화가 상당히 중요해서 프록시 패턴을 사용할 곳이 많을 것 같다.

구성 요소

  • Proxy: 원 객체에 대한 참조자를 관리한다. 원 객체인 RealSubject와 Subject 인터페이스를 동일하게 제공하여 실제 대상으로 대체될 수 있어야 한다. 접근 제어, 생성 및 삭제에 관여할 수 있다. 외부에서 받은 요청을 RealSubject 객체에게 전달한다.
  • Subject: RealSubject와 Proxy가 공유하는 인터페이스로, 둘이 대체될 수 있도록 하는 요소이다.
  • RealSubject: Proxy 객체가 대체하고 있는 실제 객체.

예시 코드

 유튜브에 재미있는 코드가 있어서 가져왔다.

링크: https://www.youtube.com/watch?v=WcAV9rOGjxw&list=PL8B19C3040F6381A2&index=5 

 위 유튜브 링크에서는 저그 코쿤을 프록시 객체로 삼아 저그의 다른 유닛들을 대리하는 코드를 작성하고 있다. 코쿤은 움직이지 못하는 유닛이며, 일정 시간이 지나면 구체적인 유닛으로 변태한다. 이때 코쿤 상태에서도 랠리 포인트를 지정할 수 있으며 해당 랠리 포인트는 실제 유닛으로 변태한 이후 동작한다. 영상에서는 이러한 개념을 프록시 객체를 이용하여 구현하였으며, 객체가 움직이는 모습도 보여주고 있다.

// 코드 아이디어 출처: https://www.youtube.com/watch?v=WcAV9rOGjxw&list=PL8B19C3040F6381A2&index=5

interface Zerg {
    move(xPos: number, yPos: number): void;
}

interface IConstructor<T> {
    new(...args: any[]): T;
}

interface IInitInfo<T> {
    Target: IConstructor<T>
    time: number; // 생성에 걸리는 시간
    // 이외로 자원 등의 정보 포함될 수 있을듯?
}

interface IZergInitInfo extends IInitInfo<Zerg> { }

class Drone implements Zerg {

    move(xPos: number, yPos: number) {
        console.log(`Drone: move to (${xPos},${yPos})`);
    }
}


class Cocoon implements Zerg {
    private target?: Zerg;
    private rallyPoint?: Point;
    private t: NodeJS.Timeout;

    constructor({
        time,
        Target
    }: IZergInitInfo) {
        this.t = setTimeout(() => {
            console.log("Time Completed");
            this.onTimerComplete(Target);
        }, time);
    }

    private onTimerComplete(target: IConstructor<Zerg>) {
        this.target = new target();
        if (this.rallyPoint) {
            this.target.move(this.rallyPoint.x, this.rallyPoint.y);
        }
    }

    cancel() {
        clearTimeout(this.t);
        // remove this...
    }

    move(xPos: number, yPos: number): void {
        if (this.target) {
            this.target.move(xPos, yPos);
        } else {
            this.rallyPoint = new Point(xPos, yPos);
            console.log("Cocoon: save rallypoint");
        }
    }
}

class Point {
    constructor(
        public readonly x: number,
        public readonly y: number) { }
}

export function main() {
    const droneInitInfo: IZergInitInfo = {
        time:3000,
        Target: Drone
    };

    const drone = new Cocoon(droneInitInfo);
    drone.move(1, 3);
    drone.move(4, 2);
}

main 함수 결과

 위 영상과는 달리 단순히 텍스트로 구현하기는 했으나 드론 객체가 생성된 이후(time completed) 드론이 주어진 좌표로 움직인다는 메시지를 출력하는 모습을 볼 수 있다. 여기서 Zerg는 Subject, Drone은 RealSubject에 해당하며, Cocoon은 저그 유닛들에 대한 프록시 객체이다. 실제 스타크래프트에서는 코쿤 이전에 라바가 존재하므로 위와 완전히 동일하게 구현되지는 않겠지만, 재미있는 발상인 것 같다.

 여기서 제시한 코드 말고도 Proxy와 관련된 다양한 예시 코드가 있다. 예를 들어 gof 책에서는 최적화 측면에서 이미지에 대한 프록시를 설정하여 실제로 이미지가 화면에 디스플레이 되기 전까지 이미지 초기화 비용을 미루는 목적으로 프록시 객체를 이용하는 코드를 제시한다. 한번 보는 것을 권장한다.

'CS > 디자인패턴' 카테고리의 다른 글

[디자인패턴] Observer 패턴  (0) 2023.05.08
[디자인패턴] Chain Of Responsibility 패턴  (0) 2023.05.06
[디자인패턴] Mediator 패턴  (0) 2023.04.30
[디자인패턴] Facade 패턴  (0) 2023.04.30
[디자인패턴] SOLID 원칙  (0) 2023.04.27