개요

2024-03-15일 저녁에 공통 프로젝트 팀원들과 회식을 하면서 비동기 처리 관련 글을 포스팅 했던 얘기가 나왔었는데, 여기서 내가 글을 썼음에도 여러가지 부족한 점들이 느껴져서 조금만 더 정리해본다. 그 중 자바스크립트 에서 비동기 처리를 어떻게 하는것인가에 대해서 얘기하다 어물쩡 거려가지고 좀 부끄러웠다. 어떻게 진행되는지 한번 보자.

동기와 비동기

동기

동기(Synchronous)는 순차적인 흐름을 가진다. 하나의 작업이 실행되는 동안, 뒤의 다른 작업들은 그대로 멈춘 채 자신의 차례가 오기까지를 기다린다.

이 일련의 과정 중에서 동기식 프로그램의 특징을 알 수 있다. 손님이 주문을 요청하면, 점원은 그 요청에 대한 처리를 거처 응답을 내놓는다. 손님은 응답이 돌아오기까지 카운터를 떠나지 않고 기다린 뒤, 응답을 받고 나서야 다음 행동을 할 수 있다.

한번에 하나의 태스크를 수행하는 것은 싱글 스레드에서 코드가 동작할 경우이다. 요청에 대한 처리를 할 수 있는 주체가 하나만 존재하기 때문에, 위와 같이 순차적인 흐름을 가질 수 밖에 없다.

비동기

비동기(Asynchronous)는 반대로, 어떠한 요청을 보내면 해당 요청의 응답에 관계없이 바로 다음 동작이 실행된다. 즉, 실행중인 작업이 끝나기를 기다리지 않고 바로 다음 작업으로 넘어갈 수 있다.

점원들의 작업은 비동기식으로 처리되고 있다. 점장은 점원1에게 요청을 보낸 후, 응답을 받지 않은 상태에서 점원2에게 다른 요청을 보낸다. 둘 다 완료 보고라는 응답이 돌아오지 않았지만 점장은 신경쓰지 않고 다음 작업으로 넘어갈 수 있다.

멀티 스레드 방식에서는 요청을 처리할 수 있는 작업 단위가 여러 개이기 때문에, 하나의 태스크가 끝나기를 기다리지 않아도 다음 실행을 할 수 있게된다.

자바스크립트 엔진이란?

자바스크립트의 코드를 해석하고 분석하고 실행하는 인터프리터, 즉 해석기이다. 주로 웹 브라우저에서 사용된다. (자바스크립트 엔진은 브라우저 안에 내장되어 있다.) 엔진에 의해 해석하기 때문에 컴파일 과정일 불필요하다. 웹 브라우저에서 곧바로 해석하고 실행한다.

브라우저별 엔진 종류

  • V8 : 자바스크립트 대표 엔진이다. 구글에서 개발했으며, 크롬과 노드js에서 사용한다.
  • 웹킷 : 애플에서 개발한 오픈소스 엔진이고, 사파리에서 사용한다.
  • 스파이더 몽키, 차크라 등등

자바스크립트 엔진 구조

자바스크립트 엔진은 크게 메모리 힙과 콜 스택으로 나눌 수 있다. 이벤트 루프, 콜백 큐와 같은 엔진 외부 요소들과 함께 코드를 실행한다.

메모리 힙과 콜스택

메모리 힙

객체, 함수, 참조 타입과 같은 데이터를 저장하는 공간.

콜 스택

코드를 실행할 때 코드 안의 실행 순서를 기록하고 순서대로 코드가 실행될 수 있도록 도와주는 스택이다. 자세히 말하자면 함수를 호출 시 함수 실행 컨텍스트가 스택에 쌓이면서 순서대로 실행된다.

<aside> 💡 실행 컨텍스트란? 브라우저가 HTML 문서를 해석할 때, <script> 태그로 감싸진 자바스크립트 코드 또는 onClick과 같은 속성을 가진 태그를 만나면 이것을 자바스크립트에 보낸다. 그리고 자바스크립트 엔진이 브라우저가 넘겨준 코드를 변환하고 실행시키기 위해 특별한 환경을 구성하는데, 이것을 실행 컨텍스트 라고한다.

