파이썬 async·await — asyncio
기다리는 동안 다른 일을. 100개 URL 호출이 1초 안에 끝나는 비밀.
20편의 requests 로 URL 100개를 호출하면 한 번에 평균 1초씩, 총 100초. 그런데 사실 컴퓨터가 일한 시간은 0.1초도 안 됩니다 — 99% 가 서버 응답을 기다리는 시간이거든요. 한 번에 한 줄씩 순서대로 기다리는 게 비효율인 거죠. asyncio 가 이걸 해결합니다 — 한 요청이 기다리는 동안 다음 요청을 보내고, 응답이 오는 대로 처리. 100초 작업이 1-2초로 줍니다.
24편을 마치면 ① async def·await 문법 ② asyncio.gather 로 동시 실행 ③ httpx 같은 async 라이브러리 ④ 동시성 vs 병렬성 ⑤ 언제 async 가 효과 있는지 — 5가지가 손에 익습니다.
async 의 첫 모습
(.venv) $ pip install httpx
import asyncio
import httpx
async def fetch(url: str) -> int:
async with httpx.AsyncClient(timeout=10) as client:
r = await client.get(url)
return r.status_code
async def main():
urls = ["https://example.com" for _ in range(5)]
# 순차 — 5초
# for u in urls: await fetch(u)
# 동시 — 1초
results = await asyncio.gather(*(fetch(u) for u in urls))
print(results) # [200, 200, 200, 200, 200]
asyncio.run(main())
4가지 키워드.
- async def — 이 함수는 코루틴(coroutine). 호출해도 즉시 안 실행, 코루틴 객체만 반환
- await — "이 코루틴 결과를 기다린다". 기다리는 동안 다른 코루틴이 실행될 수 있게 양보
- asyncio.gather — 여러 코루틴을 동시에 시작, 모두 끝나면 결과 리스트
- asyncio.run — 일반 코드에서 async 코드를 시작하는 진입점
동시성 vs 병렬성 — 다른 개념
📌 같은 일을 빠르게 하는 두 방법
· 동시성(concurrency, asyncio) — "한 사람이 여러 일 동시에 신경 쓰기". CPU 1개로 충분, IO 가 많은 작업(웹 호출·DB·파일)에 적합. asyncio 의 영역
· 병렬성(parallelism, multiprocessing) — "여러 사람이 진짜 동시에 일하기". CPU 여러 개 필요, 계산 무거운 작업(이미지 처리·머신러닝)에 적합. multiprocessing 의 영역
asyncio 가 100배 빨라지는 이유는 "기다리는 시간이 길어서". 1초 걸리는 HTTP 호출 중 99% 가 그냥 기다림이라 그 사이에 다른 호출을 끼워넣는 것. CPU 가 진짜 일하는 작업(예: 큰 행렬 곱)은 asyncio 로 빨라지지 않습니다 — 그건 multiprocessing 또는 NumPy.
async 라이브러리 — 쓰던 라이브러리의 형제
# requests → httpx (또는 aiohttp)
import httpx
async with httpx.AsyncClient() as c:
r = await c.get(url)
# sqlite3 → aiosqlite
import aiosqlite
async with aiosqlite.connect("db.sqlite") as db:
async with db.execute("SELECT * FROM users") as cur:
rows = await cur.fetchall()
# open → aiofiles
import aiofiles
async with aiofiles.open("log.txt") as f:
text = await f.read()
# time.sleep → asyncio.sleep
await asyncio.sleep(2)
거의 모든 동기 라이브러리에 async 짝이 존재합니다. requests 의 async 짝은 httpx. async 코드 안에서 동기 함수(time.sleep·requests.get)를 부르면 그 동안 전체가 멈춰버려요 — 같은 그룹의 async 버전을 써야 합니다.
실전 패턴 — 100개 동시 호출 안전판
import asyncio, httpx
async def fetch(client: httpx.AsyncClient, url: str, sem: asyncio.Semaphore) -> dict:
async with sem: # 동시 실행 제한 (rate-limit)
try:
r = await client.get(url, timeout=10)
r.raise_for_status()
return {"url": url, "status": r.status_code, "size": len(r.content)}
except Exception as e:
return {"url": url, "error": str(e)}
async def main(urls: list[str]):
sem = asyncio.Semaphore(20) # 최대 20개 동시
async with httpx.AsyncClient() as client:
tasks = [fetch(client, u, sem) for u in urls]
results = await asyncio.gather(*tasks, return_exceptions=False)
return results
asyncio.run(main(["https://example.com"] * 100))
Semaphore 가 동시 실행 개수를 제한 — 1000개 URL 을 한꺼번에 던지면 본인 네트워크도 막히고 상대 서버도 차단당해요. 20-50개가 안전한 기본값.
requests·동기 코드가 훨씬 단순. 같은 코드를 async 로 바꾸는 건 비용이 있어요(전부 await 추가·async 라이브러리 사용·디버깅 어려움). 여러 IO 를 동시에 처리할 때만 가치가 있습니다.
다음 미션: ① 20편의 GitHub 사용자 5명 정보를 순차/동시 호출로 비교 (시간 측정) ② asyncio.sleep 으로 가짜 작업 만들어 동작 확인 ③ Semaphore 로 동시 5개 제한.
다음 편 미리보기
25편 — "pytest 사용법": 자동 테스트 첫 발걸음. assert·fixture·매개변수 테스트로 자신감 있게 리팩토링.
← 23편 "웹 크롤링" · 다음: 25편 "pytest"