본문 바로가기

Prog. Langs & Tools/JavaScript

JS #7. 비동기성(Asynchrony): 지금과 나중(now and later)

자바스크립트에서 일정 시간 동안 발생하는 프로그램의 움직임을 어떻게 표현하고 나타낼 것인지에 대해 그동안 많은 연구가 있었다. 오늘은 그러한 연구의 결과 중 하나인 비동기성(Asynchrony)에 대해서 알아보고자 한다. 프로그램에서 '지금'에 해당하는 부분 그리고 '나중'에 해당하는 부분 사이의 관계가 바로 비동기 프로그래밍의 핵심이다.

자바스크립트 프로그램은 .js 파일 하나로도 작성될 수 있지만 보통은 여러 개의 덩이(chunk), 곧 '지금' 실행 중인 프로그램 덩이 하나 + '나중'에 실행할 프로그램 덩이들로 구성된다. 가장 일반적인 프로그램 덩이 단위는 함수이다. 여기서 명심해야 할 부분은 프로그램은 '지금'과 '나중' 사이에 중단되지 않으며 이 때 작업이 비동기적으로 처리되어 단지 '지금' 요청하고 결과를 '나중'에 받을 뿐이라는 점이다.

 

이벤트 루프(event loop)

사실 엄밀하게 보았을 때 자바스크립트는 비동기라는 개념이 있는 것이 아니라, 자바스크립트 엔진은 요청하면 프로그램을 주어진 시점에 한 덩이씩 묵묵히 수행할 뿐이다. 자바스크립트 엔진은 혼자서는 요청을 할 수 없고 반드시 호스팅 환경에서 실행을 할 수가 있는데, 대표적인 호스팅 환경이 웹 브라우저이다.

이 때 환경이 바뀌어도 스레드(thread)는 공통이다. 여러 프로그램 덩이를 시간에 따라 매 순간 한 번의 엔진을 실행시키는 이벤트 루프(event loop)라는 장치이다. 다시 말해, 자바스크립트 엔진은 임의의 자바스크립트 코드 조각을 주는 대로 받아 처리하는 실행기일 뿐, '이벤트(자바스크립트 코드 실행)'를 스케줄링하는 일은 언제나 엔진을 감싸고 있던 주위 환경의 몫이었다.

이벤트 루프는 다음과 같은 형태로 구현되어 있다.(단순하게 의사 코드로 표현)

코드에 while 무한 루프가 있는데 이 루프의 매 순회를 틱(Tick)이라고 부른다. 틱이 발생할 때마다 큐에 적재된 이벤트(콜백 함수)를 꺼내어 실행한다. 자바스크립트 프로그램은 수 많은 덩이로 잘게 나누어지고 이벤트 루프 큐에서 한 번에 하나씩 차례대로 실행된다. ES6 부터는 이벤트 루프 큐가 호스팅 환경이 아닌 자바스크립트 엔진의 관할이 되었으므로 참고하자.

 

병렬 스레딩

비동기와 병렬은 의미가 완전히 다르다. 비동기는 '지금'과 '나중' 사이의 간극에 관한 용어고 병렬은 동시에 일어나는 일들과 연관된다. 프로세스와 스레드는 가장 많이 쓰는 병렬 컴퓨팅 도구로, 별개의 프로세서, 심지어는 물리적으로 분리된 컴퓨터에서도 독립적으로 (때로는 동시에) 실행되며 여러 스레드는 하나의 프로세스 메모리를 공유한다.

반면 이벤트 루프는 작업 단위로 나누어 차례대로 실행하지만 공유 메모리에 병렬로 접근하거나 변경할 수는 없다. 병렬성과 직렬성이 나뉜 스레드에서 이벤트 루프를 협동하는 형태로 공존하는 모습이다. 병렬 실행 스레드 인터리빙과 비동기 이벤트 인터리빙은 완전히 다른 수준의 단위에서 일어난다. 단일-스레드 환경에서는 스레드 간섭은 일어나지 않으므로 스레드 큐에 저수준 작업의 원소가 쌓여 있어도 별 문제 없다. 하지만 하나의 프로그램에서 여러 스레드를 처리하는 병렬 시스템에선 예상치 못했던 일들이 일어날 수도 있다.

자바스크립트의 작동 모드는 단일-스레드 이므로 foo() 내부의 코드는 원자적이다. 즉, 일단 foo()가 실행되면 이 함수 전체 코드가 실행되고 나서야 bar() 함수로 돌아간다는 뜻이다. 이를 완전 실행(Run-to-completion)이라 한다. AJAX를 통해 함수를 호출하게 되면 호출 순서에 따라 결과값이 달라져 비 결정적으로 볼 수도 있지만, 여기서 비결정성은 함수(이벤트)의 순서에 따른 것이지, 스레드처럼 statement의 순서 수준까지는 아니다. 즉, 스레드 보다는 결정적이라고 할 수 있다.

 

동시성

