JS - 비동기 (Asynchronous)

sarang_daddy·2023년 2월 10일
0

Javascript

목록 보기
15/26

🌱 학습 내용

동기 (Synchronous)

자바스크리브처럼 싱글 스레드인 경우 실행 컨텍스트에 함수가 스택과 같이 싸이고
상위 함수가 종료되어야 다음 함수가 실행이 된다. 여기서 하위 함수는 블로킹(작업중단) 처리된다.
이처럼 현재 실행 중인 태스크가 종료할 때까지 다음 태스크가 대기하는 방식을 동기처리라 한다.

: A가 끝나면 B가 시작한다. (A가 끝난 시간과 B가 시작하는 시간을 맞춘다.)
: 제어권반환과 결과값이 함께 일어난다.


비동기 (Asynchronous)

동기와는 다르게 상위 함수가 종료되지 않았음에도 하위 함수가 실행되는 방식을 비동기 처리라 한다.

function foo() {
  console.log("foo");
}

function bar() {
  console.log("bar");
}

setTimeout(foo, 3000);
bar();

// bar
// foo (3초 후 반환)

위 예제 처럼 상위함수 foo()가 종료되기 전에 bar()함수값이 먼저 반환되는 경우를 비동기 처리라 한다.

: A의 끝과 상관없이 B가 실행 될 수 있다.
: 결과값이 없는데 제어권이 반환 될 수 있다.

즉, 여러 일이 동시에 발생할 수 있다.
한 함수의 실행 흐름을 막지(block)않고, 걔속 실행한다.
그리고 그 함수가 끝났을 때, 프로그램은 실행결과에 접근한다.

비동기 함수는 전통적으로 콜백 패턴을 사용하여 만든다.
그리고 타이머 함수인 setTimeout과 senInterval, HTTP요청, 이벤트 핸들러는 비동기 처리 방식으로 동작한다.

이벤트 루프와 태스크 큐

앞서 설명한대로 자바스크립트는 싱글 스레드로 한번에 하나의 태스크만 처리한다.
하지만 브라우저를 살펴보면 태스크가 동시에 처리되는 것처럼 느껴진다.
이는 자바스크립트의 동시성을 지원하는 이벤트 루프와 관계가 있다.

  • 이벤트 루프는 브라우저에 내장되어있는 기능 중 하나다.
  • JS엔진은 콜 스택을 통해 순차적으로 진행할 뿐이다.
  • 비동기 처리에서 소스코드의 평가와 실행을 제외한 모든 처리는 JS엔진을 구동하는 브라우저 또는 NodeJS가 담당한다.

즉, 비동기로 동작하는 setTimeout의 콜백 함수의 평가와 실행은 JS엔진이 담당하지만
호출 스케줄링을 위한 타이머 설정과 콜백 함수의 등록은 부라우저(NodeJS)가 담당한다.
이를 위해 부라우저 환경은 태스크 큐와 이벤트 루프를 제공한다.

태스크 큐

비동기 함수의 콜백 함수 또는 이벤트 핸들러가 일시적으로 보괸되는 영역.

이벤트 루프

콜 스택에 현재 실행 중인 태스크가 있는지 그리고 태스크 큐에 대기중인 함수가 있는지 계속 확인한다.
만약 콜 스택이 비어 있고 태스크 큐에 대기 중인 함수가 있다면 이벤트 루프는 순차적으로
태스크 큐에 대기 중인 함수를 콜 스택으로 이동시킨다. 이때 콜스택에서 실행된다.
즉, 태크스 큐에 일시 보관된 함수들은 비동기 처리 방식으로 동작한다.

JS엔진, 태스크큐, 이벤트 루프 움직임 확인하기

비동기 함수인 콜백함수는 태스크 큐에 푸시되어 대기하다가 콜 스택이 비게되면,
즉, 전역 코드 및 명시적으로 호출된 함수가 모두 종료되면(이것은 이벤트 루프가 확인한다.)
콜 스택에 푸시되어 실행된다.

특정 스레드에서 이벤트 루프를 만들어서 각 이벤트를 전달하는 방식이란?

By chatGPT :
이것은 특정 스레드에서 이벤트 루프를 만들어서 각 이벤트를 처리하는 것을 의미합니다.
이벤트 루프는 어플리케이션에서 일어나는 사용자 이벤트, 네트워크 이벤트 등을 기다리고 처리하는 루프입니다.
예를 들어, GUI 기반의 어플리케이션에서는 사용자가 어플리케이션에서 일어난 이벤트 (예: 버튼 클릭)을
처리하는 데 사용될 수 있습니다.
이벤트 루프는 계속해서 이벤트를 기다리고 있으며, 새로운 이벤트가 발생할 때마다 적절한 처리를 수행합니다.
이벤트 루프는 다양한 프로그래밍 언어와 프레임워크에서 지원되며,
비동기 프로그래밍과 다중 스레드 프로그래밍에서 많이 사용됩니다.


동기 vs 비동기

동기는 순서대로 작업이 진행되고 비동기는 동시다발적으로 진행된다고 할 수 있는데,
비동기적으로 진행되면 대기없이 빠른 작업 처리가 가능하기에 당연히 좋다고 볼 수 있지만,
동시에 자원을 사용한다면, 자원의 일관성 즉 원치 않은 자원의 망가짐이 발생 할 수 있다.
때문에 개발자는 비동기적으로 프로그래밍을 최대한 구현하되
이로 인한 문제들이 발생 하지 않도록 "동기화"를 시켜줘야 한다.


🔥 구현 해보기

