1편: Node.js 탄생 배경 – 서버의 역사는 어떻게 흘러왔는가
이 글은 Node.js가 왜 필요해졌는지를 이해하기 위한 출발점이다.
문법도, 프레임워크도 잠시 내려두고 서버가 요청을 처리해 온 역사부터 차근차근 따라간다.
그래야 Node.js의 설계 선택이 우연이 아니라 필연이었음을 볼 수 있다.

1. 초기 웹 서버: 요청 하나, 프로세스 하나
웹 초창기의 서버 모델은 단순했다.
- 사용자가 요청을 보낸다
- 서버는 프로세스 하나를 만들어 요청을 처리한다
- 응답을 보내면 프로세스는 종료된다
대표적인 방식이 **CGI(Common Gateway Interface)**다.
구현은 쉬웠지만 대가는 컸다.
- 요청마다 프로세스 생성 → 비용이 매우 큼
- 동시 접속자가 늘면 서버가 바로 한계에 도달
“웹은 가볍다”는 인식이 있던 시절이라 가능했던 모델이었다.

1️⃣ 그림 한 줄 요약
브라우저 → 웹 서버 → CGI 애플리케이션 → 데이터베이스/레거시 시스템
이렇게 요청이 흘러가는 초기 웹 시스템 구조를 나타낸 그림이야.
2️⃣ 왼쪽부터 흐름 설명
(1) 사용자 PC → Internet → HTTP
- 사용자는 브라우저에서 URL을 입력
- 요청은 HTTP 프로토콜을 통해 인터넷을 타고 서버로 감
- 이 시점까지는 지금과 100% 동일함
(2) Web Server 내부
Web Server 박스 안에 중요한 요소들이 모여 있어.
🔹 HTTP Server
- Apache, Nginx 같은 웹 서버
- 요청을 받아서 정적 파일인지 / 프로그램 실행인지 판단
🔹 HTML Documents
- 단순한 정적 리소스
- 요청이 /index.html 같은 거면 여기서 바로 응답
- DB 접근 없음, 로직 없음
(3) CGI Applications (핵심 포인트)
여기가 이 그림의 주인공이야.
- CGI(Common Gateway Interface)
- 웹 서버가 외부 프로그램을 실행해서 요청을 처리하는 방식. Linux/Unix 커널은 항상 실행중인데 CGI는 요청이 올 때마다 커널에서 fork()->exec() 를 호출함. (OS레벨에서 프로세스를 생성. OS가 일을 다함, 요청마다 OS 건드림) Apache는 "이 요청은 CGI니까 OS야 이 프로그램 실행해" 라고 전달
- 보통:
- C
- Perl
- Shell Script
- 초창기 Java 프로그램
HTTP 요청 수신
→ Apache가 커널에 fork 요청
→ 새 프로세스 생성
→ exec로 CGI 프로그램 로드
→ CGI 프로그램이 DB 연결
→ 결과 출력
→ 프로세스 종료
👉 요청 1건 = 프로세스 1개 생성, OS호출 빈번, 컨텍스트 스위칭 많음, 메모리 낭비 심함
이게 나중에 큰 문제가 됨.
3️⃣ 오른쪽 연결 요소들
(4) Database
- CGI 프로그램이 직접 DB 연결
- SQL 실행 후 결과를 받아 HTML로 만들어서 반환
📌 지금으로 치면:
(5) Another Legacy System
- ERP
- 메인프레임
- 사내 레거시 시스템
- TCP/IP, 파일, 전용 프로토콜 등으로 연동
👉 웹은 단지 프론트 창구 역할
4️⃣ 이 구조의 치명적인 문제점
❌ 성능
- 요청 올 때마다 프로세스 생성
- 동시 접속 늘면 서버 바로 터짐
❌ 확장성
- 상태 관리 불가
- 세션 공유 어려움
❌ 유지보수
- HTML + 비즈니스 로직 + DB 로직이 한 덩어리
- 코드 지옥
5️⃣ 그래서 진화한 구조들
이 그림을 거쳐서 이렇게 발전함:
- CGI
- FastCGI / mod_php
- WAS (Tomcat, WebLogic)
- MVC
- Node.js (이벤트 루프)
- MSA + API 서버
👉 네가 지금 공부 중인 Node.js 이벤트 루프 구조는
바로 이 CGI 모델의 “프로세스 폭발 문제”를 해결하려고 나온 진화 결과야.
6️⃣ 실무 관점에서의 의미
이 그림은 단순한 역사 그림이 아니라:
- 왜 동기 I/O가 위험한지
- 왜 요청당 스레드/프로세스 모델이 한계가 있는지
- 왜 Node.js가 탄생했는지
이걸 직관적으로 이해하게 해주는 출발점이야.
2. 스레드 기반 서버: 효율은 올랐지만…
요청이 늘어나자 서버는 진화한다.
- 프로세스 하나 (메모리에 상주)
- 그 안에서 요청마다 스레드 하나 (프로세스 내부 스레드/ 이벤트 루프에서 처리)
이른바 Thread-per-request 모델이다.
Java 기반 서버_Java Tomcat (Spring 포함_ Spring MVC ), 초기 Apache_ 전통적인 Servlet 기반 서버 가 이 모델을 사용했다.
이 방식의 장점은 분명했다.
- 프로세스 생성 비용 제거 (프로세스는 요청마다 생성 삭제가 아니라 상주하니까)
- 코드 작성이 직관적 (동기 처리)
하지만 시간이 지나며 문제가 드러난다.
- 스레드 수 증가 → 메모리 사용량 폭증 (요청마다 스레드가 증가. 서버 과부하 문제 여전히)
- 컨텍스트 스위칭 비용
- I/O 대기 중에도 스레드가 점유됨
특히 DB 조회, 파일 읽기, 외부 API 호출 같은 I/O 작업이 많아질수록
“아무 일도 안 하는 스레드”가 서버 자원을 잡아먹는 상황이 반복된다.

