Python Asyncio의 이벤트 루프 이해하기
Python에서 비동기 프로그래밍을 구현할 때 핵심이 되는 이벤트 루프(Event Loop)에 대하여 설명한다. 이벤트 루프는 모든 asyncio 응용 프로그램의 중심 메커니즘으로, 비동기 태스크들을 효율적으로 관리하고 실행하는 역할을 한다.
개요: 이벤트 루프가 필요한 이유
파이썬으로 개발 시 웹 페이지 여러 개를 다운로드하거나, API 요청을 동시에 보내야 하는 경우가 발생한다. 이러한 I/O 작업은 대기 시간이 길어서 순차적으로 처리하면 시간이 과도하게 소요된다. 이벤트 루프는 이런 작업들을 효율적으로, 마치 동시에 처리하는 것처럼 관리해준다.
1. 이벤트 루프의 개념과 동작 원리
개념
이벤트 루프는 간단히 말해 실행할 작업들을 관리하는 관리자라고 볼 수 있다. 이벤트 루프는 다음과 같은 역할을 수행한다:
- 비동기 태스크 및 콜백을 실행
- 네트워크 I/O 연산 수행
- 자식 프로세스 실행 관리
- 효율적인 리소스 사용으로 동시성 구현
동작 원리
이벤트 루프는 다음과 같은 방식으로 동작한다:
- 스케줄링: 이벤트 루프는 실행할 작업들의 목록을 관리하며, 어떤 작업이 실행 준비되었는지 확인한다.
- 대기 상태 관리: I/O 작업과 같이 시간이 오래 걸리는 작업은 대기 상태로 두고 다른 작업을 먼저 실행한다.
- 제어권 전환:
await
키워드를 만나면 해당 작업을 일시 중단하고 다른 작업으로 제어권을 넘긴다. - 완료 처리: 대기 중인 작업이 완료되면 다시 그 작업으로 돌아가 계속 실행한다.
2. 이벤트 루프 사용 예제
다음은 이벤트 루프를 사용한 간단한 예제이다:
import asyncio
async def 빵_굽기():
print("빵 굽기 시작")
await asyncio.sleep(2) # 빵이 구워지는데 2초 걸린다고 가정
print("빵 완성!")
async def 커피_내리기():
print("커피 내리기 시작")
await asyncio.sleep(1) # 커피 내리는데 1초 걸린다고 가정
print("커피 완성!")
async def 테이블_닦기():
print("테이블 닦기 시작")
await asyncio.sleep(0.5) # 테이블 닦는데 0.5초 걸린다고 가정
print("테이블 깨끗해짐!")
async def 카페_운영():
await asyncio.gather(
빵_굽기(),
커피_내리기(),
테이블_닦기()
)
# 이벤트 루프 시작
asyncio.run(카페_운영())
이 코드를 실행하면 다음과 같은 결과가 출력된다:
빵 굽기 시작
커피 내리기 시작
테이블 닦기 시작
테이블 깨끗해짐!
커피 완성!
빵 완성!
이 결과는 이벤트 루프가 어떻게 동작하는지 잘 보여준다. 각 작업이 await asyncio.sleep()
호출로 대기 상태가 되면 이벤트 루프는 다른 작업으로 제어권을 넘긴다. 그래서 가장 짧은 시간이 소요되는 테이블 닦기가 먼저 완료되고, 그 다음에 커피 내리기, 마지막으로 빵 굽기가 완료된다.
3. 이벤트 루프 관련 주요 함수
이벤트 루프를 효과적으로 사용하기 위한 주요 함수들이다:
이벤트 루프 얻기
asyncio.get_running_loop()
: 현재 OS 스레드에서 실행 중인 이벤트 루프를 반환한다.asyncio.get_event_loop()
: 현재의 이벤트 루프를 가져온다. 현재 스레드에 설정된 이벤트 루프가 없다면 새로 생성하여 반환한다.
이벤트 루프 제어
asyncio.run(코루틴)
: 가장 간단하게 비동기 코드를 실행하는 방법이다. 코루틴을 실행하고 결과를 반환한다.loop.run_until_complete(future)
: future가 완료될 때까지 이벤트 루프를 실행한다.loop.run_forever()
: stop()이 호출될 때까지 이벤트 루프를 실행한다.loop.stop()
: 이벤트 루프를 중지한다.loop.close()
: 이벤트 루프를 닫는다.
4. 실제 응용 사례: FastAPI
FastAPI와 같은 웹 프레임워크에서는 이벤트 루프가 어떻게 활용되는지 살펴보자:
from fastapi import FastAPI
import asyncio
app = FastAPI()
@app.get("/주문/빵")
async def 빵_주문():
# 빵을 준비하는데 2초 걸린다고 가정
await asyncio.sleep(2)
return {"메시지": "빵 준비 완료!"}
@app.get("/주문/커피")
async def 커피_주문():
# 커피를 준비하는데 1초 걸린다고 가정
await asyncio.sleep(1)
return {"메시지": "커피 준비 완료!"}
FastAPI에서는 uvicorn ASGI 서버가 이벤트 루프를 자동으로 관리한다. 여러 사용자가 동시에 API 엔드포인트를 호출해도, 이벤트 루프는 각 요청을 효율적으로 처리한다. 한 요청이 await
를 만나 대기 상태가 되면 다른 요청을 처리할 수 있어, 서버가 많은 동시 요청을 효율적으로 처리할 수 있다.
5. asyncio와 스레드(Threading) 비교
Python에서 동시성을 구현하는 방법은 여러 가지가 있다. 이벤트 루프 기반의 asyncio와 스레드 기반의 threading의 차이점을 표로 정리하면 다음과 같다:
특성 | Asyncio (이벤트 루프) | Threading |
---|---|---|
동시성 모델 | 협력형 멀티태스킹 | 선점형 멀티태스킹 |
실행 방식 | 단일 스레드 | 여러 스레드 |
전환 시점 | await 키워드에서 명시적 전환 | OS가 결정 (비결정적) |
상태 공유 | 안전함 (단일 스레드) | 경쟁 상태 주의 필요 |
적합한 작업 | I/O 바운드 작업 | I/O 바운드 작업 |
확장성 | 수천~수만 개 작업 가능 | 수백 개 정도 제한적 |
디버깅 | 비교적 쉬움 | 복잡함 |
6. 결론
Python에서 이벤트 루프는 비동기 프로그래밍의 핵심 요소로, 다음과 같은 이점을 제공한다:
- I/O 작업 수행 시 CPU 자원을 효율적으로 활용할 수 있다.
- 단일 스레드에서 많은 동시 작업을 처리할 수 있어 메모리 사용량이 적다.
- 명시적인 제어권 전환으로 디버깅이 용이하다.
- 경쟁 상태나 데드락과 같은 멀티스레딩 문제를 피할 수 있다.
이벤트 루프 기반의 비동기 프로그래밍은 웹 서버, 네트워크 애플리케이션, 데이터 처리 파이프라인 등 I/O 바운드 작업이 많은 애플리케이션에 특히 적합하다. Python의 asyncio 라이브러리는 이러한 이벤트 루프를 손쉽게 사용할 수 있는 인터페이스를 제공하여, 개발자가 효율적인 비동기 애플리케이션을 쉽게 구현할 수 있도록 돕는다.
'Python' 카테고리의 다른 글
curl_cffi 도입기: 웹 스크래핑 문제 해결하기 (0) | 2025.05.11 |
---|---|
Python thread (0) | 2025.04.20 |