As i wish

[You don't know JS] - 비동기성: 지금과 나중 본문

JavaScript

[You don't know JS] - 비동기성: 지금과 나중

어면태 2019. 6. 15. 13:25

안녕하세요. 엄티 입니다.

오늘은 You don't know JS에 크게 2장으로 이루어진 챕터 중 2 장에 1번 '비동기성: 지금과 나중'에 대하여 포스팅해보겠습니다.

저의 포스팅은 한빛미디어에서 나온 'You don't know JS'를 기준으로 포스팅합니다.

 

일단 프로그래밍 비동기적 요소가 많이 포함되어있습니다. 실제 실무를 접하더라도 자바스크립트뿐만 아니라 파이썬, 자바 도 마찬가지로 비동기적 요소가 들어가 있죠.

예를 들어 서버에 REST ful API, Database 접근, 사용자 입력, AJAX 등 대부분이 다 비동기적 요소로 구성되어 있기 때문에 비동기적 요소를 알아야 합니다.

 

1. 프로그램 덩이

자바스크립트 프로그램은. js 파일 하나로도 작성할 수 있지만 보통은 여러 개의 덩이, 곧 '지금' 실행 중인 프로그램 덩이 하나와 '나중'에 실행할 프로그램 덩이들로 구성되고 가장 일반적인 프로그램 덩이 단위는 '함수'입니다.

 

// ajax 는 라이브러에 있는 임의의 AJAX 함수 입니다.
var data = ajax('http://some.url')

console.log(data);

이런 식의 코드는 ajax에 대한 결과를 data에 담지 못하죠. 이유는 AJAX는 비동기식 함수이고 따라서 '지금' 요청하고 '나중'에 결과를 받기 때문이죠.

// ajax 는 라이브러에 있는 임의의 AJAX 함수 입니다.
ajax('http://some.url.1', function callBack(data) {
    console.log(data); // 'data' 수신 완료
})

가장 기본적인 비동기식 프로그램을 처리하는 방법인 callBack으로 처리를 하면 data를 받아 볼 수 있게 되죠.

즉, 쉽게 말해서 '지금' 무언가를 요청하고 '나중'에 요청에 대한 답을 받는 것을 비동기식 프로그램이라고 할 수 있겠죠.

 

2. 이벤트 루프

사실 자바스크립트는 비동기란 개념이 있지는 않습니다. 단지 자바스크립트 엔진은 요청하면 프로그램을 주어진 시점에 한 덩이씩 묵묵히 실행할 뿐이죠. 여러 프로그램 덩이를 시간에 따라 매 순간 한 번씩 엔진을 실행 히시키는 '이벤트 루프'라는 장치를 통해서 말이죠.

자바스크립트 엔진은 애당초 시간이란 관념 따윈 없었고 임의의 자바스크립트 코드 조각을 시시각각 주는 대로 받아 처리하는 실행기 일 뿐이죠.

 

예를 들어 AJAX 요청을 할 때 함수 형태로 응답 처리 코드(callBack)를 작성하는 건 마치 자바스크립트 엔진이 호스팅 환경에 이렇게 이야기하는 것과 같다고 하네요.

"이보게, 지금 잠깐 실행을 멈출 테니 AJAX 요청이 다 끝나서 결과 데이터가 만들어지거든 언제라도 이 함수(callBack)를 다시 불러주게나"

자바스크립트 프로그램은 수많은 덩이로 잘게 나누어지고 이벤트 루프 큐에서 한 번에 하나씩 차례대로 실행됩니다. 엄밀히 말해 이 큐엔 개발자가 작성한 프로그램과 직접 상관없는 여타 이벤트들도 중간에 끼어들 가능성도 있죠.

 

3. 병렬 스레딩

비동기 와 병렬은 아무렇게나 섞어 쓰는 경우가 많지만 그 의미는 완전히 다르죠. 비동기는 '지금'과 '나중' 사이의 간극에 관한 용어이고 병렬은 동시에 일어나는 일들과 연관됩니다.

 

프로세스와 스레드는 가장 많이 쓰이는 병렬 컴퓨팅 도구로, 별개의 프로세서는 독립적으로 실행되며, 여러 스레드는 하나의 프로세스 메모리를 공유합니다.

 

반면 이벤트 루프는 작업 단위로 나누어 차례대로 실행하지만 공유 메모리에 병렬로 접근하거나 변견 할 수는 없습니다. 병렬 실행 스레드 인터리빙과 비동기 이벤트 인터리빙은 완전히 다른 수준의 단위에서 일어납니다.

function later () {
    answer = answer * 2;
    console.log('Life is ' + answer);
}

later() 함수 전체 내용은 이벤트 루프 큐가 하나의 원소로 취급하므로 이 함수를 실행 준인 스레드 입장에선 실제로 여러 상이한 저수준의 작업들이 일어날 수 있죠. answer = answer *2는 '현재 answer 값 조회 -> 곱셈 연산 수행 -> 결괏값을 다시 answer에 저장' 순으로 처리하죠.

단일-스레드 환경에서는 스레드 간섭은 일어나지 않으므로 스레드 큐에 저수준 작업의 원소가 쌓여 있어도 별문제는 없습니다. 그러나 하나의 프로그램에서 여러 스레드를 처리하는 병렬 시스템에선 예상치 못했던 일들이 일어날 수 있죠.

var a = 20;

function foo() {
    a = a + 1;
}

function bar() {
    a = a * 2;
}

ajax('http://some.url.1', foo);
ajax('http://some.url.2', bar);

자바스크립트는 단일-스레드 환경에서 작동하므로 foo() -> bar() 또는 bar() -> foo()로 실행돼서 값이 42 또는 41 이 될 수 있죠 그런데 만약 이 코드가 같은 데이터를 공유하는 병렬 스레드로 동작하면 문제가 생기죠.

 

각 코드의 스레드 의사 코드 목록을 보겠습니다.

foo():
 a. a 값을 X에 읽어 들입니다.
 b. 1을 Y에 저장한다.

 c. X와 Y를 더하고 그 결괏값을 X에 저장한다.
 d. X 값을 a에 저장한다.
():
 a. a 값을 X에 읽어 들입니다.
 b. 2를 Y에 저장한다.
 c. X와 Y를 곱하고 그 결괏값을 X에 저장한다.
 d. X 값을 a에 저장한다.

두 스레드가 병렬 상태로 실행되면 중간 단계에서 X와 Y의 메모리 공간을 공유합니다. 그것이 문제가 되죠.

 

1a. X에서 a 값을 읽어 들인다 -> 20
2a. X에서 a 값을 읽어 들인다 -> 20
1b. Y에 1을 저장한다 -> 1
2b. Y에 2를 저장한다 -> 2
1c. X와 Y를 더하고 그 결괏값을 X에 저장한다 -> 22
2c. X와 Y를 곱하고 그 결괏값을 X에 저장한다 -> 40
1d. a에 X 값을 저장한다 -> 40
2d. a에 X 값을 저장한다 -> 40
1a. X에서 a 값을 읽어 들인다 -> 20
2a. X에서 a 값을 읽어 들인다 -> 20
2b. Y에 2를 저장한다 -> 2
1b. Y에 1을 저장한다 -> 1
2c. X와 Y를 곱하고 그 결괏값을 X에 저장한다 -> 20
1c. X와 Y를 더하고 그 결괏값을 X에 저장한다 -> 21
1d. a에 X 값을 저장한다 -> 21
2d. a에 X 값을 저장한다 -> 21

이런 식으로 X, Y 메모리를 공유하고 잇기 때문에 인터 럽션/인터리빙 같은 요소가 발생하지 않도록 조치하지 않으면 결과가 제멋대로 왔다 갔다 합니다.

하지만 자바스크립트는 단일-스레드 이기 때문에 위와 같은 문제는 발생하지 않습니다. 그렇지만 위에 예제처럼 foo()와 bar()의 실행 순서에 따라 결괏값이 바뀌긴 하죠.

 

이런 것을 비결정성의 수준 문제라고 하는데 자바스크립트는 병렬성에 의한 비결정성의 수준 문제는 발생하지 않는다. 따라서 자바스크립트는 스레드보단 결정적이라고 할 수가 있습니다.

 

자바스크립트에서 함수 순서에 따른 비결정성을 흔히 경합 조건이라고 표현합니다. foo(), bar() 중 누가 먼저 실행되나 내기하는 경합 같다고 하여 이름이 붙여졌습니다.

 

4. 동시성

사용자가 스크롤바를 아래로 내리면 계속 갱신된 상태 리스트가 화면에 표시되는 웹 페이지를 만들고자 합니다. 이런 기능은 2개의 분리된 '프로세스'를 동시에 실행할 수 있어야 제대로 기능을 구현할 수 있죠.

 

첫 번째 프로레스는 사용자가 페이지를 스크롤 바로 내리는 순간 발생하는 onscroll 이벤트이고 두 번째는 그의 대한 응답(AJAX) 프로세스이죠. 만약 성미 급한 사용자가 아주 빨리 스크롤바를 내리면 처음 수신된 응답을 처리하는 도중 2개 이상의 onscroll 이벤트가 발생하기 십상이고 onscroll 이벤트와 AJAX 응답 이벤트가 빠르게 발생하여 인터리빙 되죠.

 

동시성은 복수의 프로세스가 같은 시간 동안 동시에 실행됨을 의미하며, 각 프로세스 작업들이 병렬로 처리되는지와는 관계가 없죠. 

 

<요청 프로세스>

onscroll, request 1
onscroll, request2
onscroll, request3

<응답 프로세스>

response 1
response 2
response 3

위 프로세스들을 시간 순에 따라 나열해 보면

onscroll, request 1
onscroll, request2  response 1
onscroll, request3  response 2
response 3

하지만 이벤트 루프 개념을 다시 곱씹어보면 자바스크립트는 한 번에 하나의 이벤트만 처리합니다. 따라서 같은 시간에 두 가지가 일어날 수가 없죠. 따라서 이런 식으로 인터리빙 되죠.

onscroll, request 1
onscroll, request2  
response 1
onscroll, request3  
response 2
response 3

요청 프로세스와 응답 프로세스가 동시에 실행되지만 이들을 구성하는 이벤트들은 이벤트 루프 큐에서 차례대로 실행되죠.

 

4.1 비 상호 작용

어떤 프로그램 내에서 복수의 프로세스가 이벤트를 동시에 인터리빙 할 때 이들 프로세스 사이에 연관된 작업이 없다면 프로세스 간 상호 작용은 의미가 없죠.

var res = {}

function foo(result) {
    res.foo = result;
}

function bar(result) {
    res.bar = result;
}

ajax('http://some.url.1', foo);
ajax('http://some.url.2', bar);

위처럼 동시 프로세스 foo(), bar() 중 누가 먼저 실행되어도 서로에게 영향을 끼치지 않을 때에 비 상호 작용이라고 하고 언제나 정확이 작동하므로 경합 조건에 따른 버그는 아닙니다.

 

4.2 상호 작용

var res = []

function foo(result) {
    res.push(result);
}

ajax('http://some.url.1', foo);
ajax('http://some.url.2', foo);

두 동시 프로세스 모두 AJAX 응답을 처리하는 foo() 함수를 호출하는 터라 선발 순으로 처리됩니다. 따라서 어느 프로세스가 먼저 실행되냐에 따라 결과 값이 변할 수 있죠. 

경합 조건을 해결하려면 상호 작용의 순서를 잘 조정해야 합니다.

var res = []

function foo(result) {
    if (result.url == 'http://some.url.1') {
        res[0] = result;
    } else if (result.url == 'http://some.url.2') {
        res[1] = result;
    }
}

ajax('http://some.url.1', foo);
ajax('http://some.url.2', foo);

이런 식으로 원하는 값을 얻을 수 있도록 비결정성 일 때 처리를 해주어야 합니다.

 

4.3 협동

협동적 동시성 역시 동시성을 조정하는 다른 방안으로, 스코프에서 값을 공유하는 식의 상호작용엔 별 관심이 없죠. 협동적 동시성은 실행 시간이 오래 걸리는 프로세스를 여러 단계로 쪼개어 다른 동시 프로세스가 각자 작업을 이벤트 루프 큐에 인터리빙 하도록 하는 게 목표입니다.

예를 들어 아주 긴 리스트를 받아 값을 변환하는 AJAX 응답 처리가 있다고 가정해보죠.

var res = [];

function response(data) {
    res = res.concat(
        data.map(function(val) {
            return val * 2;
        })
    );
}

ajax('http://some.url.1', reponse);
ajax('http://some.url.2', response);

 처음 몇 번은 괜찮지만 만약 개수가 천만 개의 이르는 요청이라면 시간이 제법 걸릴 것입니다. 따라서 이 프로세스는 멈춰 버리는 현상이 오죠.

따라서 이벤트 루프 큐를 독점하지 않는 좀 더 친화적이고 협동적인 시스템이 되려면 각 결과를 비동기 배치로 처리하고 이벤트 루프에서 대기 중인 다른 이벤트와 함께 실행되게끔 해줘야 합니다.

var res = [];

function response(data) {
    // 한 번에 1000 개씩만 실행
    var chunk = data.splice(0, 1000);

    res = res.concat(
        chunk.map(function(val) {
            return val * 2;
        })
    )

    // 아직도 처리해야할 프로세스가 남아 있나?
    if (data.length > 0) {
        // 다음 배치를 비동기 스케줄링 한다.
        setTimeout(function() {
            response(data);
        }, 0);
    }
}

ajax('http://some.url.1', reponse);
ajax('http://some.url.2', response);

최대 1000개 원소를 가진 덩이 단위로 데이터 집합을 처리했죠. 이렇게 하면 더 많은 후속 프로세스를 처리해야 하지만 각 프로세스 처리 시간은 단축되므로 이벤트 루프 큐에 인터리빙이 가능하고 응답성이 좋은 페이지를 만들 수 있죠.

 

물론 이렇게 나뉜 프로세스 들의 실행 순서까지 조정한 것은 아니므로 res 배열에 어떤 순서로 결과가 저장될지 예측하긴 어렵지만 앞서 설명한 기법들을 통하여 원하는 결과를 받아 낼 수 있어야 합니다.

 

5. 잡

잡 큐는 ES6부터 이벤트 루프 큐에 새롭게 도입된 개념입니다. 잡 큐는 이벤트 루프 큐에서 매번 끝자락에 매달려 있는 큐라고 생각하면 쉽습니다. 이벤트 루프 도중 발생 가능한, 비동기 특성이 내재된 액션으로 인해 전혀 새로운 이벤트가 이벤트 루프 큐에 추가되는 게 아니라 현재 처리하고 있는 루프의 바로 다음 부분에 추가되는 것이죠.

 

마치 이런 말을 하는 것과 같죠. "자 이건 '나중'에 처리할 작업인데 이거 끝나자마자 해줘" 노래방에 우선 예약이라고 보면 쉬울 듯하네요.

console.log('a')

setTimeout(function() {
    console.log('b')
}, 0);

schedule(function() {
    console.log('c');

    schedule(function() {
        console.log('d')
    })
})

실행 순서가 a, b, c, d 일 것 같지만 잡 스케쥴링인 (schedule)로 인하여 a, c, d, b 가 됩니다.

 

정리

- 자바스크립트 프로그램은 언제나 2개 이상의 덩이로 쪼개지며 이벤트 응답으로 첫 번째 덩이는 '지금', 다음 덩이는 '나중'에 실행됩니다. 한 덩이씩 실행되어도 모든 덩이가 프로그램의 스코프/상태에 똑같이 접근할 수 있으므로 상태 변화는 차례대로 반영됩니다.

 

- 실행할 이벤트가 있으면 이벤트 루프는 큐를 다 비울 때까지 실행합니다. 이벤트 루프를 한 차례 순회하는 것을 틱이라고 합니다.

 

- 언제나 한 번에 정확히 한 개의 이벤트만 큐에서 꺼내 처리합니다. 이벤트 실행 도중, 하나 또는 그 이상의 후속 이벤트를 직/간접적으로 일으킬 수 있죠.

 

- 동시성은 복수의 이벤트들이 연쇄적으로 시간에 따라 인터리빙 되면서 고수준의 관점에서 볼 때 꼭 동시에 실행되는 것처럼 보입니다.

 

- 동시 프로세스들은 어떤 형태로든 서로 영향을 미치는 작업을 조정하여 실행 순서를 보장하거나 경합 조건을 예방하는 등의 조치를 해야 합니다. 이 프로세스 자체를 더 작은 덩이로 잘게 나누어 다른 프로세스에 인터리빙 되는 형태의 협동 또한 가능합니다.

'JavaScript' 카테고리의 다른 글

[JavaScript] Closure  (0) 2019.07.08
[JavaScript] Scope와 Hoisting  (0) 2019.07.08
[You don't know JS] - 작동 위임  (0) 2019.06.07
[You don't know JS] - 프로토타입  (0) 2019.05.29
[You don't know JS] - 클래스와 객체의 혼합  (0) 2019.05.21
Comments