프로그래밍에서 자주 사용되는 개념인 Synchronous/Asynchronous와 Blocking/Non-blocking은 서로 다른 차원의 작업 수행 방식을 설명하는 두 가지 개념이다. 이 개념들은 종종 혼용되거나 비슷한 것으로 오해하는 경우가 있는데, 실제로는 매우 다른 의미를 가지고 있다.
동기와 비동기는 작업 완료 여부를 신경 쓰는 방식과 관련이 있다. 즉, 어떤 작업이 시작되면 그 작업의 완료 여부를 기다리는지 또는 바로 다른 작업을 처리할 수 있는지에 따라 동기와 비동기로 나뉜다.
블로킹과 논블로킹은 작업을 요청한 후 해당 작업이 시스템 자원을 차단(blocking)하는지 여부에 대한 관점이다. 즉, 현재 작업이 다른 작업의 진행을 막느냐에 따라 블로킹과 논블로킹으로 나뉜다.

이처럼 두 개념에 대한 의미 차이는 명확하지만, 프로그래밍에서는 종종 혼용되어 사용되기도 한다. 대표적으로 자바스크립트의 setTimeout() 함수를 일반적으로 비동기 함수라고 부르지만 동시에 논블로킹 함수이기도 하다. 즉, 우리가 편의상 부르는 자바스크립트 비동기 함수는 사실 비동기+논블로킹 함수인 것이다. 따라서 이들을 정확하게 구분하고 이해하는 것이 컴퓨터 아키텍쳐를 이해하는 데 있어 중요하다.

동기(Synchronous)라는 것은 작업을 하나하나 순서대로 처리하는 방식이다. 어떤 작업을 요청했을 때, 그 작업이 끝날 때까지 기다려야 다음 작업을 할 수 있다. 즉, 작업이 순차적으로 처리되는 것이다.
예를 들어, 친구한테 “커피 좀 사다줘”라고 말했을 때, 친구가 돌아올 때까지 기다리고, 그 다음에 다른 일을 할 수 있는 것이다. 완료 여부를 확인하고 나서야 다른 일을 시작하는 방식인 셈이다.
비동기(Asynchronous)는 그 반대다. 어떤 작업을 시키더라도 그 결과를 기다리지 않고 바로 다른 일을 할 수 있다. 기다릴 필요 없이 동시에 여러 작업을 처리하는 것처럼 느껴진다.
친구에게 “커피 좀 사다줘”라고 한 후, 커피가 올 때까지 기다리지 않고, 그동안 다른 일을 하면서 보낼 수 있는 것이다. 친구가 커피를 사오면 그때 커피를 받는다.
비동기가 왜 성능이 더 좋을까? 느린 작업을 기다리지 않고 다른 일을 바로 할 수 있기 때문이다.
예를 들어, 웹 애플리케이션이 데이터베이스에서 데이터를 가져와야 하는 상황을 가정해 보자. 동기 방식일 경우, 데이터베이스에서 데이터를 다 가져올 때까지 기다려야 다음 요청을 처리할 수 있다. 그러면 서버가 대기 상태에 빠지고, 많은 사용자가 한꺼번에 접속하면 서버는 느려질 것이다.
하지만 비동기 방식일 경우, 데이터를 가져오는 동안에도 다른 작업을 계속 처리할 수 있다. 데이터베이스 응답이 오면 그때 결과를 처리하고, 그동안은 다른 사용자의 요청도 처리할 수 있는 것이다. 때문에 비동기 방식은 대규모 트래픽에서도 효율적이고, 서버 성능을 더 최적화할 수 있다.

자바스크립트에서는 비동기 작업을 많이 사용하는데, 예를 들어 setTimeout()라는 함수가 있다. 이는 지정된 시간이 지난 후에 특정 작업을 수행하도록 하는 함수다.
중요한 점은, 시간을 기다리는 동안 다른 코드도 계속 실행된다는 것이다. 자바스크립트는 싱글 스레드로 작동해서 한 번에 하나의 작업만 할 수 있는 것처럼 보이지만, 브라우저에서는 멀티 스레드로 돌아가는 Web API가 있어 비동기 작업을 백드라운드에서 처리하도록 한다.
이런 구조 덕분에 자바스크립트는 한꺼번에 여러 작업을 처리할 수 있는 것처럼 보이고, 서버 성능을 크게 향상시킬 수 있다.
동기와 비동기의 가장 큰 차이 중 하나는 작업 순서다.
동기에서는 와 같이 순서대로 작업이 진행된다. 하나가 끝나야 그 다음 작업을 할 수 있다.

