[Node.js] Blocking VS Non-Blocking

in #nodejs6 years ago

본 블로그 내용은 nodejs.org의 내용을 읽고 조금 더 간략하게 번역한 글 입니다.
오역이 많이 있을 수 있습니다.
잘못된 부분을 댓글로 알려주시면 수정하도록 하겠습니다.

Blocking VS Non-Blocking 개요

Node.js 에서 BlockingNon-Blocking 호출에 대해 알아보자.
이 글은 event loop와 libuv를 인용하지만 굳이 몰라도 된다.
이 글을 읽는이들은 기본적인 JavaScript의 이해와 Node.js의 callback 패턴을 알고 있다고 가정한다.

"I/O 는 주로 lilbuv에 의해 지원되는 시스템의 디스크와 네트워크의 상호작용을 말한다.

Blocking

Blocking 은 Node.js 안에서 JavaScript가 실행할 때 먼저 실행한 JavaScript가 완료되기 전까지 다음 JavaScript를 실행하지 않는 것을 의미한다. 그 이유는 Blocking이 발생하는 동안 event loop는 다른 JavaScript를 실행하지 못하기 때문이다.

Node.js 에서는 CPU를 주로 사용하는것 보다 비-JavaScript의 동작을 기다리는것 때문에 성능이 떨어진다. 때문에 일반적으로는 Blocking을 지향하지 않는다. Node.js에서 사용되는 동기적인 기본 라이브러리들은 libuv를 사용하며 보통은 Blocking 형식으로 동작한다. Native 모듈들 또한 Blocking 메소드들을 가지고 있다.

Node.js 기본 라이브러리 중 I/O 관련 모든 메소드들은 비동기 버전의 Non-Blocking 메소드를 지원하는데 이놈들은 callback 함수를 매개변수로 받는다. 몇몇 메소드들은 Blocking 으로 동작하는데 이런 애들은 메소드 이름에 sync 라고 붙어 있다.

정리. Node.js 의 메소드들 중 기본 메소드는 비동기로 동작하며, 동기일 경우 sync가 붙는다.

코드 비교

Blocking 메소드는 동기적으로 실행하고, Non-Blocking 메소드는 비동기적으로 실행한다.

fs 모듈을 예로 먼저 동기적인 파일 읽기 코드를 보자.

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



이번에는 똑같은 비동기 예제를 보자.

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



처음 예제가 두 번째 예제보다 간단하지만 첫 번째 예제는 전체 파일을 읽기 전까지 다른 JavaScript들이 Blocking 되므로 별로다. 중요한건 첫 번째에서 에러가 발생할 경우 따로 처리를 해야 하거나 아니라면 프로그램이 뻗어버릴 것이다. 반면 두 번째 예제는 에러를 처리해도 되고 안해도 된다. 즉, 사용자 몫이다.

자, 예제를 조금 더 확장해보자.

const fs = require('fs`);
const data = fs.readFileSync('/file.md'); // blocks here until file is read
console.log(data);
// moreWork(); will run after console.log



그리고 비슷하게 하지만 비동기 예제를 보자.

const fs = require('fs');
fs.readFile('/file.md', (err, data) => {
    if (err) throw err;
    console.log(data);
});
// moreWork(); will run before console.log



위에서 첫 번째 예제는 moreWork()가 호출되기 전에 먼저 console.log가 호출될 것이다. 두 번째 예제의 fs.readFile()Non-Blocking 이므로 fs.readFile() 호출 이후 계속해서 다음 JavaScript들을 실행할 것이고, console.log 보다도 먼저 moreWork()가 호출될 것이다. 이처럼 파일 읽기를 끝내기도 전에 moreWork()를 실행하는 것이 높은 퍼포먼스를 내는 핵심 이다.

동시성과 생산성

Node.js에서 JavaScript를 실행하는 것은 단일 쓰레드이다. 때문에 동시성은 event loop의 수용능력과 관련이 있으며 이놈은 다른 작업이 완료된 후에 JavaScript callback 함수를 실행한다. 동시성 방식으로 동작하는 코드들은 event loop에서 지속적으로 수행할 수 있어야 한다. I/O 같은 비-JavaScript 동작들 말이다.

한 가지 예로, 웹 서버로 각각의 요청이 처리되는데 50ms 가 소요되며, 그 중에 45ms는 database의 I/O가 비동기적으로 처리된다고 가정해보자. 그럼 45ms 만큼 다른 요청을 처리하므로 개이득 아니 굉장히 이득이다. 이것은 Blocking 방식과 Non-Blocking 방식의 굉장한 차이점이다.

Event loop는 다른 많은 언어들이 동시성 작업에 쓰레드를 사용하는 것과는 다른 방식이다.

Blocking과 Non-Blocking을 같이 사용할 경우의 위험성

다음은 I/O를 처리할 때 하면 안되는 패턴이다.
예제를 보자.

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



위 예제에서 fs.unlinkSync()fs.readFile() 이 수행되기 전에 수행되면서 file.md 파일을 읽기 전에 지워버린다. 이런 경우 모든 동작을 Non-Blocking 으로 작성하는게 최고다. 아래처럼 작성하자.

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() 안에서 Non-Blocking 메소드인 fs.unlink() 를 호출하는것이 정확한 순서를 보장한다.

이상한 내용이 있으시면 언제든 댓글에 달아주시기 바랍니다.

기부
bitcoin: 18pTjQxpmtMokQLNvvfWAa3J7GW7KiscKR
ethereum: 0xaED2A76Cd18228c4cFf8eCcD95F8414cfD6df317
litecoin: LVHMEHUzSF3DtH58PLrZ7CFjKDAF4wh8tM
EOS: 0xaED2A76Cd18228c4cFf8eCcD95F8414cfD6df317