
IOCP란 멀티 프로세서 환경에서 다수의 비동기 입출력(I/O)을 처리하기 위해 고안된 모델입니다.
그 동안 우리가 비동기 함수에 대해서 배웠던 걸 기억하시나요? setTimeout같은 비동기 함수의 실행 결과를 Promise 객체 안에 저장해서 then이나 async-await으로 활용해본 기억이 다들 있으실 텐데요. 동기식 입출력과 비동기식 입출력의 동기, 비동기도 같은 의미를 가지고 있습니다.
동기식 입출력은 사용자가 "입출력을 해줘!" 라고 요청을 했을 때 입출력 작업이 끝날 때까지 기다린 후에야 CPU 제어권이 사용자 프로그램으로 넘어갑니다. 그러나 비동기 입출력은 "입출력을 해줘!"라고 요청을 보낸 후에 다시 CPU 제어권을 얻어와서 다른 작업을 수행하는 게 가능해집니다.

이해를 돕기 위해 예시를 들어 설명을 드리겠습니다. 여기, 디스크가 있습니다. 디스크는 아시다시피 컴퓨터의 저장 매체입니다. 여러분들의 컴퓨터에는 아마 수많은 파일들이 저장되어 있을 겁니다. 만약 여러분이 그 파일의 내용을 읽어보고 싶다면 어떻게 해야 할까요? 컴퓨터의 디스크로부터 데이터 입력을 받아와야 하겠죠. 그런데 싱글 스레드 기반 동기식 입출력의 경우, 데이터를 읽기 위해 응답을 기다리는 동안 사용자 프로그램은 대기하고 있게 됩니다.
여러분이 과제를 하기 위해 대량의 파일을 읽고 또 써야 한다고 가정해봅니다. 동기식 입출력은 그 하나하나의 작업이 끝날 때까지 기다리라고 말할 테고, 어쩌면 키보드 입력조차도 자유롭게 할 수 없을지 모릅니다. 작업 효율성이 급격히 떨어지게 되겠지요.
반면, 비동기식 입출력은 입출력 작업이 끝나기를 기다리지 않고 제어가 사용자 프로그램에 즉시 넘어갑니다. 즉, 입출력을 요청하는 것과 결과를 처리하는 과정이 서로 분리되기 때문에 다수의 입출력을 병렬적으로, 효율적으로 처리할 수 있게 됩니다.
그렇다면 IOCP라는 게 정확히 뭘까?

사실 IOCP를 이해하기 위해서는 많은 배경지식이 필요합니다. 그러나 저희의 대부분은 비전공자이기 때문에 이번 글에서는 최대한 쉽게 설명하기 위해 노력하겠습니다. 혹시라도 이미 컴퓨터 사이언스에 대한 지식이 풍부하신 분들은 게시글 말미에 읽어볼 만한 자료들의 링크를 남길 테니 실망하지 않으셨으면 좋겠습니다.
자, IOCP에 대해 아주 간략하게 설명을 해봅시다. IOCP 는 고성능 I/O 처리 메커니즘입니다. 앞서 말씀 드린 비동기 입출력(Asynchronous I/O) 작업을 개발자들이 매번 구현해내는 건 너무 가혹하잖아요? 그래서 Windows 운영 체제에서는 우리의 노고를 줄여주기 위해 IOCP기술을 제공하고 있습니다.
IOCP가 작동하는 방식
먼저, IOCP는 I/O Completion Port의 약자입니다. 여기서 친구를 한 명 소개할게요. 바로 '포트'라는 녀석입니다. 얘가 하는 일은 컴퓨터의 입력과 출력의 완료를 담당하는 거예요. 즉, 끝난 일들을 대기열에 쌓아두고 끝났다고 알리는 역할인 거죠. 그런데, 사실 전통적인 동기식 입출력 처리 방식에선 사실 얘의 역할은 이미 다른 애가 해주고 있었습니다. 그럼 그게 누구였던 걸까요? 그건 바로 '스레드'라는 녀석이었습니다.

스레드는 컴퓨터 프로세스의 기본적인 작업 단위입니다. 과거 애플리케이션의 입출력은 스레드라는 친구는 입출력 장치들의 상태를 점검하거나 제한을 하면서 요청을 차례차례 처리하였습니다. 이를 테면 매우 엄하게 숙제 검사를 하는 선생님이 계시고 그 아래에서 숙제를 걷어가는 반장(스레드)이 있는 셈이죠. 그러나 문제가 있었어요.


