기본 콘텐츠로 건너뛰기

[node.js] promise pattern을 활용하여 async의 callback hell 해결

동기, 비동기 그리고 promise

Asynchronous(비동기) Code란 무엇일까? node를 접하지 않았다면 대부분의 개발자들은 동기방식의 코드에 익숙할 것이다. 다음 코드를 보면 이해하기 쉬울 것이다.
void FTest(){
    for(int i = 0 ; i < 10 ; i++){
        printf("%d", i);
    }
    return ;
}
void main(){
    FTest();
    printf("success");
    return 0;
}
위 코드를 실행한다면, FTest()가 실행이 되면서 0부터 9까지 출력이 될것이다. 해당 함수가 종료가 된 후 success가 출력이 될것이다. 프로그램을 실행시키는 프로세스는 해당 함수가 종료가 될 때까지 점유를 하고있다. 즉 다른 처리를 하지 못하고 끝날때까지 기다리는 것을 의미한다.(만약 해당 함수가 끝나지 않았는데 처리를 하고 싶다면 쓰레드를 만들면 된다.)
java spring기반의 서버를 예를 들어보자 spring은 클라이언트로부터 request가 발생하면 즉시 쓰레드를 만들어낸다. 이후 요청에 대한 처리가 완료된 후 해당 쓰레드를 없앤다. spring같은 경우는 약 1000개의 쓰레드를 생성하여 처리를 할 수 있다고 한다. 물론 서버 PC의 사양에 따라 넘을수도 아닐수도 있을것이다.
만약 쓰레드가 꽉 찼다면? 쓰레드가 꽉 차면 클라이언트는 더이상 요청을 할 수가 없다.
비동기 방식을 사용하면 이러한 문제를 해결할 수 있다. 비동기 방식은 위 코드와 반대로 코드가 끝날 때까지 기다리지 않는다. 즉 요청을 언제든지 받을 수 있다는 것을 의미한다.


그림을 보면 node.js내부에 event loop라는 놈이 존재한다. event loop는 외부에서 요청이 들어와 비동기 함수를 호출 할 경우 해당 처리 부분을 그냥 다른놈에게 연산을 하라고 넘겨주기만 한다. 연산이 완료가 되면 callback이라는 특수 함수를 인자로 받는다. callback 함수는 연산이 끝나면 실행을 하라는 무명 함수이다. 아래의 예시 코드를 보자.
MongoClient.connect(url, function(err, db) {
    console.log("Connected succesfully to server");
    db.collection('inserts').insert({_id: 123, a:2}, function(err, r) {
        console.log('insert success');
        db.close();
    });
    console.log('test point');
});
위코드는 db에 data를 insert시킨후 insert success를 출력을 하게된다.
동기방식으로 생각을 하면 Connected succesfully to server insert success test point 순으로 출력을 해야하지만





test point가 먼저출력이 된다.
그렇다 event loop는 코드를 전부 떠넘긴다. 그리고 다음 요청을 받을 준비를 한다.
이 설명만 읽어본다면 비동기 방식은 처리를 떠넘기고 사용자의 요청을 받을 준비를 하기 때문에 싱글 쓰레드로도 충분히 서버를 돌릴 수 있는 매우 훌륭한 방법이다. 하지만 좋은 방법임에 반에 코드가 매우 더러워지는 callback지옥을 경험을 하게 된다. 또한 callback을 루프로 돌리는 건 쉬운일이 아니다.
MongoClient.connect(url, function(err, db) {
    console.log("Connected succesfully to server");
    db.collection('inserts').insert({_id: 123, a:2}, function(err, r) {
        console.log('insert success');
        db.collection('inserts').insert({_id: 123, a:2}, function(err, r) {
            console.log('insert success');
            db.collection('inserts').insert({_id: 123, a:2}, function(err, r) {
                console.log('insert success');
                db.collection('inserts').insert({_id: 123, a:2}, function(err, r) {
                    console.log('insert success');
                    db.close();
                });
            });
        });
    });
    console.log('test point');
});
이런 현상이 될 것이다.(코드만 봐도 한숨이 나오지 아니한가!!!) 이러한 callback hell을 탈출하는 방법 중 하나가 async모듈, promise패턴을 사용하는 것인데, async 모듈보다는 promise 사용법에 더욱 익숙해져 있기 때문에 promise에 대해서 설명 하겠다.

Promise patternn

MongoClient.connect(url, function(err, db) {
    console.log("Connected succesfully to server");
    pr(db).then(function(r){
        console.log('test point');
    });
});
var pr = function(db){
    return new Promise(function(resolve, reject){
        console.log('test');
        db.collection('inserts').insert({_id: 123, a:2}, function(err, r) {
            console.log('insert success');
            db.close();
            resolve('success');
        });
    })
}
먼가 코드가 복잡해 보인다. Promise 객체를 생성하여 코드를 분리 하였기 때문에 코드가 좀더 길어질 수 있다.




