Promise.all의 λ™μ‹œμ„± μ΄ν•΄ν•˜κΈ°

@choi2021 Β· March 03, 2024 Β· 10 min read

πŸ™„ μžλ°”μŠ€ν¬λ¦½νŠΈκ°€ λ³‘λ ¬μ²˜λ¦¬?

μ•„λ§ˆ μž‘λ…„ 말뢀터 2μ›”κΉŒμ§€ μž‘μ—…ν–ˆλ˜ μž‘μ—… 쀑 κ°€μž₯ 많이 ν•œ μž‘μ—… 쀑 ν•˜λ‚˜κ°€ 순차적으둜 μ§„ν–‰λ˜λŠ” 비동기 μ½”λ“œλ₯Ό Promise.all()을 μ΄μš©ν•΄ ν•œλ²ˆμ— λ™μž‘ν•  수 있게 μ΅œμ ν™”ν•˜λŠ” μž‘μ—…μ΄μ—ˆλ‹€. μ‚¬λžŒλ“€μ—κ²Œ 말할 λ•ŒλŠ” λ³‘λ ¬μ²˜λ¦¬λΌκ³  ν‘œν˜„ν•˜κ³€ ν–ˆμ§€λ§Œ λ‚΄μ•ˆμ— 어색함이 λŠκ»΄μ‘Œλ‹€. μžλ°”μŠ€ν¬λ¦½νŠΈλŠ” μ‹±κΈ€μŠ€λ ˆλ“œ 언어인데 μ–΄λ–»κ²Œ μ—¬λŸ¬κ°€μ§€ 일을 λ™μ‹œμ— μ²˜λ¦¬ν•˜λŠ” κ±ΈκΉŒμ— λŒ€ν•œ μ§ˆλ¬Έμ„ μ°Ύμ•„κ°”λ˜ 과정을 정리해보렀 ν•œλ‹€.

πŸ€” μžλ°”μŠ€ν¬λ¦½νŠΈ μ—”μ§„μ˜ λ™μž‘ νŒŒν—€μ³λ³΄κΈ°

μžλ°”μŠ€ν¬λ¦½νŠΈλŠ” μ‹±κΈ€ μŠ€λ ˆλ“œ μ–Έμ–΄λ‹€. 이말의 μ˜λ―ΈλŠ” ν•œλ²ˆμ— ν•˜λ‚˜μ˜ 일만 ν•  수 있게 μ„€κ³„λ˜μ–΄ μžˆλŠ” μ–Έμ–΄λ‘œ 이벀트 루프λ₯Ό μ΄μš©ν•΄ call stack으둜 λ“€μ–΄μ˜¨ ν•˜λ‚˜μ˜ 일만 ν•œλ²ˆμ— μ²˜λ¦¬ν•  수 μžˆλ‹€. 그러면 μ–΄λ–»κ²Œ Promise.all([...]) 같은 μ½”λ“œλ₯Ό 톡해 λ™μ‹œμ— μ—¬λŸ¬κ°€μ§€ 일을 λ™μ‹œμ— μ²˜λ¦¬ν•˜λŠ” 것이 κ°€λŠ₯ν• κΉŒ.

Sequence vs Parallel vs Concurrent

