JavaScript

자바스크립트는 싱글 스레드인데 왜 비동기가 가능할까?

한땀코딩 2021. 3. 21. 18:31

동공 지진, 그 시작

자바스크립트를 이용해 본격적으로 웹 개발에 입문할 때, 주기적으로 세뇌(?)당하는 이야기가 있는데 바로 자바스크립트가 싱글 스레드 기반의 언어라는 것입니다. 취업 준비하면서도 종종 나오는 주제 중 하나로 언급되고, 관련한 문제도 봤던 기억이 있습니다만, 도대체 이게 왜 자꾸 강조가 되는지 마음속으로 받아들이지 못했던 것 같습니다. 무엇보다 싱글 스레드라면 굉장히 제한적일 거 같은데 가능한 것들 중에는 이런 질문들이 있었습니다.

  1. 비동기는 어떻게 가능한 것인가. 동시에 두 가지가 동작하려면 멀티 스레딩을 해야한다고 배웠는데!
  2. 그럼 node 기반의 서버는 무조건 하나의 스레드에서만 동작한다는 뜻일 텐데 느리지 않을까?

 

아쉽게도, 아니 정확히는 이 질문에 대해 이해를 시켜주기에는 넓은 영역에 대한 기반 지식과 긴 설명이 필요하기에 그 누구도 제게 '정답'을 알려주진 않았습니다 (그리고 동공 지진이 시작되었습니다). 사실 정답이라고 딱 명확하게 적힌 이야기는 없지만, 조목조목 뜯어볼 개념은 분명 존재합니다. 그래서 아직도 전문가 수준의 시원한 대답을 내리지는 못 하지만, 나름대로 아주 초반에 가졌던 이런 의문점을 정리해보면 좋을 거 같다는 생각이 들었습니다.

그래서 자바스크립트는 진짜 싱글스레드가 맞아?

네, 자바스크립트는 싱글 스레드 기반으로 동작하는 언어가 맞습니다. 그리고 비동기 동작 때문에 흔히 asynchronous 하다고 많이 얘기하지만, 자바스크립트 자체는 하나의 흐름을 주욱 타고 흘러가는 synchronous 한 언어입니다. '싱글 스레드'라는 표현이 함축하고 있는 것이 여러 가지가 있을 텐데 이와 관련된 자바스크립트의 특징을 간단히 꼽아보자면 이렇습니다.

  • 코드 실행 중 하나의 코드에서 멈추거나 걸리면 다음으로 진행할 수 없습니다. 예를 들어, alert등의 코드가 실행되면 경고창에서 확인을 누르기 전까진 UI 조작이 불가합니다. 혹은 그 이후 코드도 실행되지 않는 걸 알 수 있습니다. 또 다른 예시로는, 아래의 경우입니다.
setTimeout(() => {
  console.log("hello");
}, 0);

while (true) {
  console.log("world");
}

console.log("!");

이 코드에서 !는 물론이고, hello도 영원히 출력되지 않습니다. setTimeout같은 비동기 함수는 멀티스레드처럼 별도로 동작해서 출력될 거 같지만, 그러지 않습니다. 이에 대한 이야기는 밑에서 좀 더 자세하게 풀어보겠습니다.

  • 직접 관찰하기는 어려울 수 있지만, 자바스크립트의 특징에는 'run-to-completion'이라는 것이 있습니다. 하나의 task를 끝내기 전에 다른 task를 실행하지 않습니다. MDN의 이벤트 루프 페이지를 참고해보면 C와 비교해서 설명하고 있습니다.
Each message is processed completely before any other message is processed. This offers some nice properties when reasoning about your program, including the fact that whenever a function runs, it cannot be pre-empted and will run entirely before any other code runs (and can modify data the function manipulates). This differs from C, for instance, where if a function runs in a thread, it may be stopped at any point by the runtime system to run some other code in another thread.

정리해보면, 자바스크립트는 함수가 실행되면 해당 함수가 종료될 때까지 다른 코드를 실행할 수 없습니다. 반면, C는 함수를 하나의 스레드가 실행시키다 다른 스레드의 다른 코드를 동작시키기 위해 해당 함수 관련 처리를 멈출 수도 있다고 합니다.

그럼 어떻게 비동기 작업이 가능하지?