pr이라는 놈은 Promise라는 객체를 생성하여 반환을 해준다. Promise의 콜백 함수는 두개의 인자를 받는다 첫번째 인자인 resolve는 해당 객체가 성공적으로 실행 됬을 경우 반활 될 값. reject는 해당 함수가 중간에 에러가 났을 경우 error로 넘겨줄 값.
위 코드에서 pr() 좀더 다듬어 보자.
var pr = function(db){
    return new Promise(function(resolve, reject){
        console.log('test');
        db.collection('inserts').insert({_id: 123, a:2}, function(err, r) {
            if(err) reject(err);
            console.log('insert success');
            db.close();
            resolve('success');
        });
    })
}
if(err) reject(err)를 추가함으로써 에러 부분을 처리를 해 줄 수 있다.
pr()을 호출한 후 then()이 붙어있다. 요넘은 이전의 함수의 return값을 callback으로 받는다고 생각하면 된다. 그렇기 때문에 만약 function(r)에서 r을 출력을 한다면 resolve('success')의 success를 출력을 할 것이다.
then은 두번째 인자로 이전의 호출에서 error 발생 및 reject됬을 때 호출 되는 콜백을 받기도 한다.
MongoClient.connect(url, function(err, db) {
    console.log("Connected succesfully to server");
    pr(db).then(function(r){
        console.log('test point');
        return 123;
    },function(err){
        //pr(db)가 error이 났을 경우 호출 
        console.log(err);
    }).then(function(data){
        console.log(data);
        return 2;
    },function(err){
        //return 123;이 찍힌 부분이 error 났을 경우 호출 
        console.log(err);
    }).then(function(r){
        console.log(r);
        return 0;
    })
});
 





순처적으로 처리가 됨을 볼 수 있다. 또한 각각의 시점에서의 에러 처리를 할 수 있다.
promise.all을 이용하연 여러개의 처리를 한 번에 할 수 있다. 단 하나라도 에러가 날 결우 error 부분을 호출을 한다.
MongoClient.connect(url, function(err, db) {
    console.log("Connected succesfully to server");
    Promise.all([pr(db), pr(db)]).then(function(r){
        //모두 성공했을 경우 
        console.log('test point');
        console.log(r);
        return 123;
    },function(err){
        //하나라도 실패했을 경우 
        console.log(err);
    });
});





new Promise가 두번이 실행되고 resolve의 값이 배열로 넘어갔음을 볼 수 있다.
추가적으로 mongodb가 에서 many라는 것이 추가되면서 loop문이 많이 해소 될 것으로 보인다. (mongodb로 바꿔서 짜본 결과 코드라인수도 많이 줄어들고 가독성도 많이 향상이 되었다.)
db.collection('inserts').insertMany([{a:2}, {b:3}], function(err, r) {
        console.log('insert success');
        db.close();
});
db.collection('inserts').insertMany(dbTest, function(err, r) {
        console.log('insert success');
        db.close();
});
insertMany를 사용하면 객체 하나가 아닌 array형태로 넘기면 각각을 루프를 돌면서 insert를 해주는것 같다.

javascript 특성만을 이용하여 가독성 향상

마지막으로 promise나 async를 사용하지 않고 순수 javascript의 특성을 이용하여 가독성을 향상시켜 보겠다. javascript는 함수를 1급객체(first class object) 취급을 한다.
아래와 같은 조건을 가진 것을 1급객체(first class object)라 한다.
  1. 변수(variable)에 담을 수 있다.
  2. 인자(parameter)로 전달할 수 있다.
  3. 반환값(return value)으로 전달할 수 있다.
추가적으로 아래와 같은 조건을 가진 것을 1급함수(first class function)라 한다.
  1. 런타임(runtime) 생성이 가능하다.
  2. 익명(anonymous)으로 생성이 가능하다.
위조건을 따지면 c언어에서의 함수는 1급함수로 취급할 수 없다. 반면에 javascript에서는 함수를 1급함수 또는 1급객체로 취급을 한다. 통상 1급객체라 부른다.
다시 본론으로 돌아와 위와 같은 특징을 이용하여 가독성을 높여주는 코드를 작성을 해보겠다.
var fs = require('fs');
fs.readFile('db1.txt','utf-8', function(err, data1){
    fs.readFile('db2.txt','utf-8', function(err, data2){
        console.log(data1, data2);
    });
});
요런 코드가 있다. 위 코드를 async나 promise를 이용하여 바꿀경우 외부에 promise 객체를 생성하여 해당 객체를 불러와 then으로 체인연결을 하여 짤 것이다.
var fs = require('fs');
var content_check2 = function(err, data){
    console.log(data);
}
var content_check1 = function(err, data){
    console.log(data);
    fs.readFile('db1.txt','utf-8', content_check2);
}
fs.readFile('db2.txt','utf-8', content_check1);
이렇게 코드를 바꿔주면 아무런 모듈을 쓰지 않고도 충분히 가독성을 높여주는 코드를 작성할 수 있다.
var fs = require('fs');
var content = {
    check:function() {
        fs.readFile('db2.txt','utf-8', this.check1);
    },
    _check1:function(err, data) {
        console.log(data);
        fs.readFile('db1.txt','utf-8', this._check2);
    },
    _check2:function(err, data) {
        console.log(data);
    }
};
content.check();
module.exports = {
    content : content
}
위와 같이 모듈로 만들어서 쓰기도 간단하다.

