해당 포스트는
쏙쏙 들어오는 함수형 코딩
을 학습하며 필요한 내용을 정리한 포스트입니다.
🌈 17. 타임라인 조율하기
이번 장에서 살펴볼 내용
- 타임라인을 조율하기 위한 동시성 기본형을 만들어 본다.
- 시간에 관한 중요한 관점인 순서와 반복을 함수형 개발자들이 어떻게 다루는지
💻 좋은 타임라인의 원칙
1) 타임라인은 적을수록 이해하기 쉽다.
타임라인이 하나인 시스템이 가장 이해하기 쉽다.
하지만 요즘 시스템에는 여러 타임라인이 필요하다.
멀티스레드, 비동기 콜백, 클라이언트-서버 간 통신 등에 새로운 타임라인이 필요
2) 타임라인은 짧을수록 이해하기 쉽다.
- 타임라인의 단계를 줄이면 실행 가능한 순서도 많이 줄일 수 있다.
3) 공유하는 자원이 적을수록 이해하기 쉽다.
서로 다른 타임라인에 있는 두 액션이 서로 자원을 공유하지 않는 다면 실행 순서에 신경 쓸 필요가 없다.
서로 자원을 공유하는 액션을 주의 깊게 살펴보자.
4) 자원을 공유한다면 서로 조율해야 한다.
공유 자원을 안전하게 공유할 수 있어야 한다.
올바른 순서대로 자원을 쓰고 돌려준다는 말
타임라인 간 조율은 올바른 결과를 주지 않는 실행 순서를 없애는 것
5) 시간을 일급으로 다룬다.
- 타임라인 다루는 재사용 가능한 객체를 만들면 타이밍 문제를 쉽게 만들 수 있다.
새로 버그가 생긴 장바구니에 다섯 번째 원칙을 적용해보자.
💻 버그가 있다!
제품을 하나만 추가해도 가끔 잘못된 합계가 표시됨
장바구니에 제품 하나만 추가해도, 또한 여러 개의 제품을 빠르게 추가해도 발생한다.
하지만 재현할 수 없다.
먼저 제품 하나를 추가했을 때의 버그를 수정해보자.
💻 코드가 어떻게 바뀌었을까
최적화하기 전에는 모든 코드가 잘 동작했음.
하지만, 이제 가끔 실패함
뭐가 바뀐걸까?
🔻 최적화하기 전(동작✅)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function add_item_to_cart(item){
cart = add_item(cart, item);
update_total_queue(cart);
}
function calc_cart_total(cart, callback){
let total = 0;
cost_ajax(cart, function(cost){
total += cost;
shipping_ajax(cart, function(shipping){
total += shipping;
callback(total);
})
})// 닫는 괄호의 위치가 바뀌었음
}
function calc_cart_worker(cart, done){
calc_cart_total(cart, function(total){
update_total_dom(total);
done(total);
})
}
let update_total_queue = DroppingQueue(1, calc_cart_worker);
🔻 최적화한 후(동작❌)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function add_item_to_cart(item){
cart = add_item(cart, item);
update_total_queue(cart);
}
function calc_cart_total(cart, callback){
let total = 0;
cost_ajax(cart, function(cost){
total += cost;
})
shipping_ajax(cart, function(shipping){
total += shipping;
callback(total);
})// 닫는 괄호의 위치가 바뀌었음
}
function calc_cart_worker(cart, done){
calc_cart_total(cart, function(total){
update_total_dom(total);
done(total);
})
}
let update_total_queue = DroppingQueue(1, calc_cart_worker);
닫는 괄호의 위치가 바뀌었다
왜 이런 일이 벌어지는걸까?
💻 액션을 확인하기 : 단계 1
다이어그램을 그리기 위한 세 단계
액션을 확인하기
각 액션을 그리기
단순화하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function add_item_to_cart(item){
cart = add_item(cart, item);
//액션
update_total_queue(cart);
} //액션 //액션
function calc_cart_total(cart, callback){
let total = 0;
// 액션
cost_ajax(cart, function(cost){
//액션
total += cost;
//액션
})
shipping_ajax(cart, function(shipping){
total += shipping;
//액션
callback(total);
//액션
})
}
function calc_cart_worker(cart, done){
calc_cart_total(cart, function(total){
update_total_dom(total);
//액션
done(total);
})
}
let update_total_queue = DroppingQueue(1, calc_cart_worker);
안전하게 공유되는지?
어떤 가정을 하지 않고 타임라인 다이어그램을 그리는 것이 중요
💻 모든 액션을 그리기 : 단계 2
첫 번째 단계를 진행하면서 코드에 모든 액션을 확인했다.
일단 전에 했던 가정들은 모두 무시할 것!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function add_item_to_cart(item){
cart = add_item(cart, item);
update_total_queue(cart);
}
function calc_cart_total(cart, callback){
let total = 0;
cost_ajax(cart, function(cost){
total += cost;
})
shipping_ajax(cart, function(shipping){
total += shipping;
callback(total);
})
}
function calc_cart_worker(cart, done){
calc_cart_total(cart, function(total){
update_total_dom(total);
done(total);
})
}
액션
1) cart 읽기
2) cart 쓰기
3) total = 0 쓰기
4) cart 읽기
5) cost_ajax() 부르기
6) total 읽기
7) total 쓰기
8) cart 읽기
9) shipping_ajax() 부르기
10) total 읽기
11) total 쓰기
12) total 읽기
13) update_total_dom() 부르기
이를 통해 다이어 그램을 그려보면
💻 다이어그램 단순화하기
🍳 자바스크립트 스레드 모델에서 단순화하기 위한 단계
하나의 타임라인에 있는 모든 액션을 하나로 통합
타임라인이 끝나느 곳에서 새로운 타임라인이 하나만 생긴다면 통합
첫 번째 단계
- 실행순서를 명확하게 하기 위해 점선을 단계가 끝나는 지점으로 이동
두 번째 단계
타임라인이 끝나는 곳에 새로운 타임라인이 생긴다면 하나로 합치는 단계
새로 생기는 타임라인이 하나만 있어야 적용할 수 있음
큐 타임라인과 두 ajax 콜백 타임라인은 합칠 수 없음
- 큐 타임라인이 끝나면서 두 개의 타임라인이 생기기 때문
이제 타임라인에 있는 단계가 어떤 자원을 공유하는지 알 수 있음
공유자원은 total 변수뿐
- total은 지역변수지만 모든 타임라인에서 접근하고 있다.
💻 실행 가능한 순서 분석하기
앞서 타임라인 다이어그램을 완성하고 타임라인이 공유하는 자원은 total 변수뿐이라는 것을 확인했다.
두 콜백 타임라인의 실행 가능한 순서를 알아보자.
두 콜백이 기대하지 않은 수너로 실행될 수 있음
요청 순서는 올바르지만 실행 순서가 바뀔 수 있음
왜 잘못된 코드가 더 빠르게 실행되는거지?
💻 왜 지금 타임라인이 더 빠를까?
cost_ajax() 응답은 3초가 걸리고 shipping_ajax()는 4초가 걸린다고 가정해보자.
왼쪽 타임 라인은 두 응답을 순서대로 기다려야 한다. 따라서 걸리는 시간을 더해야 한다.
오른쪽 타임라인은 두 응답을 병렬로 기다린다. 따라서 둘 중 더 긴 시간이 걸린다.
그러므로 오른쪽 타임라인이 더 빨리 끝난다.
실패하지 않고 병렬로 응답을 기다려 실행 속도를 개선할 수 있을까?
-> 동시성 기본형!
💻 모든 병렬 콜백 기다리기
동시에 도착하는 ajax 응답을 모두 기다렸다가 DOM을 업데이트하자.
원하는 결과는 다음과 같다.
두 콜백은 서로 끝나기 를 기다린다.
다이어그램에서 점선이 이러한 것을 나타내며 컷
이라고 해보자.
컷은 앞서 사용했던 점선처럼 순서를 보장해주는 역할
컷은 앞에서 본 점선과 다르게 여러 타임라인의 끝에 맞춰 그림
타임라인에 컷이 있다면 컷 위에 있는 단계는 컷 아래가 실행되기 전에 모두 종료되어야 함
컷의 장점
타임라인을 앞부분과 두시부분으로 구분
컷을 기준으로 앞과 뒤의 타임라인을 따로 분석할 수 있음
컷의 앞부분과 뒷부분의 액션들은 서로 섞이지 않음
실행 가능한 순서를 줄여주기 때문에 애플리케이션의 복잡성을 줄여줌
💻 타임라인을 나누기 위한 동시성 기본형
여러 타임라인이 다른 시간에 종료되도 서로 기다릴 수 있는 간단하고 재사용할 수 있는 기본형이 필요
여러 타임라인이 실행되는 순서를 신경쓰지 않아도 되고 타임라인이 모두 끝나는 것도 쉽게 처리할 수 있기 때문
경쟁 조건을 막을 수 있다.
경쟁조건
어떤 동작이 먼저 끝나는 타임라인에 의존할 때 발생
멀스레드를 지원하는 언어에서는 원자적 업데이트 기능을 사용
자바스크립트는 단일 스레드이므로 가능한 동기적으로 접근하는 간단한 변수로 동시성 기본형을 구현할 수 있음
1
2
3
4
5
6
7
8
9
10
11
//기다릴 타임라인 수
function Cut(num, callback){ // 모든 것이 끝났을 때 실행할 콜백
var num_finished = 0; //카운터 0으로 초기화
return function(){
// 리턴되는 함수는 타임라인이 끝났을 때 호출
num_finished += 1; // 함수를 호출할 때마다 카운터 증가
if(num_finished === num){
callback(); // 마지막 타임라인이 끝났을 때 호출
}
}
}
💻 코드에 Cut() 적용하기
장바구니에 제품을 추가하는 코드에 적용해보자
- Cut() 을 보관할 범위
- Cut() 에 어떤 콜백을 넣을지
🔻 Cut() 을 적용한 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function calc_cart_total(cart, callback){
var total = 0;
let done = Cut(2, function(){
callback(total);
})
}
cost_ajax(cart, function(cost){
total += cost;
done();
})
shipping_ajax(cart, function(shipping){
total += shipping;
done();
})
💻 불확실한 순서 분석하기
💻 병렬 실행 분석
💻 여러 번 클릭하는 경우 분석
처음 보는 표현이 생겼는데, 실제로 타임라인은 두 개지만 Cut()을 사용했기 때문에 하나로 합쳐짐
이런 표현은 다이어그램이 유연하다는 것을 보여줌
세부적인 것이 많은 복잡한 상황을 분석하기 위해 이런 표현을 사용하면 좋음
가능한 실행 순서의 개수 공식
💻 암묵적 시간 모델 VS 명시적 시간 모델
자바스크립트의 시간 모델은 간단함
순차적 구문은 순서대로 실행된다.
두 타임라인에 있는 단계는 왼쪽 먼저 실행되거나, 오른쪽 먼저 실행될 수 있다.
비동기 이벤트는 새로운 타임라인에서 실행된다.
액션은 호출될 때마다 실행된다.
💻 요약 : 타임라인 사용하기
타임라인 수를 줄인다.
타임라인 길이를 줄인다.
공유 자원을 없앤다.
동시성 기본형으로 자원을 공유한다.
동시성 기본형으로 조율한다.
💻 요점 정리
함수형 개발자는 언어가 제공하는 암묵적 시간 모델 대신 새로운 시간 모델을 만들어 사용한다.
- 새로운 모델은 해결하려고 하는 문제를 푸는 데 도움이 된다.
명시적 시간 모델은 종종 일급 값으로 만든다.
- 일급 값으로 만든 시간 모델은 프로그래밍 언어를 사용해서 시간을 다룰 수 있다.
타임라인을 조율하기 위해 동시성 기본형을 만들 수 있다.
- 간으한 순서를 제한해 항상 올바른 결과가 나올 수 있도록 보장
타임라인을 나누는 것도 타임라인을 조율하는 방법 중 하나.
- 컷은 모든 타임라인의 작업이 끝날 때까지 기다렸다가 새로운 타임라인을 시작할 수 있다.
📚 레퍼런스
Normand, 김은민, and Normand, Eric. (쏙쏙 들어오는) 함수형 코딩 : 심플한 코드로 복잡한 소프트웨어 길들이기 / 에릭 노먼드 지음 ; 김은민 옮김 (2022). Print.