Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

node.js에서 비동기로 인한 의존성 문제 해결하기

node.js는 기본적으로 모든 I/O를 비동기로 처리하고 있고 많은 기능들도 비동기적으로 동작하고 있습니다. 클라이언트측 비동기와는 달리 node.js에서는 이런 비동기적 처리가 계속 연결되기에 Whiteship님이 포스팅한 mongoose의 비동기식 처리처럼 끝입없이 콜백으로 연쇄되는 흐름이 만들어지고 이 글처럼 특정라이브러리에 의존한 형태가 아니라고 하더라도 콜백으로 이어지는 흐름은 꽤나 자주 만들어지게 됩니다.



예를 들어 아래와 같은 소스가 있습니다.

app.get('/', function(req, res) {
    db.open(function(err, db) {
        db.authenticate(USERID, PASS, function() {
            db.collection("", function(err, collection) {
                collection.find({},{}, function(err, cur) {
                    res.render('index.jade', {});
                });   
            }); 
        });       
    });
});

이 부분은 express에서 / 로 요청이 왔을때 MongoDB에 쿼리를 날려서 데이터를 가져온뒤 그 데이터로 응답은 해주는 소스입니다.(각 함수의 기능같은 것은 여기서 중요치 않습니다.) 처음에는 이런 구조의 소스가 무척 이상해보이지만 계속 보자보면 익숙해지고 node.js의 구조가 그렇게 되어 있는 것이니 머 그리 이상할 것도 없습니다.

하지만 문제는 리팩토링을 하기 시작했을 때 발생합니다. app.get()은 express에서 라우팅을 담당하고 있는 함수이므로 이곳에서 디비처리까지 해버리는 것은 별로 좋지 않기 때문에 디비만 따로 모으기 위해서 디비관련부분을 메소드 추출하면 아래와 같이 됩니다.


app.get('/', function(req, res) {
   readData(req, res);
});

var readData = function(req, res) {
    db.open(function(err, db) {
       db.authenticate(USERID, PASS, function() {
           db.collection("", function(err, collection) {
               collection.find({},{}, function(err, cur) {
                   res.render('index.jade', {});
               });  
           });
       });      
   });
}


이전의 소스에서는 JavaScript가 클로저를 지원하기 때문에 콜백함수 내에서 res를 그냥 사용할 수 있지만 이렇게 메서드를 분리하면 req와 res를 파라미터로 넘겨주어야 합니다. 왜냐하면 응답을 만들어주는 부분이 최종콜백 안에서 처리되어야 하기 때문입니다.(물론 이 소스에서는 req까지 넘겨줄 필요는 없습니다.) 여기서는 메서드를 하나로만 분리하였지만 복잡한 로직이나 I/O접근을 여러개의 메서드로 분리되어 있을경우에는 중간단계의 메서드에서는 필요도 없는 req와 res를 계속 물고 다녀야 하는 문제가 발생합니다.(메서드는 한가지 기능만 하는게 좋기 때문에 조금만 복잡한 걸 처리하더라도 금새 메서드는 여러가지로 분리되게 됩니다.)

그냥 파라미터 2개 추가하는 것 자체는 큰 문제가 아니라고 생각할 수도 있지만 더 큰 문제는 테스트에서 발생합니다. 클라이언트측 자바스크립트에서는 UI와 밀접하기도 하고 그래서 라이브러리개발이 아닌 다음에야 TDD나 유닛테스트가 아직 일반화되어 있지는 않은것 같지만(최소한 국내에서는...) node.js는 서버사이드이기 때문에 다른 서버언어들 처럼 유닛테스트를 작성할수 있는데(비동기는 접근이 다르기는 합니다.) 이렇게 분리된 모듈이 웹서버에 대한 의존성을 깊게 가지기 때문에 테스트를 할 수 없게 되어버립니다. 아주 불가능 하지는 않겠지만 req와 res에 대한 값을 만들어서 테스트를 실행하고 결과 응답을 확인해야 합니다. readData()는 단순히 디비를 읽는 함수이지만 테스트까지 express 웹프레임웍에 종속되어서 테스트가 어려워지는 문제가 발생합니다.




이런 문제에 대해서 오랫동안 고민했지만 마땅한 해결책은 못 찾고 있었는데 얼마전 발표준비를 하면서 본 Introduction to Node.js with Ryan Dahl에서 그에 대한 해결책을 찾을 수 있었습니다.

app.get('/', function(req, res) {
    readData(function() {
        res.render('index.jade', {});
    });
});

var readData = function(callback) {
    db.open(function(err, db) {
        db.authenticate(USERID, PASS, function() {
            db.collection("", function(err, collection) {
                collection.find({},{}, function(err, cur) {
                    callback();
                }); 
            });
        });     
    });
}

다른 메서드를 호출할 때 req와 res를 넘겨주는 대신에 콜백함수를 넘겨주고 해당처리가 다 끝난 최종 콜백함수 내에서 파라미터로 박은 콜백함수를 다시 실행시켜줍니다. 이렇게 하면 불필요하게 req, res를 물고다닐 필요가 없이 callback하나만 물고 다니면 되게 바뀌고 분리된 메서드가 콜백에만 의존하기 때문에 express에 가지고 있는 의존성도 사라지고 테스트도 훨씬 간단히 할 수 있게 됩니다.(이런 콜백패턴은 아주 흔하게 사용하는 방식인데 이런걸 왜 스스로는 생각해 내지 못하는지 모르겠군요 ㅠㅠ)
2011/07/13 02:03 2011/07/13 02:03