(번역) 자바스크립트의 await 사건의 지평선

sehyun hwang·2024년 1월 3일
42

FE 번역글

목록 보기
26/30
post-thumbnail

원문 : https://frontside.com/blog/2023-12-11-await-event-horizon/

모든 블랙홀 주변에는 중력을 벗어나기 위해 필요한 속도가 광속을 초과하는 경계가 있습니다. 빛을 포함한 무엇이든 그 경계를 넘어가면 불가사의한 블랙홀의 내부에 영원히 갇히게 됩니다. 탈출구는 없으며 나머지 우주로 돌아갈 방법도 없습니다. 이 경계를 블랙홀의 사건의 지평선이라고 부릅니다.

자바스크립트의 Promise 주위에는 이와 유사한 경계가 있습니다. 실행 흐름이 일단 이 경계를 넘어가면, 강제로 탈출하거나 원래 있던 곳으로 돌아올 방법이 없습니다. 저는 이 경계를 Promise의 await 사건의 지평선이라고 부르겠습니다.

async 함수는 Promise의 결괏값을 위해 멈출 때마다 await 사건의 지평선을 가로지르게 됩니다. 일단 한번 넘어가면 Promise가 처리되기 전까지 제어권은 절대 넘어오지 않습니다. 그리고 Promise가 처리된다고 보장할 수 있는 방법이 전혀 없습니다. 아마 이벤트 루프의 다음 tick에서 바로 처리되거나, 최악의 경우에는 절대 처리되지 않을 수도 있습니다. 만약 이 상황이 된다면 불쌍한 대기 함수는 영원히 무기력한 상태에 빠지게 됩니다. 마치 호박석 속의 곤충과 같이 말이죠.

이는 이론적이기보다는 중요하고 실질적인 문제입니다. 예를 들어, 일부 설정을 진행하고, 동작을 수행하며, 필요한 해체 작업을 하는 매우 일반적인 패턴을 구현한 async 함수를 생각해 보세요. 아래의 코드에서 async 함수는 lock을 얻고, 인자로 받은 함수를 기다린 다음, 작업이 완료되면 lock을 해제합니다.

async function protect(work) {
  let lock = await acquireLock();
  try {
    await work();
  } finally {
    release(lock);
  }
}

그런데 work()로부터 반환된 Promise가 절대 처리되지 않으면 어떻게 될까요? 정답은 protect()함수는 await 사건의 지평선을 넘게 되고, 절대 재개되지 않습니다. 그 결과, 처음 획득한 lock은 절대 해제되지 않으므로 우리는 lock이 "누수"되었다고 합니다.

자원 누수는 많은 사람들로부터 소프트웨어에서 가장 교활한 버그 유형으로 간주됩니다. 왜냐하면 추적이 어렵기도 하고 시스템이 무거운 작업으로 인해 스트레스받을 때까지 숨겨져 있는 경우가 많기 때문입니다.

물론 원칙적으로 충분히 기다린다면 대부분의 Promise가 처리되고, 대부분의 상황에서 Promise가 그럴 것이라는 행복한 시나리오를 따릅니다. 그러나 행복한 가정이 큰 범위로 적용되는 경우는 거의 없습니다. 사실 영원히 지속되는 Promise만이 문제가 아니라는 것을 우리는 꽤 빨리 깨닫게 됩니다.

커맨드 라인 인터페이스에서 protect()함수를 호출하고 싶다고 가정해 보겠습니다. 사용자가 CTRL-C를 입력하면 프로세스가 종료됩니다. 만약 작업이 총 10초가 걸리는 상황에서 사용자가 9.5초가 지난 시점에 CTRL-C를 입력했다면, 제어권은 await 사건의 지평선으로부터 절대 넘어오지 않게 되고 lock은 다시 한번 누수됩니다.

영원히 처리되지 않는 Promise에 대해 말하려는 게 아닙니다. 곧 처리될 수 있었지만 겨우 500ms 차이로 인해 누수가 발생했다는 것이 문제입니다.

명시적 자원 관리(Explicit Resource Management)가 해결책일까?

