
파이썬(Python)을 사용하는 개발자라면 list 객체를 가장 빈번하게 사용하게 됩니다. 하지만 우리가 단순히 append() 함수를 호출하여 데이터를 추가할 때, 컴퓨터 내부 메모리에서는 어떤 복잡한 과정이 일어나는지 깊게 고민하는 경우는 드뭅니다. 파이썬의 리스트는 단순한 배열이 아닙니다. 데이터가 늘어날 때마다 메모리를 새로 할당하는 비효율을 줄이기 위해 '동적 할당 전략(Dynamic Over-allocation)'이라는 고도의 설계 원칙을 따르고 있습니다. 본 포스팅에서는 파이썬 리스트의 내부 동작 원리인 Over-allocation의 수학적 근거와, 이것이 실제 프로그래밍 성능에 미치는 영향, 그리고 대규모 데이터 처리 시 발생할 수 있는 메모리 파편화 문제를 해결하는 구체적인 방법을 전문적인 시각에서 다룹니다.
1. 파이썬 리스트의 본질: 왜 'Over-allocation'인가?
표준적인 배열(Array)은 생성 시점에 크기가 고정됩니다. 만약 크기가 5인 배열에 6번째 원소를 넣으려면, 크기가 6인 새로운 메모리 공간을 확보하고 기존 데이터를 모두 복사해야 합니다. 데이터가 10,000개일 때 하나를 추가하기 위해 10,000번의 복사 연산이 발생하는 것은 끔찍한 성능 저하를 초래합니다.
파이썬은 이를 해결하기 위해 '초과 할당(Over-allocation)' 전략을 사용합니다. 리스트에 원소가 꽉 찼을 때, 딱 하나만큼의 공간만 늘리는 것이 아니라 향후 추가될 데이터까지 고려하여 더 큰 여유 공간을 미리 확보하는 방식입니다. 이를 통해 대부분의 append() 연산을 $O(1)$(상수 시간)에 처리할 수 있게 됩니다.
2. CPython 내부의 할당 알고리즘 분석
파이썬의 공식 구현체인 CPython의 소스 코드(listobject.c)를 살펴보면, 리스트의 크기가 커질 때 다음과 같은 수식에 기반하여 새로운 용량(Capacity)을 계산합니다.
CPython 3.x 할당 공식 (근사치):new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6);
이 공식의 핵심은 리스트의 크기가 커질수록 약 12.5% (1/8)의 여유 공간을 추가로 확보한다는 점입니다. 초기 단계에서는 더 공격적으로 공간을 확보하고(3 또는 6의 가산점), 리스트가 거대해질수록 비율에 맞춰 완만하게 증가시킵니다.
3. 리스트 크기 변화에 따른 메모리 점유 비교
다음 표는 리스트에 원소를 하나씩 추가할 때, 파이썬이 내부적으로 메모리(Slot)를 어떻게 미리 할당하는지 보여줍니다. 실제 데이터 개수(Size)와 할당된 용량(Allocated Capacity)의 차이를 확인해 보십시오.
| 0 | 0 | 0 | - |
| 1 | 4 | 3 | 발생 (최초 할당) |
| 4 | 4 | 0 | - |
| 5 | 8 | 3 | 발생 (2배 확장) |
| 9 | 16 | 7 | 발생 (공식 적용) |
| 17 | 25 | 8 | 발생 (비율 적용) |
4. Over-allocation이 유발하는 문제와 해결 방법
동적 할당은 속도 면에서 이득을 주지만, 메모리 효율성 관점에서는 양날의 검입니다.
문제점: 메모리 낭비와 파편화
- Unused Memory: 대규모 리스트(예: 1억 개의 원소)에서 12.5%의 여유 공간은 수백 MB의 메모리 낭비를 의미합니다.
- Reallocation Overhead: 메모리가 가득 차서 재할당이 일어날 때, 기존 데이터를 모두 새로운 위치로 복사해야 하므로 일시적인 CPU 스파이크가 발생합니다.
해결 방법 1: `list.clear()` vs `list = []`
기존 리스트를 비울 때 list = []를 사용하면 새로운 객체가 생성되지만, list.clear()는 객체를 유지하며 메모리를 재사용할 수 있는 기회를 제공합니다.
해결 방법 2: 최적의 성능을 위한 'Pre-allocation' 기법
입력될 데이터의 총 개수를 미리 알고 있다면, 루프 내에서 append()를 반복하기보다 처음부터 고정 크기의 리스트를 만드는 것이 훨씬 유리합니다.
5. Sample Example: 할당 전략 시각화 코드
실제로 파이썬 리스트의 할당 크기가 어떻게 변하는지 확인해볼 수 있는 예제 코드입니다. sys.getsizeof()를 통해 객체의 실제 바이트 크기를 측정할 수 있습니다.
import sys
# 리스트 크기 변화 측정 예제
my_list = []
last_size = -1
print(f"{'Count':>10} | {'Bytes':>10} | {'Delta':>10}")
print("-" * 35)
for i in range(20):
current_bytes = sys.getsizeof(my_list)
if current_bytes != last_size:
print(f"{len(my_list):10d} | {current_bytes:10d} | {current_bytes - (last_size if last_size != -1 else 0):10d}")
last_size = current_bytes
my_list.append(i)
# 해결 방법 예시: 대량 데이터 처리 시 빈 슬롯 미리 확보
# append() 대신 인덱싱을 활용하여 속도 향상
n = 1000000
optimized_list = [None] * n
for i in range(n):
optimized_list[i] = i
6. 결론 및 요약
파이썬 리스트의 동적 할당 전략은 '시간 복잡도의 효율성'을 위해 '공간 복잡도의 희생'을 선택한 결과입니다. append() 연산의 평균 시간 복잡도를 $O(1)$로 유지하기 위한 필수적인 설계입니다.
개발자는 다음 세 가지를 기억해야 합니다:
- 빈번한
append()는 내부적으로 가끔 큰 비용의 메모리 복사를 발생시킨다. - 메모리가 극도로 제한된 환경에서는
array.array나numpy.ndarray를 사용하는 것이 대안이 될 수 있다. - 데이터 크기를 안다면
[None] * N방식을 사용하여 재할당 횟수를 제로(0)로 만드는 것이 가장 빠른 해결 방법이다.
내용 출처 및 참고 문헌:
- CPython GitHub Repository:
Objects/listobject.csource code. - Python Software Foundation: "Data Structures - Lists" Official Documentation.
- Fluent Python (Luciano Ramalho): 고성능 파이썬 구조 분석.
'Artificial Intelligence > 60. Python' 카테고리의 다른 글
| [PYTHON] 가변 인자 *args, **kwargs의 언패킹 메커니즘 차이와 3가지 성능 최적화 방법 (0) | 2026.03.04 |
|---|---|
| [PYTHON] 효율적인 데이터 처리 : 고차 함수 3가지 지연 평가 특성과 성능 해결 방법 (0) | 2026.03.04 |
| [PYTHON] 딕셔너리 성능을 결정짓는 2가지 핵심 : Hash Table 구현 방식과 Python 3.7 이후 순서 보장 원리 해결 (0) | 2026.03.03 |
| [PYTHON] 메모리 누수 해결하는 3가지 비결 : Weakref 모듈 활용 방법과 강한 참조와의 차이 (0) | 2026.03.03 |
| [PYTHON] 객체 복사의 2가지 메커니즘 : copy와 deepcopy의 내부 순회 방식 차이 해결 (0) | 2026.03.03 |