Python 제너레이터와 yield 완전 정복: 메모리 효율적인 대용량 데이터 처리 패턴과 실전 예제

Updated Feb 6, 2026

제너레이터란 무엇인가?

파이썬에서 대용량 데이터를 처리할 때 메모리 부족 문제를 겪어본 적이 있나요? 제너레이터(Generator)는 이러한 문제를 해결하는 강력한 도구입니다. 일반 함수가 return으로 값을 한 번에 반환하는 반면, 제너레이터는 yield를 사용해 값을 하나씩 생성하면서 메모리를 절약합니다.

제너레이터는 이터레이터를 생성하는 함수로, 필요할 때마다 값을 생성하여 메모리 효율성을 극대화합니다.

일반 함수 vs 제너레이터 비교

특징 일반 함수 제너레이터
반환 키워드 return yield
메모리 사용 모든 값을 메모리에 저장 한 번에 하나씩 생성
실행 방식 한 번 실행 후 종료 상태를 유지하며 중단/재개
반환 타입 값 또는 리스트 제너레이터 객체
대용량 데이터 처리 메모리 부족 위험 효율적

yield의 동작 원리

yield는 함수의 실행을 일시 중단하고 값을 호출자에게 반환합니다. 다음 호출 시 중단된 지점부터 재개됩니다.

def simple_generator():
    print("첫 번째 값 생성")
    yield 1
    print("두 번째 값 생성")
    yield 2
    print("세 번째 값 생성")
    yield 3

gen = simple_generator()
print(next(gen))  # 출력: 첫 번째 값 생성 \n 1
print(next(gen))  # 출력: 두 번째 값 생성 \n 2
print(next(gen))  # 출력: 세 번째 값 생성 \n 3

yield의 핵심 특징

  • 지연 평가(Lazy Evaluation): 값이 실제로 필요할 때만 계산
  • 상태 보존: 로컬 변수와 실행 위치를 기억
  • 무한 시퀀스 생성 가능: 메모리 제약 없이 무한 데이터 스트림 생성

실전 예제 1: 대용량 파일 처리

수백만 줄의 로그 파일을 처리할 때 제너레이터가 얼마나 효율적인지 비교해봅시다.

# 비효율적인 방법 - 전체 파일을 메모리에 로드
def read_file_all(filename):
    with open(filename, 'r') as f:
        return f.readlines()  # 100GB 파일이면 메모리 초과!

# 효율적인 방법 - 제너레이터 사용
def read_file_generator(filename):
    with open(filename, 'r') as f:
        for line in f:
            yield line.strip()

# 사용 예시
for line in read_file_generator('huge_log.txt'):
    if 'ERROR' in line:
        print(line)

메모리 사용량 비교:
– 일반 방식: 파일 크기만큼 메모리 사용 (10GB 파일 = 10GB RAM)
– 제너레이터: 한 줄씩 처리 (수 KB 수준)

실전 예제 2: 피보나치 수열 무한 생성

def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

# 처음 10개의 피보나치 수 출력
fib = fibonacci()
for _ in range(10):
    print(next(fib))
# 출력: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34

수학적으로 피보나치 수열의 nn번째 항 FnF_n은 다음 재귀 관계식으로 정의됩니다:

Fn=Fn1+Fn2F_n = F_{n-1} + F_{n-2}

여기서 F0=0F_0 = 0, F1=1F_1 = 1입니다. 제너레이터는 이 수열을 메모리 제약 없이 무한히 생성할 수 있습니다.

실전 예제 3: 데이터 파이프라인 구축

여러 제너레이터를 연결하여 데이터 파이프라인을 만들 수 있습니다.

# 1단계: 데이터 읽기
def read_numbers(filename):
    with open(filename, 'r') as f:
        for line in f:
            yield int(line.strip())

# 2단계: 짝수만 필터링
def filter_even(numbers):
    for num in numbers:
        if num % 2 == 0:
            yield num

# 3단계: 제곱 계산
def square(numbers):
    for num in numbers:
        yield num ** 2

# 파이프라인 실행
pipeline = square(filter_even(read_numbers('numbers.txt')))
for result in pipeline:
    print(result)

파이프라인의 장점

