파트 1 - 4) 자료구조와 자료형 - 8)위크맵과 위크셋


위크맵과 위크셋 파트 링크 : https://ko.javascript.info/weakmap-weakset

자바스크립트 엔진은 도달 가능한 (그리고 추후 사용될 가능성이 있는) 값을 메모리에 유지한다. (참고: 가비지컬렉션 https://ko.javascript.info/garbage-collection)

예시

let john = { name: "John" }; // 참조

// 위 객체는 john이라는 참조를 통해 접근할 수 있다.

// 그런데 참조를 null로 덮어쓰면 위 객체에 더 이상 도달이 가능하지 않게 되어 
// 객체가 메모리에서 삭제된다.
john = null;

자료구조를 구성하는 요소(맵, 셋 등 자료구조를 구성하는 요소들)도 자신이 속한 자료구조가 메모리에 남아있는 동안 대개 도달 가능한 값으로 취급되어 메모리에서 삭제되지 않는다. 객체의 프로퍼티나 배열의 요소, 맵이나 셋을 구성하는 요소들이 이에 해당한다.

예를 들어 배열에 객체를 요소로 추가할 경우 배열이 메모리에 남아있는 한, 배열의 요소인 이 객체도 메모리에 남아있다. 이 객체를 참조하는 것이 아무것도 없더라도 배열이 메모리에 있기 때문에 이 객체 또한 메모리에 남아있다.

let john = { name: "John" };

let array = [ john ];

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

// john을 나타내는 객체는 배열(메모리에 남아있는 자료구조)의 요소이기 때문에 가비지 컬렉터의 대상이 되지 않는다.
// array[0]을 이용하면 해당 객체를 얻는 것도 가능하다.
alert(JSON.stringify(array[0]));

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

예시

let john = { name: "John" };

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

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

// john을 나타내는 객체는 맵 안에 저장되어있다.
// map.keys()를 이용하면 해당 객체를 얻는 것도 가능하다.
// 참조를 null로 덮어써도 맵 안에 저장된 객체 john은 그대로 존재하는 것.
for(let obj of map.keys()){
  alert(JSON.stringify(obj)); 
  // 맵에 저장된 객체의 key를 호출, {"name": "John"}
}

alert(map.size);

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

위크맵

맵과 위크맵의 첫 번째 차이위크맵의 키가 반드시 객체여야 한다는 점이다. 원시값은 위크맵의 키가 될 수 없다.

let weakMap = new WeakMap();

let obj = {};

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

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

위크맵의 키로 사용된 객체를 참조하는 것이 아무것도 없다면 해당 객체는 메모리와 위크맵에서 자동으로 삭제된다.
즉, 가비지 콜렉터의 대상이 된다.

let john = { name: "John" };

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

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

// john을 나타내는 객체는 이제 메모리에서 지워진다!
for(let obj of weakMap.keys()){
  alert(JSON.stringify(obj)); 
} // john 객체가 메모리에서 지워지고 위크맵에서도 자동으로 지워졌기 때문에 아무 동작도 일어나지 않는다.

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

맵과 위크맵의 두 번째 차이위크맵은 반복 작업과 keys(), values(), entries() 메서드를 지원하지 않는다.
따라서 위크맵에선 키나 값 전체를 얻는 게 불가능하다.

아래는 위크맵이 지원하는 메서드들이다.

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

위처럼 위크맵이 딱 4가지의 메서드만 지원하는 이유는 가비지 컬렉션의 동작 방식 때문이다. 위 예시의 john을 나타내는 객체처럼, 객체는 모든 참조를 잃게 되면(=도달할 수 없게 되면) 자동으로 가비지 컬렉션의 대상이 된다. 그런데 가비지 컬렉션의 동작 시점은 정확히 알 수 없다.

가비지 컬렉션이 일어나는 시점은 자바스크립트 엔진이 결정한다. 객체는 모든 참조를 잃었을 때, 그 즉시 메모리에서 삭제될 수도 있고, 다른 삭제 작업이 있을 때까지 대기하다가 함께 삭제될 수도 있다.
즉, 위크맵의 요소(객체)가 언제 삭제될지 정확하지 않기 때문에 현재 위크맵에 요소가 몇 개 있는지 정확히 파악하는 것 자체가 불가능한 것이다. 가비지 컬렉터가 한 번에 메모리를 청소할 수도 있고, 부분 부분 메모리를 청소할 수도 있으므로 위크맵의 요소(키/값) 전체를 대상으로 무언가를 하는 메서드는 동작 자체가 불가능하다.

아래는 위크맵을 활용할 수 있는 경우들이다.

1) 추가 데이터
위크맵은 부가적인 데이터를 저장할 곳이 필요할 때 그 진가를 발휘한다.

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

위크맵에 원하는 데이터를 저장하고, 이때 키는 객체를 사용하면 객체가 가비지 컬렉션의 대상이 될 때, 데이터도 함께 사라지게 된다.
객체가 참조를 모두 잃으면 해당 객체는 위크맵에서도, 객체 자체도 사라지니까.

weakMap.set(john, "비밀문서");
// john이 사망하면, 비밀문서는 자동으로 파기됩니다.

더 구체적인 예시로 아래에 사용자의 방문 횟수를 세어 주는 코드를 보자.
관련 정보는 맵에 저장하고 있는데 맵 요소의 키엔 특정 사용자를 나타내는 객체를, 값엔 해당 사용자의 방문 횟수를 저장하고 있다. 어떤 사용자의 정보를 저장할 필요가 없어지면(가비지 컬렉션의 대상이 되면) 해당 사용자의 방문 횟수도 저장할 필요가 없어진다.

