
Python Web 개발을 FastAPI로 시작하다보니 비동기 상태에서 Python을 다루는게 너무나 당연했습니다. 그동안 막연히 Python을 비동기적으로 쓰려면 asnyc 를 써야지 생각만해왔습니다. 주말을 맞이해서 이번에 그 기반을 닦아보려고 합니다. 오늘은 그래서 asyncio 는 무엇인지 그리고 왜 필요한지, node.js 그리고 Java 와의 차이점은 무엇인지 등을 알아보고 정리하려 합니다.
asyncio란 무엇인가?
공식 문서에서는 asyncio를 “async/await 문법을 이용하는 동시성(concurrent)처리를 위한 라이브러리" 라고 소개하고 있습니다. 이러한 asyncio 라이브러리를 이용하여 IO bound 되어있는 작업을 효율적으로 처리할 수 있고, 이러한 이유로 고성능 웹 프라임워크, 네트워크 연결, 데이터베이스 연결등의 다른 라이브러리의 근간이 됩니다.
asyncio는 Python에서 비동기 I/O를 지원하기 위한 라이브러리로, async/await 구문을 사용하여 동시성 있는 코드를 작성할 수 있게 해줍니다, 즉 Python 코드에서 동시성을 구현하기 위한 내장 도구입니다.
asyncio를 사용하면 한 번에 여러 작업을 동시에 수행하는 듯한 코드를 작성할 수 있지만, 실제로는 단일 스레드 상에서 비동기적으로 실행됩니다. 이러한 비동기 프로그래밍은 주로 I/O-bound 작업(입출력 대기 시간이 많은 작업)에 적합합니다. 예를 들어 파일 읽기/쓰기, 네트워크 요청 처리, DB 쿼리 등에서 asyncio를 사용하면 한 작업이 I/O를 기다리는 동안 다른 작업을 수행함으로써 효율을 높일 수 있습니다. 실제로 공식 문서에서도 asyncio는 I/O-bound와 고수준 네트워크 코드에 이상적으로 맞는다고 언급합니다. 반면 CPU 연산 위주의 작업에는 asyncio가 적합하지 않을 수 있는데, 이는 뒤에서 자세히 다루겠습니다. 정리하자면 asyncio는 Python에 내장된 “이벤트 루프 기반 비동기 I/O 라이브러리”입니다.
asyncio를 사용하면 개발자가 명시적으로 코루틴(coroutine)을 정의하고, 이벤트 루프를 통해 이들을 동시 실행하여 동시성을 달성할 수 있습니다. 이러한 디자인은 전통적인 스레드 기반 접근과 달리 **협력적 멀티태스킹(cooperative multitasking)**으로 이루어지며, 하나의 스레드안에서 여러 작업이 교대로 실행되는 방식입니다.
asnycio 가 Python 공식 라이브러리로 등장하기 전에는 Twisted나 Gevent 같은 서드파티 비동기 라이브러리가 있었습니다.
Python에서 asyncio가 등장한 배경
Python에서 asyncio가 등장한 배경에는 언어 자체의 실행 모델과 I/O 병목 문제, 그리고 기존 스레드 모델의 한계 등이 있습니다. Python은 전통적으로 동시성 처리를 위한 기본 수단으로 멀티스레딩(threading)과 멀티프로세싱(multiprocessing)을 제공해 왔습니다. 그러나 두 접근법 모두 장단점이 있고, Python만의 제약도 존재합니다.
GIL과 싱글 스레드 중심 구조
Python 인터프리터(CPython 구현)에는 GIL(Global Interpreter Lock)이라는 락이 있습니다. GIL 때문에 한 시점에 하나의 스레드만 Python 바이트코드를 실행할 수 있게됩니다. 그 결과, 멀티스레드를 사용해도 CPU-bound 작업(순수 파이썬 연산이 많은 작업)의 경우 동시에 여러 스레드가 실행되지 못하고 하나씩 실행됩니다. 즉, CPU를 많이 쓰는 작업에서는 스레드를 늘려도 성능 향상이 거의 없고, 오히려 컨텍스트 스위칭 오버헤드로 약간 느려질 수도 있습니다.
Python 3.13 부터는 GIL 제약조건이 약화되었습니다. 하지만 이러한 변화가 바로 asyncio의 장점을 퇴색시키기는 어려울것으로 보입니다. 아래에도 나오지만 CPU 스케줄링의 원리에 따라 스레드가 I/O 작업을 처리할때 다른 스레드가 실행되기 때문에 GIL이 asyncio 가 큰 장점을 발휘하던 I/O에 치명적이지 않기 때문입니다. 다만 Python의 CPU Bound 작업 성능이 크게 개선될것으로 기대됩니다.
I/O-bound 작업의 병목
반면 I/O-bound 작업(파일/네트워크 입출력처럼 대기 시간이 큰 작업)의 경우, GIL 하에서도 스레드가 유용하긴 합니다. 예를 들어 하나의 스레드가 네트워크 응답을 기다리는 동안 GIL을 놓고 다른 스레드가 실행될 수 있기 때문입니다. 하지만 전통적인 스레드 기반 I/O 처리에는 다른 문제가 있습니다. 만약 수천 개의 동시 연결을 처리해야 한다면, 수천 개의 스레드를 생성하는 것은 큰 메모리 소모와 문맥 전환 비용을 초래합니다. Python 스레드는 운영체제 스레드로 구현되므로, 많은 양을 띄우면 OS 스케줄러 부하도 커지고 성능이 떨어집니다.
스레드 기반 모델의 복잡도
스레드를 사용하면 공유 자원 접근 동기화(락 등) 문제, 데드락, 경쟁조건 같은 복잡도가 따라옵니다. 특히 Python에서는 데이터 공유에 대한 동기화를 프로그래머가 신경써야 하고, 디버깅도 어려울 수 있습니다. I/O처리를 위해 단순히 동시성을 얻고 싶었는데, 이러한 부수적인 어려움이 있는 것이죠.