구현 목표

  • 주문 담당자(Cashier)는 음료 주문을 연속해서 받을 수 있다.
  • 음료 주문을 받으면 주문 대기표(Queue)에 추가한다.
  • 주문 대기표도 이벤트를 받아서 처리하는 별도 모듈/객체로 분리해서 구현한다.
  • 매니저(Manager)는 음료를 확인하기 위해서 주문 대기표를 1초마다 확인한다.
  • 주문이 있을 경우 작업이 비어있는 (제작할 수 있는) 바리스타에게 작업 이벤트를 전달한다.
  • 바리스타가 보낸 특정 고객의 음료 제작 완료 이벤트를 받으면 결과를 출력한다.
  • 바리스타(Barista)는 동시에 2개까지 음료를 만들 수 있다고 가정한다.
  • 스레드를 직접 생성하는 게 아니라 이벤트 방식으로 동작해야 한다.
  • 바리스타는 음료를 만들기 시작할 때와 끝날 때 마다 이벤트를 발생한다.
  • 이벤트가 발생할 때마다 음료 작업에 대한 로그를 출력한다.
  • 주문 담당자, 매니저, 바리스타가 한 명(인스턴스가 한 개)만 있다고 가정한다

구현 설계

구현 내용

  1. Drink, Cashier, ListQueue, Barista, Manager를 클래스로 구성하여
    각자의 상태와 역할을 분배하고 협력할 수 있도록 한다.

  2. Drink 클래스

// 주분 받은 음료의 상태를 가진다.
class Drink {
  constructor(number) {
    this.number = number;
    this.runTime = 0;
    this.name = this.getName();
    this.endTime = this.getEndTime();
    this.state = false;
  }

  getName() {
    if (this.number === 1) return "아메리카노";
    else if (this.number === 2) return "카페라떼";
    else if (this.number === 3) return "프라푸치노";
  }

  getEndTime() {
    if (this.number === 1) return 3;
    else if (this.number === 2) return 5;
    else if (this.number === 3) return 10;
  }

  changeState() {
    if (this.runTime === this.endTime) return true;
    else if (this.runTime !== this.endTime) return false;
  }
}
  1. Cashier 클래스
// 주문을 받으면 큐 리스트에 전달한다.
class Cashier {
  constructor(inputStr) {
    this.inputStr = inputStr;
    [this.number, this.quantity] = this.inputStr.split(":");
    this.orderList = this.getOrder();
  }

  getOrder() {
    const orderList = [];
    for (let i = 1; i <= this.quantity; i++) {
      orderList.push(this.number);
    }
    return orderList;
  }
}
  1. ListQueue 클래스
// 주문 받은 정보를 큐 스택에 순차적으로 채워준다.
class listQueue {
  constructor() {
    this.queue = [];
  }

  initOrder(str) {
    const newOrder = new Cashier(str).orderList;
    newOrder.map((v) => this.queue.push(v));
    return this.queue;
  }

  showOrderList() {
    return this.queue;
  }
}
  1. Barista 클래스
// 제작중인 스택을 가지고 커피를 만든다.
// 제작 시작과 종료시 이벤트(?) 호출한다.
class Barista {
  constructor() {
    this.callStack = [];
    this.finishStack = [];
  }

  pushDrink(number) {
    if (this.callStack.length < 2) {
      this.callStack.push(new Drink(number));
      return this.callStack;
    } else if (this.callStack.length >= 2) {
      return;
    }
  }

  makeDrink() {
    for (let i = 0; i < this.callStack.length; i++) {
      if (this.callStack[i].runTime === 0) {
        console.log(`${this.callStack[i].name} 시작`);
      }
      this.callStack[i].runTime++;
      if (this.callStack[i].runTime === this.callStack[i].endTime) {
        console.log(`${this.callStack[i].name} 완성`);
        const finishDrink = this.callStack.splice(i, 1);
        this.finishStack.push(finishDrink[0].name);
      }
    }
  }
}
  1. Manager 클래스
// 전체적인 관리자다
// 이벤트 루프를 돌며 리스트큐와 바리스타 스택을 1초마다 체크한다.
// 주문 리스트가 존재하고 바리스타 스택이 비어있으면 제작 지시를 한다.
class Manager {
  constructor(listQueue) {
    this.listQueue = listQueue;
    this.barista = new Barista();
    this.callStack = this.barista.callStack;
    this.finishStack = this.barista.finishStack;
  }

  eventLoop() {
    let i = 1;
    const timer = setInterval(() => {
      this.runtime(i);
      console.log(this.listQueue);
      this.checkQueueList();
      this.commandMake();
      i++;

      if (this.listQueue.length === 0 && this.callStack.length === 0) {
        console.log(`\n모든 메뉴가 완성되었습니다.\n`);
        clearInterval(timer);
      }
    }, 1000);
  }

  checkQueueList() {
    if (this.listQueue.length > 0 && this.callStack.length < 2) {
      const makeDrink = this.listQueue.shift();
      this.barista.pushDrink(Number(makeDrink));
    } else if (this.listQueue.length === 0) {
      return;
    }
  }

  commandMake() {
    this.barista.makeDrink();
  }

  runtime(i) {
    console.log(`\x1b[30m\n-----------------${i}초-------------------\x1b[0m`);
  }

  init() {
    this.eventLoop();
  }
}

const queue = new listQueue();
queue.initOrder("1:2");
queue.initOrder("3:2");
queue.initOrder("2:2");

const start = new Manager(queue.showOrderList());
start.init();

출력 내용

🛠️ 보완점

  • Node.js - Event Emitter를 학습해서 Input값도 비동기적으로 계속 입력이 가능하도록 구현해보자.
  • 바리스타가 2명인 경우도 구현해보자.

🙏 참고자료

모던 자바스크립트 Deep Dive

profile
한 발자국, 한 걸음 느리더라도 하루하루 발전하는 삶을 살자.

0개의 댓글