Skip to content

블로킹 vs 논블로킹 개요

이 개요는 Node.js에서 블로킹 호출과 논블로킹 호출의 차이점을 다룹니다. 이 개요에서는 이벤트 루프와 libuv를 언급하지만 해당 주제에 대한 사전 지식은 필요하지 않습니다. 독자는 JavaScript 언어와 Node.js 콜백 패턴에 대한 기본적인 이해를 가지고 있다고 가정합니다.

INFO

"I/O"는 주로 libuv에서 지원하는 시스템의 디스크 및 네트워크와의 상호 작용을 나타냅니다.

블로킹

블로킹은 Node.js 프로세스에서 추가 JavaScript 실행이 비 JavaScript 작업이 완료될 때까지 기다려야 하는 경우입니다. 이는 블로킹 작업이 진행되는 동안 이벤트 루프가 JavaScript 실행을 계속할 수 없기 때문에 발생합니다.

Node.js에서 I/O와 같은 비 JavaScript 작업을 기다리는 것보다 CPU 집약적이어서 성능이 저하되는 JavaScript는 일반적으로 블로킹이라고 하지 않습니다. libuv를 사용하는 Node.js 표준 라이브러리의 동기 메서드는 가장 일반적으로 사용되는 블로킹 작업입니다. 네이티브 모듈에도 블로킹 메서드가 있을 수 있습니다.

Node.js 표준 라이브러리의 모든 I/O 메서드는 논블로킹이고 콜백 함수를 허용하는 비동기 버전을 제공합니다. 일부 메서드에는 Sync로 끝나는 이름을 가진 블로킹 대응 메서드도 있습니다.

코드 비교

블로킹 메서드는 동기적으로 실행되고 논블로킹 메서드는 비동기적으로 실행됩니다.

파일 시스템 모듈을 예로 들어, 다음은 동기적 파일 읽기입니다.

js
const fs = require('node:fs')
const data = fs.readFileSync('/file.md') // 파일이 읽힐 때까지 여기서 블로킹

다음은 동일한 비동기적 예입니다.

js
const fs = require('node:fs')
fs.readFile('/file.md', (err, data) => {
  if (err) throw err
})

첫 번째 예는 두 번째 예보다 간단해 보이지만 전체 파일을 읽을 때까지 두 번째 줄이 추가 JavaScript 실행을 블로킹한다는 단점이 있습니다. 동기 버전에서 오류가 발생하면 catch해야 프로세스가 충돌하지 않습니다. 비동기 버전에서는 표시된 대로 오류를 발생시킬지 여부를 작성자가 결정합니다.

예를 조금 더 확장해 보겠습니다.

js
const fs = require('node:fs')
const data = fs.readFileSync('/file.md') // 파일이 읽힐 때까지 여기서 블로킹
console.log(data)
moreWork() // console.log 이후에 실행됩니다.

다음은 유사하지만 동일하지 않은 비동기 예입니다.

js
const fs = require('node:fs')
fs.readFile('/file.md', (err, data) => {
  if (err) throw err
  console.log(data)
})
moreWork() // console.log 이전에 실행됩니다.

위의 첫 번째 예에서는 console.logmoreWork() 이전에 호출됩니다. 두 번째 예에서는 fs.readFile()논블로킹이므로 JavaScript 실행이 계속될 수 있고 moreWork()가 먼저 호출됩니다. 파일 읽기가 완료될 때까지 기다리지 않고 moreWork()를 실행할 수 있는 기능은 더 높은 처리량을 가능하게 하는 핵심 설계 선택입니다.

동시성과 처리량

Node.js에서의 JavaScript 실행은 단일 스레드이므로, 동시성은 다른 작업을 완료한 후 JavaScript 콜백 함수를 실행하는 이벤트 루프의 능력을 의미합니다. 동시적으로 실행될 것으로 예상되는 코드는 I/O와 같은 비 JavaScript 작업이 발생하는 동안 이벤트 루프가 계속 실행되도록 허용해야 합니다.

예를 들어, 웹 서버에 대한 각 요청을 완료하는 데 50ms가 걸리고 해당 50ms 중 45ms가 비동기적으로 수행할 수 있는 데이터베이스 I/O인 경우를 고려해 보겠습니다. 논블로킹 비동기 작업을 선택하면 요청당 45ms를 확보하여 다른 요청을 처리할 수 있습니다. 이는 블로킹 메서드 대신 논블로킹 메서드를 사용하는 것만으로도 용량에 큰 차이가 있습니다.

이벤트 루프는 동시 작업을 처리하기 위해 추가 스레드를 생성할 수 있는 다른 많은 언어의 모델과는 다릅니다.

블로킹 코드와 논블로킹 코드 혼합의 위험성

I/O를 처리할 때 피해야 할 몇 가지 패턴이 있습니다. 예를 들어 보겠습니다.

js
const fs = require('node:fs')
fs.readFile('/file.md', (err, data) => {
  if (err) throw err
  console.log(data)
})
fs.unlinkSync('/file.md')

위의 예에서 fs.unlinkSync()fs.readFile()보다 먼저 실행될 가능성이 높으며, 이는 실제로 읽기 전에 file.md를 삭제합니다. 이를 완전히 논블로킹 방식으로 작성하고 올바른 순서로 실행되도록 보장하는 더 나은 방법은 다음과 같습니다.

js
const fs = require('node:fs')
fs.readFile('/file.md', (readFileErr, data) => {
  if (readFileErr) throw readFileErr
  console.log(data)
  fs.unlink('/file.md', unlinkErr => {
    if (unlinkErr) throw unlinkErr
  })
})

위의 코드는 fs.readFile()의 콜백 내에 fs.unlink()에 대한 논블로킹 호출을 배치하여 작업의 올바른 순서를 보장합니다.