본문 바로가기

프로그래밍/node.js

Async - 비동기 자바스크립트 프로그래밍을 위한 유용한 유틸리티 모듈


요즘에는 비동기 프로그래밍이 대세인것 같습니다. Apache의 대항마로 떠오른 Nginx도 비동기 프로그래밍 모델을 사용하고, 자바스크립트 서버계의 뜨거운 화두인 node.js도 비동기 I/O 모듈로 소개되고 있습니다. 더하여 윈도우에서 새로 출시되는 운영체제인 윈도우 8 에서도 비동기 프로그래밍 모델을 지원하며, 마이크로소프트에서는 이를 권장하고 있습니다. 비동기 프로그래밍이 옜날부터 존재해오던 개념이지만 최근에 와서 구현이 비교적 쉬워지고 비동기적 모델을 채용했을때 효과가 눈에 띄기 때문에 요즘에 와서 많이 사용되기 시작하는듯 합니다.

자바스크립트는 비동기 프로그래밍에서 사용하기 쉽게 설계되어 있습니다. 인라인으로 함수를 선언할 수 있어서 콜백 패턴을 구현하는데 타 언어보다 쉽고, 콜백 패턴으로 비동기 프로그래밍을 구현할 수 있기 때문입니다. 한가지 문제라고 하면 비동기 프로그래밍을 하게 되면 콜백 함수에 콜백 함수가 꼬리를 물기 때문에 들여쓰기를 엄청 쓰게 되고 코드가 지저분해 집니다. 아래와 같은 예가 있죠.

somefunction1(param1, param2, function () {
	/*some work*/
	somefunction2(param2, param2, function() {
		/*some work*/
		somefunction3(param2, param2, function() {
			/*some work*/
			somefunction4(param2, param2, function() {
				/*some work*/
				somefunction5(param2, param2, function() {
					/*some work*/
					somefunction6(param2, param2, function() {
					});
				});
			});			
		});
	});
});

자바스크립트를 쓰다 보면 보게되는 코드이죠. 들여쓰기가 많아서 코드가 지저분해 보이고 괄호 열고 닫는 부분을 틀리기 쉽습니다. 함수 내부 코드가 길어지게 되면 코드가 어느 함수에 속해 있는지도 헷갈리기 쉽상입니다. 이 코드를 어떻게 하면 좀 깔끔하게 바꿀 수 없을지 고민을 할 수 밖에 없게 됩니다. 실제로 많은 사람들이 이런 고민을 많이 하였고 여러가지 라이브러리 들이 많이 나왔습니다. 그 중에서 오늘 소개하고자 하는 것은 Async라는 라이브러리 입니다.

Async: https://github.com/caolan/async

Async 모듈은 크게 3가지 분류로 나누어 집니다. Collection, Control Flow, Utils로 나누어 지는데, Collection의 기능들은 Underscore.js에도 있는 기능들이고, utils의 기능들도 다른 라이브러리에도 많이 있는 기능이고 하니 여기서는 Control Flow에 대해서만 자세히 다루도록 하겠습니다.

위의 코드를 Async 모듈을 이용해서 코드를 바꿔 보도록 합시다. 여기서 유용한 함수는 Async의 series라는 것입니다. series는 2개의 인자(parameters)를 받습니다. 하나는 배열이고 하나는 콜백함수(Optional)입니다. 배열은 실행할 함수들의 배열이 들어가게 되고 두번째 인자인 콜백 함수는 배열 안의 함수들이 다 끝나거나 에러를 내뱉게 되면 실행되는 함수입니다. 그래서 위의 코드를 바꾸면 아래와 같이 됩니다.

async.series([
	function (callback) {
		// some work
		somefunction1(param1, param2, callback(null, null));
	},
	function (callback) {
		// some work
		somefunction2(param1, param2, callback(null, null));
	},
	function (callback) {
		// some work
		somefunction3(param1, param2, callback(null, null));
	},
	function (callback) {
		// some work
		somefunction4(param1, param2, callback(null, null));
	},
	function (callback) {
		// some work
		somefunction5(param1, param2, callback(null, null));
	},
	function (callback) {
		// some work
		somefunction6(param1, param2, callback(null, null));
	},
]);

