노트
- 통념적으로 Node.js 는 파일 입출력을 수행하는데 있어어 다른 언어들에 비해 이점을 갖는다고 알려져있다.
- Node.js는 모든 요청이 Non-Block 이다. 또한 비동기 처리도 당연히 지원한다. 그리고 싱글스레드 기반이며, 이벤트 루프를 통해 동시적으로 일들을 처리한다. 그리고 그건 파일입출력도 마찬가지다.
- 싱글스레드는 멀티스레드에 비해 오버헤드가 작다는 이점을 가지고 있다. 그래서 컨텍스트 스위칭이 불필요하게 발생하지 않기 때문에, 여러 파일 I/O 를 처리할 때도 더 빠르게 돌아간다. 이는 타 언어의 코루틴 작동원리와 동일하다.
- 자바와 비교해보자. 자바는 기본적으로 파일입출력이 Blocking-Sync 방식이다. 그러나 Node.js 는 그렇지 않다. 모든 작업을 이벤트 루프로 비동기하게 처리한다.
- 그러나 반대로 CPU Bound 한 작업을 처리한다면? 오히려 멀티스레드가 더 이점을 가진다. CPU 바운드 작업은 비동기적으로 처리되지 않는다. libuv의 비동기 메서드를 사용하는 것들만 비동기로 처리된다. 그냥 CPU 바운드한 작업만 처리한다면 블로킹된다.
- 따라서 이는 수행하려는 작업이 CPU Bound 인가 I/O Bound 인가에 여부에 따라 Node.js의 파일 입출력 성능에 영향을 준다고 볼 수 있다.
- CPU 바운드한 작업과 I/O 바운드 작업을 스레드로 분리해서 구현하는 방법도 있는데 이렇게 하면 더 최적화할 수 있다.
- 그래서 Node.js 에서는 Worker Thread 라는 모듈이 따로 존재한다. CPU 바운드한 작업을 Worker Thread로 위임하여 분리하면, 이벤트 루프의 블로킹을 최소화 하여 효율을 극대화 할 수 있다.
- Node.js가 파일 입출력을 비동기로 지원할 수 있는 가장 큰 이유는, Node.js 내부에서 파일 입출력을 메인 싱글스레드에서 처리하는게 아니라, 다른 스레드풀(또는 커널에)에 I/O 작업을 위임하기 떄문이다. 그리고 그러한 비동기 작업을 이벤트루프가 처리한다.
- 참고로 이 기본적으로 생성되는 스레드풀이 스레드 개수가 4개이다. 따라서 동시 다발적으로 많은 파일 I/O를 요청하면, 스레드 풀 개수를 넘어서는 작업량으로 인해 성능이 저하되는 것이다. (???, 부정확함)
- 예외적인 몇몇 경우가 있을 수 있다.
- 파일 I/O 가 그냥 엄청 많으면, 노드 내부의 파일 I/O 스레드 풀이 처리 가능한 용량을 넘어서서 오히려 성능이 안 좋아지기도 한다. 물론 이건 그냥 컴퓨팅 리소스가 적어서 발생하는 일이기도 하다.
- 다른 프로세스에서 파일 Lock 을 걸어서 접근이 안되는 경우가 있을 수도 있다.
- 노드 프로세스를 클러스터링해서 사용하는 경우에는 공유 자원에 대한 동기화 문제를 해결해야한다.
참고로 노드에서는 블로킹은 무조건 동기 이고 논블로킹은 무조건 비동기이다. 일반적으로 블로킹, 논블러킹과 동기, 비동기 개념은 구분되어서 사용되지만, 노드에서는 블로킹 작업을 동기로만 처리하고, 논블로킹 작업을 비동기로만 처리한다.
const fs = require('fs'); const data = fs.readFileSync('/file.md'); // 파일을 읽을 때까지 여기서 블로킹 됩니다. console.log('done');
여기서 위의 메서드는 동기이며 블로킹이다. 그래서 에러가 나면 노드 프로세스가 죽어버린다.
const fs = require('fs'); fs.readFile('/file.md', (err, data) => { if (err) throw err; }); console.log('done');
위의 메서드는 비동기이며 논블로킹이다. 에러 처리는 개발자에게 위임한다.
그러면 아래와 같은 async/await 은 블로킹인가? 라고 생각할 수 있겠지만 그렇지 않다.
const r = await f.readFile() // 비동기, 여전히 논블로킹 console.log(r);
await은 하나의 비동기 함수에서 병행제어를 위해 사용되는 것이다. 애초에 async/await 문법은 콜백 지옥을 대체하기 위해 사용된다. 사실 위의 내용은 아래와 동일하다. 차이점이라면 위의 내용은 지금 async 함수 내에서 호출되는 형태이고, 아래는 곧바로 비동기 함수인 readFile을 그냥 호출하는 셈이다. 그러니까 위의 내용은 또 다른 어떤 비동기 함수내에서 호출되는 중인 것의 차이라고 볼 수 있다.
f.readFile(() => { console.log(r); });
잘 생각해보면 위와 같이 async/await 함수를 호출함에도 어플리케이션이 다른 요청을 처리하지 못하는(스레드가 블로킹 된) 상태가 되지 않는다. 엄밀히 말해 블로킹의 개념은 보통 스레드가 어떤 하나의 작업 때문에 기다리는 상황을 일컫는다. 여기서 await은 비동기 함수 하나에 대해 결과를 기다리는(Promise가 resolved 될 때까지, 그전까진 unresolved promise) 형태인데, 이 상황에도 여전히 노드는 이벤트 루프 내에서 다른 비동기 작업들을 열심히 처리하고 있을 것이다.
이와 관련한 스택오버 플로우의 답변이 있다.
이보다 더 자세한 걸 알려면 Node.js 의 이벤트 루프에 대해 자세히 알아야 한다.
이는 심화편에서 다룬다.
요약
요약: 일반적으로 대부분의 경우에 있어서 Node.js 는 파일입출력에 적합하다. 물론 아닌 경우도 있다.(CPU Bound 작업이 많다면)