본문 바로가기

Development/JavaScript

Javascript의 병렬 처리에 관한 견해(동기, 비동기, blocking, non-blocking) 개념 정리

개요

Javascript를 공부해본 사람이라면 단일 쓰레드, 콜백, 비동기, 병렬처리에 대한 내용을 접해본 경험이 있을 것이다.

위의 용어 자체가 추상적이기도 하고, 쉽게 와닿지 않기 때문에 단번에 이해하기에는 꽤 어려운 개념일수도 있는데, 사실 위에서 언급 내용은 모두 연관성이 있기 때문에 Javascript의 동작 방식을 이해하고 나면 전체적인 그림이 그려질 것이라 생각한다.

이번 글은 100% 정확한 사실에 기반한 내용을 정리한 것이 아니라는 점을 참고하기 바란다.

병렬처리에 대한 주관적인 생각

구글링을 하다보면, 'Javascript는 병렬적으로 수행된다'라는 내용을 가끔 확인할 수 있다. 이 내용을 처음 보았을 때에는 '그런가보다'하며 넘어갔었는데, 단일 쓰레드, 콜스택, 콜백큐에 대해서 알게된 후에는 다음과 같은 의문이 들기 시작하였다.

  • Javascript는 단일 쓰레드로 동작하는데 병렬처리가 가능하다고?
  • Javascript가 단일 쓰레드라는 정보가 잘못된 것인가?
  • 아니면, 병렬처리라는 내용이 잘못된 건인가?

위 내용을 이해하기 위해서는 프로세스와 쓰레드에 대한 개념을 알고 있어야 가능한데, 이에 대한 내용은 생략하도록 하겠다.

어찌되었든 Javascript는 웹 개발에 최적화된 단일 쓰레드 방식을 채택하였다는 것은 사실이라는 것이다.

멀티 쓰레드 방식은 웹 개발에서 다소 무겁다는 평가가 있으며, 동시성 문제를 해결해야 하므로 프로그래밍 난이도가 높아진다.

그렇다면 병럴 처리에 대한 내용이 잘못된 내용이라는 확률이 더 높아질 수 밖에 없다. 이후에 여러 글을 취합해본 결과, Javascript의 비동기 처리가 멀티 쓰레드의 동작 방식과 비슷하게 보여서 병렬처리라는 내용이 등장한 것으로 보인다.

동기와 비동기

동기와 비동기, blocking과 non-blocking에 대한 이해 없이도 Promise, async/await을 통해 비동기적으로 동작하는 코드를 작성할 수 있었음에도 불구하고, 내가 Javascript를 공부하면서 가장 헷갈렸던 개념이다.

아마 그 이유 중 하나는 개념 자체가 추상적이어서 동기와 비동기가 각각 무엇을 의미하는지 명확하게 풀어낼 수 없기 때문이 아닐까 생각한다. 사실 지금도 예시를 통해 설명하지 않으면 '동기와 비동기는 ~이다'라고 정확하게 풀어내기는 어렵다.

Javascript는 단일 쓰레드에서 동작하며, 그 작업은 동기적으로 수행된다. 이게 도대체 무슨말인가? 아래 코드를 살펴보자.

const a = () => 'a';
const b = () => 'b';
const c = () => 'c';

console.log(a()); 
console.log(b());
console.log(c());

위의 코드를 실행하면 콘솔에 a → b → c 순으로 출력되는 것을 확인할 수 있다. 이를 통해 알 수 있는 동기의 특징에 대해서 정리해보면 다음과 같다.

  • 작업이 순차적으로 수행된다.
  • 요청의 return 값을 직접 받는다.
  • 함수의 return과 요청의 결과값이 같다.

여기서 '순차적'이라는 의미에 큰 비중을 두지 않길 바란다. '순차적'이라는 용어는 단지 동작의 흐름을 나타내기 위한 용어일 뿐이다.

위의 특징 중에서 첫번째와 두번째는 쉽게 이해할 수 있겠으나, 세번째는 살짝 애매하게 느껴질 수 있다. 이는 비동기의 특징을 이해하고 나면 무엇을 의미하는지 알 수 있으리라 생각한다. 이번에는 다른 경우를 살펴보도록 하자.

const a = () => 'a';
const b = async () => {
    let bstring;
    await setTimeout(() => {
        bstring = 'b';
    }, 2000)
    return bstring;
};
const c = () => 'c';

console.log(a());
console.log(b());
console.log(c());

위의 코드를 실행하면 콘솔에 a → undefined → c 순으로 출력되는 것을 확인할 수 있다. 우리가 원하는 것은 2초 후에 'c'라는 값을 return 받아서 이를 콘솔에 출력하는 것임에도 불구하고, 곧바로 undefined라는 어처구니 없는 값을 받아낸 것을 볼 수 있다.

