대용량 파일 처리에서 메모리가 계속 늘어난 이유 — 제너레이터로 해결
문제 발생
수백만 줄짜리 로그 파일을 한 줄씩 파싱하는 스크립트가, 파일 크기가 커질수록 메모리 사용량이 함께 커지다가 결국 MemoryError로 죽는 문제가 있었습니다.
def load_lines(path):
with open(path) as f:
return [line.strip() for line in f] # 전체를 리스트로 메모리에 적재
for line in load_lines("huge.log"):
process(line)원인 분석
load_lines는 리스트 컴프리헨션으로 파일의 모든 줄을 한 번에 메모리에 올립니다. 함수가 반환되기 전에 전체 파일 내용이 리스트 객체로 메모리에 존재해야 하므로, 파일이 크면 그만큼 메모리를 점유합니다. 실제로 처리는 한 줄씩 순차적으로 하는데도, 준비 단계에서 전체를 미리 다 읽어버리는 것이 낭비입니다.
해결 방안
제너레이터(generator)를 쓰면 값을 필요할 때 하나씩 계산해서 내어줍니다 — 전체를 미리 메모리에 만들어두지 않습니다.
def load_lines(path):
with open(path) as f:
for line in f:
yield line.strip() # 호출될 때마다 한 줄씩만 반환
for line in load_lines("huge.log"):
process(line)yield가 있는 함수를 호출하면 실제 코드는 즉시 실행되지 않고 제너레이터 객체만 반환됩니다. for문이 다음 값을 요청할 때마다 그 지점에서 코드가 재개되어 딱 한 줄만 처리하고 다시 멈춥니다 — 파일 전체가 메모리에 동시에 존재할 필요가 없습니다.
사실 위 예시는 더 간단하게 만들 수도 있습니다 — 파일 객체(open()의 반환값) 자체가 이미 이터레이터라서, for line in f:로 직접 순회하면 애초에 전체를 메모리에 올리지 않습니다. 문제는 중간에 리스트 컴프리헨션([... for line in f])으로 강제로 리스트화한 부분입니다.
- 결과를 여러 번 순회해야 하거나 인덱싱(
data[5])이 필요하다면 리스트가 맞습니다 — 제너레이터는 한 번 순회하면 소진되어 다시 쓸 수 없습니다. - 한 번씩만 순차적으로 처리하면 되는 대용량 데이터라면 제너레이터/제너레이터 표현식(
(x for x in ...), 대괄호 대신 소괄호)을 씁니다. - 여러 단계를 거쳐야 하는 파이프라인(필터 → 변환 → 집계)도 각 단계를 제너레이터로 연결하면 중간 결과를 리스트로 만들지 않고 스트리밍 방식으로 처리할 수 있습니다.
- 메모리 사용량을 직접 확인하고 싶다면
tracemalloc모듈이나memory_profiler같은 도구로 수정 전후를 비교합니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.