이러한 이유들로, Python 커뮤니티에서는 보다 가벼운 동시성 모델을 필요로 했습니다. 이벤트 루프 기반의 비동기 처리는 이를 해결할 대안으로 떠올랐습니다. 이미 Node.js나 JavaScript 런타임이 이벤트 루프 기반 비동기 처리를 활용하여 높은 동시성을 효율적으로 다루고 있었고, Python 생태계 내에서도 Twisted, Tornado 같은 이벤트 루프 라이브러리가 존재하고 있었습니다. 다만 Python 표준 라이브러리에는 통합된 솔루션이 없었죠. asyncio는 이러한 요구에 따라 Python 3.4 (2014년경)에 도입되었습니다.
Guido van Rossum이 주도한 Tulip프로젝트(PEP 3156)로부터 시작하여 표준화되었으며, 이후 Python 3.5에서 async/await 구문 지원(PEP 492)이 추가되면서 비로소 개발자가 쓰기 편한 형태의 비동기 프로그래밍이 가능해졌습니다. Python 3.7에서 asyncio.run() 함수가 도입되어 이벤트 루프 실행이 더욱 간편해지고, ayncio는 사실상 Python에서 비동기 프로그래밍의 표준이 되었습니다.
Python의 thread와의 차이점
위에서 말한것처럼 Tulip프로젝트 이전에는 Python에서 동시성은 스레드(thread)로 구현되곤 했습니다. asyncio의 코루틴 기반 동시성은 겉보기에는 스레드와 비슷하게 여러 작업을 동시에 처리하지만, 내부 동작과 성능 특성에서 차이가 있습니다. 주요 차이점을 하나씩 살펴보겠습니다.
동시성 구현 방식
- 스레드(Thread): 하나의 프로세스 내에서 여러 OS 스레드를 실행함으로써 동시성을 얻습니다. 운영체제가 CPU 시간을 여러 스레드에 분배해주므로, 스레드들은 선점형 멀티태스킹(preemptive multitasking)을 합니다. 각 스레드는 독립적인 호출 스택을 갖고 병렬로 실행되지만, Python의 경우 GIL로 인해 동일 시점에 하나의 스레드만 실행된다는 제약이 있습니다. 그래도 I/O 대기 중에는 자연스럽게 다른 스레드로 전환되므로 I/O-bound 작업에서 병렬성이 어느 정도 실현됩니다
- asyncio(코루틴): 하나의 스레드 안에서 이벤트 루프(event loop)가 여러 코루틴(coroutine)을 협력적으로 스케줄링합니다. 각 코루틴은 await 키워드를 만나면 자신이 제어권을 양보하여 이벤트 루프가 다른 코루틴을 실행하도록 합니다. 따라서 asyncio의 동시성은 협력적 멀티태스킹(cooperative multitasking)입니다. OS가 강제로 태스크를 바꾸는 것이 아니라, 코루틴들이 스스로 양보하는 것이죠. 이러한 코루틴 전환은 결정적인 시점(주로 I/O 작업 전에 await할 때)에 일어나므로 오버헤드가 적고, 여러 코루틴이 하나의 호출 스택을 공유합니다. 결과적으로, 수천 개의 작업도 단일 스레드에서 효율적으로 관리할 수 있게 됩니다
성능과 리소스 사용

