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

[PYTHON] 비동기 환경 내 블로킹 I/O 문제를 해결하는 3가지 실무적 방법과 성능 차이

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

블로킹 I/O
블로킹 (Blocking)

 

파이썬의 asyncio 생태계로 전환하면서 개발자들이 가장 흔히 저지르는 실수는 비동기 이벤트 루프 내부에서 동기식(Blocking) 라이브러리를 그대로 사용하는 것입니다. 예를 들어, requeststime.sleep() 같은 함수는 호출되는 순간 전체 이벤트 루프를 정지시켜 버립니다. 이는 비동기 시스템의 장점을 완전히 무효화하며, 고가용성 서버에서 치명적인 장애를 유발합니다. 본 글에서는 비동기 환경을 방해하는 블로킹 요소를 감지하고 이를 해결하는 고도화된 전략을 다룹니다.


1. 블로킹(Blocking)과 비동기(Async)의 메커니즘 차이

비동기 루프는 단일 스레드에서 여러 작업을 스위칭하며 처리합니다. 루프 안에서 블로킹 코드가 실행되면, 해당 코드가 종료될 때까지 루프가 '멈춤' 상태가 됩니다. 각 라이브러리 유형별 특성을 비교해 보겠습니다.

라이브러리 유형 루프 영향도 대표적인 예시 성능상 결정적 차이
비동기 친화적 (Async-native) 없음 (비차단) aiohttp, httpx I/O 대기 중 다른 태스크 실행 가능
네트워크 블로킹 (Blocking I/O) 전체 루프 중단 requests, urllib 응답이 올 때까지 모든 작업 마비
CPU 연산 블로킹 (CPU Bound) 심각한 지연 유발 numpy, hashlib 계산량이 많을수록 루프 주행 시간 감소
파일 시스템 블로킹 미세한 지연 누적 기본 open(), os.read() OS 수준에서 미세한 차단 발생

2. 블로킹 라이브러리 혼용 시 발생하는 문제 해결 방법 3가지

기존의 방대한 동기식 라이브러리를 버릴 수 없을 때, 이벤트 루프를 건강하게 유지할 수 있는 기술적 해결책입니다.

방법 01: loop.run_in_executor 활용 (Thread Pool)

동기식 I/O 작업을 별도의 스레드 풀에서 실행하도록 위임하는 방법입니다. 이벤트 루프는 스레드에 작업을 던져두고 자신의 갈 길을 가며, 결과가 준비되면 콜백을 받습니다.

방법 02: ProcessPoolExecutor를 통한 연산 분리

이미지 처리나 복잡한 계산처럼 CPU를 많이 쓰는 블로킹 작업은 스레드가 아닌 별도의 '프로세스'로 보내야 합니다. 파이썬의 GIL(Global Interpreter Lock) 제약을 우회하는 유일한 해결 방법입니다.

방법 03: 비동기 래퍼(Wrapper) 라이브러리 도입

aiofilesasyncer 같은 라이브러리를 사용하여 기존 블로킹 인터페이스를 비동기식으로 래핑하여 사용하십시오. 이는 코드 가독성을 await 구문으로 통일시켜 줍니다.

3. 실전 샘플 예제 (Sample Example)

아래 코드는 블로킹 라이브러리인 requests를 비동기 환경에서 안전하게 실행하는 최적화 방법을 보여줍니다.


import asyncio
import requests
import time
from concurrent.futures import ThreadPoolExecutor

# 1. 블로킹 함수 정의 (requests 사용)
def fetch_url_blocking(url):
    print(f"[{url}] 요청 시작 (Blocking)...")
    response = requests.get(url)
    return f"{url} - {len(response.content)} bytes"

# 2. 해결 방법: Executor를 통한 비동기화
async def main():
    loop = asyncio.get_running_loop()
    
    # 별도의 스레드 풀 생성
    with ThreadPoolExecutor(max_workers=5) as pool:
        urls = [
            "https://www.google.com",
            "https://www.python.org",
            "https://www.github.com"
        ]
        
        print("비동기 루프 내에서 블로킹 작업 위임 시작")
        start_time = time.perf_counter()
        
        # 리스트 컴프리헨션을 사용하여 여러 블로킹 작업을 위임
        tasks = [
            loop.run_in_executor(pool, fetch_url_blocking, url)
            for url in urls
        ]
        
        # 모든 작업의 결과를 비동기적으로 대기
        results = await asyncio.gather(*tasks)
        
        end_time = time.perf_counter()
        for res in results:
            print(f"결과: {res}")
            
        print(f"총 소요 시간: {end_time - start_time:.2f}초")

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

참고: 위 코드를 사용하면 requests가 실행되는 동안에도 이벤트 루프는 다른 태스크를 동시에 처리할 수 있는 상태가 됩니다.

4. 결론 및 실무 제언

비동기 프로그래밍의 핵심은 '협력(Cooperation)'입니다. 한 작업이 루프를 독점(Block)하는 순간 협력의 고리는 끊어집니다. 외부 라이브러리를 도입할 때는 반드시 비동기 지원 여부를 먼저 확인하시고, 지원하지 않는다면 run_in_executor를 통해 안전한 격리 환경을 구축하십시오. 이러한 세심한 설계가 대규모 트래픽에서도 죽지 않는 견고한 파이썬 애플리케이션을 만드는 차이를 만듭니다.


내용 출처 및 참고 문헌

  • Python Documentation: "asyncio - Event Loop: Executing code in thread or process pools."
  • Real Python: "Speed Up Your Python Program With Concurrency."
  • Stack Overflow: "How to use blocking function in asyncio?"
  • "Python Concurrency with asyncio" by Matthew Fowler (2025 Edition).
728x90