티스토리 뷰

728x90
반응형

nodejs는 싱글스레드이며, 논블로킹이다 라는것은 nodejs를 공부하는 사람이라면 항상듣는 개념입니다

 

그런데 막상 코드를 짤때 이부분을 간과하는 경우가 많습니다

 

 

그래서 한번 어떻게 논블로킹하게 생각하고 코딩해야되는지 알아보는 시간을 갖도록 합시다.

 

논블로킹한 코드를 짜지 않으면 어떻게 되는지 체감해보도록 해요

 

 

블로킹한 작업을 하는 다음과 같은 코드를 만듭니다

const busyWork = (ms) => {
  const date = Date.now();
  let currentDate = null;
  do {
    currentDate = Date.now();
  } while (currentDate - date < ms);
};

파라메터로 주어진 ms(밀리세컨드)만큼 뭔가 바쁜 작업을 수행하는 함수입니다

 

여기서는 do while 문으로 sleep과 비슷한 기능을 구현했습니다

 

이 함수안의 내용은 다른 무거운 cpu 관련작업이나 파일읽기쓰기와 같은 작업을 넣어도 됩니다

(쓰레드를 blocking하는 작업이면 됩니다)

 

 

그리고 express를 기반으로 2개의 라우터를 만들거예요

 

1번 API입니다

 

app.get("/api1", (req, res) => {
  const startTime = Date.now();
  busyWork(req.query.timeout);
  const endTime = Date.now();
  res.json({
    name: "api1",
    startTimeReadable: new Date(startTime).toISOString(),
    endTimeReadable: new Date(endTime).toISOString(),
    exucuteTime: endTime - startTime,
  });
});

 

timeout이라는 query 파라메터를 받아 전달받은 ms 만큼 busyWork()를 실행하는 내용을 작성했습니다

 

reponse에는 시작시간과 종료시간, 그리고 총 소요시간을 리턴합니다

 

http://localhost:3000/api1?timeout=5000

 

이렇게 실행하게 되면 5000ms(5초) 뒤에 결과값을 리턴할것으로 기대되겠죠?

 

 

2번 API입니다

app.get("/api2", (req, res) => {
  const startTime = Date.now();
  setTimeout(() => {
    const endTime = Date.now();
    res.json({
      name: "api2",
      startTimeReadable: new Date(startTime).toISOString(),
      endTimeReadable: new Date(endTime).toISOString(),
      exucuteTime: endTime - startTime,
    });
  }, req.query.timeout);
});

setTimeout을 통하여 query 파라메터로 받은 timeout 만큼의 시간뒤에 결과값을 리턴하는 내용입니다

 

http://localhost:3000/api2?timeout=2000

 

이렇게 호출하면

 

2000ms(2초) 뒤에 결과값을 리턴할것으로 기대됩니다

 

 

 

 

그리고 이 2개의 API를 동시에 호출하는 테스트케이스 2개를 만들어봅니다

 

Promise.all을 통해서 2개의 API를 병렬로 실행되도록 했습니다

 

async function fireTest() {
  console.log("test1");
  await Promise.all([
    axios.get("http://localhost:3000/api1?timeout=5000"),
    axios.get("http://localhost:3000/api2?timeout=2000"),
  ]).then((values) => console.log(values.map((value) => value.data)));
  console.log("test1 end");

  console.log("test2");
  await Promise.all([
    axios.get("http://localhost:3000/api2?timeout=2000"),
    axios.get("http://localhost:3000/api1?timeout=5000"),
  ]).then((values) => console.log(values.map((value) => value.data)));
  console.log("test2 end");
}

 

첫번째 테스트케이스는

 

api1을 5초의 timeout으로 실행하고

api2를 2초의 timeout으로 실행합니다

 

병렬로 실행되겠지만 api1이 먼저 호출되고 api2가 호출됩니다

 

 

두번째 테스트케이스는

 

api2를 2초의 timeout으로 실행하고

api1을 5초의 timeout으로 실행합니다

 

병렬로 실행되겠지만 api2가 먼저 호출되고 api1이 호출됩니다

 

 

전체 코드를 살펴볼까요

 

const { default: axios } = require("axios");
const express = require("express");
const app = express();

const busyWork = (ms) => {
  const date = Date.now();
  let currentDate = null;
  do {
    currentDate = Date.now();
  } while (currentDate - date < ms);
};

app.get("/api1", (req, res) => {
  const startTime = Date.now();
  busyWork(req.query.timeout);
  const endTime = Date.now();
  res.json({
    name: "api1",
    startTimeReadable: new Date(startTime).toISOString(),
    endTimeReadable: new Date(endTime).toISOString(),
    exucuteTime: endTime - startTime,
  });
});

