ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 자바스크립트, 동기 vs 비동기 (Synchronous vs Asynchronous)
    Frontend 2021. 8. 8. 22:55

     

    싱글 스레드(Single Thread) 언어

    자바스크립트는 싱글 스레드 언어라고 불린다. 여기서 스레드(Thread)는 프로세스 내에서 실행되는 흐름을 최소 단위를 말한다. 그리고 스레드는 자신만의 프로그램 카운터와 시스템 레지스터, 그리고 스택을 가진다.

     

    프로그램 카운터는 다음에 실행될 명령어를 계속 추적하는 역할을 하고, 레지스터는 CPU가 요청을 처리하는 데 필요한 데이터를 일시적으로 가지고 있는 기억 장치이며 스택은 함수 호출 시에 전달되는 인자나 실행 종료 후 되돌아갈 주소 값, 함수 내의 변수 등을 저장하는 메모리 공간이다.

     

     

     

    스레드는 프로세스에 할당된 메모리나 자원들을 공유하며, 각각의 스레드가 독립적으로 작업을 수행하기 위해 각자의 레지스터, 스택을 가진다. 스레드는 라이트웨이트 프로세스라고도 불린다. 병행 처리로 어플리케이션의 성능을 향상시킬 수 있기 때문이다.

     

    이제 자바스크립트 언어에 대해서 생각해보자. 자바스크립트 엔진은 하나의 스레드를 사용하는 싱글 스레드 방식인데, 즉 그 말은 하나의 레지스터, 카운터, 스택을 가지며 한 번에 하나의 작업만 수행한다는 뜻이다.

     

    자바스크립트에서 어떤 함수를 호출하면 함수에 전달되는 인자, 함수 내에서 사용하는 변수, 그리고 함수의 실행이 종료되고 되돌아갈 주소 값 등을 실행 컨텍스트라는 객체로 생성한 후에 이 것을 콜 스택에 push한다. 스택에 push되면 바로 함수가 실행되며, 실행이 종료되고 나면 실행 컨텍스트는 다시 pop되어 제거된다.

     

     

     

    동기 (Synchronous)

    하나의 콜 스택을 가지는 자바스크립트에서 어떤 작업에 1분이라는 시간이 소요된다면 다음 작업은 1분을 기다려야만 실행될 수 있다. 이렇게 앞선 작업의 소요 시간으로 인해 다음 함수의 실행이 중단되는 것을 Blocking이라고 한다. 

     

    function delay(num) {
      let i = 0;
      while(i < num) {
        i++;
      }
      console.log('delay executed');
    }
    
    function foo() {
      console.log('1:  ', new Date());
    }
    
    function bar() {
      console.log('2:  ', new Date());
    }
    
    foo();
    delay(10000000000);
    bar();

    delay 함수로 인해 bar 함수는 약 8~9초의 시간이 흐른 후에 실행되었다. 이렇게 현재 실행되고 있는 작업이 종료될 때까지 다음 실행할 작업을 대기하는 것을 동기 처리라고 한다. 동기 방식의 장점은 모든 작업을 순차적으로 진행하기 때문에 작업 순서가 보장된다는 점이다. 대신 앞의 작업이 진행되는 동안 다음 작업들이 지연된다는 단점이 있다.

     

     

    비동기 (Asynchronous)

    자바스크립트가 모든 작업을 동기 방식으로만 처리한다면 생각만 해도 답답해진다. 우리가 웹사이트에서 새로운 작업을 할 때마다 화면이 블로킹될 것이고 유저는 답답해하면서 페이지를 이탈해버릴 것이다. 다행히 자바스크립트에는 비동기 처리 방식이 존재한다. 비동기 방식은 쉽게 말해서 어떤 식당에서 앞의 사람 음식이 나오지 않았더라도 일단 다음 사람의 주문을 계속 받는 방식을 말한다. 주문을 계속 받은 후 먼저 준비된 사람의 음식부터 서빙하는 것이다. 즉, 앞선 작업이 종료되지 않아도 기다리지 않고 다음 작업을 실행하는 것을 말한다.

     

    function delay(ms) {
      console.log('delay start');
    
      setTimeout(() => {
        console.log('delay executed');
      }, ms);
    }

    아까 전의 delay 함수를 setTimeout으로 변경해보자. 코드는 foo 함수를 실행한 후에 delay를 실행한다. 그리고 delay의 실행이 종료되는 것을 기다리지 않고 바로 bar 함수를 실행한다. bar의 실행이 끝난 후에 콜스택이 비워지면 delay가 실행되고 난 이 후 5초가 흐른 시점에 setTimeout의 콜백 함수가 실행된다.

     

    이렇게 비동기 함수는 앞선 작업이 종료되지 않아도 바로 다음 작업을 실행하여 Blocking을 해결한다는 장점이 있지만 작업의 실행 속도가 보장되지 않는다는 단점이 있다.

     

    우리가 흔히 사용하는 setTimeout, setInterval과 같은 타이머 함수, HTTP 요청, 이벤트 핸들러는 브라우저에서 제공하는 Web API인데 모두 비동기 방식으로 처리된다.

     

     

    이벤트 루프 (Event Loop)

     

    대부분의 자바스크립트 엔진은 크게 콜 스택과 힙 영역으로 나뉜다.

     

    • 콜 스택(Call Stack)은 앞에서 설명했듯이 전역이나 함수 코드 등을 평가하면서 생성한 실행 컨택스트 객체를 push하거나 pop할 수 있는 자료 구조이다.
    • 힙(Heap)은 객체가 저장되는 메모리 공간으로 콜 스택에 저장되는 실행 컨택스트는 힙에 저장된 객체를 참조한다. 객체는 원시 값과는 다르게 런 타임 단계에서 동적으로 메모리 공간의 크기가 결정되는데, 이 때문에 힙은 구조화되어 있지 않다는 특징이 있다.

     

    자바스크립트 엔진은 이처럼 단순히 함수가 실행되면 순차적으로 콜 스택에 추가와 제거를 반복하면서 정해진 순서대로 작업을 진행한다. 그러다가 비동기 작업을 만나면 비동기 작업의 스케쥴링은 브라우저나 Node.js에 위임하는데, 브라우저에서는 비동기 작업의 처리를 위해 이벤트 루프와 태스트 큐를 제공한다.

     

    • 태스트 큐(Task Queue)는 비동기 함수의 콜백 함수나 이벤트 핸들러가 일시적으로 보관되는 자료 구조이다. 프로미스의 후속 처리 메소드의 콜백 함수는 태스트 큐가 아니라 마이크로 태스트 큐(Microtask Queue)에 저장되는데 참고로 마이크로 태스트 큐가 우선 순위가 높다. 따라서 setTimeout과 promise 중에서 promise의 콜백 함수가 더 먼저 실행된다.
    • 이벤트 루프(Event Loop)는 비동기 함수의 스케쥴링을 담당한다. 일단 콜 스택에 현재 실행 중인 실행 컨택스트가 있는지, 그리고 태스트 큐나 마이크로 태스트 큐에 대기 중인 함수가 있는지 반복해서 확인한다. 그 후에 콜 스택이 비어있으면서 태스트 큐에 대기 중인 함수가 있다면 선입 선출로 태스트 큐의 작업을 콜 스택으로 이동시켜서 실행시킨다.

     

    (이벤트 루프에 대해서는 예전에 작성한 글, 자바스크립트, 이벤트 루프(Event Loop)와 동시성(concurrency)에 대하여라는 글에 조금 더 자세하게 정리해두었다.)

     

    NOTE: 자바스크립트가 싱글 스레드 언어라고 했는데, 이 때 싱글 스레드 방식으로 동작하는 것은 브라우저가 아니라 브라우저에 내장된 자바스크립트 엔진만을 말한다는 것을 기억해두자. 자바스크립트 엔진은 싱글 스레드로 동작하지만 브라우저는 멀티 스레드로 동작한다. setTimeout, setInterval과 같은 타이머 함수, HTTP 요청, 이벤트 핸들러 모두 브라우저에서 제공하는 Web API라는 것을 기억하자.

     

     

    반응형

    COMMENT