[Javascript]얕은 복사와 깊은 복사에 대한 고찰

얕은 복사와 깊은 복사 제대로 알고 있는걸까?




자바스크립트 스터디를 참여하고 있다. 활용하고 있는 책은 ‘모던 자바스크립트 Deep Dive’. 스터디 방식은 각 주차에 해당하는 내용을 정리해서 올리고, 또 스터디원들이 돌아가며 챕터에 대해 발표하는 식으로 진행하게 되는데, 내 차례에 ‘원시값과 객체의 비교’를 맡게 되었다.

핵심이 되는 내용은 당연히 객체의 복사 문제……. 자바스크립트로 처음 개발을 접하는 비전공생이라면 한번쯤은 여기서 다들 덜그럭거려봤을 거라고 생각한다. (나만그랬나?) 그중에 발표 후에 질문 타임에서 얕은 복사에 대한 개념이 기존에 알고 있던 것과 다르다는 질문이 나왔다. 충분히 그렇게 생각할 수 있기에, 다시 한번 보자.

얕은 복사와 깊은 복사에 대한 모든 내용을 다룰 건 아니다. (아얘 처음 듣는 이야기라면 다른 글들을 보고 오자.)

const origin = {
  a: 1,
  b: {
    b1: "bbb",
    b2: 123,
  },
};

위와 같이 객체를 값으로 가지고 있는 origin을 다른 변수에 복사할 때, 객체를 복사하는 경우 객체는 참조값이 복사되는 동작 방식에 따라 얕은 복사(Shallow Copy)와 깊은 복사(Deep Copy)의 개념이 있다. 깊은 복사는 객체 전체 뎁스를 아울러 원 객체(origin)와의 연결성(참조 관계)가 완전히 끊어지는 복사이다.

의문은 객체를 다른 변수에 할당할때 이것을 얕은 복사라고 설명한 것에서 시작했다. 많은 블로그나 글들에서 얕은 복사를 가장 상위의 뎁스만 새로운 메모리에 생성되고, 하위의 객체들은 원 객체의 참조가 같이 연결되어 있는 복사라고 익히 들어왔다는 것, 그래서 이것은 얕은 복사가 아니라는 주장이었다. 과연 최상위 뎁스만 참조 관계가 끊기는 것만이 얕은 복사인걸까?

const origin = {
  a: 1,
  b: {
    b1: "bbb",
    b2: 123,
  },
};

const copy = origin; //그럼 얘는 뭔가?

위 경우 copyorigin의 값을 공유하게 된다. 결국 최상위 뎁스조차 관계가 동일한 경우인데, 앞서 얘기한 얕은 복사의 개념으로 따지자면, 위 코드는 얕은 복사가 아니다. 그러면 이게 할당인가? 일반 적인 할당처럼 완전히 새로운 값이 메모리에 저장된 것이 아님으로 할당으로 보기도 어렵다.

사실 따지자고 보면, copyorigin의 참조 값을 그대로 복사해서 자신의 메모리를 가지고 있다.

주소 1001 1002 1003 1004 1005 1006
    식별자:origin,
값:@5004
식별자:copy,
값:@5004
     
주소 5001 5002 5003 5004 5005 5006
‘bbb’ 1 @7004~5 7001~2 7001~2 123  
주소 7001 7002 7003 7004 7005 7006
식별자:a
값:@5002
식별자:b
값:@5003
  식별자:b1
값:@5001
식별자:b2
값:@5006
   

그럼 결국 copyorigin의 값 @5004를 복사했으므로, 위 코드는 복사로 보는게 맞다. 그러면 이제 앞서 얕은 복사의 개념에는 안맞는 것 같고, 그럼 깊은 복사란 말인가? origincopy는 서로 영향을 받는 관계이니 절대 깊은 복사가 될 수 없다. 그럼 뭐지?

copyorigin 식별자의 주소 값인 @1003을 처다보고(참조 값으로 가지고 있다)라고 얘기하는 사람들도 있지만, 식별자의 값으로 저장될 수 있는 메모리 주소는 데이터 값의 주소이다.

모던자바스크립트 딥다이브에서는 위 코드를 얕은 복사로 이야기한다. 즉, 얕은 복사란 최상위 뎁스는 참조 관계가 끊기고 하위 객체들만 참조 관계를 가진채로 복사된다는 개념이 아니라, 몇 뎁스이든, 일부만이든, 전체이든 서로 영향을 받는 참조 관계를 가지고 복사된 것을 말하는 것이다.

그럼 왜 얕은 복사를 ‘가장 상위의 뎁스만 새로운 메모리에 생성되고, 하위의 객체들은 원 객체의 참조가 같이 연결되어 있는 복사’라고 생각하는 것일까?

그건 흔히들 말하는 얕은 복사의 방법에서 야기된 것 같다.

const origin = {
  a: 1,
  b: {
    b1: "bbb",
    b2: 123,
  },
};

// 일반적으로 말하는 얕은 복사
const copy1 = { ...origin };
const copy2 = Object.assign({}, origin);

스프레드 연산자object.assign()을 보통 얕은 복사를 하는 방법이라고 말한다. 해당 결과를 보니 최상위 뎁스만 관계가 끊어지기 때문에 ‘최상위 뎁스의 관계는 끊어지고 나머지는 연결된게 얕은 복사’라는 생각이 된 것 같다. 그래서 copy = origin 같은 코드는 얕은 복사가 아니라고 하는 것이다. 물론 위 경우 해당 방법들은 얕은 복사가 맞다! 참조가 완전히 끊기지 않았으니까! 하지만 저 방법들이 무조건 얕은 복사인가? 다음의 경우를 보자

const origin = {
  a: 1,
  b: "b입니다",
};

const copy1 = { ...origin };
const copy2 = Object.assign({}, origin);

위 코드의 세개의 객체 중 어느 일부분을 아무리 바꿔도 나머지 두개의 객체는 영향을 받지 않는다. 완전히 관계가 끊긴 것이다. 그럼 여기서 스프레드 연산자와 object.assign()은 얕은 복사 방법인가? 이 경우에는 깊은 복사 방법이 되는 것이다!

따라서 얕은 복사인지, 깊은 복사인지는 방법이 정하는 것이 아니라 개념적인 것으로 정의 해야 된다고 볼 수 있다.

사실, 책에서도 다양한 시각으로 말을 한다. 어떻게 보느냐에 따라 정의가 달라진다는 것. 자바스크립트의 공식 문서에 얕은 복사라던지, 깊은 복사라던지에 대한 정의는 된 부분은 없다. 무조건으로 얕은 복사와 깊은 복사의 개념을 중첩된 객체의 복사에서만 쓰겠다! 라고 조건을 두면 스프레드 연산자object.assign() 방식이 얕은 복사가 되는 것이고,위 방법의 결과가 최상위 뎁스만 참조 관계가 끊어지는 것도 맞는 얘기일 것이다. 하지만 중첩된 객체를 = 으로 복사하는 것이라고 한다면, 최상위 뎁스 조건은 무너지게 된다.

물론, 객체가 할당된 변수를 다른 변수에 = 연산자로 연결하는 것을 할당도 아니고, 복사도 아니며, 대입이라고 말하는 사람도 있다. 거기에다가 얕은 복사, 깊은 복사 개념을 중첩된 객체에 한정해서 쓴다면 많이들 알고있는 얕은 복사의 개념도 맞는 말이다. 하지만 그런 경우가 아니라면, 얕은 복사에 대해서 다시 한번 면밀히 생각해볼 필요가 있는 것 같다.

Reference

모던 자바스크립트 deep dive