I/O 모델: 블로킹, 논블로킹, I/O 멀티플렉싱 (I/O Models)
한눈에 보기
I/O 모델은 애플리케이션이 커널에 입출력을 요청했을 때 결과를 기다리는 방식이다. 블로킹, 논블로킹, I/O 멀티플렉싱의 동작 원리와 select, epoll, io_uring, 이벤트 루프까지 정리한다.
최종 업데이트 2026.09.17
I/O 모델은 애플리케이션이 커널(kernel)에 입출력을 요청했을 때 그 결과를 기다리는 방식을 규정하는 설계 방식이다. 블로킹(blocking) I/O는 데이터가 준비될 때까지 호출한 스레드를 멈추고, 논블로킹(non-blocking) I/O는 준비 여부와 관계없이 즉시 제어권을 돌려준다. I/O 멀티플렉싱(I/O multiplexing)은 하나의 스레드가 여러 소켓의 준비 상태를 한꺼번에 감시하도록 해서 대규모 동시 연결을 적은 자원으로 처리하게 해 준다.
I/O 요청의 두 단계
애플리케이션은 하드웨어에 직접 접근할 수 없으므로 모든 입출력은 시스템 콜을 통해 커널에 위임된다. 그중에서도 네트워크 I/O는 상대가 언제 데이터를 보낼지 알 수 없다는 불확실성 때문에 대기 전략이 특히 중요하다. 소켓에서 데이터를 읽는 과정은 크게 두 단계로 나뉜다. 첫 번째는 네트워크로 도착한 데이터가 커널의 수신 버퍼에 쌓일 때까지 기다리는 대기 단계이고, 두 번째는 커널 버퍼의 데이터를 사용자 공간(user space) 버퍼로 복사하는 단계이다. I/O 모델을 구분하는 기준은 주로 첫 번째 대기 단계를 어떻게 다루느냐에 있다. 데이터가 언제 도착할지는 상대방과 네트워크 상황에 달려 있으므로, 이 대기 시간을 스레드가 어떻게 보내는지가 서버의 동시 처리 능력을 결정한다.
블로킹 I/O의 동작
블로킹 I/O에서 read 계열 시스템 콜(system call)을 호출하면 다음 순서로 진행된다.
- 애플리케이션이 커널에 읽기를 요청한다.
- 커널 버퍼에 데이터가 없으면 커널은 해당 스레드를 대기 상태로 전환하고 스케줄러에서 제외한다.
- 데이터가 도착하면 커널이 스레드를 깨우고 데이터를 사용자 버퍼로 복사한다.
- 복사가 끝난 뒤에야 호출이 반환되고 스레드가 다음 코드를 실행한다.
이 모델은 코드가 순차적이라 이해하기 쉽지만, 연결 하나당 스레드 하나를 붙여야 여러 클라이언트를 동시에 처리할 수 있다. 스레드는 각각 스택 메모리를 차지하고 문맥 교환(context switch) 비용을 유발하므로, 수만 개의 동시 연결을 다루는 서버에서는 한계가 뚜렷하다.
논블로킹 I/O의 동작
소켓을 논블로킹 모드로 설정하면 데이터가 없을 때 커널이 스레드를 재우는 대신 곧바로 오류 코드(EWOULDBLOCK 또는 EAGAIN)를 반환한다. 스레드는 멈추지 않으므로 그 사이에 다른 일을 할 수 있다. 다만 데이터가 준비됐는지 알려면 애플리케이션이 반복해서 호출을 다시 시도해야 하는데, 이런 폴링(polling)은 준비되지 않은 소켓에 시스템 콜을 낭비하므로 CPU를 소모한다. 그래서 논블로킹 소켓은 단독으로 쓰기보다 준비 상태를 통지해 주는 멀티플렉싱 기법과 함께 쓰는 것이 일반적이다.
I/O 멀티플렉싱과 이벤트 통지
I/O 멀티플렉싱은 감시하고 싶은 소켓 집합을 커널에 등록해 두고, 그중 하나라도 읽기나 쓰기가 가능해지면 통지받는 방식이다. 대기 자체는 블로킹이지만, 스레드 하나가 수천 개의 소켓을 동시에 기다릴 수 있다는 점이 핵심이다. 대표적인 인터페이스는 다음과 같다.
- 셀렉트(select), 폴(poll): 호출할 때마다 전체 소켓 목록을 커널에 넘기고, 반환된 뒤에도 어떤 소켓이 준비됐는지 전부 순회해서 찾아야 한다. 감시 대상이 늘수록 비용이 선형으로 증가한다.
- 이폴(epoll, 리눅스), 케이큐(kqueue, BSD 계열): 관심 소켓을 커널에 한 번 등록해 두면, 이후에는 준비된 소켓 목록만 돌려준다. 소켓 수가 많아도 성능이 유지되어 대규모 서버의 표준이 되었다.
- io_uring(리눅스): 준비 통지를 넘어 I/O 완료까지 커널이 대신 수행하고 결과를 큐로 돌려주는 비동기 I/O 인터페이스로, 시스템 콜 횟수 자체를 줄인다.
Nginx, Redis, Node.js의 이벤트 루프(event loop)가 모두 이 구조 위에 서 있다. 이벤트 루프는 멀티플렉싱 호출로 준비된 소켓 목록을 받고, 각 소켓에 대해 논블로킹으로 읽고 쓰며 콜백이나 태스크를 실행하는 단일 루프를 반복한다.
세 모델 비교
| 구분 | 블로킹 I/O | 논블로킹 I/O(폴링) | I/O 멀티플렉싱 |
|---|---|---|---|
| 대기 방식 | 스레드가 잠들어 기다림 | 즉시 반환, 반복 재시도 | 여러 소켓을 한 호출로 감시 |
| 스레드와 연결의 관계 | 연결당 스레드 1개 | 스레드 1개로 가능하나 CPU 낭비 | 스레드 1개로 다수 연결 |
| CPU 사용 | 대기 중 소모 없음 | 폴링으로 낭비 발생 | 준비된 것만 처리해 효율적 |
| 코드 복잡도 | 낮음 | 중간 | 높음(이벤트 기반 구조 필요) |
| 적합한 상황 | 연결 수가 적은 단순 서버 | 단독 사용은 드묾 | 대규모 동시 연결 서버 |
모델 선택의 기준
연결 수가 적고 요청당 작업이 무거운 내부 도구라면 블로킹 모델과 스레드 풀(thread pool)로도 충분하며 코드가 단순해진다. 반면 채팅 서버나 API 게이트웨이처럼 유휴 연결이 많고 동시 접속이 큰 서비스는 멀티플렉싱 기반 이벤트 모델이 자원 효율에서 압도적으로 유리하다. 다만 이벤트 루프 위에서 CPU를 오래 쓰는 작업을 실행하면 루프 전체가 막혀 모든 연결이 지연되므로, 무거운 연산은 워커 스레드로 분리해야 한다는 점이 실무에서 자주 겪는 함정이다. 최근에는 가상 스레드(virtual thread)나 고루틴(goroutine)처럼 런타임이 내부적으로는 논블로킹 I/O를 쓰면서 개발자에게는 블로킹 스타일 코드를 허용하는 절충안도 널리 쓰인다.
면접에서 이렇게 물어본다
- 블로킹 I/O와 논블로킹 I/O의 차이를 커널과 스레드 상태 관점에서 설명해 보라.
- select와 epoll의 차이는 무엇이고, epoll이 대규모 연결에서 유리한 이유는 무엇인가.
- Node.js나 Nginx가 적은 스레드로 수많은 동시 연결을 처리할 수 있는 원리를 설명해 보라.
- 논블로킹 소켓만 사용하고 멀티플렉싱을 쓰지 않으면 어떤 문제가 생기는가.
- 이벤트 루프 기반 서버에서 CPU 집약 작업이 들어오면 어떤 일이 벌어지고 어떻게 해결하는가.
네트워크에서 같이 읽기
- CORS (Cross-Origin Resource Sharing)CORS는 브라우저의 동일 출처 정책을 안전하게 완화하는 HTTP 헤더 기반 메커니즘이다. 단순 요청과 프리플라이트, 자격 증명, 서버 설정에서 자주 하는 실수까지 정리했다.
- DNS (Domain Name System)DNS는 도메인 이름을 IP 주소로 변환하는 분산 계층형 이름 해석 시스템이다. 루트, TLD, 권한 서버와 재귀 리졸버의 역할, 해석 과정, 레코드 타입, 캐싱과 DNSSEC, DoH까지 정리한다.
- HTTP와 HTTPS (HTTP and HTTPS)HTTP는 웹의 요청과 응답을 주고받는 프로토콜이고, HTTPS는 이를 TLS 위에 얹어 암호화한 것이다. 무상태성, 인증서 신뢰 사슬, HTTP/1.1과 2, 3의 차이까지 비교해 정리한다.
- 로드 밸런싱 (Load Balancing)로드 밸런싱은 트래픽을 여러 서버에 분산해 처리량과 가용성을 높이는 기술이다. L4와 L7의 차이, 라운드 로빈과 최소 연결 등 분산 알고리즘, 헬스 체크와 세션 처리 전략을 정리한다.
이 개념, 내 코드에서는 어떻게 물어볼까요
데브핏(DevFit)에 채용공고 URL을 붙여 넣고 레포지토리를 연결하면, 내가 쓴 커밋에서 예상 질문과 어필 포인트를 만듭니다. 하루 10회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.
공고 넣고 시작하기