동기/비동기, 블로킹/논블로킹. 이 네 단어는 면접이나 문서에서 자주 튀어나오는데, 막상 설명하려면 말이 꼬입니다. 대부분 "동기 = 블로킹, 비동기 = 논블로킹"으로 뭉뚱그려 외우고 있어서 그렇습니다. 실제로는 서로 다른 축을 묻는 질문입니다.
먼저 기준부터 잡아 두면 편합니다.
- 동기 / 비동기: 호출한 함수의 결과를 누가 처리하는가
- 블로킹 / 논블로킹: 호출된 함수가 제어권을 언제 돌려주는가
하나는 결과의 행방, 하나는 제어권의 행방입니다. 이 두 질문을 따로 던져 보면 헷갈릴 일이 줄어듭니다.
결과를 내가 받아서 쓰면 동기
함수 A가 함수 B를 불렀고, B가 돌려준 값을 A가 그 자리에서 받아 처리한다면 동기입니다. 우리가 평소에 짜는 코드는 거의 다 이 형태예요. VB.NET에서 Dim n = CInt(Console.ReadLine()) 같은 줄을 쓰는 것도 같은 이야기입니다.
Java에서 키보드 입력을 받는 코드로 보면 이렇습니다. C의 scanf()도 성격이 같습니다.
Scanner sc = new Scanner(System.in);
int num = sc.nextInt();
nextInt()가 사용자가 입력한 값을 돌려주고, 그 값은 호출한 쪽의 num 변수에 바로 담깁니다. 결과를 호출한 쪽이 직접 받아 썼으니 동기 방식입니다.
결과를 콜백이 처리하면 비동기
반대로 호출한 쪽이 결과를 직접 받지 않고, "끝나면 이걸 실행해 줘"라고 다른 함수를 맡겨 두는 방식이 비동기입니다. 자바스크립트의 setTimeout()이 가장 흔한 예입니다.
setTimeout( foo, 3000);
function foo(){
console.log("2");
}
console.log("1");
setTimeout()은 3초 뒤에 foo()를 실행하라는 예약만 걸어 둡니다. 그 3초 동안 프로그램은 다음 줄로 넘어가서 console.log("1")을 먼저 실행하고, 시간이 다 되면 foo()가 돌아 2를 찍습니다. 그래서 콘솔에는 1, 2 순서로 나옵니다. 코드 순서만 보고 "2가 먼저 나오겠지"라고 생각하면 틀립니다.
여기서 foo()처럼 어떤 작업이 끝났을 때(이벤트가 발생했을 때) 대신 불려 결과를 처리하는 함수를 콜백(callback) 함수라고 부릅니다. 비동기에서는 결과 처리를 호출한 쪽이 아니라 이 콜백이 맡습니다.
블로킹과 논블로킹은 제어권 이야기
블로킹은 호출된 함수가 자기 일이 끝날 때까지 제어권을 쥐고 놓지 않는 것입니다. 논블로킹은 호출되자마자 제어권을 호출한 쪽에 바로 돌려줘서, 호출한 쪽이 그동안 다른 일을 할 수 있게 하는 것이고요.
앞의 두 예제를 이 기준으로 다시 보면 감이 옵니다. nextInt()를 부르면 사용자가 무언가 입력할 때까지 프로그램은 아무것도 하지 못합니다. 제어권이 nextInt() 안에 붙잡혀 있다가, 입력이 들어와야 비로소 다음 줄로 넘어가죠. 블로킹입니다.
setTimeout(foo, 3000)은 호출되는 즉시 제어권을 돌려줍니다. 그러니 3초를 기다리지 않고 곧바로 console.log("1")이 실행되고, 나중에 "3초가 지났다"는 이벤트가 생기면 그때 콜백인 foo()가 실행됩니다. 논블로킹입니다.
이 주제를 검색해 보면 위처럼 두 축을 사분면으로 그린 그림이 자주 나옵니다. 우리가 익숙한 건 "동기 + 블로킹", "비동기 + 논블로킹" 두 칸이고, 위 예제도 사실 그 두 조합에 해당합니다. 그림에 나머지 칸이 있는 것 자체가 두 개념이 같은 말이 아니라는 증거입니다. 여기서는 그 두 조합만 다뤘고, 나머지 조합은 참고 링크의 글에 자세히 정리돼 있습니다.
실무에서 이 구분이 왜 필요한가
데스크톱 프로그램을 짜다 보면 체감이 확 됩니다. WinForms 버튼 클릭 이벤트 안에서 오래 걸리는 DB 조회나 HTTP 요청을 그대로 호출하면, 그 작업이 끝날 때까지 UI 스레드가 묶여서 창이 "응답 없음"으로 바뀝니다. 전형적인 블로킹 호출이 UI 스레드에서 일어난 경우입니다. 이걸 피하려고 백그라운드 스레드로 돌리거나 Async/Await를 쓰는 거고요.
반대로 비동기로 바꾸고 나면 "결과가 언제 오느냐"를 신경 써야 합니다. 위 setTimeout 예제처럼 코드 순서와 실행 순서가 달라지기 때문에, 결과값을 쓰는 코드를 호출 바로 다음 줄에 두면 아직 값이 없는 상태로 실행됩니다. 결과를 쓰는 로직은 콜백 안(또는 Await 뒤)에 둬야 한다는 점, 비동기로 넘어갈 때 가장 많이 실수하는 부분입니다.
참고: Blocking-NonBlocking-Synchronous-Asynchronous (homoefficio)