동기랑 블로킹은 같은 말 아닌가요?

과거 면접에서 동기와 비동기, 블로킹과 논블로킹의 차이를 물어봤다. 정확히 어떤 맥락에서 나왔던 질문인지는 기억이 안 나는데, 솔직히 답을 잘 못 했다. 머릿속에서는 "동기는 블로킹, 비동기는 논블로킹" 같은 식으로 굳어 있어서 두 단어를 거의 같은 말처럼 썼고, 면접관이 "둘이 다른 개념입니다" 하고 슬쩍 정정해줬을 때 머쓱했다.

면접 끝나고 집에 와서 다시 정리해본 내용을 적는다. 이미 잘 아시는 분들에게는 새로울 게 없을 수 있는데, 저처럼 처음에는 같은 거라고 생각했던 분이 있다면 도움이 될지도 모르겠다.

두 개념은 서로 다른 축을 보고 있다

다시 정리하면서 가장 결정적이었던 건 이 한 줄이었다.

블로킹/논블로킹은 "지금 기다릴까?" 동기/비동기는 "결과는 언제 받을까?"

이 두 질문은 같은 동작을 다른 각도에서 보는 거다. 그래서 두 축이 독립적이고, 결국 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가지 조합이 다 가능한데, 가장 자주 쓰이는 건 동기/블로킹과 비동기/논블로킹 두 개입니다.

다만 아직도 머릿속에서 깨끗이 정리되지 않은 부분이 있다. 비동기/블로킹 조합이 정말 항상 무의미한 건지, 아니면 어떤 특수 케이스에서 쓰이는지 잘 모르겠다 (찾아봤는데 명쾌한 답을 못 찾았다). 일단은 "실무에서는 거의 안 쓴다" 정도로 이해하고 있다.

면접에서 처음 듣고 헷갈렸던 게 결국 좋은 학습 계기가 됐다. 다음에 같은 질문을 받으면 이번에는 좀 더 자신 있게 답할 수 있을 것 같다.


참고