Study JavaScript 0525 - 1hr WeakMap & WeakSet

변승훈·2022년 5월 25일
0

Study JavaScript

목록 보기
21/43

WeakMap & WeakSet

"가비지 컬렉션"에서 배웠듯이 javascript엔진은 도달 가능한 값(추후 사용될 가능서잉 있는)을 메모리에 유지한다.
자료구조를 구성하는 요소도 자신이 속한 자료구조가 메모리에 남아있는 동안 대개 도달 가능한 값으로 취급되어 메모리에서 삭제되지 않는다.
객체의 프로퍼티나 배열의 요소, map이나 set을 구성하는 요소들이 이에 해당 된다.

아래의 코드를 통해 배열에 객체를 추가하여 배열이 메모리에 남아있는 한, 배열의 요소인 이 객체를 참조하는 것이 아무것도 없어도 메모리에 남아있게 되는 것을 확인해보자.

let john = { name: "John" };

let array = [ john ];

john = null; // 참조를 null로 덮어씀

// john을 나타내는 객체는 배열의 요소이기 때문에 가비지 컬렉터의 대상이 되지 않는다.
// array[0]을 이용하면 해당 객체를 얻는 것도 가능하다.
console.log(JSON.stringify(array[0]));

map에서 객체를 key로 사용한 경우도 역시, map이 메모리에 있는 한 객체도 메모리에 남는다. 가비지 컬렉터의 대상이 되지 않는다.

let john = { name: "John" };

let map = new Map();
map.set(john, "...");

john = null; // 참조를 null로 덮어씀

// john을 나타내는 객체는 맵 안에 저장
// map.keys()를 이용하면 해당 객체를 얻는 것도 가능하다.
for(let obj of map.keys()){
  console.log(JSON.stringify(obj));
}

console.log(map.size);

이러한 관점에서 WeakMap은 일반 map과 전혀 다른 양상을 보인다.
WeakMap을 사용하면 key로 쓰인 객체가 가비지 컬렉션의 대상이 된다.

1. WeakMap

1-1. map과 WeakMap의 차이

  1. WeakMap의 key가 반드시 객체여야 하며 원시 값은 WeakMap의 key가 될 수 없다.
let weakMap = new WeakMap();

let obj = {};

weakMap.set(obj, "ok"); //정상적으로 동작(객체 키).

// 문자열("test")은 키로 사용할 수 없다.
weakMap.set("test", "Whoops"); // Error: Invalid value used as weak map key

WeakMap의 키로 사용된 객체를 참조하는 것이 아무것도 없다면 해당 객체는 메모리와 WeakMap의 자동으로 삭제된다.

let john = { name: "John" };

let weakMap = new WeakMap();
weakMap.set(john, "...");

john = null; // 참조를 덮어씀

// john을 나타내는 객체는 이제 메모리에서 지워진다!

위에서 john을 나타내는 객체는 오로지 WeakMap의 key로만 사용되고 있으므로, 참조를 덮어쓰게 되면 이 객체는 WeakMap과 메모리에서 자동으로 살제된다.

  1. WeakMap은 반복 작업과 keys(), values(), entries() 메소드를 지원하지 않는다. 따라서 WeakMap에선 key나 값 전체를 얻는 게 불가능합니다.
    WeakMap이 지원하는 메소드는 아래와 같다.
  • weakMap.get(key)
  • weakMap.set(key, value)
  • weakMap.delete(key)
  • weakMap.has(key)

WeakMap에서 지원하는 메소드가 적은 이뉴는 가비지 컬렉션의 동작 방식 때문이다.
가비지 컬렉션이 일어나는 시점은 javascript엔진이 결정하며 객체가 모든 참조를 잃었을 때, 그 즉시 메모리에서 삭제될 수도 있고, 다른 삭제 작업이 있을 때 까지 대기하다가 함께 삭제될 수도 있다.
즉, 가비지 컬렉터가 한 번에 메모리를 청소하거나 부분 부분 청소할 수도 있으므로 WeakMap의 요소(key/value) 전체를 대상으로 무언가를 하는 메소드는 동작이 불가능하다.

1-2. UseCase: 추가 데이터

WeakMap은 부차적인 데이터를 저장할 곳이 필요할 때 진가를 발휘한다.

서드파티 라이브러리와 같은 외부 코드에 ‘속한’ 객체를 가지고 작업을 해야 한다고 가정해 보자. 이 객체에 데이터를 추가해줘야 하는데, 추가해 줄 데이터는 객체가 살아있는 동안에만 유효한 상황이다. 이럴 때 WeakMap을 사용할 수 있다.

WeakMap에 원하는 데이터를 저장하고, 이때 key는 객체를 사용하면 된다. 이렇게 하면 객체가 가비지 컬렉션의 대상이 될 때, 데이터도 함께 사라지게 된다.

예시를 들어보자. 방뭄 횟수를 세어주는 코드가 있는데 관련 정보를 map에 저장해보자.
key엔 특정 사용자를 나타내는 객체를, value엔 해당 사용자의 방문 횟수를 저장한다.
특정 사용자의 정보를 저장할 필요가 없어진다면(가비지 컬렉션의 대상이 되면) 해당 사용자의 방문 횟수도 저장할 필요가 없어진다.

// visitsCount.js
let visitsCountMap = new Map(); // 맵에 사용자의 방문 횟수를 저장함

