파이썬 제너레이터와 yield 완벽 가이드: 대용량 데이터 메모리 절약
파이썬 제너레이터와 yield의 동작 원리, 이터레이터와의 차이, 대용량 파일 처리와 메모리 절약 방법을 실전 예제로 정리합니다.

파이썬 제너레이터는 모든 데이터를 리스트에 먼저 저장하지 않고, 필요한 값을 yield로 하나씩 만들어 전달하는 이터레이터입니다. 대용량 파일이나 긴 데이터 흐름을 순차 처리할 때 메모리 사용을 줄일 수 있으며, 핵심은 “값을 반환하고 끝내는 것”이 아니라 “현재 실행 상태를 잠시 보존했다가 다음 요청에서 이어가는 것”입니다.
핵심 요약
yield가 들어간 함수는 호출 즉시 본문을 끝까지 실행하지 않고 제너레이터 객체를 반환합니다.next()를 호출할 때 다음yield까지 실행하고 상태를 보존합니다.- 리스트는 값을 모두 보관하지만 제너레이터는 보통 필요한 시점에 하나씩 계산합니다.
- 제너레이터는 한 번 소비하면 되돌릴 수 없으므로 재사용하려면 새로 만들어야 합니다.
- 메모리 절약 효과는 전체 데이터를 한꺼번에 보관하지 않을 때 생기며, 무조건 더 빠르다는 뜻은 아닙니다.
1. 제너레이터와 yield 동작 원리
일반 함수의 return은 결과를 반환하고 함수 실행을 종료합니다. 반면 yield는 값을 하나 전달한 뒤 실행 위치, 지역 변수, 예외 처리 상태 등을 보존합니다. 다음 값이 필요해지면 멈춘 지점 바로 다음 줄부터 다시 실행합니다.
def countdown(n):
while n > 0:
yield n
n -= 1
numbers = countdown(3)
print(next(numbers)) # 3
print(next(numbers)) # 2
print(next(numbers)) # 1
countdown(3)을 호출하는 순간에는 반복문이 실행되지 않습니다. next(numbers)가 호출되어야 첫 번째 yield까지 진행합니다. 더 이상 내보낼 값이 없으면 함수가 끝나고 내부적으로 StopIteration이 발생하며, 일반적인 for문은 이를 자동으로 처리합니다.
2. 이터러블·이터레이터·제너레이터 차이
| 구분 | 의미 | 대표 예 |
|---|---|---|
| 이터러블 | iter()로 이터레이터를 만들 수 있는 객체 |
리스트, 튜플, 문자열 |
| 이터레이터 | next()로 값을 하나씩 꺼내는 객체 |
iter([1, 2, 3]) |
| 제너레이터 | yield 함수나 표현식으로 만든 이터레이터 |
(x*x for x in range(3)) |
모든 제너레이터는 이터레이터지만 모든 이터레이터가 제너레이터인 것은 아닙니다. 리스트는 여러 번 반복할 수 있는 이터러블이고, 그 리스트에서 만든 이터레이터는 현재 위치를 가진 일회성 흐름입니다. 제너레이터 역시 현재 위치를 가지므로 끝까지 소비한 뒤 다시 순회하려면 제너레이터 함수를 다시 호출해야 합니다.
3. 리스트보다 메모리를 절약하는 이유
리스트 컴프리헨션은 계산된 모든 항목의 참조를 리스트에 저장합니다. 제너레이터 표현식은 현재 필요한 항목을 순서대로 계산하므로 전체 결과를 동시에 보관하지 않습니다.
# 전체 결과를 메모리에 보관
squares_list = [x * x for x in range(1_000_000)]
# 요청할 때 하나씩 계산
squares_gen = (x * x for x in range(1_000_000))
total = sum(squares_gen)
다만 sys.getsizeof()로 두 객체의 겉 크기만 비교한 결과를 전체 메모리 사용량처럼 단정하면 안 됩니다. 공식 문서에 따르면 이 함수는 객체가 직접 차지하는 크기만 계산하며, 객체가 참조하는 다른 객체의 크기는 포함하지 않을 수 있습니다. 실제 서비스에서는 처리 중 최대 메모리, 입출력 속도, 계산 비용을 함께 측정해야 합니다.
| 선택 기준 | 리스트 | 제너레이터 |
|---|---|---|
| 데이터 재사용 | 여러 번 순회하기 쉬움 | 다시 생성해야 함 |
| 인덱스 접근 | 가능 | 불가능 |
| 전체 길이 | len() 가능 |
일반적으로 바로 알 수 없음 |
| 대용량 순차 처리 | 메모리 부담 가능 | 유리한 경우가 많음 |
| 중간 결과 확인 | 간단함 | 소비 여부에 주의 |
4. 대용량 파일을 한 줄씩 처리하는 방법
파일 객체 자체가 줄 단위 이터레이터이므로 readlines()로 전체 내용을 읽지 않아도 됩니다. 여기에 제너레이터를 결합하면 필터링된 데이터만 다음 단계로 흘려보낼 수 있습니다.
def error_lines(path):
with open(path, encoding="utf-8") as file:
for line in file:
if "ERROR" in line:
yield line.rstrip("\n")
for line in error_lines("app.log"):
print(line)
with 블록과 제너레이터를 함께 사용할 때는 소비 도중 반복을 멈추면 파일 정리 시점을 고려해야 합니다. 보통 제너레이터가 소멸되거나 닫히면서 정리되지만, 자원 해제가 중요한 코드에서는 소비 범위를 명확히 하고 예외 상황도 테스트하는 것이 좋습니다. CSV나 엑셀 데이터를 분석한 뒤 모든 행을 다시 사용해야 한다면 무조건 제너레이터로 바꾸기보다 청크 처리와 자료 구조 선택을 함께 검토해야 합니다.
5. 제너레이터 표현식 활용법
제너레이터 표현식은 리스트 컴프리헨션의 대괄호 대신 소괄호를 사용합니다. 결과를 한 번만 합산하거나 필터링할 때 간결합니다.
numbers = range(10)
even_squares = (n * n for n in numbers if n % 2 == 0)
print(sum(even_squares))
sum(), min(), max(), any(), all()처럼 이터러블을 받는 함수에 바로 전달할 수 있습니다. 한 번 소비한 even_squares를 다시 출력하면 값이 남아 있지 않습니다. 여러 차례 사용할 결과라면 리스트로 만들거나 제너레이터를 생성하는 함수를 별도로 두는 편이 분명합니다.
6. 제너레이터 파이프라인 구성
제너레이터의 장점은 읽기, 필터링, 변환, 집계를 단계별 함수로 나누면서 중간 리스트 생성을 피할 수 있다는 점입니다.
def read_rows(path):
with open(path, encoding="utf-8") as file:
for line in file:
yield line.strip()
def non_empty(rows):
for row in rows:
if row:
yield row
def to_upper(rows):
for row in rows:
yield row.upper()
pipeline = to_upper(non_empty(read_rows("data.txt")))
for item in pipeline:
print(item)
각 단계는 입력 전체를 기다리지 않고 항목 하나를 받아 다음 단계로 넘깁니다. 다만 어느 단계에서 예외가 발생했는지 확인할 수 있도록 함수 이름과 로그를 명확히 해야 합니다. 네트워크 요청을 동시에 처리하는 문제는 일반 제너레이터만으로 해결되지 않으며, I/O 동시성이 목적이라면 asyncio와 비동기 이터레이터를 별도로 검토해야 합니다.
7. yield from·send·close 이해하기
yield from으로 하위 이터러블 위임
다른 이터러블의 값을 하나씩 다시 yield해야 할 때 yield from을 쓰면 중첩 반복문을 줄일 수 있습니다.
def flatten(groups):
for group in groups:
yield from group
print(list(flatten([[1, 2], [3, 4]])))
send()와 close()
send(value)는 멈춰 있는 yield 표현식 안으로 값을 전달하고 다음 yield까지 실행합니다. 처음 시작할 때는 아직 멈춘 yield가 없으므로 send(None) 또는 next()가 필요합니다. close()는 제너레이터에 GeneratorExit를 발생시켜 종료를 요청합니다. 이 기능들은 양방향 제어가 꼭 필요할 때만 사용하고, 단순 데이터 흐름에는 기본 반복 구조를 우선하는 편이 읽기 쉽습니다.
8. 실수 방지 체크리스트
- 한 번 소비한 제너레이터를 다시 순회하려고 하지 않았는지 확인합니다.
len()이나 인덱스 접근이 필요하다면 리스트가 더 적합한지 판단합니다.- 메모리 절약을 기대하면서 마지막에 곧바로
list()로 전체 변환하지 않는지 확인합니다. - 무한 제너레이터에는 반드시 종료 조건이나
itertools.islice()같은 제한을 둡니다. - 파일·소켓 등 외부 자원의 정리 시점을 확인합니다.
- 중간 디버깅 출력 때문에 제너레이터를 먼저 소비하지 않았는지 점검합니다.
- 제너레이터가 처리 속도까지 항상 높여준다고 가정하지 않습니다.
- 동시성과 지연 평가를 같은 개념으로 혼동하지 않습니다.
자주 묻는 질문
1. 함수에 yield가 하나라도 있으면 어떻게 되나요?
그 함수는 제너레이터 함수가 되며, 호출하면 일반 결과 대신 제너레이터 객체를 반환합니다.
2. yield와 return을 함께 사용할 수 있나요?
가능합니다. 제너레이터에서 return은 반복을 끝내며, 반환값은 StopIteration에 담길 수 있지만 일반적인 for문에서는 직접 사용하지 않습니다.
3. 제너레이터는 왜 한 번만 사용할 수 있나요?
현재 실행 위치를 앞으로 이동하며 값을 소비하는 이터레이터이기 때문입니다. 끝난 뒤에는 새 객체를 만들어야 합니다.
4. 제너레이터가 리스트보다 항상 빠른가요?
아닙니다. 메모리 사용을 줄일 수 있지만 값이 필요할 때마다 계산하므로 작업과 재사용 횟수에 따라 속도는 달라집니다.
5. 제너레이터의 길이를 바로 알 수 있나요?
일반적으로 len()을 지원하지 않습니다. 길이를 세면 값을 소비할 수 있으므로 전체 개수가 꼭 필요하면 설계를 다시 검토해야 합니다.
6. 제너레이터를 리스트로 바꾸려면 어떻게 하나요?
list(generator)를 사용합니다. 다만 모든 값을 메모리에 올리므로 대용량 처리의 장점이 사라질 수 있습니다.
7. yield from은 언제 사용하나요?
하위 이터러블이나 다른 제너레이터의 값을 순서대로 위임할 때 사용합니다.
8. 제너레이터 표현식과 리스트 컴프리헨션의 문법 차이는 무엇인가요?
제너레이터 표현식은 소괄호, 리스트 컴프리헨션은 대괄호를 사용하며 평가 시점과 저장 방식이 다릅니다.
9. 비동기 제너레이터와 같은 것인가요?
아닙니다. 비동기 제너레이터는 async def와 yield를 사용하고 async for로 소비하는 별도 구조입니다.
10. 대용량 데이터에서는 무조건 제너레이터를 써야 하나요?
순차 1회 처리라면 좋은 선택이지만 정렬, 임의 접근, 반복 재사용이 필요하면 청크 처리나 데이터베이스 등 다른 방법이 더 적합할 수 있습니다.
공식 출처
- Python Functional Programming HOWTO: Generators
- Python 언어 참조: Yield expressions
- Python 표준형: Generator Types
- Python sys.getsizeof 문서
공식 문서 확인일: 2026년 9월 2일