Node.js 코드에서 장애를 만드는 흔한 실수 중 하나는 리소스 정리를 잊는 것입니다. 파일 핸들을 열어 두고 닫지 않거나, 타이머를 만들고 해제하지 않거나, 연결을 사용한 뒤 반환하지 않으면 처음에는 조용해 보여도 운영에서는 누수와 지연으로 이어지기 쉽습니다.

이럴 때 눈여겨볼 기능이 using, Disposable, AsyncDisposable입니다. 핵심은 리소스를 쓰는 범위를 코드로 명확하게 묶고, 블록을 벗어날 때 자동으로 정리되게 만드는 것입니다.

Node.js using이 왜 중요한가

H3. try-finally는 강력하지만 반복되면 빠뜨리기 쉽다

지금도 많은 코드는 try-finally로 정리 작업을 보장합니다. 이 방식 자체는 맞지만, 리소스가 여러 개로 늘어나면 정리 순서와 누락 위험이 함께 커집니다.

const handle = await openFile(path);

try {
  await processFile(handle);
} finally {
  await handle.close();
}

이 정도는 괜찮지만 아래 같은 상황에서는 복잡도가 빠르게 올라갑니다.

  • 파일 핸들 여러 개를 동시에 열 때
  • 타이머, 이벤트 리스너, DB 연결을 함께 관리할 때
  • 함수 중간에 반환 경로가 많을 때
  • 예외를 래핑하거나 재시도 로직이 섞일 때

이때 using은 “이 리소스는 이 범위가 끝나면 정리된다”는 의도를 더 직접적으로 드러냅니다.

H3. 정리 책임을 코드 구조에 묶으면 유지보수가 쉬워진다

리소스 정리는 주석이나 팀 규칙보다 언어 수준 구조로 강제하는 편이 안전합니다. using은 선언 시점에 정리 책임을 붙일 수 있어서, 코드 리뷰에서도 누락 여부를 더 빨리 잡아낼 수 있습니다.

Node.js using 기본 사용법

H3. 동기 정리는 Symbol.dispose로 연결한다

기본 구조는 Symbol.dispose를 구현한 객체를 using으로 선언하는 방식입니다.

class TempTimer {
  constructor(fn, ms) {
    this.id = setTimeout(fn, ms);
  }

  [Symbol.dispose]() {
    clearTimeout(this.id);
  }
}

{
  using timer = new TempTimer(() => {
    console.log('run');
  }, 1000);

  console.log('work in progress');
}

이 코드에서 블록을 벗어나면 timerSymbol.dispose가 호출됩니다. 즉 타이머 해제를 사람이 직접 기억하지 않아도 됩니다.

H3. 비동기 정리는 Symbol.asyncDispose와 await using을 쓴다

파일 닫기, 연결 반환, flush 같은 작업은 비동기 정리가 필요할 수 있습니다. 이럴 때는 Symbol.asyncDisposeawait using을 사용합니다.

class FileSession {
  constructor(fileHandle) {
    this.fileHandle = fileHandle;
  }

  async read() {
    return this.fileHandle.readFile({ encoding: 'utf8' });
  }

  async [Symbol.asyncDispose]() {
    await this.fileHandle.close();
  }
}

async function loadConfig(path) {
  const fileHandle = await open(path);

  await using session = new FileSession(fileHandle);
  return session.read();
}

이 패턴의 장점은 분명합니다.

  • 함수가 중간에 실패해도 정리 누락을 줄일 수 있음
  • 파일, 소켓, 연결 같은 자원을 범위 단위로 관리하기 쉬움
  • 정리 로직이 객체 정의에 모여 있어 재사용이 쉬움
  • 리뷰 시 “닫아야 하는 자원”이 눈에 더 잘 들어옴

파일 핸들을 다루는 기본 흐름은 Node.js FileHandle.readLines 가이드: 큰 로그 파일을 메모리 부담 없이 처리하는 법과 함께 보면 더 실무적으로 연결됩니다.

어떤 상황에서 특히 유용한가

H3. 임시 리소스를 짧은 범위에서 쓰고 바로 정리할 때 좋다

using은 오래 살아야 하는 공유 리소스보다, 짧은 범위에서 만들고 바로 폐기해야 하는 자원에 특히 잘 맞습니다.