// 사용자가 방문하면 방문 횟수를 늘려준다.
function countUser(user) {
  let count = visitsCountMap.get(user) || 0;
  visitsCountMap.set(user, count + 1);
}
// main.js
let john = { name: "John" };

countUser(john); // John의 방문 횟수를 증가시킵니다.

// John의 방문 횟수를 셀 필요가 없어지면 아래와 같이 john을 null로 덮어쓴다.
john = null;

위에서 map을 사용했기 때문에 join은 가비지 컬렉션의 대상이 되지 않고 메모리에서 삭제되지 않는다.
규모가 큰 프로젝트에서 이렇게 쓸모 없어진 데이터를 수동으로 비워주는 것은 힘들기 때문에 WeakMap을 사용해서 사전에 예방할 수 있다.

// visitsCount.js
let visitsCountMap = new WeakMap(); // 위크맵에 사용자의 방문 횟수를 저장함

// 사용자가 방문하면 방문 횟수를 늘려줍니다.
function countUser(user) {
  let count = visitsCountMap.get(user) || 0;
  visitsCountMap.set(user, count + 1);
}

이렇게 WeakMap을 사용하면 수동으로 visitsCountMap을 청소해줄 필요가 없어진다.

1-3. UseCase: Caching

WeakMap은 Caching이 필요할 때 유용하다.
Caching은 시간이 오래 걸리는 작업의 결과를 저장해서 연산 시간과 비용을 절약해주는 기법이다.
동일한 함수를 여러 번 호출해야 할 때, 최초 호출시 반환된 값을 어딘가에 저장해 놓았다가 함수를 호출하는 대신 저장된 값을 사용한다.

// cache.js
let cache = new Map();

// 연산을 수행하고 그 결과를 맵에 저장
function process(obj) {
  if (!cache.has(obj)) {
    let result = /* 연산 수행 */ obj;

    cache.set(obj, result);
  }

  return cache.get(obj);
}

// 함수 process()를 호출해보자

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

let result1 = process(obj); // 함수를 호출

// 동일한 함수를 두 번째 호출할 땐,
let result2 = process(obj); // 연산을 수행할 필요 없이 맵에 저장된 결과를 가져오면 된다.

// 객체가 쓸모없어지면 아래와 같이 null로 덮어쓴다.
obj = null;

console.log(cache.size); // 1 (그런데 객체가 여전히 cache에 남아있다. 메모리가 낭비되고 있는 것이다.)

위의 예시에서 process(obj)를 여러번 호출하면 최초 호출 시에만 연산이 수행되고, 그 이후엔 연산 결과를 Cache에서 가져온다.
그런데 map을 사용하고 있어서 객체가 필요 없어저도 Cache를 수동으로 청소해줘야 한다.

이를 아래에서 WeakMap을 사용하여 해결해보자.

// cache.js
let cache = new WeakMap();

// 연산을 수행하고 그 결과를 위크맵에 저장
function process(obj) {
  if (!cache.has(obj)) {
    let result = /* 연산 수행 */ obj;

    cache.set(obj, result);
  }

  return cache.get(obj);
}

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

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

// 객체가 쓸모없어지면 아래와 같이 null로 덮어쓴다..
obj = null;

// 이 예시에선 맵을 사용한 예시처럼 cache.size를 사용할 수 없다.
// 하지만 obj가 가비지 컬렉션의 대상이 되므로, 캐싱된 데이터 역시 메모리에서 삭제될 것이다.
// 삭제가 진행되면 cache엔 그 어떤 요소도 남아있지 않을 것이다.

2. WeakSet

  • WeakSet은 set과 유사한데, 객체만 저장할 수 있고, 원시값은 저장할 수 없다.
  • set 안의 객체는 도달 가능할 때만 메모리에서 유지된다.
  • set과 마찬가지로 WeakSet이 지원하는 메소드는 적다. add, has, delete를 사용할 수 있고, size, keys()나 반복 작업 관련 메서드는 사용할 수 없다.

WeakSet은 WeakMap과 유사하게 부차적인 데이터를 저장할 때 사용할 수 있다. 다만 WeakSet은 WeakMap처럼 복잡한 데이터를 저장하지 않는다.
"예", "아니오" 같은 간단한 답변을 얻는 용도로 사용된다.

let visitedSet = new WeakSet();

let john = { name: "John" };
let pete = { name: "Pete" };
let mary = { name: "Mary" };

visitedSet.add(john); // John이 사이트를 방문
visitedSet.add(pete); // 이어서 Pete가 사이트를 방문
visitedSet.add(john); // 이어서 John이 다시 사이트를 방문

// visitedSet엔 두 명의 사용자가 저장될 것이다.

// John의 방문 여부를 확인
console.log(visitedSet.has(john)); // true

// Mary의 방문 여부를 확인
console.log(visitedSet.has(mary)); // false

john = null;

// visitedSet에서 john을 나타내는 객체가 자동으로 삭제된다.

WeakMap과 WeakSet의 가장 큰 단점은 반복 작업이 불가능하다는 점이다.
WeakMap과 WeakSet에 저장된 자료를 한 번에 얻는게 불가능하다.
WeakMap과 WeakSet은 객체와 합께 "추가" 데이터를 저장하는 용도로 쓸 수 있다.

profile
잘 할 수 있는 개발자가 되기 위하여

0개의 댓글