[모던 JavaScript 튜토리얼] 27. WeakMap, WeakSet

3 분 소요

본문 바로가기


가비지 컬렉션에서 봤듯이, 자바스크립트 엔진은 도달 가능한 값을 메모리에 유지한다.

let john = { name: "John", age: 30 };
john = null;

위 예시를 보자. 먼저, 객체 { name: "John", age: 30 } 이 메모리에 할당된 뒤, 해당 주소값이 john에 할당된다. 즉, 변수 john{ name: "John", age: 30 } 이라는 객체를 참조한다. (정확히는 메모리의 어느 한 곳을 시작으로, 일련의 여러 프로퍼티들이 저장되는 시작 주소를 참조한다.)

이후 해당 참조를 null 처리 함으로써, 해당 객체 { name: "John", age: 30 } 는 특정 시간이 지나면 가바지 컬렉터에 의해 메모리에서 사라지게 될 것이다.


let john = { name: "John" };

let array = [john];
let map = new Map();
map.set(john, "something");

john = null;

console.log(JSON.stringify(array[0]));               // {"name":"John"}
for (let entry of map.entries()) console.log(entry); // [ { name: 'John' }, 'something' ]

하지만, 객체를 요소로 갖는 배열이나, 객체를 키로 갖는 Map에서는 해당 참조를 null 처리해도 객체는 배열 혹은 Map 컬렉션의 일부로 남아있어야 하기 때문에 메모리에서 삭제되면 안된다. 따라서, 위와 같이 접근이 가능하다.

위에서 본 객체의 관점에서 위크맵(WeakMap)은 일반 Map과 다른 양상을 보인다. 위크맵을 사용하면 키로 쓰인 객체가 가비지 컬렉션의 대상이 된다.


위크맵(WeakMap)

아래와 같이, 위크맵은 일반 맵과 차이점을 보인다.

  1. 위크맵은 맵과 다르게 반드시 객체 타입의 키만을 허용한다.
  2. keys(), values(), entries(), clear() 메서드와 size 프로퍼티를 갖지 않는다.


let weakMap = new WeakMap();
let obj = {};

weakMap.set(obj, "ok");
console.log(weakMap);         // WeakMap { <items unknown> }

weakMap.set("test", "error"); // TypeError: Invalid value used as weak map key

위크맵은 맵과 다르게 키가 반드시 객체여야만 한다. 원시값은 위크맵의 키가 될 수 없다.


let john = { name: "John", age: 30 };
let weakMap = new WeakMap();
weakMap.set(john, "something");

john = null;

위와 같이 위크맵에 객체를 키로 키-값 요소를 넣은 후 해당 키를 null 처리하면, 해당 객체는 가비지 컬렉션의 대상이 된다. john 이 가리키는 객체는 오로지 위크맵의 키로만 사용되므로, 참조를 덮어쓰게 되면 이 객체는 위크맵과 메모리에서 자동으로 삭제된다.


위크맵은 아래 네 개의 메서드만 지원한다.

  • weakMap.get(key)
  • weakMap.set(key, value)
  • weakMap.delete(key)
  • weakMap.has(key)

적은 메서드를 제공하는 이유는 가비지 컬렉션의 동작 방식 때문이다. 객체는 모든 참조를 잃게 되면 자동으로 가비지 컬렉션의 대상이 되는데, 동작 시점을 정확히 알 수는 없다.

가비지 컬렉션이 일어나는 시점은 자바스크립트 엔진이 결정한다. 객체는 모든 참조를 잃었을 때, 그 즉시 메모리에서 삭제될 수도 있고, 다른 삭제 작업이 있을 때까지 대기하다가 한 번에 같이 삭제될 수도 있다. 위크맵은 키가 객체로 설정되기에, 그 안에 요소가 몇 개 있는지 정확히 파악하는 것 자체가 불가능하다.


언제 사용하나?

1. 데이터를 추가할 때

let visitsCountMap = new Map();

function countUser(user) {
    let count = visitsCountMap.get(user) || 0;
    visitsCountMap.set(user, count + 1);
}

let john = { name: "John" };

countUser(john);

john = null;

예를 들어, 유저의 정보가 담긴 객체를 키로 Map에 방문 기록 횟수를 저장한다고 하자. 위와 같이 john 이라는 유저가 삭제될 상황에서 단순히 null 처리를 해 메모리에서 객체가 제거되었다고 생각할 수 있지만, 해당 객체를 키로 갖는 맵의 요소가 있기에 제거되지 않는다.

유저 수가 많아지면 제거된 유저 정보들이 불필요하게 차지하고 있는 메모리를 정리하지 못하게 된다. 이런 문제를 위크맵을 사용해 사전에 예방할 수 있다.


2. 캐싱 작업에서

let cache = new Map();

function process(obj) {
    if (!cache.has(obj)) {
        let result = obj.val * obj.val;

        cache.set(obj, result);
    }

    return cache.get(obj);
}

let obj = { val: 0 };

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

obj = null;

console.log(cache.size); // 1

위 예제는 맵을 캐시로 사용하는 간단한 코드다. 간단히 하나의 키-값 쌍만 사용한다고 가정했을 때, 함수 process()는 캐시 내부에 연산한 기록이 있으면 가져다 쓰고, 없다면 제곱한 결과를 저장한다.

위 코드도 마찬가지로, 객체 참조를 끊는다 해도 가비지 컬렉션의 대상이 되지 않고 캐시가 계속 쌓이게 될 거다. 이런 경우에도 위크맵을 사용하면 메모리를 효율적으로 사용할 수 있을 것이다.


위크셋(WeakSet)

객체만을 키로 갖는 위크맵과 유사하게, 위크셋값으로 객체만을 저장할 수 있다. 위크셋 안의 객체는 도달 가능할 때만 메모리에서 유지된다. 즉, 위크셋 안의 객체 참조를 끊으면 가비지 컬렉션의 대상이 되어 추후에 제거될 것이다.

또, 위크맵과 마찬가지로 반복 작업이 불가능하다. 이런 불편함을 감수하고도, 객체를 기준으로 데이터를 추가하는 작업에 잔재되는 객체 데이터들을 지우는데 용이해 사용한다고 한다.

댓글남기기