제너레이터 제어: send(), throw(), close() 완전 정복

제너레이터의 세 가지 제어 메서드 send(), throw(), close()의 동작 원리, 실전 활용 패턴, GeneratorExit 처리 방법을 설명합니다.

· 6 min read · PALDYN Team

지난 글에서 send()를 활용한 코루틴 패턴과 프라이밍 규칙을 익혔다. 이번 글에서는 제너레이터를 외부에서 제어하는 세 메서드 send(), throw(), close()를 체계적으로 살펴본다. 이 세 가지를 완전히 이해하면 제너레이터 프로토콜의 전체 그림이 완성된다.

세 메서드 개요

제너레이터 객체는 __next__() 외에도 외부에서 값이나 예외를 주입하는 메서드를 제공한다.

gen.send(value)          # 값을 전달해 yield 표현식의 결과로 만듦
gen.throw(exc_type, ...)  # 현재 yield 위치에 예외 발생
gen.close()              # GeneratorExit 주입, 제너레이터 종료

세 메서드 모두 제너레이터를 재개시키고, 다음 yield까지 실행한다.

제너레이터 제어 메서드 개요

send(value) 상세

send(value)는 제너레이터를 재개하면서 yield 식의 반환값을 value로 설정한다. 재개 후 다음 yield에서 멈추고 그 값을 반환한다.

def demo():
    x = yield "첫 yield"        # next() 호출 시 "첫 yield" 반환
    print(f"받은 값: {x}")
    y = yield "두 번째 yield"   # send(10) 시 x=10, "두 번째 yield" 반환
    print(f"또 받음: {y}")

g = demo()
print(next(g))        # "첫 yield"  (x에는 아직 아무것도 없음)
print(g.send(10))     # "받은 값: 10", "두 번째 yield" 반환
g.send(20)            # "또 받음: 20", StopIteration 발생

next(g)는 g.send(None)과 동일하다.

# 아래 두 줄은 완전히 동일
next(g)
g.send(None)

throw(exc_type[, exc_val[, traceback]]) 상세

throw()는 현재 yield 위치에서 예외를 발생시킨다. 제너레이터 내부에서 그 예외를 잡으면 계속 실행되고, 못 잡으면 예외가 호출자로 전파된다.

def safe_gen():
    for i in range(10):
        try:
            yield i
        except RuntimeError as e:
            print(f"RuntimeError 잡힘: {e}, 계속 진행")

g = safe_gen()
print(next(g))               # 0
print(next(g))               # 1
print(g.throw(RuntimeError, "테스트"))  # RuntimeError 잡힘: 테스트, 계속 진행 / 2 반환
print(next(g))               # 3

내부에서 잡지 않은 예외는 그대로 전파된다.

def strict_gen():
    yield 1
    yield 2

g = strict_gen()
next(g)
g.throw(ValueError, "전파됨")  # ValueError: 전파됨 — 잡히지 않음

throw()와 close() 실전 예제

close() 상세

close()는 현재 yield 위치에 GeneratorExit 예외를 발생시킨다.

  • 제너레이터가 GeneratorExit를 잡지 않으면 조용히 종료된다
  • finally 블록은 항상 실행된다 (리소스 정리에 활용)
  • GeneratorExit를 잡았다면 return으로 끝내거나 예외를 다시 올려야 한다. yield로 값을 반환하면 RuntimeError가 발생한다
def cleanup_gen():
    try:
        while True:
            yield
    except GeneratorExit:
        print("GeneratorExit 잡힘 — 정리 작업")
    finally:
        print("finally 실행")

g = cleanup_gen()
next(g)
g.close()
# GeneratorExit 잡힘 — 정리 작업
# finally 실행

GC(가비지 컬렉터)도 소멸 중인 제너레이터에 자동으로 close()를 호출한다.

def leaky():
    try:
        yield 1
    finally:
        print("GC가 close() 호출함")

g = leaky()
next(g)
del g   # GC 호출 → "GC가 close() 호출함"

GeneratorExit와 StopIteration 비교

상황발생하는 예외처리 방법
제너레이터 소진StopIterationfor 루프가 자동 처리
close() 호출GeneratorExitfinally 또는 except GeneratorExit
return 문StopIteration(value)yield from이 수집

GeneratorExit는 BaseException을 직접 상속한다. except Exception으로는 잡히지 않는다.

# BaseException 계층
# BaseException
#   ├── SystemExit
#   ├── KeyboardInterrupt
#   ├── GeneratorExit    ← 여기
#   └── Exception
#       ├── RuntimeError
#       ├── ValueError
#       └── ...

throw()로 취소 신호 구현

비동기 취소 패턴을 제너레이터로 구현할 때 throw()가 유용하다.

class CancelError(Exception):
    pass

def long_task():
    results = []
    try:
        for i in range(100):
            results.append(i)
            yield i
    except CancelError:
        print(f"취소됨. {len(results)}개 처리 완료")
        return results

task = long_task()
for _ in range(5):
    next(task)

task.throw(CancelError)   # 취소됨. 5개 처리 완료

yield from과 throw/close의 관계

yield from이 활성화된 위임 제너레이터에서 throw()나 close()를 호출하면 서브 제너레이터에 투명하게 전달된다.

def sub():
    try:
        yield 1
        yield 2
    except RuntimeError:
        print("서브에서 잡힘")
        yield 99

def delegating():
    yield from sub()

g = delegating()
print(next(g))               # 1
print(g.throw(RuntimeError)) # "서브에서 잡힘", 99 반환

위임 제너레이터는 throw()를 따로 처리하지 않아도 서브 제너레이터가 처리한다.

정리

메서드시그니처사용 목적
send(value)value → yield_result값 주입 (코루틴 패턴)
throw(exc)예외 주입취소 신호, 리셋, 오류 전달
close()GeneratorExit 주입종료 및 리소스 정리

세 메서드와 yield from의 투명 전달을 함께 이해하면 제너레이터 프로토콜의 전체 그림이 완성된다. 다음 글에서는 비동기 컨텍스트에서의 이터레이터 프로토콜인 비동기 이터레이터를 살펴본다.


지난 글: 제너레이터 기반 코루틴: send()와 초기 코루틴 패턴

다음 글: 비동기 이터레이터: __aiter__와 anext 프로토콜


읽어주셔서 감사합니다. 😊
#Python#제너레이터#send#throw#close#GeneratorExit