이전 글에서 간단하게 I/O 블로킹과 동기, 비동기 개념에 대해 알아 봤습니다. 저번 포스팅을 보고 온 사람은 "비동기와 논블로킹 I/O을 위해서는 당연히 멀티 스레드를 사용해야 하는 것 아니야?" 라는 의문이 들 것입니다.
하지만 자바스크립트는 싱글 스레드 논블로킹 I/O 기반 언어입니다. 오늘은 왜 자바스크립트는 싱글 스레드로 동작하고, 어떻게 논블로킹 I/O를 만족하는지 알아 보겠습니다.
멀티스레드 프로그래밍의 문제
동시성이란?
동시성이란 여러 작업이 동시에 실행되는 것 같이 보이는 것을 말합니다. (동시성 vs 병렬성 참고) 동시성 덕분에 우리가 웹 브라우저에 유튜브를 보면서, 댓글을 달거나 추천 영상을 찾아볼 수 있는 것이죠.
멀티스레드 프로그래밍의 동시성 문제
동시성 방식은 여러 스레드가 각 작업을 맡아 처리하는 방식입니다. 하지만 스레드는 데이터를 공유하기 때문에, 여러 작업이 한 데이터를 조회하는 경우 "동시성 문제"가 생기게 됩니다.
예를 들어, 6000만원이 있는 A의 계좌를 관리하는 프로그램을이 있고, 2개의 스레드가 작업을 수행한다고 가정하겠습니다.
- 1번 스레드: A의 계좌에서 5000만원을 인출하는 명령을 수행
- 2번 스레드: B의 계좌에 3000만원을 송금하는 명령을 수행
그런데 만약 2개 작업이 순차적으로 처리되지 않는다면 어떻게 될까요? 한 데이터(A 계좌)에 동시에 접근해, 최종 잔액으로 -2000만원이 되는 오류가 발생했습니다.
- 인출 작업: A 계좌에 6000만원이 있음을 확인하고, 계좌에서 5000만원을 인출하기 시작합니다.
- 송금 작업: A 계좌에 6000만원이 있음을 확인하고, B의 계좌로 3000만원을 송금합니다.
- 인출 완료: 인출 후 A의 계좌에서 5000만원이 차감됩니다. (잔액: 1000만원)
- 송금 완료: 송금 후 A의 계좌에서 추가로 3000만원이 차감됩니다. (잔액: -2000만원)
이러한 문제를 해결하기 위해 운영체제가 Mutex, Semaphore 등의 동기화 메커니즘을 제공합니다. 예를 들면, 작업 중에는 A의 계좌에 접근하지 못하도록 '잠금'을 설정하는 것입니다.
- 인출 작업: A의 계좌에서 5000만원을 인출하는 중입니다. (계좌 접근 '불가능')
- 송금 작업: 인출 작업 중이므로 B의 계좌로의 송금 작업을 대기합니다.
- 인출 완료: 인출 후 A의 계좌에서 5000만원이 차감됩니다. (잔액: 1000만원, 계좌 접근 '가능')
- 송금 실패: A 계좌에 1000만원이 있음을 확인하고, 잔액 부족으로 인해 중지됩니다.
그래서 멀티스레드 프로그래밍은 매우 어렵습니다. 프로그래머가 이 '잠금' 처리를 직접 처리해야 하며, 이런 매커니즘은 코드가 아니라 운영체제에 의해 관리됩니다. (물론, 프로그래머가 직접 Semaphore를 쓰진 않습니다)
왜 자바스크립트는 싱글 스레드인가?
자바스크립트는 동적 웹페이지를 위한 보조 언어로 기획되었습니다. 그런데 멀티스레딩 언어로 만들자니 '동시성 문제'에 대비해야 합니다.
- 계좌 관리 프로그램은 기껏해야 작업 몇 개(송금, 인출 등) 담당하지만, 웹페이지는 수없이 많은 작업(메일, 검색, 블로그, 유튜브..) 등을 다 구현할 수 있습니다. (특히, 화면에 여러 스레드가 접근해 수정한다면...)
- 이를 다 구현한다고 쳐도, 자바스크립트 개발자가 일일히 모든 병렬 작업에 "잠금" 처리를 해 줘야 합니다.
즉, 보조 언어인 자바스크립트에게 멀티 스레드를 적용하기에는 "너무 무겁고, 어렵다"라는 문제가 있었습니다. 그러므로 싱글 스레드 기반으로 기획되었다고 추측해볼 수 있습니다.
자바스크립트와 이벤트 루프
"어? 그러면 한 개의 작업밖에 처리하지 못하는 건가요?"
앞서 자바스크립트는 싱글 스레드 기반 언어이고, 하나의 콜 스택을 가지며 수행된다고 말했습니다. 그렇다면 어떻게 동시에 작업을 수행하는 것 일까요? 사실 자바스크립트는 싱글 스레드지만, 실행 환경은 멀티 스레드입니다(?!) 하나의 콜 스택을 가지고 동작하지만, 실행 시간이 긴 작업(I/O 작업, 타이머) 등을 모두 WebAPI에 위임합니다.
이해를 돕기 위해 사진을 보며 개념을 설명 하겠습니다.
- Stack: 자바스크립트가 실행할 함수를 순차적으로 하나씩 처리합니다.
- WebAPIs: 브라우저가 I/O 작업이나 타이머 등의 작업을 대신 수행해주기 위한 API입니다.
- Callback Queue: WebAPIs의 작업이 완료 시 수행되는 Callback 함수를 저장합니다.
- Event Loop: 자바스크립트의 Stack과 Callback Queue를 검사하며, Stack이 비어 있다면 Callback을 옮깁니다.
즉, 자바스크립트는 멀티 스레드를 환경의 브라우저(혹은 Node)를 선택적으로 활용해, "싱글 스레드 기반 논블로킹 I/O"를 만족할 수 있는 것입니다.
- 사용자 입력으로 새로운 비동기 작업(setTimeout)이 Call Stack에 추가됩니다.
- JS는 이 작업을 바로 삭제하고, WebAPI에게 Callback과 함께 전달해 위임합니다.
(Callback* 비동기 함수 완료 후 수행할 별도의 함수) - 작업은 Web API가 대신 수행하다가, 완료 시 Callback 함수를 Callback Queue에 추가합니다.
- Event Loop는 Callback Queue와 Stack을 감시하고, Stack이 완전히 비면 Callback을 옮깁니다.
자바스크립트가 왜 싱글 스레드를 사용하는지, 어떻게 동시에 작업을 처리하는지를 알아 봤습니다. 자바스크립트 언어의 이벤트 루프 개념은 알고 있었는데, 왜 싱글 스레드를 사용하는지는 잘 몰랐던 것 같습니다. 이번 포스팅을 위해 공부해 봤는데, 정말 보면 볼수록 신기한 언어 같습니다.
다음 포스팅에서는, 자바스크립트 이벤트 루프를 보다 자세히 살펴볼 예정입니다.
'IT > Javascript, Typescript' 카테고리의 다른 글
Node.js의 동작 원리를 알아보자 (+ 이벤트 루프, libuv, 스레드 풀) (1) | 2024.01.03 |
---|