본문 바로가기
백엔드 개발

#055. 리팩터링: Promise 객체 다루기

by iamjoy 2023. 8. 13.

Node가 싱글 스레드로 비동기 처리하는 방법

libuv(리버브)는 이벤트 루프를 기반으로 비동기 I/O를 지원하는 다중 플랫폼 C 라이브러리이다. nodeJS의 비동기 논블로킹인데, 이러한 비동기 논블로킹 작업을 위해 libuv를 사용한다. 리버브는 I/O 관련 블로킹 작업을 처리하여 이벤트 루프(싱글 스레드)가 blocking되지 않게 한다. I/O 관련된 작업(http, Database CRUD, filesystem) 등 블로킹 되는 작업들을 백그라운드(OS 커널 혹은 libuv의 thread pool)에서 수행하고, 이를 비동기 콜백함수로 이벤트 루프에 전달한다. 이렇게 블로킹 되는 작업을 넘기는 것을 'offloading(오프로딩)' 이라고 한다 이벤트 루프는 싱글 스레드이고, 블로킹 I/O 관련을 처리하는 백그라운드는 멀티 스레드로 이루어져 있기 때문에 (요새 싱글 코어 CPU 없으니까) Node가 여러 작업을 동시에 처리하는 것처럼 보이게 한다.

https://velog.io/@j901207/NodeBasic

동기- 비동기는 백그라운드에 넘긴 작업 완료 여부를 확인하는 지에 따라 다르고
블로킹-논블로킹은 함수가 바로 return 되는 지 여부를 의미한다.
노드에서는 동기-블로킹 방식과 비동기-논블로킹 방식이 대부분이다.

Node.js 교과서 개정 3판

Promise

Promise는 비동기 상태를 값으로 취급하여 다양한 연산을 할 수 있도록 도와주는 자바스크립트 객체이다.  Promise는 기본적으로 총 3가지의 상태를 가진다.(대기 상태(pending), 이행 상태(fullfilled), 거부 상태(rejected)) async+await 과 함께 사용하면 가독성이 높아진다.

Promise가 가진 매서드들

Promise.all()

[MDN 명세]
- 입력 값으로 들어온 프로미스 중 하나라도 거부 당하면 Promise.all()은 즉시 거부한다. (프로미스 중 하나가 reject하는 경우, 모두 reject 된다.)
- 반환하는 프로미스의 이행 값은 매개변수로 주어진 프로미스의 순서와 일치하며, 완료 순서에 영향을 받지 않는다. (실행 속도가 다르더라도 입력된 매개변수의 순서를 보장한다)
- 매개변수로 주어진 순회 가능한 객체가 비어 있으면 실행한 빈배열 값의 Promise를 반환한다.

Promise.allSettled()

[MDN 명세]
- 이행/거부 여부에 관계없이 주어진 프로미스가 모두 완료될 때까지 기다린다. 결과적으로, 주어진 이터러블의 모든 프로미스와 함수의 결과 값을 최종적으로 반환한다. (중간에 reject 되어도 모두 수행된다)
- 일반적으로 서로의 성공 여부에 관련 없는 여러 비동기 작업을 수행해야 하거나, 항상 각 프로미스의 실행 결과를 알고 싶을 때 사용한다. 그에 비해, Promise.all()이 반환한 프로미스는 서로 연관된 작업을 수행하거나, 하나라도 거부 당했을 때 즉시 거부하고 싶을 때 적합. 
-  빈 이터러블 객체를 인자로 전달받았을 경우에만 빈 배열로써 이미 이행된 객체를 반환한다.
- 반환된 각 객체별로 status를 확인할 수 있다. 만약 fulfilled 상태라면 value를, rejected 상태라면면 reason 속성을 확인할 수 있다. value나 reason을 통해 각 Promise가 어떻게 이행(또는 거부)됐는지 알 수 있다.

Promise.allSettled([
  Promise.resolve(33),
  new Promise((resolve) => setTimeout(() => resolve(66), 0)),
  99,
  Promise.reject(new Error("an error")),
]).then((values) => console.log(values));

// [
//   { status: 'fulfilled', value: 33 },
//   { status: 'fulfilled', value: 66 },
//   { status: 'fulfilled', value: 99 },
//   { status: 'rejected', reason: Error: an error }
// ]

Promise.race()

[MDN 명세]
- iterable 안에 있는 프로미스 중에 가장 먼저 완료된 것의 결과값으로 그대로 이행하거나 거부한다.
- 비어 있는 iterable을 전달하면 반환한 프로미스는 영원히 대기 상태가 된다.

주의할 점들

Q. Promise.all은 실행순서를 보장할 수 있나?
완료되는 시간을 보장할 수는 없다. 하지만 실행 결과는 파라미터의 순서를 보장한다.
아래의 예시를 보면 promise2가 promise3보다 완료되는 시간이 빠르지만 실행 결과는 입력한 순서대로(1->3->2) 로 출력된다.

const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'foo');
});

Promise.all([promise1, promise3, promise2]).then((values) => {
  console.log(values);
});
// Expected output: Array [3, "foo", 42]

Q. 어떨 때 .all 을 쓰고 어떤 때 .allSettled를 써야하나?

서로 관련이 되어있어서 하나라도 실패하면 다 실패해야한다면 -> Promise.all()
서로 분리 되어 있어 성공 실패가 서로 독립적이라면 -> Promise.allSettled()
예를 들어 모두 합쳐서 하나의 정보를 가져오는 상황이라면 .all을 써야하고 성공 실패 로그를 남기는 상황이어서 reject 또한 하나의 데이터가 된다면 .allSettled를 써야한다. 트랜젝션 처럼 하나라도 실패하면 .catch 훅으로 이동시킨다.

Q. 동기적으로 처리되는 지, 비동기적으로 처리되는 지?

비동기 호출을 순차적으로 이벤트루프레 올려 거의 병렬적으로 실행하게 된다

Reference

https://nodejs.org/ko/docs/guides/blocking-vs-non-blocking
https://medium.com/@vdongbin/node-js-%EB%8F%99%EC%9E%91%EC%9B%90%EB%A6%AC-single-thread-event-driven-non-blocking-i-o-event-loop-ce97e58a8e21
https://darrengwon.tistory.com/953
https://thebook.io/080334/0147/