본문 바로가기
Artificial Intelligence/60. Python

[PYTHON] 고성능 비동기 처리를 위한 asyncio 이벤트 루프의 3가지 핵심 원리와 해결 방법

by Papa Martino V 2026. 3. 30.
728x90

asyncio 이벤트 루프
asyncio 이벤트 루프

 

 

현대 백엔드 개발에서 비동기 프로그래밍(Asynchronous Programming)은 선택이 아닌 필수입니다. 파이썬은 asyncio 라이브러리를 통해 싱글 스레드 환경에서도 수만 개의 동시 연결을 처리할 수 있는 강력한 능력을 제공합니다. 하지만 그 중심에 있는 이벤트 루프(Event Loop)가 정확히 어떻게 작동하는지 이해하지 못하면, 오히려 동기 방식보다 성능이 떨어지는 '무한 대기'의 늪에 빠질 수 있습니다. 본 포스팅에서는 단순한 await 사용법을 넘어, 파이썬 인터프리터 수준에서 이벤트 루프가 태스크를 스케줄링하는 메커니즘을 심층 분석합니다. 또한 실무에서 흔히 발생하는 이벤트 루프 차단(Blocking) 문제를 해결하기 위한 7가지 이상의 실전 엔지니어링 예제를 다룹니다.


1. 이벤트 루프의 핵심 아키텍처: 동기 vs 비동기 차이

이벤트 루프는 무한 루프를 돌며 실행 대기 중인 태스크(Task)를 감시하고, I/O 작업이 완료된 태스크에게 실행권을 넘겨주는 '교통 관제사' 역할을 합니다. 파이썬의 비동기는 멀티스레딩과 달리 협력적 멀티태스킹(Cooperative Multitasking) 방식을 취합니다.

비교 항목 전통적인 동기 방식 (Blocking) asyncio 이벤트 루프 (Non-blocking)
실행 제어 작업이 끝날 때까지 CPU가 대기함 I/O 대기 시 루프에 제어권을 즉시 반납함
리소스 소모 스레드 생성 비용 및 메모리 점유 높음 싱글 스레드 내 가벼운 코루틴으로 저비용 유지
컨텍스트 스위칭 OS가 강제로 전환 (Preemptive) 코드에서 await로 직접 양보 (Cooperative)
병목 해결 스레드 풀(Thread Pool) 확장 필요 루프가 놀지 않도록 비동기 전용 드라이버 사용
적합한 작업 단순 연산, 소규모 서버 네트워크 서버, 대규모 크롤러, 실시간 채팅

2. 이벤트 루프 작동의 3단계 프로세스

  1. 등록(Registration): 코루틴이 Task로 감싸져 루프의 실행 큐에 등록됩니다.
  2. 폴링(Polling): selectepoll 시스템 호출을 통해 I/O 이벤트가 완료되었는지 감시합니다.
  3. 실행(Execution): 완료된 이벤트와 연결된 콜백이나 코루틴을 재개(Resume)합니다.

3. 실무 엔지니어를 위한 7가지 고성능 asyncio 해결 방법 (Examples)

Example 1: 다수의 API 요청 병렬 처리 최적화 (gather)

순차적인 await 호출은 비동기의 이점을 죽입니다. asyncio.gather를 통해 동시성을 극대화합니다.

import asyncio
import aiohttp

async def fetch_url(session, url):
    async with session.get(url) as response:
        return await response.text()

async def main():
    urls = ["https://api.example.com/data"] * 10
    async with aiohttp.ClientSession() as session:
        # 10개의 요청을 동시에 이벤트 루프에 등록
        results = await asyncio.gather(*(fetch_url(session, u) for u in urls))
        print(f"Total results: {len(results)}")

asyncio.run(main())

Example 2: 이벤트 루프 차단(Blocking) 현상 해결 방법

비동기 루프 안에서 time.sleep()이나 무거운 연산을 실행하면 루프 전체가 멈춥니다. 이를 run_in_executor로 분리합니다.

import asyncio
import time

