[JS] Map, WeakMap with GC

CheolHyeon Park·2022년 11월 22일
0

JavaScript

목록 보기
16/23

Map 자료구조는 객체 리터럴(literal)과 비슷해보인다. key-value 쌍을 이용하는 자료구조이고 입력한 순서대로 순회도 할 수 있다. 하지만 둘은 차이가 있다.

Map과 객체 리터럴의 차이

  • Map은 모든 타입 형태에 키가 가능하지만 객체 리터럴은 string, symbol의 키만 가능하다.
const iAmMap = new Map();
const iAmObj = {}

// const keyFunc = function(){};
iAmMap.set(function(){}, 1)


iAmObj[function(){}] = 1;

console.log(iAmMap)  // Map(1) {f => 1}
console.log(iAmObj)  // {function(){}: 1}
  • Map은 iterator protocol을 만족하여 for...of로 순회가 가능하지만 객체 리터럴은 for...in, Object.keys, Object.values로 순회가능하다.
const iAmMap = new Map();
const iAmObj = {}

iAmMap.set(1, 1)
iAmMap.set(2, 2)
iAmMap.set(3, 3)

iAmObj[1] = 1;
iAmObj[2] = 2;
iAmObj[3] = 3;

for (const entry of iAmMap) {
    console.log(entry);
}

for (const key in iAmObj) {
    console.log(key,iAmObj[key]);
}
  • Map의 삭제는 최적화 되어있어서 빠르고, 객체 리터럴의 삭제는 상대적으로 느리다.(1000만번 기준으로 400ms정도 차이났다.)
const iAmMap = new Map();
const iAmObj = {}

for (let index = 0; index < 10_000_000; index++) {
    iAmMap.set(index, index)
    iAmObj[index] = index;
}


console.time('map delete')
for (const [key] of iAmMap) {
    iAmMap.delete(key)
}
console.timeEnd('map delete')
console.time('obj delete')
for (const key in iAmObj) {
    delete iAmObj[key]
}
console.timeEnd('obj delete')
  • Map은 키-값 쌍의 갯수를 size 프로퍼티로 제공하지만, object는 직접 구해야한다.
const iAmMap = new Map();
const iAmObj = {}

for (let index = 0; index < 10_000_000; index++) {
    iAmMap.set(index, index)
    iAmObj[index] = index;
}

let count = 0;
for (const key in iAmObj) {
    if (Object.hasOwnProperty.call(iAmObj, key)) {
        count++;
    }
}

console.log(iAmMap.size)// 10000000
console.log(count) // 10000000

WeakMap

기본적인 특징

Map의 키는 모든 타입이 가능하지만, WeakMap은 Primitive타입은 올 수 없다. WeakMap의 키는 오직 object만 가능하다.

쓰는 이유

WeakMap을 사용하는 이유는 GC(Garbage Collection)에 의해 키가 수거 된다는 점이다. 아래 예를 보자

  • Map의 object 키가 변경되는 경우
const iAmMap = new Map();

let funcKey = function(){}
iAmMap.set(funcKey, 1);

funcKey = function(){console.log('hi')} // 키 변경

console.log(iAmMap.get(funcKey)) // undefined
console.log(iAmMap)  // Map(1) {function(){} => 1}

// funcKey가 변경되어도 iAmMap은 function(){}를 참조하고 있기 때문에 GC에 의해 수거되지 않는다.

Map은 funcKey가 변경되어도 iAmMap객체에는 function(){}키가 살아있지만 접근할 수 없는 상황이 된다. 즉, 이러한 참조가 남아 GC에 의해 수거되지 않는다.

  • WeakMap의 object 키가 변경되는 경우
const iAmWeakMap = new WeakMap();

let funcKey = function(){}
iAmWeakMap.set(funcKey, 1);

funcKey = function(){console.log('hi')} // 키 변경

console.log(iAmWeakMap.get(funcKey)) // undefined
console.log(iAmWeakMap)  // WeakMap(1) {function(){} => 1}

// funcKey가 변경되면 iAmWeakMap의 키는 GC에 의해 수거된다.


funcKey를 변경하였더니 시간이 흐르고 GC에 의해 수거되었다.

활용

살아있는 객체에 대한 데이터를 저장할 때

const countVisitMap = new WeakMap();

const countUser = (user) => {
  const count = countVisitMap.get(user) || 0;
  countVisitMap.set(user, ++count);
}

const user = {name: 'park'}
countUser(user);
user = null;  // 방문자 떠났다.

유저의 방문 횟수를 저장하는 자료구조로 Map을 사용한다면 user가 null이 되었을 때 메모리 누수가 발생한다. WeakMap을 사용한다면 user가 null이 되었을 때 GC에 의해 메모리가 수거되므로 메모리 문제에 신경쓰지 않아도 된다.

캐싱

캐싱은 필요한 데이터를 미리 저장해두고 빠르게 데이터를 제공하는 방식이다.

let cache = new WeakMap();

// 연산 후에 캐싱
function process(obj) {
  if (!cache.has(obj)) {
    let result = /* 연산 수행 */ obj;

    cache.set(obj, result);
  }

  return cache.get(obj);
}

let obj = {/* ... 객체 ... */};

let result1 = process(obj);
let result2 = process(obj);

// 객체가 쓸모없어지면 아래와 같이 null로 변경
obj = null;

WeakMap을 사용하여 캐싱 해둔 데이터가 사라져도 따로 처리할 것 없이 GC에 의해 메모리가 수거되므로 편한사용이 가능하다.

정리

WeakMap은 일시적으로 관리되는 데이터를 저장하기 위한 저장소라고 생각한다. 계속 유지되는 데이터가 아닌, 잠시 유지되는 데이터에 대한 처리를 할 때 사용한다면 GC에 의해 자연스럽게 수거되기 때문에 메모리 문제에 대해 고민하지 않아도 되서 편한것 같다.

profile
나무아래에 앉아, 코딩하는 개발자가 되고 싶은 박철현 블로그입니다.

0개의 댓글