이제는 모던 자바스크립트를 알아야지 - 메모리 생존주기, 가비지 컬렉션

황주현·2022년 3월 31일
3
post-thumbnail

혹시 C언어같은 저 수준 의 언어를 사용해 본 적이 있는가?
여기서 저 수준 이란, 질이 나쁘다는게 아니고 Low-level : 기계친화적 언어를 의미한다.

이 같은 언어들은 개발자가 직접 메모리를 할당하고, 참조하고, 관리한다.

여기까지 읽어보면 Javascript언어가 저 수준언어가 아니라는 것 쯤은 눈치챘을 것이다.
우리는 개발하며 메모리를 할당시켜주고, 해제시켜주는 작업을 직접 하지 않기 때문이다.

여기서 하나의 의문점이 생기게 된다.

그럼 대체 자바스크립트는 어떻게 메모리를 관리하는거지? 🤔

이런 의문점이 들었다면 벌써 반은 왔다.
대부분의 주니어 개발자들은 Javascript의 동작 원리가 아닌 당장의 사용법만 갈구하기 때문에 이런 부분을 놓치기 쉽다. (나포함)

이번에는 Javascript의 메모리 관리 방법(feat.가비지 컬렉터)에 대해 알아보도록 하자.



메모리란

메모리라는 단어는 흔히 들어보았을 것이다.
쉽게 말해 저장 공간인데, 프로그램이 돌아가며 저장해야하는 수많은 일련의 정보(데이터)를 저장하는 공간이라고 생각하자.


메모리 관리

메모리를 왜 관리하나요?

알다시피 메모리는 유한하다.
컴퓨터 조립을 해 본 사람은 알겠지만, 부품 하나 추가 할 때 마다 드는게 돈이다.

우리는 돈이 없기에 얼마 없는 메모리에서 최고의 효율을 뽑아내야 한다.
그리고 최고의 효율을 뽑아내려면 관리해야 할 것이다.


언어에서 메모리 관리

앞서 말했듯이, 저 수준의 언어는 개발자가 직접 메모리를 관리한다.
다행히 Javascript고 수준언어로 자체적으로 메모리를 관리해준다.

이쯤 되면 의아한 사람이 있을 것이다.

아니 자바 스크립트가 자동으로 관리해주면 굳이 알 필요가 있나요? 🤨


단순히 코드의 작동을 목적으로 한다면 충분히 일리가 있는 말이다.
하지만 우리는 단순 작동 코드가 목표가 아닌, 최적화된 작동 코드를 목표로 한다.

따라서 Javascript가 어떻게 메모리를 관리하는지 알고, 메모리 누수가 되는 부분이 없는지도 확인 할 수 있어야 한다.


자바스크립트에서 메모리 관리

메모리의 생존주기

MDN에서 메모리의 생존 주기는 다음과 같이 정의한다.

  1. 필요할때 할당한다.
  2. 사용한다. (읽기, 쓰기)
  3. 필요없어지면 해제한다.

이것을 Javascript에서는 어떻게 하는지 한번 알아보자.


메모리 할당 in JS

Javascript에서는 값을 선언할 때 자동으로 메모리를 할당한다.

// 문자열 메모리 할당됨
const testStringValue = 'test';
// 정수 메모리 할당됨
const testNumberValue = 123;
// 객체 및 객체에 포함된 값 메모리 할당됨
const testObjectValue = {				
  testOne: 'hello',
  testTwo: 'world',
};
// 배열 및 배열에 포함된 값 메모리 할당
const testArrayValue = [1, 'hello', null];	
// 함수를 위한 할당 (호출 가능한 객체)
function testNormalFunction(a){				
  return a+4;
}
// 위 함수와 동일하게 할당
const testArrowFunction = (a) => {			
  return a+4;
}

메모리 사용 in JS

Javascript에서 값을 사용하는 모든 일련의 과정들을 일컫는다.

// 메모리 할당
const testString = 'hello world';

// 메모리 사용 (읽기)
console.log(testString);

메모리 해제 in JS

Javascript고 수준언어로, 가비지 컬렉션 : GC를 채택했다.
이는 가비지 컬렉터 라는 프로그램이 자동으로 메모리를 관리해준다.

가비지 컬렉션은 엔진이 자동으로 수행하기 때문에, 개발자가 억지로 실행하거나 막을 수 없다.
-> 20221127 추가 엄밀히 따지자면 가능하긴 한 것 같다. (해보지는 않음)
하지만 답변에도 있듯이 상식적인 행위는 아닌 듯 하니 참고만 하자...


메모리 해제 in JS 자세히

가장 중요한 부분이다.

다시 앞선 내용을 복기해보자.

  1. 메모리는 '필요 없어졌을 때' 해제한다.
  2. Javascript는 메모리 관리(해제 포함)을 가비지 콜렉터가 자동으로 해준다.
  3. 결국 메모리 해제 작업은 가비지 콜렉터가 진행 한다.