</aside>

자바스크립트는 어떻게 비동기 실행이 가능할까

자바스크립트 코드가 비동기 실행으로 보여지게 만들어 주는 것은 바로 브라우저이다. 브라우저는 자바스크립트 엔진 외에도 태스크 큐, 이벤트 루프, Web API를 가지고 있다.

  • 태스크 큐 : 비동기 함수의 콜백 함수가 임시 보관되는 공간
  • 이벤트 루프 : 자바스크립트 엔진의 콜스택이 비어있는지, 태스크 큐에 대기 중인 함수가 있는지 확인
  • Web API : 브라우저 환경에서 제공하는 API
    • ex) setTimeout, DOM API, AJAX통신, Canvas, WEBGL, 오디오 또는 비디오 API

비동기 실행에서 브라우저의 역할

자바스크립트 엔진은 싱글 스레드로 동작하지만, 브라우저 엔진은 멀티 스레드로 동작한다. 멀티 스레드로 동작하는 브라우저가 제공하는 Web API를 함께 사용하여 비동기 동작을 구현할 수 있는 것 이다.

자바스크립트 엔진이 코드를 읽어 실행하는 과정

  1. 자바스크립트 엔진이 실행 컨텍스트를 차례대로 콜스택에 쌓는다.
  2. 쌓인 코드들이 순서에 따라 콜스택을 빠져나가며 실행된다.
  3. 코드 중 비동기 함수, 즉 setTimeout 또는 setInterval, 이벤트 핸들러와 같은 Web API 함수를 만나면 자바스크립트 엔진이 이들을 Web API로 보낸다.
  4. Web API는 전달받은 함수의 콜백 함수를 꺼내어 태스크 큐로 보낸다. 이때 딜레이 인자가 잇는 경우는 딜레이 시간만큼 Web API에서 대기하게 된다.
  5. 콜스택에 있던 함수들의 실행이 모두 완료되어 빈 공간이 되면, 이벤트 루프가 태스크 큐에서 대기중인 콜백함수 하나를 콜스택에 넣어 바로 실행시킨다.
  6. 다시 콜스택이 비면 이벤트 루프가 태스크큐가 빌때까지 위의 동작을 반복

결론

자바스크립트엔진은 싱글 스레드가 맞다. 그리고 멀티 스레드로 동작하는 브라우저의 도움이 있기에 비동기 통신을 효율적으로 처리할 수 있는것이다. 회식 하면서 갑자기 헷갈렸던 부분은 “자바스크립트 엔진은 어떻게 싱글 스레드이면서 이벤트 루프를 돌며 WEB API가 실행한 결과를 받아오나” 였는데, 브라우저가 이벤트 루프를 관리하기 때문에 그랬던 것으로 정리할 수 있겠다.

개요

SSAFY 계절학기 강의 수강 중 싱글스레드에서 비동기 요청을 할 수 있다는 내용이 있었는데, 필자가 개발하면서 싱글 스레드 환경에서 비동기 요청을 한 기억이 없어서 그런지 잘 이해가 되지 않았다. 이를 이해하고자 문서를 작성해본다.

자바스크립트에서 싱글 스레드와 비동기 요청

아래와 같이 “싱글스레드 비동기” 라는 키워드로 구글에 검색을 해봤다. 그러나 이 부분은 자바스크립트 언어가 어떻게 싱글 스레드에서 비동기 요청 처리를 가능하게 하는 것인지 말해주는 내용이 대다수 였다. 글들을 읽어보면 결국에는 자바스크립트 언어 자체는 싱글 스레드로 동작하는 것이고, 비동기 요청의 경우 Browser API를 통해 비동기 요청을 처리 후 콜백 처리를 해주는 것으로 이해했다.

따라서, 자바스크립트도 결국에는 완전한 싱글 스레드가 아니라고 생각했다.

그렇다면 완전한 싱글스레드 환경에서 비동기 요청은 가능한 것일까?

