JavaScript의 using을 사용해 보자 - Part 1

김준기 · 원프레딕트 프론트엔드 리드
September 20, 2023

글 목록

  1. JavaScript의 using을 사용해 보자 - Part 1
  2. JavaScript의 using을 사용해 보자 - Part 2

Intro

퇴근하기 직전, “JOKER” 님과 “부엉이” 님이 이야기하는 것을 옆에서 얼핏 듣게 되었습니다. 두 분이 이야기한 내용들은 추상 클래스로 다형성을 처리하는 방법으로 시작해서 팩토리 패턴을 스프링에서 어떻게 다루고 있는가를 건너 건너가 트랜잭션을 관리하는 방식의 차이로 까지 여정을 마무리하고 있었습니다.(긴 여정이었어… 더 깊은 내용이 궁금합니다.)

DB 트랜잭션을 다룰 때, Spring 에서는 @Transaction어노테이션을 통해 매직처럼 관리가 되고 있는데, python 에서는 connection을 직접 관리해야 되고 with라는 키워드를 써야 한다는 이야기를 듣게 됩니다.

Thinking Face
흠...어디서 들어봤는데?

JavaScript에서도 요새 비슷한 걸 추진하는 것 같았는데, using이라는 키워드였습니다. 글 쓰기 주제로 갑자기 생각이 들었습니다. 이걸로 문서를 쓰자!

"using" keyword 어디로 부터 왔는가?

2018년 7월 24일 TC39주1에서 해당 기능에 대한 첫 미팅을 진행하고, stage 1 승인이 이루어집니다. 해당 문서를 읽어보면 이 기능을 “왜” 넣었는지 아주 잘 설명해 주고 있습니다. 문서는 위에서부터 읽는 것이 국룰이지만 흥미를 돋구기 위해 잠깐은 건너가 해당 선행 기술의 컨셉부터 한 번 보면 좋을 것 같아요.

선행 기술

  • C#: using statement, declaration
  • Java: try-with-resources statement
  • Python: with statement

각각의 항목을 찾아보면, 다음과 같습니다.

C#

using 키워드의 주요 용도는 두 가지입니다. using 문은 개체가 삭제되는 끝의 범위를 정의합니다. using 지시문은 네임스페이스의 별칭을 만들거나 네임스페이스에 정의된 형식을 가져옵니다. 주2

Java

The try-with-resources statement is a try statement that declares one or more resources. A resource is an object that must be closed after the program is finished with it. The try-with-resources statement ensures that each resource is closed at the end of the statement. Any object that implements java. 주3

Python

The with statement is used to wrap the execution of a block with methods defined by a context manager (see section With Statement Context Managers). This allows common try…except…finally usage patterns to be encapsulated for convenient reuse. 주4

사용법 및 정도의 차이는 있지만, 대략 공통의 이야기들은 블록 내에서 정의한 resource를 관리하는 statement를 제공해 주고, 해당 블록이 끝날 때, resource가 닫히도록 보장하며, 명시적으로 작성하는 기능을 포함하고 있습니다. 그렇다면 JavaScript 스펙으로 진행 중인 using keyword도 비슷하지 않을까? 맞습니다. 다른 언어들이 가지고 있는 기능을 토대로 현재 제안을 진행 중입니다.

어디에서 쓸까? 왜 필요하지?

예를 들어 보겠습니다. 우리는 지금 DB에 접근하여, Posts collection을 가져오는 함수를 하나 작성한다고 해보죠.

async function getPosts() {
  // db 에 접근을 해보자
  const db = await connectToDatabase()

  // posts 를 가져와야지..?
  let collection = await db.collection("posts")
  let posts = await collection.find({}).limit(50).toArray()

  // 다 가져온 것 같아 이제 내보내자
  return posts
}

위 함수는 이상하게 느껴질 것입니다. 한 번 곰곰이 읽어보며 문제를 찾아봅시다. 문제는 많아 보이겠지만, 하나를 찾아보자면 우선 connect 된 db를 close 하는 부분이 없습니다.

async function getPosts() {

...
// close 를 해줘볼까? Profit!
db.close();

return posts;
}

문제가 해결 되었을까요?

만약 posts를 찾아오는 과정에서 문제가 생기면 어떻게 될까요?

close 까지 도달하지 못한다면?

폭탄쾅