μžλ°”μŠ€ν¬λ¦½νŠΈμ—μ„œ μ—¬λŸ¬κ°€μ§€ 일을 λ™μ‹œμ— μ²˜λ¦¬ν•˜λŠ” 것에 λŒ€ν•΄ μ •ν™•ν•˜κ²Œ μ΄ν•΄ν•˜κΈ° μœ„ν•΄μ„œλŠ” Sequence, Parallel, Concurrent의 차이λ₯Ό 이해해야 ν•œλ‹€.

  • Sequence: 순차적으둜 μ‹€ν–‰λ˜λŠ” 것을 μ˜λ―Έν•œλ‹€. A -> B -> C μˆœμ„œλŒ€λ‘œ μ‹€ν–‰λ˜λŠ” 것을 μ˜λ―Έν•œλ‹€.
  • Parallel: λ§κ·ΈλŒ€λ‘œ λ™μ‹œμ— μ‹€ν–‰λ˜λŠ” 것을 μ˜λ―Έν•œλ‹€. A, B, Cκ°€ λ™μ‹œμ— μ‹€ν–‰λ˜λŠ” 것을 μ˜λ―Έν•œλ‹€.
  • Concurrent: λ³‘λ ¬μ μœΌλ‘œ μ‹€ν–‰λ˜λŠ” κ²ƒμ²˜λŸΌ λ³΄μ΄μ§€λ§Œ, μ‹€μ œλ‘œλŠ” λ™μ‹œμ— μ‹€ν–‰λ˜μ§€ μ•ŠλŠ” 것을 μ˜λ―Έν•œλ‹€. A, B, Cκ°€ λ™μ‹œμ— μ‹€ν–‰λ˜λŠ” κ²ƒμ²˜λŸΌ λ³΄μ΄μ§€λ§Œ, μ‹€μ œλ‘œλŠ” Aκ°€ μ‹€ν–‰λ˜κ³ , Bκ°€ μ‹€ν–‰λ˜κ³ , Cκ°€ μ‹€ν–‰λ˜λŠ” 것을 μ˜λ―Έν•œλ‹€.

