Node.js vs FastAPI 이벤트 루프: 비동기 동시성 심층 탐구
Source: Dev.to
프로덕션에서의 API 스택
현대 프로덕션 시스템에서 반복적으로 등장하는 두 가지 스택:
- Node.js
- FastAPI
두 스택 모두 고동시성 워크로드를 처리하는 것으로 널리 알려져 있으며 이벤트 기반 아키텍처에 크게 의존합니다.
간단 비교
| 기능 | Node.js | FastAPI |
|---|---|---|
| 동시성 모델 | 단일 스레드 이벤트 루프 + 논블로킹 I/O | 비동기 코루틴 + 다중 워커 프로세스 |
| 일반적인 런타임 | JavaScript (V8) | Python (asyncio) |
| 코어 간 확장 | cluster 모드 필요 | 다중 프로세스를 통한 자연스러운 확장 |
| 최적 활용 분야 | I/O 중심 API, 실시간 애플리케이션, WebSockets | API, 머신러닝 추론 백엔드, 스트리밍, 마이크로서비스 |
| 잠재적 병목 현상 | CPU 집약 작업이 이벤트 루프를 차단 | 별도 프로세스를 사용해 GIL을 회피 |
Node.js
핵심 철학
- 단일 스레드에서 JavaScript를 실행합니다(브라우저 모델을 그대로 반영).
- I/O 작업은 libuv라는 고성능 C 라이브러리에게 위임됩니다.
- I/O가 완료되면 콜백이 이벤트 루프로 다시 푸시됩니다.
이 설계는 대부분의 웹 서버가 수행하는 I/O‑중심 워크로드에 매우 적합합니다.
트레이드‑오프: CPU‑집중 작업은 JavaScript 스레드를 차단하여 전체 서버를 정지시킵니다.
중요한 특성
- 모든 JavaScript 코드는 하나의 스레드에서 실행됩니다.
- I/O → libuv에 위임(네트워크, 파일 시스템, 타이머, 스레드‑풀 스케줄링).
- 이벤트 루프 단계:
| Phase | What Happens |
|---|---|
| Timers | setTimeout() / setInterval() 로 예약된 콜백을 실행합니다. |
| Pending Callbacks | 시스템 수준 콜백을 처리합니다(예: TCP 오류). |
| Poll | 루프의 핵심 – I/O 이벤트를 기다리고, 완료된 작업을 가져와 콜백을 실행합니다. |
| Check | setImmediate() 콜백을 실행합니다. |
| Close Callbacks | 정리 이벤트를 처리합니다(예: socket.destroy()). |
스레드 풀 (libuv)
- 파일 시스템 접근, DNS 조회, 압축, 암호화 등을 처리합니다.
- 기본 크기: 4 스레드(
UV_THREADPOOL_SIZE로 설정 가능). - JavaScript 실행은 여전히 단일 스레드이며, 무거운 CPU 작업은 여전히 루프를 차단합니다.
간단한 예시 (Node.js)
// Callback style (old)
db.query(sql, (err, result) => {
if (err) throw err;
console.log(result);
});
// Promise / async‑await (modern)
const result = await db.query(sql);
console.log(result);
async/await을 사용하더라도 Node는 여전히 콜백 큐를 통해 작업을 스케줄합니다; 이 문법은 프로미스와 콜백 위에 얹힌 문법 설탕에 불과합니다.
Source: …
FastAPI
핵심 철학
- FastAPI는 단순히 프레임워크일 뿐이며, 런타임은 ASGI 서버에서 제공됩니다. 예:
uvicornhypercorngunicorn + uvicorn workers
- 각 워커는:
- 별도의 OS 프로세스
- 자체 Python 인터프리터
- 자체 이벤트 루프
따라서 동시성은 두 층에서 발생합니다:
- 각 워커 내부의 비동기 코루틴.
- CPU 코어마다 다중 워커 프로세스.
일반적인 엔드포인트
from fastapi import FastAPI
app = FastAPI()
@app.get("/users")
async def get_users():
result = await db.fetch_all()
return result
- Python은 코루틴 객체(일시 중단 가능한 계산)를 생성합니다.
- 이벤트 루프는 필요에 따라 코루틴을 스케줄링하고 재개합니다.
- 이는 콜백 지옥에 비해 선형적이고 가독성 높은 프로그래밍 모델을 제공합니다.
콜백 vs 코루틴 모델
| 측면 | Node.js (콜백) | FastAPI (코루틴) |
|---|---|---|
| 문법 | db.query(sql, (err, result) => {...}) | result = await db.query() |
| 흐름 | 중첩된 콜백 → “콜백 지옥”(프라미스/async‑await 로 완화) | 일시 중단 가능한 함수 → 자연스러운 흐름 |
| 스케줄링 | 콜백 큐(이벤트 루프) | 코루틴의 협력적 스케줄링 |
프로세스 기반 병렬성
- FastAPI 배포는 일반적으로 다중 워커 프로세스를 실행합니다:
- 각 워커 → 1개의 OS 프로세스 → 1개의 Python 인터프리터 → 1개의 이벤트 루프
- 이를 통해 CPU 코어 전반에 걸친 진정한 병렬성을 구현할 수 있으며, 각 프로세스가 자체 인터프리터를 가지므로 Python의 전역 인터프리터 락(GIL)을 우회합니다.
Worker 1 → CPU core 1
Worker 2 → CPU core 2
...
동시성 전략 요약
Node.js 동시성 모델
- 싱글 스레드 이벤트 루프 + 논블로킹 I/O.
- 뛰어난 분야:
- API
- 스트리밍 서비스
- 실시간 애플리케이션 (WebSockets, 채팅 서버, 라이브 대시보드)
- 강점:
- 높은 I/O 처리량
- 실시간 시스템
- 방대한 생태계 및 성숙한 툴링
- 약점:
- CPU 집약적인 작업이 루프를 차단함 (예: 대용량 JSON 파싱, 이미지 처리, 암호화, ML 추론)
- 무거운 CPU 작업을 위해 워커 스레드 또는 별도 마이크로서비스가 필요함.
FastAPI 동시성 모델
- 비동기 코루틴 + 다중 워커 프로세스.
- 워커가 CPU 작업으로 블록되더라도 다른 워커가 계속 서비스를 제공하여 응답성을 유지함.
- 강점:
- 머신러닝 프레임워크와의 원활한 통합
- Pydantic을 통한 강력한 타입 힌트 및 자동 검증
- 뛰어난 개발자 경험
- I/O‑바운드와 CPU‑바운드 워크로드 모두에 대한 우수한 성능 (프로세스 스케일링을 통해)
- 흔한 오해: FastAPI는 모든 작업을 자동으로 비동기로 실행하지 않는다;
async def를 명시적으로 사용하고 I/O‑바운드 호출을await해야 함.
Takeaways
- Node.js는 작업 부하가 I/O‑bound이고 JavaScript 실행을 가볍게 유지할 수 있을 때 빛을 발합니다.
- FastAPI는 Python의 풍부한 생태계와 프로세스‑레벨 병렬성을 활용하여 mixed workloads에 더 유연한 모델을 제공합니다.
- performance profile, team expertise, operational constraints에 맞는 스택을 선택하세요.
FastAPI에서 비동기 엔드포인트 명확히 하기
주장: “그것은 사실이 아닙니다.”
실제 상황
def endpoint():
...
- 이것은 비동기가 아닙니다.
- FastAPI는 이를 스레드 풀 실행기 내부에서 실행합니다.
결과:
Blocking function → Executed in thread pool → Event loop remains free
이벤트 루프를 직접 사용하려면, 엔드포인트를 다음과 같이 선언해야 합니다:
async def endpoint():
...
전체 스택 레이어 이해
Node.js
V8 Engine → Node.js Runtime → libuv → Operating System
- V8은 JavaScript를 실행합니다.
- libuv는 비동기 I/O를 처리합니다.
FastAPI
Python Interpreter → FastAPI Framework → ASGI → Uvicorn Server → asyncio / uvloop → Operating System
- 많은 FastAPI 배포에서는 C로 작성된 이벤트 루프 구현인 uvloop를 사용합니다.
- uvloop 자체는 libuv 위에 구축되어 있습니다 — Node.js에서 사용되는 동일한 라이브러리입니다.
결과적인 동등성
FastAPI + uvloop = Python + libuv
다른 언어이지만, 동일한 기본 비동기 엔진을 사용합니다.
동시성 철학
Node.js와 FastAPI는 동일한 기본 철학을 따릅니다:
- 이벤트 기반, 논블로킹 I/O
하지만 두 프레임워크는 동시성을 서로 다른 방식으로 확장합니다.
| 기능 | Node.js | FastAPI |
|---|---|---|
| 실행 모델 | 단일 JavaScript 스레드 | asyncio 코루틴 |
| I/O 처리 | libuv 스레드 풀 | asyncio / uvloop |
| 병렬성 | 클러스터 모드 또는 워커 스레드 | 다중 워커 프로세스 |
| 멀티코어 확장성 | 명시적 클러스터링 필요 | 자연스러운 멀티코어 지원 |
실용적인 가이드
- Node.js 사용: 실시간 시스템을 구축하고 JavaScript 생태계 내에 머무를 때.
- FastAPI 사용: Python, 데이터 과학, 머신러닝 또는 AI 워크로드와 관련된 서비스를 구축할 때.
핵심 요점: 각 동시성 모델이 부하 하에서 어떻게 동작하는지 이해하는 것은 매우 중요합니다. 초기 아키텍처 결정이 나중에 시스템이 확장되는 방식을 좌우하기 때문입니다.
당신의 생각
고성능 동시성 API를 위해 어떤 런타임을 선호하시나요? 그리고 그 이유는 무엇인가요?
추가 읽기
이 글이 마음에 드셨다면, 여기에서 제 블로그를 더 읽어보실 수 있습니다: