BlogApplicationsGuestbook

Github | X (Twitter)

Copyright © 2024 OPNay - All Reserved | Privacy Policy

Typescript의 새로운 키워드 “using”

2023.06.30 07:11

x

타입스크립트는 현재 5.1 입니다. 그리고 준비중인 다음 버전인 5.2에서 using 이라는 새로운 키워드가 지원될 예정입니다. 이 키워드는 tc39에 제안된 내용이고, 현재 Stage 3 단계로 다음 ECMAScript에서도 볼 수 있는 후보 상태입니다.

새로운 키워드는 기존 let, const 변수 선언 키워드에 기능이 추가된 형태이며, 이 키워드를 이용해 선언한 변수는 블록 스코프를 벗어날 때 자동으로 자원을 해제해 주는 새로운 기능입니다.

try { const reader = stream.getReader(); const { value, done } = reader.read(); } finally { reader.releaseLock(); // 자원 해제 }
typescript

일반적인 자원 해제 코드 패턴

위 예제처럼 스트림, 버퍼 등 다양한 자원의 수명관리와 관련된 소프트웨어 개발 패턴이 존재하고, 다음과 같이 각 상황에 맞게 자원을 해제하는 메서드 호출이 필요합니다.

  • ECMAScript의 Iterator: iterator.return()
  • WHATWG의 Stream Reader: reader.releaseLock()
  • NodeJS의 파일 핸들러: handle.close()
  • Emscripten C++의 객체 핸들: Module._free(ptr), obj.delete(), Module.destroy(obj)
    또한 throw 를 대응하기 위해 try { ... } finally { ...release }로 오류를 검사하는 것이 일반적입니다. 이는 코드가 길어지고, 반복적인 패턴이고, 개발자가 자원을 해제하지 못할 경우 메모리 누수로도 이어지는 좋지 않은 패턴이기도 합니다.
try { const resource = getResource(); try { const reader = resource.getReader(); // ... } finally { reader.close(); } } finally { resource.release(); }
typescript

여러개의 자원을 다루는 코드

또한 자원은 새로운 자원으로 파생이 될 수 있으므로, 많은 자원을 다루게 된다면 위 예제 보다 더 코드는 깊어지고, 복잡하고, 이해하기 어려워집니다. 이러한 패턴을 단순화 하고, “자원의 해제는 변수가 존재하는 블록 스코프를 벗어날 때 만들어진 자원의 역순으로 자원을 해제”하기로 정한 것이 using 이라는 키워드입니다.

{ using resource = getResource(); using reader = resource.getReader(); const { value, done } = reader.read(); } // 블록을 벗어날 때 'reader' => 'resource' 순으로 자원 해제
typescript

using 키워드 예제

자원 해제 메서드 Symbol.dispose

using 키워드를 사용하기 위해 자원 해제를 시키는 메서드가 필요합니다. 따라서 할당되는 자원에는 Symbol.dispose 라는 알려진 심볼을 사용해 자원을 해제하는 함수를 사용하게 됩니다.

const getResource = () => ({ [Symbol.dispose]: () => { console.log("Release!"); } }); { console.log("I'm using resource"); using resource = getResource(); // ... something do } // Run `resource[Symbol.dispose]?.()` console.log("Finished!"); // Output: // I'm using resource // Release! // Finished!
typescript

Symbol.dispose 를 설명하기 위한 예제

자원을 생성하는데 Symbol.dispose 라는 이름의 속성에 자원 해제 함수를 넣었습니다. 이렇게 넣은 채로 using 키워드를 사용하게 되면, 자원이 있는 블록을 벗어날때 자동으로 resource[Symbol.dispose]?.() 를 실행하게 됩니다. 이로써 자원은 블록의 기능이 동작하는 동안에만 할당되며, 자동으로 해제되는 방식을 갖게됩니다.

try { console.log("I'm using resource"); const resource = getResource(); // ... something do } finally { resource[Symbol.dispose]?.(); } console.log("Finished!");
typescript

try { ... } finally { ... } 로 바꾼 예제

비동기 자원 해제 await using

자원 해제 함수가 비동기 함수일 때 Symbol.asyncDispose 라는 알려진 심볼을 사용합니다. 이 심볼은 일반적인 using 키워드에서는 작동하지 않으며, await using 키워드에서만 동작하게 됩니다. 반대로, Symbol.dispose 는 두 키워드 모두 지원합니다.

async 가 아닌 await 이 붙은 이유는 위에서 설명드린 바와 같이 “자원 해제의 순서는 보장” 되어야 합니다. 비동기 자원 해제 또한 이 순서를 보장해야 하기 때문에 실제 비동기 함수는 기다려야 하는 상황이기 때문입니다.

const res = { [Symbol.dispose]() {} }; const asyncRes = { [Symbol.asyncDispose]() {} }; using x = res; // ok: `res` has @@dispose using y = asyncRes; // throws: `asyncRes` does not have @@dispose await using x = res; // ok: `res` has @@dispose (fallback) await using y = asyncres; // ok: `asyncRes` has @@asyncDispose // `await using`의 예상 자원 해제 코드 const disposable = [x, y]; for (const _disposable of disposable.toReversed()) { await _disposable[Symbol.dispose]?.(); }
typescript

비동기 using 키워드

for .. of 에서 사용할 수 있습니다.

이 키워드는 const, let 과 같은 변수 선언 키워드 입니다. 이러한 특성 때문에 기존 변수 선언 키워드를 사용할 수 있던 곳에서 사용이 가능합니다. 그 중 같은 await 키워드를 사용하는 사용처인 for .. of에서의 작동 방식 표입니다.

DisposableStack 컨테이너 객체

using 키워드와 함께 제안된 DisposableStack 과 비동기 버전인 AsyncDisposableStack 이 추가되었습니다. 이 객체들은 컨테이너에서 일회용 자원들이 해제될 수 있도록 보장해주는 객체입니다. 다음은 DisposableStack의 타입입니다.

class DisposableStack { constructor(); /** * Gets a value indicating whether the stack has been disposed. * @returns {boolean} */ get disposed(); /** * Alias for `[Symbol.dispose]()`. */ dispose(); /** * Adds a resource to the top of the stack. Has no effect if provided `null` or `undefined`. * @template {Disposable | null | undefined} T * @param {T} value - A `Disposable` object, `null`, or `undefined`. * @returns {T} The provided value. */ use(value); /** * Adds a non-disposable resource and a disposal callback to the top of the stack. * @template T * @param {T} value - A resource to be disposed. * @param {(value: T) => void} onDispose - A callback invoked to dispose the provided value. * @returns {T} The provided value. */ adopt(value, onDispose); /** * Adds a disposal callback to the top of the stack. * @param {() => void} onDispose - A callback to evaluate when this object is disposed. * @returns {void} */ defer(onDispose); /** * Moves all resources currently in this stack into a new `DisposableStack`. * @returns {DisposableStack} The new `DisposableStack`. */ move(); /** * Disposes of resources within this object. * @returns {void} */ [Symbol.dispose](); [Symbol.toStringTag]; }
typescript

DisposableStack 구현 타입

// sync const stack = new DisposableStack(); const resource1 = stack.use(getResource1()); const resource2 = stack.use(getResource2()); const resource3 = stack.use(getResource3()); stack[Symbol.dispose](); // disposes of resource3, then resource2, then resource1
typescript

DisposableStack 사용방법

DisposableStack.adopt를 이용해 자원 해제 함수를 연결할 수 있습니다.

{ [Symbol.dispose]: () => { .. } } 가 구현이 안되어 있어도, DisposableStack.adopt를 이용해 using 을 이용한 자원 해제를 구현할 수 있습니다. 또한 비동기 자원해제는 AsyncDisposableStack을 이용해야 합니다.

{ using stack = new DisposableStack(); const reader = stack.adopt(createReader(), reader => reader.releaseLock()); ... }
typescript

DisposableStack.adopt() 를 이용한 자원 해제 구현

클래스 상속 관계의 생성자

자원 객체는 클래스에서도 사용이 가능합니다. 부모 클래스에서 자원 해제가 필요하고 자식 클래스에서 또한 자원 해제를 구현해야 할 때는 DisposableStack을 이용해 구현할 수 있습니다. 아래 예제는 제안서에 있던 예제를 재작성했습니다.

class ParentClass { #disposed = false; #disposables; #socketio; constructor() { // DisposableStack 객체를 생성해 생성자에서 나갈때 자원이 해제되도록 합니다. // 현재 생성자가 성공하면 자원이 남아 있어야 하므로, `stack.move()`를 후에 사용합니다. using stack = new DisposableStack(); // socket.io 객체를 생성해 등록합니다. // 만약 `Symbol.dispose`가 구현된 객체라면 `stack.use(createResource())`를 사용하면 됩니다. this.#socketio = stack.adopt(io('wss://localhost:3000', socket => socket.destory()); // 여기까지 도달하면, 오류없이 자원을 생성했으므로 `stack`에서 `disposable`로 옮길 수 있습니다. this.#disposables = stack.move(); // 생성자에서 실패를 하면, `stack`은 여기에 도달해서 자원을 해제하게됩니다. // 이벤트 핸들러가 제거되면서 `#socketio`는 GC됩니다. } [Symbol.dispose]() { if (!this.#disposed) { this.#disposed = true; const disposables = this.#disposables; // 아래 `disposables[Symbol.dispose]()`로 자원을 해제하기 때문에, // 이 과정은 필수는 아니지만, 사용하지 않는 객체를 지워두는게 좋습니다. this.#socketio = undefined; this.#disposables = undefined; // Dispose all resources in `disposables` // `DisposableStack`에 등록된 자원들을 해제합니다. disposables[Symbol.dispose](); } } }
typescript

부모 클래스의 DisposableStack 구현 예제

// 자식 클래스 class SubClass extends ParentClass { #disposables; #logger; constructor() { super(); // 자식 클래스의 생성자를 덮을 `DisposableStack`을 생성합니다. using stack = new DisposableStack(); // 자원 해제의 마지막에 부모 클래스인 `super`의 자원 해제를 실행하도록합니다. stack.defer(() => super[Symbol.dispose]()); // `Symbol.dispose` 메서드가 구현된 자원인 Logger를 만들어 등록합니다. this.#logger = stack.use(createLogger()); // 아래 `stack.move()`가 실행되기 전에 실패하면, stack의 자원은 `using` 키워드의 규칙대로 해제됩니다. // 부모 클래스가 등록되어 있으므로, 부모 클래스의 자원 또한 같이 해제됩니다. // stack의 자원을 유지시킵니다. this.#disposables = stack.move(); } [Symbol.dispose]() { this.#logger = undefined; // `#logger`의 자원이 해제됩니다. // 또한 위 생성자 주석에서 설명했듯이, 부모 클래스를 등록할 때 `stack.defer`를 사용했으므로, // `#logger` => super 순으로 자원이 해제됩니다. this.#disposables[Symbol.dispose](); } }
typescript

자식 클래스의 DisposableStack 구현 예제

타입스크립트에서 사용하는 방법

현재 5.2의 개발은 많이 이뤄졌습니다. 그에따라 Beta 버전이 릴리즈 됐고, 마이크로소프트 공식 블로그에 버전에 대한 릴리즈 포스팅이 이뤄졌습니다. 그러면서 사용되는 방법이 나오게 되었는데 tsconfig.json 에서 compilerOptions.lib에 esnext.disposable 또는 esnext를 추가하면 사용이 가능합니다.

 

마무리

자원해제는 자바스크립트 뿐만 아니라 C++, 파이썬, 자바 등 다양한 언어에서 다뤄지는 프로그래밍 패턴입니다. 이를 정규화하기 위해 만들어진 이번 제안 내용은 C++에서 구현된 using 키워드, 파이썬에서 구현된 ExitStack 에서 영감받아 만들어진 DisposableStack 모두 기존 자바스크립트에서 볼 수 없던 내용이고, 다른 언어에서 시행착오를 겪은 내용들이기 때문에 잘만 정착된다면, Node.js 뿐만 아니라 브라우저의 그래픽, 오디오 등 다양한 자원이 안전하게 관리되는 환경이 구성될 수 있을거라 보여집니다.

저는 이와 같이 메모리 관리를 좀더 쉽게 다룰 수 있는 내용을 굉장히 좋아합니다. 앞으로도 이러한 내용이 더 추가 되었으면 합니다.

 

Typescript의 새로운 키워드 “using”

    image

    Announcing TypeScript 5.2 Beta - TypeScript

    Today we are excited to announce the availability of TypeScript 5.2 Beta. To get started using the beta, you can get it through NuGet, or through npm with the following command: npm install -D typescript@beta Here’s a quick list of what’s new in TypeScript 5.2! using Declarations and Explicit Resource Management Decorator Metadata Named and […]

    Announcing TypeScript 5.2 Beta - TypeScripthttps://devblogs.microsoft.com/typescript/announcing-typescript-5-2-beta/#using-declarations-and-explicit-resource-management

    Announcing TypeScript 5.2 Beta - TypeScript

    GitHub - tc39/proposal-explicit-resource-management: ECMAScript Explicit Resource Management

    ECMAScript Explicit Resource Management. Contribute to tc39/proposal-explicit-resource-management development by creating an account on GitHub.

    GitHub - tc39/proposal-explicit-resource-management: ECMAScript Explicit Resource Managementhttps://github.com/tc39/proposal-explicit-resource-management

    GitHub - tc39/proposal-explicit-resource-management: ECMAScript Explicit Resource Management