Skip to content

타임라인 조율하기 ​

지난 글에서는 공유 자원을 동시성 기본형으로 안전하게 공유했습니다.

이번 글에서는 눈에 보이는 공유 자원은 없지만 타임라인이 함께 협력해야 하는 경우가 있습니다. 타임라인을 조율하기 위한 두가지 동시성 기본형을 만들어 봅시다.

  • Cut: “병렬 콜백을 모두 기다린 뒤 다음 단계로 넘어가기”
  • JustOnce: “정말 딱 한 번만 실행하기”

스터디 회차: 17회차 (2025년 9월 18일)

모든 병렬 콜백 기다리는 기본형 Cut ​

예제: 장바구니 계산 로직 속도 개선 ​

tsx
function add_item_to_cart(item) {
  cart = add_item(cart, item);
  update_total_queue(cart);
}

function calc_cart_total(cart, callback) {
  var 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);
  });
}

var update_total_queue = DroppingQueue(1, calc_cart_worker);
  • 원래는 shipping_ajax가 cost_ajax의 내부 콜백으로 순차 실행됐습니다.
  • 속도를 위해 병렬 호출로 바꾸면 평균 시간은 줄지만, 콜백 완료 순서가 바뀔 수 있어 간헐적 오동작이 납니다.

왜 속도가 더 빠른가요?

타임라인 조율하기

  • 순서대로 처리하는 것보다 병렬로 처리하는게 더 빨리 끝납니다.
  • 다만 나란히 실행되기 때문에 실행 순서를 보장할 수 없습니다.

total은 지역변수인데 왜 타임라인에 추가되나요?

  • total은 지역변수입니다.
  • 지역변수는 같은 타임라인에서 접근하기 때문에 액션이 아니었고 그래서 타임라인에서 제거했습니다.
  • 하지만 이제는 total은 서로 다른 타임라인에서 공유하기 때문에 다이어그램에 그려야합니다.

타임라인으로 나타내기 ​

타임라인 조율하기

  • 속도를 개선한 코드를 타임라인으로 그리면 위와 같습니다.

문제점 ​

타임라인 조율하기

  • 서로 다른 타임라인에서 실행되는 두 콜백은 기대와 다른 결과가 나타날 수 있습니다.
  • 두 콜백의 타임라인이 서로 다른 시각에 total을 업데이트하고, 그 사이에 DOM 업데이트가 끼어드는 상황이 생깁니다.

개선하기: Cut 이용하기 ​

타임라인 조율하기

  • 동시에 도착하는 ajax 응답을 모두 기다렸다가 DOM을 업데이트하면 해결할 수 있습니다.
  • Cut을 이용해서 두 콜백이 모두 끝나고 다음 단계를 실행합니다.

Cut은 경쟁 조건을 막을 수 있습니다.

  • 경쟁 조건은 어떤 동작이 먼저 끝나는 타임라인에 의존할 때 발생합니다.
  • Cut은 여러 타임라인이 다른 시간에 종료되어도 기다릴수 있는 간단한 동시성 기본형입니다.

Cut 구현하기 ​

tsx
function Cut(num, callback) {
  var num_finished = 0;
  return function () {
    num_finished += 1;
    if (num_finished === num) callback();
  };
}
  • 호출 될때마다 호출된 횟수를 증가시키고 마지막 타임라인이 함수를 호출했을때 callback을 실행합니다.
  • 따라서, 앞선 num 만큼의 호출을 기다리고 그 뒤에 실행하는 것을 보장합니다.

코드에 Cut 적용하기 ​

Cut()을 적용하기 위해서는 두 가지를 고민해야합니다.

  1. Cut()을 보관할 범위
  2. Cut()에 어떤 콜백을 넣을지
tsx
function calc_cart_total(cart, callback) {
  var total = 0;
  var done = Cut(2, function () {
    callback(total);
  });
  cost_ajax(cart, function (cost) {
    total += cost;
    done();
  });
  shipping_ajax(cart, function (shipping) {
    total += shipping;
    done();
  });
}
  • 두 응답 콜백을 만드는 calc_cart_total 에 Cut()을 선언하는 게 좋습니다.
  • calc_cart_total에는 이미 total 계산 후 부르는 콜백이 있으므로 Cut() 콜백에서 그대로 실행하면 됩니다.
  • Cut() 은 두 콜백이 모두 실행되고 나서 Cut()의 콜백이 실행되는 것을 보장합니다.

자바스크립트에서 동시성 기본형

  • 자바스크립트에서는 이미 구현된 동시성 기본형이 많이 있습니다.
  • Cut()과 매우 비슷한 Promise.all()이 있기 때문에 이것을 사용하면 됩니다.

딱 한 번만 호출하는 기본형 JustOnce ​

예제: 처음 방문한 사용자에게 환영 메세지 보내기 ​

tsx
function sendAddToCartText(number) {
  sendTextAjax(
    number,
    "Thanks for adding something to your cart. Reply if you have any questions!"
  );
}
  • 딱 한번만 실행되어야합니다.
  • 하지만 sendAddToCartText는 계속 실행될 수 있습니다.

JustOnce 구현하기 ​

tsx
function JustOnce(action) {
  var alreadyCalled = false;
  return function (a, b, c) {
    if (alreadyCalled) return;
    alreadyCalled = true;
    return action(a, b, c);
  };
}
  • alreadyCalled 는 false 값을 가지고 한번 실행되면 true가 됩니다.
  • 여러번 호출하더라도 alreadyCalled 에 의해 action은 한 번만 실행됩니다.

개선하기: JustOnce 이용하기 ​

tsx
var sendAddToCartTextOnce = JustOnce(sendAddToCartText);

sendAddToCartTextOnce("555-555-5555-55"); // 실행
sendAddToCartTextOnce("555-555-5555-55"); // 무시
sendAddToCartTextOnce("555-555-5555-55"); // 무시
sendAddToCartTextOnce("555-555-5555-55"); // 무시
  • JustOnce로 인해 첫번째 함수만 실행됩니다.

멱등원

  • 최초 한 번만 효과가 발생하는 액션을 멱등원이라고 합니다.
  • JustOnce 는 어떤 액션이든 멱등원으로 만들 수 있습니다.

정리하기 ​

  • 문제의 본질은 값이 아니라 순서입니다. 지역변수라도 여러 타임라인이 교차하면 공유자원처럼 관리해야 합니다.
  • Cut은 N개의 병렬 콜백이 모두 끝난 시점을 한 군데로 모아 레이스 컨디션을 없애는 기본형입니다. 자바스크립트에서는 Promise.all로 대체할 수 있습니다.
  • JustOnce는 “딱 한 번” 실행을 보장하는 기본형입니다.