파이썬/기초 프로그래밍

파이썬 코루틴(제너레이터)로 메인↔서브 루틴 데이터 교환 이해하기

Data Jun 2025. 9. 9. 09:30

1. 제너레이터 vs. 코루틴 한 줄 정의

  • 제너레이터(Generator): yield 키워드를 사용해 값을 차례로 생산하는 함수. 상태를 보존하며 중단과 재개가 가능.
  • 코루틴(Coroutine): send(), throw(), close() 등으로 양방향 통신과 제어가 가능한 제너레이터의 일반화. 파이썬(전통적 코루틴)은 제너레이터 기반 코루틴을 의미하는 경우가 많다.

 

2. 예시 코드

def coroutine1():
    print('>>> coroutine stated.')
    i = yield
    print('>>> coroutine received : {}'.format(i))

cr1 = coroutine1()

print(cr1, type(cr1))

next(cr1)

cr1.send(100)
  • yield는 데이터를 건네주며 일시 중단하는 스위치다.
  • next()는 코루틴을 기동(priming) 하여 첫 yield 전까지 실행한다.
  • send(x)는 중단 지점(yield)으로 값 x를 밀어넣고 다시 실행을 이어가게 한다.
  • 위 코드의 실행 순서와 출력은 다음과 같다:
    1. 제너레이터 객체 생성 → <generator object ...> 출력
    2. next(cr1) → >>> coroutine stated. 출력 후 yield에서 정지
    3. cr1.send(100) → i가 100으로 바인딩되고 >>> coroutine received : 100 출력 후 종료

 

코드 한 줄씩 뜯어보기

def coroutine1():
    print('>>> coroutine stated.')
    i = yield
    print('>>> coroutine received : {}'.format(i))
  • print('>>> coroutine stated.'): 코루틴이 처음 기동되면 실행되는 구간.
  • i = yield: 여기서 일시 정지한다. 그리고 나중에 send(value)가 오면 i에 그 값이 할당된다.
  • 마지막 print(...): 재개 후 i를 사용하여 메시지 출력.
cr1 = coroutine1()
print(cr1, type(cr1))

함수 호출 시 즉시 실행되지 않고, 제너레이터 객체를 반환한다. 예: <generator object coroutine1 at 0x...>

next(cr1)
  • 코루틴 기동(priming) 단계. 첫 yield까지 진행된다.
  • 이때 콘솔에는 >>> coroutine stated.가 출력되고, i = yield에서 정지한다.
cr1.send(100)
  • 정지했던 yield 표현식의 결과 값으로 100이 들어가 i = 100이 된다.
  • 이어서 다음 줄 실행: >>> coroutine received : 100 출력 후 StopIteration과 함께 종료.

 

3. 실행 타임라인(ASCII 다이어그램)

메인]                              [코루틴 coroutine1]
   |  cr1 = coroutine1()   -> (생성만; 아직 실행 안 됨)
   |  next(cr1)            -> print('stated') 수행 → i = yield 에서 정지
   |----------------------> (정지 상태 대기)
   |  cr1.send(100)        -> (yield 재개; i=100 할당)
   |                        -> print('received : 100') → 종료(StopIteration)

 

 

4. 왜 next()(or send(None))로 먼저 깨워야 하나?

  • 코루틴이 처음에는 함수 본문 시작 전에 정지한 상태가 아니다. 아예 실행을 시작하지 않은 상태다.
  • yield 위치까지 실행 지점을 옮겨 두어야 send(x)가 받아줄 자리가 생긴다.
  • 그래서 처음 한 번은 next(cr1) 또는 동등한 cr1.send(None)으로 기동해야 한다.
cr1 = coroutine1()
# 다음 두 줄 중 하나 택1
next(cr1)        # 권장
# cr1.send(None)  # 동일 효과
cr1.send(100)

cr1.send(100)을 기동 없이 바로 호출하면 TypeError: can't send non-None value to a just-started generator 예외가 발생한다.

 

출력 결과 전체

<generator object coroutine1 at 0x...> <class 'generator'>
>>> coroutine stated.
>>> coroutine received : 100

 

 

5. 메인↔서브 루틴 데이터 교환의 핵심 포인트

  1. 정지 지점(yield): 서브 루틴이 제어권을 양보하며 대기하는 지점.
  2. 재개와 입력(send): 메인 루틴이 값을 보내며 실행을 재개.
  3. 상태 보존: 지역 변수(i)와 실행 지점이 유지된다.
 

6. 조금 더: 예외 주입과 종료 제어

1️⃣ 예외 주입: throw()

g = coroutine1()
next(g)
try:
    g.throw(ValueError('bad'))
except ValueError:
    print('메인에서 예외 처리')

throw(exc)는 코루틴 내부의 현재 yield 위치에서 예외를 발생시킨다.

 

2️⃣ 종료: close()

g = coroutine1()
next(g)
g.close()  # 내부에서 GeneratorExit 발생 → 정리 코드가 있다면 finally에서 처리

 

 

7. 실전 활용 예

  • 이벤트 스트림 처리: 센서 값, 로그 라인 등 스트림을 send()로 흘려보내며 처리.
  • 파이프라인 단계화: 필터 → 변환 → 집계와 같이 단계별 코루틴을 체인으로 연결.
  • 테스팅/모킹: 외부에서 값을 주입하며 상태 변화를 관찰.
def accumulator():
    total = 0
    while True:
        x = yield total  # 현재까지 합을 즉시 돌려줌
        if x is None:
            break
        total += x

acc = accumulator()
print(next(acc))    # 0 (기동)
print(acc.send(10)) # 10
print(acc.send(5))  # 15
print(acc.send(3))  # 18
acc.send(None)      # 종료 신호

 

 

8. async/await 코루틴과의 비교(간단)

  • 제너레이터 기반 코루틴: yield, send()로 수동 통신. I/O 대기와는 무관.
  • async/await 코루틴: async def, await로 비동기 I/O 협력 실행. 이벤트 루프 필요.

 

 

정리하면

 

이 예시는 메인 루틴과 서브 루틴 사이에서 값이 어떻게 왕복하는지를 가장 간결하게 보여준다. 핵심은 기동 → 정지 → 값 주입 → 재개의 리듬을 이해하는 것이다. 이 패턴만 확실히 잡으면, 스트림 처리부터 파이프라인 구성, 간단한 상태 머신 구현까지 안전하고 읽기 쉬운 코드를 만들 수 있다.