Python Pandas 대용량 데이터 메모리 누수 완벽 해결: Chunking과 Generator 아키텍처
Python Pandas 대용량 데이터 메모리 누수 완벽 해결: Chunking과 Generator 아키텍처
"몇 기가바이트(GB)짜리 CSV 파일 하나 분석하려고 read_csv를 실행했는데, 주피터 노트북(Jupyter) 커널이 죽어버리거나 백엔드 서버가 메모리 초과로 뻗어버립니다."
파이썬(Python) 기반의 데이터 엔지니어링이나 백엔드(Django, FastAPI) 시스템에서 판다스(Pandas)를 다룰 때 가장 흔하게 직면하는 MemoryError(메모리 에러) 사태입니다. 판다스는 현존하는 가장 강력한 데이터 조작 라이브러리지만, 모든 데이터를 컴퓨터의 메인 메모리(RAM)에 한 번에 올려놓고(In-memory) 작업한다는 치명적인 한계를 가지고 있습니다.
만약 5GB짜리 CSV 파일을 읽어 들인다면, 판다스는 데이터 타입 변환과 객체 오버헤드로 인해 실제로는 10GB 이상의 RAM을 집어삼킵니다. 본 가이드에서는 물리적인 RAM 증설(Scale-up)이라는 1차원적인 해결책을 버리고, 무한대의 데이터를 메모리 누수 없이 안정적으로 처리하는 'Chunking(청킹)'과 'Generator(제너레이터)' 아키텍처를 단호하게 제시합니다.
📌 이 글의 핵심 포인트
- 판다스(Pandas)의 인메모리(In-memory) 처리 방식이 유발하는 한계와 에러 원인 분석
chunksize파라미터를 활용한 데이터 분할 로드(Data Chunking) 기법- 파이썬
yield제너레이터를 결합한 메모리 안전형(Memory-Safe) ETL 파이프라인 구축
1단계: 핵심 원인 - 한 번에 집어삼키려는 탐욕
초보자들이 대용량 데이터를 다룰 때 작성하는 최악의 코드는 df = pd.read_csv('huge_data.csv') 단 한 줄입니다. 이 코드가 실행되는 순간, 서버는 수천만 줄의 데이터를 RAM에 구겨 넣기 시작합니다. 메모리 용량이 8GB인 서버에서 10GB에 달하는 데이터프레임을 생성하려 하니 OS가 프로세스를 강제로 죽여버리는(OOM Killer) 현상이 발생합니다.
해결의 핵심은 데이터를 '통째로' 읽는 것이 아니라, '잘게 쪼개서(Chunk)' 읽고 처리한 뒤 메모리에서 버리는(Garbage Collection) 스트리밍 방식을 도입하는 것입니다.
2단계: 완벽한 해결책 - chunksize를 통한 분할 정복(Divide and Conquer)
판다스의 read_csv 함수는 대용량 처리를 위한 chunksize라는 강력한 파라미터를 제공합니다. 이를 사용하면 데이터프레임 전체를 한 번에 반환하지 않고, 데이터를 지정한 크기만큼 잘라서 순회할 수 있는 이터레이터(Iterator) 객체를 반환합니다.
import pandas as pd
# ❌ 최악의 방식: 한 번에 모두 메모리에 적재 (MemoryError 발생)
# df = pd.read_csv('huge_data.csv')
# ✅ 최적화 방식: 10만 줄씩(100,000 rows) 잘라서 읽기
chunk_size = 100000
chunks = pd.read_csv('huge_data.csv', chunksize=chunk_size)
total_revenue = 0
# 메모리에 10만 줄만 올리고 -> 계산하고 -> 버리고 -> 다음 10만 줄 올리기 반복
for chunk in chunks:
# 각 청크(Chunk) 단위로 필요한 연산 수행
chunk_revenue = chunk['revenue'].sum()
total_revenue += chunk_revenue
print(f"총 매출: {total_revenue}")
위와 같이 코드를 수정하면, 100GB짜리 초거대 파일이라도 서버의 RAM 사용량은 chunk_size(10만 줄)를 처리할 수 있는 아주 적은 용량(수십 MB 수준)으로 고정됩니다.
3단계: 심층 아키텍처 - Generator(yield)를 결합한 파이프라인
백엔드 API 서버에서 대용량 데이터를 처리하여 클라이언트에게 응답(Response)해야 한다면, 청킹만으로는 부족합니다. 파이썬의 제너레이터(Generator, yield) 문법을 결합하여 데이터를 처리하는 즉시 스트리밍(Streaming) 형태로 내보내는 완벽한 파이프라인을 구축해야 합니다.
def process_large_data_generator(file_path):
"""
메모리 누수 없이 대용량 데이터를 정제하여 내보내는 제너레이터 함수
"""
chunk_size = 50000
for chunk in pd.read_csv(file_path, chunksize=chunk_size):
# 1. 결측치 제거 및 데이터 정제 (Data Cleansing)
clean_chunk = chunk.dropna(subset=['user_id', 'purchase_amount'])
# 2. 특정 조건 필터링
filtered_chunk = clean_chunk[clean_chunk['purchase_amount'] > 1000]
# 3. 정제된 데이터를 메모리에 쌓지 않고 즉시 반환(yield)
yield filtered_chunk
# 백엔드 실행부: 제너레이터를 호출하여 즉시 DB에 저장하거나 클라이언트에 전송
for processed_data in process_large_data_generator('huge_data.csv'):
save_to_database(processed_data) # 가상의 DB 저장 함수
return 대신 yield를 사용하면 함수가 실행 상태를 기억한 채로 결과값을 하나씩 밖으로 밀어냅니다. 이는 대규모 ETL(추출, 변환, 적재) 작업에서 서버의 생존을 보장하는 가장 아름답고 효율적인 백엔드 아키텍처입니다.
💡 아키텍트의 시선 (Insight)
데이터 엔지니어링의 수준은 '얼마나 많은 RAM을 꽂을 수 있는가'가 아니라, '얼마나 좁은 메모리 공간 안에서도 무한대의 데이터를 흘려보낼 수 있는가'에서 판가름 납니다. '배치(Batch)'로 한 번에 처리하려는 과거의 엑셀식 사고방식을 버리십시오. 데이터를 흐르는 물처럼 취급하는 청킹(Chunking)과 스트리밍(Streaming) 아키텍처를 도입하는 순간, 당신의 코드는 어떠한 트래픽과 데이터 폭풍 앞에서도 결코 무너지지 않을 것입니다.
댓글
댓글 쓰기