Javascript - 동기(synchronous), 비동기(asynchronous) 프로그래밍

들어가기

이 글은 자바스크립트의 동기(asynchronous) + 블록킹, 비동기 + 논블록킹 구조에 대한 설명이다.

동기 synchronous 와 블록킹 Bloking 이란

일단 동기적인 동작의 예를 식당과 주방장을 통해 들어보겠다.

일단 중국집이 하나 있고 이 중국집에는 한명의 주방장이 요리를 한다고 가정하자. 군만두 한 접시를 만들어 달라는 주문서가 도착했다. 이 군만두는 냉동재료라서 전자레인지를 사용해야 한다. 하지만 전자레인지의 타이머가 고장이 난 상태이다. 주방장은 전자레인지에 만두를 넣고 전자레인지를 동작시킨뒤, 자신의 손목시계를 보며 3분을 기다린다. 그때 홀에서 짜장면 3그릇 추가라는 주문이 추가로 도착했다. 하지만 주방장은 고장난 전자레인지 때문에 한눈을 팔수가 없다. 3분동안 주방에서는 어떤 다른 음식이 만들어지기는 커녕 계속 주문이 쌓이게 된다. 이걸 보는 중국집 사장님은 속이 타들어 간다. 내일 저 망할 전자레인지를 어떻게든 해야 겠다고 다짐한다.

본론으로 돌아와서, 동기적인 소스코드의 예를 보면...

1
2
3
4
5
6
7
8
9
console.log('동기적 소스코드 시작...');

function run1(){
console.log('run1 동작...');
}

run1();

console.log('동기적 소스코드 종료...');

위 코드를 실행하면 호출된 순서대로 실행되는 것을 확인 할 수 있다.(함수선언문을 호출과 착각하지 말자.)

이것이 코드의 동기적인 동작이다.

동기적 코드의 실행결과

쓰레드를 이용하여 위 코드를 표현하면 다음과 같을 것이다.

동기적 코드의 쓰레드의 흐름

동기적 코드의 동작은 시간의 흐름대로 순서를 지키며 진행되기 때문에 익숙하고, 이해하기 쉽다.

하지만 javascript 코드의 실행환경이 싱글쓰레드 기반이기 때문에, 싱글 쓰레드 실행환경에서 이런 동기적인 코드를 사용할 때 쓰레드의 블록킹(blocking) 현상이 발생한다. 다음 코드를 보면...

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
console.log('동기적 소스코드 시작...');

function run1(){
console.log('run1 동작...');
}

//실행이 종료 될때까지 오랜시간이 걸리는 함수
function run2(){
console.log('run2 동작...');
console.log('서버로 부터 50mb 정도의 파일을 다운로드 합니다.');
/*
파일을 다운로드 하는 로직 대략 1분이 소요됨.
*/
}

run1();
run2();

console.log('동기적 소스코드 종료...');

run2 메소드는 파일을 서버로 부터 다운받는 함수라고 하자, 그리고 파일을 다운로드 받는데 대략 1분의 시간이 걸린다고 가정하자.

위 코드를 실행하게 되면

위처럼 동작하고 1분정도를 기다리면 나머지 코드가 아래처럼 실행 될 것이다.

위 동작을 쓰레드로 표현해 보자면 다음과 같다.

run2함수는 쓰레드의 흐름을 막아(blocking)버린다.

즉, 싱글쓰레드 + 동기적 코드의 흐름 + 동작시간이 오래걸리는 함수의 호출은 쓰레드의 블록킹(blocking)을 발생 시킨다.

위 코드가 시간의 흐름에 따라, Call stack이 어떻게 변하는지 확인해 보자.

위 동작을 javascript 호출 스택으로 살펴보면 다음과 같다.

a_1 a_2 a_3 a_4 a_5 a_6 a_7 a_8 a_9 a_10 a_11 a_12 a_13

이 쓰레드의 블록킹은 프로그램의 흐름의 멈춤을 의미하며, 사용자는 프로그램이 먹통이 되어버렸다고 느낄 것이다.

설명의 편의를 위해 callstack의 일을 처리하는 tread를 mainthread라고 부르겠다.

ajax 통신이나, IO 프로그래밍 같이 시간이 오래걸리는 작업들이 발생할 때마다 프로그램이 멈춰버리는 것은 큰 문제가 될 것이다.

