|

파이썬 얕은 복사와 깊은 복사: 리스트 복사 버그 정리

파이썬 얕은 복사와 깊은 복사 차이를 설명하는 대표 이미지
대입, 얕은 복사, 깊은 복사의 차이를 버그 중심으로 정리한다

파이썬 얕은 복사와 깊은 복사 차이는 처음에는 사소해 보이지만, 실제로는 리스트 복사 버그의 핵심 원인입니다. 특히 중첩 리스트를 다룰 때는 “분명 복사했는데 원본까지 바뀌는” 일이 정말 자주 생깁니다.

이 글에서는 버그 예시부터 시작해서 대입, list.copy(), copy.copy(), copy.deepcopy()가 어디까지 새 객체를 만들고 어디부터 공유하는지 단계적으로 정리하겠습니다.


먼저 버그 보기

아래 코드는 복사본인 copied만 수정하려는 의도처럼 보이지만, 실제로는 원본까지 같이 바뀝니다.

scores = [[80, 90], [70, 60]]
copied = scores[:]

copied[0][0] = 100

print("scores =", scores)
print("copied =", copied)
scores = [[100, 90], [70, 60]]
copied = [[100, 90], [70, 60]]

핵심은 scores[:]가 바깥 리스트는 새로 만들지만, 안쪽 리스트들까지 새로 만드는 것은 아니라는 점입니다. 즉, 바깥 껍데기만 새로 생겼고 내부 원소는 여전히 같은 객체를 같이 보고 있습니다.

리스트를 복사했는데 원본까지 같이 바뀌었다면, 대부분 내부의 mutable 객체를 얕게 공유하고 있는 상황입니다.


대입은 복사가 아니다

파이썬에서 = 는 복사가 아니라, 같은 객체를 다른 이름으로 가리키게 만드는 동작입니다. Python 공식 문서도 assignment statement는 객체를 복사하지 않고 binding을 만든다고 설명합니다.

a = [1, 2, 3]
b = a

b[0] = 99

print("a =", a)
print("b =", b)
a = [99, 2, 3]
b = [99, 2, 3]
  • b = a 는 복사가 아니다
  • b를 바꾸면 a도 같이 보일 수 있다
  • 독립된 사본이 필요하면 별도 복사를 해야 한다

얕은 복사

얕은 복사는 새 컨테이너를 하나 만들고, 그 안에 들어가는 원소는 기존 객체의 참조를 넣는 방식입니다. 즉, 바깥만 새로 만들고 안쪽은 같이 쓴다고 이해하면 쉽습니다.

a = [1, 2, 3]
b = a[:]

b[0] = 99

print("a =", a)
print("b =", b)
a = [1, 2, 3]
b = [99, 2, 3]

1차원 리스트에서는 이 정도 복사만으로 충분한 경우가 많습니다. 그래서 여기서 안심했다가, 다음 단계인 중첩 리스트에서 버그를 만나기 쉽습니다.


파이썬 얕은 복사와 깊은 복사 차이: 왜 중첩 리스트에서 버그가 자주 생길까

중첩 리스트에서는 얕은 복사가 안쪽 리스트까지 복사하지 않습니다. 그래서 겉보기에는 새 리스트처럼 보여도, 내부는 같은 객체를 같이 볼 수 있습니다.

a = [[1, 2], [3, 4]]
b = a[:]

b[0][0] = 99

print("a =", a)
print("b =", b)
a = [[99, 2], [3, 4]]
b = [[99, 2], [3, 4]]

겉보기에는 ab가 다른 리스트처럼 보이지만, a[0]b[0]은 같은 내부 리스트를 함께 보고 있습니다. 그래서 b[0][0]을 바꾸면 a[0][0]도 같이 바뀝니다.

  1. 바깥 리스트는 실제로 새로 만들어진다
  2. 안쪽 리스트는 새로 만들어지지 않는다
  3. 그래서 반은 복사되고 반은 공유되는 상태가 된다

복사 방법 비교

리스트 기준으로 보면 a[:], a.copy(), copy.copy(a)는 모두 기본적으로 shallow copy입니다. Python 문서도 list.copy()가 shallow copy이며 a[:]와 비슷하다고 설명합니다.

import copy

numbers = [[1, 2], [3, 4]]
a = numbers[:]
b = numbers.copy()
c = copy.copy(numbers)

a[0][0] = 99

print("numbers =", numbers)
print("a =", a)
print("b =", b)
print("c =", c)
numbers = [[99, 2], [3, 4]]
a = [[99, 2], [3, 4]]
b = [[99, 2], [3, 4]]
c = [[99, 2], [3, 4]]

실무에서는 리스트 하나를 가볍게 복사할 때는 list.copy() 또는 슬라이싱을 많이 쓰고, 타입을 일반적으로 다루면서 얕은 복사를 할 때는 copy.copy()를 씁니다. 하지만 중첩 객체를 다루는 의미에서는 셋 다 얕은 복사라는 점이 더 중요합니다.


