Dev

JavaScript 메모리 특강

ingeeC 2024. 5. 25. 00:06

한국 Mozilla Hacks 기술 블로그에서 번역 활동을 했다. 원체 좋은 글이 많이 올라오는 곳이라서 번역하면서 많이 배웠다. 내가 한 일이라곤 잘 갖춰진 커뮤니티 인프라 위에 숟가락 하나 얹은 것뿐이었다. 그런데 오랜만에 생각나는 글이 있어 살펴보니, 한국 Mozilla Hacks의 현재 상태가 별로 안 좋다. 블로그 기사에서 이미지가 누락되어 안 보인다. 숟가락 하나만 들고 있는 나로서는 할 수 있는 일이 없다. 어쩔 수 없이 Hacks의 글을 편집해서 여기에 복사본을 만든다. 모질라 커뮤니티가 건강하게 오래 가길 빈다.

 

---

메모리 특강

 

이 글은 3부작 시리즈의 첫번째 글입니다.

  1. 메모리 특강
  2. 만화로 소개하는 ArrayBuffer 와 SharedArrayBuffer
  3. Atomics 를 이용해서 SharedArrayBuffer 레이스 컨디션 피하기

 

ArrayBuffer 와 SharedArrayBuffer 가 JavaScript 에 추가된 이유를 이해하려면, 우리는 메모리 관리에 대해 조금 알아야할 필요가 있습니다.

머신에 탑재된 메모리를 상자 더미로 생각해 봅시다. 나는 이것이 사무실에 있는 사서함이나, 유치원에서 있는 사물함과 비슷하다고 생각합니다.

만약 원생들에게 어떤 선물을 남겨주고 싶다면, 우리는 선물을 사물함 상자에 넣어 두면 됩니다.

 

어떤 아이가 사물함 상자에 무언가 넣고 있다

 

각 상자들 옆면에는 숫자가 붙어 있는데, 이것을 메모리 주소라고 부릅니다. 그것이 바로 선물을 어디다 남겨야 하는지 식별하는 방식입니다.

이 상자들 각각은 모두 같은 크기이며 일정량의 정보를 보관할 수 있습니다. 상자의 크기는 머신의 종류에 따라 결정되는데, 워드 사이즈(word size)라고 부릅니다. 워드 사이즈는 통상 32-bit 또는 64-bit 입니다. 하지만 그림으로 쉽게 표현하기 위해 저는 워드 사이즈를 8-bit라고 가정하겠습니다.

 

8개 칸이 있는 상자

 

만약 숫자 2를 어떤 상자에 저장하고 싶다면? 쉽습니다. 숫자는 이진수로 표현하기가 쉽습니다.

 

숫자 2를 이진수 00000010으로 변환하여 상자에 넣는다

 

숫자가 아닌 어떤 것을 저장하고 싶다면 어떻게 할까요? 알파벳 H 같은 것 말이죠.

알파벳 H 를 숫자로 표현할 방법이 필요합니다. 그러기 위해 만든 것이 인코딩입니다. UTF-8 같은 것 말이죠. 이제 알파벳 H 를 숫자로 인코딩해줄 도구가 필요합니다. 예를 들어 인코더-링(encoder ring) 같은 것 말입니다. 이제 우리는 알파벳 H 를 저장할 수 있습니다.

 

알파벳 H를 인코더-링으로 변환하여 72를 얻고, 이를 이진수로 변환하여 상자에 넣는다

 

저장했던 정보를 다시 가져오고 싶을 때에는, 정보를 변환해주는 디코더를 사용해서 상자에서 꺼낸 정보를 다시 알파벳 H 로 변환합니다.

 

메모리 자동 관리

JavaScript 로 일할 때 우리는 메모리에 대한 생각을 할 필요가 없습니다. 메모리 문제는 전적으로 JavaScript 가 책임집니다. 이는 우리가 메모리에 직접 접근할 수 없다는 뜻입니다.

대신, JS 엔진이 중재자 역할을 합니다. JS 엔진이 우리 대신 메모리를 관리합니다.

 

사물함 상자 앞에 로프가 쳐져있고 JS엔진이 경비처럼 서있다

 

어떤 JS 코드(가령 React 같은 코드)가 변수를 만드는 상황을 생각해봅시다.

 

위와 같은 상황에서 React가 JS엔진에게 변수 생성을 요구한다

 

그러면 JS 엔진은 그 변수에 저장할 값(value)을 인코더에 넣고 돌려서 해당 값(value)에 대한 이진수 표현을 얻어냅니다.

 

JS엔진이 인코더-링으로 문자열에 대한 이진수 표현을 만든다

 

