3. 콜백을 사용한 비동기 제어 흐름 패턴

3. 콜백을 사용한 비동기 제어 흐름 패턴

  • 비동기 프로그래밍의 어려움

  • 일반 JavaScript 사용

  • Async 라이브러리

깃헙주소

https://github.com/PacktPublishing/Node.js_Design_Patterns_Second_Edition_Code.git

Node.js는 비동기, 연속전달방식을 일반적으로 사용하는 플랫폼. 일반적인 문제, 실수중 하나 콜백지옥 -> 가독성 결여, 실수 발생 제어 흐름 라이브러리를 사용하여 콜백패턴을 단순화하고 깔끔하게 만들기 목표

3.1 비동기 프로그래밍의 어려움

클로저와 인플레이스정의(in place definitions)의 활용이 관건 KISS(Keep in simple, stupid) 간단함을 유지해 멍청아ㅋㅋ 클로저 -> 비동기보다는 규칙문제가 더중요한 경우

3.1.1 간단한 웹스파이더 만들기

간단 예제 npm 라이브러리 request, mkdirp 사용

// 깃헙 : https://github.com/PacktPublishing/Node.js_Design_Patterns_Second_Edition_Code/blob/master/Chapter03/01_web_spider/package.json
{
  "name": "crawler",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "mkdirp": "^0.5.1",
    "request": "^2.67.0",
    "slug": "^0.9.1"
  }
}
// index.js
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] 콜백 : 파일 없는 경우 -> 다운로드
        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);
    }
  });
}

// 컨맨드 라인 3번째에서 URL 읽음
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`);
  }
});

// utitlities.js
"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;
};

콜백 지옥, 죽음의 피라미드 -> 가독성이 떨어짐

비동기 CPS(직접전달방식), 클로저 잘못 사용? -> 큰 재앙

클로저가 참조하는 컨텍스트가 가비지 수집시 유지된다는 사실... -> 잘못사용하면 메모리 누수

3.2 일반 JavaScript의 사용

비동기 코드 작성시 또다른 주의 사항

  • 특정 패턴과 기법을 사용해야하는 상황? -> 일반 JS를 사용할 때 특히

    음... ㅇㅋ 일단 패스

    여기서는 일반 JS를 사용하여 일반적인 제어흐름 패턴 구현

3.2.1 콜백 규치

콜백을 정의할때, 함부로 클로저를 사용하지 않는 것 콜백 지옥 문제 해결 -> 간단한 일반 상식이면 충분.

중첩 수준을 낮게 유지하는 팁

  • 가능한 빨리 종료. if문 수 줄이기. ex return, break, continue 문 활용

  • 콜백을 위해 명명된 함수(익명말고)를 생성하여, 클로저 바깥에 배치하며, 중간 결과를 인자로 전달. 명명함수 -> 스택추적에서 잘보임

  • 코드 모듈화. 코드를 작고 재사용 가능하게 함수 구현

3.2.2 콜백 규칙 적용

  1. if~else문 제거 -> 오류 검사 패턴 재구성 (상태패턴에서 활용 되기도 함)

    if(err){
     callback(err);
    } else {
     // 정상 : working
    }
    
    // 변경 후
    if(err){
     return callback(err); // return을 통해 즉시 종료.
    }
    // 정상 : working
    // spider에서 다음 부분을 함수로 빼서 모듈화
    function saveFile(filename, contents, callback){
     mkdirp(path.dirname(filename), err => {
       if(err) {
         return callback(err);
       }
       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);
       });
     });
    }
    
    // index.js spider 부분 (리팩토링)
    function spider(url, callback) {
     const filename = utilities.urlToFilename(url);
     fs.exists(filename, exists => {        //[1] 콜백 : 파일 생성여부 판단.
       if (exists) {
         return callback(null, filename, false);
       }
    
       download(url, filename, err => {
         if (err) {
           return callback(err);
         }
         callback(null, filename, true);
       });
     });
    }

3.2.3 순차실행

순서가 중요한 경우 순서에 따라 IN->OUT -> IN->OUT 으로 결과값이 입력값으로 가는 파이프형태 순차실행 적용 : 직접방식의 블로킹 API에서는 간단, 비동기 CRS 사용하여 구현 -> 콜백지옥의 원인

  • 잘알려진 순차실행

// 책임사슬 패턴과 유사,
// 비동기 코드를 처리하는데 꼭 클로저가 필요 없을음 보여줌..
function asyncOperation(callback) {
  process.nextTick(callback);
}

function task1(callback) {
  asyncOperation(() => {
    task2(callback);
  });
}
function task2(callback) {
  asyncOperation(() => {
    task3(callback);
  });
}

function task3(callback) {
  asyncOperation(() => {
    callback();
  });
}

task1(() => {
  console.log('task 1,2 and 3 executed');
});
  • 순차반복

    비동기적으로 배열을 반복 실행하게끔 구현되어 있음.

    ```javascript

    // index.js

    "use strict";

const request = require('request'); const fs = require('fs'); const mkdirp = require('mkdirp'); const path = require('path'); const utilities = require('./utilities3');

