[Node.js 디자인패턴] 3주차

김련호·2021년 5월 19일
0

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

  • node.js 자체의 특성(싱글스레드 & 이벤트 루프)로 인해서 비동기 코드 작성 시에 콜백을 사용하는 패턴이 기본적인 패턴이다.
  • 하지만 안티패턴인 콜백헬에 빠지기 쉽다.
  • 코드가 가로로 길어지고, 이로 인해 가독성이 나빠진다. 그리고 변수 이름의 중복이 발생한다.(err)
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.2 일반 Javascript 사용

Javascript 자체를 사용하여 비동기를 제어

3.2.1 일반 규칙

  • 기본적인 콜백 규칙1. 함부로 클로저를 사용하지 않는다.
  • 기본적인 콜백 규칙2. else 문장을 제거하여 중첩의 레벨을 줄인다.
// else 제거 전 - else 문 안에(들여쓰기+1) 로직이 들어간다.
if(err) {
  callback(err);
} else {
//  다음 로직 수행
}
  
  
// else 제거 - 오류 발생 시에 반드시 return 필요
if(err) {
  return callback(err);
}
// 다음 로직 수행

3.2.2 순차 실행
특정 복수의 로직들을 순차적으로 실행하는 패턴.
실행되어야 할 로직들이 고정(로직의 수, 로직의 내용)일 경우 사용가능하지만, 실전에서는 부족함

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

3.2.3 순차 반복 실행
동적인 개수의 작업을 순차적으로 반복 실행하는 패턴

function iterate(index) {
  if(index === tasks.length) {
    return finish();
  }
  const task = tasks[index];
  task(function() {
    iterate(index + 1); // 현재의 task가 종료되면 다음 task를 실행
  });
}

function finish() {
  console.log("all tasks are finished");
}

3.2.4 병렬 실행
주어진 작업들을 병렬로 실행하는 패턴.
node.js에서도 병렬 수행이 가능하지만, 멀티스레드 프로그래밍에서 사용하는 lock, mutex, semaphore, monitor 등을 사용하지 않는다.
하지만 병렬 작업 수행 시 아래와 같이 경쟁(race) 상황에 놓이는 것은 동일하다.

const tasks = [ /* tasks */ ];
let completed = 0;
tasks.forEach(task => { // 전체 tasks를 반복하며, task 수행(비동기)
  task(() => {
    if(++complieted === tasks.length) { // 각 task 종료 시 종료 조건을 체크하여 마지막에 종료된 task는 전체 작업을 종료
      finish();
    }
  });
});

function finish() {
  console.log("all tasks are finished");
}

위의 예에서 동시에 동일한 작업을 수행하는 경우를 커버하지 못하는데, 이는 전역적인 map, set 등으로 해결할 수 있다.

3.2.5 제한된 병렬 실행
3.2.4의 병렬 실행의 경우 동시에 실행 가능한 작업의 수는 무한대이다. 이것은 리소스에 대한 문제의 소지가 있기 때문에 제한이 필요하다.

const tasks = [ /* tasks */ ];
let concurrency = 2, running = 0, completed = 0, index = 0;

function next() {
  while(running < concurrency && index < tasks.length) { // 현재 실행 중인 task의 개수에 따라 실행 여부 결정
    task = tasks[index++];
    task(() => {
      if(completed === tasks.length) {
        return finish();
      }
      completed++, running--;
      next(); // task 작업 종료 후 다음 task 실행
    });
    running++;
  }
}
next();

function finish() {
  console.log("all tasks are finished");
}

3.2.6 제한된 병렬실행2 - Queue 사용
병렬 실행을 통제하기 위한 수단으로 Queue를 사용하는 패턴을 사용할 수 있다.
전체 작업을 일반화할 수 있고, 동적으로 작업이 상시 발생하는 경우에 가장 적합한 패턴이라고 할 수 있다.

module.exports = class TaskQueue {
  constructor (concurrency) {
    this.concurrency = concurrency;
    this.running = 0;
    this.queue = [];
  }

  pushTask (task) { // 동적으로 queue에 작업을 추가할 수 있다.
    this.queue.push(task);
    this.next();
  }

  next() {
    while (this.running < this.concurrency && this.queue.length) {
      const task = this.queue.shift(); // 대기 중인 첫 번째 task
      task (() => {
        this.running--;
        this.next();
      });
      this.running++;
    }
  }
};

3.3 Async 라이브러리

https://www.npmjs.com/package/async

3.3.1 순차 실행
async.serise(tasks, [callback]);
async.waterfall(tasks, [callback]);
async.eachSeries(tasks, [callback]);

3.3.2 병렬 실행
async.each(tasks, [callback]);

3.3.3 제한된 병렬실행
eachLimit()
mapLimit()
paralleLimit()
queue()
cargo()

profile
기본이지만 잘 모르고 있던 것들, 모호한 것들을 기록합니다.

0개의 댓글