
과거 면접에서 동기와 비동기, 블로킹과 논블로킹의 차이를 물어봤다. 정확히 어떤 맥락에서 나왔던 질문인지는 기억이 안 나는데, 솔직히 답을 잘 못 했다. 머릿속에서는 "동기는 블로킹, 비동기는 논블로킹" 같은 식으로 굳어 있어서 두 단어를 거의 같은 말처럼 썼고, 면접관이 "둘이 다른 개념입니다" 하고 슬쩍 정정해줬을 때 머쓱했다.
면접 끝나고 집에 와서 다시 정리해본 내용을 적는다. 이미 잘 아시는 분들에게는 새로울 게 없을 수 있는데, 저처럼 처음에는 같은 거라고 생각했던 분이 있다면 도움이 될지도 모르겠다.
두 개념은 서로 다른 축을 보고 있다
다시 정리하면서 가장 결정적이었던 건 이 한 줄이었다.
블로킹/논블로킹은 "지금 기다릴까?" 동기/비동기는 "결과는 언제 받을까?"
이 두 질문은 같은 동작을 다른 각도에서 보는 거다. 그래서 두 축이 독립적이고, 결국 4가지 조합이 가능하다는 결론으로 이어진다.
블로킹과 논블로킹
함수 A가 함수 B를 호출했다고 치자. B가 끝날 때까지 A가 다음 줄로 못 넘어간다면, 그건 블로킹이다.
// 블로킹
const data = fs.readFileSync('./file.txt');
console.log('이 줄은 파일을 다 읽어야 실행됨');
반대로 B가 "응, 요청 받았어" 하고 즉시 제어권을 돌려주면 A는 다음 줄로 넘어간다. 이건 논블로킹.
// 논블로킹
fs.readFile('./file.txt', (err, data) => { /* ... */ });
console.log('이 줄은 파일 읽기 시작 직후 바로 실행됨');
여기서 헷갈리지 말아야 할 건, 이 관점은 제어권을 누가 잡고 있느냐만 본다는 것이다. 결과를 어떻게 받는지는 다른 이야기다.
동기와 비동기
A가 B에게 일을 시켰을 때, 그 일이 끝나는 순서와 A의 다음 코드 실행 순서가 일치하면 동기다. 일치하지 않으면 비동기.
// 동기 - 순서대로 끝남
const a = computeA(); // 끝나야
const b = computeB(); // 시작
console.log(a, b);
// 비동기 - 끝나는 순서가 호출 순서와 다를 수 있음
fetchA().then(a => console.log(a));
fetchB().then(b => console.log(b));
console.log('이게 먼저 실행될 수 있음');
처음에 헷갈렸던 부분은 "비동기는 순서가 안 맞다"가 아니라 호출자가 결과를 그 자리에서 기다리지 않는다라는 쪽이었다. 결과는 콜백, Promise, async/await 같은 메커니즘으로 나중에 받는다.
그래서 4가지 조합이 나온다
두 축이 독립적이라는 건 실제로 4가지 패턴이 가능하다는 뜻이다. 처음에는 "동기/블로킹"과 "비동기/논블로킹" 둘만 있다고 생각했었다.
블로킹 논블로킹
| 동기 | 가장 흔한 패턴. 그냥 함수 호출 | 폴링(polling). 즉시 반환받지만 결과는 즉시 사용 |
| 비동기 | 의미가 흐려지는 조합. 비동기로 시작했는데 결과를 기다림 | JavaScript에서 가장 많이 쓰는 패턴 |
비동기/블로킹 조합이 신기했다. "비동기로 호출했지만 결과는 기다리는" 좀 이상한 패턴인데, 사실상 비동기의 의미를 무력화하는 거라 실무에서는 잘 안 쓴다고 한다.
동기/논블로킹은 "즉시 반환은 받지만 결과를 즉시 사용"이라 폴링 같은 데 쓰인다고 한다. 본격적으로 만나본 적은 아직 없다.
JavaScript에서 자주 보는 패턴
JavaScript는 싱글스레드라서 블로킹이 일어나면 화면 자체가 멈춘다. 그래서 거의 모든 I/O가 비동기/논블로킹으로 설계돼 있다.
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
console.log('3');
// 출력 순서: 1, 3, 2
setTimeout이 0ms여도 결과는 마지막에 찍힌다. 동기/블로킹이라면 1, 2, 3 순으로 나와야 하는데, 비동기/논블로킹이라서 setTimeout은 일단 Web API에 넘겨두고 console.log('3')이 먼저 실행된다.
이게 처음 본 사람한테는 좀 마법 같은데, 이벤트 루프와 콜백 큐가 어떻게 돌아가는지 이해하면 자연스럽다고 한다. 그건 다음에 따로 정리해보려고 한다.
그래서 면접에서 다시 답한다면
이제 같은 질문을 받으면 이렇게 답할 것 같다.
블로킹/논블로킹은 함수 호출이 즉시 제어권을 돌려주느냐를 봅니다. 동기/비동기는 호출 결과를 그 자리에서 기다려서 받느냐를 봅니다. 두 축이 독립적이라 4가지 조합이 다 가능한데, 가장 자주 쓰이는 건 동기/블로킹과 비동기/논블로킹 두 개입니다.
다만 아직도 머릿속에서 깨끗이 정리되지 않은 부분이 있다. 비동기/블로킹 조합이 정말 항상 무의미한 건지, 아니면 어떤 특수 케이스에서 쓰이는지 잘 모르겠다 (찾아봤는데 명쾌한 답을 못 찾았다). 일단은 "실무에서는 거의 안 쓴다" 정도로 이해하고 있다.
면접에서 처음 듣고 헷갈렸던 게 결국 좋은 학습 계기가 됐다. 다음에 같은 질문을 받으면 이번에는 좀 더 자신 있게 답할 수 있을 것 같다.
참고
- MDN — Asynchronous JavaScript
- Node.js — Blocking vs Non-Blocking