백엔드나 네트워크 프로그래밍 관련 레퍼런스를 보다보면, Blocking / Non-Blocking 이라는
용어를 자주 접할 수 있습니다. 특히 Node.js, 이벤트 루프, 비동기 처리 등을 이해하려면
반드시 알아야 하는 개념이기도 합니다.
많은 사람들이 Blocking == Sync, Non-Blocking == Async 라고 생각하는 경우가 많지만,
실제로 네 가지 개념은 서로 다른 기준을 가진 개념입니다.
이번 글에서는 Blocking 과 Non-Blocking 의 개념과 두 방식의 차이, 실제 동작 흐름, 4가지 조합 등에 대해 정리해보고자 합니다. 동기와 비동기는 이전 포스팅 에서 다뤘으니 참고 부탁드립니다.
먼저 Blocking 과 Non-Blocking 의 기준은 "제어권이 언제 돌아오는가" 입니다.
Blocking 은 작업이 끝날 때까지 제어권을 반환하지 않지만,
Non-Blocking 은 작업을 요청하면 바로 제어권을 반환하는 데 차이가 있습니다.
작업이 끝날 때까지 스레드가 기다립니다.
Thread
│
│ request()
│──────────── 기다림 ────────────│
│ │
│ 결과 반환
│
│ 다음 코드 실행
// 파일 읽기 요청 예시 코드
const data = fs.readFileSync("file.txt")
console.log(data)
console.log("next task")
위 예시 코드의 동작 순서는 파일 읽기 요청이 이뤄지고 -> 파일 읽기 완료까지 대기 -> 결과 반환 -> 다음 코드 실행으로 이뤄집니다.
작업 요청 후 기다리지 않고 바로 다음 코드를 실행합니다.
Thread
│
│ request()
│
│ 다음 작업 실행
│
│ 다른 작업 수행
│
│ (작업 완료)
│ │
│ callback 실행
// 파일 읽기 요청 예시 코드
fs.readFile("file.txt", (err, data) => {
console.log(data)
})
console.log("next task")
동일하게 파일 읽기 요청 예시 코드를 살펴보았을 때, 논블로킹으로 작성된 위 코드의 흐름은 파일 읽기 요청 -> 바로 다음 코드 실행 -> 파일 읽기 완료 -> callback 실행 순서로 이뤄집니다.
Sync 와 Async 의 기준을 정리해보면, "결과를 어떻게 받는가" 입니다.
Sync 는 결과를 요청한 함수가 직접 받고, Async 는 결과를 callback / promise / event 로 나중에 받습니다.
function 호출
↓
작업 수행
↓
결과 반환
↓
다음 코드 실행
// 동기 함수 예시 코드
function add(a, b) {
return a + b
}
const result = add(1,2)
console.log(result)
function 호출
↓
바로 return
↓
작업 수행
↓
callback / promise 실행
// 비동기 함수 예시 코드
fetch("/data")
.then(res => res.json())
.then(data => console.log(data))
많은 상황에서 다음의 조합처럼 사용되기 때문입니다.
// 대표적으로 Java I/O
Blocking + Sync
// Node.js
Non-Blocking + Async
그림으로 정리하면 다음과 같습니다.
Thread
│
│ request()
│──────── 기다림 ────────│
│
│ result
Thread
│
│ request()
│
│ 바로 다음 작업
│
│ 작업 완료 이벤트
function()
↓
result 반환
↓
다음 코드 실행
function()
↓
바로 return
↓
나중에 callback 실행
Blocking / Non-Blocking 과 Sync / Async 는 독립적인 개념으로써 다음 4가지 조합이 가능합니다.
가장 전통적인 방식입니다. (대표적으로 readFileSync() 함수)
request
↓
작업 수행
↓
완료까지 대기
↓
결과 반환
대표적인 예시가 Node.js 입니다.
request
↓
바로 return
↓
다른 작업 수행
↓
작업 완료
↓
callback 실행
// 예시
fs.readFile("file.txt", callback)
이 방식은 polling 구조입니다.
request
↓
결과 확인
↓
아직 안됨
↓
다시 확인
// 예시
while(!done) {
check()
}
CPU 를 많이 사용하기 때문에 비효율적일 수 있습니다.
조금 특이한 케이스입니다.
request
↓
작업 완료까지 기다림
↓
callback 실행
// 예시
Future.get()
Node.js 는 대표적인 Non-Blocking + Async 구조입니다.
요청
↓
I/O 작업 등록
↓
Event Loop 계속 실행
↓
I/O 완료
↓
Callback 실행
그래서 Node.js는 싱글 스레드지만 많은 요청을 동시에 처리할 수 있습니다.
가장 중요한 포인트는 Blocking / Non-Blocking 는 제어권 반환 시점이 기준이고, Sync / Async 는 결과 처리 방식이라는 것. 그리고 이 두 개념은 서로 다른 기준이기 때문에 조합이 가능하다는 점입니다.