그런 다음 JS 엔진은 비어 있는 메모리 공간을 찾아서 이진수로 표현된 값(value)을 거기에 넣습니다. 이것이 메모리 할당이라고 불리는 과정입니다.

 

JS엔진이 이진수 표현을 저장할 사물함 상자를 찾는다

 

이제부터 JS 엔진은 이 변수가 프로그램에 의해 사용되고 있는지 계속 확인합니다. 만약 더이상 변수가 참조되지 않으면 변수가 차지하고 있던 메모리 공간이 반환됩니다. 그러면 JS 엔진이 반환된 메모리 공간을 다시 사용할 수 있게 됩니다.

 

가비지 컬렉터가 메모리를 청소한다

 

이렇게 (문자열, 객체, 기타 모든 값들을 저장하는) 변수를 감시하다가, 더이상 변수가 사용되지 않을 때 청소하는 작업을 가비지 컬렉션(garbage collection)이라고 합니다.

JavaScript 처럼 개발자가 메모리 관리를 직접 하지 않는 랭귀지를 메모리-매니지드 랭귀지(memory-managed language)라고 부릅니다.

개발자에게는 메모리 자동 관리 방식이 편합니다. 하지만 (가비지 컬렉션으로 인한) 성능 저하를 감수해야 합니다. 그리고 보통 예상치 못한 시점에 성능 저하가 발생합니다.

 

메모리 수동 관리

메모리를 직접 관리하는 랭귀지는 다릅니다. 예를 들어, React 코드가 C 랭귀지로 작성됐다면 어떤 방식으로 메모리를 다루는지 알아봅시다 (WebAssembly 덕분에 실제로 정말 있을 수 있는 일입니다).

C 랭귀지에는 JavaScript 가 메모리 관리를 위해 제공하는 추상화 계층이 없습니다. 우리는 메모리에서 직접 값을 읽어 올 수도 있고, 직접 값을 저장할 수도 있습니다.

 

React의 웹어셈블리 버전이 메모리를 직접 억세스한다

 

C 또는 다른 랭귀지를 WebAssembly 로 컴파일할 때 우리가 사용하는 도구가 WebAssembly 결과물에 헬퍼 코드(helper code)를 추가합니다. 예를 들어, 바이트(byte) 값을 인코딩하고 디코딩하는 코드 말입니다. 이런 코드를 런타임 환경(runtime environment)이라고 부릅니다. 런타임 환경을 통해 JS 엔진이 JS 코드를 처리할 때 제공하는 편의 기능들의 일부를 제공받을 수 있습니다.

 

웹어셈블리 .wasm 파일의 일부로 제공되는 인코더-링

 

하지만 런타임은 가비지 컬렉션 기능은 제공하지 않습니다.

이것이 모든 것을 프로그래머 혼자 처리해야 한다는 뜻은 아닙니다. 메모리 수동 관리 방식을 따르는 랭귀지도 런타임으로부터 약간의 도움을 제공 받습니다. 예를 들면, C 랭귀지에서, 런타임 환경은 소위 프리-리스트(free list)라고 불리는 메모리 주소 구조체를 제공합니다.

 

사물함 옆의 프리-리스트에 비어있는 상자 목록이 적혀있다

 

우리는 malloc (memory allocate 의 단축 표현) 함수를 사용해서 런타임에게 데이터를 저장할 메모리 주소를 달라고 요청합니다. 그러면 이 함수는 프리-리스트에서 적당한 주소를 가져다 전해줍니다. 우리가 데이터 처리를 끝내고 나면, 우리는 free 함수를 호출해서 해당 메모리를 반환합니다. 그러면 그 주소는 다시 프리-리스트에 추가됩니다.

우리는 이 함수들의 호출 시점을 결정해야 합니다. 즉, 메모리를 스스로 관리해야 합니다. 그래서 이런 처리 방식을 메모리 수동 관리 방식이라고 부릅니다.

개발자로서 언제 어떤 메모리를 반환해야 하는지 매번 결정하는 것이 번거로울 수 있습니다. 적절하지 않은 시점에 메모리를 반환하면, 버그가 발생하거나 심각한 보안문제(security hole)가 발생할 수 있습니다. 만약 개발자가 사용한 메모리를 전혀 반환하지 않으면 시스템의 메모리가 모두 소진될 것입니다.

그래서 많은 수의 최신 랭귀지들은 메모리 자동 관리 방식을 사용합니다. 인간의 실수를 예방하기 위해서 입니다. 그러기 위해 실행 성능을 대가로 지불합니다. 다음 글에서 좀 더 자세히 설명하겠습니다.

 

이 글은 Lin Clark이 쓴 A crash course in memory management의 한국어 번역본입니다

한국 Mozilla Hacks의 글을 편집해서 복사합니다.