프로그래밍/Python

파이썬 - Coroutine과 await

Terry Cho 2025. 3. 20. 14:38

Python 코루틴 (Coroutine) 상세 설명

코루틴은 프로그래밍의 패러다임 중 하나로, 서브루틴(함수)을 일반화한 개념입니다. 단순히 순차적으로 실행되는 함수와 달리, 코루틴은 실행을 일시 중단(pause)했다가 나중에 다시 재개(resume)할 수 있는 능력을 핵심으로 가집니다. 이러한 특징 덕분에 코루틴은 비동기 프로그래밍과 동시성 프로그래밍에서 매우 강력한 도구로 활용됩니다.

1. 코루틴이란 무엇인가? (서브루틴과의 비교)

  • 서브루틴 (Subroutine, 일반 함수):
    • 단방향 진입/탈출 (Single entry point/exit point): 함수는 시작점에서 진입하여, 종료점에서 탈출합니다.
    • 호출자-피호출자 관계 (Caller-Callee): 함수를 호출하는 쪽(caller)과 호출되는 쪽(callee)의 관계가 명확합니다. 함수는 호출될 때 실행되고, 완료되면 결과를 반환하고 호출자에게 제어권을 넘깁니다.
    • 실행 흐름: 함수는 호출되면 시작부터 끝까지 순차적으로 실행됩니다. 중간에 멈추거나 다른 함수에게 제어권을 양보하지 않습니다.
  • 코루틴 (Coroutine):
    • 다중 진입/탈출 (Multiple entry/exit points): 코루틴은 실행을 일시 중단할 수 있으며, 나중에 중단된 지점부터 다시 실행을 재개할 수 있습니다. 이는 함수가 여러 번 진입점과 탈출점을 가질 수 있다는 의미입니다.
    • 대등한 관계 (Cooperative): 코루틴은 서로 협력적인 관계를 가집니다. 하나의 코루틴이 실행을 일시 중단하고 다른 코루틴에게 제어권을 넘겨줄 수 있습니다. 호출자-피호출자 관계보다는 동등한 레벨에서 협력하는 관계에 가깝습니다.
    • 실행 흐름: 코루틴은 시작될 때 실행되다가, 특정 시점에서 스스로 실행을 일시 중단하고, 나중에 다시 재개되어 중단된 지점부터 실행을 이어갑니다. 실행 흐름이 순차적이지 않고 중간에 끊겼다가 이어지는 특징을 가집니다.

2. 코루틴의 핵심 특징

  • 일시 중단 및 재개 (Pause and Resume): 코루틴의 가장 중요한 특징입니다. 실행 중에 특정 시점에서 스스로를 멈추고, 나중에 다시 멈춘 지점부터 실행을 이어갈 수 있습니다. 이것이 비동기 프로그래밍의 핵심 메커니즘을 제공합니다.
  • 비선점형 멀티태스킹 (Cooperative Multitasking): 코루틴은 스스로 제어권을 양보해야 실행이 중단됩니다. 운영체제가 강제로 시간을 분할하여 쓰레드를 선점하는 선점형 멀티태스킹과는 다릅니다. 코루틴은 프로그래머가 명시적으로 제어권을 넘겨주는 시점을 결정할 수 있습니다.
  • 싱글 스레드 동시성 (Single-threaded Concurrency): 코루틴은 주로 싱글 스레드 환경에서 동시성을 구현하는 데 사용됩니다. 여러 코루틴이 싱글 스레드 내에서 번갈아 가며 실행되면서 마치 동시에 여러 작업을 처리하는 것처럼 보이게 만듭니다. 실제 병렬성(Parallelism)과는 다르지만, I/O 바운드 작업에서는 매우 효율적인 방식입니다.

3. Python 코루틴 (async/await) 작동 방식

