한국어

`using`과 `await using`으로 자바스크립트의 새로운 명시적 리소스 관리를 마스터하세요. 정리 자동화, 리소스 누수 방지, 그리고 더 깔끔하고 견고한 코드 작성법을 배워보세요.

자바스크립트의 새로운 막강한 기능: 명시적 리소스 관리 심층 분석

소프트웨어 개발의 역동적인 세계에서, 리소스를 효과적으로 관리하는 것은 견고하고 신뢰할 수 있으며 성능 좋은 애플리케이션을 구축하는 데 있어 초석입니다. 수십 년 동안 자바스크립트 개발자들은 파일 핸들, 네트워크 연결, 데이터베이스 세션과 같은 중요한 리소스가 올바르게 해제되도록 보장하기 위해 try...catch...finally와 같은 수동 패턴에 의존해 왔습니다. 이 방식은 기능적으로는 문제가 없지만, 종종 장황하고 오류가 발생하기 쉬우며 복잡한 시나리오에서는 때때로 "파멸의 피라미드(pyramid of doom)"라고 불리는 패턴처럼 다루기 힘들어질 수 있습니다.

이제 언어에 패러다임 전환이 찾아왔습니다: 명시적 리소스 관리(ERM)입니다. ECMAScript 2024 (ES2024) 표준으로 확정된 이 강력한 기능은 C#, Python, Java와 같은 언어의 유사한 구조에서 영감을 받아, 리소스 정리를 처리하는 선언적이고 자동화된 방법을 도입합니다. 새로운 usingawait using 키워드를 활용하여, 자바스크립트는 이제 오래된 프로그래밍 과제에 대해 훨씬 더 우아하고 안전한 해결책을 제공합니다.

이 종합 가이드는 자바스크립트의 명시적 리소스 관리의 세계로 여러분을 안내할 것입니다. 이 기능이 해결하는 문제들을 탐구하고, 핵심 개념을 분석하며, 실제 예제를 통해 배우고, 여러분이 세계 어디에서 개발하든 더 깔끔하고 회복력 있는 코드를 작성할 수 있도록 힘을 실어줄 고급 패턴들을 발견할 것입니다.

과거의 방식: 수동 리소스 정리의 문제점

새로운 시스템의 우아함을 제대로 이해하기 전에, 우리는 먼저 기존 방식의 문제점을 이해해야 합니다. 자바스크립트에서 리소스 관리를 위한 고전적인 패턴은 try...finally 블록입니다.

논리는 간단합니다: try 블록에서 리소스를 획득하고, finally 블록에서 해제합니다. finally 블록은 try 블록의 코드가 성공하든, 실패하든, 또는 조기 반환하든 관계없이 실행을 보장합니다.

일반적인 서버 측 시나리오를 고려해 봅시다: 파일을 열고, 일부 데이터를 쓴 다음, 파일이 반드시 닫히도록 하는 것입니다.

예제: try...finally를 사용한 간단한 파일 작업


const fs = require('fs/promises');

async function processFile(filePath, data) {
  let fileHandle;
  try {
    console.log('파일 여는 중...');
    fileHandle = await fs.open(filePath, 'w');
    console.log('파일에 쓰는 중...');
    await fileHandle.write(data);
    console.log('데이터 쓰기 완료.');
  } catch (error) {
    console.error('파일 처리 중 오류 발생:', error);
  } finally {
    if (fileHandle) {
      console.log('파일 닫는 중...');
      await fileHandle.close();
    }
  }
}

이 코드는 작동하지만, 몇 가지 약점을 드러냅니다:

이제 데이터베이스 연결과 파일 핸들과 같은 여러 리소스를 관리한다고 상상해 보십시오. 코드는 금방 중첩된 엉망진창이 됩니다:


