나만의 작은 도서관

[JavaScript] 정리 1. 가비지 컬렉터(Garbage Collector)와 데이터 참조(Data Reference) 본문

JavaScript/정리

[JavaScript] 정리 1. 가비지 컬렉터(Garbage Collector)와 데이터 참조(Data Reference)

pledge24 2024. 4. 30. 20:25
유의사항: 해당 글은 공부한 내용을 정리하는 용도이므로, 수정이 필요할 경우 내용의 수정이 있을 수 있습니다.

개요

대부분의 프로그래밍 언어들에는 가비지 컬렉터(Garbage Collector)를 지원한다. 가비지 컬렉터는 개념이기 때문에 각 언어에서 사용하는 가비지 컬렉터는 조금씩 다른 부분이 있다. 오늘은 자바스크립트의 가비지 컬렉터의 작동과정을 간단히 알아보자.


가비지 컬렉터란?

직역하면 "쓰레기 수집가"인 가비지 컬렉터(줄여서 GC라고 부르기도 한다.)는 이름 그대로 쓰레기를 수집하는 기능이다. 프로그램이 실행되는 도중 더이상 사용하지 않는 데이터가 발생하게 되는데, 이렇게 자리만 차지하고 더 이상 사용하지 않는 데이터를 쓰레기라고 말한다. 이런 쓰레기를 수집함으로써 메모리를 확보하는 역할을 한다. 

 

!가비지 컬렉터 왜 써요

어느 환경에서든 메모리는 한정되어 있고, 한정된 메모리에서 사용할 수 있는 메모리는 소중한 자원인데, 이렇게 메모리 자리만 차지하는 쓰레기만 잔뜩 있다면 프로그램의 성능이 떨어진다.(쓰레기를 잔뜩 만들어 통신을 마비시키는 DDos만 봐도 쓰레기에 의한 성능저하가 얼마나 치명적인지 알 수 있다.)

 

가비지 컬렉터를 굳이 알아야 하나요?

자바스크립트를 하는 개발자라면 특히 알아놓으면 좋다. 웹 브라우저는 기본적으로 사용할 수 있는 메모리가 작기 때문에 메모리 관리 부족에 따른 이슈가 더 자주 발생할 수 밖에 없다. 그렇기 때문에 가비지 컬렉터의 기본 동작 과정 정도를 알아둔다면 후에 유용한 지식으로 사용할 수 있다.


자바스크립트에서의 가비지 컬렉터

우리가 자바스크립트를 접하면서 가장 살에 닿는 가비지 컬렉터의 사용 예시는 바로 변수 할당이다. 

자바스크립트는 변수를 선언할 때 할당하는 데이터의 타입을 알 수 없다. 그렇기 때문에 변수는 정해진 메모리가 아닌 할당한 데이터의 크기에 맞는 메모리를 할당해야한다. 이런 이유에서인지 자바스크립트는 데이터를 저장할 때 포인터 방식으로 데이터를 저장한다.

포인터 방식이란 값을 저장한 메모리를 참조하는 방식으로 대표적으로 C언어에서 사용했던 방식이다. 자바스크립트에서는 변수에 데이터를 할당할 때 변수는 오직 데이터가 저장된 메모리의 참조 정보만 가진다. (자바스크립트 변수가 고정된 8바이트를 가지는 이유이기도 한다.) 

 

예시1(단일 데이터)

 

다음과 같은 코드가 있다고 가정하자.

let a = 24;

 

단순히 변수 a에 24라는 데이터 값을 넣는 코드다. 여기서 우리가 짚고 넘어가야 할 부분은 변수의 선언(let a;)과 할당(a = 24;)이라는 2개의 코드라는 것. 변수 a는 어떤 데이터 값이 들어오는 지 모르고 있다가 할당(a = 24;)이 되는 순간 데이터 값을 알게된다. 

주소 2001 (a변수) 4001
데이터 (4001번에 대한 참조 정보) 24

 

위의 표와 같이 코드가 실행이 되면 변수 a는 2001번 메모리와 같이 데이터 값 24를 가진 4001번에 대한 참조 정보를 가지게 된다.(계속 참조 정보라고 말하는 이유는 자바스크립트에서는 직접적으로 주소를 가르키는 C언어와 조금 다르기 때문) 

 

재할당을 하면 어떻게 될까?

 

a 변수에 8을 재할당을 했다고 가정해보자. 

let a = 24;
a = 8;

 

변수 a에 데이터를 재할당하는 경우, a는 기존의 참조 정보를 지우고 재할당한 데이터의 참조 정보를 저장한다. 더이상 참조하지 않는 4001번 메모리는 GC가 쓰레기라 판단하여 할당을 해제한다.

 

주소 2001(a변수) 4001 4002
데이터 (4002번에 대한 참조 정보) 24(GC에 의해 수거!) 8

 

단일 데이터 값을 가지는 자료형( string, number, bigint, boolean, undefined, ES6 부터 추가된 symbol 과 같은 원시 타입)들은 이와 같은 방식으로 작동하며, 데이터는 스택 메모리에 저장된다.