Python에서는 async/await 문법을 통해 코루틴을 효과적으로 구현하고 사용할 수 있습니다.

  • async def: 함수를 코루틴으로 정의합니다. def 대신 async def 키워드를 사용하면 해당 함수는 코루틴 함수가 됩니다. 코루틴 함수 안에서는 await 키워드를 사용할 수 있습니다.
  • await: 코루틴 함수 안에서 await 키워드를 사용하면, 다음과 같은 일이 발생합니다.
    1. 현재 코루틴 일시 중단: await 를 만난 코루틴은 실행을 멈춥니다.
    2. 제어권을 이벤트 루프에 양도: 현재 코루틴은 이벤트 루프에게 제어권을 넘겨줍니다.
    3. awaitable 객체 대기: await 뒤에 오는 객체 (다른 코루틴, Task, Future 등 awaitable 객체) 가 완료될 때까지 기다립니다.
    4. 코루틴 재개: awaitable 객체가 완료되면, 이벤트 루프는 일시 중단되었던 코루틴을 다시 재개시켜 실행을 이어갑니다.
  • 이벤트 루프 (Event Loop): asyncio 라이브러리의 핵심 구성 요소입니다. 이벤트 루프는 코루틴의 실행을 스케줄링하고 관리하는 역할을 합니다. await 에 의해 코루틴이 일시 중단되면, 이벤트 루프는 다른 준비된 코루틴을 실행하거나 I/O 작업을 처리합니다. await 된 작업이 완료되면, 이벤트 루프는 해당 코루틴을 다시 실행 가능 상태로 만들고, 적절한 시점에 다시 실행을 재개합니다.

4. 코루틴의 장점

  • 향상된 I/O 바운드 작업 성능: I/O 작업 대기 시간을 효율적으로 활용하여 프로그램의 전체적인 처리량을 높입니다. 네트워크 요청, 파일 처리, 데이터베이스 쿼리 등 시간이 걸리는 I/O 작업을 비동기적으로 처리하여 성능을 향상시킵니다.
  • 향상된 동시성 및 응답성: 싱글 스레드 환경에서도 높은 동시성을 제공하여, 많은 클라이언트 요청을 처리해야 하는 서버 애플리케이션이나 실시간 웹 애플리케이션 개발에 적합합니다. UI가 멈추지 않고 계속 응답하는 반응형 애플리케이션 개발에도 유용합니다.
  • 비동기 코드 가독성 및 유지보수성 향상: async/await 문법을 통해 복잡한 비동기 코드를 순차적인 동기 코드처럼 작성할 수 있어서, 코드의 가독성과 유지보수성을 크게 향상시킵니다. 콜백 기반 방식의 복잡함과 콜백 헬 문제를 해결해줍니다.

Python await

await 란 무엇일까요?

awaitasync def 로 정의된 코루틴 함수 안에서만 사용할 수 있는 키워드입니다. await"여기서 잠시 멈춰서, 내가 기다리는 작업이 끝날 때까지 기다려줘!" 라고 프로그램에게 말하는 것과 같습니다.

await 는 무슨 일을 할까요?

await 키워드를 만나면, 다음과 같은 일이 벌어집니다.

  1. 현재 코루틴 일시 중단 (Pause): await 를 사용한 코루틴은 실행을 일시적으로 멈춥니다. 마치 일시 정지 버튼을 누른 것처럼요. ⏸️
  2. 제어권을 이벤트 루프에게 양도 (Yield Control): 일시 중단된 코루틴은 프로그램의 제어권을 이벤트 루프에게 넘겨줍니다. "이제부터는 이벤트 루프 네가 알아서 해!" 라고 말하는 거죠. 🕹️
  3. awaitable 객체 완료 대기 (Wait for Awaitable): await 키워드는 await 뒤에 오는 객체 (주로 다른 코루틴, Task, Future 등) 가 완료될 때까지 기다립니다. 이 객체를 awaitable 객체라고 부릅니다. "내가 기다리는 작업" 이 바로 이 awaitable 객체를 의미합니다. ⏳
  4. 코루틴 재개 (Resume): awaitable 객체가 완료되면, 일시 중단되었던 코루틴은 다시 실행을 시작합니다. 멈췄던 지점부터 다시 코드를 실행하는 거죠. ▶️