비동기에서는 와 같이 순서가 보장되지 않는다. 각 작업이 독립적으로 처리되기 때문에, 어떤 작업이 먼저 끝날지 알 수 없다. 때문에 결과가 무작위로 나올 수 있다.


블로킹은 말 그대로 작업이 끝날 때까지 기다리게 만드는 방식이다.
예를 들어, 친구에게 “커피 좀 사다줘”라고 했을 때, 친구가 돌아올 때까지 아무것도 못하고 문 앞에서 계속 기다리는 상황이다. 다른 일을 할 수가 없고, 친구가 커피를 사오는 작업이 끝날 때까지 대기해야 한다.
반대로 논블로킹은 작업이 끝나지 않았더라도 다른 일을 계속 할 수 있는 방식이다.
커피를 부탁해도 굳이 문 앞에서 기다리지 않고, 집 안에서 다른 일을 하면서 기다리는 것이다. 친구가 커피를 사오면 그때 커피를 받는다. 즉, 다른 작업이 멈추지 않고 동시에 진행되는 것이다.
비동기(Asynchronous)와 논블로킹(Non-Blocking)의 차이
- 비동기는 작업 완료 여부와 관련된 개념이다. 작업을 시켰을 때 결과가 바로 오지 않더라도 다른 작업을 먼저 할 수 있는지 여부를 말한다.
- 논블로킹은 현재 작업이 다른 작업을 막는지 여부를 말한다. 작업이 진행 중이더라도, 다른 작업을 동시에 할 수 있으면 논블로킹이다.
둘 다 결과적으로 동시에 여러 작업을 처리할 수 있다는 점에서 비슷해 보이지만, 비동기는 ‘작업이 끝났냐’를 신경쓰지 않기 때문에 계속 진행하는 것이고, 논블로킹은 ‘작업이 진행되더라도 다른 작업을 멈추지 않고 동시에 처리’하는 것이다.
프로그래밍에서 제어권은 함수나 코드 흐름을 제어할 수 있는 권리를 말한다. 제어권을 누가 가지고 있느냐에 따라 블로킹과 논블로킹을 구분할 수 있다.
블로킹 방식은 호출된 함수(B 함수)가 제어권을 가지고, 호출한 함수(A 함수)는 멈춰 있어야 하는 방식이다.

쉽게 말하면, A 함수는 B 함수가 끝날 때까지 대기해야 하고, 제어권을 잠시 B 함수에게 양보하는 상태라고 볼 수 있다.
논블로킹 방식은 호출된 함수(B 함수)가 실행되지만, 제어권은 여전히 A 함수가 가지고 있는 방식이다.

즉, A 함수는 B 함수가 끝나길 기다리지 않고 다른 작업을 바로 진행할 수 있다.
작업을 순서대로 처리하고, 하나의 작업이 끝날 때까지 기다리는 방식이다. 다른 작업이 진행되는 동안 멈추고, 결과가 나올 때까지 대기하는 형태이다.

팀장이 사원1에게 A 업무를 시키고, 사원1이 A를 끝낼 때까지 기다린 후에 사원2에게 B 업무를 시키는 상황.
사원의 작업이 끝나기 전까지 팀장은 다른 사원에게 아무 일도 시키지 못한다.
const data1 = fs.readFileSync('file1.txt', 'utf8'); // A 작업
console.log(data1);
const data2 = fs.readFileSync('file2.txt', 'utf8'); // B 작업
console.log(data2);
이 방식은 코드가 순차적으로 실행된다. 작은 작업에서는 유용할 수 있으나, 시간이 오래 걸리는 작업에서는 비효율적이다.
대표적으로 C나 Java의 코드 실행 후 커맨드에서 입력을 받는 경우가 이에 해당된다. 사용자로부터 입력을 받아야 그 입력값을 가지고 내부 처리를 하여 결과값을 콘솔에 출력해주기 때문에 순차적인 작업이 요구된다.
내부적으로 본다면 실행 코드가 콘솔창을 띄우고 Please enter your name 텍스트를 치고 난 다음 사용자의 리턴값이 필요하기 때문에 제어권을 시스템에서 사용자로 넘겨 사용자가 값을 입력할 때까지 기다리는 것이다.

