3.4.1.4 파이썬 최대 함정: 메모리 참조와 얕은/깊은 복사

학습목표

타 프로그래밍 언어에서 파이썬으로 넘어온 개발자들이 가장 많이 당황하고 피를 토하는 ‘리스트 메모리 참조(Reference)’ 현상을 완벽히 파악합니다. 원본 데이터를 보존하며 안전하게 복제판을 떠내는 슬라이싱 트릭과 얕은/깊은 복사(Shallow vs Deep Copy)의 원리를 이해합니다.


1. 🚨 리스트 메모리 주의보 (Shallow Copy Bug)

우리가 파이썬에서 a = [1, 2, 3] 이라고 적을 때, 파이썬은 변수 a 안에 [1, 2, 3]이라는 무거운 상자 전체를 집어넣지 않습니다. 대신, 저 멀리 안전한 창고(메모리힙) 공간에 [1, 2, 3] 상자를 하나 놔두고, 변수 a에게는 그 상자를 가리키는 빨간색 레이저 포인터(메모리 주소, Reference) 만 쥐여줍니다.

예제: 원본이 파괴되는 얕은 복사의 공포

이 특징을 모른 채, 아무 생각 없이 b = a 형태로 다른 변수에 백업본을 만들려다가 대참사가 발생합니다.

리스트 메모리 참조 참조 복사 함정 애니메이션

💡 다이어그램 해석: b = a 라는 명령어를 치는 순간, b 변수는 a의 리스트 도화지를 공들여 새롭게 복사(Ctrl+C)해 오지 않습니다! 단순히 a가 들고 있던 똑같은 레이저 포인터를 복사받아, 결국 a와 완전히 동일한 도화지를 노려보게 됩니다 (Shallow Copy). 이 상태에서 누군가 b[0] = 99 라고 값을 바꾸면, a는 건드리지도 않았는데 a[0]마저 99로 폭발해 버리며 치명적인 논리적 오류를 남깁니다.

a = [1, 2, 3]
b = a          # 🚨 위험! 데이터 복사가 아니라, 뇌파(주소)를 동기화한 것입니다.

b[0] = 99      # B가 자기 소유인 줄 알고 0번 방을 99로 바꿨더니...

print("b 리스트:", b) # [99, 2, 3] -> 당연히 변했습니다.
print("a 리스트:", a) # [99, 2, 3] -> 😱 충격! 건드리지도 않은 원본 a까지 변질되었습니다!

2. 해결책: 진짜 복사본 뜨기 (원본을 지키는 방패)

원본 a는 소중하게 지키면서 b가 마음껏 놀 수 있도록 하려면 어떻게 해야 할까요? a의 내용물 요소들만 똑같이 베껴서, 레이저 포인터가 가리키는 방향을 아예 다른 곳에 있는 새로운 도화지(독립된 메모리 방)로 만들어주어야 합니다.

방법 1: 가장 파이썬다운 슬라이싱 방어 [:]

가장 흔히 쓰이는 테크닉입니다. 시작점부터 끝점까지 전부 잘라서 ‘복사본’을 뱉어내는 슬라이싱의 특성을 이용하여 빈 방으로 던져줍니다.

a = [1, 2, 3]
b = a[:]       # [:] 는 "내용물만 똑같이 베껴서 아예 새 방을 파줘!" 라는 뜻입니다.

b[0] = 99
print("원본 a:", a) # [1, 2, 3] -> 복사본인 b가 난리를 쳐도 a는 평화롭습니다!
print("복사본 b:", b) # [99, 2, 3]

방법 2: 명시적인 .copy() 메서드

가독성을 높이기 위해 직관적인 함수를 호출할 수도 있습니다.

a = [1, 2, 3]
c = a.copy()   # 원리가 슬라이싱 [:] 과 완전히 같습니다.

c[1] = 88
print("원본 a:", a) # [1, 2, 3]
print("복사본 c:", c) # [1, 88, 3]

3. 심화: 이중 리스트와 깊은 복사 (Deep Copy)

하지만 얕은 복사([:].copy())마저 무력화되는 최악의 상황이 있습니다. 바로 “리스트 안에 또 리스트가 있는 경우(중첩 리스트)” 입니다. 얕은 복사는 가장 바깥쪽에 있는 껍데기 상자만 새로 만들 뿐, 그 상자 안에 들어있는 내부 리스트의 레이저 포인터까지 끊어주지는 못합니다.

이럴 때는 파이썬 기본 장착 모듈인 copy 라이브러리의 무적기 deepcopy() 를 꺼내야만 내부의 내부 메모리까지 완벽하게 분리 절단해 냅니다.

import copy

matrix_a = [[1, 2], [3, 4]]

# 1. 얕은 복사 실패 사례 (내부 연결이 끊기지 않아 원본이 또 파괴됨)
matrix_b = matrix_a.copy()
matrix_b[0][0] = 99
print("내부가 파괴된 matrix_a:", matrix_a) # [[99, 2], [3, 4]]

# 2. 깊은 복사 성공 사례 (완벽한 복제 클론 생성)
matrix_target = [[1, 2], [3, 4]]
matrix_c = copy.deepcopy(matrix_target)
matrix_c[0][0] = 99
print("안전하게 보호된 matrix_target:", matrix_target) # [[1, 2], [3, 4]]
서브목차