Shallow copy vs Deep copy

coin46·2020년 9월 8일
3

애시 당초에 제목에 대해서 빠르게 정리하려고했으나, 게으름으로 인해 지금에서야 정리하고자한다.

처음에 해당 내용을 조사했을 때, stackoverflow나 여러 블로그에서 많이들 다루고 있었다.

**그런데 대체 왜? Shallow copy vs Deep copy가 중요한데? **

라는 의문이 나중에 들기 시작했고, 여기서는 앞단에 다루어보고자 한다.

🚨 올바르지 않은 내용이 있을 경우 댓글로 남겨주시면 감사드리겠습니다.

왜 copy를 알아야하지?

다양한 여러가지 이유가 있겠지만, 그 이유 중에서 2가지를 뽑아보았다.

  1. 객체의 Immutability(변경불가성)
  1. Functional programming(함수형 프로그래밍)

위 2가지로 인해 copy를 알아야한다고 생각했다.

Immutability

객체는 기본적으로 mutable이다. 객체는 참조(reference) 형태로 전달하고 전달 받기 때문에,
객체가 참조를 통해 공유되어 있다면 그 상태가 언제든지 변경될 수 있기 때문에 문제가 될 가능성도 커지게 된다.

이 문제의 해결 방법은 비용은 조금 들지만 객체를 불변객체로 만들어 프로퍼티의 변경을 방지하는 것이다.

객체가 생성된 이후 그 상태를 변경할 수 없도록하는 디자인 패턴을 의미하는
객체의 Immutability(변경불가성)은 이 때문에 굉장히 중요하고,
Immutability은 아래 소개되는 함수형 프로그래밍의 핵심 원리이기도 하다.

Immutability이 중요한 이유는 스택오버플로우에 수 없이 나와있지만 3가지로 추려보았다.

  • Predictability - 예상치 못한 Side effect를 방지하고, 프로그램의 구조를 단순하게 유지하여,
    애플리케이션에 대해 더 쉽게 다룰 수 있게 해준다.
  • Performance - 불변 객체는 구조적 공유를 통해 메모리 오버 헤드를 줄여준다.
  • Mutation Tracking - 참조 및 값 동등성을 사용하여 애플리케이션을 최적화 할 수 있고,
    변경되는 것에 대한 쉬운 확인이 가능해진다.

만약 객체의 변경이 필요한 경우에는 객체의 복사(copy)를 통해
새로운 객체를 생성한 후 변경함으로써 기존 객체 불변성을 유지할 수 있다.

즉, 복사는 객체 불변성이란 중요한 디자인 패턴을 유지하기 위해 필요하다고 생각되어졌다.

Functional programming

최근 패러다임으로 자리잡은 함수형 프로그래밍은
순수함수(Pure function)을 통해 프로그래밍을 하는 것을 말한다.

여기서 가장 중요하게 다루어지는

순수 함수외부 스코프(외부 환경)와는 독립적으로 **
무조건
같은 input(arguments)을 받으면 같은 ouput(return)을 반환하는** 함수이다.

즉, 순수 함수는 외부 스코프의 데이터에 영향을 받지도,
또 데이터를 수정하지도 않아야 하기 때문에 어떠한 Side-effect를 발생시키지 않는다.

_결론적으로,
함수형 프로그래밍을 잘 구현하기 위해서는
=> 순수함수를 잘 구현해야하고
=> 그러기 위해 우리는 value(원시, 객체 모두 포함)의 의도치 않은 변형을 일으키지 않기위해
객체를 copy하는 방법을 알아야한다! _

참고로, Immutable.js는 List, Stack, Map, OrderedMap, Set, OrderedSet, Record와 같은 영구 불변 (Permit Immutable) 데이터 구조를 제공한다.

Shallow copy

Shallow copy는 1 depth의 객체를 복사하는 방식을 말한다.

spread 연산자 및 특정 복사 메소드를 통해 생성된 새로운 객체(A)는 복사당한(?)(B) 객체와
다른 메모리 주소 값을 참조하기 때문에 다른 값이 된다. (A !== B)

하지만 객체 내부 어느 한 메모리가, 또 다른 객체(C)를 참조 할 경우에 이는 같은 메모리 주소를 공유한다.
따라서 만약 복사한 A객체 내부의 C객체를 수정할 경우, B객체의 C객체도 함께 수정된다.

즉, shallow copy는 껍데기를 복사헀다고 이해하면 된다.
몇 가지 Shallow copy수행하는 메소드를 알아보자.

spread 연산자

const originalObject = {
  name: "kim",
  hobby: "soccer",
  age: 18,
  pet: { cat: "Pepe", dog: "messi" },
};

const cloneObject = { ...originalObject };

console.log(cloneObject); // {name: "kim", hobby: "soccer", age: 18, pet: {…}}
console.log(originalObject === cloneObject); // false

cloneObject.pet.cat = "Pipi";
console.log(originalObject.pet.cat); // "Pipi"
// cloneObject.pet과 originalObject.pet은 같은 객체를 참조.

const originalArray = [1, 2, 3, [4, 5]];
const cloneArray = [...originalArray];
console.log(cloneArray); // [1, 2, 3, Array(2)]
console.log(originalArray === cloneArray); // false

