본문 바로가기

FRONT-END/JAVASCRIPT

자바스크립트 디자인 패턴 #3. Proxy 패턴

반응형

프락시 패턴

프락시(Proxy) 패턴은 특히 자바스크립트에서 구현하기 쉬운 패턴이다.

자바스크립트에서는 함수도 하나의 객체처럼 정의해서 사용할 수 있고, 객체의 속성을 문자열로 접근할 수 있는 특징이 있기 때문이다.

프락시 패턴은 말 그대로 하나의 객체가 프락시 역할을 수행하여 상황에 따라 다른 객체에 접근하게 해주거나 다른 함수를 호출하게 해주는 패턴이다.


일반적으로 프락시란 클라이언트와 서버 연동에서 클라이언트가 바로 서버에 접근하는 것이 아니라프락시를 통해서 간접적으로 접근할 수 있게 해준다.

프락시는 내부적으로 다양한 기능을 제공하는 역할을 수행한다.


프락시는  클라이언트와 서버 사이에서 중간자 역할을 한다. 프락시의 역할 중 대표적인 것은 클라이언트가 서버에 직접 접근하지 못하도록, 한 단계 가상화 또는 캡슐화하는 기능이다.

이로써 서버의 기능들이 클라이언트에 직접 노출되지 않게 한다.


또한 클라이언트가 request를 프락시에 한번 전달하면 프락시는 서버로 여러번의 복잡한 real_request를 보낸다.

응답 또한 마찬가지로 서버가 여러 개의 real_response를 보내면 플락시에서 취합하여 하나의 response로 보내는 기능을 수행한다.

또 반대로 클라이언트가 비슷한 request를 여러번 보낼 때  서버로 그러한 request들을 묶어서 하나의 request로 보내는 역할을 수행하기도 한다.

부가적으로 웹에서 프락시는 캐시기능을 수행하기도한다.

이러한 프락시 기능을 자바스크립트에서 디자인 패턴으로 구현하여 유용하게 활용할 수 잇다.

프락시 패턴은 홀로 사용되기도 하지만 델리게이션 패턴과 조합하여 활요하면 훨씬 간결하고 유지보수 관리가 쉽다.

(function(){

var divControlPanel = document.getElementById("controlPanel"),

 videoBunny = document.getElementById("videoBunny"),

 proxyClickEventHandler = {

"play" : function(){

videoBunny.play();

},

"pause" : function(){

videoBunny.pause();

},

"volumeUp" : function(){

if(videoBunny.volume <= 0.9){

videoBunny.volume += 0.1;

}else{

videoBunny.volume = 1;

}

},

"volumeDown" : function(){

if(videoBunny.volume >= 0.1){

videoBunny.volume -= 0.1;

}else{

videoBunny.volume = 0;

}

}

 };


 divControlPanel.addEventListener("click",function(e){

var target = e.target || e.srcElement;

if(proxyClickEventHandler.hasOwnProperty(target.id)){

proxyClickEventHandler[target.id].call();

}

 },true);


}());

프락시 변수 (proxyClickEventHandler)를 하나 생성하는데, 이 변수에 각 버튼의 클릭 이벤트를 처리하는 함수를 정의하고 있다.

그리고 클릭 이벤트 핸들러에서는 클릭이벤트 대상의 id 기준으로 프락시의 속성에 접근해서 함수를 호출한다.

이처럼 프락시 변수를 정의해놓으면 클릭이벤트 핸들러들을 따라가는것이 아니라, 로직이 포함된 프락시 변수만 보면 되기 때문에 소스 관리가 더 유용해진다.


(function(){

var divControlPanel = document.getElementById("controlPanel"),

 selectControlVideo = document.getElementById("controlVideo"),

 controlVideo = {

"play" : function(video){

video.play();

},

"pause" : function(video){

video.pause();

},

"volumeUp" : function(video){

if(video.volume <= 0.9){

video.volume += 0.1;

}else{

video.volume = 1;

}

},

"volumeDown" : function(video){

if(video.volume >= 0.1){

video.volume -= 0.1;

}else{

video.volume = 0;

}

},


"getVideoById" : function(id){

return document.getElementById(id);

}

}

proxyClickEventHandler = function(command){

var video;

if(controlVideo.hasOwnProperty(command)){

video = controlVideo.getVideoById(selectControlVideo.value);

console.log(command);

controlVideo[command].call(this, video);

}

};


 divControlPanel.addEventListener("click",function(e){

var target = e.target || e.srcElement;

proxyClickEventHandler(target.id);

 },true);

}());

<video> 태그를 작은 크기, 중간, 큰 크기로 여러개를 보여준다음, 

하나의 컨트롤 패널을 통해서 세 개의 <video> 태그를 각각 제어할 수 있게 하고 있다.

컨트롤 패널이 어떠한 <video> 태그를 제어할지는 <select> 태그를 통해서 선택할 수 있다.

각각의 <video> 태그를 별도로 제어할 수 있는데, 클라이언트-서버 구조와 비교하여 서버의 역할을 controlVideo 변수가 담당하고 있고,

중간에 proxyClickEventHandler() 함수가 프락시 역할을 수행하고 있다고 볼수 있다.