이런 문제들을 방지하기 위해서 JavaScript에서는 이런 resource를 어떻게 관리 했을까요? 실제로, resource에 대한 관리가 필요한 경우 JavaScript에서는 try..catch..finally를 많이 사용하고는 했습니다.

다음은 try..catch..finally를 적용해 보겠습니다.

async function getPosts() {
  let db

  try {
    db = await connectToDatabase()
    let collection = await db.collection("posts")
    let posts = await collection.find({}).limit(50).toArray()

    return posts
  } catch (error) {
    console.error(error)
  } finally {
    await db.close()
  }
}

문제는 해결된 것처럼 보입니다. 물론 db를 연결하고, 해제하는 영역에 대해서 저러한 보일러 플레이트가 생기는 것을 제외하고는 말이죠. 유사하게 이렇게 connect가 열리는 부분에서는 지속적인 고민이 생기기 마련입니다. 파일 스트림을 받는다든지, 또는 resource를 공유하는 worker 들 사이들에서도 해당 고민은 발생할 수 있습니다. 혹시 이러한 resource 타입에 따라서 관리하는 방법들이 제각각이라면 하나의 방법으로 명시적인 관리를 하면 어떨까요? 여기에서 이 명세의 동기가 시작됩니다.

위에서는 사용을 위한 예시를 보았습니다. 흥미가 생겼으면 좋겠네요. 그 궁금함을 갖고 조금은 딱딱한 공식 문서를 들여다보겠습니다. TC-39에 제시된 문서의 제목과 동기를 읽어보면 다음과 같습니다.

제목: ECMAScript 명시적 리소스 관리

동기

리소스 관리에 대한 일관적이지 않은 패턴

  • ECMAScript Iterators: iterator.return()
  • WHATWG Stream Readers: reader.releaseLock()
  • NodeJS FileHandles: handle.close()
  • Emscripten C++ objects handles: Module._free(ptr) obj.delete() Module.destroy(obj)

resource를 관리할 때, footguns주5 피하기

const reader = stream.getReader();
...
reader.releaseLock(); // try/finally가 있어야 함.

resource의 범위를 잡을 때

const handle = ...;
try {
    ... // handle을 쓰는 데 적당합니다.
}
finally {
    handle.close();
}
// handle을 쓰는 데 적당하지 않습니다만 아직도 스코프 내에 있어요.

여러 개의 resource를 관리 할 때, footguns 피하기

const a = ...;
const b = ...;
try {
    ...
}
finally {
    a.close(); // 만약 `b.close()` 가 a 에 의존하고 있다면, 문제가 있습니다.
    b.close(); // 만약 `a.close()` 가 발생하면, `b` 에 영원히 도달하지 않습니다.
}

여러 리소스를 올바르게 관리할 때 긴 코드 방지


// sync disposal
{ // 블록은 바깥 스코프에서 `a` 나 `b` 의 누수를 방지합니다.
    const a = ...;
    try {
        const b = ...;
        try {
            ...
        }
        finally {
            b.close(); // `b`의 경우, `a` 이전에 `b`가 닫혀있는지 확인하세요.
                       // `a` 에 의존합니다.
        }
    }
    finally {
        a.close(); // `b.close()` 가 발생하더래도 `a` 가 닫혀 있는지 확인 하세요.
    }
}
// `a` 와 `b` 모두 스코프를 벗어났습니다.

비교

// `a` 또는 `b` 가 외부 스코프로 유출되는 것을 방지합니다.
// `b` 가 `a`에 종속되는 경우 `b` 가 `a` 보다 먼저 배치 되도록 보장합니다.
// 'b' 가 처리 되더라도, 'a' 가 처리되도록 보장합니다.
using a = ..., b = ...;
...

// async sync disposal
{ // 블록은 바깥 스코프에서 `a` 나 `b` 의 누수를 방지합니다.
    const a = ...;
    try {
        const b = ...;
        try {
            ...
        }
        finally {
            await b.close(); // `b`의 경우 이전에 'b' 이전에 'a' 가 닫혔는지 확인합니다.
                             // `a`에 의존합니다.
        }
    }
    finally {
        await a.close(); // `b.close()` 가 발생하더라도 'a' 가 닫혀있는지 확인합니다.
    }
}
// `a` 와 `b` 모두 스코프를 벗어났습니다.

비교

