Python 비동기 처리 도입 후기 - asyncio와 aiohttp
문제 상황
운영 중인 데이터 수집 서버가 순차적으로 API를 호출하다보니 하루 배치 작업이 4시간 넘게 걸렸다. 외부 API 응답 대기 시간이 대부분이라 병렬 처리가 필요했다.
기존 코드
import requests
def fetch_data(urls):
results = []
for url in urls:
response = requests.get(url)
results.append(response.json())
return results
asyncio 적용
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)
처리 시간은 4시간에서 40분으로 줄었다. 하지만 문제가 있었다.
겪은 이슈들
1. 동시 연결 수 제한
한번에 1000개 요청을 보내니 서버에서 429 에러가 발생했다. asyncio.Semaphore로 동시 연결 수를 제한했다.
sem = asyncio.Semaphore(10)
async def fetch_one(session, url):
async with sem:
async with session.get(url) as response:
return await response.json()
2. 에러 핸들링
한 요청이 실패하면 gather가 전체를 중단시켰다. return_exceptions=True로 해결했다.
results = await asyncio.gather(*tasks, return_exceptions=True)
3. 메모리 누수
세션을 제대로 닫지 않아 메모리가 계속 증가했다. async with를 일관되게 사용하고 타임아웃을 설정했다.
timeout = aiohttp.ClientTimeout(total=30)
async with aiohttp.ClientSession(timeout=timeout) as session:
# ...
결론
비동기 처리로 성능은 크게 개선됐지만, 동기 코드보다 디버깅이 어렵고 예외 처리가 복잡했다. I/O 바운드 작업에는 확실히 효과적이지만, 코드 복잡도와 트레이드오프를 고려해야 한다.