Python asyncio로 외부 API 호출 성능 개선하기
문제 상황
매일 새벽 실행되는 배치 작업에서 외부 결제 API를 호출해 주문 상태를 동기화하는 로직이 있었다. 약 500건의 주문을 순차적으로 처리하다 보니 전체 실행 시간이 10분을 넘어갔고, 타임아웃으로 실패하는 경우도 발생했다.
import requests
def sync_orders(order_ids):
results = []
for order_id in order_ids:
response = requests.get(f'https://api.payment.com/orders/{order_id}')
results.append(response.json())
return results
asyncio 도입
Python 3.7부터 안정화된 asyncio를 활용해 비동기 처리로 전환했다. requests 대신 aiohttp를 사용하고, asyncio.gather로 여러 요청을 동시에 처리하도록 수정했다.
import asyncio
import aiohttp
async def fetch_order(session, order_id):
url = f'https://api.payment.com/orders/{order_id}'
async with session.get(url) as response:
return await response.json()
async def sync_orders_async(order_ids):
async with aiohttp.ClientSession() as session:
tasks = [fetch_order(session, order_id) for order_id in order_ids]
results = await asyncio.gather(*tasks, return_exceptions=True)
return results
# 실행
results = asyncio.run(sync_orders_async(order_ids))
세마포어로 동시 요청 수 제한
처음에는 500개 요청을 한 번에 보냈더니 외부 API 서버에서 429 에러가 발생했다. asyncio.Semaphore로 동시 요청 수를 20개로 제한했다.
async def sync_orders_with_limit(order_ids, limit=20):
semaphore = asyncio.Semaphore(limit)
async def fetch_with_semaphore(session, order_id):
async with semaphore:
return await fetch_order(session, order_id)
async with aiohttp.ClientSession() as session:
tasks = [fetch_with_semaphore(session, order_id) for order_id in order_ids]
results = await asyncio.gather(*tasks, return_exceptions=True)
return results
결과
- 실행 시간: 10분 → 50초로 단축
- 타임아웃 에러 해결
- CPU 사용률은 거의 증가하지 않음 (I/O bound 작업이라)
재택 근무하면서 배치 작업 개선할 시간이 생겨서 미뤄뒀던 작업을 처리할 수 있었다. asyncio는 I/O bound 작업에서 확실히 효과적이었다.