햄스터 갬성 블로그

[JS] 가비지 컬렉션 (Garbage Collection)

학부 시절 운영체제 수업에서 OS를 직접 구현하다보면 간혹 메모리가 부족하다는 버그가 뜨곤 했다. 보통 메모리 누수(memory leakage) 때문인데 메모리를 할당(e.g., malloc)하고 나중에 할당 해제(e.g., free)를 하지 않아 불필요한 메모리가 계속 쌓여 발생한다. C 같은 low-level 언어는 프로그래머가 직접 메모리를 관리할 수 있다. 하지만 JS나 Python 같은 대부분의 high-level 언어는 개발자가 임의로 메모리를 관리하지 못한다. 흠, 그럼 high-level 언어는 메모리 누수 발생을 어떻게 막는 걸까? 이 역할을 하는 게 가비지 컬렉터(Garbage colletor)이다. 가비지 컬렉터는 필요 없어진 메모리를 자체적으로 판단해 할당을 해제하여 메모리 누수의 발생을 막는다. V8 같은 JS 엔진에도 가비지 컬렉션이 구현되어 있다.

메모리 누수 관리하기 #

메모리 누수는 프로그램이 더이상 사용하지 않는 메모리가 OS에 의해 사용 가능한 메모리(free memory)로 반환되지 않는 현상을 가르킨다. 각 프로그래밍 언어마다 메모리를 제어하는 저마다의 방식을 가지고 있다. 보통 프로그래밍 언어 입장에서 메모리가 실제 필요한지 아닌지 파악하기는 힘들다. 현재 메모리에 저장된 값을 사용되고 있지 않더라도 개발자가 추후 사용할 수도 있기 떄문에 쉽게 판단하기 힘들다. 반대로 개발자는 프로그램이 어떻게 돌아갈지 본인이 직접 청사진을 그리므로 사용하지 않을 메모리를 판단할 수 있다. 이에 따라 특정 프로그래밍 언어는 개발자에게 약간의 권한을 제공한다. C 언어에서 메모리 할당은 malloc 함수를 통해 이뤄지며, 할당 해제틑 free 함수를 통해 이뤄진다. 하지만 이 방식의 약점은 개발자를 너무 맹신하는 데 있다. 개발자가 까먹고 메모리 반환을 하지 않으면 그 만큼의 메모리는 불필요하게 컴퓨터의 리소스를 잡아 먹게 되어 메모리 누수가 발생한다.

자바스크립트에서 가비지 컬렉션 #

자바스크립트는 가비지 컬렉션 기능을 사용한다. 이런 종류의 언어들은 어떤 메모리가 사용되고 있는지 주기적으로 체크해주면서 개발자를 돕는다. 가비지 컬렉션 언어에서 메모리 관리의 문제는 "어떤 메모리가 아직 필요한지"를 "어떤 메모리가 앱의 다른 부분에 의해 아직도 접근되는지(reachable)"로 치환된다. 차이점은 사소해 보일 수 있지만 중요하다. 접근되고 있지 않는(unreachable) 메모리는 알고리즘적으로 결정되고 OS로 반환된다. 실제 가비지 컬렉터에서 사용하고 있는 알고리즘 중 대표적인 예는 mark-and-sweep이다.

mark-and-sweep 알고리즘 #

mark-and-sweep 알고리즘은 다음의 단계대로 진행된다.

  1. 가비지 컬렉터는 root(루트)의 리스트를 만든다. 루트는 주로 전역 변수(global variable)로 그 reference가 코드에 유지된다. 자바스크립트에서 'window' 객체가 루트로 작용하는 전역 변수의 한 예시다. window 객체는 항상 어떤 상황에서도 사용되므로, 가비지 컬렉터는 window 객체를 항상 접근 가능하다고(reachable) 간주한다.
  2. 모든 루트는 접근 가능하다고 간주되고, 해당 루트의 속성을 타고 내려가며 참조 가능한 메모리가 있는지 재귀적으로(recursive)으로 조사된다. 루트로부터 참조 가능한 모든 메모리는 접근 가능하다고 판단한다.
  3. 접근되지 않는다고 판단된 메모리 조각은 가비지로 간주된다. 가비지 컬렉터는 이제 가비지 메모리 조각을 해제하여 사용 가능한 메모리로 반환한다.

최신 가비지 컬렉터는 이 알고리즘을 기반으로 여러 방식으로 진화되었지만 기본 원리는 같다. 루트로부터 참조 가능한 메모리는 남겨두고 나머지 메모리는 가비지로 판단하여 반환한다. 하지만 가비지 컬렉터가 완벽한 알고리즘을 갖췄다 해도 메모리 누수가 발생할 수 있다.
자바스크립트에서 메모리 누수가 일어나는 가장 큰 원인은 원치 않은 참조(unwanted reference)가 유지되는 경우이다.

원치 않은 메모리 누수가 발생하는 경우 #

Unwanted reference는 개발자가 더이상 사용하지 않지만 어찌된 이유에서인지 접근 가능하다고 여겨지는 메모리 조각이다. 즉, 더 이상 사용되지 않기 때문에 충분히 해방되도 괜찮지만, 아직도 코드 어딘가에 남아 있는 변수를 가리킨다. 가장 빈번하게 발생하는 경우는 전역 객체(즉, window)에 변수를 할당할 때이다.

function print() {
text = "I'm accidently referenced -_-"
// 위 코드는 아래와 동일하다.
// window.text = "I'm accidently referenced -_-"
}

위 코드에서 text 변수는 선언 키워드(var, let 등)가 사용되지 않았기 때문에 window 객체에 선언된다. 위 mark-and-sweep 알고리즘에 따라 가비지 컬렉터는 window 객체에 포함된 text 변수가 항상 접근 가능하다고 판단한다. 원래라면 print 함수의 종료와 함께 해방되어야 하지만 가비지 컬렉션은 알고리즘 상으로 이를 인지하지 못하고 메모리 누수가 발생하게 된다. 만약 var 등을 통해 변수를 선언했다면, text는 지역 변수(local variable)가 되어 함수의 종료와 함께 접근 가능하지 않은 메모리로 판단되어 메모리에서 해방된다. 이같은 케이스는 전적으로 개발자의 실수에서 비롯된다. 이외에도 unwanted reference가 발생하는 여러 경우가 있는데, 더 자세한 사항은 아래 레퍼런스를 참조하길 바란다.

메모리 누수는 자바스크립트 같이 가비지 컬렉터를 사용하는 언어에서도 발생할 수 있다. 메모리 누수가 반복되면 시스템에 심각한 에러를 초래할 수 있다. 따라서, 개발자들은 항상 예상치 못한 메모리 누수가 발생하지 않도록 의식해야 한다. 사용자 경험에도 영향을 미칠 수 있는 부분이라 프론트엔드 개발자도 항상 유의해야 할 사항인 것 같다.

Reference
Memory management - JavaScript | MDN
4 Types of Memory Leaks in JavaScript and How to Get Rid Of Them