여기서 궁금증이 하나 더 생긴다.

그럼 대체 '필요 없어졌을 때' 의 기준이 뭐지? 😅

이 부분이 가장 중요한, 가비지 콜렉터가 어떤 알고리즘으로 메모리를 해제시키는지에 대한 내용이다.

가비지 컬렉션의 주요 알고리즘 중 두가지를 소개해보려 한다.



참조-세기(Reference-counting) 알고리즘

가비지 컬렉션 알고리즘의 핵심 개념은 참조다.
참조의 정의는 아래와 같다.

A라는 메모리를 통해 (명시적 혹은 암시적으로) B라는 메모리에 접근할 수 있을 때
"BA에 참조된다."라고 할 수 있다.


그럼 참조-세기 알고리즘은 뭐지?

(실제로 현재 Javascript에서는 사용하지 않는 알고리즘으로, 참고차 가볍게 읽어보자.)

참조-세기알고리즘은 다음과 같이 간단하게 구성되어 있다.

더 이상 필요 없는 오브젝트(정보) = 어떤 다른 오브젝트도 참조하지 않는 오브젝트 = 가비지

예시를 보며 알아보자.


let firstValue = {
  a : {
   	b : 2 
  }
};

let secondValue = firstValue;


(빨간 화살표를 참조 표시로 가정)

  • 2개의 오브젝트가 생성되었다.
  • 이때 b 오브젝트는 a 오브젝트의 속성으로 참조된다.
  • b 오브젝트는 FirstValue변수에 할당되었다.
  • secondValue변수는 위 오브젝트를 참조하는 두 번째 변수이다.
  • 현재 상황에서 가비지 컬렉션이 수행 될 메모리는 없다.

여기까지 이해되었다면 다음 상황을 보자.


firstValue = 1;

  • firstValue참조가 변경되었다.
  • 이제 secondValue만 유일하게 오브젝트를 참조한다.

기존에는 firstValuesecondValue가 오브젝트를 참조했으나, firstValue의 값이 1로 변경되며 secondValue만 오브젝트를 참조하는 모습이다.


let newValue = secondValue.a;

  • 새로운 변수 newValuea 속성을 참조했다.
  • 이제 secondValue.asecondValue가 속성으로 참조하고, newValue변수가 참조한다.

secondValue = 2;

  • secondValue참조가 변경되었다.
  • 이제 'secondValue참조하던 오브젝트'를 참조하는 오브젝트는 없다

유일한 참조변수 secondValuenewString을 대입해 오브젝트의 참조가 없어졌다.
그럼 이제 가비지 컬렉션이 수행될까?

정답은 아니다.

기존 오브젝트의 a속성은 현재 newValue가 참조하고 있기 때문이다.

따라서 아직 참조하는 데이터가 때문에 필요없는 데이터로 분류되지 않는다.


newValue = 'bye~';

  • newValue참조가 변경되었다.
  • 이제 가장 처음 firstValue가 참조한 오브젝트를 참조하는 다른 변수는 존재하지 않는다.
  • 참조하는변수X = 필요없는변수 이므로 가비지 컬렉션이 실행된다.

참조가 모두 사라지며 드디어 가비지 컬렉션이 실행되었다.
처음엔 조금 헷갈릴 수 있지만, 한 단계씩 차근차근 짚어나가면 충분히 이해 할 수 있는 내용이다.


참조-세기 알고리즘의 한계

앞서 알아본 참조-세기 알고리즘은 겉보기에는 완벽하지만, 치명적인 한계가 있어 지금은 Javascript에서 사용되지 않는다.

바로 순환참조를 다루는 한계이다.

예시와 함께 어떤건지 알아보자.

function test() {
  let firstValue = {};
  let secondValue = {};
  
  // 순환참조 구간
  firstValue.a = secondValue;
  secondValue.a = firstValue;
  
  return 'complete';
}

test();


현재 firstValuesecondValue가 서로 속성으로 참조하는 순환 구조를 보이고 있다.

정상적으로는 test()라는 함수가 실행되어 return으로 종료되면 firstValuesecondValue는 제거 되어야하지만, 실제로 그렇지 않다.

어쨌든 서로를 참조하고 있기 때문에 필요없는 오브젝트로 판단되지 않기 때문이다.

이같은 현상은 메모리 누수를 야기시켰고, 이를 보완하고자 현재는 다른 알고리즘을 사용중이다.



표시하고-쓸기(Mark-and-sweep)알고리즘


현재 Javascript에서 사용중인 가비지 컬렉션 알고리즘은 이것이다.

표시하고-쓸기 알고리즘은 필요 없는 오브젝트를 다르게 정의한다.

