티스토리 뷰

노드의 강점은 그 홈페이지에서 찾아볼 수 있는데 아래와 같이 정의되어 있다.

Node.js 는 이벤트 기반, 논 블로킹 I/O 모델을 사용해 가볍고 효율적입니다.

하지만 이 강력함으로 인해 개발자는 곤욕을 치루게 되는데 그 중 하나가 콜백지옥이다. 콜백지옥이 발생하는 근본적인 이유는 노드의 비동기를 해결하고자 할 때 중첩 콜백이 이어지기 때문인데 왜 콜백을 중첩해서 사용해야 할까? 이 문제에 대해 나는 "우리 개발자 뇌가 아직 동기적으로 코드를 이해하려고 하기 때문" 이라고 이야기한다. 노드를 더 깊게 잘 이해하려면 비동기에 대한 이해를 높이고 중첩 콜백을 풀어야겠다. 

각설하고 노드는 비동기에 특화되어 있는 플랫폼이다보니 무조건 동기적으로 처리해야만 하는 코드를 풀어내야 할 때 난항을 겪게 된다. 예를들어 데이터베이스에서 정보를 읽어오고 그 데이터로 뭔가 액션을 취해야 한다면 이는 절차적으로 처리가 되어야 하는 부분이다. 하지만 데이터베이스에서 정보를 읽어올 수 있도록 제공되는 모듈 자체가 대부분 비동기로 동작하다보니 다음 액션을 이어서 처리하는데 무리가 있겠다. 아무튼 이런 문제를 해결할 수 있는 방법으로 async 모듈이 있고 여기서는 async 모듈에서 제공되는 waterfall 을 통해 비동기를 교통정리 해보자.

연습문제 실습

실전 상황은 다음과 같다. 이 간단한 문제가 풀리면 더 깊게 이어지는 콜백도 같은 원리로 쉽게 풀린다.

  • Redis 에서 foo KEY 를 읽어온다.
  • 읽어온 KEY 값을 bar 에 넣는다.

자, 우선 아래와 같이 심플한 코드가 있다. Redis  에서 데이터를 가져오는 코드인데 foo 라는 string typeKEY 에는 firstString 이라는 값이 들어있다.

const redis = require( "redis" ),
      client = redis.createClient();

client.on( "error", (err) => {
    console.log( "Error " + err );
});

client.get( "foo", ( err, reply ) => { 
    if( err ) throw( err );
    console.log( reply ); // firstString
    client.quit();
});

console.log( "File End" );

이제 가져온 데이터를 bar 라는 다른 Redis KEY 에 넣어볼텐데 일반적인 프로그래밍 방식이라면 코드는 아래와 같은 형상일 것이다. 위의 코드와 달라진 부분은 몇 라인 안된다.

const redis = require( "redis" ),
      client = redis.createClient();

client.on( "error", (err) => {
    console.log( "Error " + err );
});

var fooVal = ''
client.get( "foo", ( err, reply ) => {
    if( err ) throw( err );
    console.log( reply ); // firstString
    fooVal = reply;
});

if( fooVal )
{
    console.log( 'fooVal: ', fooVal );
    client.set( "bar", fooVal, redis.print );
}

console.log( "File End" ); // File End
client.quit();

과연 정상동작할까? 결과는 모두가 예상 가능한 것 처럼 정상동작하지 않는다. 

$ node run.js
File End
firstString
$

이유가 뭘까? Redis 에서 값을 가져오는 과정이 비동기로 동작하다보니 fooVal 에 값이 세팅되기 전에 이미 코드는 if( fooVal ) 조건을 검사하고 run.js 프로세스를 종료해버린다. 그럼 foo 에서 가져온 값을 bar 로 세팅하기 위한 쉬운 방법은 없는걸까? 여기 async.waterfall 이 있다. 우선 모듈을 사용하기 위해 async 를 설치하자.

$ npm install async

