클라우드 컴퓨팅 & NoSQL/Vert.x & Node.js

빠르게 훝어 보는 node.js - promise를 이용한 node.js에서 콜백헬의 처리

Terry Cho 2016. 3. 18. 00:16

Promise를 이용한 node.js에서 콜백헬의 처리


조대협 (http://bcho.tistory.com)


앞의 글(http://bcho.tistory.com/1083) 에서 async 프레임웍을 이용한 콜백헬을 처리 하는 방법에 대해서 알아보았다.

async 프레임웍 이외에, 콜백헬을 해결할 수 있는 프레임웍으로 promise가 있다.

Promise는 원래 콜백헬을 해결하기 위한 프레임웍이 아니라, 프로그래밍 패턴중의 하나로 지연 응답을 통해서 동시성을 제어 하기 위한 목적으로 만들어졌다. 자바스크립트에서는 JqueryDeferred, CommonJS에 구현되어 있고, ECMAScript5 표준에 포함되서 크롬,파이어폭스,인터넷익스플로러 9 버전등에 포함되어 있다.

구현체가 많아서 설치해야 한다.

node.js는 크롬의 자바스크립트 엔진을 기반으로 하기 때문에, promise가 내장되어 있다.

 

프로미스의 개념

 

asyncfunction이라는 비동기 함수가 있다고 가정하자. 이 함수는 param1,param2를 인자로 받아서 비동기로 처리하는 함수이다. promise 패턴에서는 이 asyncfunction을 호출하면, promise라는 것을 리턴한다. promise란 미래 결과에 대한 약속이다. 그리고 promise의 결과가 성공인지 실패인지에 따라서 이를 핸들링하기 위한 로직을 정의해놓는다. asyncfunction이 처리를 끝내고 결과를 리턴하면 promise에 의해 정의된 로직에 따라 결과값을 처리한다.

약간 말이 복잡한데, 이를 풀어서 설명해보면 다음과 같다.

 

·         프로그램      : asyncfunction에게 “param1param2로 처리해줘라고 부탁한다.

·         asyncfunction : “알았어 처리해줄께, 대신 시간이 걸리니 바로 답은 줄 수 없고, 나중에 답을 줄게. 이게 그 약속(promise)라고 하고, 약속(promise) 객체를 리턴한다.

·         프로그램      : ‘언제 끝날지 모르는 작업이구나그러면 이렇게 해줘. 작업이 성공하면 결과 처리 로직을 실행하게 하고, 만약에 실패하면 에러 처리 로직을 처리하게 하자. 이 내용을 니가 준 약속(promise)에 추가로 적어 넣을께

·         asyncfunction : 실행이 성공적으로 종료되었어. 아 아까 준 약속에 성공시에 처리하는 로직이 정의되어 있군. “결과처리로직를 실행하자

 

이런 내용이 어떻게 코드로 구체화 되는지를 살펴보자

 

var promise = asyncfunction(param1,param2);

promise.then(function(result){

      //결과처리로직

},function(err){

      //에러처리로직

}

 

Figure 1 promise를 이용한 비동기 호출 처리 예제

 

var promise = asyncfunction(param1,param2);

첫번째 코드에서 asyncfunction은 앞서 언급한것과 같이 비동기 함수이다. asyncfunction이 호출되고 나서 결과 값이 아니라, 나중에 결과를 주겠다는 약속(promise) 객체를 리턴한다.

 

다음으로 비동기 함수의 처리가 끝났을때 성공과 실패의 경우 어떻게 처리를 할지를 비동기 함수가 리턴한 약속(promise)에 기술해놓는다.이를 위해서 then이라는 키워드를 사용하는데 다음과 같은 포맷을 사용한다.

 

promise.then(결과처리함수(결과값) ,에러처리 함수(err) )

Figure 2 promise.then의 문법

 

비동기 함수 실행이 끝나면, then에 정의된 첫번째 함수 결과처리함수를 실행하여 비동기 함수 실행 결과를 처리한다.

이때 결과처리함수는 비동기 함수가 처리한 내용에 대한 결과 값을 인자로 갖는다.

만약에 에러가 발생하였을 경우에는 then에 두번째 인자로 정의된 에러처리함수를 실행하여 에러를 처리한다. “에러처리함수는 에러의 내용을 err이라는 인자를 통해서 받는다.

 

앞의 예제에서 2~6줄은 then을 이용하여, 첫번째 인자로 결과처리로직을 가지는 함수를 정의하고, 두번째 인자로에러처리로직을 갖는 함수를 정의했다.

 

promise.then(function(result){

      //결과처리로직

},function(err){

      //에러처리로직

}

Figure 3 프로미스에 결과 및 에러 처리를 지정하는 방법

 

 첫번째 결과처리로직을 갖는 함수는 비동기 함수가 리턴해준 결과값인 “result”를 인자로 받고, 두번째 에러처리로직을 갖는 함수는 에러내용을 “err”라는 인자로 받는다.

 

그렇다면 약속(promise)를 리턴하는 비동기 함수는 어떤 형태로 정의되어야 할까?

promise를 지원하는 비동기 함수는 아래와 같은 형태와 같다. 리턴시에 new Promise를 이용하여 promise 객체를 만들어서 리턴하는데, 이때 두가지 인자를 받는다. resolved reject인데, 성공적으로 실행이 되었으면 이 resolved함수 를 호출하고 이때 인자로 결과값을 넣어서 넘긴다. 반대로 실패했을 경우에는 인자로 받은 rejected 함수를 호출하되 호출 인자로 에러 내용을 담고 있는 err을 넣어서 넘긴다.

 

function asyncfunction(param1,param2){

     

      return new Promise(resolved,rejected){

           if(성공하였는가?){

                 // 성공하였을 경우

                 resolved ("결과");

           }else{

                 rejected(Error(err));

           }

      }

}

Figure 4 프로미스 지원 비동기 함수 정의 방법


프로미스 예제

 

그러면 위의 개념에 따라 실제로 작동하는 코드를 작성하자

 

var Promise = require('promise');

 

var asyncfunction = function(param){

      return new Promise(function(resolved,rejected){

           setTimeout(

                 function(){

                       resolved('hello'+param);

                 },2000);

      });

         

}

 

var promise = asyncfunction(' terry ');

promise.then(console.log,console.err); // 여기가 비동기 결과에 대한 콜백함

 

Figure 5 간단한 프로미스 함수 및 사용 예제

 

promise를 사용하기 위해서는 promise 모듈을 require 이용하여 불러들인다.

다음으로 asyncfunction을 정의하고 리턴값으로 Promise객체를 리턴한다. Promise 객체 안에서는 처리할 비지니스 로직이 정의되어 있다. 위의 예제에서는  setTimeout을 이용하여 2초를 기다리도록 하였고, 2초후에 콜백함수에서 resolved 함수를 호출하여 promise를 종료하도록 하였다.

 

다음은 이 promise를 리턴하는 비동기 함수를 실제로 호출하고, 이 비동기 함수에 대해서 성공 및 실패에 대한 처리 함수를 then으로 정의한 부분이다.

var promise = asyncfunction(' terry ');

promise.then(console.log,console.err); // 여기가 비동기 결과에 대한 콜백함


then을 이용하여, 성공시 console.log 함수를 호출하도록 하였고, 실패시에는 console.err를 통해서 에러 메시지를 출력하도록 하였다.

 

프로미스 체이닝 (promise chainning)

여러개의 비동기 함수를 순차적으로 실행하는 방법에 대해서 알아보자.

async 프레임웍의 waterfall과 같은 흐름 제어이다.

다음은 asyncfunction1,2,3를 순차적으로 실행하고, 앞 비동기 함수의 결과를 뒤에 따라오는 비동기 함수의 입력값으로 받아서 처리하는 예제이다.


 

var Promise = require('promise');

 

var asyncfunction1 = function(param){

      return new Promise(function(fullfilled,rejected){

           setTimeout(

                 function(){

                       fullfilled('result 1:'+param);

                 },1000);

      });

}

var asyncfunction2 = function(param){

      return new Promise(function(fullfilled,rejected){

           setTimeout(

                 function(){

                       fullfilled('result 2:'+param);

                 },1000);

      });

}

var asyncfunction3 = function(param){

      return new Promise(function(fullfilled,rejected){

           setTimeout(

                 function(){

                       fullfilled('result 3:'+param);

                 },1000);

      });

}

 

var promise = asyncfunction1(' terry ');

promise

.then(asyncfunction2)

.then(asyncfunction3)

.then(console.log);

 

Figure 6 프로미스 태스크 체이닝 예제

 

promise를 리턴하는 3개의 비동기 함수를 정의하였고, 첫번째 함수로 promise를 만든다음. 실행을 하였다. 다음 then을 이용하여, 다음번에 실행해야하는 비동기 함수 asyncfunction2, asyncfunction3를 순차적으로 정의하였고, 마지막의 최종 결과를 출력하기 위해서 최종 then console.log를 지정하여, 결과값을 출력하도록 하였다.

 

result 3:result 2:result 1: terry

 

하나의 예제를 더 살펴보자

다음 예제는 파일을 읽어서 읽은 내용을 다른 파일에 쓰는 내용이다.

 

 

var Promise = require('promise');

 

var fs = require('fs');

var src = '/tmp/myfile.txt';

var des = '/tmp/myfile_promise2.txt';

 

var fread = Promise.denodeify(fs.readFile);

var fwrite = Promise.denodeify(fs.writeFile);

 

fread(src,'utf-8')

.then(function(text){

           console.log('Read done');

           console.log(text);

           return fwrite(des,text); // 체이닝을 하려면 return 해줘야 .

      })

.then(function(){           

           console.log('Write done');

      })

.catch(function(reason){               

           console.log('Read or Write file error');

           console.log(reason);

});

 

console.log('Promise example');

 

Figure 7 프로미스를 이용해서 파일을 읽어서 다른 파일에 쓰는 예제

 

이 코드에서 먼저 주의 깊게 봐야 하는 부분은 denodeify 부분이다.


var fread = Promise.denodeify(fs.readFile);

var fwrite = Promise.denodeify(fs.writeFile);

 

node.js의 비동기 함수들은 프로미스패턴을 지원하지 않는 경우가 많다. 그래서 프로미스 패턴을 지원하지 않는 일반 함수들을 프로미스를 지원할 수 있는 형태로 변경을 해야 하는데, 이 변경을 해주는 함수가 Promise.denodeify이다.

프로미스화가 끝났으면 이 함수를 프로미스를 사용해서 호출할 수 있다.

 

fread(src,'utf-8')

.then(function(text){

           console.log('Read done');

           console.log(text);

           return fwrite(des,text); // 체이닝을 하려면 return 해줘야 .

      })

 


fread를 수행한 후에, then에서 return시 다음 비동기 함수인 fwrite를 수행한다. 이렇게 하면 task들을 체이닝할 수 있다.


프로미스 에러 핸들링

프로미스에서 에러를 핸들링하는 방법에 대해서 알아보자. 앞의 예제에서 then 중간에 catch라는 구문을 사용했는데, catch가 에러핸들러이다.

아래 코드를 보자 아래 코드는 비동기 함수에서 인위적으로 에러를 발생시켜서 처리 하는 코드이다.


 

var Promise = require('promise');

 

var asyncfunction = function(param){

      return new Promise(function(fullfilled,rejected){

           setTimeout(

                 function(){

                       rejected(Error('this is err '+param));

                 },2000);

      });

         

}

 

asyncfunction(' terry ')

.then(console.log,console.error);

 

asyncfunction('cath')

.then(console.log)

.catch(console.error);

 

Figure 8 프로미스에서 에러처리를 하는 예제

 

asyncfunction내의 프로미스에서 setTimeout으로 2초가 지나면, rejected를 이용하여 에러를 리턴하였다.

첫번째 asyncfunction호출에서는 then에 두개의 인자를 넘겼는데, 두번째 console.error가 에러 핸들러이다. 그래서 에러를 console.error로 출력하게 된다.

두번째 asyncfunction 호출에서는 다른 문법의 에러 핸들링을 사용했는데, then에 두개의 인자를 넘기는 대신, catch를 이용해서 에러 핸들러를 정의하였다.

이 예제를 실행하면 다음과 같은 결과를 얻게 된다.

 

[Error: this is err  terry ]

[Error: this is err cath]

Figure 9 프로미스에서 에러처리를 하는 예제 실행 결과

 

만약에 여러개의 태스크가 연결된 비동기 함수 체인을 호출할때 에러 처리는 어떻게 될까? 아래 코드를 보자.

asyncfunction1,2,3,4,5 가 정의되어 있고, 2 4에서 에러를 발생 시키도록 하였다.

그리고 3번과 5번 뒤에 catch를 넣어서 에러 처리를 하도록 하였는데, 그러면 에러 처리 흐름은 어떻게 될까?


var Promise = require('promise');

 

var asyncfunction1 = function(param){

      return new Promise(function(resolved,rejected){

           setTimeout(

                 function(){

                       console.log('func1');

                       resolved('func 1 success:'+param+'\n');

                 },500);

      });

}

var asyncfunction2 = function(param){

      return new Promise(function(resolved,rejected){

           setTimeout(

                 function(){

                       console.log('func2');

                       rejected(new Error('func 2 error:'+param+'\n'));

                 },500);

      });

}

var asyncfunction3 = function(param){

      return new Promise(function(resolved,rejected){

           setTimeout(

                 function(){

                       console.log('func3');

                       resolved('func 3 success:'+param+'\n');

                 },500);

      });

}

var asyncfunction4 = function(param){

      return new Promise(function(resolved,rejected){

           setTimeout(

                 function(){

                       console.log('func4');

                       rejected(Error('func 4 error:'+param+'\n'));

                 },500);

      });

}

var asyncfunction5 = function(param){

      return new Promise(function(resolved,rejected){

           setTimeout(

                 function(){

                       console.log('func5');

                       resolved('func 5 success:'+param+'\n');

                 },500);

      });

}

 

var promise = asyncfunction1(' terry ');

promise

.then(asyncfunction2)

.then(asyncfunction3)

.catch(console.error) // errorhandler1

.then(asyncfunction4)

.then(asyncfunction5)

.catch(console.error)  // errorhandler2

.then(console.log);

 

 

Figure 10 프로미스 태스크 체인에서 에러 처리를 하는 예제

 

3,5번 뒤에 붙은 catch는 어느 비동기 함수들의 에러를 처리할까? 다음 그림을 보자


Figure 11 프로미스 태스크 체인에서 에러 처리를 하는 예제의 에러 처리 흐름

 

1,2,3 번 뒤에 catch를 정의 했기 때문에, 1,2,3번을 수행하던중 에러가 발생하면 수행을 멈추고 첫번째 에러핸들러인 //errorhandler1으로 가서 에러를 처리한다. 여기서 중요한 점은 에러처리 후에, 다시 원래 제어 흐름으로 복귀한다는 것이다. 흐름을 끝내지 않고, 다음 에러핸들러에 의해서 통제 되는 4,5번을 수행한다. 4,5번의 에러는 4,5번 호출 뒤에 붙어 있는 catch //errorhandler2에 의해서 처리 된다. 마찬가지로 //errorhandler2에 의해서 실행이 된후에 맨 마지막 비동기 함수인 console.log를 실행하게 된다.

 

앞에서 2,4번에 에러를 냈으니 실제 흐름이 어떻게 되는지 확인해보자



Figure 12 프로미스 태스크 체인에서 에러 처리를 하는 예제의 실제 수행 흐름


asyncfunction 1,2가 실행되고 에러를 만나서 첫번째 catch에 의해서 에러 처리가 되고, 에러 처리 후 4번이 실행된후 에러를 만나서 두번째 catch가 실행이 되고 마지막에 정의된 console.log가 실행이 된다.

이 흐름을 그림으로 표현해보면 다음과 같다.

 

실행 결과는 다음과 같다.

 

func1

func2

[Error: func 2 error:func 1 success: terry

 

]

func4

[Error: func 4 error:undefined

]

undefined

 

Figure 13 프로미스 태스크 체인에서 에러 처리를 하는 예제를 실행한 결과 


프로미스 지원 프레임웍

지금까지 promise 모듈을 이용하여 promise 패턴을 이용한 비동기 패턴 처리를 알아보았다. 앞에서 살펴보았듯이 쉽게 콜백헬을 해결할 수 있다. async waterfall 흐름 제어와 동일한 흐름 제어 부분만 살펴보았지만, promise 역시, asyncseries, parallel등과 같은 다양한 흐름 제어 알고리즘을 지원한다.

 이 글에서는 promise 모듈을 사용하였지만, 이 프로미스 패턴을 지원하는 모듈은 이외에도 Q (https://github.com/kriskowal/q) , bluebird (http://bluebirdjs.com/docs/getting-started.html) 등 다양한 프레임웍이 있다.

근래에는 성능이나 기능 확장성이 좋은 bluebird가 많이 사용되고 있으니, 실제 운영 코드를 작성하기 위해서는 다른 프로미스 프레임웍도 검토하기 바란다.

 

참고

https://davidwalsh.name/promises

https://github.com/stackp/promisejs 예제가 좋음

http://programmingsummaries.tistory.com/325 정리가 잘되어 있음 추천.

그리드형