// `a` 또는 `b` 가 외부 스코프로 유출되는 것을 방지합니다.
// `b` 가 `a`에 종속되는 경우 `b` 가 `a` 보다 먼저 배치 되도록 보장합니다.
// 'b' 가 처리 되더라도, 'a' 가 처리되도록 보장합니다.
await using a = ..., b = ...;
...

Non-blocking memory/IO 어플리케이션에서

import { ReaderWriterLock } from "...";
const lock = new ReaderWriterLock();

export async function readData() {
  // 뛰어난 작가를 기다리며, read lock 을 걸어둡니다.
  using lockHandle = await lock.read();
  ... // 많은 수의 reader 들
  await ...;
  ... // `await` 후에도 여전히 read lock 상태입니다.
} // read lock을 해제

export async function writeData(data) {
  // 모든 reader 들을 기다리고 write lock 을 걸어둡니다.
  using lockHandle = await lock.write();
  ... // 단 한명의 writer
  await ...;
  ... // `await` 후에도 여전히 write lock 상태입니다.
} // write lock을 해제

Fixed Layout Objects와 shared struct를 함께 사용해야 될 때주6

// main.js
shared struct class SharedData {
  ready = false;
  processed = false;
}

const worker = new Worker('worker.js');
const m = new Atomics.Mutex();
const cv = new Atomics.ConditionVariable();
const data = new SharedData();
worker.postMessage({ m, cv, data });

// worker로 data 보내기
{
  // 'm'의 잠금을 main이 얻을 때까지 기다리기
  using lck = m.lock();

  // worker를 위해 데이터에 표기
  data.ready = true;
  console.log("main is ready");

} // 'm' 잠금 해제

// 잠재적인 worker에게 알리기
cv.notifyOne();

{
  // 'm'의 잠금을 다시 획득
  using lck = m.lock();

  // 'm'의 잠금을 풀고, worker가 처리를 끝낼 때까지 기다립니다.
  cv.wait(m, () => data.processed);

} // 'm' 잠금 해제
// worker.js
onmessage = function (e) {
  const { m, cv, data } = e.data

  {
    // 'm'의 잠금을 main 이 얻을 때까지 기다리기
    using lck = m.lock()

    // 'm'의 잠금을 풀고 main() 에 데이터를 보낼 때 까지 기다립니다.
    cv.wait(m, () => data.ready)

    // 기다린 후에, 우리는 'm'의 자물쇠를 다시 한 번 얻었습니다.
    console.log("worker thread is processing data")

    // main으로 다시 데이터를 보내기
    data.processed = true
    console.log("worker thread is done")
  } // 'm' 잠금 해제
}

위의 예시를 해소하기 위해서 도입을 추진하게 됩니다.

원문이 궁금하신 분들을 위하여, 각주로 링크를 첨부합니다.주7

그래서…? 좋은 건 알겠어. 당장 쓰러 가자!

당장 하자

다만 문제가 조금 있습니다. 아쉽게도 해당 기능은 아직은 TC-39 stage 3 라는 것. 아직 정식으로 Javascript 기능에 포함되지 않았습니다. 하지만, 당장 써볼 수 있는 곳이 있습니다.

타입스크립트 5.2 using

아직 나온 지 한 달(23년 9월 기준) 정도 된 따뜻한 타입스크립트 기능입니다.

5.2에 해당 기능을 서포트하는 기능이 추가되었습니다.

Outro

내용을 다 읽으신 분, 또는 글을 밑에서부터 읽는 것을 좋아하시는 분에게 선물을 드리자면 사실 1부작 같은 2부작 이었습니다.

그렇습니다. 시리즈 입니다.

마참내

다음 문서에서는 타입스크립트를 활용해서 실제로 명시적인 코드들을 어떻게 쓰는지 알아보는 시간을 갖도록 하겠습니다.

그럼.

안녕
다음에 또 만나요


  1. tc39 프로세스라고도 불립니다. 사이트
  2. 마이크로소프트 C# 원문에서 발췌
  3. 오라클 원문에서 발췌
  4. 파이선 원문에서 발췌
  5. 자기 발등 찍기 참고 글1, 참고 글2
  6. 아직은 stage1 spec에 머물고 있습니다 Proposal Structs
  7. ECMAScript Explicit Resource Management Proposal
원프레딕트는 더 나은 제품을 고민하며 기술적인 문제를 함께 풀어낼 동료를 찾고 있습니다.
자세한 내용은 채용 사이트를 참고해 주세요.