cloneArray[3][0] = 1;
console.log(originalArray[3][0]); // 1
// cloneArray[3]과 originalArray[3]은 같은 객체를 참조.

아래 메소드들 전부 동일하다.

Object.assign()

const originalObject = {
  name: "kim",
  hobby: "soccer",
  age: 18,
  pet: { cat: "Pepe", dog: "messi" },
};

const cloneObject = Object.assign({}, originalObject);

console.log(cloneObject); // {name: "kim", hobby: "soccer", age: 18, pet: {…}}
console.log(originalObject === cloneObject); // false

cloneObject.pet.cat = "Pipi";
console.log(originalObject.pet.cat); // "Pipi"
// cloneObject.pet과 originalObject.pet은 같은 객체를 참조.

Array.from()

const originalArray = [1, 2, 3, [4, 5]];
const cloneArray = Array.from(originalArray);
console.log(cloneArray); // [1, 2, 3, Array(2)]
console.log(originalArray === cloneArray); // false

cloneArray[3][0] = 1;
console.log(originalArray[3][0]); // 1
// cloneArray[3]과 originalArray[3]은 같은 객체를 참조.

Array.prototype.slice()

const originalArray = [1, 2, 3, [4, 5]];
const cloneArray = originalArray.slice();
console.log(cloneArray); // [1, 2, 3, Array(2)]
console.log(originalArray === cloneArray); // false

cloneArray[3][0] = 1;
console.log(originalArray[3][0]); // 1
// cloneArray[3]과 originalArray[3]은 같은 객체를 참조.

_결론적으로 shallow copy는 1depth의 객체는 clone하지만,
nested된 객체까지는 clone을 하지 못한다. _

Deep copy

Deep Copy는 1 depth의 객체 뿐만 아니라 객체가 담고 있는 또 다른 참조 객체도 copy하는 방식이다.
즉, 껍데기 뿐만 아니라 안에 있는 모든 객체들까지도 참조가 아닌 복사를 수행하는 것이다.

Using Recursion

기본적으로 재귀함수를 통해 원시값 뿐만 아니라 모든 깊이에 있는 객체까지 복사가 가능하다.
아래는 객체와 배열에 대해서만 깊은 클론을 반환하는 함수이다.

function clone(obj) {
  let copy;

  if (null === obj || typeof obj !== "object") return obj;
  if (obj instanceof Array) {
    copy = [];
    for (let i = 0; i < obj.length; i++) {
      copy[i] = clone(obj[i]);
    }
    return copy;
  }

  if (obj instanceof Object) {
    copy = {};
    for (var prop in obj) {
      if (obj.hasOwnProperty(prop)) {
        copy[prop] = clone(obj[prop]);
      }
    }
    return copy;
  }
}

const originalObject = { a: 1, b: { c: 1 } };
const cloneObject = clone(originalObject);
console.log(cloneObject === originalObject); // false

cloneObject.b.c = 2;
console.log(originalObject); //  {a: 1, b: {c: 1}} nested된 객체는 다른 주소값을 가지고 있음.

const originalArray = [1, 2, [1, 2, 3]];
const cloneArray = clone(originalArray);
console.log(cloneArray === originalArray); // false

cloneArray[2][0] = 2;
console.log(originalArray); //  [1,2,[2,2,3]] nested된 객체(배열)는 다른 주소값을 가지고 있음.

JSON

위처럼 함수를 직접 구현하지 않아도, 가장 쉬운 방법으로 JSON을 활용하여 간단하게 deep clone을 하는 방법이 있다.

const original = { a: 1, b: { c: [1] } };
const clone = JSON.parse(JSON.stringify(original));

clone.b.c[0] = 5;
console.log(original); // {a: 1, b: {c: [1]}}
console.log(clone); // {a: 1, b: {c: [5]}} nested된 객체(배열)는 다른 주소값을 가지고 있음.

하지만 JSON을 통한 깊은 복사는 특정한 데이터에 대해서 제대로 된 복사를 수행하지 못한다.

const original = {
  string: 'string',
  number: 123,
  bool: false,
  nul: null,
  date: new Date(),  // stringified
  undef: undefined,  // lost
  inf: Infinity,  // forced to 'null'
  func: function b() {} // lost
}

const clone = JSON.parse(JSON.stringify(original));

console.log(clone);
/* 
{
  bool: false
  date: "2020-09-09T01:59:37.025Z"
  inf: null
  nul: null
  number: 123
  string: "string"
} */

성능적으로도 JSON이 가장 떨어지는 것을 확인할 수 있다.

출처 - https://jsben.ch/2KRm3

그 외

그 외 아래 라이브러리와 프레임워크에서는 객체를 deep copy하는 기능을 제공한다고 한다.
제이쿼리는 쓸일이 없겠지만,, 차후에 lodash를 쓸 일이 있을 경우, 한번 써보면 좋을 것 같다.

  • lodash - cloneDeep
  • AngularJS - angular.copy
  • jQuery - jQuery.extend(true, { }, oldObject)

참고

profile
한가지를 알아도 제대로 알자

1개의 댓글

comment-user-thumbnail
2020년 11월 24일

찬중님 화이팅 블로그도 여유생기면 다시 써주세요

답글 달기