여기서 데이터 영역 주소인 4001, 4002번은 값이 수정되지 않는데, 이를 불변성을 가진다고 한다.

 

예시2(여러 데이터)

 

그렇다면 단일이 아닌 여러 데이터 값을 가지는 객체 자료형은 어떨까? 아래 코드를 보자.

const person = {
  name: "john",
  age: 27
};

 

name, age 총 2개의 속성을 가지는 person 객체를 하나 선언하는 코드다. 이번에는 person, name, age를 가리키는 참조 정보가 필요해 보인다. 따라서 다음과 같다.

주소 2001( person ) 10001( name ) 10002( age ) 20001(객체 속성 정보)
데이터 ( 20001번에 대한 참조 정보) john 27 ( 10001, 10002번에 대한 참조 정보)

변수는 person 스택 메모리에 저장되며, 객체에 대한 데이터들은 힙 메모리에 저장된다. 객체 속성에 대한 정보를 가진 주소 20001번 메모리는 각 속성의 데이터가 저장된 10001, 10002의 참조 정보를 가지게 된다.

 

재할당을 하면 어떻게 될까?

 

아래와 같이  객체 person의 age 값을 재할당을 했다고 가정해보자. 

person.age = 3;

 

객체 person의 age 값을 재할당하는 경우, 원시 타입과 달리 값만 수정하고 주소는 그대로 가져간다.

주소 2001( person ) 10001( name ) 10002( age ) 20001(객체 자체)
데이터 ( 20001번에 대한 참조 정보) john 3 ( 10001, 10002번에 대한 참조 정보)

이렇듯 여러 데이터 값을 가지는 객체 자료형(참조 타입)은 메모리 해제없이 같은 메모리의 값을 수정하게 되는데, 때문에 객체 자료형은 불변성을 가지지 않는다. 

추가로 객체의 일부 속성을 아예 삭제하게 되는 경우에는 객체의 해당 속성의 연결을 끊어버리고 끊긴 속성의 데이터는 GC가 쓰레기라 판단하여 메모리를 해제한다.


얕은 복사 vs 깊은 복사

 

원시 자료형이든, 객체 자료형이든 할당을 하게 된다면 데이터의 참조 정보를 가져온다는 것은 똑같다. 문제는 참조 타입의 경우 참조의 참조와 같은 경우가 발생하기 때문에 참조의 깊이에 따라 제대로 정보를 가져오지 않거나, 서로 다른 변수의 속성이 동시에 수정되는 경우가 발생할 수 있다. 

이를 해결하기 위해선 얕은 복사와 깊은 복사에 대한 의미를 알아두면 좋다. 아래의 코드를 보며 이해해보자.

 

얕은 복사 (깊이 1)

얕은 복사는 한 번 참조해 복사해 오는 것을 말한다. 아래 코드의 경우 한 번만 참조해 복사해오는 코드이다.

const person1 = {
  name: "john"
  age: 27
};

const person2 = person1; 

person1.age = 25; 

console.log(person1);
console.log(person2);

// Output:
// { name: 'john', age: 25 }
// { name: 'john', age: 25 }

코드의 결과를 봐서 알 수 있듯이, 분명 person2에 age속성을 25로 바꾼 적이 없는데 25로 변경되었다. 이 이유는 같은 객체의 참조 정보를 가리키면서 발생하는 문제다. 이를 해결하려면 한 번 더 참조해야 한다.

 

깊은 복사(깊이 2)

 깊은 복사는 여러 번 참조해 복사해 오는 것을 말한다. 아래 코드의 경우 참조의 참조까지 복사해오는 코드이다. 

 

const person1 = {
    name: "john",
    age: 27
};
  
const person2 = {};

for(property in person1){
    person2[property] = person1[property];
}
  
  person1.age = 25; 

  console.log(person1);
  console.log(person2);

// { name: 'john', age: 25 }
// { name: 'john', age: 27 }

이제 원하는 결과가 나왔다. 하지만 이렇게 하면 깊이가 2인 경우까지만 의도대로 나오고 더 깊은 3, 4,...N 같은 경우에는 똑같이 값이 아닌 객체의 참조 정보를 가져온다. 

 

끝까지 깊은 복사를 하려면...?

 

탐색 알고리즘을 사용한다. 모든 객체의 데이터를 알아보고 싶다면 dfs나 bfs를 사용해서 모든 객체의 데이터에 접근하여 가져온다. 뭘 사용하든지 상관없이 완전 탐색 알고리즘이면 되지만 보통은 재귀함수를 이용한 dfs를 사용한다.


참고 자료

https://velog.io/@leehyunho2001/%EA%B0%80%EB%B9%84%EC%A7%80-%EC%BB%AC%EB%A0%89%EC%85%98-%EB%AA%A8%EB%A5%B4%EC%8B%9C%EB%8A%94%EB%B6%84