티스토리 뷰

학부시절동안 보통 C언어나 C++를 많이 다뤄왔고 알고리즘 공부 역시 C++로 해왔기에 나에겐 절차지향과 동기적 처리 방식이 너무 익숙했다.

이번에 Node를 공부하면서 자연스럽게 그동안 겉핥기로 배워왔던 자바스크립트도 다시 공부하게 되었고 그 중 자바스크립트의 동시성 모델에 대해 공부했던 내용을 정리하려고 한다.

C언어 같은 경우 프로그램 순서에 따라 함수들이 스택에 차곡차곡 쌓이니까 프로그램의 흐름을 예측하는 데 큰 어려움이 없다. 그러나 자바스크립트는 setTimeout과 같은 비동기적인 방식을 지원하기에 일반적인 코드 순서와 실행 결과는 다르게 나타날 수 있다.

그렇다면 자바스크립트는 비동기 작업을 지원하기 위해 멀티스레드를 사용할까 ?

답은 X다. 자바스크립트의 큰 특징 중 하나는 '단일 스레드' 기반의 언어라는 점이다.

그리고 '단일 스레드'라는 말의 의미는 동시에 하나의 작업만을 처리할 수 있다는 말이다. 그렇다면 어떻게 단일 스레드로 비동기 작업을 처리할 수 있을까?

이러한 현상을 이해하기 위해서는 자바스크립트의 실행모델인 이벤트루프, 콜스택, 콜백 큐 등의 개념을 이해해야 한다.


앞서 설명했지만 자바스크립트는 '단일 스레드' 기반의 언어로서 그 자체로 비동기적인 동작을 지원하는 것이 아니다. 자바스크립트가 비동기적으로 동작할 수 있게 하는 핵심요소는 자바스크립트 언어 자체가 아닌 브라우저에 있다. (혹은 Node의 libuv 라이브러리 등일 수 있다.)

그 구조를 그림으로 나타내면 다음과 같다

  • Heap : 메모리 할당이 발생하는 곳이다. C에서의 heap영역과 같은 의미로 생각하면 될 것같다.
  • Call Stack : 자바스크립트는 단일호출스택과 Run-to-completion을 사용한다. 이는 단 하나의 호출스택을 사용하며 함수의 실행이 끝날 때 까지 다른 작업이 중간에 끼어들 수 없음을 의미한다. 즉, 현재 스택의 맨 위에 쌓여있는 함수가 종료되어야 그 다음 함수가 실행될 수 있다. 이 때 함수들이 이 Call Stack에 차곡차곡 쌓이게 된다.
  • Web API : DOM,setTimeout등 브라우저가 제공하는 API를 의미한다. 자바스크립트는 이러한 web API의 백그라운드 작업을 통해 비동기적 동작을 지원할 수 있게 되는 것이다.
  • Callback Queue (Task Queue) : Web API 함수가 Call Stack에 들어와 실행되면 해당 함수는 Call Stack에서 pop되고 Web API에 의해 백그라운드(워커 스레드)에서 작업이 진행된다. 이 때, 이 작업에 사용되는 콜백함수들에 대한 정보를 가진 자료구조는 Event Table에 저장된다.
    그리고 워커스레드의 작업이 끝나게 되면 해당 콜백함수를 콜백큐에 push하게 된다.
    즉 콜백 큐는 콜백함수들이 대기하는 대기열이라고 생각하면 된다.
  • Event Loop : 이벤트 루프는 콜 스택이 비어있는지 확인하여 만약 호출스택이 비어있다면 콜백큐로부터 콜백함수를 꺼내와 실행하는 역할을 한다.

따라서 위 구조를 생각하며 아래 코드가 어떻게 동작할지 예측하며 결과를 이해해 보자.

console.log('1')

setTimeout(() => {
  console.log('2')
}, 1000)

console.log('3')

너무나 당연하지만 결과는 1 → 3 → 2 순으로 출력 될 것이다.

그렇다면 아래 코드의 결과는 어떠할까?

console.log('1')

setTimeout(() => {
  console.log('2')
}, 0)

console.log('3')

setTimeout을 0ms로 설정해놨으므로 결과는 1 → 2 → 3 으로 예측할 수 있다.