요청 하나 → 프로세스 하나. 요청 끝나면 프로세스 종료
하나의 서버 프로세스. 요청마다 스레드 생성. 각 스레드는 요청이 끝날 때까지 점유됨
이 방식은 개발은 쉬운데, 동시 요청이 많아지면 스레드가 병목이 된다(서버 자원 채움) 스레드가 터진다

Apache 서버가 요청(Socket)을 받아서
Listener → Worker들에게 분배하는 구조
그림을 보면 Apache Process가 위/아래로 두 개가 있어.
이건 보통 이런 의미야.
- 멀티 프로세스 구조
- Apache가 여러 개의 프로세스를 띄워서 동작
- 각 프로세스는 독립적인 메모리 공간을 가짐
👉 여기까지만 봐도 Node.js랑 완전히 다르다
Listener의 역할
클라이언트로부터 들어오는 소켓 연결을 받는 전담 스레드
- TCP 연결 수락(accept)
- 아직 “요청 처리”는 안 함
- “누가 이 요청을 처리할지”만 결정
즉,
입구에서 손님을 받아서
담당 직원을 정해주는 안내 데스크
같은 존재야.
Worker는 뭐냐 (핵심)
Worker의 역할
실제 HTTP 요청을 처리하는 스레드
- 요청 파싱
- 비즈니스 로직 실행
- 응답 생성
그림에서 Worker가 여러 개 보이는 이유는:
- 요청 1개 ≈ Worker 1개
- 동시 요청이 많으면 Worker가 많이 필요
👉 이게 바로 Thread-per-request 모델의 변형이다.
왜 Listener와 Worker를 분리했을까
이건 Apache가 나름 진화한 결과야.
예전 방식 (더 단순한 구조)
- 요청 수락 + 처리 = 같은 스레드
- 느린 요청이 있으면 수락도 지연
이 그림의 방식
- Listener: 연결만 받음
- Worker: 처리만 담당
👉 장점
- 새로운 연결을 빠르게 받음
- 어느 정도 확장성 개선
👉 하지만…
- 요청 수만큼 Worker는 여전히 필요
- Worker는 요청 처리 동안 계속 점유됨
이 구조의 본질적인 한계
여기서 핵심 포인트가 나온다.
Worker는 I/O를 기다리는 동안에도 점유된다
예를 들면:
- DB 조회
- 외부 API 호출
- 파일 읽기
이 동안 Worker는
❌ 다른 요청 처리 못 함
❌ 놀고 있지만 자원은 잡아먹음
그래서:
- 동시 요청 증가
- Worker 부족
- 큐 대기
- 성능 급락
이 흐름이 나온다
Node.js의 결정적 차이
여기서 네가 앞에서 봤던 Node.js 그림과 연결해보자.
Apache Worker 모델
- 요청 ↔ 스레드 강결합
- 처리 중이면 스레드 점유
- 동시성 = 스레드 수
Node.js 모델
- 요청 ↔ 스레드 결합 ❌
- I/O는 libuv에게 위임
- JS 실행은 이벤트 단위
즉,
Apache는
“사람을 늘려서 처리”
Node.js는
“기다리는 시간을 제거해서 처리”
3. 이벤트 기반 서버의 등장: 기다리지 말고 맡겨라
이 문제를 정면으로 해결한 쪽이 이벤트 기반(Event-driven) 서버다.
대표 주자: Nginx
Nginx의 발상은 단순하면서도 과감했다.
“요청을 처리하다가 기다려야 하면,
그냥 기다리지 말고 다른 요청을 처리하자”
핵심 아이디어는 이렇다.
- 소수의 스레드
- I/O는 OS에 맡김
- 작업이 끝났을 때 이벤트로 다시 통지
이 구조 덕분에 Nginx는
- 수만 개의 동시 연결
- 매우 낮은 메모리 사용량
을 달성했다.
하지만 여기엔 중요한 한계가 있었다.
- 애플리케이션 로직을 직접 다루기 어렵다
- 웹 서버로는 뛰어나지만, “백엔드 플랫폼”은 아님