아래 함수는 맵을 사용해 사용자의 방문 횟수를 센다.

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

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

아래는 John이라는 사용자가 방문했을 때, 어떻게 방문 횟수가 증가하는지를 보여준다.

// 📁 main.js
let john = { name: "John" };

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

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

이제 john을 나타내는 객체는 가비지 컬렉션의 대상이 되어야 하는데... 그럴리가 있나. john은 WeakMap이 아니라 Map(visitsCountMap)의 키로 사용되고 있어서 메모리에서 삭제되지 않는다.(초반에 나온 내용과 같이 자료구조를 구성하는 요소는 자신이 속한 자료구조가 메모리에 남아있는 동안 대개 도달 가능한 값으로 취급되어 메모리에서 삭제되지 않는다.)

특정 사용자를 나타내는 객체가 메모리에서 사라지면 해당 객체에 대한 정보(Map에 객체를 key로 하고 value에 저장된 방문 횟수)도 우리가 손수 지워줘야 한다. 이렇게 손수 지워주지 않으면 visitsCountMap가 차지하는 메모리 공간이 한없이 커질 것이다. 애플리케이션 구조가 복잡할 땐, 이렇게 쓸모 없는 데이터를 수동으로 비워주는 것도 복잡하다.

이런 문제는 위크맵을 사용해 예방할 수 있다.^^

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

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

위크맵을 사용해 사용자 방문 횟수를 저장하면 위크맵에 key로 저장된 객체가 메모리에서 사라질 때 visitsCountMap을 수동으로 청소해줄 필요가 없다.
john을 나타내는 객체가 도달 가능하지 않은 상태가 되면 위크맵의 key로 들어간 john과 value(john의 방문횟수)도 자동으로 가비지 컬렉션의 대상이 되어 메모리에서 삭제되기 때문이다.

2) 캐싱
위크맵은 캐싱(caching)이 필요할 때 유용하다. 캐싱은 시간이 오래 걸리는 작업의 결과를 저장해서 연산 시간과 비용을 절약해주는 기법이다. 캐싱의 예를 들면 동일한 함수를 여러 번 호출해야 할 때, 최초 호출 시 반환된 값을 어딘가에 저장해 놓았다가 그다음엔 함수를 호출하는 대신 저장된 값을 사용한다.

아래 예시는 함수 연산 결과를 맵에 저장하고 있다.(맵을 캐시처럼 사용)

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

// 연산을 수행하고 그 결과를 맵에 저장
function process(obj) {
  //cache에 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;

alert(cache.size); // 1 (객체가 여전히 cache에 남아있다! 메모리가 낭비되고 있음)

process(obj)를 여러 번 호출하면 최초 호출할 때만 연산이 수행되고, 그 이후엔 연산 결과를 cache에서 가져온다. 그런데 맵을 사용하고 있어서 객체가 필요 없어져도 cache를 수동으로 청소해 줘야 한다. 객체를 key로 사용하는 Map(cache)이 살아있으니까.

맵을 위크맵으로 교체하면 객체가 메모리에서 삭제되면, 캐시에 저장된 결과(함수 연산 결과) 역시 메모리에서 자동으로 삭제된다.
따라서 위와 같은 문제를 해결하려면 위크맵을 사용하면 된다.

// 📁 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;

// obj는 weakMap의 key로 사용되어 참조가 모두 사라지면 가비지 컬렉션의 대상이 되므로, 캐싱된 데이터 역시 메모리에서 삭제된다.
// 삭제가 진행되면 cache엔 그 어떤 요소도 남아있지 않게 된다.

위크셋(WeakSet)

위크셋은 셋과 유사한데, 객체만 저장할 수 있다는 점이 다르다. 원시값은 저장할 수 없다.
셋 안의 객체는 도달 가능할 때만 메모리에서 유지된다.
셋과 마찬가지로 위크셋이 지원하는 메서드는 많지 않다.
add, has, delete를 사용할 수 있고, size, keys()나 반복 작업 관련 메서드는 사용할 수 없다.
'위크’맵과 유사하게 '위크’셋도 부가적인 데이터를 저장할 때 사용할 수 있다. 다만, 위크셋엔 위크맵처럼 복잡한 데이터를 저장하지 않는다. 대신 "예"나 “아니오” 같은 간단한 답변을 얻는 용도로 사용된다.(당연히 객체의 형태로)

아래 코드는 사용자의 사이트 방문 여부를 추적하는 용도로 위크셋을 사용하는 예시이다.

let visitedSet = new WeakSet();

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

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

// visitedSet엔 두 명의 사용자가 저장된다. (set은 중복을 허용하지 않으니까)

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

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

john = null; //john의 모든 참조 없앰

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

위크맵과 위크셋의 가장 큰 단점은 반복 작업이 불가능하다는 점이다. 위크맵이나 위크셋에 저장된 자료를 한 번에 얻는 게 불가능하다. 반복 기능이 없다는 게 불편할 것 같아 보이지만, 위크맵과 위크셋을 이용해 할 수 있는 주요 작업을 방해하진 않는다. 위크맵과 위크셋은 객체와 함께 ‘추가’ 데이터를 저장하는 용도로 쓸 수 있다.

좋은 웹페이지 즐겨찾기