예를 들어, 반장이 숙제를 걷으러 갔다고 상상해 봅시다. 반장이 선생님께 숙제를 내고 나면, 숙제를 다 검사하셨는지 계속해서 여쭤보러 가는 경우(폴링)가 있을 수 있고, 아니면 선생님이 "다 됐다"고 말할 때까지 가만히 서서 기다리는 경우(블로킹)가 있을 수 있습니다. 이 두 방식 모두 반장이 다른 일을 하지 못하고 시간을 낭비하게 만들죠. 그럼 반 친구들은 반장이 결과물을 가지고 돌아올 때까지 아무것도 못하고 손가락만 빨면서 기다려야 하지 않겠어요? 이 방식이 너무 비효율적이라서 IOCP에서는 '포트'라는 녀석이 반장을 돕게 된 것입니다.

이때 포트가 맡는 역할은 반장(스레드)과 선생님(커널) 사이에서 "선생님이 숙제 검사 다하셨대!"라고 통보하는 것입니다. 선생님(커널)이 "채점을 완료했다"는 보고서를 포트(입출력 완료 대기열)에 전달하면, 반장(스레드)은 대기열에서 이 보고서를 꺼내 반 친구들에게 그 결과를 전달하거나 추가 작업을 수행합니다.
이 방식의 장점은, 클라이언트가 연결될 때마다 새로운 스레드를 생성하는 기존 멀티 스레드 방식과 달리, 미리 스레드를 여러 개 만들어서 입출력 완료 보고를 기다리도록 할 수 있다는 점입니다. 이렇게 하면 스레드 생성과 소멸에 따른 비용을 줄이고, 적은 수의 스레드로도 많은 작업을 효율적으로 처리할 수 있습니다.

여러 분은 여기서 중요한 의문이 들 겁니다.
=> 왜 반장 혼자서 일을 도맡고 있지? 과목별로 여러 명이 있어야 하는 거 아냐?
네, 맞습니다. IOCP는 사실 반장 한 명한테 일을 시킨 게 아니었어요. 더도 말고 덜도 말고 딱 숙제 검사를 하려는 과목의 수만큼 과목 부장을 생성해요. 과제를 걷어서 제출하는 건 과목 부장들이 하지만, 해당 작업에 대한 완료 보고는 포트가 대신해주는 셈인 거죠. 이때 미리 과목 부장을 생성하는 걸 있어 보이게 말하자면 "미리 스레드를 할당시켜 놓는 기법", 즉 스레드 풀링이라고 부릅니다.
스레드 풀링의 핵심 개념

| 항목 | IOCP 방식 | 멀티스레드 방식 |
|---|---|---|
| 스레드 생성 방식 | 미리 생성된 스레드 풀에서 스레드를 재사용. | 각 작업 요청마다 새 스레드 생성. |
| 스레드 수 관리 | 동시 실행 가능한 스레드 수를 조정하여 최적화. | 클라이언트 또는 작업 요청마다 스레드를 생성하여 수가 증가함. |
| CPU 및 메모리 사용 | 효율적: 스레드 풀 재사용으로 생성/소멸 오버헤드 감소. | 비효율적: 스레드 생성/소멸 오버헤드 발생 및 메모리 사용량 증가. |
| 컨텍스트 스위칭 | 최소화: 적은 수의 스레드로 작업 관리. | 다수의 스레드가 실행되며 컨텍스트 스위칭 비용 증가. |
| 동시성 및 확장성 | 높은 동시성 지원: I/O 작업 완료 통지 메커니즘(큐 및 이벤트) 활용. | 동시성 제한적: 스레드 수가 증가하면 성능 저하. |
| 블로킹 여부 | 비동기 방식: 입출력 요청 후 대기하지 않고 다른 작업 수행 가능. | 동기 방식: 작업 완료까지 스레드가 차단(blocking). |
| 작업 완료 처리 방식 | I/O 완료 통지(Completion Port 큐)로 작업을 관리. | 작업 완료 상태를 직접 확인(폴링)하거나, 차단 상태로 대기. |
| 사용 사례 | 고성능 네트워크 서버, 대규모 파일 처리 애플리케이션 등 대규모 병렬 작업에 적합. | 소규모 클라이언트나 작업 수가 제한된 환경에서 적합. |
| 운영체제 지원 | Windows에서 IOCP, Linux에서는 epoll/kqueue와 같은 유사 기술 사용. | 운영체제에 상관없이 스레드 기반으로 작동. |
| 장점 | - 적은 자원으로 많은 작업 처리 가능. - 높은 동시성 및 효율성. | - 구현이 비교적 간단함. - 단순한 작업에서는 적합. |
| 단점 | - 복잡한 구현. - 이해 및 디버깅 난이도가 높음. | - 스레드 오버헤드 큼. - 대규모 동시 작업 시 확장성 부족. |
이걸 눈치챘다면 당신은 이미 핵고수