그리고 코드를 수정해주면 되는데 async.waterfall 의 기본 사용법은 다음과 같다.
공식문서: https://caolan.github.io/async/docs.html#waterfall 

waterfall 안에 작업 함수를 직접 구현하는 방식

async.waterfall([
    function(callback) {
        callback(null, 'one', 'two');
    },
    function(arg1, arg2, callback) {
        // arg1 now equals 'one' and arg2 now equals 'two'
        callback(null, 'three');
    },
    function(arg1, callback) {
        // arg1 now equals 'three'
        callback(null, 'done');
    }
], function (err, result) {
    // result now equals 'done'
});

waterfall 안에 함수 이름만 달아놓고 원형은 밖에 제공하는 방식

async.waterfall([
    myFirstFunction,
    mySecondFunction,
    myLastFunction,
], function (err, result) {
    // result now equals 'done'
});
function myFirstFunction(callback) {
    callback(null, 'one', 'two');
}
function mySecondFunction(arg1, arg2, callback) {
    // arg1 now equals 'one' and arg2 now equals 'two'
    callback(null, 'three');
}
function myLastFunction(arg1, callback) {
    // arg1 now equals 'three'
    callback(null, 'done');
}

우리 예제는 작업 함수의 크기가 길지 않으니 첫 번째 방식으로 구현을 해보자. waterfall 안에서 해야할 작업이 많아지면 해당 코드의 목적을 한눈에 알아보기 쉽게하기 위해 두번째 방식이 선호될 수 있다.

const redis = require( "redis" ),
      client = redis.createClient();
const async = require( "async" );

client.on( "error", (err) => {
    console.log( "Error " + err );
});

var fooVal = ''

async.waterfall(
[
    function( cb )
    {
        client.get( "foo", ( err, reply ) => {
            if( err ) throw( err );
            console.log( reply ); // firstString
            cb( null, reply );
        });
    },
    function( fooVal, cb )
    {
        console.log( 'fooVal: ', fooVal ); // fooVal:  firstString
        client.set( "bar", fooVal, redis.print ); // Reply: OK
        cb( null );
    }
],
function( err, obj )
{
    if( err ) throw( err );
    client.quit();
});

console.log( "File End" ); // File End

실행결과는 다음과 같다.

$ node run.js
File End
firstString
fooVal:  firstString
Reply: OK
$

waterfall 을 통해 작업을 간단하게 순차처리 하였다. 여기서 주의해야하는 점이 하나 있다면 연결한 Redis 세션을 종료하거나 프로세스를 종료하는 process.exit() 와 같은 코드는 위치 선정을 잘 해야 한다. waterfall 은 분명 작업 ( task ) 에 대해서 절차적으로 처리하지만 waterfall 자체가 비동기로 동작하기 때문에 뒤쪽에 process.exit() 가 존재하면 프로그램은 비정상적으로 동작할 것이다. 아래와 같은 코드는 분명 문제가 있다. 꼭 주의하도록 하자.

async.waterfall(
task
, function( err, obj )
{
    if( err ) throw( err );
    client.quit();
});
process.exit();


마무리

async.waterfall 을 통해 비동기 처리에 대해 교통정리를 해보았다. 여기서는 따로 다루지 않았지만 async 에는 waterfall 이외에도 다양한 모듈이 제공되니 참고하면 좋다. 아울러 노드를 잘 다룬다는 것은 결국 비동기의 처리 과정을 이해한다는 의미와도 같은데 그러기 위해서는 그 내부 구조인 libuv 까지 내려가봐야 하지 않을까 생각해본다. 사실 waterfall 이나 series, parallel 등에 대한 글은 인터넷에 너무 많지만 이 정보의 홍수 속에서 이 글이 누군가에게는 도움이 될 수 있기를 바란다.


댓글
최근에 올라온 글
최근에 달린 댓글
글 보관함
Total
Today
Yesterday