본문 바로가기
Artificial Intelligence/60. Python

[PYTHON] 제너레이터가 스택 프레임을 유지하는 3가지 방법과 메모리 효율 해결 원리

by Papa Martino V 2026. 3. 16.
728x90

제너레이터(Generator)
제너레이터 (Generator)

 

파이썬의 제너레이터(Generator)는 단순히 yield 키워드를 사용하는 함수 그 이상입니다. 일반적인 함수는 실행이 끝나면 해당 함수의 스택 프레임(Stack Frame)이 소멸되지만, 제너레이터는 실행을 일시 중단하고 나중에 다시 그 지점부터 재개할 수 있는 능력을 갖추고 있습니다. 마치 게임을 하다가 '세이브(Save)'를 하고 나중에 '로드(Load)'하는 것과 같은 이 신기한 메커니즘이 내부적으로 어떻게 동작하는지, 그리고 왜 이것이 파이썬 비동기 프로그래밍의 핵심인지 심층적으로 분석해 보겠습니다.

1. 일반 함수 vs 제너레이터: 생명 주기의 차이

함수가 호출되면 파이썬 인터프리터는 PyFrameObject라고 불리는 스택 프레임을 생성합니다. 여기에는 지역 변수, 인수, 그리고 다음에 실행할 코드의 위치(Instruction Pointer)가 담깁니다.

  • 일반 함수: return을 만나면 스택 프레임이 콜 스택(Call Stack)에서 완전히 제거(Pop)되고 메모리에서 해제됩니다.
  • 제너레이터: yield를 만나면 프레임을 콜 스택에서 제거하지만, 힙(Heap) 영역에 이를 보존합니다. 즉, 함수의 상태가 메모리 어딘가에 살아남아 있는 것입니다.

2. 제너레이터의 상태 유지 3가지 핵심 메커니즘

제너레이터가 어떻게 이전 상태를 기억하고 다시 실행될 수 있는지, 그 기술적 배경을 3가지로 요약합니다.

표: 일반 함수와 제너레이터의 실행 및 메모리 관리 차이

구분 항목 일반 함수 (Regular Function) 제너레이터 (Generator)
종료 시점 return 또는 함수 끝 도달 yield 발생 시 일시 중단
스택 프레임 위치 호출 시 스택에 쌓이고 종료 시 제거 중단 시 힙(Heap)으로 이동하여 저장
지역 변수 보존 종료 후 즉시 소멸 다음 next() 호출까지 유지
제어권 반환 결과값과 함께 완전히 반환 상태(State)를 고정시킨 채 일시 반환

핵심은 f_lasti(마지막 실행 인스트럭션 위치)와 f_locals(지역 변수 딕셔너리)입니다. next()가 호출될 때마다 인터프리터는 보관해둔 프레임을 찾아 f_lasti 지점부터 다시 코드를 실행합니다.

3. 스택 프레임 유지의 기술적 해결: PyFrameObject

CPython 내부에서 제너레이터 객체는 gi_frame이라는 필드를 통해 자신의 프레임을 참조합니다. 일반 함수가 CPU 실행 흐름에 따라 순차적으로 스택을 사용하고 버리는 것과 달리, 제너레이터는 각자 자신만의 독립적인 '프레임 저장소'를 힙 영역에 가지고 있는 셈입니다. 이 구조 덕분에 파이썬은 코루틴(Coroutine)을 구현할 수 있었으며, 이는 오늘날 asyncio를 통한 비동기 처리가 가능하게 된 결정적인 토대가 되었습니다.

4. Sample Example: 제너레이터 내부 상태 들여다보기

실제 파이썬 코드를 통해 제너레이터가 중단된 지점을 어떻게 기억하고 있는지 확인해 보겠습니다.


import inspect

def stateful_generator():
    x = 10
    print(f"--- 첫 번째 yield 전 (x={x}) ---")
    yield x
    
    x = 20
    print(f"--- 두 번째 yield 전 (x={x}) ---")
    yield x

gen = stateful_generator()

# 1. 제너레이터 객체 생성 (아직 실행 전)
print(f"상태: {inspect.getgeneratorstate(gen)}")

# 2. 첫 번째 yield까지 실행
val1 = next(gen)
frame = gen.gi_frame
print(f"현재 로컬 변수: {frame.f_locals}")
print(f"마지막 실행 라인 번호: {frame.f_lineno}")

# 3. 두 번째 yield까지 실행
val2 = next(gen)
print(f"변경된 로컬 변수: {gen.gi_frame.f_locals}")

위 예제에서 gen.gi_frame을 통해 실행 중인 프레임에 직접 접근할 수 있으며, f_locals가 함수 종료 없이 계속 유지되고 있음을 볼 수 있습니다.

5. 제너레이터 활용의 혜택: 메모리 효율화

대규모 데이터를 리스트로 변환하여 처리하면 전체 데이터를 메모리에 올려야 하므로 $O(n)$의 메모리가 필요합니다. 하지만 제너레이터는 Lazy Evaluation(지연 평가) 방식을 사용하여 한 번에 하나의 아이템만 스택 프레임에 유지하므로, 데이터가 아무리 커도 메모리 사용량은 $O(1)$로 일정합니다. 이것이 바로 파이썬에서 대용량 로그 파일이나 무한한 시퀀스를 다룰 때 제너레이터가 필수적인 이유입니다.

6. 결론: 제너레이터는 '일시정지' 가능한 함수다

제너레이터가 스택 프레임을 유지하는 방법은 단순히 '데이터를 저장'하는 수준을 넘어, '실행 문맥(Context)' 자체를 힙에 동결시키는 고도의 기술입니다. 이러한 원리를 이해하면 파이썬의 비동기 프로그래밍 흐름을 제어하는 await의 동작 방식도 쉽게 이해할 수 있습니다. 메모리를 효율적으로 사용하면서도 복잡한 상태를 유지해야 하는 시스템을 설계한다면 제너레이터는 가장 강력한 무기가 될 것입니다.

참조 및 기술 출처

  • Python Internals: "The Structure of PyFrameObject and gi_frame"
  • PEP 255 – Simple Generators
  • CPython Source Code
728x90