async / await 문법

async def로 코루틴을 정의하고 await로 결과를 기다리는 문법의 정확한 의미, awaitable의 종류, async for와 async with, 코루틴과 제너레이터의 차이를 정리합니다.

· 5 min read · PALDYN Team

지난 글에서 작업을 취소하고 타임아웃을 거는 법을 다뤘다. 지금까지 asyncawait를 자연스럽게 써 왔지만, 이 두 키워드가 정확히 무엇을 의미하는지 한 번 짚고 넘어갈 필요가 있다. 문법의 의미를 정확히 알면 흔한 오류를 피할 수 있다.

async def는 코루틴 함수를 만든다

async def로 정의한 함수는 일반 함수가 아니라 코루틴 함수다. 이 함수를 호출하면 본문이 실행되는 대신 코루틴 객체가 반환된다. 실제 실행은 그 코루틴을 await하거나 루프에 등록해야 시작된다.

async def와 await의 역할

async def add(a, b):
    return a + b

coro = add(1, 2)     # 본문은 아직 실행 안 됨
print(type(coro))    # <class 'coroutine'>

import asyncio
print(asyncio.run(add(1, 2)))   # 3 — 여기서 실행

await는 결과를 기다리며 양보한다

await는 두 가지를 동시에 한다. 첫째, 대상이 끝날 때까지 현재 코루틴을 멈추고 결과를 받아 온다. 둘째, 멈춰 있는 동안 이벤트 루프에 제어권을 양보해 다른 코루틴이 실행되게 한다. 이 양보가 asyncio 동시성의 핵심이다.

await는 반드시 async def 함수 안에서만 쓸 수 있다. 일반 함수나 모듈 최상단에서 쓰면 SyntaxError가 난다.

await할 수 있는 것 — awaitable

아무것이나 await할 수 있는 것은 아니다. await의 대상은 awaitable이어야 한다. awaitable에는 세 종류가 있다.

awaitable의 세 종류

import asyncio

async def main():
    # 1) 코루틴
    await asyncio.sleep(0.1)
    # 2) Task / Future
    task = asyncio.create_task(asyncio.sleep(0.1))
    await task
    # 3) await 5  → TypeError: object int can't be used in 'await' expression

asyncio.run(main())

일반 값(정수, 문자열, 리스트 등)은 awaitable이 아니므로 await 대상이 될 수 없다. 동기 함수의 반환값을 await하려다 TypeError를 만나는 것이 초보자의 흔한 실수다.

async for와 async with

async는 반복문과 컨텍스트 매니저에도 붙는다. 비동기 이터레이터를 도는 async for는 각 항목을 가져올 때마다 await할 수 있고, async with는 진입/종료 시점에 비동기 작업(예: 비동기 연결 열고 닫기)을 수행한다.

import asyncio

async def main():
    # 비동기 컨텍스트 매니저 (예: DB 연결, HTTP 세션)
    async with open_connection() as conn:
        # 비동기 이터레이터에서 한 줄씩
        async for row in conn.stream("SELECT ..."):
            process(row)

이 둘도 async def 안에서만 쓸 수 있다. 비동기 이터레이터와 컨텍스트 매니저의 구현 자체는 시리즈의 별도 글에서 자세히 다룬다.

코루틴은 제너레이터의 사촌이다

내부적으로 코루틴은 제너레이터와 비슷한 메커니즘 위에 서 있다. 둘 다 실행 도중에 멈췄다가 재개할 수 있다. 차이는 제너레이터가 yield값을 바깥에 내보내며 멈추는 반면, 코루틴은 await다른 작업의 완료를 기다리며 멈춘다는 점이다.

# 제너레이터: 값을 생산하며 일시정지
def counter():
    n = 0
    while True:
        yield n      # 값을 내보내고 멈춤
        n += 1

# 코루틴: 다른 작업을 기다리며 일시정지
async def fetch_all(urls):
    for url in urls:
        await download(url)   # 완료를 기다리며 멈춤

이 구조적 유사성 덕분에 둘 다 “중단 가능한 함수”라는 같은 뿌리에서 출발한다. 다음 글에서는 asyncio에서 가장 위험한 함정인, 블로킹 I/O가 루프 전체를 멈추는 문제와 그 해결책을 다룬다.


지난 글: asyncio 작업 취소

다음 글: 블로킹 I/O와 asyncio


읽어주셔서 감사합니다. 😊