Python asyncio로 API 병렬 처리 성능 개선
문제 상황
데이터 동기화 배치 작업이 외부 API를 약 2000번 호출하는데, 순차 처리로 인해 30분 이상 소요되고 있었다. 각 API 호출은 I/O 대기 시간이 대부분이라 병렬 처리로 개선 여지가 충분했다.
asyncio 적용
기존 requests 대신 aiohttp를 사용해 비동기 처리로 전환했다.
import asyncio
import aiohttp
from typing import List
async def fetch_data(session: aiohttp.ClientSession, item_id: str):
url = f"https://api.example.com/items/{item_id}"
try:
async with session.get(url, timeout=10) as response:
return await response.json()
except asyncio.TimeoutError:
print(f"Timeout: {item_id}")
return None
except Exception as e:
print(f"Error {item_id}: {e}")
return None
async def process_batch(item_ids: List[str]):
connector = aiohttp.TCPConnector(limit=50)
async with aiohttp.ClientSession(connector=connector) as session:
tasks = [fetch_data(session, item_id) for item_id in item_ids]
results = await asyncio.gather(*tasks, return_exceptions=True)
return [r for r in results if r is not None]
if __name__ == "__main__":
item_ids = [...] # 2000개 항목
results = asyncio.run(process_batch(item_ids))
동시성 제어
TCPConnector의 limit으로 동시 연결 수를 50으로 제한했다. 처음엔 제한 없이 실행했다가 서버 측에서 429 에러가 발생했다. Semaphore를 추가해 더 세밀한 제어도 가능하지만, 현재는 connector limit만으로 충분했다.
결과
- 실행 시간: 30분 → 4분 30초
- 에러율: timeout 설정으로 일부 느린 응답 스킵
- 재시도 로직은 별도로 구현해 실패 건만 처리
처음 asyncio를 적용할 땐 이벤트 루프 개념이 낯설었지만, I/O 바운드 작업에서는 확실히 효과적이었다. 다만 디버깅이 동기 코드보다 까다로워 로깅을 꼼꼼히 남기는 게 중요했다.