얕은 복사와 깊은 복사는 프로그래밍에서 객체를 복사하는 두 가지 방법입니다.

 

얕은 복사(shallow copy)

얕은 복사란 원본 객체의 주소를 복사하여 새로운 객체를 생성하는 방식입니다.

원본 객체와 복사된 객체가 같은 메모리를 참조하므로 한쪽의 객체를 변경하면 다른 쪽의 객체도 함께 변경됩니다.

old_list = [[1, 2, 3], [4, 5, 6], [7, 8, 'a']]
new_list = old_list.copy()

new_list[2][2] = 9

print(f"{old_list=}, {new_list=}")
print(f"{id(old_list)=}, {id(new_list)=}")
print(f"{id(old_list[0])=}, {id(new_list[0])=}")

old_list를 중첩 리스트(nested list)로 만들었습니다. 그 후, 얕은 복사로 new_list에 변수 간 대입을 했고, new_list[2][2]=9를 통해, new_list 내부 3번째 리스트 [7, 8, 'a']에서 세 번째 원소인 'a'를 9로 변경했습니다. 그리고 old_list와 new_list를 출력해보겠습니다.

얕은 복사의 한계

new_list의 값만 변경했는데, old_list[2][2]도 9로 변경되었습니다. id(old_list)와 id(new_list)의 결과를 보면, 변수 old_list와 new_list가 가리키는 메모리 주소도 서로 다릅니다. 왜 그런 것일까요? old_list[2]와 new_list[2]가 가리키는 주소는 똑같다는 것을 알 수 있습니다. 즉, 파이썬에서 mutable 변수 내부에 또 mutable이 있는 경우, 얕은 복사로는 mutable 내부의 mutable의 메모리 주소는 달라지지 않는다는 것입니다.

 

만약 중첩 리스트나 중첩 딕셔너리 등의 복잡한 구조를 가진 mutable 변수에 대해 변수 간 대입을 하는 경우 얕은 복사로는 변수 간 독립성을 보장하지 않습니다. 이때 필요한 것이 바로 깊은 복사입니다.

깊은 복사(deep copy)

깊은 복사는 원본 객체의 모든 내용을 새로운 메모리 공간에 복사하여 새로운 객체를 생성하는 방식입니다.

원본 객체와 복사된 객체는 서로 독립된 메모리 공간을 참조하게 되므로 한 쪽의 객체를 변경해도 다른 쪽의 객체는 영향을 받지 않습니다.

import copy

old_list = [[1, 1, 1], [2, 2, 2], [3, 3, 3]]
new_list = copy.deepcopy(old_list)
new_list[2][2] = 9

print(f"{old_list=}, {new_list=}")
print(f"{id(old_list)=}, {id(new_list)=}")
print(f"{id(old_list[2])=}, {id(new_list[2])=}")

위와 마찬가지로 old_list는 중첩 리스트입니다. 위와 다른 점은 copy 모듈을 import했고, new_list = copy.deepcopy(old_list)를 통해 깊은 복사를 했습니다. new_list[2][2]를 9로 바꾼 뒤 출력해보겠습니다.

결과를 보면 old_list[2][2]는 변경되지 않고 독립성이 유지된 것을 볼 수 있습니다. 변수의 메모리 주소를 보면 old_list와 new_list의 메모리 주소도 다르고, old_list[2]와 new_list[2]의 메모리 주소도 다릅니다. 

즉, 중첩된 mutable 변수에 대해서도 완전히 독립성이 유지되는 것을 확인할 수 있습니다.

알게 된 사실

프로그래밍할 때 리스트를 자주 사용하곤 하는데 인덱스를 통해 접근해 리스트 값을 휙휙 변경했었다.

얕은 복사, 깊은 복사 개념을 학습하면서 이런 습관이 굉장히 위험하다는 것을 깨달았고 특히 중첩 리스트나 중첩 딕셔너리 같은 데이터를 다룰 경우 데이터에 변경점이 발생할 경우 깊은 복사를 적절히 활용해야 된다는 것을 알게 되었다. 

복사했습니다!