JavaScript는 왜 싱글 스레드인가

JavaScript는 왜 싱글 스레드인가

이 글은 JS를 공부하는 도중에 자바스크립트는 싱글 스레드인데 어떻게 비동기 처리를 하는가에 대해 단순 궁금증이 생겨 쓰는 글입니다.

1. 스레드

프로세스 내에서 실행되는 흐름의 단위를 나타내며, 프로세스는 스레드를 여러 개 생성해 여러 작업을 동시에 처리할 수 있습니다. 스레드는 부모 프로세스의 자원을 공유하여 영향을 받으며, 같은 주소의 메모리에 접근이 가능해 데이터 공유가 가능합니다.
스레드는 프로세스 내에서 각각 Stack만 따로 할당받고 코드, 데이터, 힙 영역은 공유합니다. 같은 프로세스 안에 있는 여러 스레드들은 같은 힙 공간을 공유하지만, 반면에 프로세스는 다른 프로세스의 메모리에 직접 접근할 수 없습니다. 스레드는 싱글 스레드멀티 스레드로 나뉩니다. 싱글 스레드는 직렬을 형태로 하나의 스레드가 하나의 작업만 수행하여 순서대로 처리하는 모습을 볼 수 있으며, 멀티 스레드는 병렬로 일을 처리하며 여러가지 스레드를 한꺼번에 처리합니다. 조금 더 자세하게 알아보겠습니다.

2. 싱글 스레드와 멀티 스레드

2-1. 싱글 스레드

코드가 순차적으로 실행되며, 하나의 작업이 끝나야 다음 작업이 실행됩니다. - 동기적 처리
여러 스레드가 서로 경쟁하거나 동기화 문제가 발생하지 않아 단순하고 예측 가능한 동작입니다.
구현이 간단하고, 병렬 처리에 한정적이라는 특징이 있습니다.
JavaScript의 주요 실행 환경인 웹 브라우저에서, 페이지의 사용자 인터페이스와 관련된 작업들을 싱글 스레드로 처리합니다. 이로 인해 웹 애플리케이션의 동작이 예측 가능하고 빠르게 느껴질 수 있습니다.

2-2. 멀티 스레드

