-
async 모듈을 통한 비동기 제어 :: 마이구미Nodejs 2017. 3. 18. 22:11반응형
비동기 관련 최신글 async/await - http://mygumi.tistory.com/328
이번 글은 Node.js 에서 비동기를 제어하기 위한 방법 중 Async 모듈을 다뤄본다.
비동기 제어는 비동기 프로그래밍을 하다보면 접할 수 있는 문제점들이 있기에 필요하다.
예를 들어, ajax, db query, request 등 비동기로 처리되다보니 콜백지옥을 맛보기도 한다.
위의 말이 이해가 가지 않더라도,
아래 예시를 통해 조금 더 자세히 다룰테니 이번 글의 요지를 알 수 있으니 끝까지 읽어보길 바란다.
다음의 코드를 보자.
app.get('/user/:userId', function(req, res) { var locals = {}; var userId = req.params.userId; db.get('users', userId, function(err, user) { locals.user = { name: user.name, email: user.email, bio: user.bio }; });
db.query('posts', {userId: userId}, function(err, posts) { locals.posts = posts; }); res.render('user-profile', locals); });
userId를 통해 2번의 DB 접근 후 페이지를 렌더하는 흐름의 코드이다.
순수하게 본다면, 위 코드의 실행 완료 순서는 get -> query -> render 라고 생각할 수 있다.
하지만 비동기 프로그래밍을 다뤄봤다면, 위와 같은 순서가 아니라는 것을 알 수 있다.
그렇기에 원하는 결과가 나오지 않을 수 있다.
사실상 위 코드로는 순서는 매번 다르기 때문에 아무도 모른다.
그래서 이를 해결하기 위해서 아래와 같이 코드를 작성한다.
실제로 많이 사용되는 패턴이지만, 비효율적인 패턴이라고 불리는 안티패턴(anti-pattern)이다.
app.get('/user/:userId', function(req, res, next) { var locals = {}; var userId = req.params.userId; var callbackCounter = 0; db.get('users', userId, function(err, user) { locals.user = { name: user.name, email: user.email, bio: user.bio }; callbackCounter++; if (callbackCounter == 2) { res.render('user-profile', locals); } }); db.query('posts', {userId: userId}, function(err, posts) { locals.posts = posts; callbackCounter++; if (callbackCounter == 2) { res.render('user-profile', locals); } }); });
count를 통해 체크한 후 원하는 완료 시점에 페이지를 render 하는 코드이다.
보다시피 굉장히 비효율적으로 보인다.
만약 db 접근 추가할 경우 그만큼 중첩된 코드가 추가되어야하고, 에러에 대해서도 문제가 많다.
if (callbackCounter == 3) { res.render('user-profile', locals); } if (callbackCounter == 4) { res.render('user-profile', locals); } ........
이러한 문제점들을 async 모듈을 통해 해결할 수 있다.
async 모듈에는 크게 3가지 메소드를 활용할 수 있다. (parallel, series, forEach)
3가지 모두 비동기 처리를 위한 역할은 같다.
역할은 같지만, 필요한 경우에 따라 사용하기 위해 제공되는 것이라 생각하면 된다.
간단한 용어로 표현하자면, 아래와 같다.
- parallel - 병렬 처리
- series - 직렬 처리
- forEach - 반복문
자세한 건 예시를 통해 다뤄보겠다.
app.get('/user/:userId', function(req, res, next) { var locals = {}; var userId = req.params.userId; async.parallel([ //Load user function(callback) { db.get('users', userId, function(err, user) { if (err) return callback(err); locals.user = { name: user.name, email: user.email, bio: user.bio }; callback(); }); }, //Load posts function(callback) { db.query('posts', {userId: userId}, function(err, posts) { if (err) return callback(err); locals.posts = posts; callback(); }); } ], function(err) { // Load user, Load posts 완료된 시점 if (err) return next(err); res.render('user-profile', locals); }); });
async.parallel을 보면 인자가 2가지가 있다.
첫번째 인자인 배열에 호출할 비동기 처리들을 넣는다.
(객체 형태도 가능하다. 조금 뒤에 다른 목적으로 다루겠다.)
두번째 인자에는 배열 안에 있는 함수들이 완료가 되면 실행되는 함수이다.
그 결과, 원하는 비동기 처리들이 끝나면 render를 할 수 있게 되었다.
parallel는 해석 그대로 병렬이다.
배열 안의 함수들이 병렬로 처리된다.
그렇기에 배열 안의 함수들의 최종 완료 시점은 알 수 있지만, 각 함수의 완료 시점은 알 수 없다.
예를 들어, 우리는 Load user를 완료한 후 Load posts가 실행되도록 하고 싶다고 가정한다.
parallel을 통해서는 이것이 가능하지 않다는 것이다.
이것은 async.series 를 활용할 수 있다.
app.get('/user/:userId', function(req, res, next) { var locals = {}; var userId = req.params.userId; async.series([ //Load user function(callback) { db.get('users', userId, function(err, user) { if (err) return callback(err); locals.user = { name: user.name, email: user.email, bio: user.bio }; callback(); }); }, //Load user 완료 후 Load posts 실행됨. function(callback) { db.query('posts', {userId: userId}, function(err, posts) { if (err) return callback(err); locals.posts = posts; callback(); }); } ], function(err) { // Load user, Load posts 완료된 시점 if (err) return next(err); res.render('user-profile', locals); }); });
원래 코드는 count를 통해 render할 시점을 체크했다.
async 모듈을 활용하여 코드의 간결성과 유지보수 등 많은 측면에서 효율적이게 되었다.
async.forEach 는 말 그대로 반복문처럼 활용하기에 최적화되어있다.
app.delete('/messages/:messageIds', function(req, res, next) { var messageIds = req.params.messageIds.split(','); async.forEach(messageIds, function(messageId, callback) { db.delete('messages', messageId, callback); }, function(err) { if (err) return next(err); res.json({ success: true, message: messageIds.length+' message(s) was deleted.' }); }); });
위와 같이 반복하여 비동기 처리를 할 경우에 forEach를 쓰면 효율적이다.
한가지 더 유용한 팁이 있다.
async 를 사용하는 가장 큰 이유는 비동기 처리이다.
하지만 코드를 짜다보면, 각 함수들의 반환값을 이용할 경우가 존재한다.
예를 들어, Load user 함수의 반환값과 Load posts의 반환값을 얻고 싶다는 것이다.
이 경우는 callback의 인자를 통해 넘기면 가능하다.
app.get('/user/:userId', function(req, res, next) { var locals = {}; var userId = req.params.userId; async.parallel([ function(callback) { db.get('users', userId, function(err, user) { ......... callback(null, user); }); }, function(callback) { db.query('posts', {userId: userId}, function(err, posts) { .......... callback(null, posts); }); } ], function(err, results) { ......... // 배열 형태로 반환. results[0] => Load user, results[1] => Load posts console.log(results); res.render('user-profile', locals); }); });
조금 더 명확한 코드와 유지보수를 위해서는 배열 대신 객체 형태로 표현하면 훨씬 좋다.
app.get('/user/:userId', function(req, res, next) { var locals = {}; var userId = req.params.userId; async.parallel({ user: function(callback) { db.get('users', userId, function(err, user) { ......... callback(null, user); }); }, post: function(callback) { db.query('posts', {userId: userId}, function(err, posts) { .......... callback(null, posts); }); } }, function(err, results) { ......... // 배열 형태로 반환. results['user'] => Load user, results['post'] => Load posts console.log(results); res.render('user-profile', locals); }); });
마지막 팁?으로 ES6를 통해 표현하면 훨씬 간결한 코드를 볼 수 있다.
async.parallel([ callback => db.save('xxx', 'a', callback), callback => db.save('xxx', 'b', callback) ], err => { if (err) throw err console.log('Both a and b are saved now') })
결과적으로 본인의 목적에 따라 많은 메소드를 제공하고 있으니 선택하여 사용하면 된다.
아래 링크를 참고하여 작성한 글로, 자세한 건 아래 링크를 참고하길 바란다.
참고 URL
http://www.sebastianseilund.com/nodejs-async-in-practice
반응형'Nodejs' 카테고리의 다른 글
Node.js vs Java 구조적 차이 :: 마이구미 (9) 2017.04.30 Ready check failed 오류 해결 [Redis] :: 마이구미 (2) 2017.03.23 라우팅 모듈화를 통한 MVC 패턴 ::마이구미 (0) 2017.01.21 [nodejs] redis session 저장소 관리 :: 마이구미 (0) 2017.01.08 pm2를 활용한 클러스터링 :: 마이구미 (0) 2016.07.26