main-logo

블로킹(blocking)과 논블록킹(non-blocking)

왜 현대 서버는 논블로킹 비동기를 표준으로 삼을까?

profile
강영민
2025년 08월 25일 · 0 분 소요

들어가며

최근 웹 서비스, 클라우드, 그리고 AI 애플리케이션 개발에서 성능과 효율성은 점점 더 중요한 요소가 되고 있습니다. 특히 Node.js나 Python의 AsyncIO, FastAPI 같은 비동기 기반 프레임워크가 주류로 떠오르면서, 자연스럽게 블로킹(Blocking) vs 논블로킹(Non-Blocking), 동기(Synchronous) vs 비동기(Asynchronous) 라는 개념을 자주 접하게 됩니다.

처음에는 용어가 헷갈릴 수 있으며 비슷해 보이지만, 이 네 가지는 서로 다른 관점에서 시스템 동작을 설명합니다.

이번 포스팅에서는 해당 주제에 대해서 이해를 돕기위해 최소한의 개념을 일상의 예시와 함께 설명해보려 합니다.


블로킹 vs 논블로킹

블로킹(Blocking)

  • 정의: 요청한 작업이 끝날 때까지 실행이 멈춰 있는 방식
  • 일상 비유: 식당에서 음식을 주문후 음식이 나오기까지 카운터 앞에서 조리 끝날 때까지 기다려야 하는 상황
  • 특징: 단순하고 직관적이지만 기다리는 동안 다른 일을 하지 못한다
const fs = require('fs');
console.log("A: 파일 읽기 시작"); 
const data = fs.readFileSync("data.txt", "utf8"); // 파일 읽기 완료 전까지 멈춤 
console.log("B: 파일 내용 →", data); 
console.log("C: 다음 작업");

실행 순서: A → B(내용) → C

논블로킹(Non-Blocking)

  • 정의: 요청 후 결과를 기다리지 않고 바로 다음 작업으로 넘어가는 방식
  • 일상 비유: 식당에서 음식을 주문하고 자리에 앉아 다른 일을 하다가 알림이 울리면 음식을 받는 상황
  • 특징: 효율적 동시에 여러 작업을 진행 가능하지만 코드가 복잡해질 수 있음

const fs = require('fs');
console.log("A: 파일 읽기 시작"); 
fs.readFile("data.txt", "utf8", (err, data) => { 
  if (err) throw err; 
    console.log("C: 파일 내용 →", data); // 파일 준비되면 실행 }); 
    console.log("B: 다음 작업");

실행 순서: A → B → (나중에) C(내용)

동기 vs 비동기

동기(Synchronous)

  • 정의: 동기는 직렬적인 처리 방식입니다 한 가지 작업이 완료될 때까지 다음 작업을 시작하지 않고 기다리는 것
  • 일상 비유: 은행 창구 업무과 같이 A→B→C 순차적으로 고객의 업무를 처리하는것과 같이 직렬적인 작업처리
  • 특징: 직관적, 디버깅 쉬움, 하지만 병목현상 발생 가능

비동기(Asynchronous)

  • 정의: 비동기는 병렬적인 처리 방식입니다 한 가지 작업을 요청해놓고 해당 작업이 완료되기를 기다리지 않고 바로 다음 작업을 진행하는 것이며 완료시 나중에 알려달라고 부탁하는 방식
  • 일상 비유: 음식을 배달 주문을 하고 음식이 도착하는 동안 기다리지 않고 그동안 다른 일들을 하며 배달이 완료 되었다는 알림이 오면 잠시 하던일을 멈추고 음식을 받는것과 같이 병렬적인 작업처리
  • 특징: 동시성이 확보 가능하지만 콜백 지옥, 복잡성 증가 → Promise, async/await 등장

동기/비동기블로킹/논블로킹

핵심 구분

  • 동기/비동기: "작업의 순서를 맞추는가?" 에 대한 개념. 요청과 결과의 타이밍을 맞추는지에 대한 관점입니다.
  • 블로킹/논블로킹: "제어권을 반납하는가?" 에 대한 개념. 특정 함수나 작업이 제어권을 가지고 있는지에 대한 관점입니다.

네 가지 조합

이 두 가지 개념을 결합하면 네 가지 조합이 가능하며, 각각 다른 동작 방식을 보입니다.

1. 동기 & 블로킹 (Synchronous & Blocking)

이 조합이 가장 흔하고 직관적인 방식입니다. 호출한 함수는 호출된 함수의 작업이 완료될 때까지 기다리고(동기), 그 동안 제어권을 완전히 넘겨주어 다른 일을 할 수 없습니다(블로킹).

  • 비유: 친구에게 "사과를 사다 줘."라고 부탁하고, 친구가 돌아올 때까지 문 앞에서 기다리는 상황. 기다리는 동안 다른 일(TV 보기, 청소)은 할 수 없습니다.

2. 동기 & 논블로킹 (Synchronous & Non-Blocking)

호출한 함수는 호출된 함수의 작업이 완료되었는지 계속 확인하지만(동기), 그 동안 제어권이 반납되어 다른 일을 할 수 있습니다(논블로킹).

  • 비유: 친구에게 "사과를 사다 줘."라고 부탁하고, 친구에게서 전화가 올 때까지 주기적으로 "다 왔니?"라고 물어보며 집에서 다른 일을 하는 상황.

3. 비동기 & 블로킹 (Asynchronous & Blocking)

이 조합은 흔하지 않으며, 효율성이 떨어져 잘 사용되지 않습니다. 호출한 함수는 결과를 기다리지 않지만(비동기), 제어권이 반납되지 않아 다른 일을 할 수 없는(블로킹) 모순적인 상황입니다.

  • 비유: 친구에게 "사과를 사다 줘. 다 사면 나한테 전화해."라고 말하고, 친구가 돌아오기 전까지 아무것도 안 하고 문 앞에서 멍하게 서 있는 상황.

4. 비동기 & 논블로킹 (Asynchronous & Non-Blocking)

이 조합이 웹서버인 Nginx 같은 고성능 서버나 Node.js의 이벤트 루프에서 주로 사용하는 방식입니다. 호출한 함수는 작업 완료를 기다리지 않고(비동기), 제어권이 반납되어 다른 일을 할 수 있습니다(논블로킹).

  • 비유: 친구에게 "사과를 사다 줘. 다 사면 전화해."라고 말하고, 나는 집에서 편하게 TV를 보거나 청소를 하는 상황. 친구의 전화가 오면 그때 사과를 받습니다.

async/await과의 관계

async/await은 논블로킹 I/O를 동기 코드처럼 표현할 수 있게 하는 문법입니다.


const fs = require("fs/promises");
(async () => { 
console.log("A"); 
const p = fs.readFile("data.txt", "utf8"); // 요청만 던짐 (논블로킹) 
console.log("B"); 
const data = await p; // 여기서만 결과 기다림 (함수 내부 흐름만 멈춤) 
console.log("C: 파일 내용 →", data); })();
  • await은 프로세스를 멈추는 블로킹이 아님
  • 단지 현재 함수의 실행만 멈추고, 이벤트 루프는 다른 작업을 계속 처리

실무에서의 고려사항

1) Sync API (...Sync) 남용 금지

  • 서버 요청 처리 중 fs.readFileSync 같은 동기 API를 사용하면 이벤트 루프 전체가 멈춤

2) CPU 바운드 작업 분리

  • 이미지 변환, 암호화, 대규모 파싱 → 워커 스레드, 별도 마이크로서비스로 분리