더 이상 필요 없는 오브젝트(정보) = 닿을 수 없는 오브젝트

도달 가능성(reachability)이라고 하는 개념을 사용해 도달 가능한(reachable)값을 판별하는 알고리즘이다.

대체 어디서부터 무엇에 닿는다는 말인지 한번 알아보자.


roots와 가비지 컬렉터

해당 알고리즘은 roots라는 오브젝트의 집합을 가지고 있다.
roots에 있는 오브젝트 root에서 어떻게든 접근이 가능하다면 필요한 오브젝트라고 판단한다.

root의 대표적인 종류는 아래와 같다.

  • 현재 함수의 지역 변수와 매개변수
  • 중첩 함수의 체인에 있는 함수에서 사용되는 변수와 매개변수
  • 전역 변수

예시를 보며 이해해보도록 하자.


let user = {
  name : "joohyun"
};


전역변수 user{name:"joohyun"} 오브젝트를 참조하고 있는 모습이다.
만약 여기서 user의 값을 변경하면 어떻게 될까?


user = "himprover";


그림처럼 전역변수 user의 값은 "himprover"로 변경되었고, 화살표가 사라졌다.
이제 {name:"joohyun"} 오브젝트에 접근할 방법이 없어진 것이다.

이때 가비지 컬렉션이 실행되며 {name:"joohyun"} 오브젝트를 삭제한다.

잘 이해가 되지 않는다면 각각의 하늘에 떠있는 섬이 있고, 화살표가 다리라고 생각하자.

가장 높은 섬에서부터 사람이 내려가는데, 다리가 연결되어 있지 않아 사람이 갈 수 없는 섬이 있다면 없애버리는 것이다.


좀 더 복잡하게

위 예제는 매우 간단한 경우라서, 좀 더 복잡한 구조를 예시로 들어본다.
가족관계를 구성하는 객체가 있다고 가정해보자.

let family = {
  father : {
    name : 'man',
  },
  mother : {
    name : 'woman',
  }
}

family.father.wife = family.mother;
family.mother.husband = family.father;

조금 복잡하지만, 현재는 모든 객체에 도달이 가능한 상태이다.
만약 여기서 참조를 두 개 지운다고 가정해보자.


delete family.father;
delete family.mother.husband;

언뜻 보기에는 연결되어 있는 것 같다.
하지만 여기서 잊지 말아야 할 것은 root에서 도달 할 수 있는지의 여부다.

<global>에서 부터 그려보면 도달하지 못한다는 것을 알 수 있다.



따라서 빨간색 배경으로 분류된 {name:"man"}은 가비지 컬렉션이 실행되어 제거된다.



(가비지 컬렉션 실행 결과)


도달할 수 없는 섬

다시 처음 가족 관계 오브젝트를 생성한 시점으로 돌아가보자.
아래 코드를 실행하면 어떻게 될까?

family = null;

family의 값이 null이 되면서 기존의 오브젝트 들이 길을 잃었다.
이 빨간색 사각형 부분을 도달할 수 없는 섬이라고 한다.

사실 섬 안에서는 서로 도달할 수 있지만, root에서의 길이 없기 때문에 섬 전체가 가비지 컬렉션에 의해 제거되는 것이다.

이처럼 단순히 연결이 되어 있는것이 중요한 게 아닌, root에서부터 도달할 수 있는지가 중요한 관점이다.



정리


가비지 컬렉션의 개념은 모던 자바 스크립트에서 탄생한 것은 아니다.
그러나, 시멘틱태그처럼 이 정도는 당연히 알고 그 다음에 모던 자바 스크립트를 건드리는게 맞다고 판단해 같은 시리즈에 넣었다.

어떤 언어이든 그냥 쓰는 것이 아닌, 어떻게 동작하는지 한번 더 생각해보고 사용하는 습관을 들여보자.


  • Javascript는 메모리 생명주기를 가비지 컬렉션을 통해 자동으로 관리해준다.
  • 메모리 해제 과정에서 표시하고-쓸기알고리즘을 사용한다.
  • 이 알고리즘은 객체가 도달 가능 한 상태가 아닐 때 메모리를 해제시킨다.
  • 가비지 컬렉션은 엔진이 자동으로 수행하므로, 개발자가 억지로 실행하거나 막을 수 없다.

+ 읽어주셔서 감사합니다.
+ 오타, 내용 지적, 피드백을 환영합니다. 많이 해주실 수록 제 성장의 밑거름이 됩니다.
profile
반갑습니다. 프론트엔드 개발자 황주현 입니다. 🤗

2개의 댓글

comment-user-thumbnail
2022년 5월 31일

자바스크립트의 가비지 컬랙션에 대해서 궁금했었는데 덕분에 이해할 수 있었습니다
감사합니다! :)

1개의 답글