깊은 복사

깊은 복사는 내부 객체까지 재귀적으로 새로 복사합니다. 그래서 중첩 리스트나 딕셔너리 안의 리스트처럼 안쪽 mutable 객체가 있을 때 의미가 커집니다.

import copy

a = [[1, 2], [3, 4]]
b = copy.deepcopy(a)

b[0][0] = 99

print("a =", a)
print("b =", b)
a = [[1, 2], [3, 4]]
b = [[99, 2], [3, 4]]

이번에는 원본 a가 바뀌지 않습니다. 안쪽 리스트까지 새로 복사되었기 때문입니다. 그래서 중첩 구조를 완전히 분리하고 싶다면 copy.deepcopy()가 가장 확실합니다.


copy.copy vs deepcopy

copy.copy

copy.copy()는 얕은 복사입니다. 새 바깥 객체는 만들지만, 그 안의 내부 객체는 공유합니다.

copy.deepcopy

copy.deepcopy()는 깊은 복사입니다. 내부 객체까지 재귀적으로 새로 복사해서 가능한 한 독립된 구조를 만듭니다. 공식 문서도 deep copy는 유용하지만, 원래 공유되어야 할 데이터까지 복사하면 과할 수 있다고 설명합니다.


언제 얕은 복사면 될까

  • 1차원 리스트처럼 내부에 mutable 컨테이너가 없을 때
  • 안쪽 객체를 수정하지 않고 바깥 구조만 새로 만들면 될 때
  • 내부 객체를 의도적으로 공유해도 의미상 안전할 때

실무에서는 내가 바꾸려는 것이 바깥 컨테이너뿐인지, 안쪽 객체까지 수정할 가능성이 있는지를 먼저 보면 판단이 빨라집니다. 안쪽 객체를 건드리지 않는다면 얕은 복사면 충분한 경우가 많습니다.


언제 deepcopy가 필요한가

  • 리스트 안에 리스트, 딕셔너리, set 같은 mutable 객체가 또 들어 있을 때
  • 복사본을 수정하는 동안 원본이 절대 바뀌면 안 될 때
  • 테스트 데이터, 백트래킹 상태, 임시 시뮬레이션처럼 완전 분리가 중요할 때
import copy

original = [{"name": "Kim", "scores": [80, 90]}]
shallow = copy.copy(original)
deep = copy.deepcopy(original)

shallow[0]["scores"].append(100)

print("original =", original)
print("shallow =", shallow)
print("deep =", deep)
original = [{'name': 'Kim', 'scores': [80, 90, 100]}]
shallow = [{'name': 'Kim', 'scores': [80, 90, 100]}]
deep = [{'name': 'Kim', 'scores': [80, 90]}]

이 예시는 실무 버그와 매우 비슷합니다. 복사본에서 점수만 추가한다고 생각했는데 원본 사용자 데이터도 함께 오염됩니다. 이런 상황에서는 deepcopy()가 안전장치 역할을 합니다.


자주 하는 오해

  1. = 도 복사라고 생각하는 오해
  2. list.copy()면 완전히 분리된다고 생각하는 오해
  3. deepcopy()가 항상 더 좋은 선택이라고 생각하는 오해
  4. 출력만 보고 객체 관계를 다 알 수 있다고 생각하는 오해

겉보기에는 같은 값처럼 보여도, 어떤 부분은 공유되고 어떤 부분은 새로 만들어졌을 수 있습니다. 그래서 중첩 구조를 다룰 때는 값이 같은가보다 어디까지 독립적이어야 하는가를 먼저 보는 편이 안전합니다.


선택 기준

  • = → 복사 아님, 같은 객체를 함께 봄
  • 얕은 복사 → 바깥만 새로 만들고 안쪽 객체는 공유
  • 깊은 복사 → 안쪽 객체까지 재귀적으로 복사

안쪽 mutable 객체를 수정할 가능성이 있으면 shallow copy를 의심하고, 원본과 완전 분리가 필요하면 deepcopy를 검토하면 됩니다.


마무리

파이썬 얕은 복사와 깊은 복사 차이는 용어 암기보다 버그 감각으로 이해하는 것이 훨씬 중요합니다. 먼저 = 는 복사가 아니라는 점을 잡고, 그다음에 얕은 복사는 어디까지 공유하는지 이해하면 대부분의 혼란이 정리됩니다.

기본 자료형과 비교 감각을 함께 정리하고 싶다면 파이썬 is와 == 차이, 파이썬 리스트와 튜플 차이 글도 같이 읽어보면 도움이 됩니다. 공식 문서 기준 설명이 더 필요하다면 Python의 copy 모듈 문서리스트 자료구조 튜토리얼도 함께 참고해 보세요.

함께보면 좋은 글