ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Node.js] 공식문서로 이해하는 이벤트 루프
    Node.js 2022. 2. 3. 17:37
    반응형

    Node.js의 공식문서 중에 아쉽게 한글화가 되어있지 않은 흥미로운 가이드가 하나 있다.(링크)

    Don't Block the Event Loop라는 타이틀에서 알 수 있듯이 자바스크립트, 특히 Node.js 환경에서 동작하는 어플리케이션을 만들 때 가장 중요한 이벤트 루프에 대해 깊게 이해할 수 있는 공식 가이드이다.

    서버사이드 Node.js 어플리케이션을 만들기 위한 여러 가지 고려사항들과 설계 원칙들을 확인할 수 있는데, 해당 가이드를 읽고 배운것들 중 일부와 Node.js의 이벤트 루프에 대해 개인적으로 알고 있던 것들을 간단하게 정리하려고 한다.

    Event Loop

    자바스크립트는 싱글 쓰레드 언어이기 때문에 여러 작업을 작업을 동시에 처리할 수가 없다. 이런 문제를 해결하기 위해 자바스크립트 런타임 환경은 이벤트 루프를 필요로 한다.

    서버사이드 Node.js 어플리케이션에서 클라이언트의 요청이 들어오면 이벤트 루프는 우리가 작성한 콜백(1)을 호출한다. 그리고 자바스크립트 코드를 실행하는 메인 쓰레드는 우리가 작성한 로직에 따라 요청의 처리를 시작한다.

    만약 비동기 작업이 필요하다면 비동기 작업을 실행하고, 비동기 작업이 완료되었을 때 이벤트 루프는 다시 우리가 작성한 콜백(2)을 호출한다.

    app.get('/:filepath', (req, res) => { // <= (1)번 콜백
      const filepath = req.params.filepath;
      fs.readFile(filepath, (error, data) => { // <= (2)번 콜백
        res.end(data);
      });
    });
    이 예제에서 실제로 우리가 작성된 콜백이 실행되기까지는 내부적으로 여러 단계가 이루어진다.

    카운터에 한 명의 직원만 있는 카페를 예로 들어보자. 이 직원은 고객의 주문이 들어오면 주문을 접수하고, 커피가 나오면 고객에게 커피를 전달해주는 일을 한다.

    이 때, 고객이 들어왔을 때 주문을 받는 직원을 호출하여 주문을 접수하고(app.get의 callback), 커피가 나왔을 때 직원을 호출하여 주문의 완료를 처리(fs.readFile의 callback) 하는것이 이벤트 루프이다.

    우리가 작성하는 프로그램이 곧 주문을 받고 처리하는 직원이 될 것이다. 하지만 예시에서 주문을 받는 직원은 직접 커피 제조를 하지 않는다.

    해당 작업은 시간이 오래 걸리는 작업이고, 주문을 받는 직원이 시간이 오래 걸리는 작업을 하게 되면 주문을 받고 처리하는 데에 지장이 생기기 때문이다.(메인 쓰레드가 블로킹 되는 현상)

    그렇다면 시간이 오래 걸리는 작업은 누가 해주는 것일까? 바로 Node.js의 Worker Pool이다.

    이벤트 루프에 대한 자세한 정보는 이 게시물에서 확인할 수 있다.
    Node.js는 이벤트 루프를 구현하기 위해 libuv라는 오픈소스를 이용한다.

    Worker Pool

    Worker Pool이란 여러 작업들을 처리할 워커 쓰레드들을 보관해놓는 곳이다. 쓰레드들이 보관되어있는 곳이라는 뜻인 쓰레드 풀(Thread Pool)이라고도 불린다.

    libuv는 비동기 작업을 실행하기 위해 운영체제의 비동기 API를 사용하지만 운영체제가 비동기 API를 제공하지 않는 작업은 워커 풀의 워커 쓰레드를 이용해서 작업을 처리한다.

    워커 쓰레드가 처리하는 작업들은 다음과 같다.

     Network I/O는 운영체제의 비동기 API를 사용한다.

    이 작업들은 메인 쓰레드에서 실행을 하기에는 작업시간이 긴 CPU 집약적인 작업이거나 대기 시간이 긴 I/O 작업이기 때문에 워커 쓰레드가 작업을 하게 된다.

    Node.js는 기본적으로 Worker pool에 4개의 쓰레드를 생성해서 보관해 놓는다.

    이것은 Node.js 프로그램을 실행하기 전에 UV_THREADPOOL_SIZE환경변수 값을 설정해서 풀의 크기를 최대 128까지 조정할 수 있다.

    Worker pool의 크기는 동시에 처리할 수 있는 작업의 수와 같기 때문에, CPU 코어 수에 따라서 Worker pool의 크기를 유연하게 설정하는 것이 Node.js 어플리케이션의 성능을 높이는 방법이 될 수 있다.

    위에서 나열한 4개의 모듈들은 Blocking API Non-Blocking API를 둘 다 제공하는데 두 API에는 어떤 차이가 있을까?

    Blocking과 Non-Blocking

    앞에서 예시로 든 카페에서 주문을 받고 주문을 처리하는 직원 중 서로 다른 유형의 직원이 있다.

    • A 직원은 주문을 받고 커피가 나올 때까지 기다리다가 커피가 나오면 주문을 완료 처리하고 다음 주문을 받는다.
    • B 직원은 주문을 받고 커피가 나올 때까지 기다리지 않고 다음 주문을 계속해서 받다가 커피가 나오면 그때그때 고객들의 주문을 완료 처리한다.

    일하는 방식의 차이로 인해 B 직원이 A 직원에 비해 같은 시간에 처리할 수 있는 주문 수는 훨씬 더 많을 것이다.

    이때, A직원의 일처리를 Blocking 방식이라고 하고, Node.js의 fs.readFileSync()같은 동기(Synchronous)적으로 실행되는 API를 통해 다음 코드처럼 나타낼 수 있다.

    const fs = require('fs');
    
    const dataA = fs.readFileSync('a.txt');
    // data로 뭔가를 처리함
    const dataB = fs.readFileSync('b.txt');
    // data로 뭔가를 처리함
    const dataC = fs.readFileSync('c.txt');
    // data로 뭔가를 처리함
    
    // ... 이후에 더 많은 로직들

    이 프로그램은 동기적으로 파일의 내용을 읽어오는 API를 사용해서 a.txt 파일에서 데이터를 읽어올 때까지 기다렸다가 처리를 하고, b.txt, c.txt 파일을 처리하는 순서대로 코드가 실행된다.

    반면에 B직원의 일처리는 Non-Blocking 방식이라고 하며, fs.readFile() 같은 콜백 방식이나 fs.promise.readFile() 같은 Promise 기반의 비동기(Asynchronous)적으로 실행되는 API를 사용해 다음 코드처럼 나타낼 수 있다.

    const fs = require('fs');
    
    fs.promises.readFile('a.txt').then(dataA => {
      // data로 뭔가를 처리함
    });
    fs.promises.readFile('b.txt').then(dataB => {
      // data로 뭔가를 처리함
    });
    fs.promises.readFile('c.txt').then(dataC => {
      // data로 뭔가를 처리함
    });
    
    // ... 이후에 더 많은 로직들

    a.txt, b.txt, c.txt 파일들의 읽기가 끝나는 것을 기다리지 않고 이후의 로직들을 처리한 다음에 파일 읽기가 끝나는 순서대로 데이터를 처리한다.

    이때 파일 읽기가 완료되는 순서는 a, b, c가 아닐 수도 있다. 그렇기 때문에 콜백이나 Promise 기반의 코드는 그 흐름을 파악하기가 동기 함수를 썼을 때보다는 어려운 것이 사실이다.(ES6이후에는 async/await 구문을 이용하면 해결이 된다)

    하지만 동기식 API와 비동기 API의 차이에 대해서 인지하고, 서버 사이드 어플리케이션에서는 블로킹 함수의 사용을 지양해야 한다.

    Conclusion

    • 메인 쓰레드를 블로킹하는 코드를 작성하지 말자.
    • Node.js의 동기 API를 사용하지 말고 비동기 API를 사용하자.

    References

     

    반응형

    'Node.js' 카테고리의 다른 글

    [Node.js] Readable Stream을 다루는 방법  (0) 2021.11.29

    댓글

Designed by Tistory.