async function logQueryResultToFile(query, filePath) {
  let dbConnection;
  try {
    dbConnection = await getDbConnection();
    const result = await dbConnection.query(query);

    let fileHandle;
    try {
      fileHandle = await fs.open(filePath, 'w');
      await fileHandle.write(JSON.stringify(result));
    } finally {
      if (fileHandle) {
        await fileHandle.close();
      }
    }
  } finally {
    if (dbConnection) {
      await dbConnection.release();
    }
  }
}

이러한 중첩은 유지하고 확장하기 어렵습니다. 이는 더 나은 추상화가 필요하다는 명백한 신호입니다. 이것이 바로 명시적 리소스 관리가 해결하기 위해 설계된 문제입니다.

패러다임 전환: 명시적 리소스 관리의 원칙

명시적 리소스 관리(ERM)는 리소스 객체와 자바스크립트 런타임 간의 계약을 도입합니다. 핵심 아이디어는 간단합니다: 객체는 어떻게 정리되어야 하는지를 선언할 수 있고, 언어는 객체가 범위를 벗어날 때 해당 정리를 자동으로 수행하는 구문을 제공합니다.

이는 두 가지 주요 구성 요소를 통해 달성됩니다:

  1. 폐기 가능 프로토콜(The Disposable Protocol): 객체가 특별한 심볼을 사용하여 자체 정리 로직을 정의하는 표준 방법입니다: 동기적 정리를 위한 Symbol.dispose와 비동기적 정리를 위한 Symbol.asyncDispose.
  2. usingawait using 선언: 리소스를 블록 범위에 바인딩하는 새로운 키워드입니다. 블록을 벗어날 때 리소스의 정리 메서드가 자동으로 호출됩니다.

핵심 개념: `Symbol.dispose`와 `Symbol.asyncDispose`

ERM의 핵심에는 두 개의 새로운 잘 알려진 심볼이 있습니다. 이 심볼 중 하나를 키로 갖는 메서드를 가진 객체는 "폐기 가능한 리소스(disposable resource)"로 간주됩니다.

`Symbol.dispose`를 사용한 동기적 폐기

Symbol.dispose 심볼은 동기적 정리 메서드를 지정합니다. 이는 파일 핸들을 동기적으로 닫거나 메모리 내 잠금을 해제하는 것과 같이 정리에 비동기 작업이 필요하지 않은 리소스에 적합합니다.

스스로를 정리하는 임시 파일에 대한 래퍼(wrapper)를 만들어 보겠습니다.


const fs = require('fs');
const path = require('path');

class TempFile {
  constructor(content) {
    this.path = path.join(__dirname, `temp_${Date.now()}.txt`);
    fs.writeFileSync(this.path, content);
    console.log(`임시 파일 생성됨: ${this.path}`);
  }

  // 이것이 동기적 폐기 가능 메서드입니다
  [Symbol.dispose]() {
    console.log(`임시 파일 폐기 중: ${this.path}`);
    try {
      fs.unlinkSync(this.path);
      console.log('파일이 성공적으로 삭제되었습니다.');
    } catch (error) {
      console.error(`파일 삭제 실패: ${this.path}`, error);
      // dispose 내부에서도 오류를 처리하는 것이 중요합니다!
    }
  }
}

`TempFile`의 모든 인스턴스는 이제 폐기 가능한 리소스입니다. 디스크에서 파일을 삭제하는 로직을 포함하는 `Symbol.dispose` 키의 메서드를 가지고 있습니다.

`Symbol.asyncDispose`를 사용한 비동기적 폐기

많은 현대적인 정리 작업은 비동기적입니다. 데이터베이스 연결을 닫는 것은 네트워크를 통해 `QUIT` 명령을 보내는 것을 포함할 수 있고, 메시지 큐 클라이언트는 나가는 버퍼를 비워야 할 수도 있습니다. 이러한 시나리오를 위해 우리는 `Symbol.asyncDispose`를 사용합니다.

`Symbol.asyncDispose`와 연관된 메서드는 반드시 `Promise`를 반환해야 합니다 (또는 `async` 함수여야 합니다).

