참고문헌 : NodeJS Design Pattern Second Edition
동기식 프로그래밍 스타일을 사용하다가 Node.js와 같은 비동기 api일반적으로 사용되는 플랫폼에 적응하는 것은 쉽지 않다.
일반적인 실수 하나가 call back hell이다. 간단한 동작도 가독성이 떨어지게 하고, 관리하기 힘들게 한다.
아래와 같은 방법을 통해 비동기 코드를 얼마나 쉽고 간단하게 만드는지 알아본다.
- 규칙과 패턴을 적용
- Async같은 제어흐름라이브러리를 활용
01. 비동기 프로그래밍의 어려움
01-1. 간단한 웹 스파이더
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | "use strict"; const request = require('request'); const fs = require('fs'); const mkdirp = require('mkdirp'); const path = require('path'); const utilities = require('./utilities'); function spider(url, callback) { const filename = utilities.urlToFilename(url); fs.exists(filename, exists => { //[1] 해당파일이 생성되었는지 확인 if(!exists) { console.log(`Downloading ${url}`); request(url, (err, response, body) => { //[2] 파일이 없다면 url 다운로드 if(err) { callback(err); } else { mkdirp(path.dirname(filename), err => { //[3] 파일을 저장할 디렉터리 확인 if(err) { callback(err); } else { fs.writeFile(filename, body, err => { //[4] 응답을 파일에 쓰기 if(err) { callback(err); } else { callback(null, filename, true); } }); } }); } }); } else { callback(null, filename, false); } }); } spider(process.argv[2], (err, filename, downloaded) => { if(err) { console.log(err); } else if(downloaded){ console.log(`Completed the download of "${filename}"`); } else { console.log(`"${filename}" was already downloaded`); } }); | cs |
./utilities.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | "use strict"; const urlParse = require('url').parse; const slug = require('slug'); const path = require('path'); module.exports.urlToFilename = function urlToFilename(url) { const parsedUrl = urlParse(url); const urlPath = parsedUrl.path.split('/') .filter(function(component) { return component !== ''; }) .map(function(component) { return slug(component, { remove: null }); }) .join('/'); let filename = path.join(parsedUrl.hostname, urlPath); if(!path.extname(filename).match(/htm/)) { filename += '.html'; } return filename; }; | cs |
- request : http호출
- mkdirp : 재귀적으로 디렉터리를 만드는 단순 유틸
1 2 3 | $ > node .\index.js http://google.com Downloading http://google.com Completed the download of "google.com.html" | cs |
01-2. 콜백헬
- 각 스코프에서 사용된 변수이름 중복
- 가독성 떨어짐
- 디버깅시 추적 불가
02. 일반 Javscript 사용
02-1. 콜백규칙
- 가능한 빨리 종료. return, continue, break 등을 사용하여 현재문을 즉시 종료
- 콜백을 위해 명명된 함수 생성하여 클로저 바깥에 배치. 스택추적시 더 잘 보이게된다.
- 코드 모듈화. 가능한 작게
02-2. 적용
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | function saveFile(filename, contents, callback) { // 일반 함수로 클로저 바깥에 배치 mkdirp(path.dirname(filename), err => { if(err) { return callback(err); // return 으로 나머지 함수의 실행을 차단 } fs.writeFile(filename, contents, callback); }); } function download(url, filename, callback) { console.log(`Downloading ${url}`); request(url, (err, response, body) => { if(err) { return callback(err); } saveFile(filename, body, err => { if(err) { return callback(err); } console.log(`Downloaded and saved: ${url}`); callback(null, body); }); }); } function spider(url, callback) { const filename = utilities.urlToFilename(url); fs.exists(filename, exists => { if(exists) { return callback(null, filename, false); } download(url, filename, err => { if(err) { return callback(err); } callback(null, filename, true); }) }); } | cs |
02-3. 순차 실행
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | "use strict"; function asyncOperation(callback) { process.nextTick(callback); } function task1(callback) { asyncOperation(() => { task2(callback); }); } function task2(callback) { asyncOperation(() => { task3(callback); }); } function task3(callback) { asyncOperation(() => { callback(); //finally executes the callback }); } task1(() => { //executed when task1, task2 and task3 are completed console.log('tasks 1, 2 and 3 executed'); }); | cs |
위와 같이 순차 실행을 코딩 할 수 있다. 만약 순차 실행을 반복하려면?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | function spider(url, nesting, callback) { const filename = utilities.urlToFilename(url); fs.readFile(filename, 'utf8', function(err, body) { if(err) { if(err.code !== 'ENOENT') { return callback(err); } return download(url, filename, function(err, body) { if(err) { return callback(err); } spiderLinks(url, body, nesting, callback); }); } spiderLinks(url, body, nesting, callback); }); } | cs |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | function spiderLinks(currentUrl, body, nesting, callback) { if(nesting === 0) { return process.nextTick(callback); } let links = utilities.getPageLinks(currentUrl, body); // 페이지에 포함된 모든 링크 목록 가져옴 function iterate(index) { if(index === links.length) { return callback(); } spider(links[index], nesting - 1, function(err) { if(err) { return callback(err); } iterate(index + 1); }); } iterate(0); } | cs |
위처럼 비동기 작업을 사용하면서 컬렉션을 반복하는 작업을 패턴화 할 수 있다.
여러가지 상황에 적용가능하다.
순차 반복자 패턴 : iterator라는 함수 작성. 작업의 목록을 차례로 실행. iterator는 다음에 사용가능한 태스크를 호출하고, 태스크가 완료될때 반복의 다음 단계를 호출 하도록 한다.
02-4. 병렬 실행
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | function spiderLinks(currentUrl, body, nesting, callback) { if(nesting === 0) { return process.nextTick(callback); } const links = utilities.getPageLinks(currentUrl, body); if(links.length === 0) { return process.nextTick(callback); } let completed = 0, hasErrors = false; function done(err) { if(err) { hasErrors = true; return callback(err); } if(++completed === links.length && !hasErrors) { return callback(); } } links.forEach(function(link) { // 병렬실행 spider(link, nesting - 1, done); }); } | cs |
무제한 병렬 실행 패턴 : 한번에 모든 항목을 생성하여 일련의 비동기작업을 병렬 실행한 다음, 콜백이 호출된 횟수를 계산하여 모든 작업이 완료되기를 기다린다.
* 동시작업에서 경쟁(race) 조건 조정
멀티스레드 환경에서 Non-blocking I/O를 사용하여 여러 작업을 병렬로 실행할 때 문제가 발생 할 수있다.
보통 잠금, 뮤텍스, 세마포어 같은 구조를 사용하여 수행되고 병렬화 성능에 상당한 영향을 미치며, 복잡한 측면도 있다.
그러나 Node.js에서는 단일 스레드에서 실행되기 때문에 복잡한 동기화 메커니즘이 필요치 않으며, 비동기작업을 병렬로 실행하는 것은 리소스 측면에서 직관적이며 비용도 적게 든다.(강점)
하지만 동기화 메커니즘을 생각하지 않는것은 경쟁조건을 가지지 않는 것을 뜻하며, 이는 일반적일 수 있으나 문제는 비동기 작업 호출과 그 결과 통지 사이에 생기는 지연이다.
만약 동일한 URL에 대해서 두개의 spider 작업을 수행한다면 같은 파일을 다운 받게 된다.
이를 제어하기 위해 상호 배제할 변수를 두면 해결할 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 | let spidering = new Map(); // 같은 작업을 막는 변수 function spider(url, nesting, callback) { if(spidering.has(url)) { return process.nextTick(callback); } spidering.set(url, true); const filename = utilities.urlToFilename(url); fs.readFile(filename, 'utf8', function(err, body) { ... }); } | cs |
경쟁상황은 단일 스레드 환경에서 많은 문제를 야기 할 수 있다. 특성상 디버그하기도 어렵다. 따라서 병렬실행시는 이러한 유형을 명확하게 확인하는 것이 필요하다.
02-5. 제한된 병렬 실행
1 2 3 4 5 6 7 | le concurrency = 2 fucntion next(){ while(running < concurraency && index < tasks.length){ ... running++; } } | cs |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | class TaskQueue { constructor (concurrency) { this.concurrency = concurrency; this.running = 0; this.queue = []; } pushTask (task) { this.queue.push(task); this.next(); } next() { while (this.running < this.concurrency && this.queue.length) { const task = this.queue.shift(); task (() => { this.running--; this.next(); }); this.running++; } } }; | cs |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | let downloadQueue = new TaskQueue(2); function spiderLinks(currentUrl, body, nesting, callback) { if(nesting === 0) { return process.nextTick(callback); } const links = utilities.getPageLinks(currentUrl, body); if(links.length === 0) { return process.nextTick(callback); } let completed = 0, hasErrors = false; links.forEach(link => { downloadQueue.pushTask(done => { spider(link, nesting - 1, err => { if(err) { hasErrors = true; return callback(err); } if(++completed === links.length && !hasErrors) { callback(); } done(); }); }); }); } | cs |
동시 실행 제어를 TaskQueue에 위임한다.
03. Async 라이브러리
(Node는 7.6 버전부터 async/await 를 별도의 도구없이도 지원하기 시작했다.)
- npm install async
- const async = require('async')
- 순차 실행 : async.series()
- 순차 반복 실행 : async.eachSeries()
- 병렬실행 : async.each()
- 제한된 병렬실행 : async.eachLimit()
'개발 이야기 > NodeJS' 카테고리의 다른 글
Nodejs 파일 읽어 Promise 다루는 방법 (0) | 2019.11.28 |
---|---|
초간단 Express + Mysql 환경 셋팅 (0) | 2019.05.14 |
NodeJS 동작 방식과 개념 정리 (0) | 2019.03.02 |
NodeJS 인기있는 Logging 모듈 Winston (0) | 2019.02.20 |
NodeJS validate로 요청 데이터 검사 (0) | 2019.02.11 |