결론부터 보기
Python에서 데이터는 객체로 표현되고, 변수처럼 보이는 이름은 값을 담는 상자가 아니라 객체에 바인딩(binding)된다. 대입은 일반적으로 객체를 복사하지 않는다. 같은 변경 가능한 객체를 여러 이름이 가리키면 한쪽에서 바꾼 내용이 다른 쪽에서도 보인다.
객체의 수명과 메모리 회수 방식은 Python 언어와 특정 구현을 나눠서 봐야 한다. CPython은 참조 횟수를 기본으로 사용하고 순환 참조를 찾는 가비지 컬렉터가 이를 보완한다. 다른 Python 구현이 똑같은 시점과 방식으로 객체를 회수한다고 가정해서는 안 된다.
이름은 객체에 바인딩된다
다음 예제에서 alias = original은 리스트 내용을 새로 복사하지 않는다. 두 이름이 같은 리스트 객체를 가리키도록 바인딩한다. 반면 original.copy()는 새 리스트를 만든다.
original = [1, 2, 3]
alias = original
copied = original.copy()
alias.append(4)
print(original)
print(copied)
print(alias is original)
print(copied is original)
[1, 2, 3, 4]
[1, 2, 3]
True
False
여기서 append는 기존 리스트의 값을 변경하는 mutation이다. is는 두 이름이 같은 정체성의 객체를 가리키는지 비교한다. 값이 같은지를 비교하려면 ==를 쓴다.
얕은 복사도 중첩된 객체까지 모두 복사하지 않는다. 리스트 안에 또 리스트가 있다면 바깥 컨테이너만 새로 만들고 안쪽 객체는 공유할 수 있다. 깊은 복사는 정말 독립된 중첩 구조가 필요할 때만 검토한다.
객체의 세 가지 기본 속성
속성 | 의미 | 관찰 방법 |
|---|---|---|
identity | 객체가 생성된 뒤 변하지 않는 정체성 | id(x), x is y |
type | 지원 연산과 가능한 값의 범위를 결정 | type(x) |
value | 객체가 나타내는 데이터, 가변 객체는 변경 가능 | 표현과 타입별 연산 |
공식 data model은 모든 객체가 identity·type·value를 가진다고 설명한다. CPython에서는 id(x)가 현재 객체의 메모리 주소이지만, 이는 CPython 구현 세부사항이다. 주소라고 가정해 산술에 사용하거나 객체가 사라진 뒤에도 계속 고유하다고 생각해서는 안 된다.
언어 모델과 CPython 메모리 구현 구분하기
Python 언어 사양은 코드 블록이 execution frame에서 실행되고 이름이 namespace와 scope 규칙에 따라 해석된다고 설명한다. 하지만 ‘지역 변수는 물리적 stack에 있고 객체는 heap에 있다’와 같은 구체적 배치를 언어 차원에서 보장하지 않는다.
입문 그림에서 stack과 heap을 나누는 것은 함수 호출의 수명과 동적으로 생성되는 객체를 구분하는 비유로는 쓸 수 있다. 다만 Python 코드를 이해할 때는 ‘함수 호출마다 frame이 생기고, frame의 지역 이름들이 객체를 참조한다’는 모델이 더 정확하다. frame 자체와 로컬 저장 방식의 내부 구현은 인터프리터에 맡긴다.
CPython C API에서 대부분의 객체는 공통 머리말을 가지며 객체 타입과 참조 횟수 같은 정보를 관리한다. PDF의 type·refcount·value 그림은 이 구현을 이해하기 위한 단순화다. 모든 객체의 실제 메모리 레이아웃이 세 칸으로 동일하다는 뜻은 아니다.
참조 횟수와 순환 가비지 컬렉터
CPython은 객체를 가리키는 강한 참조가 늘고 줄 때 참조 횟수를 관리한다. 일반적으로 참조 횟수가 0이 되면 그 객체를 정리할 수 있다. 하지만 컨테이너가 서로를 가리키는 순환이 있으면 외부에서 접근할 수 없어도 각 객체의 참조 횟수가 0이 아닐 수 있다.
left = []
right = [left]
left.append(right)
del left
del right
두 지역 이름을 연결 해제한 뒤에도 리스트들은 서로를 참조한다. CPython의 순환 가비지 컬렉터는 참조 횟수만으로 처리하기 어려운 이런 도달 불가능한 순환을 찾도록 보완한다. gc 모듈은 컬렉터 상태 확인, 수동 수집, 통계와 디버깅 인터페이스를 제공한다.
객체가 정확히 언제 정리될지에 의존해 파일이나 네트워크 연결을 닫지 않는다. 이런 자원은 with 문이나 명시적 close()로 관리한다. 구현별 메모리 회수 시점과 자원 해제 시점은 같은 개념이 아니다.
del은 객체를 직접 삭제하지 않는다
이름을 대상으로 한 del data의 의미는 그 이름의 바인딩을 제거하는 것이다. 다른 이름이나 컨테이너가 같은 객체를 계속 참조한다면 객체는 그대로 살아 있다. 리스트 원소를 대상으로 한 del items[0]도 컨테이너에서 해당 참조를 제거하는 연산으로 이해할 수 있다.
data = {"rows": 100}
same_object = data
del data
print(same_object)
{'rows': 100}
메모리를 관찰하는 도구와 한계
id()와 is — 객체 identity를 관찰하고 비교한다. None 같은 싱글턴 비교에는 is가 적합하지만 숫자·문자열의 값 비교에는 ==를 사용한다.
sys.getsizeof() — 객체 자체가 차지하는 크기를 바이트 단위로 반환한다. 그 객체가 참조하는 다른 객체의 크기까지 재귀적으로 더해 주지는 않으므로 중첩 컨테이너의 전체 메모리로 해석하면 안 된다. 결과는 구현과 빌드에 따라 달라질 수 있다.
sys.getrefcount() — CPython에서 관찰용으로 쓸 수 있지만 함수 인자로 전달하는 임시 참조까지 포함된다. 일부 객체의 매우 큰 값은 실제 참조 수를 뜻하지 않을 수 있다. 메모리 수명 로직이나 이식 가능한 코드의 판단 기준으로 사용하지 않는다.
tracemalloc — Python이 할당한 메모리 블록을 추적하고 snapshot 간 차이와 할당 traceback을 확인한다. tracemalloc.start()를 가능한 이른 시점에 호출해야 초기 할당도 추적할 수 있다. 모든 외부 네이티브 라이브러리의 메모리를 완전하게 보여 주는 프로세스 메모리 측정기와는 목적이 다르다.
데이터 분석에서 중요한 실제 패턴
큰 리스트나 DataFrame에 새 이름을 대입했다고 해서 데이터 전체가 복사되었다고 생각하지 않는다. 같은 객체를 공유하는지, 새 객체를 반환하는 연산인지 각 라이브러리 문서로 확인한다. 반대로 독립 사본이 필요한데 참조만 공유하면 전처리 결과가 원본까지 바뀌는 버그가 생길 수 있다.
한 번에 모두 읽지 않아도 되는 데이터는 iterator, generator, chunk 처리를 검토한다. 함수가 끝난 뒤에도 큰 객체가 전역 변수, 캐시, 클로저, 노트북 출력 기록에 남아 있으면 지역 이름 하나를 del해도 메모리 회수 조건이 충족되지 않을 수 있다.
프로세스 메모리가 즉시 운영체제에 반환되지 않는 현상과 살아 있는 Python 객체가 누적되는 현상도 구분한다. 먼저 재현 가능한 입력으로 측정하고, snapshot과 프로파일러로 어느 코드 경로의 할당이 늘어나는지 찾는다.
흔한 오해와 핵심 체크리스트
☐ 대입은 일반적으로 복사가 아니라 이름을 객체에 바인딩하는 연산임을 설명할 수 있다.
☐ ==는 값, is는 identity를 비교하며 두 연산을 바꿔 쓰지 않는다.
☐ ‘지역 변수는 stack, 객체는 heap’이 언어 사양이 아니라 단순화된 구현 비유임을 안다.
☐ CPython의 참조 횟수와 순환 가비지 컬렉터가 맡는 역할을 구분할 수 있다.
☐ del은 바인딩이나 컨테이너 참조를 제거하며 객체 자체의 즉시 삭제를 보장하지 않음을 안다.
☐ getsizeof와 tracemalloc의 측정 범위가 다름을 알고 실제 측정 없이 메모리를 추측하지 않는다.
다음 글: 데이터 분석용 Python 개발 환경 만들기
객체와 메모리 모델을 이해했으니 이제 실습 환경을 준비할 차례다. 다음 글에서는 Python 인터프리터와 VS Code를 연결하고, 프로젝트별 가상환경을 만들며, pip와 requirements 파일로 의존성을 재현하는 방법을 다룬다.
참고 자료
Python data model 공식 문서 — 객체의 identity·type·value와 컨테이너 참조 (2026-08-03 확인)
Python execution model의 이름과 바인딩 — frame·namespace·scope와 del의 이름 연결 해제 의미 (2026-08-03 확인)
CPython C API 참조 횟수 문서 — CPython 객체 참조 관리의 구현 관점 (2026-08-03 확인)
Python gc 공식 문서 — 참조 횟수를 보완하는 순환 가비지 컬렉터와 디버깅 인터페이스 (2026-08-03 확인)
Python sys.getrefcount·sys.getsizeof 공식 문서 — 참조 횟수와 얕은 객체 크기 관찰 시 주의점 (2026-08-03 확인)
Python tracemalloc 공식 문서 — Python 메모리 할당 snapshot과 traceback 추적 (2026-08-03 확인)
댓글 0
댓글을 불러오는 중…