하지만 결과는 위 결과와 마찬가지로 1 → 3 → 2 순으로 출력된다.

그 이유는 위에서 설명한 Web API와 콜백큐, 이벤트 루프 개념 때문이다.

위 코드가 실행되는 상황을 순서대로 재현하면 아래와 같다.

  1. console.log('1') 함수가 Call Stack에 올라갔다가, 실행되고 Pop된다.
  2. setTimeout함수가 Call Stack에 올라갔다가, 실행되고 Pop된다. 이때 setTimeout에 관한 webAPI가 백그라운드(워커 스레드)에서 실행된다. 이 때의 정보는 이벤트테이블에 있다.
  3. console.log('3') 함수가 Call Stack에 올라갔다가, 실행되고 Pop된다.
  4. 이벤트 루프에 의해 콜 스택이 비어있으므로 콜백큐로부터 setTimeout에 대한 콜백함수를 실행하게 된다. 이때 console.log('2')가 Call Stack에 올라와 실행되고 사라진다.

위 순서로 1 → 3 → 2 의 결과가 출력되는 것이다.

따라서 이 개념을 이해했다면, 메인 스레드에서 처리해야 하는 양이 많을 경우 setTimeout이 정확한 시간을 보장할 수 없다는 이유에 대해서도 이해할 수 있을 것이다.

그렇다면 이번엔 Blocking에 대해 알아보자. 아래 코드의 결과는 어떠할까?

setInterval(() => {
  console.log('hello')
  while (true) {}
}, 1000)

setInterval 함수는 특정 주기마다 인자로 전해진 콜백함수를 실행해주는 함수이다.

1초마다 'hello'가 출력이 될까? 답은 X다.

그 이유는 '호출 스택은 하나만 존재한다'는 점에 있다.

  1. setInterval이 콜스택에 올라가 실행되고 pop된다.
  2. 백그라운드에서 setInterval관련 작업이 진행된다.
  3. 작업이 완료된 후 해당 콜백함수를 콜백큐에 push한다
  4. 이벤트 루프에 의해 콜백큐에 있는 콜백함수를 실행한다. 이 때 hello가 한번 출력되고 그 후 아래에 있는 while문이 반복되게 된다. 이 콜백함수가 스택에 올라와 있는 상태에서 마무리가 되지 않아 호출스택이 비어있지 않게 된다. 따라서 계속하여 콜백큐에 콜백함수만 쌓이는 현상이 발생되고 실제로는 'hello'가 딱 한번만 출력될 것이다.

이렇게 while loop가 도는동안 call stack이 절대로 비지 않아 콜백큐에 아무리 많은 콜백이 쌓여도 이벤트루프가 해당 콜백을 실행할 수 없는 경우를 이벤트 루프를 block한다고 한다.

따라서 비동기적인 방식을 지원하기 위해서는 non-blocking IO를 지원해야한다. 메인스레드가 하나이기에 Node는 IO와 같은 작업들은 Node API를 통해 비동기적으로 처리하게 된다. 예를들어, 파일을 읽는 경우

  1. 먼저 노드에게 파일 read를 요청하고
  2. 노드는 워커스레드에서 파일을 읽기 시작한다.
  3. 이 때 메인스레드는 다음 자바스크립트 코드를 실행하게 된다.
  4. 노드가 파일을 다 읽으면 콜백함수가 콜백큐에 push된다.
  5. 이벤트루프에 의해 콜백함수가 실행된다.

위와 같은 상황으로 인해 Node 서버의 메인스레드가 단 하나임에도 빠르게 동작할 수 있게 된다. 메인스레드는 오래 걸리는 일을 기다리지 않기 때문이다. 이를 offloading 이라고도 한다.

이 외에도 Promise, 이벤트 루프와 같은 내용이 있지만 이는 나중에 더 다뤄보도록 하겠다.

노드를 공부하고, 자바스크립트 코드를 작성하는데 있어서 이러한 비동기적 처리가 어떻게 이루어지는지 아는 것은 매우 중요하다고 생각된다. 사실 아직도 완벽히 이해했다고 말하기는 어렵지만, 코드를 작성해보고 오류도 경험해보면서 계속해서 이해 해봐야 겠다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/10   »
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
글 보관함