주요 개념
- 싱글 쓰레드(Single Thread)
- 비동기(Asynchronous)
JavaScript는 싱글 쓰레드(Single Thread) 언어이다. 한 번에 하나의 작업만 수행이 가능하다는 의미로 받아들일 수 있다. 하지만 우리가 사용하는 웹 페이지에는 여러 컴포넌트들과 기능들이 동시다발적으로 수행되고 있다. 이는 JavaScript는 메인 쓰레드인 이벤트 루프가 싱글 쓰레드이기 때문에 이렇게 불리지만 결국 실행한 후(런타임)에는 NodeJS와 같은 멀티 쓰레드 환경에서 구동되므로 동시다발적으로 여러 동작이 가능하다.
기존의 동기식 요청은 인터프리터 언어이므로 위에서 차례대로 수행된다. 앞에서 연산이 오래 걸리는 작업을 수행하고 뒤에 금방 끝날 수 있는 연산을 계속 미루게 되면 리소스의 낭비가 있어 비효율적인 구조가 될 수 있다. JS에서 비동기 호출을 통해 이를 해결할 수 있다. 런타임 환경에서는 멀티 쓰레드환경이 있지만 한 스크립트 내에서는 어떠한 방식을 이용해 비동기 작업이 여러 요청을 처리하는지는 여전히 의문이 들 수 있다.
우선 아래의 그림을 보자.
JS가 실행될 때는 다음과 같은 요소들이 실행을 도와준다.
- Call Stack: 자바스크립트에서 수행해야 할 함수들을 순차적으로 스택에 담아 처리
- Web API: 웹 브라우저에서 제공하는 API로 AJAX나 Timeout 등의 비동기 작업을 실행
- Task Queue: Callback Queue라고도 하며 Web API에서 넘겨받은 Callback함수를 저장
- Event Loop: Call Stack이 비어있다면 Task Queue의 작업을 Call Stack으로 옮김
아래와 같이 비동기(Asynchronous) 처리가 되는 코드가 있다.
setTimeout(() => console.log('I'm an async'));
console.log('Hello World!');
// Hello World!
// I'm an async
이때 출력은 비동기 요청이 나중에 출력되게 된다. 우리가 알고 있는 인터프리터 특성을 생각해보면 위의 라인이 수행되어 I'm a async가 먼저 출력되어야 한다. 하지만 이 코드에선 위 라인이 끝날 때까지 기다리지 않고 다음 코드가 먼저 처리되었다. 이는 비동기 요청은 Call Stack에 먼저 담아지고 이후 Web API에 의해 Task Queue에 들어가게 된다. 이후 아래로 한 줄 더 내려와 console.log('Hello World!'); 라인이 Call Stack에 담겨 처리된 후 Event Loop가 Call Stack이 비어있다면 Task Queue에 담긴 비동기 요청을 Callback하여 Call Stack에 담아 처리한다.
JS가 싱글 쓰레드가 아니라면 동시성 제어(Concurrency Control)가 필요하다.
동시성 문제란 예를 들어 서울 성수동의 ATM과 부산 해운대의 ATM 두대에서 동시에 같은 계좌의 카드를 넣고 돈을 인출한다고 가정하자. 이때 계좌 잔액이라는 공유 변수가 존재한다면 갱신이 되기 이전에 인출을 하게 되면 잔액이 없음에도 불구하고 인출이 될 수도 있다. 이렇게 동시에(멀티 프로세싱 or 멀티 쓰레딩 환경) 공유 변수에 관한 요청을 처리 하기 위해서 일반적으로 임계 구역(Critical Section)에 Lock을 거는 방식인 Mutex(상호 배제) 또는 임계 구역 접근 제한을 위한 세마포어(Semaphore)등을 사용해주어야 한다. 하지만 싱글 쓰레드라면 이러한 교착상태(Dead Lock)나 경쟁 상태(Race Condition) 같은 문제를 회피하며 비동기 처리를 수행할 수 있다.
참고 자료
https://chanyeong.com/blog/post/44