
Java Spring MVC는 요청이 들어올 때마다 스레드 풀(Thread Pool)에서 스레드를 하나씩 꺼내 배정한다.
하지만 Node.js는 단 하나의 메인 스레드로 수천, 수만 개의 동시 접속을 처리한다. 이것이 가능한 이유는 바로 이벤트 루프(Event Loop) 기반의 비동기 I/O 덕분이다.
이벤트 루프를 이해하려면 단순히 '루프'만 보는 게 아니라, 주변 요소들과의 협업 관계를 봐야 한다.
Java의 PriorityQueue처럼, Node.js의 큐에도 우선순위가 있다. 이 개념을 모르면 비동기 코드의 실행 순서를 예측할 수 없다.
Promise.then, process.nextTicksetTimeout, setInterval, I/O 작업function first() {
second();
console.log('첫 번째');
}
function second() {
third();
console.log('두 번째');
}
function third() {
console.log('세 번째');
}
first();
위 코드를 실행하게 되면 결과는 다음과 같다.
세 번째
두 번째
첫 번째
first 함수가 제일 먼저 호출되고, 그 안의 second 함수가 호출된 뒤, 마지막으로 third 함수가 호출된다.
이번에는 특정 밀리초 이후에 코드를 실행하는 setTimeout을 사용해보면
function run() {
console.log('3초 후 실행');
}
console.log('시작');
setTimeout(run, 3000);
console.log('끝');
결과는 다음과 같다.
시작
끝
3초 후 실행
만약 호출 스택에 함수가 너무 많이 들어 있으면 3초가 지난 후에도 run 함수가 실행되지 않을 수 있다. 이벤트 루프는 호출 스택이 비어 있을 때에만 태스크 큐에 있는 run 함수를 호출 스택으로 가져오기 때문이다. 이것이 setTimeout의 시간이 정확하지 않을 수도 있는 이유이다.
Java에서는 복잡한 이미지 연산이나 암호화 작업을 수행할 때 스레드 하나만 느려질 뿐 다른 요청은 안전하다. 하지만 Node.js는 다르다.
// 드라마켓 대본 파일 수만 개를 동기적으로 처리하는 나쁜 예
scripts.forEach(script => {
const encrypted = encryptComplexSync(script.content); // Call Stack 점유!
});
위 코드가 Call Stack에서 실행되는 동안, 다른 사용자가 보내는 모든 로그인, 조회, 결제 요청은 Task Queue에서 마냥 대기하게 된다. 서비스가 마비되는 것이다.
Worker Threads를 사용하거나, 비동기 API로 쪼개어 이벤트 루프가 숨 쉴 틈을 주어야 한다."Node.js는 싱글 스레드인데 어떻게 파일 I/O를 비동기로 하나요?"라는 질문에 대한 답이다.
Node.js 내부의 Libuv 라이브러리는 내부적으로 4개(기본값)의 스레드를 가진 스레드 풀을 운영한다. DB 조회나 파일 읽기 요청이 들어오면 메인 스레드는 Libuv에게 일을 시키고 본인은 다시 루프로 돌아간다. 일이 끝나면 Libuv가 결과물을 Task Queue에 넣어주는 방식이다.