예를 들면 이런 경우입니다.

  • 요청 처리 중 잠깐 여는 파일 핸들
  • 특정 작업 동안만 유지할 AbortController 연계 자원
  • 한 번성 타이머와 이벤트 리스너
  • 테스트 안에서만 필요한 임시 객체

특히 타임아웃과 취소 신호를 함께 다루는 코드는 Node.js AbortSignal.any 가이드: timeout과 사용자 취소를 함께 처리하는 법와 같이 보면 설계가 더 깔끔해집니다.

H3. 테스트 코드에서 정리 누락을 줄이는 데 효과적이다

테스트에서는 리스너, mock 서버, 임시 디렉터리 정리를 놓치면 다음 테스트까지 오염되기 쉽습니다. using을 쓰면 테스트 블록이 끝날 때 정리된다는 점이 명확해져서 flaky test를 줄이는 데 도움이 됩니다.

class TestServer {
  constructor(server) {
    this.server = server;
  }

  [Symbol.asyncDispose]() {
    return new Promise((resolve, reject) => {
      this.server.close((error) => {
        if (error) reject(error);
        else resolve();
      });
    });
  }
}

테스트 환경 자체를 다듬는 관점에서는 Node.js test runner mock timers 가이드: 시간을 고정해 flaky 테스트를 줄이는 법도 함께 참고할 만합니다.

사용할 때 주의할 점

H3. Node.js 버전과 런타임 지원 여부를 먼저 확인해야 한다

using 계열 문법은 비교적 최신 기능이라서, 팀의 Node.js 버전과 빌드 체인이 이를 지원하는지 먼저 확인해야 합니다. 로컬에서는 되는데 CI나 배포 환경에서 파싱이 실패하면 오히려 장애를 만들 수 있습니다.

실무에서는 아래 순서가 안전합니다.

  1. 현재 프로덕션 Node.js 버전 확인
  2. 트랜스파일러나 린터가 문법을 처리하는지 확인
  3. 작은 범위의 유틸 코드부터 도입
  4. 운영 로그에서 정리 누락 감소 효과 확인

H3. 모든 자원을 using으로 바꾸는 것이 정답은 아니다

using은 강력하지만, 애플리케이션 전체에서 오래 유지해야 하는 풀(pool)이나 싱글턴 자원까지 무조건 감싸는 것은 맞지 않을 수 있습니다. 예를 들어 프로세스 생애주기 전체에서 유지하는 DB pool은 요청 함수 스코프에서 자동 정리하면 오히려 잘못된 종료를 만들 수 있습니다.

즉 아래처럼 구분하는 편이 실용적입니다.

  • 짧은 범위의 임시 자원: using 고려
  • 요청 단위 후처리 자원: await using 고려
  • 프로세스 전역 공유 자원: 별도 생명주기 관리

실무 적용 체크리스트

H3. 도입할 때는 리소스 정리 누락 지점부터 찾는다

팀 코드베이스에 적용할 때는 아래 순서로 접근하면 무리하지 않고 도입할 수 있습니다.

  1. try-finally가 반복되는 지점을 찾기
  2. 파일 핸들, 타이머, 이벤트 리스너처럼 범위형 자원을 분류하기
  3. 재사용할 수 있는 Disposable 또는 AsyncDisposable 래퍼를 만들기
  4. 테스트와 배치 작업처럼 누락 비용이 큰 곳부터 적용하기
  5. 정리 실패 시 로그가 남도록 관측성을 붙이기

정리 실패가 실제로 어떤 문맥에서 발생했는지 추적하려면 Node.js AsyncLocalStorage.snapshot 가이드: 콜백 경계에서도 요청 컨텍스트를 안전하게 넘기는 법처럼 요청 문맥 보존 패턴과 연결해 보는 것도 좋습니다.

마무리

Node.js의 using, Disposable, AsyncDisposable은 단순히 문법이 새로 생긴 정도가 아니라, 정리 책임을 사람이 기억하는 방식에서 코드 구조로 옮기는 도구에 가깝습니다.

특히 운영에서 자주 문제를 만드는 파일 핸들, 타이머, 이벤트 리스너 같은 자원을 다룰 때 효과가 큽니다. 리소스 누수나 정리 누락이 반복된다면, try-finally를 더 조심하자는 다짐보다 범위 기반 정리 구조를 도입하는 쪽이 더 안정적입니다.

함께 보면 좋은 글