본문으로 건너뛰기
개발 머꼬
개발 노트Python
hohyeon.dev19

대용량 파일 처리에서 메모리가 계속 늘어난 이유 — 제너레이터로 해결

  • #Engineering Note
  • #Memory
  • #Performance
  • #Python

문제 발생

수백만 줄짜리 로그 파일을 한 줄씩 파싱하는 스크립트가, 파일 크기가 커질수록 메모리 사용량이 함께 커지다가 결국 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])으로 강제로 리스트화한 부분입니다.

  1. 결과를 여러 번 순회해야 하거나 인덱싱(data[5])이 필요하다면 리스트가 맞습니다 — 제너레이터는 한 번 순회하면 소진되어 다시 쓸 수 없습니다.
  2. 한 번씩만 순차적으로 처리하면 되는 대용량 데이터라면 제너레이터/제너레이터 표현식((x for x in ...), 대괄호 대신 소괄호)을 씁니다.
  3. 여러 단계를 거쳐야 하는 파이프라인(필터 → 변환 → 집계)도 각 단계를 제너레이터로 연결하면 중간 결과를 리스트로 만들지 않고 스트리밍 방식으로 처리할 수 있습니다.
  4. 메모리 사용량을 직접 확인하고 싶다면 tracemalloc 모듈이나 memory_profiler 같은 도구로 수정 전후를 비교합니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.