
Python에서 비동기 프로그램의 최상위 진입점은 보통 asyncio.run()으로 충분합니다. 다만 같은 이벤트 루프와 같은 contextvars.Context 안에서 여러 top-level async 함수를 순서대로 실행해야 한다면 asyncio.Runner를 검토할 수 있습니다.
기준은 간단합니다. 한 번 실행하고 끝나는 CLI나 서비스 시작 코드는 asyncio.run()을 기본값으로 둡니다. 반복 호출이 필요하거나 이벤트 루프 생성 방식을 명시해야 하는 코드에서는 Runner와 loop_factory를 함께 봅니다.

asyncio.run은 무엇을 해주나
Python 3.14 공식 문서 기준으로 asyncio.run(coro, *, debug=None, loop_factory=None)은 awaitable 객체를 이벤트 루프에서 실행하고 결과를 반환합니다. 실행 중에는 비동기 generator 정리와 기본 executor 종료도 처리합니다.
import asyncio
async def main():
await asyncio.sleep(1)
return "done"
result = asyncio.run(main())
print(result)
asyncio.run()은 같은 스레드에서 이미 다른 이벤트 루프가 실행 중이면 호출할 수 없습니다. 또 문서는 이 함수를 asyncio 프로그램의 main entry point로 쓰고, 이상적으로는 한 번만 호출하라고 안내합니다. 그래서 일반적인 스크립트나 CLI에서는 진입점 하나를 main() coroutine으로 모으는 방식이 가장 단순합니다.
Runner는 언제 필요한가
asyncio.Runner는 context manager입니다. 문서는 여러 top-level async 함수를 같은 이벤트 루프와 같은 contextvars.Context 안에서 호출해야 할 때 이 도구가 유용하다고 설명합니다.

import asyncio
async def prepare():
await asyncio.sleep(0.1)
return "prepared"
async def work():
await asyncio.sleep(0.1)
return "worked"
with asyncio.Runner() as runner:
print(runner.run(prepare()))
print(runner.run(work()))
위 형태는 asyncio.run()을 여러 번 호출하는 코드와 다르게 하나의 Runner context 안에서 같은 루프를 재사용합니다. Runner.run()은 awaitable을 실행하고 결과를 반환하며, coroutine이 들어오면 task로 감쌉니다. 필요한 경우 context keyword-only 인자로 별도 contextvars.Context를 지정할 수 있습니다.
run과 Runner 선택 기준
| 상황 | 권장 선택 | 이유 |
|---|---|---|
| 프로그램 진입점에서 async main 한 번 실행 | asyncio.run() |
루프 생성, 실행, 정리, 닫기를 한 번에 처리함 |
| 같은 루프에서 여러 top-level async 함수 실행 | asyncio.Runner |
context manager 안에서 루프와 context를 함께 관리함 |
| 이미 실행 중인 이벤트 루프 안에서 호출 | 둘 다 직접 호출하지 않음 | 같은 스레드에 실행 중인 루프가 있으면 사용할 수 없음 |
| 이벤트 루프 구현을 명시적으로 바꿈 | loop_factory |
policy 시스템 대신 명시적 루프 생성 함수를 전달함 |
Runner를 쓰더라도 모든 비동기 코드를 top-level에서 잘게 나눠 호출하는 습관이 좋은 것은 아닙니다. 대부분의 애플리케이션은 하나의 async main() 안에서 하위 작업을 조립하는 편이 읽기 쉽습니다. Runner는 같은 루프와 context를 공유해야 하는 구체적인 이유가 있을 때 선택하는 도구로 보는 것이 좋습니다.
loop_factory는 왜 중요해졌나
Python 3.14 문서에서 asyncio policy 시스템은 deprecated로 표시되어 있으며, Python 3.16에서 제거될 예정입니다. 정책을 전역으로 바꾸는 방식 대신 원하는 이벤트 루프 구현이 필요하면 asyncio.run() 또는 asyncio.Runner의 loop_factory를 쓰라는 안내가 함께 제공됩니다.

import asyncio
async def main():
return asyncio.get_running_loop()
loop = asyncio.run(main(), loop_factory=asyncio.new_event_loop)
print(type(loop).__name__)
loop_factory를 넘기면 그 함수가 새 이벤트 루프를 만듭니다. Runner에서 loop_factory를 쓸 때는 해당 factory가 생성한 루프를 현재 루프로 설정할 책임이 있습니다. 기본값을 쓰면 asyncio.new_event_loop()으로 루프를 만들고 asyncio.set_event_loop()로 현재 루프에 설정하는 동작을 Runner가 처리합니다.
get_event_loop과 헷갈리는 지점
coroutine이나 callback 안에서 현재 실행 중인 루프가 필요하면 asyncio.get_running_loop()가 더 명확합니다. Python 3.14 이벤트 루프 문서는 get_event_loop()의 동작이 custom policy와 얽히면 복잡해질 수 있으므로 coroutine과 callback 안에서는 get_running_loop()을 선호하라고 설명합니다.
또 Python 3.14부터 현재 이벤트 루프가 없으면 get_event_loop()은 RuntimeError를 발생시킵니다. 기존 코드가 암묵적으로 루프를 가져오던 방식에 기대고 있다면, 최상위 실행은 asyncio.run()으로 모으고 루프 구현 변경은 loop_factory로 명시하는 쪽이 이식성이 낫습니다.
버전 기준
asyncio.run()은 Python 3.7에 추가되었습니다. Python 3.12에서는 loop_factory 인자가 추가되었고, Python 3.14 문서 기준으로 인자로 coroutine뿐 아니라 awaitable 객체를 받을 수 있습니다.
asyncio.Runner는 Python 3.11에 추가되었습니다. Runner.run()도 Python 3.14 문서 기준으로 awaitable 객체를 받을 수 있습니다. 라이브러리 코드나 여러 Python 버전을 지원하는 프로젝트라면 이 차이를 기준으로 최소 지원 버전을 확인해야 합니다.
FAQ
asyncio.run을 여러 번 호출해도 되나?
문법적으로 항상 금지되는 것은 아니지만, 공식 문서는 main entry point로 쓰고 이상적으로 한 번만 호출하라고 안내합니다. 같은 루프와 context를 유지해야 하는 여러 top-level 호출이라면 Runner가 더 직접적인 도구입니다.
Jupyter나 이미 실행 중인 이벤트 루프 안에서 asyncio.run을 쓰면 되나?
같은 스레드에서 다른 asyncio 이벤트 루프가 이미 실행 중이면 asyncio.run()과 Runner.run()은 호출할 수 없습니다. 그런 환경에서는 해당 환경이 제공하는 await 방식이나 루프 연동 방식을 따라야 합니다.
이벤트 루프 policy를 새 코드에서 써도 되나?
새 코드에서는 피하는 편이 맞습니다. Python 3.14 문서는 policy 시스템을 deprecated로 표시하고 Python 3.16 제거 예정이라고 설명합니다. 루프 구현을 골라야 한다면 loop_factory를 먼저 검토합니다.
참고 문서
'프로그래밍 > C, C++, Java, Python' 카테고리의 다른 글
| Python TaskGroup 기준 (0) | 2026.09.18 |
|---|---|
| Python eager task factory 기준 (0) | 2026.09.18 |
| Python Thread context 기준 (0) | 2026.09.15 |
| JavaScript Temporal 기준 (0) | 2026.09.14 |
| Python contextvars 사용법 (0) | 2026.08.20 |