댓글

이 블로그의 인기 게시물

[kali linux] sqlmap - post요청 injection 시도

아래 내용은 직접 테스트 서버를 구축하여 테스트 함을 알립니다.  실 서버에 사용하여 얻는 불이익에는 책임을 지지 않음을 알립니다. sqlmap을 이용하여 get요청이 아닌 post요청에 대해서 injection공격을 시도하자. 뚀한 다양한 플래그를 이용하여 DB 취약점 테스트를 진행을 해보려고 한다. 서버  OS : windows 7 64bit Web server : X Server engine : node.js Framework : expresss Use modules : mysql Address : 172.30.1.30 Open port : 6000번 공격자 OS : kali linux 64bit use tools : sqlmap Address : 172.30.1.57 우선 서버측 부터  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 var  express  =  require( 'express' ); var  app  =  express(); var  mysql  =  require( 'mysql' ); var  ccc  =  mysql.createConnection({     host: '127.0.0.1' ,     user: 'root' ,     pos...

[git] git log 확인하기

git log를 통해서 커밋 이력과 해당 커밋에서 어떤 작업이 있었는지에 대해 조회를 할 수 있다. 우선 git에서의 주요 명령어부터 알아보겠다. $ git push [branch name] $ git pull [branch name] 여기서 branch name은 로컬일 경우 해당 브런치 이름만 적으면 되지만 깃허브 원격 저장소로 연결을 원할 경우는 해당 브런치 이름 앞에 꼭 origin을 붙이도록 한다. $ git brnch [branch name] $ git checkout [branch name] branch일경우 해당 브런치를 생성을 한다. 여기서 현재의 브런치를 기준으로 브런치를 따는것이다. checkout은 브런치를 바꾸는 것이다.(HEAD~[숫자]를 이용하면 해당 커밋으로 움직일수 있다.. 아니면 해당 커밋 번호를 통해 직접 옮기는것도 가능하다.) -> 해당 커밋으로 옮기는 것일뿐 실질적으로 바뀌는 것은 없다. 해당 커밋으로 완전히 되돌리려면 reset이라는 명령어를 써야한다. 처음 checkout을 쓰면 매우 신기하게 느껴진다. 막 폴더가 생겼다가 지워졌다가 ㅋㅋㅋㅋㅋ  master 브런치에서는 ht.html파일이 존재하지만 a브런치에서는 존재하지않는다. checkout 으로 변경을 하면 D 로 명시를 해준다.  $ git log 해당 브런치의 커밋 내역을 보여준다. a 브런치의 커밋 내역들이다. (머지 테스트를 하느라 커밋 내용이 거의 비슷하다 ㅋㅋ) master 브런치의 커밋 내역들이다. 커밋 번호, 사용자, 날짜, 내용순으로 등장을 한다. 이건 단순히 지금까지의 내역을 훑어보기 좋다. 좀더 세밀한 내용을 봐보자. $ git log --stat --stat을 붙이면 기존의 로그에서 간략하게...

[git] pull을 하여 최신코드를 내려받자

보면 먼가 로고가 다르게 뜨는것을 확인을 할 수가있다. C:\Users\mung\Desktop\etc\study\python-gene>git checkout remotes/origin/master Note: checking out 'remotes/origin/master'. You are in 'detached HEAD' state. You can look around, make experimental changes and commit them, and you can discard any commits you make in this state without impacting any branches by performing another checkout. If you want to create a new branch to retain commits you create, you may do so (now or later) by using -b with the checkout command again. Example:   git checkout -b HEAD is now at 29e282a... fetch test C:\Users\mung\Desktop\etc\study\python-gene>git branch * (HEAD detached at origin/master)   master   test1   test2 깃이 잘 쓰면 참 좋은놈인데 어지간히 쓰기가 까다롭다. 처음에 깃을 푸시 성공하는데만 한달정도 걸렸던걸로 기억이 난다.. ㅋㅋㅋ 여담으로  깃 프로필을 가면 아래사진 처럼 보인다. 기여도에 따라서 초록색으로 작은 박스가 채워지는데 저걸 잔디라고 표현을 한다고 합니다 ㅎ 저 사진은 제 깃 기여도 사진입니당 ㅋㅋㅋㅋ 다시 본론으로 돌아와서 ㅋㅋ pull을 하면...