def heavy_cpu_bound():
    # 루프를 멈추게 하는 무거운 연산
    time.sleep(5)
    return "Done"

async def main():
    loop = asyncio.get_running_loop()
    # Executor를 사용하여 별도 스레드/프로세스에서 실행
    result = await loop.run_in_executor(None, heavy_cpu_bound)
    print(result)

asyncio.run(main())

Example 3: Task 취소 및 타임아웃 관리

응답이 없는 요청이 루프 리소스를 점유하지 않도록 wait_for를 사용하여 제어합니다.

async def long_task():
    await asyncio.sleep(100)

async def main():
    try:
        await asyncio.wait_for(long_task(), timeout=2.0)
    except asyncio.TimeoutError:
        print("작업 시간이 초과되어 안전하게 취소되었습니다.")

asyncio.run(main())

Example 4: Shield를 이용한 중요 작업의 취소 방지

타임아웃은 걸되, 특정 저장 로직은 끝까지 실행되어야 할 때 shield를 사용합니다.

async def save_data():
    await asyncio.sleep(5)
    print("Data Saved!")

async def main():
    task = asyncio.create_task(save_data())
    try:
        await asyncio.wait_for(asyncio.shield(task), timeout=1.0)
    except asyncio.TimeoutError:
        print("Timeout hit, but saving continues in background.")
        await task # 결국 완료됨

Example 5: 비동기 큐(Queue)를 이용한 부하 조절 (Throttling)

이벤트 루프에 한꺼번에 너무 많은 태스크가 쌓여 메모리가 폭발하는 것을 방지합니다.

async def worker(name, queue):
    while True:
        url = await queue.get()
        print(f"Worker {name} processing {url}")
        await asyncio.sleep(1)
        queue.task_done()

async def main():
    queue = asyncio.Queue()
    # 워커 3개만 유지하여 동시성 제어
    workers = [asyncio.create_task(worker(f"W-{i}", queue)) for i in range(3)]
    for url in ["link1", "link2", "link3", "link4"]:
        await queue.put(url)
    await queue.join()
    for w in workers: w.cancel()

Example 6: 이벤트 루프 디버그 모드 활용 방법

어떤 코드가 루프를 얼마나 지연시키는지 찾아내기 위한 설정입니다.

import asyncio
import logging

async def main():
    loop = asyncio.get_running_loop()
    loop.set_debug(True)
    # 루프를 100ms 이상 점유하는 코드가 있으면 경고 로그 발생
    loop.slow_callback_duration = 0.1 
    await asyncio.sleep(1)

asyncio.run(main())

Example 7: 콜백(Callback) 기반 코드와 비동기 통합

전통적인 콜백 방식의 라이브러리를 Future 객체를 통해 비동기로 변환합니다.

async def wrap_callback():
    loop = asyncio.get_running_loop()
    future = loop.create_future()
    
    # 3초 후 결과를 future에 세팅하는 가상의 콜백 시스템
    loop.call_later(3, future.set_result, "Success from callback")
    
    result = await future
    print(result)

asyncio.run(wrap_callback())

4. 결론: 효율적인 이벤트 루프 관리를 위한 원칙

asyncio의 성능은 이벤트 루프가 얼마나 '자유롭게' 계속 돌아갈 수 있느냐에 달려 있습니다. 절대로 루프 내부에서 동기식 블로킹 함수(requests, time.sleep 등)를 호출하지 마십시오. 만약 외부 라이브러리가 비동기를 지원하지 않는다면 반드시 run_in_executor를 통해 루프 외부로 격리해야 합니다.


5. 내용 출처 및 참조 기술서

  • Python Software Foundation. "asyncio — Asynchronous I/O." Python Official Documentation.
  • Yury Selivanov. "PEP 492 – Coroutines with async and await syntax."
  • Caleb Hattingh. "Using asyncio in Python: Understanding Python's Asynchronous Programming." O'Reilly Media.
  • "Asyncio Event Loop Implementation Details." - CPython Source Code Analysis.
728x90