프락시에서는 이벤트에 대한 처리 요청이 오면 <select>에 대한 정보를 수집한 다음, 해당하는 <video>를 제어할 수 있도록 이 정보를 가공하여 controlVideo에 넘겨준다.


프락시를 활용하는 캐시 기능 구현

프락시는 중간에 전달 역할을 수행하고 있는데, 프락시 기능 중 하나인 캐시를 활용 할수 도 있다.

DOM에 대한 탐색을 최적확하기 위해 간단한 캐시 기능

(function(){

var divControlPanel = document.getElementById("controlPanel"),

 selectControlVideo = document.getElementById("controlVideo"),

 controlVideo = {

"play" : function(video){

video.play();

},

"pause" : function(video){

video.pause();

},

"volumeUp" : function(video){

if(video.volume <= 0.9){

video.volume += 0.1;

}else{

video.volume = 1;

}

},

"volumeDown" : function(video){

if(video.volume >= 0.1){

video.volume -= 0.1;

}else{

video.volume = 0;

}

},


"getVideoById" : function(id){

return document.getElementById(id);

}

}

proxyClickEventHandler = (function(){

var cache = {};

console.log(cache);

return function(command){

var video;

if(controlVideo.hasOwnProperty(command)){

if(cache.hasOwnProperty(selectControlVideo.value)){

video = cache[selectControlVideo.value];

}else{

video = controlVideo.getVideoById(selectControlVideo.value);

cache[selectControlVideo.value] = video;

}

controlVideo[command].call(this,video);

}

};

}());


 divControlPanel.addEventListener("click",function(e){

var target = e.target || e.srcElement;

proxyClickEventHandler(target.id);

 },true);

}());

클릭 이벤트가 발생하여 <video> 태그를 찾아야할 때 미리 cache변수의 속성으로 저장해두고 있으면 프락시의 기능 중 캐시 기능을 수행 할 수 있다.

이러한 캐시 기능을 프락시 패턴과 조합해서 사용하면 XMLHttpRequest를 보낼때도 주기적으로 변경하지 않는 동일한 응담이 필요할 때 서버와의 통신을 최소화 할 수 있다.


프락시를 활용한 래퍼 기능 구현


자바스크립트는 유동적으로 변수, 함수에 대해여 쉽게 접근하고 호출할수 있다.

따라서 기존에 있는 함수를 다른 함수로 감싸는 래퍼(wrapper) 형식의 기능을 쉽게 제공할수 이 ㅆ다.

이는 다른 라이브러리나 모듈의 함수를 사용하기 전에 전처리를 편하게 할 수 있음을 의미한다.

(function(){

function wrap(func, wrapper){

return function(){

var args = [func].concat(Array.prototype.slice.call(arguments));

return wrapper.apply(this, args);

};

}


function existingFunction(){

console.log("Exsting function is called with arguments");

console.log(arguments);

}


var wrapperFunction = wrap(existingFunction, function (func){

console.log("Wrapper function is called with arguments");

console.log(arguments);

//console.log(Array.prototype.slice.call(arguments, 0));

func.apply(this, Array.prototype.slice.call(arguments, 1));

});

console.log("1. Calling existing function");

existingFunction("First argument", "second arguement", "third arguement");


console.log("2 Calling wrapped function");

wrapperFunction("First argument", "second arguement", "third arguement");


}());

wrap() 이라는 함수는 두 개의 함수를 인자로 받는다.

첫번째 인자는 기존 함수이고, 두번째 인자는 기존 함수를 호출하기전에 먼저 호출할 wrapper 함수이다.

그러면 함수 내부에서는 기존 함수와 함께 받은 인자들을 wrapper 함수로 넘겨준다.


로그를 찍고 기존 함수를 그대로 호출하고 있지만, 이를 응용할 때는 wrapper 함수 안에서 현재 입력으로 들어오는 인자들의 조건에 따라서 기존 함수 호출 여부를 결정하면 된다.

기존함수 호출과 wrapper 함수 호출 두가지 모두를 시험하고 있다.

두가지 호출에서 다른 점은 호출되는 함수의 이름 뿐이며, 이 외에 인자 전달과 호출하는 방법 등은 모두 같다.


wrapper 함수를 호출하는 경우 wrapper 함수가 먼저 호출되고 이후에 기존 함수가 정상적으로 호출된다.

기존 함수를 바로 호출하는 것과 wrapper 함수를 통해 기존 함수를 호출하는 것이 같다.


자바스크립트에서는 소스 몇줄만 추가하고도 프락시 패턴을 쉽게 구현할 수 있다.

따라서 다른 함수에서 추가로 전처리를 하고자 한다면 프락시 패턴을 이용하면된다.

프락시 패턴의 장점은 함수에서 인자는 그대로 놔두고 함수명만 수정하면 추가 기능을 큰 소스의 변경없이도 제공할 수 있다는 점이다.


기존함수명과 같은 이름으로 래퍼함수 활용

직접 정의한 static한 함수를 호출하므로 소스에서 existingFunction 함수명을 wrappedFunction으로 수정해야하는 번거로움이 있다.