여러 스레드가 동시에 실행될 수 있어서, 여러 작업을 동시에 처리할 수 있습니다.
여러 스레드 간의 데이터 공유나 동시 접근을 관리하기 위해 복잡한 동기화가 필요할 수 있습니다.
각 스레드가 독립적으로 실행될 수 있어, 작업 완료를 기다리지 않고 다음 작업을 시작할 수 있습니다. - 비동기적 처리
대규모 데이터베이스 시스템이나 병렬 처리가 중요한 서버 애플리케이션에서는 멀티 스레드를 사용하여 여러 요청을 동시에 처리하거나, 병렬로 데이터베이스 작업을 처리할 수 있습니다.
동기 처리와 비동기 처리\footnotesize\fcolorbox{black}{#6D6D6D}{\color{#F9F9F9}\bf{동기 처리와 비동기 처리}} 동기 처리는 작업이 순차적으로 진행되며, 각 작업은 이전 작업의 완료를 기다립니다. JavaScript에서는 일반적으로 함수 호출이 동기적으로 처리됩니다. 비동기 처리는 작업을 순차적으로 기다리지 않고 별도의 스레드나 이벤트 루프를 통해 작업을 백그라운드에서 처리하고, 작업 완료 시 콜백 함수를 호출하여 결과를 처리합니다. 이는 JavaScript에서 네트워크 요청, 파일 읽기 등의 작업에 주로 사용됩니다.

2-3. 싱글 스레드와 멀티 스레드의 차이점

싱글 스레드는 한 번에 하나의 작업만 처리할 수 있지만, 코드 실행이 간단하고 예측 가능하며 동기화 문제가 발생하지 않습니다. 반면 멀티 스레드는 병렬로 여러 작업을 처리할 수 있지만, 동기화 문제를 해결하기 위한 추가적인 관리가 필요하고, 코드 실행 순서를 예측하기 어렵습니다.

3. 싱글 스레드를 사용하는데 왜 비동기 작업처럼 보이는가

JavaScript는 싱글 스레드 언어이기 때문에, 이는 한 번에 하나의 작업만 실행할 수 있다는 의미입니다. 하지만, 비동기 처리를 통해 여러 작업이 이루어지는 것처럼 보이게 할 수 있습니다. 이게 어떻게 가능한 일일까요? 이를 가능하게 하는 것이 바로 이벤트 루프콜백 큐, 웹 API입니다. 이것들이 각각 무슨 역할을 하는지는 JavaScript의 작동 원리를 보면 알 수 있습니다.

4. JavaScript의 작동 원리

자바스크립트는 싱글 스레드 런타임을 가집니다. 따라서 하나의 자바스크립트 런타임은 스택 하나, 큐 하나를 가지고 스택이나 큐에 저장되어 있는 일들을 한 번에 하나씩만 처리할 수 있습니다. 여기서 중요한 규칙은 메시지 큐의 작업은 항상 콜스택이 비었을 때 실행된다는 점입니다. 이 말은, 어떤 함수를 실행했을 때 큐에 이미 다른 메시지가 있을 수도 있고 새로 실행한 함수가 큐에 새로운 메시지를 보낼 수도 있지만 어쨌든 그 함수의 실행을 모두 마치고 난 후에야 큐에 저장되어 있는 일들을 시작합니다. 그 부분에서 또 새로운 스택이 쌓입니다. JavaScript의 작동 원리를 구체적으로 뜯어 볼게요.

4-1. Call Stack (호출 스택)

호출 스택은 여러 함수들을 호출하는 스크립트에서 해당 위치를 추적하는 인터프리터를 위한 메커니즘입니다. 호출 스택은 현재 어떤 함수가 동작하고 있는지, 그 함수 내에서 어떤 함수가 동작하는지, 다음에 어떤 함수가 호출되어야 하는지 등을 제어하는 역할을 합니다. 함수를 호출하면 호출 스택에 추가하고, 함수가 종료되면 호출 스택에서 제거됩니다. 이런 식으로 JS는 호출 스택을 사용해 함수를 실행하고, 순차적으로 코드를 처리합니다. 만약 인터프리터가 비동기 함수를 만나면 어떻게 될까요?

4-2. 비동기 함수와 Web API

인터프리터가 비동기 함수를 만나면 즉시 호출 스택에서 지워버리고, 이 비동기 함수는 Web API로 넘어갑니다. Web API는 브라우저가 제공하는 기능들이며, 타이머(setTimeout), HTTP 요청(fetch), DOM 이벤트 등이 포함됩니다. 비동기 작업을 처리한 후 작업이 완료되면, 콜백 함수를 콜백 큐로 보내는 역할을 합니다.
콜백  (Callback Queue)\footnotesize\fcolorbox{black}{#6D6D6D}{\color{#F9F9F9}\bf{콜백 큐 (Callback Queue)}} - 비동기 작업이 완료된 후 호출되어야 할 콜백 함수들이 대기하는 곳 - 콜백 큐에 있는 함수들은 호출 스택이 비어 있을 때 event loop에 의해 호출 스택으로 이동함

4-3. Event Loop (이벤트 루프)

event loop는 JS 런타임 모델의 기반이 되는 기능이며, 비동기 작업을 지원하기 위해 사용합니다. 코드를 실행하고, 이벤트를 모으고, 처리하고, 큐에 저장된 내용을 실행합니다. JS의 비동기 작업은 큐에 추가되며, event loop는 이 큐에서 작업을 하나씩 꺼내어 실행합니다.
\footnotesize\fcolorbox{black}{#6D6D6D}{\color{#F9F9F9}\bf{큐}} 먼저 들어온 작업이 처리되는 (FIFO, First In First Out) 구조
JS는 main을 실행한 후 event loop를 실행하는데, 이 말은 코드의 메인 흐름이 끝나고 반환이 되어야 event loop가 돌아가는 겁니다. main 코드가 끝나고 나면 event loop가 통제권을 넘겨받고 큐의 내용들을 실행합니다. JS의 비동기 작업은 두 가지 주요 카테고리로 나눌 수 있습니다. -매크로태스크와 마이크로태스크
매크로태스크 큐와 마이크로태스크 \footnotesize\fcolorbox{black}{#6D6D6D}{\color{#F9F9F9}\bf{매크로태스크 큐와 마이크로태스크 큐}} 매크로태스크 큐(Macro Task Queue) - 매크로 태스크는 일반적인 비동기 작업입니다. - 여기에는 setTimeout, setInterval 등 Timer에 의한 콜백 함수, I/O작업, 이벤트 핸들러 등이 포함됩니다. - 매크로태크스는 큐에 쌓이며, Event Loop가 한 번 순회할 때마다 하나씩 처리됩니다. 마이크로태스크 큐(Micro Task Queue) - 마이크로태스크는 매크로태스크보다 우선적으로 처리되는 작업입니다. - 여기에는 Promise.then, MutationObserver 등이 포함됩니다. - 매크로태스크가 완료된 후, 다음 매크로태스크를 실행하기 전에 마이크로태스크 큐에 있는 모든 작업이 처리됩니다.

4-4. 한눈에 보는 예제

console.log('Start'); // 1. 호출 스택에 추가되어 실행 후 제거됨 setTimeout(() => { console.log('Callback'); // 4. 1초 뒤 Web API가 콜백 함수를 콜백 큐로 보냈기 때문에 대기 중인 함수를 호출 스택으로 이동시켜 실행 }, 1000); // 2. setTimeout을 호출, 1초 후 실행할 콜백 함수를 Web API에 넘긴 후 삭제 console.log('End'); // 3. 호출 스택에 추가되어 실행 후 제거됨
JavaScript
복사

5. JavaScript는 왜 싱글 스레드인가: 결론

JavaScript는 주로 웹 브라우저 환경에서 동작하도록 설계되었습니다. 웹 브라우저는 사용자가 상호작용하는 인터페이스를 제공합니다. 이러한 상호작용의 일관성을 유지하려면 한 번에 하나의 작업만 처리하는 것이 중요합니다. 만약 여러 스레드가 동시에 DOM을 조작하게 된다면, 충돌이 발생할 수 있습니다. 이는 사용자 경험을 크게 저하시키는 일로 이어집니다.
또한, 싱글 스레드 구조는 코드의 예측 가능성과 디버깅의 용이성을 높입니다. 여러 스레드를 사용하는 경우 동시성 문제나 경쟁 조건과 같은 복잡한 버그가 발생할 수 있습니다. 싱글 스레드 환경에서는 이러한 문제를 피할 수 있어 코드의 안정성과 유지보수성이 높아집니다.
JavaScript는 비동기 처리 메커니즘을 통해 이러한 제약을 극복합니다. 앞서 말했던 호출 스택을 사용하여 순차적으로 코드를 실행하고, 비동기 함수는 Web API를 통해 별도의 작업을 처리하며, 이후 이벤트 루프가 이 비동기 작업의 결과를 마이크로태스크 큐나 태스크 큐로 가져와 다시 호출 스택으로 전달합니다. 이를 통해 JavaScript는 싱글 스레드 환경에서도 비동기 작업을 효율적으로 처리할 수 있습니다
결론적으로, JavaScript가 싱글 스레드를 선택한 이유는 웹 브라우저 환경에서의 안정성과 일관성을 유지하고, 코드의 예측 가능성과 디버깅의 용이성을 높이기 위함입니다. 또한, 비동기 처리 메커니즘을 통해 이러한 한계를 극복해 효율적 동작을 가능하게 합니다.

참고 자료