await 는 왜 중요할까요? (비동기 프로그래밍의 핵심)

await는 Python asyncio에서 비동기 프로그래밍을 가능하게 하는 핵심입니다. await 덕분에 우리는 다음과 같은 멋진 일들을 할 수 있습니다.

  • Non-blocking I/O: I/O 작업 (네트워크 요청, 파일 읽기 등) 을 기다리는 동안 프로그램이 멈추지 않고 다른 작업을 계속 할 수 있습니다. 효율적인 프로그램의 핵심이죠! 🚀
  • 동시성 (Concurrency): 싱글 스레드 환경에서 여러 작업을 번갈아 가며 처리하여 동시성을 높일 수 있습니다. 마치 여러 개의 일을 동시에 하는 것처럼 느껴지게 만들죠! 👯‍♀️👯‍♂️
  • 코드 가독성 향상: async/await 문법 덕분에 복잡한 비동기 코드를 동기 코드처럼 순차적으로 작성할 수 있어서 코드 이해가 훨씬 쉬워집니다. 👍

await 비유 (식당 주문, 다시 활용!)

여러분이 식당에서 음식을 주문하고 기다리는 상황을 다시 떠올려 볼게요.

  • await 주문.음식준비완료(): "주문한 음식이 준비 될 때까지 기다려줘!"
    • 일시 중단: 여러분은 음식이 나올 때까지 가만히 앉아 기다립니다. (코루틴 일시 중단)
    • 제어권 양도: 웨이터는 여러분 테이블 말고 다른 테이블 주문도 받고, 서빙도 합니다. (이벤트 루프가 다른 작업 처리)
    • 완료 대기: 주방에서 음식이 준비되는 것을 기다립니다. (awaitable 객체 완료 대기)
    • 재개: 음식이 나오면, 여러분은 다시 식사를 시작합니다. (코루틴 재개)

간단한 예제 코드

import asyncio

async def 비동기_작업(이름, 시간):
    print(f"{이름} 작업 시작 ({시간}초 대기)")
    await asyncio.sleep(시간) # <--- await 등장!
    print(f"{이름} 작업 완료")
    return f"{이름} 작업 결과"

async def main():
    결과1 = await 비동기_작업("작업 1", 2) # <--- await 등장!
    결과2 = await 비동기_작업("작업 2", 1) # <--- await 등장!
    print(f"결과 1: {결과1}")
    print(f"결과 2: {결과2}")

if __name__ == "__main__":
    asyncio.run(main())

예제 설명:

  • 비동기_작업() 코루틴 안에서 await asyncio.sleep(시간) 을 사용했습니다. 이 await 때문에 비동기_작업() 코루틴은 asyncio.sleep() 이 완료될 때까지 일시 중단됩니다.
  • main() 코루틴 안에서도 await 비동기_작업(...) 을 사용해서, 비동기_작업() 코루틴이 완료될 때까지 기다립니다.

실행 결과:

작업 1 작업 시작 (2초 대기)
작업 1 작업 완료
작업 2 작업 시작 (1초 대기)
작업 2 작업 완료
결과 1: 작업 1 작업 결과
결과 2: 작업 2 작업 결과

정리

await는 Python 코루틴에서 비동기 프로그래밍의 핵심 역할을 하는 키워드입니다. await 를 사용하면 코루틴을 일시 중단하고, 제어권을 이벤트 루프에 넘겨 다른 작업을 처리할 수 있게 합니다. await 덕분에 Python asyncio는 효율적인 비동기 I/O 프로그래밍을 가능하게 해줍니다.

예제를 통한 코루틴과 await 동작 방식의 이해

아래 예제를 통해서 코루틴과 await가 어떻게 동작하는지 이해해봅시다.

import time 
import asyncio

