[Node.js] Blocking & Non-Blocking

본 글은 Node.js 공식 가이드 문서 중 ‘블로킹과 논블로킹 살펴보기’를 참고하여 작성되었습니다.

1. 블로킹과 논블로킹


1.1 블로킹

블로킹은 Node.js 프로세스에서 추가적인 JavaScript의 실행을 위해 JavaScript가 아닌 작업이 완료될 때까지 기다려야만 하는 상황을 의미한다. Node.js의 이벤트 루프가 블로킹 작업을 하는 동안 JavaScript 실행을 계속할 수 없기 때문에 발생한다.

Node.js에서 I/O 등 JavaScript 이외의 작업을 기다리는게 아닌 CPU 집약적인 작업 때문에 나쁜 성능을 보여주는 JavaScript는 블로킹으로 분류하지 않는다. libuv를 사용하는 Node.js 표준 라이브러리의 동기 메서드가 대표적인 블로킹 작업이다.

Node.js 표준 라이브러리의 일부 I/O 메소드 중 Sync 접미어가 붙은 메소드들이 블로킹 메소드들이다.

1.2 논블로킹

논블로킹은 블로킹과 반대 상황이다. JavaScript 외의 작업이 실행될 때, 해당 작업을 완료되기까지 기다리지 않고 다음 JavaSccript 코드를 실행하는 것이다. 블로킹 작업과 반대로 JavaScript 외의 I/O 작업 등이 실행될 때도, Node.js의 이벤트 루프가 계속해서 JavaScript를 실행할 수 있다.

Node.js 표준 라이브러리의 모든 I/O 메소드는 논블로킹인 비동기 방식을 제공한다. 이 메소드들은 콜백함수를 인자로 받아, 메소드 기능이 완료되면 이 함수를 실행한다.

2. 동기와 비동기


2.1 동기

동기(Asynchronous)는 일련의 작업들이 순서에 따라 실행되는 것이다. 이 설명에서 말하는 순서는 작업들의 실행, 종료 혹은 요청, 응답 관점에서 생각해볼 수 있다.

예제 2.1.1 동기

1
2
console.log('start'); // a
console.log('end'); // b

위의 JavaScript 코드를 실행하면 a, b 순으로 실행된다. 또한, start 문자열이 출력된 후, end 문자열이 출력된다. 이처럼 a의 실행이 끝나고 종료됨가 동시에 b가 실행되는 이런 상황을 동기라고 볼 수 있다.

2.2 비동기

비동기(Synchronous)는 일련의 작업들에서 완료 순서에 상관없이 실행되는 것이다.

예제 2.2.1 비동기

1
2
setTimeout(() => console.log('start'), 1000); // a
console.log('end'); // b

비동기 예제에서도 a, b 순으로 코드가 실행된다. 하지만 동기 예제와는 달리 end, start 순으로 출력될 것이다. JavaScript의 setTimeout() 는 논블로킹 메소드로 두번째 인자로 전달받은 시간(ms)이 흐른 후, 첫번째 인자인 콜백함수를 실행한다. 논리적으로 생각해보면 1초가 지나고 start 문자열이 출력된 후, end 문자열이 출력될 것 같지만 setTimeout()은 논블로킹 메소드이기 때문에 이 메소드의 종료와는 무관한 순서로, 1초가 흐르는 동안 이벤트 루푸는 다음 코드를 계속 실행한다.

2.3 동기 vs 비동기

예제 2.3.1 동기 코드 예제

1
2
const fs = require('fs');
const data = fs.readFileSync('/file.md');

예제 2.3.2 비동기 코드 예제

1
2
3
4
const fs = require('fs');
fs.readFile('/file.md', (err, data) => {
if (err) throw err;
});

첫 번째 예제에서는 fs.readFileSync()라는 블로킹 메소드로 작성된 동기 예제이고, 두 번째 예제는 fs.readFile()이라는 논블로킹 메소드로 작성된 비동기 예제이다.

블로킹 메소드는 오류가 발생하면 반드시 처리해줘야 하지만 논블로킹 메소드는 발생한 오류가 콜백함수의 첫 번째 인자로 전달되기 때문에 오류를 throw할 것인지 여부는 코드 작성자에게 달려있다.

또 동기 예제에서는 블로킹 메소드가 완료되어야만 다음 JavaScript를 실행할 수 있지만, 비동기 예제에서는 논블로킹 메소드가 완료되기를 기다리지 않고, 다음 JavaScript를 실행한다는 차이점도 있다. 따라서 비동기로 코드를 작성하여 throughput 을 높일 수 있다는 것이 Node.js의 특징이다.

3. 동시성


Node.js에서는 싱글 스레드로 JavaScript가 실행되므로 다른 작업이 완료된 후, JavaScript 콜백함수를 실행하는 이벤트 루프의 기능을 동시성이라고 한다. 동시에 실행되어야 하는 모든 코드는 I/O 등 JavaScript 외의 작업이 일어나는 동안 이벤트 루프가 계속 실행될 수 있도록 해야 한다.

예를 들어, Node.js로 작성된 웹 어플리케이션에서 요청이 완료되기까지 총 50ms의 시간이 소요되고, 그 중 45ms가 비동기로 실행될 수 있는 데이터베이스 I/O인 상황을 가정해보자. 비동기 작업이라면 요청마다 45ms 동안 다른 요청을 처리할 수 있다. 이처럼 논블로킹 메소드를 사용하면 블로킹 메소드를 사용할 때에 비해 확연히 높은 성능을 기대할 수 있다.

이런 Node.js의 이벤트 루프는 동시 작업을 다루기 위해 자식 스레드를 여러 개 생성하는 다른 언어 모델과 차이점이 있다.

4. 논블로킹 코드 작성 시의 주의 사항


예제 4.1 실행 순서를 보장할 수 없는 코드

1
2
3
4
5
6
const fs = require('fs');
fs.readFile('/file.md', (err, data) => {
if (err) throw err;
console.log(data);
});
fs.unlinkSync('/file.md');

위의 코드 예제에서 fs.readFile() 메소드는 fs.unlinkSync()보다 먼저 실행될 수 있기 때문에 완전히 논블로킹 코드로 작성하여 올바른 순서를 보장해야 한다.

예제 4.2 실행 순서를 보장하는 논블로킹 코드

1
2
3
4
5
6
7
8
const fs = require('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() 메소드를 논블로킹으로 호출하게 함으로써 올바른 실행 순서를 보장할 수 있다.

참고

Author

Jaeyun Cha

Posted on

2020-12-17

Updated on

2023-04-11

Licensed under

댓글