결론부터 말하자면, 아닙니다.

이 글을 쓰는 현재 3단계 TC39 제안인 명시적 자원 관리(Explicit Resource Management)를 사용하면 설정 및 해제 코드를 함께 묶을 수 있습니다. 이는 번거롭게 try/catch 블록을 사용하지 않아도 되고, 실수로 자원 누수가 발생할 가능성을 낮춥니다. 만약 앞서 가설에 사용된 protect() 함수가 명시적 자원 관리를 사용한다면, 아래와 같이 훨씬 깔끔하게 작성할 수 있습니다.

export async function protect(work) {
  using lock = await acquireLock();
  await work();
}

protect()가 종료되고 lock이 스코프에서 벗어날 때, 이미 내장된 할당 해제 로직에 의해 자동으로 해제됩니다.

간편한 개선점이긴 하지만, await 사건의 지평선의 근본적인 문제를 해결하지는 못합니다. 흐름 제어권이 통과한 이후에 work()가 처리되기 전까지 돌아오지 않습니다. 만약 작업이 너무 오래 걸린다면, protect() 함수 또한 오래 걸리게 되고, 결과적으로 lock 자원이 자동으로 해제되는 로직 역시 작동하지 않습니다. 다시 말해, 누수가 발생합니다.

AbortSignal은 도움이 될까?

이론상으로는 그렇지만, 실제로는 딱히 그렇지 않습니다. 중단 신호(abort signal)를 규율과 함께 적용했을 때 문제를 약간 교정할 수는 있지만, 해결하지는 못합니다. 이는 AbortSignal을 전달받았을 때 Promise를 "취소"할 수 있는 합의된 방법이 없다는 사실에서 비롯됩니다. 사실 이는 거의 약 10년 전 TC39를 두 손 두 발 다 들게 한 매우 까다로운 문제입니다. 만약 당신이 이 문제에 대한 답을 알고 있다고 생각한다면 충분히 생각해보지 않았을 가능성이 높습니다. 하지만 반드시 해야 한다면, 한 가지 방법은 safe() 함수로 모든 Promise를 감싸서 await 사건의 지평선으로부터 보호하는 것입니다.

export function safe(promise, signal) {
  return Promise.race([
    promise,
    new Promise((_,reject) => signal.addEventListener("abort", reject)),
  ])
}

promise가 처리되기 전에 signal이 발동하면, safe 함수가 즉시 거부되고 에러를 발생하며 호출자에게 제어권을 반환합니다. 이 메커니즘을 활용하여, 전체 계산에 걸쳐 중단 신호를 활용하도록 protect() 함수를 다시 작성할 수 있습니다.

export async function protect(work, signal) {
  let lock = await safe(acquireLock(), signal);

  try {
    await safe(work(signal), signal);
  } finally {
    release(lock);
  }
}

이제 이 함수는 safe()를 사용하여 모든 await 표현식을 보호 원으로 감싸서 사건의 지평선 너머로 실행이 갇히는 것을 방지합니다. 또한, signalwork()로 넘겨주어 이 함수와 이 함수가 호출하는 모든 함수가 동일한 작업을 수행할 수 있도록 합니다. 이제 signal이 발동되면, work()가 갇혔는지 여부에 상관없이 protect() 함수는 종료되고, lock은 해제될 것입니다. 하지만 이 접근법에는 여전히 심각하고 피할 수 없는 경고 사항이 있습니다. 바로 중단 신호는 제약이 아니라 희망이라는 것입니다.

추가적인 중단 신호가 사용하고 전달하기가 번거롭다는 것만이 문제가 아닙니다. 그렇습니다. 만약 work() 또는 이 함수가 호출하는 어떤 함수라도, 또는 그들이 호출하는 어떤 함수라도 signalsafe()를 사용하지 못한다면 호출 트리의 비동기 함수가 await 사건의 지평선 너머에 갇혀 결과적으로 자원이 누수되는 상황이 다시 발생하게 됩니다.