- 스레드의 성능 및 오버헤드: Python에서 스레드는 I/O-bound 작업에 한해 유용하며, 이러한 경우 여러 스레드가 있으면 (GIL로 인해 한 번에 한 스레드씩 실행되지만) 한 스레드가 파일/네트워크 대기 시 다른 스레드가 실행되어 대기 시간을 숨길 수 있습니다. 하지만 스레드를 많이 생성하면 각 스레드는 스택 메모리를 차지하고, 컨텍스트 스위치에 비용이 듭니다.
- asyncio의 성능 특성: asyncio는 수많은 작업을 동시 처리할 때도 메모리 오버헤드가 낮고(OS 스레드를 생성하지 않으므로), 컨텍스트 스위치가 필요할 때만 발생하여 매우 효율적입니다, 즉 스레드보다 적은 자원으로 높은 동시성을 달성합니다
GIL때문에 Python MultiThread 환경에서 CPU Bound 작업의 효율성이 낮아지는것은 언급했습니다. 하지만 그렇기 때문에 asyncio 를 이용한 CPU Bound 작업이 효율적이라고 생각하면 안됩니다. 하나의 코루틴이 CPU를 오래 점유하면(예: 복잡한 계산을 계속하면) await로 양보하기 전까지는 다른 코루틴이 실행되지 못하므로 전체 이벤트 루프가 정지된 것처럼 됩니다. 따라서 CPU 집약적인 일은 asyncio 안에서 하지 않고, 별도의 프로세스나 스레드로 offload 하거나(예: run_in_executor), 계산을 적절히 await로 나눠주어야 합니다.
코드 작성 및 구조
쓰레드 코드: 쓰레드를 사용하려면 각 작업을 함수로 만들고 threading.Thread(target=함수) 방식으로 실행합니다. 결과를 모으려면 join()으로 모든 쓰레드가 끝날 때까지 기다려야 하고, 작업 사이에 공유 데이터가 있다면 Lock 같은 동기화 도구도 직접 사용해야 합니다. 예를 들어 여러 URL을 다운로드하는 코드를 스레드로 짜면 다음과 같습니다:
import threading
import requests
urls = [ ... 리스트 ...]
results = []
def download(url):
resp = requests.get(url) # 네트워크 I/O (블로킹)
results.append(resp.text) # (공유 리스트 접근 - 동기화 필요할 수 있음)
threads = []
for url in urls:
t = threading.Thread(target=download, args=(url,))
t.start()
threads.append(t)
for t in threads:
t.join() # 모든 스레드 작업 완료 대기
print(len(results), "개의 응답 수신 완료")
위 코드에서는 requests.get 호출이 블로킹 I/O이지만, 여러 스레드를 통해 동시 요청이 가능합니다. 다만 results.append는 공유 리스트 접근이라 스레드 간 경합이 발생할 수 있어서, 엄격히 따지면 Lock이 필요할 수도 있습니다. 이렇듯 스레드 코드는 동시성 처리를 비교적 자동으로 OS에 맡기지만, 공유상태 관리 등에서 주의가 필요합니다.
asyncio 코드: asyncio를 사용하면 우선 async def로 코루틴 함수를 정의하고, 그 안에서 await으로 비동기 작업을 호출합니다. 그리고 asyncio.run() 또는 이벤트 루프를 통해 이 코루틴들을 실행합니다. 예를 들어 위와 동일한 작업을 asyncio와 aiohttp로 구현하면 다음처럼 작성할 수 있습니다 (requests는 동기 라이브러리라 사용할 수 없고, aiohttp 같은 비동기 HTTP 클라이언트를 사용해야 합니다):
import asyncio
import aiohttp
urls = [ ... 리스트 ...]
results = []
async def download(url, session):
async with session.get(url) as resp: # 비동기 HTTP GET
text = await resp.text() # 결과 본문 읽기 (비동기 I/O)
results.append(text)
async def main():
async with aiohttp.ClientSession() as session:
tasks = [asyncio.create_task(download(url, session)) for url in urls]
await asyncio.gather(*tasks) # 모든 다운로드 코루틴 병행 실행
print(f"{len(results)}개의 응답 수신 완료")
asyncio.run(main())
위 asyncio 코드는 async with 문으로 HTTP 세션을 관리하고, asyncio.gather로 다수의 다운로드 작업을 동시에 실행합니다. await resp.text() 하는 동안 다른 task들이 실행되므로, 진짜 동시에 네트워크를 기다리고 처리하는 효과를 얻습니다. 또한 코드에서 공유 리스트 results에 append 하는 부분은 한 스레드 내에서 실행되므로 데이터 경합 없이 안전합니다. 전반적으로, asyncio 코드는 동기 코드처럼 순차적 구조를 유지하면서도, await 키워드 덕분에 자연스럽게 동시성을 부여합니다.
상황별 효율성과 선택 기준
- I/O-bound 작업이 많은 서버나 애플리케이션에서는 asyncio가 빛을 발합니다. 예를 들어 웹 크롤러, 대용량 API 호출 처리, 채팅 서버 등 동시에 수천 개의 네트워크 I/O를 해야 하는 경우, 쓰레드로 구현하면 수천 스레드를 만들기 어려운 반면 asyncio는 한 이벤트 루프에서 수천 코루틴을 관리해낼 수 있습니다. 이러한 경우 asyncio가 더 가볍고 확장성 있게(scale) 동작합니다
- CPU-bound 작업(예: 이미지 처리, 머신러닝 연산 등 계산 위주)은 asyncio로 처리하면 오히려 전체가 느려집니다. 이때는 오히려 멀티스레드보다 멀티프로세싱(또는 C 확장 모듈 활용)이 필요합니다. CPU 작업은 여러 코어에서 병렬로 돌려야 빨라지는데, Python 쓰레드는 GIL로 막혀 있으니, 차라리 멀티프로세싱으로 별도 프로세스를 돌리는것이 권장됩니다. 혹은 Python 3.11 이후 도입된 concurrent.futures.ThreadPoolExecutor/ProcessPoolExecutor를 적절히 쓰는 것이 나을 수 있습니다.
I/O 효율적이라고 알려진 node.js 와의 비교