app.get("/api2", (req, res) => {
  const startTime = Date.now();
  setTimeout(() => {
    const endTime = Date.now();
    res.json({
      name: "api2",
      startTimeReadable: new Date(startTime).toISOString(),
      endTimeReadable: new Date(endTime).toISOString(),
      exucuteTime: endTime - startTime,
    });
  }, req.query.timeout);
});

app.listen(3000, () => {
  console.log("server start");
  fireTest();
});

async function fireTest() {
  console.log("test1");
  await Promise.all([
    axios.get("http://localhost:3000/api1?timeout=5000"),
    axios.get("http://localhost:3000/api2?timeout=2000"),
  ]).then((values) => console.log(values.map((value) => value.data)));
  console.log("test1 end");

  console.log("test2");
  await Promise.all([
    axios.get("http://localhost:3000/api2?timeout=2000"),
    axios.get("http://localhost:3000/api1?timeout=5000"),
  ]).then((values) => console.log(values.map((value) => value.data)));
  console.log("test2 end");
}

 

 

여러분은 어떤결과를 기대하고 있나요?

 

이 코드의 실행을 통해서 논블로킹 코드가 얼마나 앱의 전체적인 성능에 문제를 일으키는지 알수 있습니다

 

결과값을 볼까요

server start
test1
[
  {
    name: 'api1',
    startTimeReadable: '2022-09-21T05:45:41.708Z',
    endTimeReadable: '2022-09-21T05:45:46.708Z',
    exucuteTime: 5000
  },
  {
    name: 'api2',
    startTimeReadable: '2022-09-21T05:45:46.711Z',
    endTimeReadable: '2022-09-21T05:45:48.713Z',
    exucuteTime: 2002
  }
]
test1 end
test2
[
  {
    name: 'api2',
    startTimeReadable: '2022-09-21T05:45:48.725Z',
    endTimeReadable: '2022-09-21T05:45:53.726Z',
    exucuteTime: 5001
  },
  {
    name: 'api1',
    startTimeReadable: '2022-09-21T05:45:48.725Z',
    endTimeReadable: '2022-09-21T05:45:53.725Z',
    exucuteTime: 5000
  }
]
test2 end

 

test1번 케이스를 살펴볼께요

 

api1을 5초의 타임아웃으로 실행하고

api2를 2초의 타임아웃으로 실행했습니다

 

시각순서대로 살펴보면

 

API1 시작

API1 종료(5초뒤)

API2 시작

API2 종료(2초뒤)

 

이렇게되었습니다

 

좀더 쉽게 풀어 설명을 하면

 

API1이 쓰레드를 블록시키는 작업을 하게 되어 API2는 꼼짝없이 API1이 끝날때까지 기다립니다

 

API1이 끝난후에 API2가 실행되어서 결과적으로 API2를 호출한 클라이언트는 자신의 작업은 2초밖에 걸리지 않는 작업었이지만

 

앞선 작업에 영향을 받아 7초뒤에 결과값을 받게 되었습니다

 

쓰레드를 블록시키게 되는경우 이후에 오는 모든 request들이 영향을 받아 지연된다는 사실을 알수 있습니다

 

 

 

test2번 케이스를 보겠습니다

 

api2를 2초의 타임아웃으로 실행하고

api1을 5초의 타임아웃으로 실행합니다

 

결과값을 시각 순서대로 살펴보면

 

API2 시작(API2와 API1이 ms까지 표시한 시각으로는 같은 시각에 시작되었지만 실제적으로는 API2가 먼저 도달하여 실행됨)

API1 시작

API1 종료(5초뒤)

API2 종료

 

결과를 풀어 설명하면

 

API2가 먼저 실행되었지만 쓰레드를 블록하지 않았기에 해당 작업의 종료를 기다릴필요없이

 

API1이 시작이 되긴 하였습니다

 

그런데 API1의 작업이 쓰레드를 블록하는 작업이라

 

API2는 2초뒤 자신의 작업이 끝났음에도 불구하고 결과값을 리턴하지 못한채로 API1의 작업이 끝나고 난 뒤에야 결과값을 리턴하게 됩니다

 

API2는 2초뒤에 결과값을 응답받을거라 기대했지만 5초가 걸리게 되었습니다

 

 

 

두가지의 사례에서 볼수 있듯이 쓰레드를 블록하는 작업은 그 작업의 전후에 들어온 작업들이 모두 영향을 받게 됩니다

 

쓰레드를 블로킹하지 않도록 작업하는것

 

바로 그것이 nodejs의 성능을 극대화하는 방법입니다

 

명심하세요~

 

 

 

 

 

 

 

728x90
반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함