작업이 시작되면 결과를 기다리지는 않지만, 다른 작업이 완료될 때까지는 막혀있다.

3.2와 비교했을 때, Async Blocking은 Sync Blocking 수행과 큰 차이가 없어 보인다. 실제로 개념적으로만 차이가 존재할 뿐이지, 성능적으로 큰 차이가 없을 뿐더러 실무에서 마주할 일이 없다.
보통 Async Blocking은 개발자가 비동기 논블로킹으로 처리하려다가 실수하는 경우에 발생하거나, 자기도 모르게 블로킹 작업을 실행하는 의도치 않은 경우에 사용된다. 그래서 이 방식을 안티 패턴(anti-pattern)이라고 치부하기도 한다.
다만 Async Blocking이 실제로 적용된 실무 사례가 있 다. Node.js + MySQL의 조합이 대표적인데, Node.js에서 비동기 방식으로 데이터베이스에 접근하기 때문에 Async지만, MySQL 데이터베이스에 접근하기 위한 MySQL 드라이버가 블로킹 방식으로 작동되기 때문이다.

결과가 나오기 전에 다른 작업을 할 수 있지만, 작업 순서는 보장돼야 한다. 즉, 순서대로 작업을 해야 하지만, 각 작업이 진행되는 동안 다른 작업을 방해하지 않는다.

팀장에게 A, B, C 세 개의 업무가 있는데, A 작업을 끝내야 B 작업을, B 작업을 끝내야 C 작업을 시작할 수 있다. 팀장은 각각의 작업을 시작한 후, 결과가 나올 때까지 동시에 다른 부서와 회의를 하거나 이메일을 처리할 수 있다.
Thread thread = new Thread(new MyTask());
thread.start(); // 논블로킹 실행
System.out.println("Main thread is running...");
while (thread.isAlive()) {
System.out.println("Waiting for the thread to finish...");
}
// A가 끝나야 B를 실행
System.out.println("Thread finished!");
System.out.println("Run the next tasks");

스레드를 이용하여 작업을 병렬적으로 처리하도록 지시했지만, while 문을 수행함으로써 요청한 작업의 완료 여부를 계속 확인하고, 결과적으로 순서대로 작업이 수행되는 것을 확인할 수 있다.
자바스크립트의 경우 동기+논블로킹 코드를 구현하기에는 지원하는 메서드에 한계가 있어 완벽히 표현할 수는 없다. 다만 이와 비슷하게 구현하려면 Promise와 async/await를 사용하면 된다.
const fs = require('fs');
const { promisify } = require('util');
const readFileAsync = promisify(fs.readFile); // fs.readFile 함수를 Promise 객체를 반환하는 함수로 변환
async function readFiles() {
try {
// Promise.all() 메소드를 사용하여 여러 개의 비동기 작업을 병렬로 처리한다. (비동기 논블로킹)
const [data1, data2, data3] = await Promise.all([
readFileAsync('file.txt', 'utf8'),
readFileAsync('file2.txt', 'utf8'),
readFileAsync('file3.txt', 'utf8')
]);
// 파일 읽기가 완료되면 data에 파일 내용이 들어온다.
console.log(data1);
console.log(data2);
console.log(data3);
// 파일 비교 로직 실행...
} catch (err) {
throw err;
}
}
readFiles(); // async 함수를 호출
Promise.all()을 사용하여 여러 파일을 동시에 읽는 작업을 비동기적으로 실행할 수 있다. 이렇게 하면 작업이 병렬로 진행되어 논블로킹 방식이 된다.await를 사용하여 파일 읽기 작업이 모두 완료된 후 그 결과를 순차적으로 처리할 수 있다. 즉, 파일을 동시에 읽되, 결과를 동기적으로 처리할 수 있는 방법이다.
async/await은 비동기 작업을 동기적으로 표현해주는 기법처럼 보이지만, 그 이면에서는 비동기 논블로킹으로 작동한다는 점을 기억해야 한다. 자바스크립트의 메인 콜 스택이 모두 비워진 후에async함수가 실행되기 때문에, 여전히 비동기적으로 작동하면서 다른 작업을 막지 않는다는 것이다.
게임에서 맵을 이동할 때를 생각해보자. 맵 다운로드는 시간이 걸리는 작업이다. 이 작업은 비동기적으로 백그라운드에서 처리된다. 즉, 게임은 맵 다운로드가 끝날 때까지 기다리지 않고 다른 작업을 계속할 수 있다. 여기서 다른 작업이란 맵 다운로드가 진행되는 동안 게임이 로딩 바를 업데이트하거나 UI를 계속해서 표시하는 것을 의미한다.

