C++ 게임 서버를 개발하기 위해서는 소켓 프로그래밍(Socket Programming)에 대한 이해가 필수적입니다. 그리고 소켓 프로그래밍을 제대로 다루기 위해서는, 그 기반이 되는 네트워크(Network)와 운영체제(OS)에 대한 기본적인 지식이 선행되어야 합니다.
두번 째 글의 목표는 Socket Model 중 IOCP에 대한 정확한 이해입니다. 이번 글은 IOCP을 제대로 다루기 위한 마지막 글입니다. IOCP 모델은 전에 배운 Select 모델에 비해 난이도가 높고 다루는 개념 또한 많기에 동기 & 비동기 I/O, 커널에 대해 제대로 알아보겠습니다.
논외로 글을 원활하게 이해하기 위해서는 TCP / UDP의 기본 개념과 주요 Socket Option에 대한 사전 지식이 필요합니다. 해당 내용이 아직 익숙하지 않다면, 먼저 제가 작성한 Network 관련 글을 참고하신 후 읽어주시기 바랍니다.
(이 글은 여러 기술 문서와 교재 등 다양한 자료를 참고하였습니다)
Synchronous & Asynchronous 사전적 정의를 보면 Synchronous의 Syn는 '함께'이란 뜻이고 chrono는 '시간'이라는 뜻입니다 마찬가지로 Asynchronous 또한 앞에 A라는 접두사가 붙어있는 형태입니다.
따라서 아래와 같은 의미 또한 내포하고 있습니다.
먼저, 동기란? 요청(Request)을 보낸 주체가 그 결과(Response)가 올 때까지 흐름을 멈추고 기다리는 방식입니다.
Synchronous I/O (Wikipedia)
In synchronous I/O, a thread initiates an I/O operation and waits for it to complete before continuing execution.

즉 동기 I/O(Synchronous I/O)란? I/O 요청을 호출한 스레드가, I/O 작업이 완료될 때까지 블로킹(blocking)되어 기다리는 방식입니다.
참고로 지금까지 TCP, UDP Server & Client 모두 accept(), connect(), send() & sendto() 등등 모두 Blocking Socket + Sync 방식으로 작성했습니다.
Q1. 그렇다면 무엇이 문제이길래 Asynchronous I/O을 배워요?
이는 전에 설명한 MMORPG 게임 도중 아이템을 흭득하는 순간 캐릭터가 움직이는 않는 사례와 똑같이 적용됩니다. Non-Blocking은 소켓 함수 호출 시 조건을 만족하지 않더라도 함수가 즉시 리턴하여 스레드가 중단 없이 다음 코드를 수행방식이지만, I/O 가능 여부를 응용 프로그램이 직접 반복적으로 확인해야 한다는 점에서 근본적인 한계를 가집니다.
반면, Asynchronous I/O는 I/O 완료 시점을 Kernel이 직접 통지해주기 때문에, 대규모 동시 접속 환경에서 보다 효율적이고 확장성 있는 서버 구조를 구성할 수 있습니다.
비동기 I/O(Asynchronous I/O)란 I/O 작업의 수행과 완료 관리를 Kernel에 맡기고, 작업이 완료되었을 때 이를 이벤트나 완료 통지 방식으로 전달받는 구조입니다. 이로 인해 스레드는 I/O 완료를 기다리지 않고 다른 작업을 수행할 수 있어 시스템 자원을 효율적으로 활용할 수 있습니다.
Asynchronous I/O (Wikipedia)
Asynchronous I/O allows a process to continue execution without waiting for an I/O operation to complete.

