일요일

🔍 검색 키워드: Python asyncio RuntimeError 해결, event loop is closed 에러, asyncio 이벤트 루프 닫힘, Python 비동기 에러, asyncio.run() RuntimeError, httpx asyncio event loop closed

Python 비동기 코드를 짜다 보면 이런 에러가 나온다.

RuntimeError: Event loop is closed

또는:

RuntimeError: This event loop is already running.

FastAPI, SQLAlchemy async, httpx, aiohttp 같은 비동기 라이브러리를 쓰다 보면 특히 자주 마주친다. 에러 메시지는 짧은데 원인은 여러 가지라 헷갈린다. 이 글에서 상황별로 원인과 해결책을 정리한다.

asyncio 이벤트 루프 기초

Python asyncio는 단일 이벤트 루프(Event Loop)를 기반으로 돌아간다. 이 루프가 열려 있어야 async/await 코드가 실행되고, 루프가 닫히면 그 위에서 뭔가를 실행하려 할 때 RuntimeError: Event loop is closed가 발생한다.

문제가 생기는 대표 상황은 루프가 이미 닫혔는데 비동기 작업을 추가 실행하려 할 때, asyncio.get_event_loop()를 잘못 쓸 때, Windows 환경에서 asyncio.run() 이후 후처리 과정에서, pytest 등 테스트 프레임워크와 충돌할 때다.

원인 1 — asyncio.run() 이후 루프에 접근

import asyncio

async def main():
    await asyncio.sleep(1)
    print("done")

asyncio.run(main())  # 루프가 생성되고 실행 후 닫힘

# 여기서 다시 루프에 접근하면 에러
loop = asyncio.get_event_loop()
loop.run_until_complete(some_coroutine())  # RuntimeError: Event loop is closed

asyncio.run()은 새 이벤트 루프를 만들고, 코루틴 실행 후 루프를 닫는다. 이후 get_event_loop()로 루프를 가져오면 이미 닫힌 루프다.

해결: 모든 비동기 로직을 main() 코루틴 안으로 옮긴다.

import asyncio

async def main():
    await step_one()
    await step_two()  # 모든 비동기 작업을 하나의 main 안에서 처리
    await step_three()

asyncio.run(main())

원인 2 — Windows에서 발생하늕 Event loop is closed

Windows에서 asyncio.run() 이후 ProactorEventLoop의 종료 과정에서 내부적으로 에러가 발생한다. Python 3.10 이전 버전에서 특히 자주 보인다.

Exception ignored in: <function _ProactorBasePipeTransport.__del__>
RuntimeError: Event loop is closed

해결: 스크립트 시작 부분에 이벤트 루프 정책을 변경한다.

import asyncio
import sys

if sys.platform == "win32":
    asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())

async def main():
    pass

asyncio.run(main())

원인 3 — SQLAlchemy async 사용 시 엔진 미처리

SQLAlchemy의 AsyncEngine을 쓸 때 프로그램이 종료되면서 루프가 닫히기 전에 엔진을 제대로 dispose하지 않으면 에러가 난다.

from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession

engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/db")

async def main():
    async with AsyncSession(engine) as session:
        result = await session.execute(...)

    # 반드시 엔진 정리
    await engine.dispose()

asyncio.run(main())

engine.dispose()를 빠뜨리면 프로그램 종료 시 asyncpg 내부 커넥션 정리 과정에서 이벤트 루프가 이미 닫힌 상태라 에러가 발생한다.

원인 4 — httpx/aiohttp AsyncClient를 재사용할 때

httpx나 aiohttp의 AsyncClient는 이벤트 루프에 바인딩된다. 루프가 바뀌면 기존 클라이언트를 쓸 수 없다.

import asyncio
import httpx

# 잘못된 패턴 — 전역 클라이언트
client = httpx.AsyncClient()  # 이 시점에 루프에 바인딩

async def fetch(url):
    return await client.get(url)

asyncio.run(fetch("https://example.com"))
asyncio.run(fetch("https://example.com"))  # 두 번째 실행에서 문제

해결: async with로 컨텍스트 매니저를 활용한다.

import asyncio
import httpx

async def fetch(url):
    async with httpx.AsyncClient() as client:  # 루프마다 새 클라이언트
        response = await client.get(url)
        return response.json()

asyncio.run(fetch("https://example.com"))
asyncio.run(fetch("https://example.com"))  # 정상 작동

원인 5 — pytest에서 asyncio 테스트 충돌

pytest로 비동기 테스트를 작성할 때 이벤트 루프 설정이 잘못되면 에러가 난다. pytest-asyncio를 설치하고 사용한다.

pip install pytest-asyncio
# 올바른 패턴
import pytest

@pytest.mark.asyncio
async def test_async_func():
    result = await some_async_func()
    assert result == expect%d
# pytest.ini
[pytest]
asyncio_mode = auto

원인별 에러 상황 체크리스트

에러 상황원인해결
asyncio.run() 이후 다시 루프 사용닫힌 루프에 접근모든 로직을 main 코루틴 안으로
Windows에서 종료 시 에러 출력ProactorEventLoop 종료 버그WindowsSelectorEventLoopPolicy 설정
SQLAlchemy async 종료 시 에러엔진 dispose 누락await engine.dispose() 명시적 호출
httpx/aiohttp 두 번째 실행 시 에러클라이언트 루프 바인딩async with 패턴으로 클라이언트 관리
pytest 비동기 테스트 에러이벤트 루프 충돌pytest-asyncio 사용

디버깅 팁

에러 나왔는 짧아서 어디서 왔는지 파악하기 어려울 때, asyncio 디버그 모드를 켜면 상세 정보가 나온다.

import asyncio
import logging

logging.basicConfig(level=logging.DEBUG)

async def main():
    pass

asyncio.run(main(), debug=True)  # debug=True 추가

또는 환경변수로:

PYTHONASYNCIODEBUG=1 python main.py

정리

  • RuntimeError: Event loop is closed는 닫힌 루프에 접근하거나, 루프 생명주기를 제대로 관리하지 않아서 발생한다
  • asyncio.run()을 쓴다면 모든 비동기 로직은 그 안에서 시작하고 끝내야 한다
  • Windows는 WindowsSelectorEventLoopPolicy로 별도 처리 필요
  • httpx/aiohttp 끴라 �언트는 async with라 관리
  • 테스트는 pytest-asyncio라 통일

비동기 코드는 루프의 생릅하가 관리하면 대부분의 문제가 해결된다.

댓글 없음:

댓글 쓰기