⏱️ Sync Blocking vs Sync Non-Blocking
블로킹이든 논블로킹이든, 메인 함수에서 결국 코드를 동기적으로 실행하면 이 둘은 차이가 없어 보인다. 그럼 두 방식은 도긴개긴인 것인가?
성능 차이는 상황에 따라 다르겠으나, 일반적으로 동기+논블로킹이 동기+블로킹보다 효율적일 수 있다. 왜냐하면 동기+논블로킹은 호출하는 함수가 제어권을 가지고 있어 다른 작업을 병렬적으로 수행할 수 있기 때문이다. 반면, 동기+블로킹은 호출하는 함수가 제어권을 잃어서 다른 작업을 수행할 수 없다.
작업이 끝나기 전에 다른 작업을 처리할 수 있고, 작업 순서에 구애받지 않는다. Node.js같은 환경에서 많이 사용되는 방식이다.

팀장이 사원1, 사원2, 사원3에게 동시에 업무를 시키고, 결과가 언제 나오든 나오는 대로 처리하는 방식이다. A, B, C라는 작업을 동시에 시킬 수 있고, 완료되는 순서와 상관없이 결과를 처리할 수 있다.
fs.readFile('file.txt', 'utf8', (err, data) => {
console.log(data);
});
fs.readFile('file2.txt', 'utf8', (err, data) => {
console.log(data);
});
console.log('done'); // 이게 먼저 출력됨
여기서는 파일 읽기가 비동기적으로 처리되고, 순서에 상관없이 논블로킹으로 작업이 진행된다. 비동기 논블로킹 방식은 작업량이 많을 때, 다른 작업을 동시에 처리할 수 있어 성능을 최적화할 수 있다.
fs.readFileSync와fs.readFile함수는 둘 다 똑같이 파일을 읽는 함수이지만, 동기냐 비동기냐에 차이가 있다. 즉,fs.readFileSync함수는 동기적으로 파일을 읽고,fs.readFile함수는 비동기적으로 파일을 읽는다.
비동기 논블로킹을 활용하는 프로그램은 많다. 그중 예를 들자면, 웹 브라우저의 파일 다운로드가 비동기 논블로킹을 활용하는 예시라고 할 수 있다. 웹 브라우저는 웹 사이트에서 파일을 다운로드할 때, 파일의 전송이 완료될 때까지 다른 작업을 하지 않고 기다리는 것이 아니라, 다른 탭이나 창을 열거나 웹 서핑을 할 수 있다. 이는 웹 브라우저가 파일 다운로드를 비동기적으로 처리하고, 콜백 함수를 통해 다운로드가 완료되면 알려주는 방식으로 구현되어 있기 때문이다.

파일들이 거의 동시에 다운 받아지는 것을 Waterfall 그래프에서 시작적으로 확인할 수 있다.
Reference
안녕하세요! 개발자 준비하시는 분이나 현업에 종사하고 계신 분들만 할 수 있는 시급 25달러~51달러 LLM 평가 부업 공유합니다~ 제 블로그에 자세하게 써놓았으니 관심있으시면 한 번 읽어봐주세요 :)