자바스크립트 동시성 모델


자바스크립트는 이벤트 루프에 기반한 동시성(Concurrency) 모델이라는 것을 가지고 있습니다. 이 모델은 다른 C나 Java와 같은 언어가 실행되는 방식과 다릅니다.


본디 자바스크립트는 웹 브라우저 상에서 동작하는 스크립트 언어로 만들어졌습니다. 그리고 자바스크립트로 DOM API에 접근해서 브라우저의 UI를 제어하기 위해 UI스레드를 공유할 수 밖에 없는 환경입니다.


만약 자바스크립트가 일반적인 C나 Java언어 처럼 동작하고, 동시성을 제공하지 않는다면 다음과 같은 상황이 벌어질 수 있습니다.


서버에서 비동기적으로 데이터를 받아오기 위해 XHR을 통해 Ajax요청을 보내고, 해당 데이터를 받아오기 까지 시간이 소요됩니다. 그러면 해당 데이터를 받아오기 전 까지 UI를 제어하는 자바스크립트는 멈추게 되므로 사용자는 UI를 제어할 수 없습니다.


이러한 경우가 Ajax가 잦은 페이지에서는 클라이언트 리소스문제가 아니라도 브라우저가 매우 자주 버벅일 것이고 이는 사용자에게 매우 나쁜 UX를 제공합니다.


이러한 상황을 피하기 위해서 자바스크립트는 이벤트 기반 동시성 모델을 제공합니다. Ajax요청을 보내는 것과 응답을 받는 것, 각각을 다 이벤트로 처리하며 Ajax와 같이 I/O 요청 이후 결과를 계속 기다리는 것이 아니라 곧바로 다른 처리할 수 있는 작업을 처리하게 되며 UI 쓰레드가 멈추는 것을 막을 수 있습니다.

동시성과 병렬성

동시성(Concurrent)는 병렬성(Parallel)과는 다릅니다. 둘 다 여러개의 실행 흐름이 동시에 일어나는 것이라는 공통점이 있습니다.


동시성

동시성(Concurrent)는 실제 물리적으로 동시에 일어나는 것이 아니라, 흐름을 실행시키는 것은 하나(e.g. CPU 코어 혹은 쓰레드)지만 작은 타임 슬라이스(Time slice, Time quantum)단위로 다른 흐름을 돌아가면서 실행시켜서 동시에 일어나는 것 처럼 보이게 하는 방식입니다. 논리적인 의미에 동시 실행으로 볼 수 있습니다.


병렬성

병렬성(Parallel)은 실제로 흐름을 실행시키는 것(e.g. CPU 코어 혹은 쓰레드)이 복수개여서, 각각 실행 흐름을 할당받아 동시에 실행시킵니다. 물리적인 의미에 동시 실행입니다.




자바스크립트는 언어 스펙이 싱글쓰레드이므로, 병렬성(Parallel)을 지원할 수 없습니다. 대신 동시성(Concurrent)는 지원할 수 있습니다.


자바스크립트 런타임 모델

자바스크립트는 브라우저에서 더 좋은 UX를 제공하기 위해 동시성 모델을 제공한다. 하지만 이 동시성 모델은 자바스크립트 언어 자체의 스펙이 아니라, 자바스크립트를 구동하는 환경인 웹 브라우저의 자바스크립트 엔진에서 구현한 모델이다. 또한 범용 언어로서 자바스크립트 런타임인 Node.js도 비슷한 방식의 동시성 모델이 구현되어 있다.

다음 그림은 런타임의 구성요소들을 시각화 한 것이다.



image/svg+xml Stack Heap Queue


스택, 힙, 큐로 이루어져 있다.


스택

스택은 C와 같은 다른 프로그래밍 언어에서 사용하는 함수 호출시 할당, 리턴 시 해제되는 그 콜 스택이다.

함수가 호출될 때 마다 해당 함수에 대한 스택 프레임이 생성되고, 해당 스택 프레임은 해당 함수의 인자와 지역변수들을 저장한다.

함수가 리턴될 때 해당 함수의 스택 프레임은 해제된다.


일반적으로 힙은 동적으로 할당된 변수나 메모리 등을 포함한다. 자바스크립트는 인터프리터형 스크립트 언어이므로 선언된 것들을 저장할 수 있지만, 지역변수나 함수 인자들은 스택에 저장되므로, 전역 객체와 같은 것들이 힙에 저장되게 된다.

자바스크립트 런타임에서 큐는 메시지 큐이다. 이 큐를 이용해서 자바스크립트는 동시성 모델을 지원하며, 큐에는 처리해야 할 메시지들이 저장되어 있다.
메시지는 처리해야할 함수들의 셋이라고 볼 수 있으며, 큐는 메시지 단위로 처리한다. 큐가 메시지를 처리하는 로직은 다음과 같다.

1. 콜 스택이 비었으면 큐에서 메시지를 꺼내서 처리한다.
2. 콜 스택이 비지 않았으면 콜 스택이 빌 때 까지 처리하던 함수를 계속 처리한다.

메시지 처리는 콜 스택에 처음 비어서 해당 메시지를 큐에서 꺼내어서 해당 메시지에 대한 함수를 호출하면서 메시지 처리가 시작이 된다.
콜 스택에 다시 비워질 경우 해당 메시지 처리는 종료된다.

메시지 큐 동작 방식을 확인할 수 있는 간단한 예제를 보자.

 

function foo(a) {

	while(true) {
		a += bar(5);
	}
}

function bar() {
	var a = 1;
	return a;
}

function baz() {
	console.log('Another task');
}

foo();
baz();


브라우저에서 위 코드를 실행하면 Another task라는 메시지는 보이지 않는다. foo함수가 맨 처음 메시지 큐에 들어가서 처리되게 되는데, foo 함수는 bar함수를 호출, 리턴을 무한 반복하게 되며 콜 스택에 비워지지 않기 때문이다. 그러므로 다음 메시지 큐에 baz함수가 있지만 계속해서 실행이 되지 않게 된다.
 

function foo(a) {
	setTimeout(0, bar);
}

function bar(x) {
	setTimeout(0, foo);
}

function baz() {
	console.log('Another task');
}

foo();
baz();


브라우저에서 위 코드를 실행하게 되면 Another task라는 메시지는 잘 보인다. setTimeout(time, function); 함수를 호출하면, time시간 이후에 function 함수를 메시지 큐에 추가하게 된다.

메시지 큐에는 다음과 같은 방식으로 메시지가 들어가고, 처리되게 된다.

foo 

 baz

 bar

 foo

 bar



이러한 이벤트 루프의 특성을 잘 파악해야지, 여러개의 테스크를 동시성있게 실행되게 자바스크립트 코드를 작성할 수 있다. 이러한 이벤트 루프 모델에 반하는 방식으로 코드를 작성하면 나쁜 UX를 사용자에게 제공하게 되며, 이는 거의 버그나 마찬가지이다.



------------------------------------------------------

참고

https://developer.mozilla.org/ko/docs/Web/JavaScript/EventLoop

+ Recent posts