javascript 싱글스레드 설계와 이벤트루프를 통한 병렬 실행

2022-10-10 오전 12:25:50
JavaScript
자바스크립트
EventLoop
javascript 싱글스레드 설계와 이벤트루프를 통한 병렬 실행

개요

이번에 회사프로젝트 인증관련 로직을 수정하게 되었다.

특정 화면에서 3개의 ajax요청이 필요하다고 가정했을때,
각 요청은 비동기적으로 실행해 주어야 한다.

이 때, 비동기적으로 실행된 ajax요청들이 서버로 날아가기 전
accessToken의 만료여부를 체크 후 미리 갱신해주어야 하는데
단순히 각 요청 전에 갱신 로직만을 추가하게 되면,
3번의 accessToken 갱신이 불필요하게 일어날 수 있다.😱

그렇다고해서 모든 화면에서 accessToken만료여부를 체크하는
로직을 ajax요청 전에 넣기에는 중복코드가 발생하고 관리하기 번거로웠다.

이 문제점들을 해결하고, accessToken을 필요한시점에 한번만 재발급받기위해
interceptormutex 개념을 도입해보게 되었다.

accessToken을 재발급 받아야 하는 시점에
비동기적으로 실행되는 ajax 요청 작업들을 pending시키고,
token 재발급 이후 pending상태의 작업들을 resolve 시키도록 구조를 개편하였다.

해당 PR에 대해 코드리뷰를 진행하던 중

"자바스크립트는 싱글스레드기반 언어인데 실제로 이게 비동기적으로(병렬) 실행되는 원리가 뭐지?"

라는 의문에 명확한 설명을 하기 어려웠다.

이전에 이벤트루프에 관한 글을 작성한 적이 있었다. ( 링크 )
이때 알고 있던 개념으로도 원하는 flow대로 코드를 작성하고, 동작시키는데 큰 문제는 없었지만,
궁금해서 찾아보기로 했다.

1. 이벤트루프

먼저 자바스크립트가 실행되는 구조를 이해하기 위해
이벤트루프를 조금 더 자세히 알아보자.

아래 영상은 이벤트루프에 대해 가장 잘 설명해주는 영상인것같다.
한글 자막도 잘 되어있어서 꼭 시청해보기를 추천한다.
(Node.js 환경과 브라우저 환경의 이벤트루프 구조는 조금 다르니 참고)

image

영상 내용 요약

  • 이벤트루프는 JS엔진(v8)에 포함된것이 아니다.
  • 이벤트루프와 webApis(Node.js의 경우 C++ Api) 등은 브라우저(혹은 Node.js)에서 제공한다.
  • 이벤트루프는 callStack이 비어있으면 callBack Queue에서 task를 하나 꺼내서 stack에 쌓아 실행될 수 있도록 한다.
  • javascript가 single thread라는 말은 곧 "callStack이 하나다"라는 말과 같다.

2. v8

2-1. v8이란?

V8은 구글이 도입한 오픈소스 javascript 엔진이다.
C++로 작성되었으며 구글 크롬, 크로미움 웹 브라우저, Node.js를 지원한다.
환경과 상호작용하고 프로그램을 실행하기 위한 바이트코드를 생성하는 역할을 담당한다.

2-2. v8은 싱글스레드?

V8은 javascript 실행 엔진이지만, v8자체가 싱글스레드이냐 라고 한다면 그건 아니라고 한다.

It’s single threaded on the JavaScript side, but there are multiple threads under the hood of v8.
V8 is a single threaded execution engine. It’s built to run exactly one thread per JavaScript execution context. ( 링크 )

3. Interceptor

자바스크립트를 분석, 실행하고 callStack을 관리하는 것은 자바스크립트 엔진(v8)이다.
ajax 요청의 경우 callStack에 쌓였다가 사라진 후 webApis(브라우저의 멀티스레드 환경)에서 실행된다.
먼저 완료된 ajax 작업의 콜백함수가 callBack Queue(microTask Queue)에 먼저 쌓이게된다.
작업이 실제로 실행되는 곳은 멀티스레드 환경에서 병렬로 이루어지고,
완료된 순서대로 callStack에 다시 쌓이게되므로 동기였다가 비동기였다가 헷갈리게
짬뽕으로 돌아간다고 볼 수 있다.

그러나 인증로직에 추가된 mutex 개념은 요청을 보내기 이전의 시점을 핸들링 하고있다.

