프로그래밍

아는척 하기 시리즈 - async / await, 코루틴, 그리고 이벤트 루프의 진실

근면한 거위 2025. 6. 1. 14:53
“asyncio? 멀티스레드랑 비슷한 거 아냐?”
“await 하면 함수가 블로킹된다던데….”

“이벤트 루프가 신호를 받아서 뭘 한다는데, 정확히 뭘 받는다는 건데?”

 

비동기 프로그래밍을 처음 접하면 이런 물음표가 끊임없이 튀어나온다.

 

이번 글은 Python asyncio 를 기준으로, async def, await, 코루틴, 이벤트 루프, 그리고 운영체제(OS)와의 상호작용까지 한 번에 정리해 준다. 다 읽고 나면 “아, 그래서 await가 ‘양보’ 라는 말이었구나” 하고 고개를 끄덕일 수 있을 것이다.

 

예의상 공식문서는 읽어보도록 하자:
https://docs.python.org/3/library/asyncio.html

 

asyncio — Asynchronous I/O

Hello World!: asyncio is a library to write concurrent code using the async/await syntax. asyncio is used as a foundation for multiple Python asynchronous frameworks that provide high-performance n...

docs.python.org

 

1. 비동기 프로그래밍이 필요한 이유

컴퓨터 프로그램은 종종 기다려야 하는 작업을 만난다.

  • 데이터베이스 쿼리
  • 외부 API 호출
  • 디스크 읽기/쓰기

전통적인(동기) 코드는 이때 CPU가 멍하니 대기한다.

비동기 코드는 “기다릴 동안 다른 일부터 처리하자”는 방식을 택한다.

덕분에 동일 스레드로도 처리량(throughput)을 크게 끌어올릴 수 있다.

2. async def : 실행이 아니라 “예약”이다

async def greet():
    print("안녕!")

coro = greet()         # 아직 “안녕!” 안 찍힘
print(coro)            # <coroutine object greet at 0x...>

  • async def로 선언된 함수는 호출 즉시 코루틴 객체를 반환한다.
  • 실행은 안 된다. 단지 “언제든 실행할 수 있는 스크립트”가 손에 들린 셈이다.

3. await : 지금 멈추고, 제어권을 이벤트 루프에 양보

await greet()          # 여기서 비로소 “안녕!” 출력
  • await는 “이 코루틴이 끝날 때까지 내 실행을 잠시 중단하고,
  • 이벤트 루프야 다른 일 먼저 처리해”라는 의미다.
  • 전체 프로그램이 멈추는 것이 아니라,“현재 코루틴만” 일시 정지한다.

4. 이벤트 루프(event loop) : 한 줄짜리 while 문

asyncio의 이벤트 루프는 단순하다.

한마디로 “어떤 코루틴을 지금 실행할지” 를 무한히 결정하는 while 문이다.

while True:
    (1) 끝난 Task / Future 확인 → 콜백 실행
    (2) 타이머 만료 확인
    (3) OS에서 I/O 완료 이벤트 수신
    (4) 실행 준비된 코루틴 하나 꺼내 실행

  • 싱글 스레드다.
  • 코루틴이 await 를 만나면 (1)~(3)을 돌며 다른 작업을 수행한 뒤,
  • 다시 (4)에서 해당 코루틴을 이어서 실행한다.

5. “OS가 신호를 보낸다”는 말의 정체

네트워크 요청을 예로 들자.

  1. 코루틴이 await socket_recv() 를 호출한다.
  2. asyncio는 non-blocking 소켓을 OS에 등록한다
  3. (Linux : epoll, macOS : kqueue, Windows : IOCP).
  4. 코루틴은 일시 중단되고, 이벤트 루프는 다른 작업을 실행한다.
  5. 패킷이 도착하면 커널이 “읽을 수 있다”는 이벤트(신호)를 발생시킨다.
  6. 이벤트 루프가 이 신호를 감지하고, 중단된 코루틴을 재개한다.

“신호” = 커널 I/O 완료 알림 이다.

await는 결국 OS에게 의뢰 → 이벤트 루프가 알림을 대기 → 완료되면 다시 실행

 

이 과정을 코드 한 줄로 표현한 셈이다.

6. 병렬(parallel) vs 병행(concurrent)

구분 설명
병렬 (parallel) 여러 CPU / 스레드가 동시에 서로 다른 코드를 실행
병행 (concurrent) 하나의 CPU가 빠르게 번갈아 작업을 실행해 “동시에” 보이게 함

 

asyncio는 병행 모델이다.

같은 스레드에서 코루틴이 양보와 재개를 반복하며 CPU를 공유한다.

멀티코어를 쓰고 싶으면 multiprocessing이나 loop.run_in_executor(ThreadPool/ProcessPool) 같은 별도 방식을 써야 한다.

7. 블로킹 함수가 문제를 일으키는 이유

import requests

async def bad():
    r = requests.get("<https://example.com>")   # 블로킹
    print(r.text)

  • requests.get()은 내부에서 time.sleep()류 블로킹 I/O를 사용한다.
  • 코루틴이 중단되지 않으므로 이벤트 루프가 멈춘다.
  • 비동기의 장점이 0%.

해결책 → aiohttp, aiomysql, aioredis처럼 awaitable 라이브러리를 써야 한다.

8. 전체 흐름 한 눈에 보기

async def 작업():
    await I/O    ----+
                     |
[이벤트 루프] <----+(중단된 코루틴 기록)
    |
    | OS I/O 완료 이벤트 도착
    v
(중단 지점부터 재개)

 

9. 흔한 오해 — 그리고 정확한 한 줄 대답

오해 올바른 설명
“asyncio는 멀티스레드다” 아니다. 기본은 싱글 스레드다.
“await는 블로킹이다” 아니다. 현재 코루틴만 중단하고, 이벤트 루프는 계속 돈다.
“신호를 보낸다 = 파이썬 내부 이벤트” 아니다. OS가 보내는 I/O 완료 알림이다.

10. 진짜 아는 척하려면 이렇게 말하자

“await는 OS에 I/O 맡기고, 커널의 epoll(kqueue·IOCP) 이벤트를 asyncio 루프가 받으면 그때 다시 코루틴을 재개하는 구조야.

싱글 스레드지만 I/O 대기를 100% 활용하니까 고성능이 나오는 거지.”

 

마무리

asyncio를 이해하려면 양보( await ), 감시( event loop ), 그리고 OS와의 약속( readiness event )

이 세 가지만 기억하면 된다.

 

이제 await를 만날 때마다 “지금 양보했고, 커널이 알림 주면 다시 달린다”고 자연스럽게 떠올린다면 된다.