자바스크립트 강의를 좀 열심히 시청하신 분들은 아마 이런 의문을 가지게 되실 겁니다.
"아니, 강의에서는 분명 자바스크립트가 싱글스레드 언어라고 했는데… Node.js는 어떻게 IOCP를 사용하고 있는 거지?" 라고 말이죠.
결론만 말씀드리자면 네, 분명 자바스크립트는 싱글스레드 언어입니다.
하지만 Node.js 속에는 자바스크립트 엔진만 들어있는 게 아닙니다!

바로 LIBUV라는 크로스플랫폼 비동기 I/O 라이브러리 내부에서 비동기 I/O 작업이 이루어지고 있습니다. 이벤트 기반 모델과 결합하여, 백그라운드(libuv)에서 수행되는 것입니다.
이해를 돕기 위해 libuv 내부를 잠시 살펴보도록 하겠습니다.

1. Network I/O (TCP, UDP, TTY, Pipe): libuv는 비동기 네트워크 통신을 위한 TCP와 UDP 소켓을 지원한다. 또한, 터미널(TTY) 및 파이프(Pipe) 연산도 비동기적으로 처리할 수 있다.
2. File I/O: 비동기 파일 시스템 작업을 지원하여 파일 읽기/쓰기 작업을 비동기적으로 처리할 수 있다.
3. DNS Ops: DNS 조회와 같은 네트워크 이름 해석도 비동기적을 처리한다.
4. User code: 사용자의 코드가 비동기 작업을 요청할 때, libuv를 통해 이를 스케줄링하고 실행한다.
5. epoll, kqueue, event ports: 리눅스, BSD, 솔라리스 같은 유닉스 기반 시스템에서 비동기 I/O 이벤트를 폴링하는 메커니즘이다.
6. IOCP: I/O Completion Ports는 Windows에서 비동기 I/O 작업을 관리하는 메커니즘이다.
7. Thread Pool: libuv의 내부 스레드 풀에서는 복잡한 작업이나, 비동기로 처리할 수 없는 블로킹 작업을 처리한다.
출처 : Node.js에서 주로 사용되는 libuv 알아보기
즉, Node.js의 비동기 I/O 작업은 운영체제의 비동기 API를 활용하게 되는데요. 그 운영체제가 만약 Windows라면, 우리가 열심히 공부했던 IOCP가 맡게 되는 것입니다! 쉽게 말하자면 Node.js가 운영체제한테 해당 기능을 대신 수행하라고 위임하고 있었다는 의미죠.
"이미 잘하는 녀석이 따로 있는데 왜 내가 해야 되지?"
만약 제가 Node.js라면 이렇게 말할 것 같네요. 확실히 이쪽이 더 효율적인 이야기로 들리지 않나요?
따라서 Node.js가 비동기 입출력을 처리할 땐, 아까 열심히 일했던 과목 부장들과 포트들 있죠? 걔네들이 백그라운드 스레드 풀에서 열심히 작업을 완료하고 완료되었다는 "이벤트"를 알려주게 됩니다. 그리고 Node.js의 이벤트 루프는 IOCP로부터 알림을 받아서 해당 작업과 연관된 콜백 함수를 실행하게 됩니다. 이렇게 함으로써 Node.js는 싱글스레드로 동작하면서도 대규모 비동기 작업을 효율적으로 처리할 수 있습니다. 마냥 낯설기만 하던 IOCP가 사실은 우리가 무심코 사용하던 콜백 함수의 뒤편에서 열심히 일하고 있었던 셈입니다.
(1) js로 코드를 작성하고 실행하게 되면 스택에 코드가 쌓입니다.
(2) 이때 스택에 쌓인 코드를 실행하게 되면 libuv를 호출합니다.
(3) libuv는 비동기 처리를 할지 , 동기 처리를 할지 검사 후, 시스템 API를 이용하거나 쓰레드 풀에 생성된 쓰레드에게 작업을 위임합니다.
(4) 작업이 완료되면 콜백 함수를 테스크 큐에 넘겨줍니다.
(5) 이벤트 루프는 콜스택에 쌓여있는 함수가 없을 때, 테스크 큐에 대기하고 있던 콜백함수를 콜스택으로 넘겨줍니다.
(6) 콜스택에 쌓인 콜백 함수가 실행되고, 콜스택에서 제거됩니다
출처 : Node.js의 libUV 라이브러리에 대해 알아보자
이벤트루프
이 libuv 라이브러리에서 핵심은 uv_io와 이벤트 루프입니다. 자바스크립트 엔진이 아닌, 구동하는 환경에서 가지고 있는 장치입니다.

