TestForge | Aidevops | 📊 Plogger ✍️ Blog 📚 Docs
plogger

AI DevOps Korea

AI 서비스 개발, 운영, 성능개선을 하나의 루프로 연결합니다

aidevops.kr에서 LLMOps, RAG, AI Agent, 관측성, 평가, 비용-성능 최적화를 실전 운영 관점으로 정리합니다.

Python asyncio 비동기 프로그래밍 실전 가이드

· 수정 4월 21일
Python asyncio 비동기 프로그래밍 실전 가이드 다이어그램
이 글에서 다루는 핵심 흐름, 아키텍처 구조, 주요 판단 포인트를 한눈에 이해할 수 있도록 정리한 그림입니다.
`asyncio`는 자주 과대평가됩니다. Python을 전반적으로 빠르게 만들어 주는 것도 아니고, 운영 난도를 자동으로 낮춰주는 것도 아닙니다. 대신 네트워크 I/O, 타이머 대기, 다수의 동시 소켓 처리처럼 **기다리는 시간이 긴 작업**에서는 꽤 강력합니다.

문제는 많은 팀이 이 경계를 흐리게 가져간다는 점입니다. blocking 라이브러리와 async 코드를 섞고, cancellation 설계를 빼먹고, 나중에 event loop 전체가 멈추는 문제를 겪습니다. 그래서 asyncio는 문법보다 운영 규칙이 더 중요합니다.

asyncio가 잘 맞는 문제

다음과 같은 형태에는 asyncio가 잘 맞습니다.

  • 여러 upstream을 fan-out 하는 API 게이트웨이
  • 수천 개 소켓을 다루는 크롤러와 백그라운드 작업
  • 채팅, 알림, 이벤트 처리처럼 대기 작업이 많은 시스템
  • CPU보다 동시 I/O 수가 더 중요한 control plane 서비스

공통점은 간단합니다. 계산보다 기다리는 시간이 길다는 점입니다.

asyncio가 해결하지 못하는 것

반대로 아래 상황에서는 asyncio가 기본 답이 아닙니다.

  • 병목이 CPU 바운드 파싱, 압축, 추론 작업이다
  • 핵심 라이브러리가 대부분 동기식이다
  • timeout과 cancellation 정책을 팀이 일관되게 관리하지 못한다
  • 현재도 단순한 threaded worker 구조로 충분히 운영 가능하다

적당한 동시성과 익숙한 blocking 라이브러리 조합이라면, 오히려 동기 구조가 더 싸고 안정적일 수 있습니다.

실무에서 중요한 경계

실제 아키텍처 질문은 “전부 async로 만들까?”가 아니라 “어디부터 어디까지를 async 경계로 볼까?”입니다.

건강한 코드베이스는 보통 아래 원칙을 가집니다.

  • I/O가 많은 경계에만 async 적용
  • blocking 작업은 명시적으로 분리
  • timeout, retry, cancellation 정책을 하나의 규칙으로 통일
  • 소유자 없는 background task를 만들지 않음

이 원칙이 없으면 await는 코드 전반으로 퍼지지만, 시스템은 오히려 더 예측 불가능해집니다.

예시: 구조화된 동시성과 timeout

아래 예시는 단순한 gather() 예제보다 실전에 가깝습니다. task lifetime, timeout, 실패 전파가 모두 보이기 때문입니다.

import asyncio
from collections.abc import Sequence


async def fetch_all(client, urls: Sequence[str]) -> list[dict]:
    results: list[dict] = []

    async with asyncio.TaskGroup() as group:
        tasks = [group.create_task(fetch_one(client, url)) for url in urls]

    for task in tasks:
        results.append(task.result())

    return results


async def fetch_one(client, url: str) -> dict:
    try:
        async with asyncio.timeout(2.0):
            response = await client.get(url)
            response.raise_for_status()
            return response.json()
    except TimeoutError as exc:
        raise RuntimeError(f"upstream timeout: {url}") from exc

중요한 것은 문법이 아니라 task의 생명주기와 timeout 정책이 코드에 드러난다는 점입니다.

cancellation은 설계 이슈다

asyncio 시스템이 흔들리는 지점은 대부분 cancellation입니다.

건강한 서비스는:

  • 요청 취소가 하위 task까지 전파되고
  • 정리 작업이 finally나 context manager에서 실행되며
  • timeout이 응급처치가 아니라 계약 일부로 다뤄지고
  • 장기 실행 background task의 소유자가 분명합니다

반대로 취소된 요청 뒤에서도 task가 계속 돌고, 소켓이 남고, 종료 시간이 길어진다면 구조가 이미 흔들리고 있는 것입니다.

blocking 작업은 반드시 격리해야 한다

가장 흔한 asyncio 장애는 event loop를 모르게 막아버리는 것입니다.

주된 원인은 다음과 같습니다.

  • 동기식 DB 드라이버
  • 파일 시스템 중심 코드
  • CPU 바운드 직렬화나 이미지 처리
  • 겉으로만 async처럼 보이는 레거시 SDK

이런 작업이 필요하다면 executor로 보내거나 별도 worker 모델로 분리해야 합니다. 그렇지 않으면 한 경로의 지연이 전혀 상관없는 요청까지 멈추게 만듭니다.

성공 여부를 판단하는 지표

async 전환의 성공은 async def 개수가 아니라 운영 지표로 판단해야 합니다.

  • 정해진 CPU/메모리 예산에서의 처리량
  • p95, p99 지연 시간
  • event loop lag
  • timeout 빈도
  • 열린 연결 수 증가 추세
  • 종료 시 cleanup 안정성

부하가 올라갈수록 event loop lag가 커진다면, blocking 작업이 섞였거나 backpressure 없이 task를 너무 많이 만들고 있을 가능성이 큽니다.

자주 나오는 실수

  • 동기 라이브러리를 async handler 안에서 직접 호출하는 경우
  • 실패 정책 없이 asyncio.gather()만 쓰는 경우
  • 소유자 없는 fire-and-forget task를 만드는 경우
  • timeout 예산 없이 retry만 추가하는 경우
  • async를 범용 성능 최적화처럼 다루는 경우

이 문제들은 문법 문제가 아니라 시스템 규율 문제입니다.

리뷰 체크리스트

  • 워크로드가 정말 I/O 중심이라 async 복잡도를 감수할 가치가 있는가
  • timeout과 cancellation이 코드 계약으로 보이는가
  • 모든 task에 소유자와 생명주기가 있는가
  • blocking 라이브러리가 event loop에서 격리돼 있는가
  • event loop lag, timeout rate, open resource 지표가 준비돼 있는가

마무리 판단

asyncio는 고동시성 I/O에 아주 강력한 도구지만, 막연한 현대화 전략으로 쓰면 금방 비싸집니다. 좋은 async 시스템은 coroutine이 많은 시스템이 아니라, task ownership과 timeout 정책, 실패 전파가 코드에서 명확히 읽히는 시스템입니다.

Continue Reading

다음으로 읽기 좋은 글

다음 탐색

이 주제를 시스템 관점으로 더 이어서 보기