비동기적으로 풀(pool)에 다시 반환되어야 하는 모의 데이터베이스 연결을 모델링해 봅시다.


// 모의 데이터베이스 풀
const mockDbPool = {
  getConnection: () => {
    console.log('DB 연결 획득.');
    return new MockDbConnection();
  }
};

class MockDbConnection {
  query(sql) {
    console.log(`쿼리 실행 중: ${sql}`);
    return Promise.resolve({ success: true, rows: [] });
  }

  // 이것이 비동기적 폐기 가능 메서드입니다
  async [Symbol.asyncDispose]() {
    console.log('DB 연결을 풀로 반환 중...');
    // 연결 해제를 위한 네트워크 지연 시뮬레이션
    await new Promise(resolve => setTimeout(resolve, 50));
    console.log('DB 연결 반환됨.');
  }
}

이제 모든 `MockDbConnection` 인스턴스는 비동기 폐기 가능 리소스입니다. 더 이상 필요하지 않을 때 비동기적으로 자신을 해제하는 방법을 알고 있습니다.

새로운 구문: `using`과 `await using`의 실제 사용

폐기 가능한 클래스가 정의되었으므로, 이제 새로운 키워드를 사용하여 자동으로 관리할 수 있습니다. 이 키워드들은 `let`이나 `const`처럼 블록 범위의 선언을 만듭니다.

`using`을 사용한 동기적 정리

using 키워드는 `Symbol.dispose`를 구현하는 리소스에 사용됩니다. 코드 실행이 `using` 선언이 이루어진 블록을 떠날 때, `[Symbol.dispose]()` 메서드가 자동으로 호출됩니다.

우리의 `TempFile` 클래스를 사용해 봅시다:


function processDataWithTempFile() {
  console.log('블록 진입...');
  using tempFile = new TempFile('이것은 중요한 데이터입니다.');

  // 여기서 tempFile로 작업할 수 있습니다
  const content = fs.readFileSync(tempFile.path, 'utf8');
  console.log(`임시 파일에서 읽음: "${content}"`);

  // 여기에는 정리 코드가 필요 없습니다!
  console.log('...추가 작업 수행 중...');
} // <-- 바로 여기서 tempFile.[Symbol.dispose]()가 자동으로 호출됩니다!

processDataWithTempFile();
console.log('블록을 벗어났습니다.');

출력은 다음과 같을 것입니다:

블록 진입...
임시 파일 생성됨: /path/to/temp_1678886400000.txt
임시 파일에서 읽음: "이것은 중요한 데이터입니다."
...추가 작업 수행 중...
임시 파일 폐기 중: /path/to/temp_1678886400000.txt
파일이 성공적으로 삭제되었습니다.
블록을 벗어났습니다.

얼마나 깔끔한지 보십시오! 리소스의 전체 생명주기가 블록 내에 포함됩니다. 우리는 그것을 선언하고, 사용하고, 잊어버립니다. 언어가 정리를 처리합니다. 이것은 가독성과 안전성 면에서 엄청난 개선입니다.

여러 리소스 관리하기

같은 블록에 여러 개의 `using` 선언을 가질 수 있습니다. 그것들은 생성된 역순으로 폐기됩니다 (LIFO 또는 "스택과 같은" 동작).


{
  using resourceA = new MyDisposable('A'); // 먼저 생성됨
  using resourceB = new MyDisposable('B'); // 두 번째로 생성됨
  console.log('블록 내부, 리소스 사용 중...');
} // resourceB가 먼저 폐기되고, 그 다음 resourceA가 폐기됨

`await using`을 사용한 비동기적 정리

await using 키워드는 `using`의 비동기적 대응물입니다. `Symbol.asyncDispose`를 구현하는 리소스에 사용됩니다. 정리가 비동기적이므로, 이 키워드는 `async` 함수 내부나 모듈의 최상위 수준에서만 사용할 수 있습니다 (최상위 await가 지원되는 경우).

