Comet5 블로그
개발·운영

Python asyncio: 동시 실행·타임아웃·동시성 제한

Comet5 · 최초 발행 · 수정 · 약 3분

대기 작업을 병렬로 묶되 실패 전파와 외부 서비스 부하를 제한하는 예제입니다.

이 글의 목차

비동기는 작업을 무조건 더 빨리 계산하는 기술이 아닙니다. 네트워크처럼 기다리는 동안 다른 작업을 진행하도록 구성하는 방식입니다. 기다리는 요청을 한꺼번에 너무 많이 만들면 외부 서비스와 자신의 서버에 부담을 줄 수 있습니다.

동시 실행 수를 제한하는 예제

아래 코드는 실제 네트워크 대신 sleep으로 대기를 흉내 냅니다. Python 3.11 이상에서 실행할 수 있으며, 성능 측정 결과가 아니라 동작 구조를 설명하는 예제입니다.

import asyncio

async def fetch_demo(item, limit):
    async with limit:
        async with asyncio.timeout(1):
            await asyncio.sleep(0.01)
            return item * 2

async def main():
    limit = asyncio.Semaphore(3)
    results = await asyncio.gather(
        *(fetch_demo(i, limit) for i in range(6))
    )
    assert results == [0, 2, 4, 6, 8, 10]

if __name__ == "__main__":
    asyncio.run(main())

Semaphore는 여기서 제한 구간에 들어가는 작업 수를 제어합니다. 초당 요청 수 제한과는 다르므로 공급자가 속도 제한을 요구하면 별도 정책이 필요합니다. 실제 HTTP 라이브러리의 연결·읽기 타임아웃도 설정해야 합니다.

실패가 어디까지 전파되는가

gather에서 예외가 발생했을 때 다른 작업을 어떻게 다룰지 이해해야 합니다. 모든 작업을 한 단위로 묶어 실패 시 형제 작업 취소가 필요하다면 TaskGroup 같은 구조를 검토합니다. 부분 성공을 허용한다면 각 결과와 실패를 명시적으로 기록합니다.

취소되더라도 이미 외부 서버에 전달된 요청의 부작용이 취소되는 것은 아닙니다. 결제나 메시지 전송은 재시도 전에 중복 처리와 멱등성을 설계해야 합니다.

이벤트 루프를 막는 작업 구분하기

비동기 함수 안에서 오래 걸리는 동기 호출을 그대로 실행하면 다른 작업도 기다립니다. 파일 처리나 라이브러리 호출이 비동기인지 확인합니다. CPU를 오래 사용하는 계산은 단순히 async def로 바꿔도 병렬 계산이 되지 않습니다. 작업 성격에 맞는 실행 모델을 선택하세요.

대량 입력 전체를 한꺼번에 태스크로 만드는 방식도 메모리를 사용합니다. 동시 실행 제한과 태스크 생성량 제한은 별개이므로 입력이 매우 많으면 배치나 작업 큐가 필요합니다.

실시간 기능이 필요한지 먼저 묻기

화면 갱신에 몇 초의 지연을 허용할 수 있는지 정합니다. 짧은 주기의 조회, 서버 이벤트, 양방향 연결은 운영 비용이 다릅니다. 즉시성이 필요하지 않은 기능에 긴 연결을 도입하기보다 실제 사용자 요구를 확인하고 부하를 측정한 뒤 선택합니다.

참고 자료