// sudo code // fetch 요청이 나가기 전 interceptor 영역 import { Mutex } from 'async-mutex'; const mutex = new Mutex(); async function interceptor() { ... /* mutex가 lock상태면 여기서 pending후 * lock이 풀리면 진행됨 */ await mutex.waitForUnlock(); // return promise const now = new Date().getTime(); const expired = getExpired(accessToken) ?? 0; if (!mutex.isLocked()) { /* 토큰이 있는데 만료시 재발급 시도 */ if (now > expired) { /* mutex.acquire 실행시 mutex.isLocked() -> true */ const release = await mutex.acquire(); try { const token = `Bearer ${accessToken?? ''}`; const res = await axios.get( `${envs.backendApiHost}/renew-access-token`, { headers: { Authorization: token }, } ); if (!res.data.result?.accessToken) { throw res; } await setAccessToken(res.data.result.accessToken); } catch (err: unknown) { console.error('error in Interceptor :', err); } finally { /* 토큰 발급 완료 후 mutex lock을 풀어준다. */ release(); } } } else { /* mutex가 lock상태면 여기서 pending후 * lock이 풀리면 진행됨 */ await mutex.waitForUnlock(); } // Do Tasks after renew token... ... }

axios콜은 비동기적으로 완료되기전에 또 실행될 수 있다.
코드가 실행되는 중에 mutex.waitForUnlock()이라는 코드를 만나게 되면
accessToken 재발급이 진행중인 상태(lock)일때 새로운 promise를 리턴받게된다.

이 리턴받은 promise는 callStack에 올라왔다가 뭔가를 실행 후 바로 없어지고,
mutex 인스턴스에 Todo개념으로 작업이 쌓이게된다.

mutex 인스턴스는 하나를 사용하고 있으므로,
accessToken 재발급이 완료되면 mutex.release()를 실행하여 대기중이던
promise를 모두 resolve시키면 중단되었던 작업들이 다시 실행된다.

이렇게 비동기적으로 ajax 요청을 실행해도 accessToken은 가장먼저 실행되는 요청의 직전
시점에 딱 한번만 갱신할 수 있는 구조를 만들게 되었다.

4.Mutex

mutex는 그럼 이 복잡한 작업을 어떻게 중단시키고 다시 실행되게 하는걸까?
간단하게만 살펴보겠다.
아래는 mutex의 waitForUnlock 내부 코드이다.

waitForUnlock(weight = 1): Promise<void> { if (weight <= 0) throw new Error(`invalid weight ${weight}: must be positive`); return new Promise((resolve) => { if (!this._weightedWaiters[weight - 1]) this._weightedWaiters[weight - 1] = []; // resolve를 _weightedWaiters에 push해두고있다. this._weightedWaiters[weight - 1].push(resolve); this._dispatch(); }); }

목차 3.interceptor의 코드에서 release()가 실행되면
_weightedWaiters에 쌓여있던 resolve들이 반복문을 돌면서 하나씩 실행되게된다.
resolve된 promise들은 다시 microTask Queue에 쌓이게되고,
이벤트루프가 돌면서 callStack에 올라가 다시 실행되게 되는것이다.

4. 결론

이걸 싱글 스레드라고 해도 되나...?라는 생각도 든다.
어쨌든 자바스크립트는 싱글 스레드 기반으로 설계된것은 확실하다.

병렬작업도 할 수 있지만 스레드 신경쓰지말고
개발자는 싱글스레드 처럼 코드를 작성하는게 컨셉 인것같다.
어떻게 보면 복잡한 멀티스레드로 개발하는것보다 편한것 같긴 하다.
보면 볼 수록 특이한 언어인것같다.

생각보다 공식문서에 관련 정보가 별로 없거나 정리가 되어있지 않아서
검색이 어려웠고, 잘못된 정보가 포함되어있을 수도 있다.

Develogger에 어서 댓글을 구현해야 잘못된 부분이 있을때 피드백을 받을 수 있을텐데..ㅋㅋ
혹시라도 추후 알게되면 수정해 둘 예정이다.


References

https://develogger.kro.kr/blog/LKHcoding/106
https://ui.toast.com/posts/ko_20220725
https://postlude.github.io/2021/11/16/js-eventloop-microtask/
https://quick-advisors.com/is-chrome-v8-multithreaded/
https://xzio.tistory.com/939
https://dev.to/jasmin/difference-between-the-event-loop-in-browser-and-node-js-1113
https://skchawala.medium.com/javascript-is-a-single-threaded-beast-then-how-the-heck-asynchronous-code-execution-works-bf3279bd7bff
https://ui.toast.com/posts/ko_20210909
https://evan-moon.github.io/2019/08/01/nodejs-event-loop-workflow/



User Profile Icon
LKHcoding
Front-End Developer