우리의 `MockDbConnection` 클래스를 사용해 봅시다:


async function performDatabaseOperation() {
  console.log('async 함수 진입...');
  await using db = mockDbPool.getConnection();

  await db.query('SELECT * FROM users');

  console.log('데이터베이스 작업 완료.');
} // <-- 여기서 await db.[Symbol.asyncDispose]()가 자동으로 호출됩니다!

(async () => {
  await performDatabaseOperation();
  console.log('async 함수가 완료되었습니다.');
})();

출력은 비동기적 정리를 보여줍니다:

async 함수 진입...
DB 연결 획득.
쿼리 실행 중: SELECT * FROM users
데이터베이스 작업 완료.
DB 연결을 풀로 반환 중...
(50ms 대기)
DB 연결 반환됨.
async 함수가 완료되었습니다.

`using`과 마찬가지로, `await using` 구문은 전체 생명주기를 처리하지만, 비동기 정리 프로세스를 정확하게 `await`합니다. 심지어 동기적으로만 폐기 가능한 리소스도 처리할 수 있습니다—단순히 그것들을 await하지 않을 뿐입니다.

고급 패턴: `DisposableStack`과 `AsyncDisposableStack`

때로는 `using`의 간단한 블록 범위 지정이 충분히 유연하지 않을 수 있습니다. 단일 어휘 블록에 묶이지 않는 생명주기를 가진 리소스 그룹을 관리해야 한다면 어떨까요? 또는 `Symbol.dispose`를 가진 객체를 생성하지 않는 오래된 라이브러리와 통합해야 한다면요?

이러한 시나리오를 위해 자바스크립트는 두 개의 헬퍼 클래스를 제공합니다: `DisposableStack`과 `AsyncDisposableStack`입니다.

`DisposableStack`: 유연한 정리 관리자

`DisposableStack`은 정리 작업 모음을 관리하는 객체입니다. 그 자체로 폐기 가능한 리소스이므로, `using` 블록으로 전체 생명주기를 관리할 수 있습니다.

몇 가지 유용한 메서드가 있습니다:

예제: 조건부 리소스 관리

특정 조건이 충족될 때만 로그 파일을 여는 함수를 상상해 보십시오. 하지만 모든 정리는 끝에 한 곳에서 일어나기를 원합니다.


function processWithConditionalLogging(shouldLog) {
  using stack = new DisposableStack();

  const db = stack.use(getDbConnection()); // 항상 DB를 사용

  if (shouldLog) {
    const logFileStream = fs.createWriteStream('app.log');
    // 스트림에 대한 정리를 지연시킴
    stack.defer(() => {
      console.log('로그 파일 스트림 닫는 중...');
      logFileStream.end();
    });
    db.logTo(logFileStream);
  }

  db.doWork();

} // <-- 스택이 폐기되면서 등록된 모든 정리 함수가 LIFO 순서로 호출됩니다.

`AsyncDisposableStack`: 비동기 세계를 위하여

짐작하셨겠지만, `AsyncDisposableStack`은 비동기 버전입니다. 동기 및 비동기 폐기 가능 리소스를 모두 관리할 수 있습니다. 주요 정리 메서드는 `.disposeAsync()`이며, 모든 비동기 정리 작업이 완료되면 해결되는 `Promise`를 반환합니다.

예제: 혼합된 리소스 관리

데이터베이스 연결(비동기 정리)과 임시 파일(동기 정리)이 필요한 웹 서버 요청 핸들러를 만들어 봅시다.


async function handleRequest() {
  await using stack = new AsyncDisposableStack();

  // 비동기 폐기 가능 리소스 관리
  const dbConnection = await stack.use(getAsyncDbConnection());

  // 동기 폐기 가능 리소스 관리
  const tempFile = stack.use(new TempFile('request data'));

  // 오래된 API의 리소스를 채택
  const legacyResource = getLegacyResource();
  stack.adopt(legacyResource, () => legacyResource.shutdown());

  console.log('요청 처리 중...');
  await doWork(dbConnection, tempFile.path);

} // <-- stack.disposeAsync()가 호출됩니다. 비동기 정리를 정확하게 await합니다.