들여쓰기가 확실이 줄어들었습니다. 그리고 함수들이 완전히 분리되어서 특정 코드가 어느 함수에 들어가 있는지도 확인하기 쉽습니다. series에서 배열안에 있는 함수들이 차례대로 실행되는데, 모든 함수는 callback 인자를 가지고 있습니다. 그리고 callback 함수를 실행하게 되면 다은 함수로 넘어가게 되는 것입니다. 여기의 콜백함수는 2개의 인자를 받는데 하나는 err이고 하나는 result 값입니다. result들은 마지막 함수에서 results의 배열로 들어가게 됩니다. 아래 코드를 보면 조금더 잘 이해할 수 있지 않을까 생각되네요.

async.series([
	function (callback) {
		// some work
		callback(null, 'one');
	},
	fucntion (callback) {
		// some work
		callback(null, 'two');
	},
],
function (err, results) {
	// results는 콜백으로 넘겨준 값들의 배열이 된다.
	// 여기서 results는 ['one', 'two'] 겠죠?
});

중간에 에러 처리를 하고 싶으면 다음과 같이 하면 됩니다. callback을 실행할때 첫번째 인자에 에러를 넣어주면 됩니다. 에러가 있으면 다음에 나오는 함수들은 모두 실행되지 않고 함수 배열 다음에 있는 콜백할수가 실행됩니다.

async.series([
	function (callback) {
		// some work
		if (err) {
			callback(err);
			// 에러가 나면 다음 함수를 실행하지 않고 바로 마지막 함수를 실행합니다.
		}
		callback(null, 'one');
	},
	fucntion (callback) {
		// some work
		callback(null, 'two');
	},
],
function (err, results) {
	// 에러가 나면 에러 메시지를 출력합니다.
	if (err) {
		console.log(err.message); 
	}
});

series를 쓰니 들여쓰기가 줄어들고 코드가 깔끔해지고 함수의 코드가 분리되고 results를 처리하기 쉬워지고, 에러 처리 또한 간편해지니 일석삼조라고 할 수 있겠네요.

Async에는 series만 있는게 아닙겠죠? series는 단순히 여러 함수들을 실행 시키는 함수였습니다. 하지만 다른 함수를 실행할 때 인자를 넘겨줘야 한다면 어떻게 될것인가? 아래의 코드와 같이 말이죠

somefunction1(param1, param2, function (a, b) {
	/*some work*/
	somefunction2(param2, param2, function(c, d) {
		/*some work*/
		somefunction3(param2, param2, function(e, f) {
			/*some work*/
			somefunction4(param2, param2, function(g, h) {
				/*some work*/
				somefunction5(param2, param2, function(i, j) {
					/*some work*/
					somefunction6(param2, param2, function(k, l) {
					});
				});
			});			
		});
	});
});

이럴 때는 waterfall 함수를 쓰면 됩니다. 방법은 series와 비슷합니다. waterfall도 2개의 인자를 받습니다. 첫번째는 함수의 배열이고 두번째는 에러를 처리하거나 결과값을 처리할 콜백함수입니다. 함수의 배열에 들어가는 함수는 아래와 같이 생겼습니다.

function (arg1, arg2, callback) {  .... });

살짝 다르죠? callback을 실행할 때는 다음과 같이 실행합니다.

callback(null, arg3, arg4)

콜백을 실행할때 첫번째 인자는 series에서와 마찬가지로 에러가 들어갑니다. 그리고 다음 함수의 인자들로 들어가게 될 값들이 들어가게 됩니다.

그러면 waterfall를 써서 위의 코드를 바꾸어 보도록 하겠습니다.

async.waterfall([
	function (callback) {
		// some work
		callback(null, 'one');
	},
	fucntion (arg1, callback) {
		console.log(arg1) // 'one'
		// some work
		callback(null, 'two');
	},
],
function (err, result) {
	console.log(arg1) // 'two'
});

이 이외에도 여러 함수를 동시에 실행하고 모든 함수가 다 끝났을때 특정 코드를 실행할 수 있는 paralle, 특정 결과가 나올때 까지 함수를 반복하는 whilst, 함수 큐를 넣어 실행하는 queue 등등 유용한 함수들이 많이 있습니다.

Async의 Control Flow를 잘 쓰면 깔끔하고 가독성 있고 유지보수가 좋은 코드를 만들 수 있겠죠?