groom 무료강좌 일부를 참고했다.
지금까지 대부분의 애플리케이션은 Blocking I/O를 사용하였고, 이 때문에 멀티 쓰레드를 사용할 수밖에 없었다. 멀티 쓰레드는 개발자 입장에서는 직관적이고 멀티 태스킹을 위해서는 어쩔 수 없는 선택이기도 하지만, 네트워크에서 동시에 대규모 요청을 동시에 처리하는 데에는 부적절하다고 한다.
일반적인 애플리케이션들은 대부분 Blocking I/O를 사용한다. Blocking I/O란, "하나의 프로세스가 어떤 자원을 사용하고자 할 때 그 자원을 다른 프로세스가 점유하고 있다면, 그 프로세스가 그 자원의 사용을 끝마칠 때까지 기다려야 한다는 것"을 의미한다. 먼저 애플리케이션이 운영체제의 커널에게 파일을 읽기 위해 시스템 콜이라는 형태로 요청을 보낸다. 커널은 파일을 읽기 위한 동작을 수행하기 시작하고 애플리케이션은 커널이 파일을 다 읽을 때까지 기다려야 한다. 일반적으로 이 상태를 애플리케이션이 Blocked 되었다고 표현하며 이 시간 동안 실제로 애플리케이션은 아무것도 하지 않는 상태가 된다.
웹서버와 같이 다수의 요청이 들어오게 된다면, 멀티 스레드라는 개념을 이용해서 여러 요청을 처리해야할 수 밖에 없다. 멀티 쓰레드는 말 그대로 쓰레드 여러 개가 동시에 실행되어 요청을 처리한다는 개념인데, CPU의 시분할이라는 개념으로 설명할 수 있다. CPU를 여러 프로세스 또는 쓰레드가 시간을 나누어 동작하도록 함으로써 마치 CPU를 공유하여 사용하는 것과 같은 효과를 낸다. 가장 위의 그림은 멀티 쓰레드가 아닌 싱글 쓰레드다. 먼저 들어온 요청을 처리하는 것이다.
아래 2개의 그림은 멀티 쓰레드라고 할 수 있다. 먼저 들어온 순이 아니라, 일정 시간단위로 쪼개는 스케쥴링을 했을 때 작업양이 적은 쓰레드가 먼저 응답되는 것이다.
nodejs는 이러한 문제들을 싱글 쓰레드와 이벤트 기반의 비동기 I/O 처리로 해결하고 그 성능을 끌어올릴 수 있도록 하는 비동기 프로그래밍 모델을 제공해주고 있다.
싱글 쓰레드를 가진 노드는 I/O 작업이 시작되면 I/O 작업 처리에 대한 응답을 기다리지 않고, 바로 다음 작업을 실행한다. 대신 I/O 작업이 종료되면 이벤트를 발생시키고, 이 이벤트는 해당 프로세스의 이벤트 큐에 등록한다. 노드로 개발된 프로세스는 이 이벤트 큐에 등록된 새로운 이벤트를 감지하여, 해당 이벤트 시 수행하여야 할 작업을 실행하게 된다.
이벤트 루프라는 것은 작업을 요청하면서, 그 작업이 완료됐을 때 어떤 작업을 진행할 것인지에 대한 콜백 함수를 지정해둔다. 그래서 동작이 완료됐을 때, 콜백함수를 실행하는 동작 방식을 말한다.
예를 들어, 클라이언트가 웹서버에 http 형식으로 요청을 하면 서버에서는 이벤트 루프가 계속 돌고 있다가 이를 감지하고 알맞은 작업을 워커스레드를 생성하여 실행한다. 이 때, 이벤트 루프는 해당 워커스레드가 작업을 마친 뒤 그 결과와 함께 응답할 때까지 기다리는 것이 아니라 바로 루프로 복귀하여 다른 요청을 기다리게 된다.
즉, 이벤트 루프는 어떤 요청이 발생하면 그 작업에 대한 스레드 실행만을 일으킬 뿐이다.
이후 작업을 할당받았던 해당 쓰레드가 모든 작업을 마치면 미리 전달받은 콜백 함수를 실행하도록 이벤트 루프로 응답하게 되며 이벤트 루프는 이것을 실행하여 클라이언트에게 결과를 응답해준다.