근본적으로 놓친 것은 추상화의 힘입니다. 즉, work()를 블랙 박스로 생각하고 내부적으로 어떻게 구성되었는지에 상관없이 가장 필요할 때마다 호출자에게 제어권을 넘겨줄 것이라는 확신이 느껴지는 자유입니다. 중단 신호를 사용하여 이를 달성하는 방법은 소스 코드 및 코드와 연관된 의존성을 모두 확인하면서 모두 중단 신호 규율이 적용되어있는지 확인하는 것입니다. 실제로는 누구도 그렇게 하지 않을 것이며, 중단 신호를 사용한 라이브러리가 부족하다는 사실이 이를 증명합니다. 규율을 적용하여 장애물을 일시적으로 해결할 수 있지만, 완전히 사라지게 할 수는 없습니다. 제어권이 await 사건의 지평선을 넘어가면 다시 가져올 수 없다는 사실은 변하지 않습니다.

구조적 병행성(Structured Concurrency)과 await 사건의 지평선

이 문제를 해결할 수 있는 방법은 여러 가지가 있지만, 대부분 중단 신호 방식의 변형에 그칩니다. 그러나 await 사건의 지평선은 그 메커니즘이 런타임의 핵심에 내장되어 있기 때문에 공리적으로 남아있습니다. 이를 안정적으로 관통하는 방법은 없습니다.

이에 대한 결과로 주어진 어떠한 async 함수의 최소 수명은 오직 가장 안쪽 Promise의 수명에 의해서 결정됩니다. 다시 말해, async 함수의 타고난 수명은 내부에서 외부로 결정됩니다.

work()가 처리되기 전까지 protect()는 이어 나갈 수 없습니다... 그게 언제든 말이죠. 즉, 언제 protect()를 이어 나갈지 결정하는 건 work()이고, 메인 함수의 타고난 수명을 결정하는 건 protect()입니다. 프로세스 출구를 넘어가는 어떤 것이든 누수될 수 있는 "위험 영역"에 속합니다. 이는 앞서 가설에 사용된 lock도 해당합니다.

필요한 것 이상으로 실행되는 코드는 누수의 위험이 있습니다

사실 이는 구조적 병행성 시스템에서 요구하는 것과 정확히 반대입니다. 즉, 함수의 최대 수명은 함수를 호출하는 함수의 수명에 의해 제약된다는 것입니다. 계산 결과와 관련이 없는 비동기 연산이 완료될 때까지 기다리는 대신에, 구조적 병행성 시스템은 더 이상 필요하지 않은 순간에 해당 함수에서 즉시 반환합니다. 앞선 커멘드 라인 인터페이스 예제에서 사용자가 ctrl-c를 누르는 순간 다른 모든 것들이 즉시 무의미해집니다.

이 상황에서 우리가 기대하는 것은 제어권이 강제로 protect() 함수로 반환되어, finally 블록이 실행되고 lock이 누수되지 않으며 프로세스가 정상적으로 종료되는 것입니다.

잘 동작하는 작업은 항상 반환됩니다

하지만 관련 없는 함수들을 강제로 종료하기 위해서는 제어권을 탑다운 방식으로 반환하도록 강제하는 식의 메커니즘이 반드시 필요합니다. 그러나 async 함수에서 제어권이 await 사건의 지평선을 넘어가면 다시 되돌릴 수 없다는 것을 방금 살펴보았습니다. async 함수를 기반으로 하는 프리미티브가 기껏해야 구조적 병행성을 기대할 수 있고, 보장할 수는 없는 이유가 바로 여기에 있습니다.

구조적 병행성과 자바스크립트

이 시점에 당신은 아마 자바스크립트의 구조적 병행성이 async 함수와 함께 달성할 수 없기 때문에 쓸모 없는 것이라고 생각할 수도 있습니다. 전혀 그렇지 않습니다! 구조적 병행성은 가능할 뿐만 아니라, 이미 Effection, Effect-TS, 그리고 StarFx와 같은 프로젝트들에서 사용되고 있습니다. 이러한 라이브러리들은 매우 다양하게 있지만, 이들이 공유하는 공통점은 바로 핵심 기술로 제너레이터 함수를 채택한 것입니다.