여기서 중요한 건 '자바스크립트' 이 자체로는 싱글 스레드가 맞다는 점입니다. 하지만 웹 개발을 하거나, 노드로 개발을 하는 과정에서 순수하게 자바스크립트만을 실행시키는 경우가 과연 많을까요? 특히 프론트엔드 입장이라면 자바스크립트 코드는 반드시 브라우저를 통해 실행되게 되어있습니다. 그리고 이 브라우저 때문에 싱글 스레드 언어인 자바스크립트가 우리 눈으로 보기엔 동시성을 갖는 작업을 해내는 것으로 보이게 됩니다.

이벤트 루프

위에서 얘기한 눈의 착시를 이해하기 위해선 브라우저의 '이벤트 루프'를 이해할 필요가 있습니다. 이와 관련해서는 굉장히 유명한 다음 영상이 있는데, 이벤트 루프에 대해 처음 들어보시거나 상세한 설명이 필요하다면 꼭 참고하시길 바랍니다.

이 포스팅이 이벤트 루프를 상세하게 설명하기 위한 포스팅은 아니기에, 엄청나게 상세한 디테일은 포함하지 않겠지만, 어쨌든 중요한 것은 자바스크립트의 코드는 이벤트 루프 기반으로 작동한다는 것입니다. 즉, 실행되는 함수를 싱글 스레드 형식으로 처리하는 메인 '콜 스택'이 있고, 이 콜 스택이 비었을 때마다 '콜백 큐'에서 대기 중인 함수를 콜 스택으로 불러와 실행시킵니다.

setTimeout이나 ajax 같은 비동기 함수가 호출되면, 해당 작업은 브라우저에 내장된 WebAPI에게 넘겨집니다. 그 안에서 타이머가 동작을 하거나 네트워크 통신을 진행하고, 코드가 실행될 준비가 되면 콜백 큐에 해당 작업을 넣어줍니다. 그래서 사실상 비동기 함수가 동기적인 함수들과 '동시'에 동작하는 것처럼 보이지만, 이는 아주 재빠르게 콜 스택에 넘어와 실행되었기 때문입니다. 즉, 모든 건 순차적으로 하나의 스레드(콜 스택) 안에서 돌아가고 우리는 그저 이 시간 차가 너무 작아서 동시에 돌아간다고 착각을 하는 것입니다.

비슷한 맥락으로 node는 Java 같은 멀티스레딩 언어에 비해 불리하지 않을까?

이 주제만으로도 또 엄청나게 많은 이야기가 파생될 수 있을 거 같아 길게는 다루지 않겠지만, node 코드를 실행할 때 별다른 처리 (클러스터링 같은 것이 있습니다)를 하지 않는다면 하나의 스레드에서만 돌아가는 것이 맞습니다. 그렇기에 요즘 같이 멀티코어가 제공되는 하드웨어에서 동작한다면, 여러 코어를 사용하여 최적화를 시키거나 로드 밸런싱을 적용하려면 worker thread나 클러스터링 작업이 필요하다고 합니다. 그리고 성능 면으로는 여러 글에서 Java와 비교를 하고 있는데, CPU intensive 한 작업은 Java가 node에 비해 빠르다고 합니다. 다만, 속도를 측정하는 기준은 역시나 다양해질 수 있고 프로그램의 종류에 따라서도 차이가 있어서 일반화하기는 어렵다고 합니다. 이와 관련해서는 아래에 레퍼런스로 url을 적어두겠습니다.

마치며

이런 주제는 마치 판도라의 상자 같습니다. 아예 접하질 않았다면 모르겠지만, 어설프게 알고 있어서 계속 모르는 걸 따라서 꼬리에 꼬리를 물고 찾아가다 보면 너무 방대한 범위에 대한 이해가 필요하지 않나 싶습니다. 성능을 계산하고 최적화에 대해 고민하는 단계까진 아직 저도 진행해보진 않았지만, 적어도 개발 공부 초반에 가졌던 의문에 대해서는 이제 해결의 시작점을 찾아낼 수 있는 정도는 되지 않았나 싶습니다. 저와 비슷한 고민을 가지고 계신 분들이 있다면 이걸 보면서 약간이나마 해소가 되셨으면 좋겠습니다.

레퍼런스

Multithreading in Java vs Node.JS

Single threaded Node.js on a multi core CPU vs Java (Tomcat)