Python asyncio에서 blocking 코드 처리하기

문제 상황

FastAPI로 이미지 처리 API를 만들다가 응답 속도가 현저히 느려지는 현상을 발견했다. Pillow로 이미지를 리사이징하는 작업이 동기로 동작하면서 전체 이벤트 루프를 블로킹시키고 있었다.

@app.post("/resize")
async def resize_image(file: UploadFile):
    image = Image.open(file.file)
    resized = image.resize((800, 600))  # 여기서 블로킹
    # ...

다른 요청들도 이 작업이 끝날 때까지 대기하는 상황이었다.

해결 방법

run_in_executor를 사용해 블로킹 작업을 별도 스레드풀에서 실행하도록 변경했다.

import asyncio
from concurrent.futures import ThreadPoolExecutor

executor = ThreadPoolExecutor(max_workers=4)

def sync_resize(image_data):
    image = Image.open(io.BytesIO(image_data))
    return image.resize((800, 600))

@app.post("/resize")
async def resize_image(file: UploadFile):
    image_data = await file.read()
    loop = asyncio.get_event_loop()
    resized = await loop.run_in_executor(
        executor, sync_resize, image_data
    )
    # ...

결과

동시 요청 10개 기준 응답 시간이 평균 3초에서 0.5초로 개선됐다. CPU 바운드 작업은 ProcessPoolExecutor도 고려해볼만 하지만, 이미지 데이터 직렬화 오버헤드 때문에 ThreadPoolExecutor가 더 효율적이었다.

외부 sync 라이브러리를 async 환경에서 쓸 때는 항상 이벤트 루프 블로킹을 염두에 두어야 한다는 걸 다시 확인했다.