사용자가 스크롤바를 내릴 때 마다 발생되는 onscroll 이벤트(이후 ajax로 데이터를 불러오는 과정까지 있다)가 있다고 가정해 보자. 만약에 엄청 빠른 속도로 스크롤을 내리면 처음 수신된 응답을 처리하는 도중 두 개 이상의 onscroll 이벤트가 발생할 수 있고 그러면 onscroll 이벤트와 ajax 요청 이벤트가 빠르게 발생하며 인터리빙된다.

동시성은 복수의 프로세스가 같은 시간 동안 동시에 실행됨을 의미하며, 각 프로세스 작업들이 병렬로 처리되는지와는 관계 없다. 동시성은 처리 수준(operation-level) 병행성(개별 프로세서의 스레드)과 상반되는 개념의 프로세스 수준(process-level)의 병행성이라고 할 수 있다.

동시 프로세스들은 필요할 때 스코프나 DOM을 통해 간접적으로 상호 작용을 한다. 이 때 이미 한 번 살펴봤던 것처럼 경합 조건이 발생하지 않도록 잘 조율해주어야 한다. 다음은 암묵적인 순서 때문에 두 개의 동시 프로세스가 깨지는 예제이다.

두 동시 프로세스 모두 AJAX 응답 처리를 하는 response() 함수를 호출하는 터라 선발 순으로 처리된다. 만약에 첫 번째 결과를 res[0]에, 두 번째 결과를 res[1]에 담고 싶었다면 response 함수 안에 조건문을 넣어서 순서를 조정해 주어야 한다. 동시성이 문제가 되는 경우 해결을 하는 방법은 관문(Gate)을 넣어서 여러개의 조건이 도착해야 그 다음 열리게 한다거나, 선착순으로 먼저 도착한 함수만 조건을 통과하게 하는 식으로 설정할 수가 있다.

협동적 동시성(cooperative concurrency) 역시 동시성을 조정하는 다른 방안으로, 스코프에서 값을 공유하는 식의 상호 작용에는 별 관심이 없다. 협동적 동시성은 실행 시간이 오래 걸리는 프로세스를 여러 단계/배치로 쪼개어 다른 동시 프로세스가 각자 작업을 이벤트 큐에 인터리빙 하도록 하는 게 목표이다.

예를 들어 아주 긴 리스트를 받아 값을 반환하는 ajax 응답 처리기가 있다고 가정해 보자. 너무 많은(한 1000만개 이상?) 데이터가 들어오면 처리 시간이 오래 걸리고 프로세스 실행 중에 페이지가 멈춰 버릴 수도 있다. 따라서 이벤트 루프 큐를 독점하지 않는, 좀 더 친화적이고 협동적인 동시 시스템이 되려면 각 결과를 비동기 배치로 처리하고 이벤트 루프에서 대기 중인 다른 이벤트와 함께 실행되게끔 해야 한다. 간단한 예시는 아래와 같다.

최대 1000개의 원소를 가진 덩이 단위로 데이터 집합을 처리했다. 이렇게 하면 더 많은 후속 프로세스를 처리해야 하지만 각 프로세스 처리 시간은 단축되므로 이벤트 루프 큐에 인터리빙이 가능하고 응답성이 좋은 사이트/앱을 만들 수 있다. 

 

정리하기

자바스크립트 프로그램은 언제나 사실상 2개 이상의 덩이로 쪼개지며 이벤트 응답으로 첫 번째 덩이는 '지금', 다음 덩이는 '나중'에 실행된다. 한 덩이씩 실행되어도 모든 덩이가 프로그램의 스코프/상태에 똑같이 접근할 수 있으므로 상태 변화는 차례대로 반영된다.

실행할 이벤트가 있으면 이벤트 루프는 큐를 다 비울 때 까지 실행한다. 이벤트 루프를 한 차례 순회하는 것을 틱이라 한다. UI, IO, 타이머는 이벤트 큐에 이벤트를 넣는다.

언제나 한 번에 정확한 한 개의 이벤트만 큐에서 꺼내 처리한다. 이벤트 실행 도중, 하나 또는 그 이상의 후속 이벤트를 직/간접적으로 일으킬 수 있다.

동시성은 복수의 이벤트들이 연쇄적으로 시간에 따라 인터리빙 되면서 고 수준의 관점에서 볼 때 (실제로는 특정 시점에 한 개의 이벤트만 처리되고 있지만) 꼭 동시에 실행되는 것처럼 보인다.

(OS와 시스템 프로세스와는 달리) 동시 프로세스들은 어떤 형태로든 서로 영향을 미치는 작업을 조정하여 실행 순서를 보장하거나 경합 조건을 예방하는 등의 조치를 해야 한다. 이 "프로세스"는 자체를 더 작은 덩이로 잘게 나누어 다른 "프로세스"에 인터리빙 되는 형태의 협동 또한 가능하다. 

 

참고자료

  1. <YOU DON'T KNOW JS (this와 객체 프로토타입, 비동기와 성능)> 카일 심슨 저
  • DICo 2020.10.21 13:18 댓글주소 수정/삭제 댓글쓰기

    책을 읽어도 잘 이해가 안되는 느낌이었는데 핵심을 이해하기 쉽게 잘 정리해주셨네요! 덕분에 도움이 많이 되었습니다 ㅎㅎ