3) 네트워크 호출 관리

  • 비동기라도 무한 대기 상태는 위험 → 타임아웃, 재시도, 서킷브레이커 패턴 필요

4) 대용량 데이터 처리

  • 대규모 파일은 스트리밍 API(createReadStream) 사용
  • DB 쿼리도 페이징/스트리밍 접근

언제 어떤 방식을 쓸까?

  • CLI 스크립트, 초기화 코드 → 블로킹/동기 방식도 단순하고 OK
  • 웹 서버 요청 처리 → 반드시 논블로킹 + 비동기
  • 복잡한 비즈니스 로직 → async/await로 가독성과 유지보수성 확보
  • CPU 무거운 연산 → 워커 스레드/별도 프로세스로 분리

마치며

정리해보면, 블로킹과 논블로킹의 차이는 결국 “기다리느냐, 기다리지 않느냐” 에 있다고 생각합니다 블로킹은 결과가 나올 때까지 멈춰 기다리는 단순하고 직관적인 방식이지만, 효율성 측면에서는 많은 자원을 낭비할 수 있습니다. 반대로 논블로킹은 기다리지 않고 다른 작업을 이어갈 수 있어 현대의 대규모 서버 개발에서 표준처럼 자리 잡았죠.

또한 동기와 비동기의 구분은 “작업이 순서대로 진행되느냐, 아니면 나중에 결과를 이어받느냐”의 관점에서 이해할 수 있습니다. 동기는 코드가 직렬적으로 흘러가기 때문에 이해하기 쉽고 디버깅에도 편리하지만, 복잡한 환경에서는 병목이 발생하기 쉽습니다 반대로 비동기는 복잡성을 감수해야 하지만, 대규모 요청을 동시에 처리할 수 있다는 장점이 있습니다.

현대의 서버 개발에서는 대부분 논블로킹 비동기가 필수처럼 여겨지지만, 그렇다고 블로킹이나 동기적 표현이 불필요한 것은 아닙니다. 작은 스크립트나 초기화 과정처럼 단순성이 더 중요한 순간에는 오히려 블로킹 방식이 직관적이고 유리할 때도 있습니다. 그리고 async/await 같은 문법은 이러한 논블로킹 비동기 구조를 동기 코드처럼 깔끔하게 작성할 수 있게 해주어 개발자가 구조적 복잡성과 가독성 사이에서 균형을 잡도록 도와줍니다.

결국 중요한 것은 “무조건 논블로킹/비동기만이 답이다”가 아니라, 서비스의 성격과 규모, 그리고 작업의 특성에 맞는 방식을 선택하는 일입니다. 특히 서비스 규모가 커질수록 이벤트 루프를 불필요하게 막지 않는 습관을 들이고, CPU 집약적 작업(수학 계산, 암호화, 압축/해제, 이미지/동영상 처리, 대용량 데이터 파싱 등등)은 워커 스레드나 별도 서비스로 분리하는 등 아키텍처적 고려가 필수적일것 같습니다

읽어주셔서 감사합니다.