위와 같은 문제를 해결하기 위해서 javascript는 멀티스레드 프로그래밍을 지원하는 것이 아니라, 싱글쓰레드에서 비동기적 프로그래밍을 javascript 실행환경의 API로 지원 한다.

멀티 쓰레드 프로그래밍은 굉장히 어렵고, 복잡한 작업이다. 거기에 프로그램의 동작을 100퍼센트 예측하기는 불가능에 가깝다. javascript는 이런 어려운 프로그래밍을 피하기 위해서 일부로 개발자가 제어할 수 있는 쓰레드(main thread)를 1개로 제한하고 비동기적 프로그래밍을 지원 함으로써 멀티쓰레드 프로그래밍과 비슷한 효과를 발생시킨다.

여기서 잠깐 다른 이야기를 하자면, javascript는 싱글 쓰레드로 동작 한다고들 말해서 진짜 쓰레드가 1개 뿐일거라 착각 할수 있다. 하지만 javascript의 실행환경은 멀티스레드이며 개발자가 javascript 코드로 제어할 수 있는 쓰레드(main tread)를 하나로 제한 하였기 때문에 javascript는 싱글 쓰레드 프로그래밍 이라고 부른다. 다시 본론으로 돌아가서...

이런 싱글 쓰레드환경의 동기적 코드흐름에서 발생하는 쓰레드의 blocking을 해결하기 위해 run2함수를 비동기적으로 실행시킬 필요가 생긴다.

비동기 asynchronous적 동작과 논블록킹 Non Blocking

이번에는 비동기적으로 일하는 주방장의 예를 한번 들어보겠다.

일단 중국집이 하나 있고 이 중국집에는 한명의 주방장이 요리를 한다고 가정하자. 중요한 것은 이 주방장이 비동기적으로 일한다는 것이다. 군만두 1접시 라는 주문서가 도착했다. 이 군만두는 냉동재료이다. 비동기 주방장은 군만두를 전자레인지에 넣고 타이머를 3분 맞춘뒤, 전자레인지를 돌린다. 그리고 다음 할일을 위해 재 자리에 돌아온다. 그때 홀에서 짜장면 1그릇 추가라는 주문이 추가로 도착했다. 주방장은 짜장면을 만든다. 짜장면 1그릇을 만들었을 때, 아까 돌린 전자레인지의 동작 종료를 알리는 알람이 들린다. 이 비동기 주방장은 만들던 짜장면 1그릇 까지만 완성하고 홀에 전달한 뒤, 전자레인지에서 만두를 꺼내서 홀에 전달한다. 중국집 사장님은 쉬지 않고 열심히 일하는 주방장을 보며 다음달 주방장 월급 인상을 고려하게 된다.

위 비동기적으로 일하는 주방장을 보면 오래걸리는 특정 작업(만두 해동)들을 누군가(전자레인지)에게 미루고 자신은 계속 일을 진행한다. 결과적으로 1번째로 주문된 군만두 보다 2번째로 주문된 짜장면이 더 빨리 홀에 전달 되었다. 이렇게 호출에 순서(음식 주문 순서)와 다르게 실행(주문된 음식이 홀에 나오는것)되는 프로그래밍을 비동기적 프로그래밍이라고 한다.

이 비동기적 프로그래밍의 가장큰 장점은, 주방장이 노는 시간(mainthread이 blocking)이 없어지므로 음식주문은 밀리지 않고(프로그램은 멈추지 않고) 효율적으로 음식이 생산되게 된다.

여기서 잠깐 비동기적으로 처리해야할 작업들을 구별해보자.

  1. 파일을 읽거나, 쓰기 처럼 오래걸리는 작업
  2. ajax 통신작업
  3. Dom의 이벤트 처리작업
  4. 일정 시간 뒤에 동작을 해야 하는 작업

위 작업들을 callstack작업 처리를 담당하는 mainthread 하게 된다면, 스레드 블록킹이 발생하고 프로그램은 멈추게 된다. 따라서 위 작업들을 mainthread 대신 처리하는 녀석들(전자레인지 같은)이 필요하다.

javascrpit는 이런 전자레인지들을 API를 통해서 제공한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
console.log('동기적 소스코드 시작...');

