몽키 패칭 vs. Async Await: 두 가지 파이썬 동시성 패러다임 이야기
Wenhao Wang
Dev Intern · Leapcell

소개
다재다능함과 가독성으로 찬사를 받는 파이썬은 현대 소프트웨어 개발의 요구 사항을 충족하기 위해 끊임없이 발전하고 있습니다. 동시성, 즉 여러 작업을 동시에 실행하는 것처럼 보이는 능력은 응답성이 뛰어나고 성능이 뛰어난 애플리케이션을 구축하는 데 중요한 측면이며, 특히 오늘날 데이터 중심적이고 네트워크 집약적인 세상에서는 더욱 그러합니다. 수년에 걸쳐 파이썬 개발자들은 동시성을 달성하기 위해 다양한 전략을 채택해 왔습니다. 그중에서 매우 독특한 두 가지 철학이 등장했습니다. 즉, 동적이고 런타임을 변경하는 몽키 패칭의 강력함과 async/await 구문이 제공하는 보다 구조화되고 명시적인 제어입니다. 이 기사에서는 동시 작업에 있어 한 가지를 선택할 때 수반되는 메커니즘, 일반적인 애플리케이션 및 절충점을 비교하며 이러한 두 가지 패러다임을 탐구할 것입니다. 이러한 차이점을 이해하는 것은 애플리케이션의 유지 관리성, 확장성 및 안정성에 영향을 미치는 정보에 입각한 아키텍처 결정을 내리는 데 중요합니다.
몽키 패칭과 Async/Await의 세계
직접적인 비교에 들어가기 전에 관련된 핵심 개념을 명확하게 이해해 봅시다.
핵심 용어
- 동시성(Concurrency): 여러 작업을 한 번에 처리하는 능력입니다. 반드시 작업이 동시에 실행된다는 것을 의미하지는 않지만(병렬성), 일정 기간 동안 하나 이상의 작업에서 진행 상황을 만들 수 있다는 것을 의미합니다.
- 몽키 패칭(Monkey Patching): 원래 소스 코드를 변경하지 않고 프로그램의 런타임 코드를 확장하거나 수정하는 기술입니다. 일반적으로 런타임에 메서드, 클래스 또는 전체 모듈을 교체하는 것을 포함합니다.
async/await: 파이썬의 코루틴(일시 중지 및 다시 시작할 수 있는 함수)을 정의하고 실행하기 위한 내장 구문입니다. 이 논블로킹 I/O 접근 방식은 비동기 프로그래밍의 핵심이며, 단일 스레드가 여러 I/O 바운드 작업을 효과적으로 관리할 수 있도록 합니다.- 이벤트 루프(Event Loop):
asyncio및 유사한 비동기 프레임워크의 핵심입니다. 코루틴을 예약하고 실행하며 I/O 작업을 관리하고 이벤트를 디스패치할 책임이 있습니다. - 코루틴(Coroutines): 파이썬의 특수 함수(
async def로 정의)로, 실행을 일시 중지하고 이벤트 루프에 제어권을 다시 양도하여 다른 작업을 실행할 수 있도록 할 수 있습니다. 기다리던 I/O 작업이 완료되면 다시 시작할 수 있습니다.
동시성을 위한 몽키 패칭
동시성을 위한 몽키 패칭은 일반적으로 gevent 또는 eventlet과 같은 라이브러리를 사용합니다. 이러한 라이브러리는 표준 라이브러리 함수(예: socket 또는 time.sleep과 같은 I/O 작업)를 '협력적으로 예약되는' 방식으로 패치하여 동시성을 달성합니다. 패치된 I/O 작업이 호출되면 전체 스레드를 차단하는 대신 gevent 또는 eventlet 스케줄러에 제어권을 양도하며, 스케줄러는 다른 '그린렛'(이러한 라이브러리에서 제공하는 경량 코루틴)으로 전환합니다.
작동 방식:
- 표준 라이브러리를 가져와 '몽키 패치'합니다.
# gevent 예제 from gevent import monkey monkey.patch_all() # socket, ssl, threading, time 등 표준 라이브러리 모듈을 패치합니다. import gevent import requests # 이제 내부적으로 gevent를 인식하는 소켓을 사용합니다. - 일반적인 I/O 작업을 수행하는 함수를 정의합니다. 특별한
async또는await키워드는 필요하지 않습니다. gevent.spawn또는eventlet.spawn을 사용하여 그린렛을 생성하고gevent.joinall을 사용하여 대기합니다.
예제: gevent로 여러 URL을 동시에 가져오기:
# gevent_example.py from gevent import monkey import gevent import time import requests # 표준 라이브러리(예: socket, time) 패치 monkey.patch_all() def fetch_url(url): print(f"Fetching...") try: response = requests.get(url) print(f"Finished fetching: {url}, Status: {response.status_code}") return len(response.content) except Exception as e: print(f"Error fetching {url}: {e}") return 0 urls = [ "http://www.google.com", "http://www.yahoo.com", "http://www.bing.com", "http://www.python.org", ] if __name__ == "__main__": start_time = time.time() # 각 URL에 대한 그린렛 생성 geenlets = [gevent.spawn(fetch_url, url) for url in urls] # 모든 그린렛이 완료될 때까지 대기 gevent.joinall(geenlets) total_bytes = sum(g.value for g in geenlets) end_time = time.time() print(f"\nTotal bytes fetched: {total_bytes}") print(f"Total time taken: {end_time - start_time:.2f} seconds")
이 예제에서 requests.get()은 일반적으로 차단됩니다. 그러나 monkey.patch_all() 이후에는 내부 소켓 작업이 논블로킹이 되어 제어권을 양도하므로 동일한 스레드에서 다른 fetch_url 호출이 동시에 진행될 수 있습니다.
애플리케이션 시나리오:
- 최소한의 코드 변경으로 기존 차단 코드를 동시성 모델로 마이그레이션합니다.
async/await을 지원하지 않는 레거시 라이브러리와 통합합니다.- 여러 동시 연결을 처리하기 위한 웹 서버(예:
gevent또는eventlet워커를 사용하는 Gunicorn).
Async/Await 세계
async/await은 파이썬 3.5에서 asyncio 라이브러리를 통해 도입된 비동기 프로그래밍을 위한 파이썬의 명시적인 네이티브 지원입니다. 이는 이벤트 루프를 사용하는 협력적 다중 작업 원칙에 따라 작동합니다. async def로 표시된 함수는 코루틴이며, await 키워드를 사용하여 실행을 일시 중지할 수 있는 지점을 명시적으로 나타냅니다.
작동 방식:
async def를 사용하여 코루틴을 정의합니다.- 코루틴 내에서
await를 사용하여awaitable(다른 코루틴, Future 또는 Task)이 완료될 때까지 실행을 일시 중지합니다. - 이벤트 루프가 이러한 코루틴을 실행하고, 하나가 I/O 작업을 기다릴 때 코루틴 간에 전환합니다.
예제: asyncio 및 httpx로 여러 URL을 동시에 가져오기:
# asyncio_example.py import asyncio import httpx # 비동기 HTTP 클라이언트 import time async def fetch_url_async(client, url): print(f"Starting fetching: {url}") try: response = await client.get(url) # 비동기 HTTP GET 요청을 기다립니다. print(f"Finished fetching: {url}, Status: {response.status_code}") return len(response.content) except Exception as e: print(f"Error fetching {url}: {e}") return 0 async def main(): urls = [ "http://www.google.com", "http://www.yahoo.com", "http://www.bing.com", "http://www.python.org", ] start_time = time.time() async with httpx.AsyncClient() as client: # 비동기 HTTP 클라이언트 사용 tasks = [fetch_url_async(client, url) for url in urls] # 작업을 동시에 실행하고 모두 완료될 때까지 기다립니다. results = await asyncio.gather(*tasks) total_bytes = sum(results) end_time = time.time() print(f"\nTotal bytes fetched: {total_bytes}") print(f"Total time taken: {end_time - start_time:.2f} seconds") if __name__ == "__main__": asyncio.run(main()) # 메인 코루틴 실행
여기서 httpx.AsyncClient().get()은 awaitable입니다. await client.get(url)가 호출되면 fetch_url_async는 일시 중지되고 이벤트 루프는 다른 보류 중인 작업으로 전환하거나 새로운 get 요청을 동시에 시작할 수 있습니다.
애플리케이션 시나리오:
- 고성능 네트워크 애플리케이션(웹 서버, API, 채팅 애플리케이션) 구축.
- 데이터베이스 및 마이크로서비스는 다른 비동기 서비스를 소비합니다.
- 동시성 흐름에 대한 명시적인 제어를 원하는 모든 I/O 바운드 애플리케이션.
- FastAPI 및 Starlette와 같은 최신 웹 프레임워크는 전적으로
asyncio를 기반으로 구축되었습니다.
비교 및 절충점
| 특징 | 몽키 패칭 (예: gevent) | Async/Await (asyncio) |
|---|---|---|
| 명시성 | 암시적: 표준 차단 호출이 비차단 작업으로 전환됩니다. | 명시적: async 및 await 키워드는 동시성을 명확하게 표시합니다. |
| 침습성 | 매우 침습적: 런타임에 전역 상태 및 표준 라이브러리를 수정합니다. | 비침습적: 명시적인 async API에 의존합니다. |
| 학습 곡선 | 기존 차단 코드의 경우 낮지만, 패칭 이해는 복잡할 수 있습니다. | 초보자의 경우 더 높으며, 이벤트 루프와 코루틴을 이해해야 합니다. |
| 생태계 지원 | 틈새 시장, 패치된 라이브러리 버전에 의존합니다. 모든 라이브러리를 패치할 수 있는 것은 아닙니다. | 빠르게 성장 중이며, 많은 최신 라이브러리가 asyncio 네이티브입니다. |
| 디버깅 | 암시적 제어 흐름 및 수정된 스택 추적으로 인해 어려울 수 있습니다. | 명시적인 제어로 인해 일반적으로 더 명확한 오류 메시지와 스택 추적을 제공합니다. |
| 성능 | 단일 스레드에서 I/O 바운드 작업에 탁월합니다. 많은 경우 asyncio와 유사합니다. | I/O 바운드 작업에 탁월합니다. 네이티브 지원은 최적화 가능성을 제공합니다. |
| 호환성 | 자체적으로 저수준 I/O 또는 스레딩을 수행하는 라이브러리와 문제가 발생할 수 있습니다. | 라이브러리가 asyncio 호환이 되거나 async 래퍼를 제공해야 합니다. |
| 정신 모델 | "그냥 작동합니다" (작동하지 않을 때까지). | "명시적으로 협력적"입니다. |
몽키 패칭이 빛나는 곳:
- 레거시 코드 마이그레이션: 전통적인 차단 호출로 작성된 기존 코드베이스가 방대할 때, 몽키 패칭은 대규모 재작성 없이 동시성을 도입하는 빠른 방법을 제공할 수 있습니다.
- 신속한 프로토타이핑:
asyncio에 대한 재작성 오버헤드가 너무 높다고 판단되는 비동시성 애플리케이션에 동시성을 빠르게 추가하기 위해.
Async/Await가 빛나는 곳:
- 새로운 프로젝트: 명시적인 동시성 모델이 선호되는 최신 고성능 애플리케이션 구축.
- 견고성 및 유지 관리성:
async/await의 명시적인 특성은 코드를 더 쉽게 이해하고 추론하고 디버깅하여 장기적으로 더 유지 관리하기 쉬운 애플리케이션으로 이어집니다. - 최신 생태계:
asyncio네이티브 라이브러리 및 프레임워크의 성장하는 생태계와 원활하게 통합됩니다. - 명확한 제어 흐름: 개발자는 컨텍스트 전환이 발생하는 시점에 대해 세밀한 제어를 할 수 있습니다.
결론
몽키 패칭(주로 gevent와 같은 라이브러리를 통해)과 async/await 모두 파이썬에서 동시성을 달성하는 강력한 방법을 제공합니다. 몽키 패칭은 기존 차단 코드를 위한 거의 마법과 같은 전환을 제공하여 최소한의 구문 변경으로 동시 실행되도록 하지만, 전역 상태 수정 및 잠재적인 디버깅 문제라는 대가를 치릅니다. 대조적으로, async/await는 비동기 작업을 명확하게 표시하는 더 명시적이고 의도적인 동시 프로그래밍 접근 방식을 요구하며, 특히 새로운 프로젝트나 완전한 재작성의 경우 더 견고하고 유지 관리 가능하며 현대적인 코드로 이어집니다. 이 두 가지 패러다임 간의 선택은 특정 프로젝트의 요구 사항, 기존 코드베이스 및 팀의 명시적인 비동기 프로그래밍에 대한 편안함 수준에 따라 달라지지만, 트렌드는 명시적인 제어와 async/await의 성장하는 생태계를 분명히 선호합니다. 신규 프로젝트의 경우, 확장 가능하고 투명한 동시 파이썬 애플리케이션을 위한 길은 의심할 여지 없이 async/await를 채택하는 것입니다.

