배우는거끄적이기

[번외]JS로 Node.js를 만든 이유

PARKpatchnotes 2025. 9. 5. 13:06

왜 JavaScript로 서버를 만들었나? Node.js와 전통적 서버 방식 비교

브라우저에서만 사용되던 JavaScript가 어떻게 서버 개발의 판도를 바꾸었을까? Node.js의 등장은 "서버는 원래 이렇게 만드는 것"이라는 고정관념에 대한 도전이었다. 이 글에서는 Node.js가 탄생한 배경과 그 핵심 철학, 그리고 전통적인 서버 방식과의 차이점을 알아본다.


1. 기존 서버의 한계: '기다림의 비효율', 블로킹 I/O

Node.js가 등장하기 전, Java, PHP, Ruby 등으로 만들어진 대부분의 서버는 '요청 하나당 스레드(Thread) 하나' 방식으로 동작했다.

  • 스레드(Thread): 서버가 처리하는 작업의 단위. '일꾼'에 비유할 수 있다.

이 방식의 가장 큰 문제는 I/O(입출력) 작업에서의 '기다림', 즉 블로킹(Blocking)이다.

식당 비유: 스레드-퍼-리퀘스트(Thread-per-Request) 방식

  1. 손님(요청)이 오면, 종업원(스레드) 한 명이 그 손님에게 전담 배정된다.
  2. 손님이 스테이크(DB 조회, 파일 읽기 등)를 주문한다.
  3. 담당 종업원은 주방에서 요리가 끝날 때까지, 다른 아무 일도 하지 않고 그 앞에서 가만히 기다린다. (블로킹 발생)
  4. 만약 모든 종업원이 각자 다른 손님의 요리를 기다리고 있다면, 새로 온 손님은 종업원이 올 때까지 하염없이 기다려야 한다.

이처럼 작업이 완료될 때까지 스레드가 멈춰서 다른 일을 처리하지 못하는 것을 블로킹 I/O라고 한다. 이는 서버 자원의 심각한 낭비로 이어진다.

2. Node.js의 해결책: '기다림 없는 일처리', 논블로킹 I/O

Node.js를 만든 라이언 달(Ryan Dahl)은 이 '기다림의 비효율'을 해결하고자 했다. 그는 이벤트 루프(Event Loop)를 기반으로 한 논블로킹(Non-Blocking) I/O 모델을 제시했다.

식당 비유: Node.js의 이벤트 기반 방식

  1. 종업원(싱글 스레드)은 단 한 명이다.
  2. 이 종업원은 모든 테이블을 돌며 주문(요청)을 받은 뒤, 즉시 주방에 전달한다.
  3. 요리를 기다리지 않고, 바로 다음 손님의 주문을 받으러 간다. (논블로킹)
  4. 잠시 후, 주방에서 "1번 테이블 요리 완료!"라는 신호(이벤트)를 보내면, 그때 가서 음식을 가져다준다.

이처럼 I/O 작업을 요청한 뒤 완료될 때까지 기다리지 않고, 작업이 끝나면 신호를 받아 처리하는 방식이 논블로킹 I/O이다. 적은 수의 스레드(일꾼)로 훨씬 많은 요청(손님)을 효율적으로 처리할 수 있다.

3. 왜 하필 JavaScript였는가?

라이언 달이 이러한 논블로킹 모델을 구현하려 할 때, JavaScript는 가장 이상적인 파트너였다.

  1. 태생부터 이벤트 기반 언어: 브라우저에서 '클릭하면', '마우스를 올리면' 같은 이벤트에 반응하도록 만들어진 JavaScript의 특성은 Node.js의 이벤트 루프와 완벽한 궁합을 보였다. 
  • Node.js의 이벤트 루프는 파일 읽기, 네트워크 요청 등 시간이 오래 걸리는 작업을 바로 요청한 뒤, 완료된 작업의 결과를 이벤트 큐에 등록된 콜백을 통해 나중에 처리한다.
  • 즉, 비동기 작업을 효율적으로 처리하여 높은 동시성과 빠른 응답성을 제공한다
  1. 강력한 V8 엔진의 등장: 구글 크롬의 V8 엔진이 JavaScript를 기계어로 컴파일하여 C++에 버금가는 속도를 내주었기에, 서버 언어로서 충분한 성능을 확보할 수 있었다.
  2. 풀스택(Full-Stack) 개발의 가능성: 프론트엔드 개발에 익숙한 개발자들이 별도의 언어 학습 없이 서버까지 개발할 수 있게 되어 생산성이 폭발적으로 증가했다.
  3. JSON과의 완벽한 호환성: 데이터 교환 표준인 JSON(JavaScript Object Notation)은 이름 그대로 JavaScript의 일부이다. 별도 변환 과정 없이 데이터를 다룰 수 있어 매우 편리하다.

