Experiences/Node.js

CPS(Continuation-passing Style) Coding Pattern.

프로그래머 2015. 4. 5. 03:35


사진 출처 : http://www.slideshare.net/jeongsangbaek/nodejs-15279050

CPS코딩 패턴은 함수 실행이 끝남과 동시에 연이어 함수가 또 실행되는 프로그래밍 모델로 비동기 프로그래밍 방식을 사용하는 Node.js에서 많이 활용되는 패턴입니다.


좀 더 쉽게 이야기하면 일반적인 동기식 프로그래밍 모델에서는 프로시저가 호출자에게 리턴하는 구조를 사용하지만,

function sum(a, b) {
    return a + b;
}

console.log(sum(10, 20));


CPS는 continuation 인자(매개변수)로 전달된 콜백함수를 리턴할 위치에서 호출하는 구조를 사용합니다.

// 전체 소스
function each(arr, iterator, callback){

  if(!arr.length){
    return callback(null, arr);
  }

  var completed = 0;

  function done(err) {
    if(err) {
      callback(err);
    }
    else {
      completed ++;
      if (completed >= arr.length) {
        callback(null, arr);
      }
    }
  }

  arr.forEach(function (element, index, array) {
    iterator(array, index, done);
  });
}

function square(array, index, callback) {
  if (typeof array[index] !== "number"){
    callback(new Error((index + 1) + '번째 데이터는 숫자가 아닙니다.'));
  }
  else{
    array[index] *= array[index];
    callback();
  }
}

var emptyArr = [];

each(emptyArr, square, function(err, result){
  if (err) {
    console.log(err.message);
  }
  else {
    console.log(result);
  }
});

var numberArr = [1, 2, 3, 4, 5];

each(numberArr, square, function(err, result) {
  if (err) {
    console.log(err.message);
  }
  else{
    console.log(result);
  }

var errorArr = [1, 2, '3', 4, 5];

each(errorArr, square, function(err, result) {
  if (err) {
    console.log(err.message);
  }
  else{
    console.log(result);
  }
});

먼저 우리가 만들 함수 each는 첫 번째 매개변수 배열 arr의 각각의 원소에 대해 두 번째 매개변수인 iterator를 수행합니다.

function each(arr, iterator, callback){

  if(!arr.length){
    return callback(null, arr);
  }

  var completed = 0;

  function done(err) {
    if(err) {
      callback(err);
    }
    else {
      completed ++;
      if (completed >= arr.length) {
        callback(null, arr);
      }
    }
  }

  arr.forEach(function (element, index, array) {
    iterator(array, index, done);
  });
}

함수 each는 먼저 빈 배열의 경우 바로 continuation callback을 호출에 비어있는 배열을 출력합니다.
원소가 존재할 경우 자바스크립의 표준함수 Array.forEach() 함수를 이용, 배열 원소의 각각에 대해 반복함수 iterator를 수행합니다.


그렇다면 자세한 소스의 실행 경로를 따라가보면서 확실하게 이해하도록 해봅시다.

실행순서(전체소스)

  1) 37번 라인을 거쳐 빈 배열인 emptyArr을 갖고 each 함수를 실행합니다. (line 39)
  2) emptyArr, square, callback function 을 인자값으로 가지고 each 함수가 실행됩니다. (line 2)
  3) emptyArr의 값을 갖고 있는 array이 빈 배열인지 검사합니다. 
     (여기서 말하는 값은 리터럴 값이 아닌 참조된 값을 의미합니다.) (line 4)
  4) array.length는 0이므로 callback(null, arr)을 호출합니다.
  5) 호출된 위치는 함수가 실행되었던 위치 line 39 입니다. 
  6) 3번째 인자로 만들어진 익명 함수가 실행되고 err은 null 이므로 else문이 실행됩니다.
     arr값을 가지고 있는 result를 console에 띄어주면서 결과를 나타냅니다.

  7) 48번 라인을 거쳐 numberArr을 갖고 each 함수를 실행합니다. (line 50)
  8) each 함수가 실행됩니다. (line 2)
  9) 빈 배열인지 검사합니다. (line 4)
 10) 빈 배열이 아니므로 Array.forEach를 이용하여 배열의 수 만큼 반복합니다. (line 22)
 11) iterator(array, index, done)을 실행하면 iterator의 인자인 square 함수가 호출됩니다. (line 27)
 12) 각 배열의 원소의 타입이 number인지 검사합니다. (line 28)
 13) 아니라면 error을 만들어서 done함수의 인자로 넣어 호출합니다. (line 29)
 14) 맞다면 배열의 원소를 제곱하고 done 함수를 호출합니다. (line 33)
 15) 콜백함수가 전달되면 iterator의 3번째 인자인 done 함수가 호출 됩니다. (line 23)
 16) done 함수는 err가 존재하는지의 여부를 확인합니다. (line 10)
 17) err이 존재하면 callback(err)을 호출합니다. (line 12)
 18) 존재하지 않는다면 completed > arr.length의 조건을 통해 배열의 개수만큼 모든 원소가 제곱이 되도록 합니다.
 
 19) errorArr도 7)~18) 과정을 반복하다가 17) error를 체크하는 부분에서 callback(err)을 호출하고 종료합니다.

결과 출력입니다.

$ node cps_pattern.js
[]
[ 1, 4, 9, 16, 25 ]
Error : 3번째 데이터는 숫자 타입이 아닙니다.


최종정리를 하면 첫 번째 결과는 var emptyArr =[] 이기 때문에 곧바로 callback(null, arr)이 호출되었습니다.
두 번째는 var numberArr = [1, 2, 3, 4, 5]의 모든 원소에 대해 square 함수를 호출하고 callback(null, arr)이 호출되었습니다.
마지막으로 세 번째 결과는 var errorArr = [1, 2, '3', 4, 5] 이기 때문에 세 번째 원소가 number 타입이 아니므로
square 함수 호출 시 callback(err)이 호출되어 만들어졌음을 알 수 있습니다.



이제 CPS coding pattern에 대해서 이해가 되셨나요?
혹시나 제가 놓친 부분이 있다거나 잘못 이해한 부분이 있다면 댓글로 남겨주시면 수정하겠습니다 : )
node.js를 배우고 있고, javascript에 관심이 많은데 이런 코딩 패턴은 생각을 전환하는데 큰 도움이 되는 것 같습니다.

이 자료의 출처는 T아카데미에서 진행한 임정환 강사님의 node.js 수업에서 배운 내용을 토대로 작성하였습니다.