Python asyncio로 API 호출 성능 개선하기

문제 상황

데이터 동기화 배치 작업이 3시간 넘게 걸리고 있었다. 약 5000개 레코드에 대해 외부 API를 호출하는 작업이었는데, 각 요청마다 평균 2초씩 소요되니 당연한 결과였다.

기존 코드는 단순한 for 루프였다.

import requests

def sync_data(items):
    results = []
    for item in items:
        response = requests.get(f'https://api.example.com/data/{item.id}')
        results.append(response.json())
    return results

asyncio와 aiohttp 도입

Python 3.7이 릴리즈되면서 asyncio가 안정화되었고, 이번 기회에 제대로 적용해보기로 했다.

import asyncio
import aiohttp

async def fetch_data(session, item_id):
    async with session.get(f'https://api.example.com/data/{item_id}') as response:
        return await response.json()

async def sync_data_async(items):
    async with aiohttp.ClientSession() as session:
        tasks = [fetch_data(session, item.id) for item in items]
        results = await asyncio.gather(*tasks)
        return results

# 실행
results = asyncio.run(sync_data_async(items))

결과

동시 요청 수를 조절하기 위해 Semaphore를 추가했다. API 서버 부하를 고려해 동시 50개로 제한했다.

async def sync_data_async(items, concurrency=50):
    semaphore = asyncio.Semaphore(concurrency)
    
    async def bounded_fetch(session, item_id):
        async with semaphore:
            return await fetch_data(session, item_id)
    
    async with aiohttp.ClientSession() as session:
        tasks = [bounded_fetch(session, item.id) for item in items]
        results = await asyncio.gather(*tasks)
        return results

3시간 걸리던 작업이 20분으로 단축되었다. API 서버 응답 속도가 병목이었던 상황에서 비동기 처리가 확실한 효과를 보였다.

주의사항

  • API rate limit 확인 필수
  • 에러 핸들링을 제대로 해야 일부 실패가 전체에 영향을 주지 않음
  • 로컬 개발 환경에서는 Windows의 ProactorEventLoop 이슈 주의