Python asyncio와 aiohttp로 API 병렬 호출 최적화
문제 상황
데이터 수집 배치 작업이 있었다. 외부 API 100여 개를 순차적으로 호출하는데 평균 25분이 소요됐다. 각 API 응답 시간은 2~3초 정도였지만, 순차 처리라 전체 시간이 길었다.
기존 코드는 requests 라이브러리로 단순하게 구현되어 있었다.
import requests
def fetch_data(urls):
results = []
for url in urls:
response = requests.get(url)
results.append(response.json())
return results
asyncio 전환
aiohttp를 사용해 비동기 처리로 전환했다. Python 3.9 환경이라 async/await 문법을 안정적으로 사용할 수 있었다.
import asyncio
import aiohttp
async def fetch_one(session, url):
async with session.get(url) as response:
return await response.json()
async def fetch_data(urls):
async with aiohttp.ClientSession() as session:
tasks = [fetch_one(session, url) for url in urls]
return await asyncio.gather(*tasks)
# 실행
results = asyncio.run(fetch_data(urls))
에러 핸들링
초기 구현에서 일부 API가 타임아웃되면 전체 작업이 실패하는 문제가 있었다. return_exceptions=True 옵션으로 해결했다.
async def fetch_one(session, url):
try:
async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as response:
return await response.json()
except Exception as e:
return {"error": str(e), "url": url}
async def fetch_data(urls):
async with aiohttp.ClientSession() as session:
tasks = [fetch_one(session, url) for url in urls]
return await asyncio.gather(*tasks, return_exceptions=True)
결과
- 실행 시간: 25분 → 5분
- 에러율: 동일 (타임아웃 발생 건수 변화 없음)
- 메모리 사용량: 큰 차이 없음
동시 연결 수가 많아 일부 API에서 rate limit 에러가 발생했다. asyncio.Semaphore로 동시 요청 수를 20개로 제한하니 안정화됐다.
async def fetch_data(urls, concurrency=20):
semaphore = asyncio.Semaphore(concurrency)
async def fetch_with_limit(session, url):
async with semaphore:
return await fetch_one(session, url)
async with aiohttp.ClientSession() as session:
tasks = [fetch_with_limit(session, url) for url in urls]
return await asyncio.gather(*tasks)
I/O bound 작업에서는 asyncio가 확실히 효과적이었다.