JavaScript의 새로운 슈퍼파워: 명시적 리소스 관리
(v8.dev)- Explicit Resource Management 제안은 파일 핸들, 네트워크 연결 등 리소스의 라이프사이클을 명확하게 제어하는 새로운 방법
- Chromium 134와 V8 v13.8부터 해당 기능을 이용할 수 있음
- 언어에 추가 되는 부분들
-
using
및await using
선언과Symbol.dispose
,Symbol.asyncDispose
심볼 도입으로 자동 정리 메커니즘을 제공 -
DisposableStack
,AsyncDisposableStack
은 여러 리소스를 안전하게 그룹화하고 해제 -
SuppressedError
는 정리 중 발생한 오류와 기존 오류를 함께 관리함
-
- 이 방법은 코드 안전성과 유지보수성을 크게 올려주며, 리소스 누수 방지에 효과적임
- 기존 try...finally 패턴을 단순화하고, 대규모 복합 리소스 환경에서 신뢰성 높은 자원 처리가 가능해짐
명시적 리소스 관리 제안 개요
- Explicit Resource Management 제안은 파일 핸들, 네트워크 연결 등의 리소스를 명확하게 생성‧해제할 수 있는 새로운 방법을 도입함
- 주요 구성요소는 다음과 같음
-
using
및await using
선언: 스코프 종료 시 자동으로 리소스 해제 -
[Symbol.dispose]()
,[Symbol.asyncDispose]()
심볼: 해제(cleanup) 동작 구현을 위한 메서드 - 글로벌 객체 DisposableStack, AsyncDisposableStack: 여러 리소스를 그룹화해 효율적으로 관리
-
SuppressedError
: 리소스 정리 중 발생한 에러와 기존 에러 모두를 포함하는 신규 에러 타입
-
- 이 기능들은 개발자가 세밀하게 리소스를 관리하고, 코드의 성능과 안전성을 향상시키는 데 초점을 맞춤
using
과 await using
선언
-
using
선언은 동기 리소스에,await using
선언은 비동기 리소스에 사용함 - 선언된 리소스는 스코프를 벗어날 때 자동으로 Symbol.dispose 또는 Symbol.asyncDispose 가 호출됨
- 이를 통해 동기/비동기 리소스 누수 문제를 줄이고, 일관된 해제 코드를 작성할 수 있음
- 이 키워드는 코드 블록, for 루프, 함수 바디 내에서만 사용할 수 있고, 최상위 레벨에서는 사용할 수 없음
- 예시
- 예를 들어,
ReadableStreamDefaultReader
를 사용할 때,reader.releaseLock()
을 반드시 호출해야 스트림을 재활용할 수 있음 - 오류 발생 시, 이 호출이 누락되면 스트림이 영구적으로 잠기는 문제가 발생함
- 예를 들어,
- 전통적인 방식
- 개발자는 try...finally 블록을 사용하여 리더의 잠금 해제를 보장함
- finally 블록에
reader.releaseLock()
코드 작성이 필요함
-
개선된 방식:
using
도입- 해제 동작을 포함하는 디스포저블 객체(readerResource)를 생성
-
using readerResource = {...}
패턴을 사용하면 코드 블록 탈출 즉시 자동으로 해제됨 - 향후 웹 API에서
[Symbol.dispose]
및[Symbol.asyncDispose]
지원 시, 별도 래퍼 객체 작성 없이 자동 관리 가능성이 있음
DisposableStack과 AsyncDisposableStack
- 여러 리소스를 효율적이고 안전하게 그룹화하도록
DisposableStack
과AsyncDisposableStack
이 도입됨 - 각 스택에 리소스를 추가하고, 스택 자체를 해제할 때 내부 모든 리소스를 역순으로 해제함
- 의존 관계가 있는 복잡한 리소스 집합을 다룰 때 위험을 줄이고 코드를 단순화함
-
주요 메서드
-
use(value)
: 스택 맨 위에 디스포저블 리소스를 추가함 -
adopt(value, onDispose)
: 비디스포저블 리소스에 해제 콜백을 묶어 추가함 -
defer(onDispose)
: 리소스 없이 해제 동작만 추가함 -
move()
: 현재 스택의 모든 리소스를 새 스택으로 이동시켜 소유권 이전 가능함 -
dispose()
,asyncDispose()
: 스택 내 리소스 전체를 해제함
-
지원 현황 및 활용 가능 시점
- Chromium 134, V8 v13.8 이상에서 명시적 리소스 관리 기능 이용 가능함
- 향후 다양한 웹 API와 호환 확대 기대감이 있음
Hacker News 의견
-
이 제안은 "함수의 색깔" 문제와 비슷한 느낌 전달. 동기 함수와 비동기 함수의 구분이 모든 기능에 계속 침범. 예를 들어 Symbol.dispose와 Symbol.asyncDispose, DisposableStack과 AsyncDisposableStack 사례 확인 가능. Java가 가상 스레드(virtual threads)로 간 이유에 만족. JVM에 복잡함을 추가함으로써 응용프로그램 개발자와 라이브러리 작성자, 디버거의 부담을 줄여주는 선택이라고 생각
-
비동기를 숨기면 코드 흐름을 이해하기 더 어려워진다는 점에서 동의하지 않음. 자원이 비동기적으로 해제되는지, 네트워크 문제 등 외부에 영향을 받을 수 있는지도 알고 싶음
-
요즘 대부분의 언어에서 "모든 코드를 비동기로 작성하는 게 상식"이라는 현상 정말 짜증. Purescript는 Eff(동기 효과)나 Aff(비동기 효과)로 코드를 작성하고 호출 시점에 선택 가능한 유일한 사례로 봄. 구조화된 동시성(Structured concurrency)은 멋지지만, 실제로는 구조화된 동시성을 얻기 위한 문법적 작업이라기보다는 서버에서 여러 최상위 요청 핸들러를 가지기 위한 작업에 가까움. 병렬 처리를 쉽게 하기 위한 수단에 불과
-
JVM에서 어떻게 구현됐는진 모르겠지만, 일반적으로 멀티스레딩은 정말 직관적으로 다루기 어려운 기술. 온갖 경합조건, 데드락, 라이브락, 기아, 메모리 가시성 문제 등 다룬 책 많음. 이와 비교하면 단일 스레드 비동기 프로그래밍이 훨씬 부담 적음. 함수 색깔 문제를 감수하는 게 멀티스레드 앱에서 "Heisenbug" 디버깅하는 것보다 덜 고통스러운 선택
-
Java가 그 선택을 내려서 정말 기쁘다는 감정
-
정상 실행과 비동기 함수가 서로 닫힌 카테시안 범주(closed Cartesian categories)를 형성하기 때문이라는 설명. 정상 실행 범주는 비동기 범주에 직접 임베드될 수 있음. 모든 함수는 카테고리(즉, 함수의 색깔)를 가지며, 어떤 언어는 이를 더 노골적으로 드러냄. 이는 언어 설계 선택이고, 카테고리 이론은 스레딩을 넘어서 강력하게 활용 가능. Java와 쓰레드 기반 접근법은 동기화 문제에 직면하게 되는데, 이게 특히 어렵다는 특징. JavaScript는 모나딕 카테고리 중 특히 Continuation-passing 방식에 제한을 둠
-
-
defer 함수를 이용한 using 사용 예제를 봤을 때 매우 신선하게 느껴짐. 다른 많은 사람에겐 이미 직관적일 수 있겠지만, 언급할 가치가 있다고 생각
-
using
제안에 포함된 DisposableStack과 AsyncDisposableStack 활용하면 콜백 등록을 내장 지원.using
이 블록 스코프이기 때문에 스코프 가로지르기나 조건부 등록에 필요. 하지만using
변수는const
와 유사하게 바로 초기화되어야 해서 조건부 초기화가 불가능. 이럴 땐 함수 최상위에 Stack을 만들고 사용하는 자원을 defer로 스택에 올리는 패턴이 필요. 필요한 경우에만 해제시점을 함수 레벨로 쉽게 조정 가능 -
golang과 비슷한 느낌
-
-
정말 좋은 아이디어라고 생각하지만,<p>웹 API 스트림 등에서 [Symbol.dispose]와 [Symbol.asyncDispose] 통합이 미래에 가능할 수 있다고 해도, 가까운 미래엔 일부 API와 라이브러리만 기능을 지원하고 나머지(대부분)는 지원하지 않을 상황. 결국 "using"과 try/catch를 섞거나, 아예 모든 코드에 try/catch를 써서 이해하기 더 쉬운 코드를 선택하는 딜레마. 이로 인해 이 기능이 "실용적으로 쓸 수 없다"는 평판을 얻게 될 위험성 존재. 결국 실제 문제를 해결하는 좋은 설계임에도 도입이 어려울 수 있다는 점에서 아쉬움
-
이런 기능을 지원하지 않는 API에는 DisposableStack을 사용해
using
을 적용 가능. 여러 자원을 함께 다룰 때도 try/catch보다 훨씬 단순해지는 장점. 런타임만 지원하면 기존 리소스의 업데이트를 기다릴 필요 없이 바로 쓸 수 있다 -
JavaScript에서는 이런 상황이 15년간 반복. 새로운 언어 기능은 Babel 같은 컴파일러에 먼저 도입되고, 그 다음 스펙에 들어가고, 마지막으로 안정적 API와 브라우저에 적용되기까지 3-4년 걸리는 경우 많음. 개발자들은 어차피 작은 래퍼(wrapper)로 웹 API 감싸는 데 익숙하고, 폴리필보단 래퍼가 더 나은 경우 많음. 유용한 새로운 언어 기능이 생겨도 "사용하기 어렵겠다"고 생각한 적 한 번도 없음
-
실제로 많은 기능이 이미 폴리필로 구현돼 있어, NodeJS 생태계 대부분이 이 패턴을 사용하며, 사용자는 트랜스파일러로 문법만 맞춰 쓰기도 함. 작년에 관련 발표를 준비하면서 NodeJS나 주요 라이브러리에 이미 Symbol.dispose 지원 API가 꽤 많다는 걸 발견. 프론트엔드에선 생명주기 관리 시스템이 있어서 덜 쓰일 듯하지만, 일부 상황에선 여전히 유용. 테스트 라이브러리나 백엔드에서는 충분히 퍼질 거라 생각
-
TC39는 Rust의 trait/protocol 같은 근본적인 언어 기능에도 집중할 필요. Rust에선 새 trait 정의와 구현이 비교적 쉬운 반면, 동적 언어이자 고유 심볼이 있는 JS라면 훨씬 간단하게 도입 가능. Orphan rule 같은 단점이 있지만, 훨씬 유연한 구조로 발전 가능
-
JavaScript 세계에서는 보통 폴리필로 해결하는 방식
-
-
C#이 떠오름. IDisposable과 IAsyncDisposable 통해 락 관리, 큐, 임시 스코프 관리 등 추상화에 매우 유용
-
제안 작성자가 Microsoft 출신이라 문법이 C#과 유사하게 정해졌음. 관련된 깃허브 이슈에서도 일관된 맥락
-
기본적으로 C#에서 차용된 디자인. 원래 제안이 Python의 context manager, Java의 try-with-resources, C#의 using statement 등도 참조하고 있음. using 키워드와 dispose 훅 메서드는 상당한 힌트
-
-
JavaScript가 하위 호환성 유지가 중요하다는 건 이해하지만,
[Symbol.dispose]()
문법이 어색하게 느껴짐. 배열에 메서드 핸들이 있는 것처럼 혼동. 이 문법이 무엇인지, 더 알아보고 싶다는 궁금증-
객체 리터럴에서 좌변에 대괄호로 감싼 동적 키(dynamically computed property)가 ES6 이후 10년 가까이 쓰였다는 설명. 또한 심볼은 문자열로 참조할 수 없기 때문에 동적 키와 메서드 단축 문법을 조합. 근본적으로 새 문법은 아니라는 생각
-
든든한 자료와 함께, 이는 기존 객체에 심볼 키를 할당하는 방식에서 유래. 자연스러운 흐름
-
다른 사용자들이 이미 무엇인지 설명했지만, 왜 그런지에 대한 설명은 없었던 듯. 메서드 이름에 Symbol을 쓰면 기존 메서드와 충돌 없이 새로운 API임을 보장. 실수로 클래스가 disposable로 잘못 처리되는 걸 막아주는 효과
-
다이나믹 프로퍼티 액세스(dynamic property access) 개념 언급. 객체 프로퍼티는 점(.) 혹은 대괄호([])로 접근 가능한 점, 문자열과 심볼 모두 지원. 심볼은 고유 객체로 비교하며, "[Symbol.dispose]"같은 well known symbol(자주 쓰이는 특수 심볼)로 확장성 보장. Python의 dunder 메서드와 유사한 개념도 설명
-
이 문법은 벌써 여러 해 사용. JavaScript의 iterator도 같은 방식이고, 거의 10년 전에 도입
-
-
자원 관리, 특히 렉시컬 스코프가 특징일 때 JS에 구조화된 동시성을 도입하기 위해 노력해 온 이유 소개. 관련 구조화 동시성 라이브러리도 공유
-
Bun 1.0.23 버전 이상에서 이미 해당 기능 지원. 실험적으로 써볼 수 있음
-
이렇게 복잡한 코드 스타일로 어떻게 프로그램의 실행 흐름을 이해하고 제어할 수 있는지 도저히 모르겠다는 의문
-
그게 바로 핵심. 웹 개발의 90%는 쓸모없거나 아무도 원하지 않는 업그레이드이고, 그 결과 생긴 문제를 또 10%의 시간으로 고치는 것이 현실. 낮은 확률로 예전에 작성한 코드를 누가 봐야 하는 상황이 오는데, 여기서 버그를 신입의 입문 과제로 남기는 아이디어 추천. 심지어 20년 된 레거시 시스템도 여전히 사용되는 현실
-
예시로 제시된 코드는 심각한 문법 오류가 많아 실제 JS와 거리가 멎음. 그리고 JS 개발자들이 이런 식으로 혼합 사용(while, promise chain, finally 등)하지 않으며, await 또는 적절한 예외처리 구조를 사용하는 게 일반적. 잘 설계된 라이브러리에서는 여러 단계의 핸들러를 겹겹이 붙이지 않고, DisposableStack을 활용해 더 간결하게 코딩 가능. 요즘에는 즉시실행 async 함수조차 필요 없는 경우 많음
-
해당 언어로 프로로 일하면서 그 언어 키워드 의미와 동작에 익숙해지면 코드를 자연스럽게 이해. Haskell 프로그래머도 비슷하게 익숙해짐
-
HN에서 코드 임베드할 때는 각 줄에 2칸 이상 들여쓰기 필요. (코드 이해가 어렵다는 점에는 동의)
-
들여쓰기가 도움이 된다는 간결한 조언
-
-
왜 익명(anonymous) 클래스 소멸자(destructor)로 가지 않았는지, 또는 Symbol 외 구조를 쓰지 않았는지 궁금. 두 Symbol(동기/비동기)이 존재하면 추상화가 새기(누출)되는 문제 제기
-
소멸자는 예측 가능한(cleanup이 명확한) 동작이 필요한데, 발전된 GC(garbage collector)는 이런 패턴과 맞지 않음. 현대 언어는 scope(범위) 기반 정리(cleanup)를 지원하고, HoF(고차 함수), 특별한 훅, 콜백 등록 등 다양한 방식으로 구현. Python은 초기엔 소멸자 기반(refcount GC)이었지만 한계 때문에 context manager가 도입됨
-
다른 언어의 소멸자는 GC 타이밍에 따라 동작하므로 신뢰하기 어려움. 반면 dispose 메서드는 변수 스코프가 끝날 때 명확히 호출되므로, 파일 닫기나 락 해제 등에 예측 가능. Symbol 기반 메서드는 기존 기능들과의 충돌을 피하고, 보통은 라이브러리 개발자가 신경쓰면 됨. 동기/비동기 구분이 명확해야 하고, await using a = await b()처럼 약간 낯선 구문이 필요할 수 있음
-
GC 언어에서 소멸자는 동기 호출이 어려워 대부분 비결정적 동작. JS에서는 WeakRef와 FinalizationRegistry가 있지만, Mozilla조차 예측 불가로 사용을 권장하지 않음
-
이 방식은 클래스 인스턴스가 아닌 대상에도 사용 가능한 점이 강점
-
JavaScript에는 익명 속성(anonymous property) 개념이 없어서 질문 자체가 모호하게 느껴짐. 이 방법 외 대안이 없다는 주장
-
-
제안문 첫 예시는 try/finally로 락을 안전하게 해제하는 코드 사례. 이런 패턴이 오랜 실행이 필요한 상황만 중요한지, 브라우저나 CLI 환경에서 에러로 프로세스 종료 시에도 락이 해제되는지 궁금
-
명세에는 블록 실행이 정상적으로 끝나든, 예외/분기/탈출로 끝나든 무조건 dispose가 실행된다고 나옴. 즉, using이든 try/finally든 동일. 강제 종료(프로세스 강제 종료)는 명세 영역 밖이라 ECMAScript는 관여하지 않음. 예시의 stream은 JS 내부 객체라, 인터프리터가 사라지면 락 개념 자체가 의미 상실. 만약 OS 자원(메모리, 파일 등)이면, 보통 OS에서 일괄 정리하지만, 동작은 플랫폼별로 다름
-
브라우저 웹페이지는 달리 보면 아주 오래 실행되는 응용프로그램. 심지어 서버 프로세스보다 오래 실행됨. 오류가 난다고 페이지가 죽지 않고, 예외를 포함한 에러 처리는 명확한 규칙에 따라 finally에서 처리. NodeJS에선 기본적으로 에러 시 프로세스가 종료되지만, 서버 상황에 따라 다른 처리가 흔함. 즉, finally에서 해제 함수가 반드시 호출됨
-