Dev./Node.js

[Node.js] 이벤트 루프는 pending Promise를 기다려주지 않는다.

인쥭 2022. 6. 3. 18:02
반응형

업무 도중에 Node.js로 다음과 같은 기능을 작성할 일이 있었다.

  1. 수 많은 Promise를 생성하되, 각각의 Promise는 1:1로 대응되는 stream의 end 또는 error 이벤트에서 상태가 결정된다.
  2. Promise.all로 작업이 완료되는 것을 기다린다.
  3. 후속 작업을 처리한다.

그런데 await Promise.all(promises); 에서 대기하던 중 Node.js 애플리케이션이 아무런 로그 없이 code 0으로 정상 종료되는 상황이 발생하였다. (물론 코드 전역에 try - catch를 걸어도 어떠한 에러도 잡히지 않았다.)


해당 문제의 원인은 다음과 같으며, 핵심은 stream의 에러 또는 스펙이 아닌 Node.js 동작 방식에 있었다.

  1. Node.js는 애플리케이션을 실행할 경우, 이벤트 루프를 생성한다.
  2. 애플리케이션에 포함된 코드를 한 줄 한 줄 차례대로 실행한다.
  3. 모두 실행되어 콜 스택이 비게 되는 경우, 이벤트 루프를 확인한다.
  4. 이벤트 루프가 활성 상태라면 비동기 작업에 할당된 콜백이 대기 중인 것이므로, 이벤트 루프를 돌며 이를 처리한다.
  5. 이벤트 루프가 비활성 상태라면 애플리케이션에 더 이상 실행할 작업이 없는 것으로 보고, process.on('exit', ()=>{})를 실행한 후 종료한다.

결국 내 경우에는 수많은 비동기 작업 중에 높은 확률로 모든 Promise가 pending 상태로 대기하여 이벤트 루프가 비워지는 상황이 발생하였으며, 이로 인해 Node.js가 후속 작업을 진행하지 않고 종료되는 것이었다.


이러한 상황을 간단하게 재현하기 위해 다음의 코드를 확인하자.

// app.js
process.on('exit', (exitCode) => {
  console.log(`node exit with code ${exitCode}`);
});

(async function main() {

  await new Promise((resolve => {
    // 해당 프로미스는 resolve되지 않으므로, pending 상태를 유지한다.
    console.log('in promise');
  }));

  console.log('end of main function');
})();
console.log('end of code');
  • 애플리케이션이 종료되는 경우, exit 콜백에 의해 로그가 출력된다.
  • main은 IIFE이며, 절대 resolve 되지 않으므로 영영 pending 상태인 객체에 대해 await한다.
  • 그럴 일은 없겠지만, Promise의 상태가 결정된다면 로그를 출력한다.

상술한 코드의 실행 결과는 다음과 같이 반환된다.

[~] node app.js
in promise
end of code
node exit with code 0
[~]

나는 'end of code'가 노출된 후에 애플리케이션이 종료되지 않고 무한정 대기할 것으로 생각했으나, 위 코드는 실제로는 다음과 같은 순서로 동작하기에 pending 상태인 Promise를 기다려주지 않는다.

  1. node app.js 명령어를 통해 애플리케이션이 시작되고, Node.js는 이벤트 루프를 생성한다.
  2. main 함수가 즉시 실행되며, pending 상태인 Promise를 생성하는 과정에서 in promise 로그가 출력된다.
  3. await 키워드에 의해 async 함수인 main은 일시정지되며, async 함수는 즉시 pending 상태인 새로운 Promise를 반환한다.
  4. 마지막 줄의 console.log가 실행된다.
  5. 마지막 줄의 console.log가 완료되어 콜 스택이 비워지면, Node.js는 이벤트 루프를 확인한다.
  6. 현재 시점에서 이벤트 루프는 다음과 같은 이유로 활성화되어 있지 않다.
    1. Promise는 이벤트 루프가 관리하는 콜백이 아닌 '언젠가는 상태가 결정될' 객체이다.
    2. Promise와 관련된 microTaskQueue는 Promise 자체가 아닌, resolve된 Promise의 콜백을 관리하는 큐이다.
    3. 그러나 상술한 코드에서 반환되는 Promise는 모두 pending 상태이므로, microTaskQueue는 비어 있는 상태이다.
    4. 결국 이벤트 루프의 각 페이즈에서 수행해야 할 콜백도 없고, microTaskQueue도 아무런 콜백을 갖지 않는다.
  7. 때문에 Node.js는 더 이상 수행할 작업이 없다고 보고 실행을 종료한다.

Node.js의 꽃인 비동기 작업을 문제 없이 수행하는 코드를 작성하려면, 역시 이벤트 루프에 대한 이해가 필수적인 것 같다...


참고

아래의 링크는 상술한 문제의 원인을 파악하는 데에 가장 큰 도움이 되었던 nodejs의 github 이슈이다.

 

Nodejs does not wait for promise resolution - exits instead · Issue #22088 · nodejs/node

Version: v10.6.0 Platform: Linux 4.15.0-24-generic #26~16.04.1-Ubuntu SMP Fri Jun 15 14:35:08 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux Subsystem: The code bellow will end with output: Looping before...

github.com