iBetter Books
수정

한 번에 한 명만 받는 서버의 한계

PART 02에서 만든 에코 서버를 다시 떠올려 봅시다. accept로 한 클라이언트를 받아, 그 클라이언트가 떠날 때까지 대화를 나눴습니다. 잘 동작했지만, 거기에는 치명적인 약점이 숨어 있습니다. 그 클라이언트와 대화하는 동안 다른 클라이언트는 전혀 받을 수 없다는 것입니다. 이 장에서는 그 약점의 정체, 곧 블로킹의 본질을 파헤칩니다. 해법은 다음 장부터 다루지만, 문제를 정확히 아는 것이 먼저입니다.

블로킹이라는 멈춤

PART 01에서 accept를 설명하며 잠깐 블로킹이라는 말을 꺼냈습니다. 이제 그 의미를 제대로 들여다볼 때입니다.

블로킹이란 어떤 함수가 할 일을 마칠 때까지 그 자리에서 프로그램을 멈춰 세우는 것을 말합니다. accept는 클라이언트가 연결해 올 때까지 멈춥니다. recv는 데이터가 도착할 때까지 멈춥니다. 우리가 지금까지 쓴 소켓 함수는 기본적으로 모두 이렇게 블로킹으로 동작합니다.

평소에는 이 멈춤이 고맙습니다. 데이터가 올 때까지 알아서 기다려 주니 우리는 그저 recv를 부르기만 하면 됩니다. 그런데 클라이언트가 둘 이상이 되면 이 멈춤이 발목을 잡습니다.

두 번째 손님은 왜 기다리는가

PART 02의 서버 흐름을 따라가 봅시다. 서버는 accept로 첫 클라이언트를 받은 뒤, 그 클라이언트와 recvsendall을 반복합니다. 자, 이때 두 번째 클라이언트가 연결을 시도하면 어떻게 될까요.

서버는 지금 첫 클라이언트의 recv에 매달려 있습니다. 첫 클라이언트가 다음 데이터를 보내기 전까지 그 recv에서 멈춰 있습니다. 두 번째 클라이언트를 받으려면 다시 accept로 돌아가야 하는데, 그러질 못합니다. 첫 클라이언트와의 대화에 묶여 있기 때문입니다. 두 번째 손님은 첫 손님이 완전히 떠날 때까지 문밖에서 하염없이 기다려야 합니다.

직접 확인해 볼 수도 있습니다. PART 02의 에코 서버를 띄워 두고, 클라이언트를 두 개 띄워 보세요. 첫 클라이언트는 잘 동작하지만, 둘째 클라이언트는 첫째가 끝나기 전까지 응답을 받지 못합니다. 손님이 한 줄로 서서 한 명씩만 처리되는 식당인 셈입니다.

식당 비유로 보는 세 가지 길

이 문제를 푸는 방법을 식당에 빗대어 미리 그려 보겠습니다. 손님이 몰리는 식당의 주인은 어떻게 할까요. 세 가지 길이 있습니다.

첫째, 손님마다 종업원을 한 명씩 붙입니다. 손님이 올 때마다 새 종업원을 고용해 전담시키는 방식입니다. 이것이 다음 장에서 배울 프로세스와 스레드 방식입니다. 직관적이고 강력하지만, 종업원을 무한정 고용할 수는 없습니다.

둘째, 주인 한 명이 모든 테이블을 부지런히 돌며 주문을 받습니다. 한 테이블에 매달리지 않고, 준비된 테이블만 골라 빠르게 처리합니다. 이것이 그다음에 배울 I/O 멀티플렉싱, 곧 select와 poll 방식입니다. 한 가닥의 흐름으로 여러 손님을 상대하는 영리한 방법입니다.

셋째, 둘째 방식을 극한까지 끌어올려 수만 명도 거뜬히 상대하는 방법입니다. 이것이 PART 06에서 다룰 epoll 같은 고성능 기법입니다.

블로킹을 넘어서기 위한 두 가지 발상

세 가지 길은 결국 두 가지 큰 발상으로 나뉩니다.

하나는 흐름을 여럿으로 늘리는 것입니다. 블로킹이 문제라면, 멈춰도 괜찮은 흐름을 손님 수만큼 만들면 됩니다. 한 흐름이 한 손님의 recv에서 멈춰 있어도, 다른 흐름이 다른 손님을 상대하면 됩니다. 프로세스와 스레드가 이 발상입니다.

다른 하나는 멈추지 않는 것입니다. 소켓을 블로킹하지 않도록 바꾸고, 여러 소켓 중 지금 당장 처리할 수 있는 것이 무엇인지 운영체제에게 물어 그것만 처리합니다. 어느 것에도 매달려 멈추지 않으니 한 흐름으로 여럿을 상대할 수 있습니다. select, poll, epoll이 이 발상입니다.

두 발상 모두 장단점이 있고, 실무에서는 상황에 따라 골라 씁니다. 다음 장부터 하나씩 직접 만들어 보며 그 차이를 몸으로 익히겠습니다. 먼저 더 직관적인 프로세스와 스레드부터 시작합니다.