Python의 asyncio를 이야기할 때 자주 비교되는 대상이 Node.js입니다. Python은 Node.js의 이벤트 루프에서 영향을 받아 asyncio를 도입했다고 해도 과언이 아닙니다. 두 환경의 공통점과 차이를 살펴보겠습니다.
이벤트 루프와 싱글 스레드 아키텍처
공통점
Node.js와 Python asyncio 모두 이벤트 루프(event loop)가 핵심입니다. 이벤트 루프는 비동기 작업(네트워크 I/O, 타이머 등)의 완료 이벤트를 감지하고 등록된 콜백이나 코루틴을 실행하는 역할을 합니다. 또한 단일 스레드 기반이라는 점도 같습니다. Node.js의 JavaScript 코드 실행은 기본적으로 하나의 쓰레드에서 일어나고, asyncio도 하나의 메인 쓰레드에서 코루틴들을 처리합니다.
따라서 하나의 작업이 CPU를 너무 오래 점유하면 다른 작업이 지연된다는 점도 공통됩니다.
차이점
이벤트 루프의 시작과 관리 방식이 다릅니다. Node.js에서는 이벤트 루프가 런타임 자체에 내장되어 있습니다. 즉, Node.js로 실행되는 스크립트는 기본적으로 이벤트 루프 위에서 동작하며, 모든 I/O API가 비동기(논블로킹)로 설계되어 있습니다. 반면 Python에서는 이벤트 루프가 기본 활성화되어 있지 않고, asyncio를 사용할 때 명시적으로 asyncio.run() 등을 호출하여 이벤트 루프를 돌립니다.
Python은 필요할 때만 이벤트 루프를 “켜는” 온/오프 개념이고, Node.js는 항상 이벤트 루프가 동작하고 있는 형태라고 볼 수 있습니다.
또 다른 차이는 언어와 런타임의 역사에서 비롯됩니다. Node.js (그리고 브라우저의 JS)는 원래부터 비동기 작업을 콜백으로 처리해왔고, 이후 Promise와 async/await을 도입하여 개선했습니다. Python은 전통적으로 동기/블로킹 함수들이 많고, asyncio 도입 후에도 기존 라이브러리 상당수가 동기 방식으로 남아 있습니다. 그래서 Python 개발자는 동기 코드와 비동기 코드의 호환을 고민해야 하는 경우가 많습니다. (예: Django ORM은 동기이기 때문에 asyncio 웹 서버에서 사용할 때 별도의 쓰레드풀로 돌리는 등 추가 작업이 필요함)
성능 및 활용 차이
Node.js와 Python asyncio 중 어떤 것이 성능이 더 좋냐는 단순 비교는 어렵습니다. Node.js는 V8 엔진으로 구동되어 JavaScript가 JIT 컴파일된다는 이점이 있고, Python은 인터프리터라 단순 반복문 같은 CPU 작업은 JS보다 느릴 수 있습니다. 하지만 I/O 처리 성능은 양쪽 다 훌륭하며, 큰 차이가 없습니다. 둘 다 커널 비동기 I/O 기술(epoll 등)을 활용하고 최적화되어 있어 수만 개 소켓 연결 처리도 가능한 수준입니다.
차이가 있다면, Python asyncio는 선택적이므로, 상황에 따라 동기 코드와 혼용이 가능하고 필요시 멀티스레드/멀티프로세스로 전환이 가능합니다. Node.js는 기본적으로 싱글 스레드이므로, CPU-bound 처리에 한계를 느낄 땐 Cluster/fork나 Worker Threads 등을 사용해야 합니다.
결국 Node vs Python asyncio는 동시성 모델의 철학은 비슷하나, Python이 동기와 비동기 모델을 모두 포괄하는 유연함이 있고 Node는 일관되게 비동기라는 차별점이 있습니다.
asyncio 를 이용하는 방법 (기본 사용법 정리)
기본적인 코루틴 정의부터 asyncio.run()으로 실행, 여러 코루틴을 한꺼번에 실행하는 asyncio.gather(), 그리고 비동기 반복문(async for), 컨텍스트 매니저(async with), 마지막으로 코루틴에서의 yield 사용법까지 하나씩 예제 코드와 함께 정리해보겠습니다.
코루틴 정의와 asyncio.run()
코루틴(coroutine)은 Python에서 async/await 기능의 핵심 단위입니다. 코루틴은 async def로 정의된 함수로, 호출 시 즉시 실행되는 것이 아니라 코루틴 객체를 반환합니다. 이 코루틴 객체는 “미래에 실행될 작업”으로 생각하면 됩니다. 아래에 정의된 hello 라는 코루틴을 실제로 실행하려면 이벤트 루프가 필요합니다.
import asyncio
async def hello():
print("Hello ...")
await asyncio.sleep(1) # 1초 비동기 대기 (다른 작업에 양보)
print("... World!")
# 코루틴 함수를 호출해도 바로 실행되지 않음
coro = hello()
print(coro)
# <coroutine object hello at 0x...>
asyncio.run()은 이벤트 루프를 시작하고 주어진 코루틴을 최종 완료까지 실행해주는 편의 함수입니다. 보통 asyncio.run(main()) 형태로, 하나의 메인 코루틴을 실행하는 데 사용합니다. asyncio.run이 내부에서 이벤트 루프를 새로 만들어 main() 코루틴을 실행하고, 끝나면 루프를 정리해줍니다. Python 3.7+에서 권장되는 진입점 실행 방식이죠.
async def main():
print("작업 시작")
await hello() # 위에서 정의한 코루틴을 await로 실행
print("모든 작업 완료")
asyncio.run(main()) # 이벤트 루프를 열고 main 코루틴 실행
위 코드를 실행하면 main() 코루틴 내부에서 await hello()를 만나 hello() 코루틴으로 들어갔다가, hello가 1초 쉬는 동안 다시 main으로 복귀… 이런 식으로 동작하며, 결과적으로 "Hello ..." (즉시 출력) → 1초 대기 → "... World!" → "모든 작업 완료" 순으로 출력됩니다.
핵심은, await 키워드입니다. await는 코루틴의 실행을 일시 중지하고(await 키워드 뒤의 작업 완료를 기다린 뒤) 다시 재개시킵니다. await를 호출하면 현재 코루틴은 이벤트 루프에게 제어권을 넘기고, 그 사이에 다른 코루틴이 실행될 수 있습니다.
흐름을 정리하면 다음과 같습니다.
- async def func(): ... : 코루틴 정의.
- await something : 코루틴 내에서 다른 코루틴/작업이 끝날 때까지 non-blocking 대기.
- asyncio.run(coro) : 코루틴 실행을 위해 이벤트 루프 생성 및 돌입.
동시에 여러 코루틴 실행: asyncio.gather()와 create_task()
await를 사용하면 순차적으로 코루틴을 실행하게 됩니다. 만약 여러 코루틴을 동시에 실행하고 싶다면 어떻게 해야 할까요? 대표적으로 asyncio.gather()와 asyncio.create_task()를 활용합니다.
- asyncio.create_task(coro): 주어진 코루틴을 백그라운드 Task로 스케줄링합니다. 즉, 즉시 해당 코루틴 실행을 시작하고, Task 객체를 반환합니다. Task는 미래에 결과를 담게 되는 Future의 한 종류라고 볼 수 있습니다.
- asyncio.gather(*coros): 여러 코루틴들을 동시에 실행하고 모두 완료될 때까지 기다린 뒤 결과를 한꺼번에 반환합니다. 내부적으로 각 코루틴을 Task로 만들고 추적합니다.
3개의 작업을 동시에 실행하여 실행 시간을 단축해보겠습니다. 각 작업은 1초씩 걸린다고 가정합니다:
import asyncio
import time
async def work(id):
print(f"작업 {id} 시작")
await asyncio.sleep(1) # 1초짜리 비동기 작업 (예: 네트워크 요청)
print(f"작업 {id} 완료")
return id
async def main():
start = time.time()
# 방법 1: asyncio.gather로 동시에 실행
results = await asyncio.gather(work(1), work(2), work(3))
end = time.time()
print("동시 실행 결과:", results)
print(f"총 실행 시간: {end - start:.2f}초")
asyncio.run(main())
다음과 같은 결과가 출력됩니다.
작업 1 시작
작업 2 시작
작업 3 시작
작업 1 완료
작업 2 완료
작업 3 완료
동시 실행 결과: [1, 2, 3]
총 실행 시간: 1.00초 (내외)
asyncio.gather를 사용하지 않고, create_task를 사용해서 다음과 같이 할 수 있습니다:
async def main():
start = time.time()
# 방법 2: create_task를 사용하여 태스크 생성 후 개별 await
task1 = asyncio.create_task(work(1))
task2 = asyncio.create_task(work(2))
task3 = asyncio.create_task(work(3))
# 이 시점에서 작업들이 백그라운드로 진행 중
result1 = await task1
result2 = await task2
result3 = await task3
end = time.time()
print("동시 실행 결과:", [result1, result2, result3])
print(f"총 실행 시간: {end - start:.2f}초")
create_task로 태스크를 생성하면 즉시 작업이 시작되므로, 바로 await를 하지 않고 있다면 다른 코드도 동시에 실행될 수 있습니다. 위 예에서 task1 생성 후 task2를 생성하는 동안도 task1의 작업은 진행 중입니다. 결국 await task1 등을 통해 결과를 모을 때까지 3개 작업이 병행되는 것이죠. asyncio.gather는 이런 패턴을 한 줄로 축약한 것이라 볼 수 있습니다.
에러 처리
asyncio.gather는 기본적으로 내부 태스크 중 하나에서 예외가 발생하면 즉시 예외를 전파하고 다른 태스크도 취소합니다. 반면 return_exceptions=True 옵션을 주면, 예외도 결과로 모아서 반환해줍니다. 일반적으로는 예외 처리를 개별 코루틴 내에서 하거나, gather 바깥에서 try/except로 묶어 처리합니다.
asyncio.wait
gather와 비슷한 asyncio.wait도 있는데, 이건 태스크 집합을 주면 완료된 것과 안 된 것을 구분해서 돌려주는 저수준 함수입니다. 대부분의 경우 gather가 더 편리하고, Python 공식문서에서도 저수준 API를 직접 다루는 것을 권장하지 않기 때문에 깊이 다루지 않겠습니다.
비동기 이터레이션: async for (비동기 제너레이터)
async for는 비동기 이터레이터/제너레이터와 함께 사용되는 구문입니다. 일반적인 for문은 이터레이터의 __iter__/__next__를 호출해 동작하는데, 비동기 이터레이터는 __anext__라는 비동기(next) 메서드를 가집니다. 그리고 async for 루프는 이 __anext__가 반환하는 코루틴을 await하면서 루프를 돕니다. 말이 어렵지만, 실제로 언제 쓰이냐 하면
- 파일이나 소켓에서 비동기적으로 한 줄씩 읽어들일 때 (aiofiles나 asyncio.StreamReader 등은 async iterator를 지원하여 async for line in reader 형태로 쓸 수 있음).
- 대기 시간이 있는 데이터를 스트리밍 받을 때 (예: 비동기 발생 이벤트 소비)
아울러 비동기 제너레이터(async generator)도 있습니다. 이는 async def 함수 안에서 yield 키워드를 써서 비동기적으로 값을 순차 생성하는 함수입니다. PEP 525에 도입된 기능으로, 이러한 함수는 호출 시 AsyncGenerator 객체를 반환하며, 이는 앞서 말한 비동기 이터레이터 프로토콜을 따릅니다. 비동기 제너레이터는 async for로 순회해야 합니다.
간단한 비동기 제너레이터와 async for 사용법을 보겠습니다
# 비동기 제너레이터 함수 정의
async def async_count(stop):
for i in range(stop):
await asyncio.sleep(1) # 매 반복마다 1초 비동기 대기
yield i # 값을 산출 (yield) - async generator
async def main():
# async for를 사용하여 비동기 제너레이터로부터 값 받기
async for num in async_count(3):
print(f"{num} 출력 (시간: {time.time():.0f})")
asyncio.run(main())
각 숫자가 1초 간격으로 출력되는 것을 볼 수 있습니다. async_count 함수는 매 loop마다 await asyncio.sleep(1)으로 1초 쉬고 yield i로 값을 내보냅니다. 이처럼 async def 내에서 yield를 쓰면 일반 함수의 제너레이터와 달리 Async Generator가 됩니다. 그리고 async for num in async_count(3) 구문이 내부적으로 다음과 같이 동작합니다. 개발자는 async for 구문만 써주면 이런 복잡한 처리가 자동으로 이루어집니다.
iterator = async_count(3).__aiter__()
while True:
try:
num = await iterator.__anext__()
except StopAsyncIteration:
break
# loop body
print(num)
비동기 컨텍스트 매니저: async with
async with 구문은 비동기 컨텍스트 매니저를 사용할 때 쓰입니다. 일반적인 with 문은 객체의 __enter__와 __exit__를 호출해서 자원 관리를 하는데, 비동기 상황에서는 __aenter__와 __aexit__라는 비동기 메서드를 사용합니다. 예를 들어, asyncio.Lock은 비동기 락 객체인데, 이것을 async with로 획득/해제할 수 있습니다. 또 aiohttp의 ClientSession이나 데이터베이스의 AsyncSession 등도 async with로 열고 닫는 처리를 합니다.
참고
Python document, asyncio: https://docs.python.org/ko/3/library/asyncio.html
Python document, corruines and task: https://docs.python.org/3/library/asyncio-task.html#coroutine
Python document, eventloop: https://docs.python.org/3/library/asyncio-eventloop.html
Python Asyncio vs Threading: https://www.geeksforgeeks.org/asyncio-vs-threading-in-python/
Async IO in Python: A Complete Walkthrough: https://www.geeksforgeeks.org/asyncio-vs-threading-in-python/
'탐구 생활 > python' 카테고리의 다른 글
Python의 Type System (0) | 2025.03.20 |
---|---|
python 사용자 지정 불변 객체를 만드는 3가지 방법 (0) | 2024.11.17 |
Python 가변 객체와 불변 객체 (0) | 2024.11.17 |