소켓 — 코드가 네트워크를 만지는 창구
지금까지 본 TCP·UDP·IP는 운영체제가 처리한다. 그럼 우리 프로그램은 그걸 어떻게 쓸까? 그 창구가 **소켓(socket)**이다. 소켓은 네트워크 연결의 끝점으로, 운영체제가 파일 디스크립터로 다룬다 — 그래서 네트워크도 읽고 쓰는 파일처럼 다뤄진다. 이 작은 API 하나를 어떻게 쓰느냐가 — 서버가 동시 접속을 몇 개나 감당하느냐를 가르는, 네트워크 프로그래밍의 핵심 갈림길이다.
연결을 맺는 절차 — bind·listen·accept
TCP 서버와 클라이언트가 소켓으로 연결을 맺는 순서는 정해져 있다.
서버는 소켓을 만들어(socket) 포트에 묶고(bind) 연결을 기다리다(listen), 들어온 연결을 받아들인다(accept) — 이 accept가 반환하는 새 소켓으로 그 클라이언트와 read/write한다(앞의 TCP 3-way 핸드셰이크가 바로 이 사이에서 일어난다). 클라이언트는 소켓을 만들어(socket) 서버에 연결한다(connect). 단순해 보이는 이 절차에서 진짜 문제는 — read를 호출했는데 데이터가 아직 안 왔을 때 무엇을 하느냐다.
막힘이 문제다 — blocking과 C10K
기본 소켓은 blocking이다 — read를 부르면 데이터가 올 때까지 그 스레드가 멈춰 기다린다. 그래서 가장 단순한 서버는 연결마다 스레드 하나를 붙인다(thread-per-connection). 연결이 수십 개면 괜찮다. 그런데 *동시 접속이 1만 개(C10K)*가 되면? 스레드 1만 개는 스택 메모리(수 GB)와 컨텍스트 스위칭으로 무너진다 — 게다가 대부분은 데이터를 기다리며 놀고 있다. 연결 수에 비례해 스레드를 늘리는 모델은 여기서 한계에 부딪힌다.
한 스레드가 여럿을 — non-blocking과 I/O 멀티플렉싱
해법은 소켓을 non-blocking으로 바꾸고, 한 스레드가 수많은 소켓을 감시하는 것이다. 데이터가 준비된 소켓만 골라 처리하면, 스레드는 놀지 않고 일한다. 이 "여러 소켓을 한꺼번에 감시"가 I/O 멀티플렉싱인데, 그 도구가 진화해 왔다.
옛 select·poll은 매 호출마다 감시 대상 전체를 스캔해서 — 소켓이 많아질수록 그 자체가 느려졌다(O(n)). 리눅스의 epoll(BSD의 kqueue)은 이를 뒤집었다 — 커널이 "준비된 소켓 목록만" 돌려줘서, 감시 대상이 1만 개여도 실제 일이 생긴 것만 O(1)에 가깝게 처리한다. (출처: select vs poll vs epoll.) 이벤트 알림 방식엔 레벨 트리거(준비돼 있으면 계속 알림)와 엣지 트리거(상태가 바뀐 순간만 알림, non-blocking 필수)가 있는데, 엣지 트리거가 더 효율적이지만 읽을 수 있는 만큼 다 읽어야 한다.
왜 이게 핵심인가 — 바로 이 epoll 위에 고성능 네트워크 서버들이 선다. *"한 스레드가 epoll로 수많은 연결을 논블로킹하게 다룬다"*는 이벤트 루프 모델이 — 수만 동시 연결을 소수의 스레드로 감당하는 비결이다. 핵심 한 문장은 "기다리는 동안 다른 일을 한다" — 한 연결의 응답을 기다리느라 스레드를 묶어 두지 않고, 그 시간에 다른 연결을 처리하는 것이다.
또 다른 길 — 경량 스레드
epoll·이벤트 루프는 강력하지만 코드가 콜백·비동기로 복잡해진다. 그래서 반대 길을 택하는 런타임도 있다 — blocking 코드를 그대로 쓰되, 스레드를 너무 싸게 만들어 수만 개를 띄우는 경량 스레드 방식이다. 겉보기엔 *연결당 스레드(읽기 쉬움)*인데, 내부적으로는 블로킹 지점에서 OS 스레드를 양보해 — 적은 OS 스레드로 많은 동시성을 낸다. 즉 *같은 목표(많은 동시 연결)*를 *이벤트 루프(논블로킹)*와 *경량 스레드(싼 블로킹)*가 다른 방식으로 푼다.
실무에서는 — 파일 디스크립터 한계가 의외의 병목이다. 소켓도 fd라, 동시 연결이 많은 서버는
ulimit -n(프로세스당 fd 상한)을 충분히 올려야 "Too many open files"로 안 죽는다(TIME_WAIT 소켓 누적과 함께 챙길 자원 한계다, TCP 편). 그리고 대부분의 앱 개발자는 epoll을 직접 안 짜고 — 프레임워크가 그 위에 올린 추상을 쓴다. 다만 왜 빠른지, 어디서 막히는지를 알려면 이 바닥(소켓·epoll)을 이해해야 한다.
정리하면, 소켓은 프로그램이 TCP/UDP를 다루는 파일 디스크립터다 — 서버는 socket→bind→listen→accept, 클라이언트는 socket→connect로 연결을 맺는다. 기본 blocking은 연결당 스레드라 C10K에서 무너지고, *non-blocking + I/O 멀티플렉싱(select→poll→epoll)*은 한 스레드가 수많은 연결을 감시해 이를 푼다(이벤트 루프). 경량 스레드는 싼 블로킹이라는 다른 길로 같은 곳에 닿는다. 결국 *"기다리며 스레드를 놀리지 마라"*가 — 서버 동시성의 한 줄 원리다.