4. '스레드-퍼-리퀘스트' 방식은 단점만 있는가?

그렇다면 전통적인 '요청 하나당 스레드 하나' 방식은 버려져야 할 구시대의 유물일까? 절대 그렇지 않다. 이 방식 역시 명확한 장점을 가지고 있다.

  1. 직관적인 프로그래밍 모델: 코드가 위에서 아래로 순서대로 실행되므로 흐름을 이해하기 매우 쉽다. 비동기 처리의 복잡성을 고민할 필요가 없다.
  2. CPU 집약적 작업에 유리: 동영상 인코딩, 복잡한 연산 등 CPU를 많이 사용하는 작업을 처리할 때, 여러 스레드가 각 CPU 코어를 효율적으로 활용할 수 있다. (싱글 스레드인 Node.js는 이런 작업에 취약하다.)
  3. 안정성과 격리: 하나의 스레드에서 에러가 발생해도 다른 스레드에 영향을 주지 않아 서버 전체의 안정성이 높다.
  4. 성숙한 생태계: 수십 년간 발전해 온 Java, C# 등은 방대한 라이브러리와 검증된 솔루션, 풍부한 자료를 보유하고 있다.

5. 코드로 보는 동작 방식의 차이

동일한 로직을 Java와 Node.js로 작성했을 때, 코드의 동작 방식은 근본적으로 다르다.

Java: 순서대로 기다리는 '동기 블로킹'

public void handleRequest(Request req, Response res) {
    // 1. DB에서 user를 찾는다 (결과가 올 때까지 여기서 실행이 멈춤)
    User user = db.findUser(req.getUserId());

    // 2. user 정보로 posts를 찾는다 (결과가 올 때까지 또 멈춤)
    List<Post> posts = db.findPosts(user.getId());

    // 3. 모든 과정이 끝나면 결과를 전송한다
    res.send(renderHtml(posts));
}
  • 특징: 코드의 흐름이 직관적이다. 각 줄은 이전 줄의 작업이 완전히 끝나야만 실행된다.

JavaScript (Node.js): 요청하고 다른 일을 하는 '비동기 논블로킹'

async function handleRequest(req, res) {
    try {
        // 1. DB에 user를 요청하고, 기다리는 동안 다른 요청을 처리한다
        const user = await db.findUser(req.getUserId());

        // 2. user 정보로 posts를 요청하고, 또 다른 일을 처리한다
        const posts = await db.findPosts(user.id);

        // 3. 두 결과가 모두 준비되면 결과를 전송한다
        res.send(renderHtml(posts));
    } catch (error) {
        res.status(500).send("Server Error");
    }
}
  • 특징: await 키워드는 "이 함수의 실행은 잠시 멈추지만, 서버 전체는 멈추지 않고 다른 일을 계속한다"는 의미이다.

6. 결론

두 방식의 특징을 한눈에 비교하면 다음과 같다.

구분 스레드-퍼-리퀘스트 (Java, C# 등) 이벤트 기반 논블로킹 (Node.js)
강점 CPU 집약적 작업, 코드의 직관성, 안정성 I/O 집약적 작업, 많은 동시 요청 처리
약점 I/O 작업 시 자원 낭비, 많은 동시 요청에 불리 CPU 집약적 작업에 불리, 비동기 코드의 복잡성
어울리는 서비스 복잡한 비즈니스 로직, 데이터 분석, 대규모 엔터프라이즈 시스템 실시간 채팅, API 서버, 소셜 미디어, 스트리밍 서비스

결론적으로, Node.js는 "서버의 비효율적인 I/O 대기 시간을 없애기 위해, 이벤트 기반으로 설계된 JavaScript를 강력한 V8 엔진 위에 올려 탄생시킨 런타임"이다. 어떤 기술이 절대적으로 우월하다기보다는, 만들고자 하는 서비스의 특성에 맞는 아키텍처를 선택하는 것이 가장 중요하다.