“이벤트 기반 모델이 서버 성능 문제를 해결할 수 있더라”
Node.js는 바로 이 아이디어를 애플리케이션 레벨까지 끌고 온 존재야.
4. JavaScript의 문제의식
2000년대 중반, 웹은 급격히 변한다.
- AJAX 등장
- 실시간성 증가
- 요청 수는 늘고, 응답은 짧아짐
그런데 당시 서버 언어들은 대부분 이랬다.
- 동기 I/O 중심
- 스레드 기반
- 대규모 동시 연결에 비효율적
이때 한 사람이 질문을 던진다.
“왜 서버는 브라우저처럼 이벤트 기반이 아니지?”
브라우저는 이미
- 단일 스레드
- 이벤트 루프
- 비동기 처리
로 수많은 사용자 인터랙션을 처리하고 있었다.
이 질문이 바로 Node.js의 출발점이다.
5. Node.js의 탄생: 서버에 이벤트 루프를 옮기다
Node.js의 핵심 아이디어는 놀라울 정도로 명확하다.
브라우저에서 검증된 자바스크립트 이벤트 모델을
서버로 가져오자
이를 위해 세 가지가 결합된다.
- V8 엔진
- 빠른 자바스크립트 실행
- libuv
- 비동기 I/O 추상화
- 이벤트 루프 구현
- 단일 스레드 + 논블로킹 I/O
- 기다리지 않는 서버
Node.js는 이렇게 정의할 수 있다.
Node.js는
**“요청을 스레드에 묶지 않는 서버 런타임”**이다.
Call Stack, Event Loop, Task Queue / Microtask Queue, libuv
이 구조는 브라우저 JS 모델, 서버 I/O 가 결합된 Node.js 고유 구조. 자바스크립트 실행 모델
즉, 앞의 이미지들 → “왜 문제가 생겼는가”
Nginx 이미지 → “해결 실마리는 있었다”
Node.js 이미지 → “이걸 서버 런타임으로 만든 결과”
“Node.js는 이벤트 루프가 있고 비동기고 빠릅니다” 가 아니라
“서버가 겪어온 병목을 해결하려다 보니
Node.js 같은 구조가 나올 수밖에 없었다”

6. Node.js가 해결한 문제, 그리고 남긴 과제
Node.js는 기존 서버 모델의 핵심 병목을 제거했다.
- 대규모 동시 접속 처리
- I/O 중심 서비스에 최적화
- 실시간 서비스에 매우 유리
하지만 동시에 새로운 제약을 만들었다.
- CPU 연산에 취약
- 비동기 코드 복잡도
- 개발자의 이해도에 따라 품질 편차 큼
그래서 Node.js는 만능이 아니다.
잘 맞는 문제 영역이 분명히 존재한다.
7. 이 글의 정리
서버의 흐름을 한 줄로 요약하면 이렇다.
- 프로세스 기반 → 단순하지만 비쌈
- 스레드 기반 → 직관적이지만 확장성 문제
- 이벤트 기반 → 구조는 어렵지만 효율적
- Node.js → 이벤트 기반을 애플리케이션 레벨까지 확장
Node.js는 “자바스크립트 서버”가 아니라
서버 실행 모델의 패러다임 전환에 가깝다.
다음 글 예고
다음 글에서는 이 질문으로 들어간다.
“싱글 스레드인데,
Node.js는 어떻게 수천 개 요청을 동시에 처리하는가?”
👉 2편: Node.js 런타임 구조와 이벤트 루프
여기서부터 본격적으로
Node.js가 “어떻게 돌아가는지”를 파고들어간다.
'framework_library > nodejs' 카테고리의 다른 글
| 콜백기반 -> Promise기반 (0) | 2026.02.02 |
|---|---|
| 4편: Node.js에서 HTTP 서버를 직접 만들어보며 한계 느끼기 (1) | 2026.01.01 |
| 3편: 동기 코드와 비동기 코드의 실행 차이 (0) | 2025.12.31 |
| 2편: Node.js 런타임 구조와 이벤트 루프 (0) | 2025.12.29 |
| Node.js란 무엇인가 – 자바스크립트의 서버 진출 | 노드의 구성요소와 활용분야 (0) | 2025.04.08 |
댓글