async def first_coroutine():
    print("First coroutine started")
    await asyncio.sleep(1)
    print("First coroutine done")

async def second_coroutine():
    print("Second coroutine started")
    await asyncio.sleep(1)
    print("Second coroutine done")

async def example1():
    print("### Example 1")
    await first_coroutine()
    await second_coroutine()

async def example2():
    print("### Example 2")
    task1 = asyncio.create_task(first_coroutine())
    task2 = asyncio.create_task(second_coroutine())

    print("# Tasks created")

    await task1
    await task2

async def example3():
    print("### Example 3")
    task1 = asyncio.create_task(first_coroutine())
    task2 = asyncio.create_task(second_coroutine())
    await asyncio.sleep(3)
    print("# Tasks created")

    await task1
    await task2

async def example4():
    print("### Example 4")
    task1 = asyncio.create_task(first_coroutine())
    task2 = asyncio.create_task(second_coroutine())

    print("# Tasks created")


asyncio.run(example1())
asyncio.run(example2())
asyncio.run(example3())
asyncio.run(example4())

각 코루틴별 실행 결과와 그렇게 결과가 나오는 원인을 설명합니다.

Example 1

async def example1():
    print("### Example 1")
    await first_coroutine()
    await second_coroutine()
### Example 1
First coroutine started
First coroutine done
Second coroutine started
Second coroutine done

설명: await는 코루틴이 완료될 때까지 기다립니다. first_coroutine()이 먼저 완전히 실행된 후, second_coroutine()이 순차적으로 실행됩니다. 따라서 "First coroutine" 관련 메시지가 먼저 출력되고, 그 다음에 "Second coroutine" 관련 메시지가 출력됩니다.

Example 2

async def example2():
    print("### Example 2")
    task1 = asyncio.create_task(first_coroutine())
    task2 = asyncio.create_task(second_coroutine())

    print("# Tasks created")

    await task1
    await task2
### Example 2
# Tasks created
First coroutine started
Second coroutine started
First coroutine done
Second coroutine done

설명: asyncio.create_task()는 코루틴을 백그라운드에서 실행하도록 예약하고 즉시 Task 객체를 반환합니다. print("# Tasks created")는 Task 생성 직후 바로 실행됩니다. 그 후 await task1, await task2가 호출되어 Task들이 완료될 때까지 기다리므로, 코루틴들이 병렬적으로 실행되고, "# Tasks created"가 먼저 출력됩니다.

Example 3

async def example3():
    print("### Example 3")
    task1 = asyncio.create_task(first_coroutine())
    task2 = asyncio.create_task(second_coroutine())
    await asyncio.sleep(3)
    print("# Tasks created")

    await task1
    await task2
### Example 3
First coroutine started
Second coroutine started
First coroutine done
Second coroutine done
# Tasks created

설명: create_task로 코루틴들이 시작되고, await asyncio.sleep(3)으로 3초를 기다리는 동안 백그라운드 Task들이 실행됩니다. sleep(3)가 끝난 후 print("# Tasks created")가 출력되지만, Task들은 이미 거의 또는 완전히 완료되었을 가능성이 높습니다. await task1, await task2는 이미 완료된 Task들을 기다리므로 즉시 반환됩니다.

Example 4

async def example4():
    print("### Example 4")
    task1 = asyncio.create_task(first_coroutine())
    task2 = asyncio.create_task(second_coroutine())

    print("# Tasks created")
### Example 4
# Tasks created
First coroutine started
Second coroutine started

설명: create_task로 코루틴들이 백그라운드에서 실행되도록 예약되고, print("# Tasks created")가 즉시 실행됩니다. example4() 함수는 await task1, await task2로 Task의 완료를 기다리지 않고 종료됩니다. 따라서 코루틴들은 백그라운드에서 부분적으로 실행될 수 있지만, 완료되기 전에 프로그램이 종료될 수 있습니다. 결과적으로 코루틴의 "done" 메시지는 출력되지 않았습니다.