HW부터 Node.js + Fastify/NestJS까지 — 서버 실행 구조
한 문장 요약
NIC(네트워크 카드) → OS TCP 스택 → libuv 이벤트 루프 → V8 JS 실행 → Fastify/NestJS 라우팅 → 응답
전체 실행 스택 (HTTP 요청 기준)
┌─────────────────────────────────────────────────────┐
│ 클라이언트 (브라우저 / curl / 앱) │
│ GET /users HTTP/1.1 │
└──────────────────────┬──────────────────────────────┘
│ TCP 패킷 (이더넷 프레임)
▼
┌─────────────────────────────────────────────────────┐
│ NIC (Network Interface Card) │
│ - 전기 신호 / 광신호 → 디지털 데이터 │
│ - DMA: CPU 개입 없이 RAM으로 패킷 전송 │
│ - 인터럽트 발생 → CPU에게 알림 │
└──────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ OS 커널 네트워크 스택 │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 이더넷 프레임 수신 │ │
│ │ ↓ │ │
│ │ IP 레이어 (목적지 IP 확인) │ │
│ │ ↓ │ │
│ │ TCP 레이어 (포트 3000, 시퀀스 번호, ACK) │ │
│ │ ↓ │ │
│ │ 소켓 버퍼 (fd=7 에 데이터 축적) │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ Node.js는 소켓 fd를 epoll(Linux)/kqueue(macOS)로 │
│ 감시 → 데이터 도착 시 이벤트 발생 │
└──────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ libuv (Node.js의 비동기 I/O 라이브러리) │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ Event Loop (단일 스레드) │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ timers │→│ poll │→│ check │ │ │
│ │ │setTimeout│ │I/O 콜백 │ │setImmed. │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ ↑ │ │ │
│ │ └───────────────────────────┘ │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ Thread Pool (기본 4개 스레드): │
│ ┌──────────────────────────────────────────────┐ │
│ │ 파일 I/O, DNS 조회, crypto, zlib 등 │ │
│ │ (OS가 비동기를 지원 안 하는 작업) │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ 소켓 읽기 가능 → 콜백을 Event Loop 큐에 등록 │
└──────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ V8 엔진 (JS 실행) │
│ │
│ HTTP 파싱 → Fastify/NestJS 콜백 함수 호출 │
│ JIT 컴파일 (핫 코드는 기계어로 최적화) │
│ → [[how-javascript-runs|JS 실행 구조]] 참조 │
└──────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Fastify 라우팅 레이어 │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ HTTP 요청 파이프라인 │ │
│ │ │ │
│ │ Request 수신 │ │
│ │ ↓ │ │
│ │ onRequest hooks (인증, 로깅 등) │ │
│ │ ↓ │ │
│ │ Radix Tree 라우팅 (URL 매칭) │ │
│ │ /users → getUsersHandler │ │
│ │ ↓ │ │
│ │ preHandler hooks (검증, rate-limit 등) │ │
│ │ ↓ │ │
│ │ 핸들러 함수 실행 │ │
│ │ async (req, reply) => { ... } │ │
│ │ ↓ │ │
│ │ JSON 직렬화 (fast-json-stringify) │ │
│ │ ↓ │ │
│ │ onSend hooks │ │
│ │ ↓ │ │
│ │ 응답 전송 │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ Fastify 특징: │
│ - Radix Tree: O(k) 라우팅 (k=경로 길이) │
│ - JSON 스키마 기반 직렬화 → 일반 대비 2-3배 빠름 │
│ - 플러그인 시스템: Avvio 기반 의존성 주입 │
└──────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ NestJS 아키텍처 레이어 (Fastify 위에서 동작) │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ HTTP 요청 │ │
│ │ ↓ │ │
│ │ Middleware (Express/Fastify 레벨) │ │
│ │ ↓ │ │
│ │ Guards (인증/인가: @UseGuards) │ │
│ │ ↓ │ │
│ │ Interceptors (before) (로깅, 변환) │ │
│ │ ↓ │ │
│ │ Pipes (입력 검증/변환: @Body() DTO) │ │
│ │ ↓ │ │
│ │ Controller 메서드 실행 │ │
│ │ @Get('/users') getUsers() │ │
│ │ ↓ │ │
│ │ Service (비즈니스 로직, DI로 주입) │ │
│ │ ↓ │ │
│ │ Interceptors (after) │ │
│ │ ↓ │ │
│ │ Exception Filters (에러 처리) │ │
│ │ ↓ │ │
│ │ 응답 반환 │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ NestJS IoC 컨테이너 (DI): │
│ @Injectable() 서비스 → 메타데이터 저장 │
│ → reflect-metadata로 타입 정보 추출 │
│ → 부트스트랩 시 싱글턴 인스턴스 생성 │
└──────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ 응답 전송 │
│ reply.send({users: [...]}) │
│ → TCP 소켓에 write() │
│ → 커널 소켓 버퍼 → NIC → 클라이언트 │
└─────────────────────────────────────────────────────┘
Event Loop 상세: 논블로킹의 핵심
Node.js 프로세스 = 메인 스레드 1개 + Thread Pool 4개
┌──────────────────────────────────────────────────────┐
│ 메인 스레드 (Event Loop) │
│ │
│ 요청 1 처리 중: │
│ ┌─────────────────────────────────────────────┐ │
│ │ getUsersHandler() 실행 │ │
│ │ db.query(sql) ← 비동기 DB 호출 │ │
│ │ → I/O 요청 등록하고 즉시 반환 │ │
│ │ → Event Loop는 다음 요청 처리로 이동 │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ 요청 2 처리 중 (요청 1의 DB 결과 기다리는 동안): │
│ ┌─────────────────────────────────────────────┐ │
│ │ createUserHandler() 실행 ... │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ DB 결과 도착 → Event Loop가 콜백 실행: │
│ ┌─────────────────────────────────────────────┐ │
│ │ 요청 1 콜백: reply.send(users) │ │
│ └─────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
블로킹 비교:
멀티스레드 (Spring): 요청당 스레드 1개 → 10,000 요청 = 10,000 스레드
Node.js 싱글스레드: 이벤트 루프 1개 → 10,000 요청 = 1 스레드 + 콜백 큐
Fastify vs NestJS 위치 관계
┌──────────────────────────────────────────┐
│ NestJS (프레임워크 레이어) │
│ - 모듈/DI/데코레이터/파이프라인 │
│ ┌────────────────────────────────────┐ │
│ │ Fastify (HTTP 어댑터) │ │
│ │ - 라우팅/파싱/직렬화 │ │
│ │ ┌──────────────────────────────┐ │ │
│ │ │ libuv (비동기 I/O) │ │ │
│ │ │ ┌──────────────────────┐ │ │ │
│ │ │ │ V8 (JS 실행) │ │ │ │
│ │ │ │ ┌────────────────┐ │ │ │ │
│ │ │ │ │ OS + 커널 │ │ │ │ │
│ │ │ │ │ ┌──────────┐ │ │ │ │ │
│ │ │ │ │ │ CPU/HW │ │ │ │ │ │
│ │ │ │ │ └──────────┘ │ │ │ │ │
│ │ │ │ └────────────────┘ │ │ │ │
│ │ │ └──────────────────────┘ │ │ │
│ │ └──────────────────────────────┘ │ │
│ └────────────────────────────────────┘ │
└──────────────────────────────────────────┘
NestJS는 기본 Express를 사용하지만
@nestjs/platform-fastify로 교체 가능:
성능: Fastify가 Express보다 약 2배 빠름
이유: Fastify의 JSON 직렬화 + 라우팅 최적화
핵심 요약
GET /users 요청
↓ NIC (전기신호 → 패킷)
↓ OS 커널 TCP 스택 (소켓 버퍼)
↓ libuv epoll/kqueue (이벤트 감지)
↓ Event Loop (콜백 스케줄링)
↓ V8 JIT (JS 코드 실행)
↓ Fastify (Radix Tree 라우팅 + JSON 직렬화)
↓ NestJS (Guards → Interceptors → Pipes → Controller → Service)
↓ 비즈니스 로직 실행 (DB 쿼리 등 → 다시 비동기 I/O)
↓ 응답 직렬화 → TCP write() → NIC → 클라이언트
핵심: Node.js의 성능 비결은 스레드를 늘리는 것이 아니라
I/O를 기다리는 동안 다른 일을 하는 이벤트 루프.