본문 바로가기

Backend/디자인 패턴

Proxy Pattern (프록시 패턴)

일반적으로 프록시는 다른 객체에 대한 접근을 제어하는 객체입니다. 여기서 프록시와 다른 객체는 동일한 인터페이스를 가지고 있으며

이를 통해 다른 인터페이스와 호환되도록 바꿔버릴 수도 있습니다.

프록시 패턴은 다른 객체에서 실행될 작업의 전부 혹은 일부를 가로채서 해당 동작을 변경합니다. (이 패턴은 서로게이트 라고도 합니다)

 

아래는 프록시의 여러 유용한 케이스입니다.

 

  • Data validation : 프록시가 다른 객체로 입력을 전달하기 전에 데이터 유효성을 검사
  • Security : 권한을 체크하고 확인되었을 경우에만 프록시가 다른 객체로 요청을 전달
  • Caching : 프록시가 캐싱을 해 캐싱된 데이터가 없을 경우에만 다른 객체로 작업 전달
  • Lazy initialization : 필요할 때 까지 다른 객체의 생성 지연
  • Logging : 인터셉트해서 프록시가 로깅

케이스에서 보이듯 프록시는 다른 객체로 작업을 전달하며 전 후 과정에서 여러가지 처리를 하는 형태로 사용합니다.

대표적으로 Express.js 에서 미들웨어도 요청의 전 후 처리를 하기 때문에 프록시로 볼 수 있습니다.

 

- 프록시 구현

 

const createProxy = (subject) => {
  return {
    method1: () => subject.method1() + 'up up up',
    method2: () => subject.method2.apply(subject, arguments),
  };
};

class Subject {
  method1() {
    return 'normal method1';
  }
  method2() {
    return 'normal method2';
  }
}

const proxySubject = createProxy(new Subject());

console.info(proxySubject.method1()); // normal method1 up up up
console.info(proxySubject.method2()); // normal method2

 

위는 프록시를 구현하는 컴포지션 이라는 방법입니다. 프록시로서 기능을 확장 또는 사용하기 위해 createProxy 라는 팩토리와 결합하여 동일한 인터페이스를 가진 새로운 객체가 만들어지고 기존 객체는 프록시 내부에 저장됩니다.

 

JS 에선 객체 리터럴을 이용해 위처럼 심플한 코드 구현이 가능합니다. 이 방법은 새로운 객체를 만들어내기 때문에 변경하고자 하는 특정 메서드(method1) 외의 메서드(method2)에 대해선 원래의 대상에 위임해야합니다.

 

const createProxy = (subject) => {
  const method1 = subject.method1;
  subject.method1 = () => method1.call(this) + 'up up up';

  return subject;
};

class Subject {
  method1() {
    return 'normal method1';
  }
  method2() {
    return 'normal method2';
  }
}

const proxySubject = createProxy(new Subject());

console.info(proxySubject.method1()); // normal method1 up up up
console.info(proxySubject.method2()); // normal method2

 

컴포지션에 비해 보다 간단한 방법은 객체 증강입니다. 코드에서처럼 기존 객체를 직접 수정하는 것입니다.

편리하긴하지만, 대상 객체를 직접 수정해야 하는 단점이 제 눈엔 너무 크게 보이는 방법이네요.

 

class Subject {
  method1() {
    return 'normal method1';
  }
  method2() {
    return 'normal method2';
  }
}

class ProxySubject extends Subject {
  method1() {
    return super.method1() + 'up up up';
  }

  method2() {
    return super.method2();
  }
}

const proxySubject = new ProxySubject();
console.info(proxySubject.method1()); // normal method1 up up up
console.info(proxySubject.method2()); // normal method2

 

근래엔 JS 에서도 사용 가능한 상속 때문에 위 방법의 필요성이 없을 것 같습니다.

 

대체로는 가장 편리하기 때문에 객체 증강을 사용하는 케이스가 많은 것 같습니다.

프록시 패턴은 함수 후킹이라고 불리기도 하며, AOP 로 불리기도 합니다.

관점 지향 프로그래밍에서 비관심사를 분리하고 이를 구현하기 위한 용도로도 사용이 가능할 것 같습니다.

 

- Proxy 전역 객체

 

const person = {
  name: 'peter',
  address: 'newyork',
};

const upperPerson = new Proxy(person, {
  get: (target, property) => target[property].toUpperCase(),
});

console.info(upperPerson.name, upperPerson.address); // PETER NEWYORK

 

ES6 부터는 Proxy 라는 전역 객체를 사용할 수 있게 되었습니다.

Proxy 생성자의 인자는 target, handler 이며 target 은 프록시가 적용되는 객체, handler 는 프록시 동작을 정의하는 객체입니다.

위 결과에서 보이는 것 처럼 Proxy 는 단순한 래퍼가 아니기 때문에 target 객체의 일반 속성에 대한 접근을 가로챌 수 있습니다.

 

Proxy API 를 사용해서도 프록시 패턴을 잘 사용할 수 있을 것 같습니다. Proxy API 에 대한 내용은 아무래도 공식 문서를 참고하는 것이 가장 좋은 방법이겠죠.