setTimeout은 주로 비동기 처리를 설명할 때 많이 사용된다. 이를 정확히 이해하려면 웹 브라우저와 이벤트 루프라고 불리우는 콜백큐에 대한 설명이 필요하겠으나, 글이 점점 길어지고 있으므로 이 내용은 생략하도록 하겠다.

우리가 원하는 작업을 수행하려면, 코드의 순서를 바꿔주어야 하는데, 이때 등장하는 개념이 바로 callback이다. 즉, 위의 코드에서는 b() 함수의 return 값을 console.log로 넘겨주었다면, 이번에는 console.log를 b() 함수의 인자로 넘겨주고, 2초 뒤에 출력되도록 해주어야 한다.

const b = (callback) => {
    setTimeout(() => {
        callback("b");
    }, 2000);
};

b(console.log);

이를 통해 알 수 있는 비동기 처리의 특징을 정리해보면 다음과 같다.

  • 현재 작업의 완료 상태를 기다리지 않고 다음 작업을 처리한다.
  • 요청한 return 값을 간접적으로 받는다.
  • 함수의 return 값과 요청의 결과값이 다른 경우가 있다.
  • callback을 통해 작업 완료 상태를 확인할 수 있다.

쉽게 말자하면, callback을 통한 작업 처리가 곧 비동기 처리라고 할 수 있다.

blocking과 non-blocking

위에서는 동기와 비동기에 대해서 정리해보았다. 간혹, 동기/비동기와 blocking/non-blocking을 연결짓는 경우가 있는데, 이 둘은 엄연히 다르다. 이 두 개념은 어떻게 다른 것인지 아래 코드를 통해 살펴보도록 하자.

const a = () => 'a';
const b = () => 'b';
const c = () => 'c';

console.log(a()); 
alert(b());
console.log(c());

웹 브라우저 환경에서 위의 코드를 실행시켜보면, alert(b()) 이후의 코드는 실행되지 않는 것을 확인할 수 있다. 즉, 앞의 작업이 완료 처리가 완료되지 않으면 뒤의 작업도 수행하지 않는다는 것인데, 이를 blocking이라고 한다.

  • 현재 작업이 종료 처리가 될 때까지 계속 대기한다.
  • 작업이 완료되면 즉시 return 한다.

반대로, setTimeout과 같이 2초 뒤에 실행되어야 함에도 불구하고 결과를 바로 return하는 경우를 non-blocking이라고 할 수 있다.

  • 현재 작업의 종료 처리가 늦어진다면 일단 return 한다.

쉽게 말하자면, 호출된 함수가 아직 return 되지 않고 제어권을 계속 갖고 있는 상태를 blocking, 호출된 함수가 바로 return 되어 후속 작업으로 제어권을 넘겨줄 수 있는 상태를 non-blocking이라고 할 수 있다.

동기/비동기와 blocking/non-blocking이 의미하는 개념은 각각 다음과 같이 정리할 수 있다.

  • 동기/비동기 : 작업 완료 여부를 어떻게 확인할 것인가?
  • blocking/non-blocking : 작업 결과를 바로 return할 것인가?(즉, 완료 여부에 상관없이 제어권을 다음 작업으로 넘겨줄 것인가?)

작업 결과를 값으로 return 받는 경우를 동기, callback으로 대신 처리하는 경우를 비동기라고 할 수 있다. 그리고, 작업 완료 여부에 상관없이 바로 return하는 경우를 non-blocking, 반드시 작업이 완료된 후 return하는 경우를 blocking이라고 할 수 있다.

조금 더 자세하게 설명하고 싶지만, 이미 글이 충분히 길어졌기 때문에 마지막으로 편의점(동기 & blocking)과 푸드코트(비동기 & non-blocking)로 예시를 남겨보겠다.

  • 편의점에서는 손님이 카운터로 와서 순차적으로 계산을 진행한다. 즉, 앞의 계산이 오래 걸리면 뒤의 손님들은 계속 기다려야 하는 상황이 발생하는 것이다.
  • 푸드코트에서는 결재를 하면 번호표를 받는다. 나중에 이 번호표를 통해 음식을 받을 수 있는데, 이렇게 나중에 음식을 전달받는 방법을 callback 함수 또는 뒤에서 언급할 Promise라고 할 수 있다. 이는 주문한 음식을 당장 받을 수 없기 때문에 나중에 해당 음식을 전달 받기 위한 일종의 티켓이라고 할 수 있다. 또한, 푸드코트에 가보았다면 알 수 있겠지만, 반드시 번호표에 적힌 순서대로 음식을 받는 것이 아니라 대부분 조리가 완료된 순으로 음식을 받는다.

즉, 동기 처리가 반드시 blocking은 아니고, 비동기 처리가 반드시 non-blocking은 아니라는 사실만 확실하게 알아두면 혼란스러운 상황에서 탈출할 수 있으리라 생각한다.