본문 바로가기

FRONT-END/JAVASCRIPT

자바스크립트 디자인 패턴 #7. Memoization 패턴

반응형

메모이제이션(memoization) 패턴

자바스크립트의 특징으로 인하여 쉽게 구현하고 활요할 수 있다.

패턴의 이름대로 '메모'를 하는 것이 특징인데, 이 메모를 하는 대상은 바로 함수 또는 객체이다.

자바스크립트의 객체들은 쉽게 새로운 속성을 추가하고 수정하고 접근할 수 있다는 점에서 이 패턴을 간단하게 활용할 수 있다.


이 패턴의 활용 용도는 일반적인 프로그래밍 언에서 캐시를 사용하는 것 처럼 사용한다고 생각하면된다.

특히 컴퓨팅 자원이 소모되거나 처리 시간이 긴 산술식에 대하여 캐시를 해두는 식으로 활용하면 좋다.

특정 아이템을 검색하려고 할 때 검색결과를 ID기반으로 캐시 처리하는 메모이제이션 패턴이다.

<!DOCTYPE html>

<html lang="en">

 <head>

   <meta charset="utf-8">

   <title>hello</title>

 </head>

 <body>

  <input type="text" id="itemId"/>

  <button type="button" id="search">Search</button>

 <script>

(function(){

  var inputItemId = document.getElementById("itemId");


  function searchItem(id){

    var xhr;

    if(searchItem.cache.hasOwnProperty(id)){

      return searchItem.cache[id];

    }


    xhr = new XMLHttpRequest();

    xhr.open("GET","./searchItem.php");

    xhr.onload = function(){

      var item = JSON.parse(xhr.responeText);

      searchItem.cache[item.id] = item;

    };

    xhr.onerror = function(){

      var item = JSON.parse('{"id": "test"}');

       searchItem.cache[item.id] = item;

    }

    xhr.send();

  }


  searchItem.cache = {};

  searchItem.cache["test"] = JSON.parse('{"id": "test"}');


  document.getElementById("search").addEventListener("click", function(){

    searchItem(itemId.value);

    console.log(searchItem.cache);

  });


}());

 </script>

 </body>

</html>

메모이제이션 패턴의 메모를 하는 대상은 변수가 될수도 있고 함수가 될수도 있다.

이러한 특징은 자바스크립트의 유연한 객체의 변형때문이다.

검색하는 함수가 searchItem()이라면 이 함수 자체에 cache라는 속성을 객체로 추가하고, cache에는 아이템이 있으면 캐시에 저장된 객체를 반환하고 없으면 서버로 요청한다.

그리고 서버에서 아이템의 검색 결과과 나오면  ID를 기반으로 cache에 저장한다.

이처럼 호출되는 함수에 필요한 정보들을 메모처럼 기록하는 것을 메모이제이션 패틴이라고 한다.


메모이제이션 패턴으로 피보나치 수열 구현


산술적으로 복잡한 경우에도 메모이제이션 패턴을 사용하면 좋다.

(function(){

  function fibonacci(n){

    if(n === 0 || n === 1){

      return n;

    }else{

      return fibonacci(n-1) + fibonacci(n-2);

    }

  }


  var fibonacciMemo = (function(){

    return function(n){

      var result = fibonacciMemo.memo[n];

      if(typeof result !== 'number'){

        result = fibonacciMemo(n-1) + fibonacci(n-2);

        fibonacciMemo.memo[n] = result;

      }

      return result;

    };

  }());


  fibonacciMemo.memo = [0, 1];


  var testNum =40,

      start, end;


  start = Date.now();

  console.log(fibonacci(testNum));

  end = Date.now();

  console.log(`Elapsed time of ${((end - start)/1000).toFixed(2)} seconds for recursive finbonacci(${testNum})`);


  start = Date.now();

  console.log(fibonacciMemo(testNum));

  end = Date.now();

  console.log(`Elapsed time of ${((end - start)/1000).toFixed(2)} seconds for recursive fibonacciMemo(${testNum})`);

  console.log(fibonacciMemo.memo);


}());


재귀적인 피보나치 수열과 재귀적이면서 메모이제이션 패턴을 사용하는 피보나치 수열 두가지의 실행시간을 비교하고있다.

메모이제이션 패턴은 피보나치 수열의 입력값을 메모에 저장하여 메모에 있으면 다시 불러오는 식으로 동작한다.


함수결과는 동일하다. 그러나 실행시간을 비교하면 일반 재귀적인 피보나치 수열의 fibonacci(40)에 대한 계산시간과 finbonacciMemo(40) 계산시간의 차이가 난다.

이러한 식으로 이전에 계산된 결과를 토대로 새로운 결과를 계산할 수 있을 때 캐시 등을 이용하면 그것을 다이내믹 프로그래밍이라고 한다.

이처럼 메모이제이션 패턴은 계산이나 요청이 오래 걸리면서 반복적으로 같은 호출이 일어날 때 아주 효율적으로 사용할 수 있는 패턴 중의 하나이다.


Function.prototype 활용

이러한 메모이제이션 패턴을 일반적으로 사용할 수 있도록 Function의 프로토타입에 추가해서 사용하면 복잡하지않고 편리하게 활용할 수 있다.

(function(){

  Function.prototype.memoize = function(){

    var _this = this,

        memo ={};

    return function(){

      var argsString = JSON.stringify(arguments),

          returnValue;

      if(memo.hasOwnProperty(argsString)){

        return memo[argsString];

      }else{

        returnValue = _this.apply(this, arguments);

        memo[argsString] = returnValue;

        return returnValue;

      }

    }

  };


  function fibonacci(n){

    if(n === 0 || n === 1){

      return n;

    }else{

      return fibonacci(n-1) + fibonacci(n-2);

    }

  }


  var fibonacciMemo = fibonacci.memoize();


  var testNum = 40,

      start, end;


  start = Date.now();

  console.log(fibonacciMemo(testNum));

  end = Date.now();

  console.log(`Elapsed time of ${((end - start)/1000).toFixed(2)} seconds for recursive fibonacciMemo(${testNum}) for the first time`);


  start = Date.now();

  console.log(fibonacciMemo(testNum));

  end = Date.now();

  console.log(`Elapsed time of ${((end - start)/1000).toFixed(2)} seconds for recursive fibonacciMemo(${testNum}) for the second time`);




}());

Function.prototype에 memoize()함수를 추가하여 함수들이 기본적으로  memoize() 함수를 가져서, 필요할 때 메모이제이션 패턴을 적용한 함수가 바로 반환되도록 구현

fibonacci()함수에  memoize()함수를 호출하여 메모이제이션 패턴을 적용하였다.


단순히 fibonacci()  함수 밖을 한번만  memoize해서 둘러싼 것 뿐이라 fibonacci() 함수를 직접 수정하지 않으면 최초 실행은 메모를 활용할 수 없다.

따라서 메모이제이션 패턴으로 둘러싼 fibonacciMemo() 함수를 호출했는데도 불구하고 2초이상 걸렸지만, 두번째 호출에서는 메모에 결과를 가져와서 바로 처리되는 것을 알수 있다.

따라서 Function.prototype에 추가하는 memoize() 함수는 재귀가 아닌 일반적인 산술처리나 XMLHttpRequest 등과 같이 캐시를 활용할 수 있는  함수에 추가 기능으로 사용하면 좋고, 재귀함수는

직접 메모이제이션 패턴을 설계하여 사용하는 것이 더 좋은 성능을 보여준다.


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

반응형