이벤트루프는 timers => pending callbacks => idle, prepare -> poll => check => close callbacks 순으로 호출됩니다.
각각의 단계는 자신만의 큐를 가지고있습니다. 우리가 등록한 작업 각각의 유형에 맞는 큐에 등록이됩니다. 이 큐가 실행되고 완료되면 다시 이벤트루프로 넘어와 실행됩니다.
각 단계에서 다음 단계로 넘어가는 것을 '틱(tick)'이라고 합니다.
- timers
타이머에 관한 비동기 작업을 관리합니다. 예를 들어setTimeout , setInterval에 등록된 콜백을 관리합니다. 이 타이머 콜백은 min-heap 자료구조 기반으로 구성되어있습니다.
min-heap 자료구조
데이터를 완전 이진 트리 형태로 관리하면서 최댓값 또는 최솟값을 찾아내는 효율적 자료구조입니다. 최소힙은 최소값을 찾아내는데 최적화되어있습니다. 떄문에 이를 사용하면 실행할 수 있는 가장 이른 타이머를 손쉽게 찾을 수 있습니다.
- pending callbacks
이 단계는 , 이전 이벤트 루프 반복에서 수행되지 못했던 콜백들이 있습니다. 처리하지 못하고 넘어간 작업을을 쌓아놓고 실행하는 단계입니다.
이벤트 루프에 pending_queue에 들어가있는 콜백들을 실행합니다. 에러 핸들러 콜백도 여기로 들어오게됩니다. - idle, prerare
매틱마다 실행되며, node.js 내부 관리를 위해 사용한다고 합니다. - poll
거의 모든 콜백이 여기에 해당됩니다. ( ex) 데이터베이스에 쿼리를 보낸 후 결과가 왔을 떄 실행되는 콜백, HTTP 요청을 보낸 후 응답이 왔을 때 실행되는 콜백, 파일을 비동기로 다시 읽고 다 읽었을 때 실행되는 콜백)
여기서 watcher_queue를 이용하여 관리합니다. 해당 큐를 사용하는 이유는 비동기 작업이 완료되었을 경우, 순서를 보장하기 위해서입니다.
watcher은 FD(File Descriptor)를 가지고 있습니다. 운영체제가 FD가 준비되었다고 알리면 event loop 이에 해당하는 watcher를 찾을 수 있고 콜백을 실행할 수 있습니다. - check
setImmediate()로 등록된 콜백을 관리하기위한 단계입니다 - close callbacks
close, destroy 와 같은 이벤트 타입 콜백을 처리합니다.
이벤트 루프는 이 6단계를 라운드 로빈 방식으로 순회하며 동작합니다.
요약
그럼 마지막으로 IOCP에 대해 짧게 요약해보겠습니다.

- IOCP(Input/Output Completion Port)는 Windows 운영 체제에서 제공하는 고성능 비동기 I/O 처리 메커니즘으로, 많은 동시 작업을 효율적으로 처리하기 위해 설계되었습니다.
- 기존 멀티스레드 방식의 문제점을 해결하기 위해 스레드 풀링(Thread Pooling) 과 비동기 작업 완료 통지(Completion Notification) 를 활용하며, 적은 자원으로도 많은 작업을 처리할 수 있습니다.
- Node.js와 같은 비동기 기반의 환경에서는 IOCP를 통해 비동기 I/O 작업을 운영체제에 위임하여, 싱글스레드로도 대규모 비동기 작업을 효율적으로 처리할 수 있게 합니다.
마무리
최대한 쉽고 간단하게 전달을 드리려고 했는데 이해가 잘 되셨는지 모르겠네요. 여러 가지 읽을 거리를 링크로 소개해드릴 테니 혹시 궁금하시다면 읽어보시는 걸 추천드립니다. 매우 부족한 글이지만 누군가의 이해를 도울 수 있으면 좋겠습니다.
출처
[IOCP - Input/Output Completion Port](https://www.joinc.co.kr/w/Site/win_network_prog/doc/iocp)
[Iocp 기본 구조 이해 | PPT](https://www.slideshare.net/namhyeonuk90/iocp)
[I/O 완료 포트 - Win32 apps | Microsoft Learn](https://learn.microsoft.com/ko-kr/windows/win32/fileio/i-o-completion-ports?redirectedfrom=MSDN)
[What is Node.JS and When to use it? A comprehensive guide with examples](https://www.simform.com/blog/what-is-node-js/)
'JS > TIL(Today I Learned)' 카테고리의 다른 글
| 2024-12-26 (2) | 2024.12.26 |
|---|---|
| 2024-12-24 (2) | 2024.12.24 |
| 2024-12-22 <전송 계층> (2) | 2024.12.22 |
| 2024-12-20 <트러블 슈팅> (2) | 2024.12.20 |
| 2024-12-19 (4) | 2024.12.19 |
댓글