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 작업에서 확실히 효과적이었다.

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