이는 자바스크립트의 제너레이터 함수가 await 사건의 지평선에 의해 제한되지 않기 때문입니다. 제너레이터 함수는 완전한 자격을 갖춘 구분된 연속(delimited continuation)을 나타내며, 가장 강력한 흐름 제어 프리미티브 중 하나입니다. 구분된 연속이 무엇인지 자세히 설명하지 않더라도, while 루프부터 try/catch 블록, 대수 이펙트 핸들러에 이르기까지 구현하려는 서로 다른 모든 제어 메커니즘을 표현하는 데 사용할 수 있다고 말하면 충분할 것 같습니다. 사실 async 함수는 본질적으로 Promise 처리라는 특정 영역에 국한된 제너레이터 함수의 축소 버전입니다.

우리의 사용 사례에서 중요한 것은 제너레이터 함수가 명시적으로 return()을 사용할 수 있다는 것인데, 이는 현재 실행 중인 위치에 상관없이 즉시 종료하라는 신호를 보냅니다. 하지만 그럼에도 finally {} 블록이나 명시적 자원 메서드와 같이 중요한 코드 경로를 따르게 됩니다.

아마 먼 미래에는 async 함수가 await 사건의 지평선을 탈출할 방법을 제공할 것입니다. 그러나 그전까지 async/await에 기반한 구조적 병행성 모델은 공상 과학 소설에 불과할 것입니다.

3개의 댓글

comment-user-thumbnail
2024년 1월 11일

좋은 글이네요!

답글 달기
comment-user-thumbnail
2024년 1월 12일

Exploring the Latest Fashion Trends for Men in 2024

The world of men’s fashion is always moving in new directions, and 2024 will be no exception. Staying one step ahead of the trends is crucial for everyone who wants to make a fashionable statement, as fashion evolves in tandem with the changing of the seasons. This blog will take an in-depth look at trends in men’s fashion for the year 2024. Minelooks.com has you covered with all of the insider information you require to remain fashion-forward in 2024, regardless of whether you are a fashion fanatic or simply looking to enhance your wardrobe.

For More Info Visit:- https://www.minelooks.com/mens-fashion-for-2024/

답글 달기
comment-user-thumbnail
2024년 2월 22일

All of our escort models are mature and know everything about sexual pleasure for which you’re waiting for a long time. Independent Escorts in have that much charm on their face to attract anyone towards their sizzling Sexy Escorts Girls.

https://app.roll20.net/users/12940430/saahil-g
https://coub.com/880a42eb7fc8a8843452
https://www.walkscore.com/people/251024744879/saahil-gupta
https://vocal.media/authors/saahil-gupta
https://custodes.ujaen.es/gitea/saahillguptaa
https://in.radiocut.fm/user/saahil/
https://www.credly.com/users/saahil-gupta.9902cca7/badges
https://newspicks.com/user/9863577
https://participa.rosario.gob.ar/profiles/saahillguptaa/activity?locale=en
https://app.roll20.net/users/12940430/saahil-g
https://www.walkscore.com/people/251024744879/saahil-gupta
https://vocal.media/authors/saahil-gupta
https://newspicks.com/user/9863577
https://www.credly.com/users/saahil-gupta.9902cca7/badges
http://dayworkonyachts.com/author/saahillguptaa/
https://thewriterscommunity.in/freelancer/saahil-gupta/
https://codefor.fr/profiles/saahillguptaa/activity
https://socialsocial.social/user/saahillguptaa/
https://www.akva.sk/burza/?author=17180
https://www.decidimmataro.cat/profiles/saahillguptaa/activity
https://www.servinord.com/phpBB2/profile.php?mode=viewprofile&u=574023
https://www.jumpinsport.com/users/saahillguptaa
http://newdigital-world.com/members/saahillguptaa.html
https://myforexforums.com/member.php/37452-saahillguptaa
https://decidim.sciencescitoyennes.ovh/profiles/saahillguptaa/activity

답글 달기