지난 편에서는 대용량 데이터를 다룰 때 메모리 낭비를 줄이고 게으른 연산(Lazy Evaluation)을 가능하게 하는 이터레이터와 제너레이터의 활용법을 다루었습니다. 파이썬은 C나 C++ 같은 언어와 달리 개발자가 직접 malloc()이나 free() 명령어로 메모리를 할당하고 해제할 필요가 없습니다. 파이썬의 메모리 관리 시스템이 뒤에서 알아서 쓰이지 않는 객체를 정리해 주기 때문입니다.
하지만 이 자동화 뒤에 숨겨진 파이썬의 메모리 참조(Reference) 방식을 제대로 이해하지 못하면 뜻밖의 치명적인 문제에 직면하게 됩니다. 리스트나 객체를 복사해서 A의 값을 수정했는데 B의 값까지 동시에 수정되어 데이터가 오염되거나, 더 이상 쓰지 않는 객체가 메모리에 계속 남아 RAM을 갈아먹는 현상이 대표적입니다.
저 역시 초보 시절, 원본 리스트를 보존하고 복사본만 수정할 생각으로 list_b = list_a나 copy()를 썼다가 원본 데이터까지 싹 바뀌어 몇 시간 동안 작성한 데이터 분석 결과를 날려 먹었던 뼈아픈 경험이 있습니다. 오늘은 파이썬 뒤편에서 작동하는 가비지 컬렉션(Garbage Collection)의 원리와 얕은 복사(Shallow Copy) vs 깊은 복사(Deep Copy)의 함정을 완전히 정복해 보겠습니다.
1. 파이썬 변수의 본질: 상자가 아니라 '표찰(Tag)'이다
많은 입문자가 변수를 "데이터를 담는 상자"로 오해하곤 합니다. 하지만 파이썬에서 모든 것은 객체이며, 변수는 실제 객체가 위치한 메모리 주소를 가리키는 '이름표(참조 / Reference)'에 불과합니다.
a = [1, 2, 3]
b = a # 객체를 복사한 것이 아니라, a가 가리키던 동일한 메모리 주소 표찰을 b에도 붙인 것!
b.append(4)
print(a) # 출력: [1, 2, 3, 4] (a도 함께 바뀜!)
b = a는 복사가 아니라 메모리 주소 공유(Aliasing)일 뿐입니다. 이처럼 파이썬에서 복사를 다룰 때는 항상 메모리의 참조 상태를 명확히 의식해야 합니다.
2. 얕은 복사(Shallow Copy) vs 깊은 복사(Deep Copy)의 함정
그렇다면 원본과 독립된 진짜 복사본을 만들려면 어떻게 해야 할까요? 여기서 얕은 복사와 깊은 복사의 차이가 발생합니다.
1) 얕은 복사 (Shallow Copy)
가장 겉면의 껍데기 객체만 새로 만들고, 내부에 들어있는 요소(중첩된 객체)들은 기존 메모리 주소를 그대로 참조하는 복사 방식입니다.
만드는 방법:
list.copy(), 슬라이싱a[:],copy.copy(a)
import copy
# 중첩된 리스트 (2차원 리스트)
original = [[1, 2], [3, 4]]
shallow = copy.copy(original) # 얕은 복사 실행
# 1. 껍데기 요소 추가
shallow.append([5, 6])
print(original) # 출력: [[1, 2], [3, 4]] (영향 없음)
# 2. 내부 중첩 요소 수정 (함정 발생!)
shallow[0][0] = 99
print(original) # 출력: [[99, 2], [3, 4]] (원본의 내부 데이터까지 함께 바뀜!)
1차원 구조에서는 얕은 복사로도 독립성이 유지되는 것처럼 보이지만, 리스트 안에 리스트나 객체가 포함된 다차원/중첩 구조에서는 내부에 위치한 메모리 주소를 여전히 공유하고 있으므로 원본이 오염되는 함정에 빠지게 됩니다.
2) 깊은 복사 (Deep Copy)
가장 겉면의 객체뿐만 아니라, 내부에 재귀적으로 얽혀있는 모든 중첩 객체들까지 메모리상에 완벽하게 새로 복제하는 방식입니다.
만드는 방법:
copy.deepcopy(a)
original = [[1, 2], [3, 4]]
deep = copy.deepcopy(original) # 깊은 복사 실행
deep[0][0] = 99
print(original) # 출력: [[1, 2], [3, 4]] (원본이 완벽하게 보호됨!)
중첩된 객체나 클래스 인스턴스 내부의 속성까지 원본과 완전히 분리된 완벽한 독립 객체를 만들고 싶다면 반드시 copy.deepcopy()를 사용해야 합니다.
3. 파이썬 메모리 청소부: 레퍼런스 카운팅과 가비지 컬렉션(GC)
그렇다면 사용이 끝난 메모리 객체들은 언제 어떻게 삭제될까요? 파이썬은 주로 두 가지 메커니즘으로 메모리를 자동 관리합니다.
1) 레퍼런스 카운팅 (Reference Counting)
파이썬의 주요 메모리 관리 방식입니다. "어떤 객체를 가리키고 있는 참조(변수)의 개수"를 실시간으로 세는 방식입니다.
객체가 생성되거나 변수에 할당되면 카운트가 1 증가합니다.
변수가 삭제(
del)되거나 스코프(함수)를 벗어나면 카운트가 1 감소합니다.레퍼런스 카운트가 0이 되는 순간, 파이썬은 그 즉시 해당 객체를 메모리에서 완전히 삭제합니다.
import sys
x = [1, 2, 3]
print(sys.getrefcount(x) - 1) # 참조 카운트 확인
2) 순환 참조(Circular Reference)와 순환 참조 GC
레퍼런스 카운팅 방식만으로는 해결할 수 없는 치명적인 예외가 있습니다. 바로 두 객체가 서로를 서로 참조하는 '순환 참조' 상황입니다.
class Node:
def __init__(self):
self.parent = None
self.child = None
node1 = Node()
node2 = Node()
# 서로가 서로를 참조하도록 설정 (순환 참조)
node1.child = node2
node2.parent = node1
# 외부 변수 연결 끊기
del node1
del node2
del node1, del node2를 실행하여 외부 변수를 지웠지만, node1과 node2는 내부에서 서로의 주소를 움켜쥐고 있어 참조 카운트가 0으로 떨어지지 않습니다. 이로 인해 메모리에 영원히 갇히는 메모리 누수(Memory Leak)가 발생합니다.
파이썬은 이를 해결하기 위해 백그라운드에서 주기적으로 세대별(0세대, 1세대, 2세대)로 메모리를 감시하는 '순환 참조 가비지 컬렉터(gc 모듈)'를 실행합니다. 서로만 참조하고 외부 연결이 끊긴 객체 그룹을 찾아내 강제로 메모리에서 비워주는 보완 기법입니다.
핵심요약
파이썬 변수는 데이터를 담는 상자가 아닌 메모리 주소를 가리키는 표찰(참조)에 불과합니다.
얕은 복사(
copy.copy)는 중첩 객체의 내부 메모리 주소를 공유하므로 원본 오염의 위험이 있으며, 완벽한 독립 복사를 위해서는 깊은 복사(copy.deepcopy)를 사용해야 합니다.파이썬은 레퍼런스 카운팅(참조 수=0시 즉시 삭제)과 순환 참조 가비지 컬렉터(gc)를 결합하여 백그라운드에서 메모리를 자동으로 관리합니다.


0 댓글