`AsyncDisposableStack`은 복잡한 설정 및 해체 로직을 깔끔하고 예측 가능한 방식으로 조율하는 강력한 도구입니다.

`SuppressedError`를 사용한 견고한 오류 처리

ERM의 가장 미묘하지만 중요한 개선점 중 하나는 오류를 처리하는 방식입니다. 만약 `using` 블록 내부에서 오류가 발생하고, 그 후 자동 폐기 중에 *또 다른* 오류가 발생하면 어떻게 될까요?

과거의 `try...finally` 세계에서는 `finally` 블록의 오류가 일반적으로 `try` 블록의 원래 더 중요한 오류를 덮어쓰거나 "억제"했습니다. 이로 인해 디버깅이 매우 어려워지는 경우가 많았습니다.

ERM은 새로운 전역 오류 유형인 `SuppressedError`로 이 문제를 해결합니다. 다른 오류가 이미 전파 중일 때 폐기 중에 오류가 발생하면, 폐기 오류는 "억제"됩니다. 원래 오류가 발생하지만, 이제는 폐기 오류를 포함하는 `suppressed` 속성을 갖게 됩니다.


class FaultyResource {
  [Symbol.dispose]() {
    throw new Error('폐기 중 오류 발생!');
  }
}

try {
  using resource = new FaultyResource();
  throw new Error('작업 중 오류 발생!');
} catch (e) {
  console.log(`잡힌 오류: ${e.message}`); // 작업 중 오류 발생!
  if (e.suppressed) {
    console.log(`억제된 오류: ${e.suppressed.message}`); // 폐기 중 오류 발생!
    console.log(e instanceof SuppressedError); // false
    console.log(e.suppressed instanceof Error); // true
  }
}

이 동작은 원래 실패의 컨텍스트를 절대 잃지 않도록 보장하여, 훨씬 더 견고하고 디버깅하기 쉬운 시스템으로 이어집니다.

자바스크립트 생태계 전반의 실제 사용 사례

명시적 리소스 관리의 적용 분야는 방대하며, 백엔드, 프론트엔드, 또는 테스트 환경에서 작업하는 전 세계 개발자들에게 관련이 있습니다.

브라우저 및 런타임 지원

최신 기능으로서, 명시적 리소스 관리를 어디서 사용할 수 있는지 아는 것이 중요합니다. 2023년 말 / 2024년 초 기준으로, 주요 자바스크립트 환경의 최신 버전에서 광범위하게 지원됩니다:

오래된 환경의 경우, `using` 구문을 변환하고 필요한 심볼 및 스택 클래스를 폴리필(polyfill)하기 위해 Babel과 같은 트랜스파일러와 적절한 플러그인에 의존해야 합니다.

결론: 안전과 명확성의 새로운 시대

자바스크립트의 명시적 리소스 관리는 단순한 문법적 설탕(syntactic sugar) 그 이상입니다; 이것은 안전성, 명확성, 유지보수성을 증진시키는 언어에 대한 근본적인 개선입니다. 지루하고 오류가 발생하기 쉬운 리소스 정리 과정을 자동화함으로써, 개발자들이 핵심 비즈니스 로직에 집중할 수 있도록 해줍니다.

핵심 요약은 다음과 같습니다:

새로운 프로젝트를 시작하거나 기존 코드를 리팩토링할 때, 이 강력한 새 패턴을 채택하는 것을 고려해 보십시오. 이는 여러분의 자바스크립트를 더 깔끔하게, 애플리케이션을 더 신뢰성 있게, 그리고 개발자로서의 삶을 조금 더 쉽게 만들어 줄 것입니다. 이것은 현대적이고 전문적인 자바스크립트를 작성하기 위한 진정한 글로벌 표준입니다.