I/O 모델

서버가 동시 요청을 처리하는 방식

Blocking I/O는 요청마다 스레드를 만들어 응답을 기다리지만, 스레드가 많아지면 컨텍스트 스위칭과 메모리 낭비가 심해진다. Non-blocking I/O는 응답을 기다리지 않고 다른 요청을 처리하며, epoll 같은 I/O 멀티플렉싱으로 준비된 소켓만 골라서 처리한다. 이 구조 덕분에 스레드 하나로도 수천 개의 동시 연결을 감당할 수 있으며, Nginx와 Node.js가 이 방식을 쓴다.

Io

개념

서버가 하는 일의 대부분은 I/O(네트워크, 디스크)다. 클라이언트 요청 받기, DB 쿼리, 파일 읽기, 응답 보내기 — 전부 I/O이고, CPU 연산은 전체 시간의 1%도 안 된다. 나머지는 기다리는 시간이다. 이 기다리는 시간을 어떻게 다루느냐에 따라 서버의 동시 처리 능력이 갈린다.

Blocking I/O

가장 단순한 방식. 요청 하나당 스레드 하나를 만들고, I/O 응답이 올 때까지 스레드가 멈춰서 기다린다.

요청 1000개 → 스레드 1000개 → 각자 DB 응답 기다리며 멈춤

문제는 두 가지다.

  • 컨텍스트 스위칭 — 대부분 놀고 있는 스레드 사이를 스케줄러가 왔다갔다하면서 CPU 낭비
  • 메모리 — 스레드 하나에 스택 ~1MB. 1000개면 1GB, 10000개면 10GB

Non-blocking I/O

I/O 요청을 보내고 기다리지 않는다. 바로 다른 요청을 처리하러 간다.

[0.00ms] 1번 요청 → DB 쿼리 보냄 → 안 기다리고 다음으로
[0.01ms] 2번 요청 → DB 쿼리 보냄 → 안 기다리고 다음으로
...
[10.0ms] 1번 DB 응답 옴 → 1번 처리

서버 요청 하나의 실제 CPU 작업 시간은 ~0.03ms이고 DB 대기가 ~10ms다. 기다리는 시간에 다른 요청을 처리하니까 스레드 하나로도 수천 개를 감당할 수 있다.

I/O 멀티플렉싱 (epoll)

non-blocking에서 "응답이 왔는지"를 어떻게 아느냐의 문제를 해결하는 것이 I/O 멀티플렉싱이다. Linux의 epoll, macOS의 kqueue가 대표적이다.

소켓은 네트워크 연결 하나를 가리키는 번호다. 파일 디스크립터와 같은 개념이다.

소켓 3번 → 1번 클라이언트와의 연결
소켓 4번 → 2번 클라이언트와의 연결
소켓 5번 → DB와의 연결

스레드가 이 소켓들을 epoll에 등록하면, epoll은 잠들어 있다가 소켓에 데이터가 도착하면 커널이 깨워준다. 준비된 소켓 목록만 스레드한테 돌려준다.

스레드 → DB한테 쿼리 보냄 (5번 소켓)
      → "5번 소켓 감시해줘" epoll에 등록
      → 다른 요청 처리

DB 응답 → 5번 소켓에 도착 → 커널 감지 → epoll 깨움 → 스레드한테 "5번 준비됐어"

주기적으로 확인하는 polling이 아니라, 준비될 때까지 잠들어 있다가 깨어나는 방식이라 CPU를 낭비하지 않는다.

이벤트 루프

epoll 기반 서버의 동작 구조가 이벤트 루프다. 스레드 하나가 이 루프를 반복한다.

  1. epoll에게 "준비된 거 있어?" → 없으면 잠듦
  2. 준비된 게 생기면 커널이 깨워줌
  3. 준비된 소켓들 처리
  4. 다시 1번으로

Node.js가 "싱글 스레드인데 동시 요청을 잘 처리한다"는 게 이 구조다. 스레드 1000개 대신 스레드 1개 + epoll로 동시 연결 수천 개를 처리한다. 컨텍스트 스위칭도 없고 메모리도 안 먹는다.

Blocking vs Non-blocking

Blocking Non-blocking + epoll
요청 1000개 스레드 1000개 스레드 1개
대기 방식 스레드가 멈춤 다른 요청 처리하러 감
메모리 스레드당 ~1MB 거의 없음
대표 Apache (prefork) Nginx, Node.js, Go

Reactor vs Proactor

epoll + 이벤트 루프 구조가 Reactor 패턴이다. 대부분의 서버가 이 방식이다.

  • Reactor — "소켓에 데이터 왔어" 알림 → 스레드가 소켓 버퍼에서 직접 읽어옴
  • Proactor — 커널이 소켓 버퍼에서 읽기까지 끝내놓고 → 스레드한테 결과만 전달

Reactor는 스레드가 읽기를 직접 하고, Proactor는 커널이 읽기까지 대신 해주는 차이다.

  • Reactor — Nginx, Node.js, Go (epoll/kqueue 기반)
  • Proactor — Windows IOCP, Linux io_uring

io_uring

Linux에서 Proactor 방식을 구현한 최신 기술이다. epoll은 "준비됐다" 알림 후 스레드가 직접 읽어야 하지만, io_uring은 커널이 읽기/쓰기까지 완료해서 결과만 돌려준다. 스레드가 할 일이 한 단계 줄어든다.

더 보기

sunshinemoon · 2026