한양과학기술고등학교

에듀테크 프로젝트 대시보드

코루틴과 제너레이터

코루틴과 제너레이터 (Coroutines & Generators)

보통 함수는 호출 → 실행 → 반환이 한 번에 끝난다. 반환하는 순간 그 함수의 지역변수·실행 위치를 담은 스택 프레임이 통째로 사라진다. 코루틴은 다르다. 중간에 yield로 실행을 멈추고 제어권을 호출자에게 돌려준 뒤, 나중에 멈춘 그 지점·그 상태 그대로 이어서 실행한다. 파이썬의 제너레이터, 자바스크립트의 function*, 그리고 그 위에 세워진 async / await가 모두 이 "멈췄다 재개" 위에서 돌아간다.

1. next()를 누를 때마다 한 조각씩 — 피보나치 제너레이터

아래 제너레이터는 피보나치 수를 만든다. 다음 next()를 누르면 실행이 yield 줄까지 달려가 값 하나를 뱉고 그 자리에서 얼어붙는다. 다시 누르면 얼어붙은 그 다음 줄부터 재개된다. 오른쪽 함수 프레임의 a, b가 호출 사이에도 살아남는 것에 주목하자 — 보통 함수라면 반환과 함께 지워졌을 값이다.

def fib(limit):    a, b = 0, 1    while a < limit:        yield a          # 멈춤 지점        a, b = b, a + b    # 루프 끝 → StopIteration
🧊 fib() 함수 프레임
a–
b–
limit50
실행 위치호출 전
일시정지
제너레이터 객체만 만들어진 상태. 아직 본문은 한 줄도 실행되지 않았다. 첫 next()가 와야 line 1부터 돈다.

2. 서브루틴 vs 코루틴 — 진입·이탈이 한 곳인가, 여러 곳인가

일반 함수 (서브루틴)코루틴 / 제너레이터
진입점항상 함수 첫 줄처음엔 첫 줄, 그다음부턴 지난번 멈춘 곳
이탈점return 한 번yield가 있는 여러 곳
지역 상태반환 시 파괴멈춰 있는 동안 보존
호출 결과값 하나필요할 때마다 값의 스트림
제어 흐름호출자 → 함수 → 호출자서로 주고받기 (협력적)

코루틴은 스스로 CPU를 놓아주는 협력적 멀티태스킹의 기본 단위다. OS가 강제로 스레드를 빼앗는 선점형 방식과 달리, 코루틴은 yield하는 그 순간에만 양보하므로 경쟁 상태가 생길 틈이 좁고 문맥 교환 비용도 거의 없다(스택 프레임 포인터만 바꾸면 끝).

# 파이썬
def countdown(n):
    while n > 0:
        yield n
        n -= 1

for x in countdown(3):   # 3, 2, 1 을 하나씩 당겨 씀
    print(x)

// 자바스크립트 — function* 과 yield
function* countdown(n) {
  while (n > 0) yield n--;
}
for (const x of countdown(3)) console.log(x);

3. 지연 평가 — 필요한 만큼만 계산한다

제너레이터는 값을 미리 다 만들어 두지 않는다. 소비자가 하나 당길 때(pull)마다 파이프라인을 거슬러 올라가 딱 한 개를 길어 온다. 그래서 무한 수열도 다룰 수 있다 — 끝이 없어도 take(k)로 k개만 뽑으면 그만이니까.

아래는 자연수(1,2,3,…) → 짝수만 통과 → 제곱 → 앞에서 k개 파이프라인이다. 결과 1개 당기기를 눌러, 각 단계가 실제로 몇 번씩 돌았는지 세어 보자. 리스트로 먼저 다 만들었다면 자연수를 무한히 만들다 멈추지 못했을 것이다.

① 자연수 n = 1, 2, 3, 4, … 를 하나씩 발생 0
② 짝수 필터 홀수는 버리고 짝수만 위로 올려보냄 0
③ 제곱 map x → x·x 로 변환 0
④ take(k) 소비 완성된 값을 결과에 담기 0
대기 중 — "결과 1개 당기기"를 눌러 pull 요청을 보내세요.
총 발생한 자연수 0개 · 짝수 통과 0개 · 제곱 0개 · 결과 0개.

4. async / await는 코루틴에 이름표를 바꿔 단 것

  • 제너레이터: yield로 값을 내보내며 멈춘다. 재개는 next()가 시킨다.
  • async 함수: await에서 "이 작업 끝나면 깨워줘"라고 남기고 멈춘다. I/O(네트워크·파일)가 끝나면 이벤트 루프가 그 코루틴을 멈춘 지점부터 재개한다.
  • 덕분에 스레드 하나로 수천 개의 대기 중 연결을 다룰 수 있다 — 각 연결은 await에서 잠든 코루틴일 뿐, 스레드를 붙잡고 있지 않다.
  • 스택리스(stackless) 코루틴은 한 프레임만 저장(파이썬·JS·Rust async), 스택풀(stackful)은 통째 스택을 저장(Go 고루틴, Lua). 전자는 가볍고, 후자는 아무 함수 깊이에서나 양보 가능.
# await 지점마다 코루틴은 "멈췄다 재개"한다
async def fetch_all(urls):
    results = []
    for u in urls:
        r = await http_get(u)   # 여기서 양보 → 응답 오면 이 줄부터 재개
        results.append(r)
    return results

더 깊이: 이벤트 루프 · 프로세스 vs 스레드 · 작업 훔치기 스케줄러