async 공식 문서
https://caolan.github.io/async/
async npm 페이지
https://www.npmjs.com/package/async
Nodejs에서 코드를 작성하다보면, 필연적으로 부딪히게 되는 것 중 하나가, Callback hell이다.
비동기 Non-blocking I/O가 Nodejs의 메인 특징중 하나이다 보니, Nodejs에서 동기적인 로직의 구현이 필요할 경우 이를 풀어나가는 방법이 필요하다.
동기적인 로직이라 함은, 어떤 처리의 순서가 보장되어야 한다는 것이다. 즉, 코드의 A라는 부분이 완료된 이후에야 B라는 부분이 실행되어야 하는 경우 이러한 요구사항이 충족되는 경우를 동기성이 보장된다고 볼 수 있다.
func1();
func2();
위와 같은 코드가 있는 경우를 생각해보자. 그리고 각각의 func1, func2함수는 I/O를 처리한다고 생각해보자.
여기서 I/O라는 것은 CPU에서 계산을 처리하는 것 이외, Disk에 값을 쓰거나 읽는다던지, 인터럽트를 처리한다던지 등의 커널레벨에서 처리해야 할 만 한 것들을 처리한다고 생각을 해 보자.
일반적인 동기, Blocking I/O 방식인 경우, 즉 일반적인 C언어와 같은 환경일 경우에는 func1(); 함수가 리턴된(완료된) 이후에, func2(); 함수가 수행된다는 것을 알고 있다.
하지만 Nodejs의 경우는 비동기 Non-blocking I/O가 기본 설계 원칙(Design Principle)로 제작된 javascript 런타임이고, 이를 이벤트 루프와 poll-큐를 통해서 구현했다. 그래서 위와 같은 코드를 실행할 경우, func1라는 I/O가 먼저 시도되고 func2라는 I/O가 시도되는 것은 보장이 되나, func1가 먼저 끝나라는 보장은 없고, func1가 끝나고 나서 func2가 시작한다는 보장은 더더욱 없다.
따라서 동기성이 보장되지 않게 되는 것이다. 그래서 이러한 동기성을 보장하기 위해서 Nodejs에서는 Callback을 이용하는 방식으로 동시성을 보장하게 만들 수 있다. 다음 코드를 보자.
func1(function() {
func2();
});
callback을 이용한다고 했는데, 이는 간단히 설명하면, 함수를 다른 함수의 인자로 넘기는 것이다.
func1라는 함수를 선언할 때, 첫번째 인자로 callback함수를 받도록 했는데, 이 인자로 들어간 callback함수는 func1 함수에서 처리하는 I/O가 완료된 이후 func1함수 내부에서 실행되도록 구성한 것이다. 따라서 func1의 I/O가 끝난 이후 func2의 I/O를 시도하도록 할 수 있게 된다. 이렇게 콜백 함수를 통해서 Nodejs에서 동시성을 보장하도록 코드를 구성하였다.
하지만 다음 경우를 보자.
func1(function() {
func2(function() {
func3(function() {
func4(function() {
func5();
});
});
});
});
위 예제는 func1부터 func5까지 순서대로 동기성이 보장되어야 하는 경우를 콜백 함수 중첩을 통해 코드로 구현한 모습이다.
코드가 오른쪽으로 계속 indenting이 되어서 마치 > 와 같은 모양이 되었다. 특히나 이런 경우, I/O처리 도중 에러가 발생했을 때 에러처리와 같은 이슈가 발생했을때 코드의 가독성이 매우 떨어지게 된다. 이러한 현상을 callback hell이라고 한다.
func1(function(err1) {
if (err1) {
console.error(err1);
return;
}
func2(function(err2) {
if (err2) {
console.error(err2);
return;
}
func3(function(err3) {
if (err3) {
console.error(err3);
return;
}
func4(function(err4) {
if (err4) {
console.error(err4);
return;
}
func5();
});
});
});
});
위와 같은 코드는, 각각의 함수에서 I/O 등을 처리할 때 에러가 발생했을 경우 에러를 처리하는 로직이 포함된 코드이다. 보는 바와 같이 가독성이 매우 떨어지며, 에러처리 부분은 공통적으로 처리할 수 있을 것 같은데, 중복코드가 많이 발생한다.
이러한 콜백 중첩 방식의 가독성 문제, 에러처리의 불편함을 npm async 모듈을 이용해서 해결해보자. 위와 같이 func1->func2->func3->...와 같은 식의 I/O가 발생할 경우, 즉 이전 I/O가 완료된 이후에 다음 I/O 작업을 수행해야하는 동기성을 보장해야 할경우 async.waterfall을 이용하면 된다.
var async = require('async');
async.waterfall([
function(callback) {
console.log(1);
callback(null);
},
function(callback) {
console.log(2);
callback(null);
},
function(callback) {
console.log(3);
callback(null);
}
], function(err) {
console.log('error:', err);
});
async.waterfall은 말 그대로 폭포가 계단식으로 내려오듯 순서대로 실행하게 된다. 첫번째 인자는 콜백함수의 배열로, 순서대로 실행되어야 하는 작업들을 함수로 받는다. 두번째 인자는 함수로, 마지막 처리를 맡는다. 위의 코드를 실행하면 아래와 같은 결과가 나타난다.
1
2
3
error: null
만약 console.log(1)가 있는 코드 부분에서, callback 함수의 첫번째 인자는 지금 null이라고 되어 있다. 이는 에러가 들어가는 부분으로, false 값으로 판별되는 값이 들어가게 되면 에러가 없기 때문에 다음 함수를 실행하게 된다. 하지만 true값이로 판별되는 값이 들어가게 되면, 다음 callback으로 넘어가지 않고, waterfall함수의 두번째 인자인, 가장 마지막 콜백 함수를 실행하게 된다. 따라서 마지막 콜백 함수에는 에러 처리 로직이 들어가면 된다.
var async = require('async');
async.waterfall([
function(callback) {
console.log(1);
callback('Logical error');
},
function(callback) {
console.log(2);
callback(null);
},
function(callback) {
console.log(3);
callback(null);
}
], function(err) {
if (err) {
console.log('error:', err);
}
});
위 코드를 실행시키면 다음과 같은 결과가 나타난다.
1
error: Logical error
에러값이 true를 갖는 string으로 인자가 들어갔으므로, 곧바로 에러 처리 로직으로 넘어가게 되는 것이다. 또한 callback함수의 두번째부터의 인자는 데이터로, 다음에 실행되는 콜백에서 인자로 받아서 사용할 수 있다. 예제는 아래와 같다.
var async = require('async');
async.waterfall([
function(callback) {
console.log(1);
callback('Logical error', 'Some other data', 'Some other data2', 'Some other data3');
},
function(callback) {
console.log(2);
callback(null);
},
function(callback) {
console.log(3);
callback(null);
}
], function(err, data, data2, data3) {
if (err) {
console.log('error:', err);
}
console.log(data);
console.log(data2);
console.log(data3);
});
1
error: Logical error
Some other data
Some other data2
Some other data3
에러값이 false일 경우, 다음 callback으로도 데이터를 넘겨줄 수 있다. 아래의 예제를 보자.
var async = require('async');
async.waterfall([
function(callback) {
console.log(1);
callback(false, 'Some other data', 'Some other data2', 'Some other data3');
},
function(data, data2, data3, callback) {
console.log(2);
console.log(data);
console.log(data2);
console.log(data3);
callback(null);
},
function(callback) {
console.log(3);
callback(null);
}
], function(err, data, data2, data3) {
if (err) {
console.log('error:', err);
}
console.log(data);
console.log(data2);
console.log(data3);
});
1
2
Some other data
Some other data2
Some other data3
3
undefined
undefined
undefined
error값이 항상 false 진리값을 갖으므로, 순차적으로 계속 실행된다. callback함수 호출 시 2개 이상의 인자를 가진 경우, 2번째 이후 인자는 다음 콜백에서 데이터로 넘어간다. 따라서 2번째 콜백에서 앞에서 넘긴 인자들을 받기 위해 함수 선언형에 인자가 늘었다. 마지막 에러 처리 콜백에서 data, data2, data3의 경우는 그 직전 콜백에서 데이터를 넘겨주지 않았으므로 undefined의 값을 갖는다.