좜처: [Does Promise.all Execute in Parallel? How Promise.all Works in JavaScript](https://javascript.plainenglish.io/does-promise-all-execute-in-parallel-how-promise-all-works-in-javascript-fffc2e8d455d)
좜처: [Does Promise.all Execute in Parallel? How Promise.all Works in JavaScript](https://javascript.plainenglish.io/does-promise-all-execute-in-parallel-how-promise-all-works-in-javascript-fffc2e8d455d)

λ”°λΌμ„œ μœ„ μ •μ˜μ— 따라 μžλ°”μŠ€ν¬λ¦½νŠΈκ°€ μ—¬λŸ¬κ°€μ§€ 일을 λ™μ‹œμ— μ²˜λ¦¬ν•  수 μžˆλ‹€λŠ” κ²ƒμ˜ μ˜λ―Έκ°€ parallel이라면 μ‹€μ œλ‘œ μ—¬λŸ¬κ°€μ§€ 일을 λ™μ‹œμ— μ‹€ν–‰ν•˜λŠ” κ²ƒμœΌλ‘œ, concurrentν•˜λ‹€λ©΄ 순차적으둜 μ§„ν–‰ν•˜μ§€λ§Œ λΉ λ₯΄κ²Œ μ§„ν–‰λ˜κΈ°μ— λ™μ‹œμ— μ§„ν–‰λ˜λŠ” κ²ƒμ²˜λŸΌ 보인닀고 ν•  수 μžˆλ‹€. parallelκ³Ό concurrent 두가지 λ™μž‘ 쀑 μ–΄λ–€ 게 λ§žλŠ”μ§€ ν™•μΈν•˜κΈ° μœ„ν•΄ 예λ₯Ό λ“€μ–΄, μ½”λ“œμ™€ ν•¨κ»˜ μ•Œμ•„λ³΄μž.

μžλ°”μŠ€ν¬λ¦½νŠΈ μŠ€λ ˆλ“œλ₯Ό μ›¨μ΄ν„°λ‘œ 예λ₯Ό λ“ λ‹€λ©΄ λ‹€μŒκ³Ό 같이 정리할 수 μžˆλ‹€.

  • Sequence: 웨이터가 ν•˜λ‚˜μ˜ ν…Œμ΄λΈ”μ— λŒ€ν•΄ 주문을 λ°›κ³  μŒμ‹μ΄ λ‚˜μ˜€λ©΄, λ‹€μŒ ν…Œμ΄λΈ”μ— λŒ€ν•΄ 주문을 λ°›λŠ” 것을 μ˜λ―Έν•œλ‹€.
  • Parallel: μ—¬λŸ¬ 웨이터가 μ—¬λŸ¬ ν…Œμ΄λΈ”μ— λŒ€ν•΄ λ™μ‹œμ— 주문을 λ°›κ³  λ™μ‹œμ— μŒμ‹μ΄ λ‚˜μ˜€λŠ” 것을 μ˜λ―Έν•œλ‹€.
  • Concurrent: ν•œλͺ…μ˜ 웨이터가 μ—¬λŸ¬ ν…Œμ΄λΈ”μ˜ 주문을 μˆœμ„œλŒ€λ‘œ λ°›κ³  주문을 λ„£μ§€λ§Œ λΉ λ₯΄κ²Œ 이 과정이 μ§„ν–‰λ˜λ‹€ λ³΄λ‹ˆ μ—¬λŸ¬ ν…Œμ΄λΈ”μ˜ λŒ€ν•œ μŒμ‹μ΄ 거의 λ™μ‹œμ— λ‚˜μ˜€λŠ” 것을 μ˜λ―Έν•œλ‹€.

그러면 예제 μ½”λ“œλ₯Ό μ΄μš©ν•΄ μœ„ μ„Έκ°€μ§€ 상황에 λŒ€ν•΄ ν™•μΈν•΄λ³΄μž. μ•„λž˜ μ½”λ“œλŠ” orderAndServeλΌλŠ” ν•¨μˆ˜λ₯Ό μ΄μš©ν•΄ ν…Œμ΄λΈ”μ— λŒ€ν•œ 주문을 λ°›κ³  μŒμ‹μ„ μ„œλΉ™ν•˜λŠ” μ½”λ“œμ΄λ‹€.

function orderAndServe(startTime, ms, tableNumber) {
  console.log(`ν…Œμ΄λΈ” ${tableNumber} 도착 - ${new Date() - startTime}ms`)
  return new Promise(resolve => {
    console.log(`ν…Œμ΄λΈ” ${tableNumber} μ£Όλ¬Έ λ°›μŒ - ${new Date() - startTime}ms`)
    return setTimeout(() => {
      resolve(tableNumber)
    }, ms)
  }).then(number => {
    console.log(
      `ν…Œμ΄λΈ” ${number} ${ms}ms ν›„ μŒμ‹ λ‚˜μ˜΄ - $${new Date() - startTime}ms`
    )
  })
}

async function sequenceRun(startTime) {
  await orderAndServe(startTime, 1000, 1)
  await orderAndServe(startTime, 1000, 2)
}

async function concurrentOrParallelRun(startTime) {
  await Promise.all([
    orderAndServe(startTime, 1000, 1),
    orderAndServe(startTime, 1000, 2),
  ])
}

λ§Œμ•½ μžλ°”μŠ€ν¬λ¦½νŠΈκ°€ Parallelν•˜κ²Œ λ™μž‘ν•˜λ‹€λ©΄ 두가지 가정을 ν•΄λ³Ό 수 μžˆλ‹€.

  • μ‹€ν–‰ν•  λ•Œ λ§ˆλ‹€ λλ‚˜λŠ” ν…Œμ΄λΈ”μ˜ μˆœμ„œκ°€ λ‹€λ₯Ό 수 μžˆλ‹€. 독립적인 μŠ€λ ˆλ“œμ—μ„œ μ§„ν–‰λ˜κΈ° λ•Œλ¬Έμ— 결과의 μˆœμ„œκ°€ 보μž₯λ˜μ§€ μ•Šμ„ 수 μžˆλ‹€.
  • 각 ν…Œμ΄λΈ”μ— λ„μ°©ν•˜λŠ” μ‹œκ°„, 주문을 λ°›λŠ” μ‹œκ°„, μŒμ‹μ΄ λ‚˜μ˜€λŠ” μ‹œκ°„μ΄ κ°™λ‹€.
Sequential Concurrent
sequential concurrent

μœ„ 사진은 두가지 ν…Œμ΄λΈ”μ— λŒ€ν•΄μ„œ 각 ν•¨μˆ˜λ₯Ό μ‹€ν–‰ν•œ 결과둜, sequential처럼 항상 ν…Œμ΄λΈ”1μ—μ„œ ν…Œμ΄λΈ” 2둜 같은 μˆœμ„œλ‘œ μ§„ν–‰λ˜λŠ” 것을 λ³Ό 수 μžˆλ‹€.

μœ„ κ°€μ •ν–ˆλ˜ 두가지 쀑 parallel ν•˜λ‹€λŠ” κ°€μ •μ˜ μ²«λ²ˆμ§Έκ°€ μ–΄κΈ‹λ‚œ 것을 λ³Ό 수 μžˆλ‹€. ν•˜μ§€λ§Œ 두 번째 κ°€μ •μ΄μ—ˆλ˜ ν…Œμ΄λΈ”λ³„ 도착, μ£Όλ¬Έ, μ„œλΉ™ μ‹œκ°„μ΄ κ°™λ‹€λŠ” 가정은 λ§žλŠ” 것을 λ³Ό 수 μžˆλ‹€. 그러면 쑰금 더 λ§Žμ€ ν…Œμ΄λΈ”μ„ μ„œλΉ™ν•˜κ²Œ ν•΄λ³΄μž

function orderAndServe(startTime, ms, tableNumber) {
  console.log(`ν…Œμ΄λΈ” ${tableNumber} 도착 - ${new Date() - startTime}ms`)
  return new Promise(resolve => {
    console.log(`ν…Œμ΄λΈ” ${tableNumber} μ£Όλ¬Έ λ°›μŒ - ${new Date() - startTime}ms`)
    return setTimeout(() => {
      resolve(tableNumber)
    }, ms)
  }).then(number => {
    console.log(
      `ν…Œμ΄λΈ” ${number} ${ms}ms ν›„ μŒμ‹ λ‚˜μ˜΄ - $${new Date() - startTime}ms`
    )
  })
}
const tables = Array.from({ length: 10 }, (_, i) => i + 1)

async function concurrentOrParallelRun(startTime) {
  await Promise.all(
    tables.map(tableNumber => orderAndServe(startTime, 1000, tableNumber))
  )
}
concurrentRun(new Date())

μ•„λž˜ 사진은 μœ„ 10개의 ν…Œμ΄λΈ”μ— λŒ€ν•œ μ½”λ“œλ₯Ό μ‹€ν–‰ν•œ κ²°κ³Όλ‹€. μ•„λž˜κ²°κ³Όλ₯Ό 보면 ν…Œμ΄λΈ” 1λΆ€ν„° 10κΉŒμ§€ μˆœμ„œκ°€ μœ μ§€λ˜κ³ , ν…Œμ΄λΈ”λ³„ 각 λ™μž‘ μ™„λ£Œμ‹œκ°„μ˜ 차이가 λ‚˜λŠ” 것을 λ³Ό 수 μžˆλ‹€.

ν…Œμ΄λΈ”μ΄ 10개일 λ•Œ κ²°κ³Ό
ν…Œμ΄λΈ”μ΄ 10개일 λ•Œ κ²°κ³Ό

이λ₯Ό 톡해 Promise.all()은 병렬적(parallelism)으둜 μ²˜λ¦¬ν•˜λŠ” 것이 μ•„λ‹ˆλΌ μžλ°”μŠ€ν¬λ¦½νŠΈμ˜ λ™μ‹œμ„±(concurrency)을 μ΄μš©ν•΄ 순차적으둜 μ§„ν–‰ν•˜μ§€λ§Œ λΉ λ₯΄κ²Œ μ§„ν–‰ν•¨μœΌλ‘œμ„œ μ„±λŠ₯의 이점을 얻을 수 μžˆλŠ” λ©”μ†Œλ“œμž„μ„ μ•Œ 수 μžˆμ—ˆλ‹€.

λ²ˆμ™Έ: 병렬을 μ§€μ›ν•˜λ©΄ κ²°κ³Όκ°€ μ–΄λ–»κ²Œ λ‹¬λΌμ§ˆκΉŒ

λ‚΄κ°€ ν–ˆλ˜ parallelism에 λŒ€ν•œ 두가지 가정이 μ‹€μ œ 병렬 처리 μ§€μ›ν•˜λŠ” μ–Έμ–΄μ—μ„œ μœ μ˜λ―Έν•˜κ²Œ λ™μž‘ν•˜λŠ”μ§€μ— λŒ€ν•΄ μ•Œμ•„λ³΄κ³ μž ChatGPTλ₯Ό μ΄μš©ν•΄ Goμ–Έμ–΄λ‘œ μœ μ‚¬ν•œ μ½”λ“œλ₯Ό μž‘μ„±ν•΄λ³΄μ•˜λ‹€.

package main

import (
	"fmt"
	"sync"
	"time"
)

func orderAndServe(startTime time.Time, ms time.Duration, tableNumber int, wg *sync.WaitGroup) {
	defer wg.Done() // 이 ν•¨μˆ˜κ°€ λλ‚˜λ©΄ WaitGroup의 μΉ΄μš΄ν„°λ₯Ό κ°μ†Œμ‹œν‚¨λ‹€.

	fmt.Printf("ν…Œμ΄λΈ” %d 도착 - %vms\n", tableNumber, time.Since(startTime).Milliseconds())
	fmt.Printf("ν…Œμ΄λΈ” %d μ£Όλ¬Έ λ°›μŒ - %vms\n", tableNumber, time.Since(startTime).Milliseconds())
	time.Sleep(ms)
	fmt.Printf("ν…Œμ΄λΈ” %d %vms ν›„ μŒμ‹ λ‚˜μ˜΄ - %vms\n", tableNumber, ms.Milliseconds(), time.Since(startTime).Milliseconds())
}

func main() {
	startTime := time.Now()
	var wg sync.WaitGroup

	// 10개의 ν…Œμ΄λΈ”μ— λŒ€ν•΄ λ³‘λ ¬λ‘œ 처리
	for i := 1; i <= 10; i++ {
		wg.Add(1) // WaitGroup의 μΉ΄μš΄ν„°λ₯Ό μ¦κ°€μ‹œν‚¨λ‹€.
		go orderAndServe(startTime, 1000*time.Millisecond, i, &wg)
	}

	wg.Wait() // λͺ¨λ“  고루틴이 μ™„λ£Œλ  λ•ŒκΉŒμ§€ κΈ°λ‹€λ¦°λ‹€.
}

μœ„ μ½”λ“œλŠ” goμ–Έμ–΄λ‘œ μž‘μ„±ν•œ μ½”λ“œλ‘œ, sync.WaitGroup을 μ΄μš©ν•΄ λ³‘λ ¬λ‘œ ν•¨μˆ˜λ₯Ό μ§„ν–‰μ‹œμΌ°λ‹€. μ•„λž˜λŠ” μœ„ μ½”λ“œλ₯Ό μ‹€ν–‰ν•œ κ²°κ³Όλ‹€.

goμ–Έμ–΄λ‘œ μž‘μ„±ν•œ μ½”λ“œ κ²°κ³Ό
goμ–Έμ–΄λ‘œ μž‘μ„±ν•œ μ½”λ“œ κ²°κ³Ό

κ²°κ³Όλ₯Ό 보면 ν…Œμ΄λΈ” 도착과 μ£Όλ¬Έλ°›μŒκΉŒμ§€ μ§„ν–‰ μˆœμ„œκ°€ ν…Œμ΄λΈ” 1λΆ€ν„° μ§„ν–‰ν•˜μ§€ μ•Šκ³  (첫번째 κ°€μ •μ˜ 독립적인 μŠ€λ ˆλ“œ), μ™„λ£Œν•œ μ‹œκ°„μ„ 보면 λ™μΌν•˜κ²Œ 처리된 것 (λ‘λ²ˆμ§Έ κ°€μ •μ˜ 같은 μ‹œκ°„μ— μ™„λ£Œ)을 λ³Ό 수 μžˆμ—ˆλ‹€. 이λ₯Ό 톡해 μ•žμ„  두가지 가정이 λ³‘λ ¬μ²˜λ¦¬κ°€ κ°€λŠ₯ν•œ μ–Έμ–΄μ˜ νŠΉμ§•μ„ μž˜λ³΄μ—¬μ£ΌλŠ” κ°€μ •μ΄μ—ˆκ³ , Promise.all()은 λ³‘λ ¬μ μœΌλ‘œ μ²˜λ¦¬ν•˜λŠ” 것이 μ•„λ‹ˆλΌ μžλ°”μŠ€ν¬λ¦½νŠΈμ˜ λ™μ‹œμ„±μ„ μ΄μš©ν•˜λŠ” λ©”μ†Œλ“œμž„μ„ ν•œλ²ˆ 더 확인할 수 μžˆμ—ˆλ‹€.

[μ°Έμ‘°]

@choi2021
맀일의 μ‹œν–‰μ°©μ˜€λ₯Ό κΈ°λ‘ν•˜λŠ” κ°œλ°œμΌμ§€μž…λ‹ˆλ‹€.