C10K 문제
C10K. 영어로 Connection 10,000, 곧 1만 개의 동시 연결이라는 뜻입니다. 1999년에 한 엔지니어가 던진 이 질문은 네트워크 프로그래밍의 역사에서 하나의 분기점이 되었습니다. 한 대의 서버가 1만 명을 동시에 감당하려면 어떻게 해야 하는가. 이 장에서 그 문제의 본질을 짚고, 왜 PART 04의 방법들로는 부족한지를 분명히 합니다. 이 PART 전체가 이 질문에 대한 답입니다.
1만 명이 왜 어려운가
PART 04에서 우리는 동시 접속을 잘 다뤘습니다. 그런데 그때의 클라이언트는 셋이었습니다. 1만 명은 차원이 다른 문제입니다. 왜 그럴까요.
스레드나 프로세스 방식을 떠올려 봅시다. 손님마다 종업원을 한 명씩 붙이는 방법이었습니다. 1만 명이면 종업원 1만 명입니다. 스레드 하나가 차지하는 메모리가 적지 않은데, 1만 개면 그것만으로 막대한 메모리를 잡아먹습니다. 게다가 운영체제가 1만 개의 스레드를 번갈아 실행시키느라 치르는 전환 비용이 눈덩이처럼 불어납니다. 정작 일은 못 하고 일꾼을 바꾸는 데 시간을 다 써 버리는 상황이 옵니다.
그러면 select는 어떨까요. 한 흐름으로 여러 소켓을 지켜보는 영리한 방법이었습니다. 하지만 select에는 보통 1024개라는 상한이 있어 1만 개는 아예 지켜볼 수조차 없습니다. poll은 그 상한을 풀지만, 더 근본적인 문제가 남습니다.
진짜 병목, 매번 전부를 훑기
select와 poll의 진짜 한계는 호출할 때마다 등록된 소켓 전부를 훑는다는 데 있습니다. 이 점을 정확히 이해하는 것이 이 PART의 핵심입니다.
상황을 그려 봅시다. 1만 개의 연결이 맺어져 있는데, 그 순간 실제로 데이터가 도착한 것은 그중 열 개뿐이라고 합시다. 흔한 상황입니다. 대부분의 연결은 대개 조용히 있고 가끔만 말을 거니까요. 그런데 select나 poll을 부르면, 운영체제는 준비된 열 개를 찾기 위해 1만 개를 처음부터 끝까지 훑습니다. 쓸모 있는 일은 열 개에 대한 것인데, 1만 개를 검사하는 헛수고를 매번 반복하는 것입니다.
연결이 늘어날수록 이 헛수고는 비례해서 커집니다. 연결이 두 배가 되면 검사 비용도 두 배가 됩니다. 이렇게 일의 양이 연결 수에 그대로 비례해 늘어나는 구조로는, 1만 개의 벽을 넘을 수 없습니다. 연결을 늘릴수록 검사에 드는 시간이 길어져, 어느 순간 서버가 검사만 하다 지쳐 버립니다.
발상의 전환, 준비된 것만 알려 달라
해법은 발상의 전환에 있습니다. 매번 전부를 훑는 대신, 운영체제가 준비된 소켓만 콕 집어 알려 주면 어떨까요. 1만 개를 지켜보더라도, 데이터가 도착한 열 개만 알려 준다면 우리는 그 열 개만 처리하면 됩니다. 조용한 9990개는 검사할 필요조차 없습니다.
이것이 가능하려면 운영체제가 지켜볼 소켓들을 미리 기억해 두고, 어떤 소켓에 일이 생기면 그것만 따로 모아 두는 방식이어야 합니다. 그러면 우리가 물을 때 전부를 훑지 않고 모아 둔 것만 건네줄 수 있습니다. 일의 양이 전체 연결 수가 아니라 실제로 준비된 소켓 수에만 비례하게 됩니다. 연결이 1만 개든 10만 개든, 그 순간 바쁜 소켓이 열 개라면 열 개만큼만 일하면 됩니다.
리눅스에서 이 방식을 구현한 것이 epoll이고, macOS와 BSD에서는 kqueue입니다. 둘은 이름과 사용법이 다르지만 발상은 똑같습니다. 준비된 것만 알려 준다는 것입니다. 바로 이 발상이 C10K 문제를 푼 열쇠입니다.
이 PART의 여정
C10K 문제는 단순한 옛날이야기가 아닙니다. 오늘날 우리가 쓰는 거의 모든 고성능 서버, 그리고 Python의 asyncio, Node.js, Go, Rust의 비동기 시스템이 모두 이 문제를 푸는 과정에서 태어났습니다. 그 뿌리를 이해하면 현대의 모든 비동기 기술이 한 줄기로 보입니다.
다음 장에서 그 핵심인 epoll을 직접 만나 봅니다. 준비된 것만 알려 주는 마법이 코드에서 어떻게 펼쳐지는지, 그리고 그 안에 숨은 또 하나의 중요한 선택인 동작 방식의 차이까지 깊이 파고들겠습니다.