function spiderLinks(currentUrl, body, nesting, callback) { if(nesting === 0) { return process.nextTick(callback); }

let links = utilities.getPageLinks(currentUrl, body); //[1] 내부 링크 다가져옴

function iterate(index) { //[2] 반복 패턴, 페이지 링크 수와 인덱스가 같아질때까지 반복 if(index === links.length) { return callback(); }

spider(links[index], nesting - 1, function(err) { //[3] : 중첩레벨을 줄여 반복 실행
  if(err) {
    return callback(err);
  }
  iterate(index + 1); // 반복적인 재귀 실행 구조
});

} iterate(0); //[4] 최초 0부터 시작, 재귀시작 }

function saveFile(filename, contents, callback) { mkdirp(path.dirname(filename), err => { if(err) { return callback(err); } 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, 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);

}); }

spider(process.argv[2], 1, (err) => { if(err) { console.log(err); process.exit(); } else { console.log('Download complete'); } });

// utilities.js "use strict";

const urlParse = require('url').parse; const urlResolve = require('url').resolve; const slug = require('slug'); const path = require('path'); const cheerio = require('cheerio');

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); }) .join('/'); let filename = path.join(parsedUrl.hostname, urlPath); if(!path.extname(filename).match(/htm/)) { filename += '.html'; } return filename; };

module.exports.getLinkUrl = function getLinkUrl(currentUrl, element) { const link = urlResolve(currentUrl, element.attribs.href || ""); const parsedLink = urlParse(link); const currentParsedUrl = urlParse(currentUrl); if(parsedLink.hostname !== currentParsedUrl.hostname || !parsedLink.pathname) { return null; } return link; };

module.exports.getPageLinks = function getPageLinks(currentUrl, body) { return [].slice.call(cheerio.load(body)('a')) .map(function(element) { return module.exports.getLinkUrl(currentUrl, element); }) .filter(function(element) { return !!element; }); };

- iterate 일반화
`reduce`연산 알고리즘의 기반베이스

```javascript
function iterate(index){
  if(index === tasks.length){
    return finish();
  }

  const task = tasks[index];
  task(function(){
    iterate(index + 1);
  });
}

function finish(){
  // 반복 작업이 완료된 후 처리 
}

iterate(0);

3.2.4 병렬실행

단일스레드 인데 병렬 가능? -> 논블로킹 때문에 그렇게 보이는 것 -> 논블로킹 위에서 실행되고 있기 때문 가능 -> 정확히는 논블로킹 API위에서 실행되고, 이벤트 루프에 의해 인터리브 된다는 것을 의미

동기(블로킹)작업 -> 비동기로 처리, setTimeout(), setImmediate()로 지연 시키지 않는 한 동시에 실행 안됨 (9장에서 다룸)

  • 웹스파이더 3버전 병렬성을 띄어 성능을 향상 시킴

    "use strict";
    
    const request = require('request');
    const fs = require('fs');
    const mkdirp = require('mkdirp');
    const path = require('path');
    const utilities = require('./utilities3');
    
    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);  // 실행하면서 카운트를 증가시켜서 확인 어케보면 done에 의해 completed는 클로저
      });
    }
    
    function saveFile(filename, contents, callback) {
      mkdirp(path.dirname(filename), err => {
        if(err) {
          return callback(err);
        }
        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);
        });
      });
    }
    
    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) {
        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);
      });
    }
    
    spider(process.argv[2], 2, (err) => {
      if(err) {
        console.log(err);
        process.exit();
      } else {
        console.log('Download complete');
      }
    });
  • 패턴 일반화

    ```javascript

    const tasks = [];

    let completed = 0;

    tasks.forEach(task => {

    if(++completed === tasks.length){

    finish();

    }

    })

function finish() { // work }

경쟁조건... 이 부분은 특이해서 학습이 더 필요해보임  

### 3.2.5 제한된 병렬 실행
너무 많은 요청에 의한 병렬실행은 리소스 부족현상을 발생시킴.  
따라서, 동시실행 작업수를 제한하는 것이 좋음.  

```javascript

// 큐
module.exports = 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++;
    }
  }
};

// index.js
const request = require('request');
const fs = require('fs');
const mkdirp = require('mkdirp');
const path = require('path');
const utilities = require('./utilities5');
const TaskQueue = require('./taskQueue');   // 큐사용
let downloadQueue = new TaskQueue(2);       // 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 => {        // 이 부분 Task 등록
      spider(link, nesting - 1, err => {
        if(err) {
          hasErrors = true;
          return callback(err);
        }
        if(++completed === links.length && !hasErrors) {
          callback();
        }
        done();
      });
    });
  });
}

function saveFile(filename, contents, callback) {
  mkdirp(path.dirname(filename), err => {
    if(err) {
      return callback(err);
    }
    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);
    });
  });
}

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) {
    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);
  });
}

spider(process.argv[2], 1, (err) => {
  if(err) {
    console.log(err);
    process.exit();
  } else {
    console.log('Download complete');
  }
});

3.3 Async 라이브러리

Last updated