function run1(){
console.log('run1 동작...');
}


function run2(){
console.log('run2 동작...');
}

run1();
setTimeout(run2, 1000 * 3); // 3초 이후에 run2 함수를 실행

console.log('동기적 소스코드 종료...');

일단 위 코드의 결과를 보자.

1
2
3
4
동기적 소스코드 시작...
run1 동작...
동기적 소스코드 종료...
run2 동작...

동기적 소스코드 종료... 라는 문구가 run2 동작이라는 문구보다 더 빨리 출력되었다. 코드의 결과를 보면 자바스크립트의 쓰레드는 run2라는 함수를 setTimeout이라는 전자레인지에 넣고 3초 라는 타이머를 돌린 뒤, 자신의 일을 계속 진행한 것으로 보여진다.

위의 setTimeout은 특정 시간 뒤에, callback 함수를 호출가능하게 만들어주는 전자레인지이다. 즉 mainthread 대신 시간을 세어주는 javascript 비동기 API이다.

위 코드의 동작도 함수의 호출스택으로 보면 다음과 같다. setTimeout 호출직전은 동기적인 동작과 같으니 생략하고, setTimeout 호출부터 본다면...

setTimeout을 만난 스레드는 이것을 Web API로 넘긴다. 이 행위는 Web API라는 전자레인지에 run2를 넣고 동작시키는 행위라고 생각하면 된다.

Web API에서는 이런 쓰레드의 블록킹을 현상을 발생시키는, 지연작업등을 비동기적으로 실행할 수 있게 여러가지 전자레인지를 구비해 놓고 있다.

setTimout은 전자레인지 중 하나이며, ajax, Dom 등도 존재한다.

쓰레드는 전자레인지는 신경쓰지 않고 자신의 callstack에 돌아와 계속 남은일을 처리해 나간다. 이 시점에도 동시에 setTimeout은 자신이 해야하는 일, 즉 3초의 시간을 재고 있다.

쓰레드는 callstack의 작업을 모두 끝내고, 전자레인지 setTimeout은 여전히 시간을 재고 있다.

드디어 setTimeout은 3초를 다 세고, run2라는 callback함수를 task Queue에 집어 넣는다.

Event Loop는 callstack이 비어있는지 확인하고 비어있다면 task queue에 가장 먼저 들어온 run2함수를 callstack에 전달한다.

쓰레드는 CallStack 에 있는 run2작업을 처리하기 시작한다.

여기서 Web API의 역할은 callstack의 일들을 처리하는 MainThread 대신 비동기적으로 처리해야 할 일을 대신 처리하는 것이다. 숫자를 센다던지, 서버의 요청을 기다린다 던지, DOM에서 발생할 수 있는 이벤트를 기다리는 등의 일들을 처리한다.

개발자가 해야할 일은 이런 머리아픈 비동기적 작업을 API에게 맡기고, 비동기적 작업 결과에 따라 처리해야 할일을 callback에 담아주기만 하면 되는것이다.

싱글쓰레드이지만 동시성작업이 필요한 작업들을 javascript API를 통해 대신처리하므로, MainThread는 Non blocking(막히지 않고) 동작하게 된다.


참고자료

https://joshua1988.github.io/web-development/javascript/javascript-asynchronous-operation/

https://engineering.huiseoul.com/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%9E%91%EB%8F%99%ED%95%98%EB%8A%94%EA%B0%80-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A3%A8%ED%94%84%EC%99%80-%EB%B9%84%EB%8F%99%EA%B8%B0-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D%EC%9D%98-%EB%B6%80%EC%83%81-async-await%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%BD%94%EB%94%A9-%ED%8C%81-%EB%8B%A4%EC%84%AF-%EA%B0%80%EC%A7%80-df65ffb4e7e

https://hudi.kr/%EB%B9%84%EB%8F%99%EA%B8%B0%EC%A0%81-javascript-%EC%8B%B1%EA%B8%80%EC%8A%A4%EB%A0%88%EB%93%9C-%EA%B8%B0%EB%B0%98-js%EC%9D%98-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B2%98%EB%A6%AC-%EB%B0%A9%EB%B2%95/

https://developer.mozilla.org/ko/docs/Web/JavaScript/EventLoop