특징 설명
모듈화 각 단계를 독립적으로 테스트/수정 가능
메모리 효율 중간 결과를 저장하지 않음
가독성 데이터 흐름이 명확함
재사용성 단계별 함수를 다른 파이프라인에서 재사용

실전 예제 4: 배치 데이터 처리

머신러닝 학습 시 대용량 데이터를 배치로 나누어 처리하는 패턴입니다.

def batch_generator(data, batch_size):
    """데이터를 batch_size 크기로 나누어 반환"""
    for i in range(0, len(data), batch_size):
        yield data[i:i + batch_size]

# 사용 예시
data = range(1000000)  # 100만 개의 데이터
for batch in batch_generator(data, batch_size=1000):
    # 각 배치를 모델에 입력
    process_batch(batch)  # 1000개씩 처리

제너레이터 표현식

리스트 컴프리헨션과 유사하지만 [] 대신 ()를 사용합니다.

# 리스트 컴프리헨션 - 즉시 모든 값 생성
squares_list = [x**2 for x in range(1000000)]  # 메모리 많이 사용

# 제너레이터 표현식 - 필요할 때 생성
squares_gen = (x**2 for x in range(1000000))   # 메모리 절약

print(sum(squares_gen))  # 값이 필요할 때만 계산

yield from: 서브 제너레이터 위임

yield from을 사용하면 다른 제너레이터의 값을 간단히 위임할 수 있습니다.

def generator1():
    yield from range(3)
    yield from range(3, 6)

# 위 코드는 아래와 동일
def generator2():
    for i in range(3):
        yield i
    for i in range(3, 6):
        yield i

print(list(generator1()))  # [0, 1, 2, 3, 4, 5]

제너레이터 활용 패턴 정리

패턴 사용 사례 메모리 절감 효과
파일 스트리밍 대용량 로그/CSV 처리 파일 크기에 무관
무한 시퀀스 난수 생성, 시계열 데이터 무한대
데이터 파이프라인 ETL, 데이터 전처리 중간 결과 제거
배치 처리 ML 학습, API 페이지네이션 배치 크기만큼
필터링/변환 조건부 데이터 추출 불필요한 데이터 제거

성능 비교: 실측 데이터

1억 개의 숫자 중 짝수의 제곱을 구하는 작업을 비교했습니다.

import sys

# 리스트 사용
data_list = [x**2 for x in range(100000000) if x % 2 == 0]
print(f"리스트 메모리: {sys.getsizeof(data_list) / 1024 / 1024:.2f} MB")
# 출력: 약 400 MB

# 제너레이터 사용
data_gen = (x**2 for x in range(100000000) if x % 2 == 0)
print(f"제너레이터 메모리: {sys.getsizeof(data_gen) / 1024:.2f} KB")
# 출력: 약 0.1 KB

결과: 제너레이터는 리스트 대비 400만 배 적은 메모리를 사용합니다!

주의사항

  1. 일회성: 제너레이터는 한 번 순회하면 소진됩니다.
gen = (x for x in range(3))
print(list(gen))  # [0, 1, 2]
print(list(gen))  # [] - 이미 소진됨
  1. 인덱싱 불가: 제너레이터는 gen[0] 같은 인덱스 접근이 불가능합니다.

  2. 길이 확인 불가: len(gen)을 호출할 수 없습니다.

제너레이터는 스트림 데이터 처리에 최적화되어 있으며, 랜덤 액세스가 필요하면 리스트를 사용해야 합니다.

마무리

Python 제너레이터와 yield는 메모리 효율적인 프로그래밍의 핵심 도구입니다. 주요 활용 시나리오를 정리하면:

  • 대용량 파일 처리: 파일 전체를 메모리에 로드하지 않고 한 줄씩 처리
  • 무한 데이터 스트림: 피보나치, 난수 생성 등 끝없는 시퀀스 생성
  • 데이터 파이프라인: 읽기-필터링-변환을 메모리 낭비 없이 연결
  • 배치 처리: ML 학습이나 API 호출 시 데이터를 적절한 크기로 분할

제너레이터를 사용하면 400만 배까지 메모리를 절약할 수 있으며, 코드의 가독성과 모듈화도 향상됩니다. 다음 프로젝트에서 대용량 데이터를 다룬다면 yield를 적극 활용해보세요!

Did you find this helpful?

☕ Buy me a coffee

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

TODAY 399 | TOTAL 2,622