Q2. 비동기 I/O는 멀티스레딩이랑 병렬 처리랑 비슷해보이는데요?
정의를 들어보면 비동기 I/O는 멀티스레딩이나 병렬 처리와 비슷하게 느껴질 수 있습니다.
이러한 혼동은 Asynchronous(비동기)라는 용어가 주는 인상에서 비롯됩니다. ‘비동기’라는 단어는 마치 여러 작업이 동시에 실행되는 것처럼 느껴지지만, 비동기 I/O의 본질은 동시에 실행하는 것이 아니라 기다리지 않는 것에 있습니다.
즉, 멀티스레딩 & 병렬 처리가 CPU 작업을 어떻게 나누어 실행할 것인가에 대한 문제라면, 비동기 I/O는 I/O 대기 시간을 어떻게 제거할 것인가에 대한 문제라고 볼 수 있습니다.
Blocking & Non-Blocking와 Asyn & Sync I/O는 별개의 얘기가 아닙니다. 프로그램 아키텍쳐에서는 이 두 개념이 함께 조합되어 사용됩니다.
가장 전통적인 소켓 모델로, 기본적인 TCP/UDP 소켓 통신 방식이 이에 해당합니다.
recv(), send()와 같은 소켓 함수 호출 시, I/O 작업이 완료될 때까지 호출한 스레드는 블로킹 상태로 대기합니다. 또한 I/O 요청과 완료가 같은 실행 흐름 안에서 처리되므로 동기(Synchronous) 방식입니다.
철수 : 이 서류 검토 좀 지금 바로 해줄 수 있을까요?
민수 : 네, 잠시만요. 제가 끝낼 때까지 기다려주세요.
철수 : 네… (가만히 기다리는 중)
민수 : 검토 끝났습니다. 여기 결과입니다.
철수 : 아, 이제야 확인할 수 있겠네요.
철수는 서류 검토가 끝날 때까지 아무 다른 일을 하지 못하고 대기합니다. 민수의 작업이 끝나야만 다음 행동을 할 수 있습니다.
Blocking + Async 조합은 실무적인 소켓 모델 관점에서는 거의 사용되지 않으며, 일반적인 네트워크 서버 설계에서는 의미 있는 구조로 취급되지 않습니다.
Non-Blocking 소켓을 사용하는 동기 I/O 모델입니다. 소켓을 Non-Blocking 모드로 설정하면 recv(), send() 호출 시 즉시 반환되며, 데이터가 준비되지 않은 경우 에러 코드를 반환합니다.
이 경우 스레드는 Blocking되지 않지만, I/O가 가능한지를 응용 프로그램이 직접 확인해야 하므로 결과적으로 여전히 동기(Synchronous) 방식에 해당합니다.
Select / Poll / Epoll 모델에서 소켓 자체는 Non-Blocking이지만 “이제 처리해도 된다”라는 시점을 응용 프로그램이 직접 판단한다는 점에서 비동기 I/O와는 명확히 구분됩니다.
철수 : 이 서류 검토 좀 부탁드려도 될까요?
민수 : 네, 진행 중입니다.
철수 : (잠시 후) 다 됐나요?
민수 : 아직입니다.
철수 : (다른 업무 진행 후 다시) 이제는 됐나요?
민수 : 네, 방금 완료됐습니다.
철수 : 알겠습니다. 바로 확인할게요.
철수는 결과를 기다리며 멈추지는 않지만, 서류가 완료되었는지 직접 반복해서 확인해야 합니다.
운영체제가 제공하는 진정한 비동기 I/O 모델입니다. I/O 요청 이후의 처리와 완료 관리를 Kernel이 전적으로 담당하며, 응용 프로그램은 I/O 완료 시점을 이벤트나 완료 통지 방식으로 전달받습니다.
이 모델에서는 스레드가 I/O 완료를 기다리지 않으며, 이미 완료된 작업만을 처리하므로 매우 높은 확장성과 효율을 가집니다.
이러한 모델은 대규모 동시 접속을 처리해야 하는 MMORPG 서버, 고성능 네트워크 서버에서 사실상 필수적인 구조입니다. 대표적으로 다음에 다룰 IOCP Model입니다.
철수 : 이 서류 검토 좀 맡아줄 수 있을까요?
민수 : 네 알겠습니다. 끝나면 바로 알려드릴게요.
철수 : 알겠습니다. (다른 업무 진행 중...)
민수 : 검토 완료됐습니다. 결과 전달드릴게요!
철수 : 네, 알려줘서 고마워요 바로 확인할게요
철수는 "됐나?"라고 계속 확인하지 않고, 완료 시점은 민수가 주도적으로 통지합니다. 그렇기에 철수는 "완료된 결과"만 받아 처리합니다.
Kernel은 운영체제(OS)에서 마치 작업 관리자처럼 시스템 전반의 자원을 통제하고 관리하는 핵심 구성 요소입니다. Kernel에서는 중요한 자원을 관리하기 때문에 사용자가 해당 자원에 쉽게 접근하지 못하도록 보호하기 위해 User Mode와 Kernel Mode로 나누어집니다.
위키백과
컴퓨터 과학에서 커널(kernel)은 컴퓨터 운영 체제의 핵심이 되는 컴퓨터 프로그램으로, 시스템의 모든 것을 완전히 제어(control)한다.[1] 운영 체제의 다른 부분 및 응용 프로그램 수행에 필요한 여러 가지 서비스를 제공한다.
나무위키
커널(Kernel)은 운영체제의 핵심이라 할 수 있는 매우 중요한 소프트웨어로 하드웨어의 자원을 자원이 필요한 프로세스에 나눠주고, 덩달아 프로세스 제어(작업 관리), 메모리 제어, 프로그램이 운영 체제에 요구하는...
지금까지 TCP, UDP 서버와 클라이언트를 직접 프로그래밍하면서 Kernel은 매우 많은 역할을 수행해 왔습니다.
소켓 생성부터 패킷의 송·수신, TCP 상태 관리, 버퍼링, 재전송 처리까지, 실제 네트워크 I/O의 대부분은 Kernel 내부에서 처리됩니다. 응용 프로그램은 단지 send(), recv()와 같은 인터페이스를 통해 Kernel에 작업을 요청할 뿐입니다.
Q1. 그러면 이렇게 중요한 Kernel은 비동기 I/O에서 다루는 이유가 있나요?
처음에는 당연히 이런 의문이 들 수 있습니다. 하지만 비동기 I/O에서 Kernel 존재를 모르고 지나친다면 비동기 I/O을 제대로 이해했다고 말할 수 없기에 Kernel을 비동기 I/O에서 다루는 것입니다.
비동기 I/O에서는 I/O 요청은 User Mode에서 시작되고, 그 이후의 실제 작업은 Kernel이 처리합니다. 그동안 User Mode의 스레드는 대기하지 않고 다른 작업을 수행할 수 있습니다.
User Mode란? 일반 응용 프로그램이 실행되는 영역입니다. 우리가 작성하는 게임 서버, 클라이언트, 툴 프로그램은 모두 User Mode에서 동작한다.
아래와 같은 특징이 있습니다.
반대로 Kernel Mode란? 운영체제의 핵심 영역이며 CPU, 메모리, 디스크, 네트워크 카드 등 모든 시스템 자원을 직접 제어할 수 있습니다.
아래와 같은 특징이 있습니다.
이번 글에서는 IOCP를 이해하기 위한 사전 개념으로, 동기(Synchronous)와 비동기(Asynchronous)의 차이, 그리고 비동기 I/O에서 Kernel이 맡는 역할에 대해 살펴보았습니다.
특히 비동기 I/O는 단순히 “기다리지 않는다”는 개념이 아니라, I/O 요청 이후의 처리와 완료 관리를 Kernel에 맡기는 구조라는 점을 이해하는 것이 중요합니다.
이제 비동기 I/O와 Kernel의 역할을 이해했으므로, 다음 글에서는 이러한 개념들이 실제로 어떻게 구현되는지, Windows 환경에서의 IOCP(I/O Completion Port) 모델을 중심으로 그 구조와 동작 원리를 본격적으로 배워보겠습니다.
[위키백과(커널, 비동기]
[MS]
[나무위키]
[교보문구]
[블로그]