본 글의 예제 코드는 JavaScript를 사용했습니다.
JavaScript를 모르고, Node.js를 모른다면 Node.js에는 이런 개념이 있구나~ 정도로 이해하면 감사하겠습니다~!
최근 자바를 열심히 하고 있다.
JS도 잊지않기 위해 간단한 개념 정리를 해보려고 한다.
오늘은 비동기 프로그래밍에 대해 정리해보고자 한다.
JS에서 함수를 호출하면 함수의 실행 컨텍스트(실행 컨텍스트란 JS에서 실행할 코드에 제공할 환경 정보를 모아놓은 객체이다!)가 생성된다.
https://junilhwang.github.io/TIL/Javascript/Domain/Execution-Context/
실행 컨텍스트(VE, LE , this binding)는 콜 스택에 쌓이고, 함수가 실행된다.
함수가 실행을 끝내면, 해당 함수의 실행 컨텍스트는 스택에서 팝되어 사라진다.
다음 코드의 실행을 보고, 콜 스택을 그려보자.
함수가 실행되려면 함수의 실행 컨텍스트가 콜 스택으로 푸시되어야 한다.
즉, 콜 스택에 실행 컨텍스트가 푸시되었다 === 함수 실행의 시작 을 의미한다.
함수는 호출된 순서대로 콜 스택에 푸시된다.
함수의 실행 순서는 콜 스택으로 관리한다고 이해하면 편하다!
비동기 프로그래밍을 얘기하는데 실행 컨텍스트에 관한 얘기는 왜 했을까?
자바스크립트 엔진은 단 하나의 콜 스택(실행 컨텍스트 스택)을 갖기때문이다.
함수를 실행할 수 있는 콜 스택이 단 하나이며, 동시에 2개 이상의 함수를 실행할 수 없다는 것입니다.
콜 스택의 가장 위에 있는 "실행 중인 실행 컨텍스트"를 제외한 실행 컨텍스트는 모두 실행 대기 중인 태스크(task)입니다.
태스크는 현재 실행되는 실행 컨텍스트가 종료된 뒤 콜 스택에서 사라지고 난 뒤에서야 실행할 수 있습니다.
위의 내용과 같은 말이 자바스크립트는 싱글 스레드 방식이라는 것이다.
한 번에 하나의 태스크만 실행할 수 있다는 뜻이다! (자바는 멀티 스레드니까 한번에 여러 작업을 수행할 수 있다!)
(스레드는 일하는 일꾼이라 생각을 하면 된다. 일꾼이 한 명이기에, 한번에 하나의 일만 할 수 있는 것!!)
만약 어떤 태스크가 시간이 걸리는 작업이어서, 뒤에 있는 태스크들이 실행 안되고 멈춰있는 경우를 블로킹(blocking)이라고 한다.
예를 들어
위 코드를 실행하면
3초 뒤 foo 실행
3초 뒤 bar 실행
이렇게 동작한다.
bar함수는 [처음 3초 + foo 실행시간] 이 지난 후 실행이 된다.
즉 , 바로 실행되지 못한다.
블로킹이 발생했다!
위처럼 실행 중인 태스크가 종료될 때까지 다음 태스크가 대기하는 방식을 동기(synchronous) 처리라고 한다.
동기 처리 방식은 실행 순서가 보장된다는 장점이 있지만, 하나의 태스크가 종료할 때까지 대기 태스크들이 블로킹되는 단점이 있다. (스레드가 하나인 Node.js에겐 매우 큰 단점이다.)
위 예제를 setTimeout 함수를 사용해 수정해보자!
위 함수의 실행 결과는 어떻게 될까?
이렇게 나올까??
정답은...
bar가 먼저나오고, 3초 후 foo가 나온다!
아니 foo가 먼저 실행된거 아닌가!?
왜 bar가 먼저 나올까?
처음 예제와 뭐가 다르길래...?
setTimeout 함수는 sleep예제와 유사하게 일정 시간이 지난 후 콜백 함수를 호출한다.
그러나 다른 점은 setTimeout 함수는 이후 태스크를 블로킹하지 않고 바로 실행이 된다.
이렇게 현재 실행중인 태스크가 종료되지 않은 상태라도 다음 태스크를 곧바로 실행하는 방식을 비동기(asynchronous) 처리라고 한다.
비동기 처리 방식은 현재 실행 중인 태스크가 종료되지 않은 상태라 해도 다음 태스크를 곧바로 실행하므로 블로킹이 발생하지 않는다는 장점이 있지만, 태스크의 실행 순서가 보장되지 않는 단점이 있다.
타이머 함수(setTimeout, setInterval etc), HTTP 요청, 이벤트 핸들러는 비동기 처리 방식으로 동작한다.
비동기 처리를 이해하기 위해선 이벤트 루프와 태스크 큐에 대해 알아야 한다.
자바스크립트는 싱글 스레드이다.
하지만 브라우저가 동작하는 것을 살펴보면 태스크들이 동시에 처리되는 것처럼 느껴진다.
HTTP 요청을 보내고, 화면의 요소를 움직이기도 하고...
이렇게 자바스크립트의 동시성(concurrency)을 지원하는 것이 바로 이벤트 루프(event loop) 이다.
https://youtu.be/8aGhZQkoFbQ?si=bX5yqfqSvMDCWlzt (이벤트 루프에 관한 영상이다. 매우 명강의이니까 관심이 있으면 꼭 보기를 추천한다!)
이벤트 루프는 브라우저에 내장된 기능 중 하나이다.
브라우저 환경을 봐보자!
JS 엔진의 Heap은 객체가 저장되는 메모리 공간이다.
실행 컨텍스트는 힙에 저장된 객체를 참조한다.
메모리에 값을 저장하려면 메모리 공간의 크기를 결정해야한다!
객체는 원시 값과 달리 크기가 정해져 있지 않고, 런타임 도중에 메모리에 할당 될 메모리 크기를 결정한다.(동적 할당)
그러므로 객체가 저장되는 메모리 공간인 Heap은 구조화되어 있지 않다!
콜스택과 힙으로 구성되어 있는 JS 엔진은 태스크가 요청되면 콜 스택을 통해 요청된 작업을 순차적으로 실행할 뿐dlek.
비동기 처리에서 소스코드 평가 실행을 제외한 모든 처리는 JS 엔진을 구동하는 환경인 브라우저 또는 Node.js가 담당한다.
setTimeout의 예시를 다시 생각해보자!
setTimeout의 콜백 함수의 평가와 실행은 JS 엔진이 담당한다. 그러나 호출 스케쥴링을 위한 타이머 설정 및 콜백 함수의 등록은 브라우저 또는 Node.js가 담당한다.
이를 위해 브라우저 환경은 태스크 큐와 이벤트 루프를 제공한다!
태스크 큐와 이벤트 루프에 대해 궁금하다면!? 👇
- 태스크 큐
비동기 함수에 전달되는 콜백 함수 또는 이벤트 핸들러가 일시적으로 보관되는 영역
(마이크로태스크 큐라는 것도 존재하지만, 본 글에선 다루지 않겠습니다.)
- 이벤트 루프(event loop)
콜 스택에 현재 실행 중인 실행 컨텍스트가 있는지, 태스크 큐에 대기 중인 함수(콜백 함수, 이벤트 핸들러 등)가 있는지 반복해서 확인합니다. 만약 콜 스택이 비어 있고 태스크 큐에 대기 중인 함수가 있다면 이벤트 루프는 순차적(FIFO)으로 태스크 큐에 대기 중인 함수를 콜 스택으로 이동시킵니다. 이때 콜 스택으로 이동한 함수는 실행됩니다. 태스크 큐에 일시 보관된 함수들은 비동기 처리 방식으로 동작합니다.
브라우저 환경에서 다음 코드가 어떻게 동작할지 예측해보자.
setTimeout의 delay가 0초이므로, 바로 foo()가 실행된 뒤 bar()가 실행될까?
실행 결과는 위와 같다.
bar가 먼저 출력된 것을 볼 수 있다.
왜 그럴까?
다음 그림을 보면서 알아보자.
위 그림과 같은 상황이다.
여기서 setTimeout은 비동기 함수이므로, 무조건 WebAPI로 가서 setTimeout 함수를 실행시킨 뒤, 콜백 함수인 foo 함수를 Task Queue로 보낸다.
TaskQueue에 있는 함수는 콜 스택의 실행 컨텍스트가 있는 경우 , 실행 컨텍스트가 다 사라지기 전까진, 콜 스택으로 push될 수 없기에 bar가 실행된 뒤에 foo가 실행 되는 것이다!
자바스크립트는 싱글 스레드 방식으로 동작한다!
오해할 수 있는게 있는데,
싱글 스레드 방식으로 동작하는 것은 브라우저가 아니라 브라우저에 내장된 JS 엔진이라는 사실을 기억해야 한다.
만약 모든 JS코드가 JS 엔진에서 싱글 스레드 방식으로 동작한다면 자바스크립트는 비동기로 동작할 수 없다.
즉, JS 엔진은 싱글 스레드로 동작하지만 브라우저는 멀티 스레드로 동작한다~! 우린 브라우저로 여러 작업을 동시에 할 수 있다!!
함수의 모든 처리가 JS 엔진에서 비동기적으로 수행된다고 가정해보자.
setTimeout 함수를 실행했을 때 setTimeout 함수의 호출 스케줄링을 위한 타이머 설정도 JS 엔진에서 수행을 하게 된다. (모두 JS 엔진에서 돌아간다고 가정!)
그러면 타이머 설정 조차 싱글 스레드로 진행되기에, 타이머 설정을 한 뒤 delay 되는 시간동안 어떠한 태스크도 실행될 수 없다.
즉, setTimeout 함수의 타이머 설정까지 JS 엔진에서 싱글 스레드 방식으로 동작한다면, 비동기 처리가 불가능해진다!
JS의 비동기 프로그래밍에 대해 정리해보았다!
끗!