2025-09-22 00:57
-
이 두 가지 개념의 조합을 통해 다양한 I/O 모델을 이해하고 시스템 성능을 최적화할 수 있다.
블로킹 논블로킹 완벽 정복 핸드북 동기 비동기 개념까지
개발자라면 누구나 한 번쯤은 마주치는 단어, 바로 ‘블로킹(Blocking)‘과 ‘논블로킹(Non-blocking)‘이다. 이 개념은 동기(Synchronous), 비동기(Asynchronous)와 얽히면서 많은 이들에게 혼란을 안겨준다. 하지만 이들의 차이를 명확히 이해하는 것은 효율적인 프로그램을 설계하고 성능을 최적화하는 데 있어 핵심적인 열쇠와 같다. 특히 네트워크 프로그래밍이나 대용량 데이터 처리와 같이 I/O 작업이 빈번한 환경에서는 그 중요성이 더욱 부각된다.
이 핸드북은 블로킹과 논블로킹의 근본적인 차이점부터 시작하여, 이들이 동기, 비동기 개념과 어떻게 상호 작용하는지, 그리고 실제 시스템에서 어떻게 활용되는지까지 심도 있게 탐구한다. 단순한 개념 나열을 넘어, 왜 이러한 개념들이 탄생했는지 그 배경부터 차근차근 파헤쳐 보자.
1. 모든 것의 시작 CPU와 I/O 장치의 속도 차이
블로킹과 논블로킹이라는 개념이 왜 필요하게 되었을까? 그 근원을 따라가 보면 컴퓨터 시스템의 핵심 구성 요소인 CPU와 I/O(Input/Output) 장치 간의 엄청난 속도 차이라는 본질적인 문제에 도달하게 된다.
CPU는 초당 수십억 번의 연산을 수행할 정도로 눈부시게 빠르다. 하지만 하드 디스크에서 파일을 읽거나, 네트워크를 통해 데이터를 주고받는 등의 I/O 작업은 기계적인 움직임이나 물리적인 신호 전달을 필요로 하기에 CPU의 속도를 전혀 따라가지 못한다.
요리사(CPU)와 재료 배달원(I/O)의 비유
유능한 요리사(CPU)가 파스타를 만든다고 상상해 보자. 면을 삶고 소스를 만드는 작업은 순식간에 해치울 수 있다. 그런데 갑자기 필요한 희귀한 트러플(데이터)이 창고(하드 디스크)에 있다는 것을 깨달았다. 요리사는 재료 배달원(I/O 장치)에게 트러플을 가져다 달라고 요청한다.
만약 이 요리사가 블로킹 방식으로 일한다면, 배달원이 트러플을 찾아서 주방으로 가져올 때까지 아무것도 하지 않고 하염없이 기다리기만 할 것이다. 다른 재료를 다듬거나, 채소를 썰거나, 심지어 다른 요리를 준비할 수도 있는데 말이다. 주방 전체의 효율성은 급격히 떨어진다.
반면 논블로킹 방식으로 일하는 요리사는 다르다. 배달원에게 트러플을 요청한 뒤, 바로 다른 일을 시작한다. 주기적으로 “트러플 다 가져왔어?”라고 확인하며 자신의 작업을 계속 진행한다. 배달이 완료되면 그때 트러플을 받아 요리에 사용한다. 기다리는 시간 동안 주방은 멈추지 않고 계속 돌아간다.
이 비유처럼, I/O 작업이 완료될 때까지 CPU가 아무 일도 못 하고 멈춰버리는 비효율을 해결하기 위해 논블로킹이라는 개념이 등장했다. 즉, 시스템의 전체 처리량(Throughput)을 높이기 위한 고민의 산물이 바로 이것이다.
2. 블로킹 vs 논블로킹 핵심은 제어권
블로킹과 논블로킹을 구분하는 가장 중요한 기준은 **‘제어권(Control)‘**이다. 함수 A가 함수 B를 호출했을 때, 함수 B가 자신의 작업을 마칠 때까지 함수 A의 제어권을 계속 가지고 있다면 ‘블로킹’이고, 호출 즉시 제어권을 함수 A에게 돌려준다면 ‘논블로킹’이다.
| 구분 | 설명 | 핵심 특징 |
|---|---|---|
| 블로킹 (Blocking) | 호출된 함수가 자신의 작업을 모두 마칠 때까지 호출한 함수에게 제어권을 돌려주지 않고 대기시킨다. | 호출한 함수는 호출된 함수의 작업 완료와 결과값을 동시에 받는다. |
| 논블로킹 (Non-blocking) | 호출된 함수가 작업을 완료하지 않았더라도, 호출한 함수에게 제어권을 즉시 돌려준다. | 호출한 함수는 호출된 함수의 작업 완료 여부를 스스로 계속 확인해야 한다. |
블로킹 I/O 모델
초기의 프로그래밍 모델은 대부분 블로킹 방식이었다. 코드를 작성하고 이해하기가 매우 직관적이기 때문이다.
function read_file(path) {
const file_data = system_call.read(path); // read 함수가 끝날 때까지 여기서 멈춤 (Blocking)
process(file_data); // 파일 읽기가 완료된 후에야 실행됨
return "Done";
}
위 코드에서 system_call.read(path) 함수는 파일을 다 읽어서 file_data에 담아줄 때까지 read_file 함수를 멈춰 세운다. 이 시간 동안 read_file 함수는 다른 어떤 일도 할 수 없다.
논블로킹 I/O 모델
논블로킹 방식에서는 호출 즉시 결과(데이터가 준비되었든, 아직 안 되었든)를 반환하고 제어권을 넘겨준다.
function read_file_non_blocking(path) {
system_call.read(path); // 즉시 반환 (Non-blocking)
while(true) { // 계속 확인하는 로직 (Polling)
const status = system_call.check_status(path);
if (status === "DONE") {
const file_data = system_call.get_data(path);
process(file_data);
break;
}
}
return "Done";
}
호출한 함수는 제어권을 돌려받았지만, 일이 끝났는지는 알 수 없다. 따라서 “일 다 끝났니?”라고 계속 물어보는 과정(Polling)이 필요하다. 이는 CPU 자원을 불필요하게 낭비하는 단점이 될 수 있다.
3. 동기 vs 비동기 관심의 주체는 누구인가
블로킹/논블로킹이 ‘제어권’의 관점이었다면, 동기/비동기는 **‘작업 완료 여부를 신경 쓰는 주체’**의 관점이다. 즉, 호출된 함수의 결과나 종료를 호출한 함수가 직접 챙기는지, 아니면 다른 누군가(시스템, 콜백 함수 등)에게 맡기는지의 차이다.
| 구분 | 설명 | 핵심 특징 |
|---|---|---|
| 동기 (Synchronous) | 호출한 함수가 호출된 함수의 작업 완료를 직접 기다리거나, 주기적으로 확인하며 결과값을 챙긴다. | 작업의 순서와 결과 처리가 일치하고 예측 가능하다. |
| 비동기 (Asynchronous) | 호출한 함수가 호출된 함수의 작업 완료를 신경 쓰지 않는다. 작업이 완료되면 호출된 함수 쪽에서 콜백(Callback) 등을 통해 알려준다. | 호출한 함수는 자신의 로직을 계속 수행하며, 결과는 별도의 메커니즘으로 처리된다. |
레스토랑 웨이터(호출한 함수)와 주방(호출된 함수)의 비유
동기적 웨이터는 손님에게 주문을 받은 후, 주방에 주문서를 전달하고 요리가 완성될 때까지 주방 앞에서 계속 기다리거나, 주기적으로 “요리 다 됐나요?”라고 물어본다. 요리가 나와야만 다음 손님의 주문을 받으러 갈 수 있다.
비동기적 웨이터는 주문서를 주방에 전달한 뒤, 진동벨을 손님에게 나눠주고 바로 다른 테이블로 가서 주문을 받는다. 주방에서 요리가 완성되면 진동벨을 울려 알려주고, 웨이터는 그때 가서 음식을 가져다준다. 기다리는 시간 동안 다른 일을 효율적으로 처리할 수 있다.
이처럼 비동기 모델은 호출한 함수가 결과 처리에 얽매이지 않고 자신의 흐름을 이어갈 수 있게 해주므로 시스템 전체의 효율성을 극대화할 수 있다.
4. 네 가지 조합으로 완성되는 I/O 모델
이제 블로킹/논블로킹과 동기/비동기라는 두 가지 축을 조합하여 실제 I/O 모델에서 어떻게 나타나는지 살펴보자. 이 네 가지 조합을 이해하면 복잡한 시스템의 동작 방식을 명확하게 파악할 수 있다.
| 동기 (Synchronous) | 비동기 (Asynchronous) | |
|---|---|---|
| 블로킹 (Blocking) | Sync-Blocking | (일반적으로 조합되지 않음) |
| 논블로킹 (Non-blocking) | Sync-Non-blocking | Async-Non-blocking |
4.1. 동기-블로킹 (Synchronous-Blocking)
가장 직관적이고 흔한 모델. 함수를 호출하면 작업이 끝날 때까지 모든 것이 멈춘다. 호출한 함수는 작업 완료와 결과 수신을 모두 직접 챙긴다.
-
동작: A 함수가 B 함수를 호출 → B 함수가 끝날 때까지 A 함수는 대기(Block) → B 함수가 결과를 반환하면 그제야 A 함수는 다음 작업을 수행.
-
비유: 요리사가 배달원에게 재료를 요청하고, 배달원이 올 때까지 주방 입구에서 아무것도 안 하고 하염없이 기다리는 상황.
-
장점: 구현이 간단하고 코드 흐름을 이해하기 쉽다.
-
단점: I/O 작업 동안 시스템 전체가 멈출 수 있어 효율성이 매우 낮다.
4.2. 동기-논블로킹 (Synchronous-Non-blocking)
함수를 호출하면 일단 제어권은 바로 돌려받지만, 호출한 함수가 작업 완료 여부를 주기적으로 확인해야 한다.
-
동작: A 함수가 B 함수를 호출 → B 함수는 즉시 “아직 작업 중”이라고 반환하고 A 함수는 제어권을 돌려받음 → A 함수는 다른 일을 하다가 주기적으로 B 함수에게 “작업 끝났니?”라고 물어봄(Polling) → B 함수가 “끝났어”라고 답하면 결과를 가져와 처리.
-
비유: 요리사가 배달원에게 재료를 요청하고 바로 다른 일을 시작한다. 하지만 1분마다 창고에 가서 “재료 다 왔어?”라고 계속 확인한다.
-
장점: 블로킹 모델보다는 대기 시간을 활용할 수 있어 효율적이다.
-
단점: ‘작업이 끝났는지’ 계속 확인하는 과정(Polling) 자체가 CPU 자원을 소모시킨다.
4.3. 비동기-논블로킹 (Asynchronous-Non-blocking)
현대적인 애플리케이션에서 가장 널리 사용되는 고성능 모델. 함수를 호출하면 제어권을 즉시 돌려받고, 작업이 완료되면 시스템이나 콜백 함수를 통해 결과를 통지받는다.
-
동작: A 함수가 B 함수를 호출하면서 “이 일 끝나면 이 함수(콜백) 실행해 줘”라고 등록 → B 함수는 즉시 제어권을 A에게 반환 → A 함수는 자신의 다른 작업을 계속 수행 → B 함수의 작업이 시스템(커널) 수준에서 완료되면, 등록해 둔 콜백 함수가 실행되어 결과를 처리.
-
비유: 요리사가 배달원에게 재료를 요청하고 ‘배달 완료 알림 시스템’에 등록한다. 요리사는 다른 요리를 계속 만들고, 재료가 도착하면 시스템이 자동으로 알람을 울려 알려준다.
-
장점: 대기 시간 없이 CPU 자원을 최대한 효율적으로 사용할 수 있어 성능이 가장 뛰어나다.
-
단점: 코드의 흐름이 직관적이지 않고 복잡해질 수 있다(콜백 지옥 등).
이 모델은 I/O Multiplexing(select, poll, epoll 등)이나 Reactor, Proactor 패턴과 같은 기술을 통해 구현된다. 커널이 여러 I/O 작업을 동시에 감시하고, 준비된 작업이 생기면 애플리케이션에 알려주는 방식으로 동작한다.
5. 심화 내용: 언제 무엇을 사용해야 할까?
그렇다면 어떤 상황에서 어떤 모델을 선택해야 할까? 정답은 없다. 애플리케이션의 특성과 요구사항에 따라 최적의 선택이 달라진다.
-
동기-블로킹:
-
적합한 경우: 로컬에서 실행되는 간단한 스크립트, 내부적으로 스레드 풀을 통해 동시성을 처리하는 경우, 혹은 작업의 순차적 실행이 매우 중요한 경우.
-
예시: 간단한 파일 복사 프로그램.
-
-
동기-논블로킹:
-
적합한 경우: 여러 개의 I/O 작업을 동시에 처리해야 하지만, 각 작업의 완료를 즉시 알아야 하는 특정 실시간 시스템. 하지만 보통은 비동기 모델이 더 효율적이어서 잘 사용되지는 않는다.
-
예시: 실시간 게임 서버에서 여러 클라이언트의 입력을 빠르게 폴링해야 하는 경우.
-
-
비동기-논블로킹:
-
적합한 경우: 대규모 동시 접속을 처리해야 하는 웹 서버, 채팅 애플리케이션, 실시간 데이터 스트리밍 등 I/O 바운드 작업이 많은 고성능 시스템.
-
예시: Node.js, Nginx, Netty 기반의 서버 애플리케이션.
-
결론: 패러다임의 이해가 핵심이다
블로킹, 논블로킹, 동기, 비동기는 단순히 네 개의 기술 용어가 아니다. 이는 컴퓨터 시스템의 자원을 어떻게 효율적으로 활용할 것인가에 대한 깊은 고민에서 출발한 프로그래밍 패러다임이다.
-
블로킹 vs 논블로킹: 호출된 함수의 제어권 반환 시점에 대한 이야기.
-
동기 vs 비동기: 호출된 함수의 결과 처리를 누가 신경 쓰는지에 대한 이야기.
이 두 가지 관점을 명확히 분리하여 이해하고, 이들의 조합이 만들어내는 네 가지 시나리오를 머릿속에 그릴 수 있다면, 더 이상 이 개념들 앞에서 혼란스러워하지 않을 것이다. 오히려 주어진 문제 상황에 가장 적합한 아키텍처를 설계하고, 시스템의 잠재력을 최대한으로 끌어올리는 강력한 무기를 손에 쥔 것과 같다. 이 핸드북이 그 길잡이가 되기를 바란다.