알고리즘을 풀 때 처럼 main 함수만 있는 환경에서도 비동기 요청이 가능할 지 궁금했다.

대표적인 비동기 요청 사례인 File I/O를 통해 이를 알아보자.

동기 및 비동기 I/O

동기 파일 I/O에서 스레드는 I/O 작업을 시작하고 I/O 요청이 완료될 때까지 즉시 대기 상태로 돌입합니다. 비동기 파일 I/O를 수행하는 스레드는 적절한 함수를 호출하여 커널에 I/O 요청을 보냅니다. 커널에서 요청을 수락하는 경우, 커널이 스레드에 I/O 작업이 완료되었다는 신호를 보낼 때까지 호출 스레드가 다른 작업을 계속 처리합니다. 그런 다음 현재 작업을 중단하고 필요에 따라 I/O 작업에서 데이터를 처리합니다.

 

아래는 동기 I/O와 비동기 I/O를 그림으로 표현한 것이다.

 

간단하게 정리해보면, I/O 처리의 경우 커널모드 스레드에서 수행된다. 싱글 스레드라고 믿었던 프로그램도 사실은 커널모드 스레드를 통해 I/O 작업을 수행하고 있다. 따라서 위와 같은 파일 I/O 작업의 경우 싱글스레드 프로그램에서도 비동기 요청이 가능하다. 다만, 파일 I/O의 경우 커널모드의 스레드를 이용하므로, 이와 같이 타 스레드에 작업을 위임하지 않는 이상 싱글 스레드에서 비동기 요청은 불가능하다고 생각한다.

싱글 스레드에서 비동기, 효율적인가?

서비스를 개발하면서 싱글 스레드에서 비동기 요청을 하는 경우를 많이 이용해본 적이 없다. 왜일까? 만약, 파일 읽기 요청과 다른 작업을 함께 수행한다고 생각해보면 굉장히 불편할 것이라고 생각한다. 다른 작업을 수행하면서 수시로 커널에 I/O가 완료 되었는지 물어봐야 할 것이고, 에러처리도 어려울 것이다.

비동기 처리 아키텍쳐

Spring, Android와 같은 많은 프레임워크에 비동기 처리를 지원하는 API를 포함하거나, 라이브러리 형태로 사용하기 좋게끔 만들어져 있다. 이러한 라이브러리들은 어떤 구조로 만들어져 있을까? 대표적인 비동기 요청 라이브러리인 Spring WebClient를 통해 알아보자.

Spring WebFlux includes a client to perform HTTP requests with. WebClient has a functional, 
fluent API based on Reactor, see Reactive Libraries, which enables declarative composition 
of asynchronous logic without the need to deal with threads or concurrency. It is fully 
non-blocking, it supports streaming, and relies on the same codecs that are also used to 
encode and decode request and response content on the server side.

Spring WebFlux에 내장된 WebClient의 경우 netty를 기반으로 만들어졌다고 한다. netty는 Reactor Pattern으로 만들어졌다고 하는데, Reactor Pattern에 대해서도 간단히 알아보자.

Reactor Pattern

하나 이상의 클라이언트로부터의 요청을 동시 처리하기 위해서 사용하는 패턴이다.

어플리케이션에서 이벤트를 처리하기 위한 루프를 도는 것이 아니라, 이벤트에 반응하는 객체(reactor)를 만들고, 이벤트가 발생하면 어플리케이션 대신 reactor가 반응하여 처리하는 것이다.

정리

이와 같이 싱글 스레드에서 비동기 요청을 할 수는 있지만, 사용되지 않는 이유가 있는 것 같다. 결론을 내보자면 아래와 같다.

  • 싱글 스레드에서 일부 작업의 경우 비동기 요청이 가능하다.
  • Reactor Pattern이 등장 한 것처럼 비동기 요청의 경우 멀티 스레드를 이용하여 구현하는 것이 효율적일 것이다.

Reference

동기 및 비동기 I/O - Win32 apps

WebClient :: Spring Framework

리액터패턴 / 프로액터패턴

 

+ Recent posts