이럴때는 클로저를 이용해서 하위 클로저에서 기존함수를 백업해두고 같은 이름의 변수나 함수를 정의한다면 동일하게 사용할 수 있다.

(function(){

function wrap(func, wrapper){

return function(){

var args = [func].concat(Array.prototype.slice.call(arguments));

return wrapper.apply(this, args);

};

}


function existingFunction(){

console.log("Exsting function is called with arguments");

console.log(arguments);

}


var wrapperFunction = wrap(existingFunction, function (func){

console.log("Wrapper function is called with arguments");

console.log(arguments);

//console.log(Array.prototype.slice.call(arguments, 0));

func.apply(this, Array.prototype.slice.call(arguments, 1));

});

console.log("1. Calling existing function");

existingFunction("First argument", "second arguement", "third arguement");


console.log("2 Calling wrapped function");

wrapperFunction("First argument", "second arguement", "third arguement");


//


(function(){

var existingFunction = wrapperFunction;

console.log("\n3. Calling wrapped existing function");

existingFunction("First argument", "second arguement", "third arguement");

}());


}());

클로저를 통해서 구현하는 방법이 있지만, 기존 함수가 호출되는 상위에 새로운 클로저를 정의하는 번거로움이 있다.

또한 같은 함수명과 변수명이 존재하게 되어 소스를 분석하는데 어려움이 따른다.


비슷하게 호출되는 소스를 수정하고 싶지 않을떄는 정의된 함수 위에 같은 이름의 변수를 할당해서 사용하는 방법도 있다.

기존함수 위에 기존함수를 다른 변수에 백업해두고 같은 이름의 변수명을 정의하면 변수 접근 우선순위에 따라서 변수에 먼저접근해서 사용한다.

(function(){

function wrap(func, wrapper){

return function(){

var args = [func].concat(Array.prototype.slice.call(arguments));

return wrapper.apply(this, args);

};

}


var _existingFunction = existingFunction,

existingFunction = wrap(_existingFunction, function(func){

console.log("Wrapper function is called with arguments");

console.log(arguments);

func.apply(this, Array.prototype.slice.call(arguments,1));

});


function existingFunction(){

console.log("Exsting function is called with arguments");

console.log(arguments);

}


console.log("1. Calling existing function");

existingFunction("First argument", "second arguement", "third arguement");


}());

소스변경이 가장 적으므로 충분히 고려해볼 수 잇는 상황이지만, 향후 같은 함수명과 변수명이 있을 때 소스분석이 어려운 상황에 처할 수 도 있다.

따라서 기존 소스를 절대로 수정할수 없다고 판단되거나 호출되는 범위나 소스를 유추할 수 없을 때만 분별해서 사용하면 좋다.


래퍼를 활용한 로그 기록 구현

모듈이나 객체의 속성으로 있는 함수를 호출하게 될때는 수작업이 아니라 자동으로 전체 함수에 대한 래퍼 함수를 설정할수 있다.

(function(){

var car = {

beep: function beep(){

alert("BEEP");

},

brake: function brake(){

alert("STOP!");

},

accelerator: function accelerator(){

console.log("GO");

}

};


console.log(car.beep);


function wrap(func, wrapper){

return function(){

var args = [func].concat(Array.prototype.slice.call(arguments));

return wrapper.apply(this, args);

};

}


function wrapObject(obj, wrapper){

var prop;

for(prop in obj){

//console.log(prop);

if(obj.hasOwnProperty(prop) && typeof obj[prop] === "function"){

obj[prop] = wrap(obj[prop], wrapper);

console.log(obj[prop]);

}

}

}


wrapObject(car, function(func){

console.log(func.name + "has been invoked");

func.apply(this, Array.prototype.slice.call(arguments, 1));

console.log(func.name + "has been invoked");

});

console.log(car.beep);

car.accelerator();

car.beep();

car.brake();




}());

wrapObject() 라는 추가함수를 정의하고 잇다.

이 함수는 객체를 인자로 받으면 해당 인자의 모든 속성을 대상으로 함수인지를 판단하여, 함수이면 래퍼 함수를 먼저 호출한다.

이때 함수인지 판단하는 것이 중요하다.


래퍼함수는 기존 함수와 동일하게 호출하였는데도 어떠한 이름의 함수가 호출되었는지 로그로 출력된다.

실행결과에서 원래 함수의 알림창은 정상으로 뜨면서 , 콘솔 창을 통해 car.accelerator(), car.beep(), car.brake() 함수까지 호출되었다는 로그를 확인할 수 있다.

이처럼 함수를 객체로 관리하는 경우에는 일괄적으로 프락시 패턴을 적용해서 추가적인 전처리 로직을 삽입할 수 있다.

이러한 기능들은 디버깅이나 사용자의 행동 패턴 분석, 또는 라이브러리의 활용도를 통계적으로 분석하고자 할 때 활용할 수 있다.


자바스크립트는 객체나 함수를 재정의하고 호출하는 작업이 자유로워서 프락시 패턴을 쉽게 다양하게 응용할 수 있다.


출처-속깊은 자바스크립트 양성익 지음

반응형