Published on

깊은 복사

Overview

Introduction

이전 글에서는 매개변수를 변경하는 비순수 함수를 조금 더 안전하게 만드는 방법에 대해서 알아보았다

사용자에게 사이드 이펙트를 알리는 건 안 하는 것보다는 낫지만 근본적인 해결책은 되지 않는다

여전히 함수를 사용하는 사람에게 책임을 떠넘기고 있다

그렇다면 어떻게 해야 함수를 안전하게 만들 수 있을까?

애초에 사이드 이펙트를 일으키지 않으면 된다!

Copy Object

문제가 발생하는 이유는 함수 내부와 바깥이 한 객체를 공유하기 때문이다

따라서 함수 내부에서 객체를 복사해서 쓰면 문제가 발생하지 않는다

/**
@param { [number, number] } point board 상 현재 위치
@param { number[][] } board 탐색할 배열
@param { boolean[][] } visited 방문 체크
@returns { number } 탐색한 cell의 수
 */
const dfs = (point, board, visited) => {
+  // 변경할 객체를 복사
+  const visitedClone = copy(visited);
  const [r, c] = point;
  // 이미 방문했거나 갈 수 없는 곳이면 탐색 종료
-  if (visited[r][c] || !board[r][c]) return 0;
+  if (visitedClone[r][c] || !board[r][c]) return 0;

  // ⚠ 변경 발생
-  visited[r][c] = true;
+  visitedClone[r][c] = true;

  // 움직일 수 있는 쪽으로 탐색 진행
  const count = [directions]
-    .map((next) => dfs(next, board, visited))
+    .map((next) => dfs(next, board, visitedClone))
    .reduce((acc, curr) => acc + curr, 0);

  return count + 1;
}

Deep Clone

가장 간단하고 안전한 방법은 객체를 통째로 복사하는 것이다

깊은 복사를 하는 방법은 여러 가지가 있다

샘플 객체를 가지고 성능을 비교해보자

실험 환경: jdoodle.com에서 제공하는 node.js 17.1.0

// 샘플 객체를 만드는 함수
// 2^20개의 원소가 있고 깊이가 20인 트리
const createSample = (depth = 0, init = {}) => {
  if (depth === 20) return [];
  init[0] = createSample(depth + 1);
  init[9] = createSample(depth + 1);
  return init;
};

JSON 문자열

JSON.stringify로 객체를 JSON 문자열로 만들고 JSON.parse로 새로운 객체를 만들 수 있다

const copyUsingJSON = (obj) => JSON.parse(JSON.stringify(obj));

// test

const obj = { a: 1, b: { c: 100 } };

const cloned = copyUsingJSON(obj);

assert.notEqual(obj, cloned);

cloned[b][c] = 0;

assert.notEqual(obj[b][c], cloned[b][c]);
// performance

const sample = createSample();

console.time('json');

const copied = copyUsingJSON(sample);

console.timeEnd('json'); // json: 1.253s

JSON 문자열을 이용한 깊은 복사는 1.253s 걸렸다

Structured Clone

Chrome 98, Node.js 17부터 지원하는 전역함수 structuredClone을 사용할 수도 있다

const obj = { a: 1, b: { c: 100 } };

const cloned = structuredClone(obj);

assert.notEqual(obj, cloned);

cloned[b][c] = 0;

assert.notEqual(obj[b][c], cloned[b][c]);
// performance

const sample = createSample();

console.time('structuredClone');

const copied = structuredClone(sample);

console.timeEnd('structuredClone'); // structuredClone: 2.549s

structuredClone은 2.549s 걸렸다

Recursive Shallow Copy

앞에서 설명한 방법들은 깊은 복사만 하는 게 아니기 때문에 다소 느리다

속도가 중요하다면 검증된 라이브러리를 이용하거나 재귀함수를 이용해 얕은 복사를 반복하면 된다

/**
단순 재귀 복사
얕은 복사를 반복
@template T
@param {T} obj
@returns {T} */
const recursiveShallowClone = (obj) => {
  if (typeof obj !== 'object' || obj === null) return obj;
  const cloned = new obj.constructor();
  for (const key in obj) cloned[key] = recursiveShallowClone(obj[key]);
  return cloned;
};

// test
const obj = { a: 1, b: { c: 100 } };

const cloned = recursiveShallowClone(obj);

assert.notEqual(obj, cloned);

cloned[b][c] = 0;

assert.notEqual(obj[b][c], cloned[b][c]);
// performance

const sample = createSample();

console.time('recursiveShallowClone');

const copied = recursiveShallowClone(sample);

console.timeEnd('recursiveShallowClone'); // recursiveShallowClone: 1.207s

재귀 얕은 복사는 1.207s 걸렸다

Conclusion

방식걸린 시간(ms)
recursion1207.0
JSON1253.0
structuredClone2549.0

단순 재귀 복사는 JSON 방식에 비해 근소하게 빨랐다

JSON 방식은 structuredClone보다 약 2배 빨랐다

structuredClone이 느린 이유는 다른 방식들보다 많은 작업을 하기 때문이다

예를 들어 structuredClone은 순환참조가 있는 객체도 복사할 수 있다

const circular = { apple: 1 };
// 자기 자신을 참고하는 객체
circular.banana = circular;

const clonedByJSON = JSON.parse(JSON.stringify(circular)); // 오류
const clonedByRecursion = recursiveShallowClone(circular); // 오류
const clonedByStructuredClone = structuredClone(circular); // 성공

반면 함수는 복사하지 못한다

const instance = new (class {
  constructor() {
    this.name = 'asd';
    this.obj = { apple: [1, 2, 3] };
  }
  log() {
    console.log(this.obj);
  }
})();

const clonedByJSON = JSON.parse(JSON.stringify(circular));
clonedByJSON.log(); // Error: clonedByJSON.log is not a function
const clonedByRecursion = recursiveShallowClone(circular);
clonedByRecursion.log(); // 성공. 그러나 이 경우 log 함수는 원본과 같은 함수다
const clonedByStructuredClone = structuredClone(circular);
clonedByStructuredClone.log(); // Error: clonedByJSON.log is not a function

References