비동기와 코루틴
1. 비동기와 코루틴
1.1 비동기 프로그래밍
비동기 처리 방식이 왜 필요한지 아래 예시를 들어 살펴보도록 하겠습니다. 먼저 동기 방식에 대해 살펴보겠습니다. 동기는 순차적으로 처리되는 방식입니다.
- (10시) licat : 로봇 청소기를 돌립니다.
- (11시) licat : 빨래를 합니다.
- (12시) licat : 설거지를 합니다.
- (01시) licat : 요리를 합니다.
위의 예시는 동기 방식으로 처리되는 방식입니다. 순차적으로 처리되기 때문에 로봇 청소기를 돌리고 나서 빨래를 하고, 빨래를 하고 나서 설거지를 하고, 설거지를 하고 나서 요리를 합니다.
반면 비동기 방식은 순차적으로 처리되지 않습니다. 아래 예시를 살펴보겠습니다.
- (10시) licat : 로봇 청소기를 돌리면서
- (10시) licat : 빨래를 합니다.
- (10시) licat : 설거지를 합니다.
- (10시) licat : 요리를 하려고 물도 끓입니다.
우리는 당연히 비동기 처리가 효율적이라는 것을 알고 있습니다. 다만 컴퓨터는 항상 모든 일을 순차적으로 진행하려 합니다. 앞에 일이 다 끝나야만 뒤에 일을 하는 것이죠. 이는 특히 I/O 작업, 네트워크 요청, 긴 계산 과정 등을 처리할 때 유용합니다.
카페에서 주문을 받는 아래 이미지를 예로 들어보겠습니다. 만약 주문을 받는 점원이 주문을 받고 모든 음료를 만들어야만 다음 주문을 받는다면 어떻게 될까요? 이런 경우가 없을 것 같지만 code에서는 이런 경우가 많이 발생합니다.

이번에는 실제 프로젝트에서 사용하는 예시를 살펴보도록 하겠습니다. 아래 이미지는 위니브월드라는 서비스입니다. 캐릭터를 움직이며 프로그래밍을 배울 수 있는 툴이죠. 여기서 왼쪽 소스코드와 오른쪽 캐릭터는 비동기로 움직이고, 오른쪽 소스코드는 동기로 움직입니다. 만약 이것이 동기로 움직인다면 어떤 일이 발생될까요? 부드러운 애니메이션은 적용시킬 수 없을겁니다. 코드의 실행 속도는 워낙 빠르니까요.

1.1.1 동기 vs 비동기 프로그래밍 개념
- 동기 프로그래밍(Synchronous Programming): 코드가 순차적으로 실행되며, 한 작업이 완료될 때까지 다음 작업은 대기합니다. 이 방식은 코드의 흐름을 이해하기 쉽지만, 리소스 활용도가 낮을 수 있습니다.
- 비동기 프로그래밍(Asynchronous Programming): 동시에 여러 작업을 진행할 수 있으며, 한 작업이 완료될 때까지 기다리지 않고 다른 작업을 수행합니다. 이벤트 루프와 콜백 함수, 프로미스(Promise), async/await 구문 등을 활용하여 작업을 관리합니다.
코드가 순차적으로 실행되며, 특정 작업이 완료될 때까지 프로그램이 기다리는 방식입니다. 해당 실습은 로컬에서도 실습해보시길 권합니다. 진행하고 있는 환경인 colab에서 진행할 수 있도록 별도의 라이브러리를 설치하여 진행하겠습니다.
Google Colab의 환경에서는 이미 기본적으로 이벤트 루프가 실행 중입니다. 이 이벤트 루프는 Google Colab 환경의 비동기 작업을 처리하기 위해 사용됩니다. 그러므로, Google Colab에서는 asyncio.run() 함수를 직접 호출하면 "cannot be called from a running event loop"와 같은 에러 메시지가 출력됩니다. 이를 해결하려면 아래와 같은 코드를 추가해야 합니다.
!pip install nest_asyncio
import nest_asyncio
nest_asyncio.apply()
import time
def job(number):
print(f"Job {number} started")
time.sleep(3) # 이 time.sleep이 매우 오래 걸리는 작업 이라 가정하고 그 효율을 생각해봅시다. 일반 sleep은 CPU를 쉬게 합니다.
print(f"Job {number} completed")
job(1)
job(2)
job(3)
이제 이를 해결하기 위한 비동기 프로그래밍을 해보도록 하겠습니다. 비동기 프로그래밍은 동시에 여러 작업을 진행할 수 있습니다. 이때, 이벤트 루프와 콜백 함수 등을 활용하여 작업을 관리합니다. 이 작업은 colab에서는 위에 nest_asyncio 모듈이 설치된 다음 진행하실 수 있습니다.
import asyncio
async def job(number):
print(f"Job {number} started")
await asyncio.sleep(1) # 매우 오래 걸리는 작업, asyncio.sleep은 비동기 처리를 할 수 있도록 합니다.(다른 작업이 가능합니다.)
print(f"Job {number} completed")
async def main():
await asyncio.gather(job(1), job(2), job(3)) # 세 작업을 동시에 진행합니다.
asyncio.run(main())
print('hello world')
일부 작업은 비동기 프로그래밍을 중간에 동기로 바꿔야 하는 순간이 오기도 합니다. 아래와 같이 비동기 프로그래밍을 동기로 만들 수 있습니다.
import asyncio
async def job(number):
print(f"Job {number} started")
await asyncio.sleep(1) # 매우 오래 걸리는 작업
print(f"Job {number} completed")
asyncio.run(job(1))
asyncio.run(job(2))
asyncio.run(job(3))
위와 동일한 동작을 하는 colab에서만 가능한 코드를 보도록 하겠습니다. .py 파일에서는 await이 함수 밖에 사용되는 것을 허락하지 않습니다.
import asyncio
async def job(number):
print(f"Job {number} started")
await asyncio.sleep(1) # 매우 오래 걸리는 작업
print(f"Job {number} completed")
await job(1)
await job(2)
await job(3)
다음 코드는 .py 파일에서 위와 동일하게 작동하는 코드입니다.
import asyncio
async def job(number):
print(f"Job {number} started")
await asyncio.sleep(1) # 매우 오래 걸리는 작업
print(f"Job {number} completed")
async def main():
await job(1)
await job(2)
await job(3)
asyncio.run(main())
1.1.2 코루틴(Coroutines)
코루틴은 비동기 프로그래밍의 핵심 개념 중 하나로, 파이썬에서는 async / await 구문을 통해 코루틴을 간편하게 구현할 수 있습니다. 코루틴은 파이썬 3.5부터 async / await 구문을 통해 지원되기 시작했습니다.
- 코루틴 함수:
async def키워드를 사용하여 정의된 함수입니다. 이 함수는 코루틴 객체를 반환하며, 비동기적으로 실행될 수 있습니다. - 코루틴 객체: 코루틴 함수가 호출될 때 생성되는 객체로, 이벤트 루프에서 실행될 준비가 된 비동기 작업을 나타냅니다.
await키워드: 코루틴 안에서 다른 코루틴을 호출하고 완료를 기다릴 때 사용됩니다.await키워드를 만나면 해당 코루틴은 일시 중단되고, 호출된 코루틴의 작업이 완료될 때까지 다른 작업으로 제어가 넘어갑니다.
코루틴은 asyncio 모듈의 함수를 통해 실행됩니다.
asyncio.run(coro): 새 이벤트 루프를 만들고 주어진 코루틴coro를 실행한 후, 이벤트 루프를 닫습니다.asyncio.gather(*coros): 여러 코루틴을 동시에 실행하도록 스케줄링합니다. 이 함수는 모든 코루틴이 완료될 때까지 기다린 후 결과를 반환합니다.
아래 함수는 일반 함수입니다.
def job():
print('job')
아래 코드는 async를 붙인 함수, 코루틴 함수입니다. await 키워드를 만나면 코루틴 실행을 잠시 중단하고, 코루틴의 작업이 완료될 때까지 기다린 후 결과를 반환합니다.
async def job():
print('job')
print(job) # <function job at 0x...>, 함수 그 자체입니다.
job() # <coroutine object job at 0x...>, 호출하면 코루틴 객체가 만들어질 뿐
# print('job')은 아직 실행되지 않습니다!
일반 함수는 호출하는 순간 몸통이 실행되지만, 코루틴 함수는 호출해도 '실행할 준비가 된 작업'만 만들어질 뿐 아무것도 실행되지 않습니다. 실제로 실행하려면 await을 붙이거나 asyncio.run()에 넘겨 이벤트 루프에 맡겨야 합니다.
async def main():
return await job() # 'job' 출력을 기다림
main() # <coroutine object main at 0x7fc8cb22bf40>
print(await main()) # None
코루틴은 비동기 프로그래밍에서 중요한 역할을 합니다. 여러 I/O 작업이나 네트워크 요청을 병렬로 처리할 때, 코루틴은 코드의 복잡성을 줄이고 성능을 향상시키는 데 큰 도움을 줍니다.
1.2 이벤트 루프(Event Loop)
이벤트 루프는 비동기 프로그래밍에서 중심적인 역할을 하는 구성 요소입니다. 이벤트 루프의 주요 기능은 애플리케이션의 흐름을 제어하고, 비동기 작업을 관리하는 것입니다.
1.2.1 이벤트 루프의 작동 원리
- 작업 관리: 이벤트 루프는 실행할 코루틴과 작업을 관리합니다. 루프는 작업이 완료될 때까지 실행하고, 완료된 작업의 결과를 반환합니다.
- 이벤트 모니터링: 이벤트 루프는 I/O 이벤트(예: 네트워크 요청, 사용자 입력)를 모니터링합니다. 적절한 이벤트 핸들러 또는 콜백 함수가 이벤트에 응답합니다.
- 비동기 작업 스케줄링: 이벤트 루프는 비동기 작업을 스케줄링하고, 실행 준비가 된 작업을 실행합니다. 이를 통해 작업이 병렬로 실행되는 것처럼 보이게 합니다.
1.2.2 이벤트 루프 사용 예시
import asyncio
async def async_task():
# 비동기 작업 정의
...
# 이벤트 루프를 만들고, 실행하고, 닫는 일을 한 번에 처리합니다.
asyncio.run(async_task())
이 코드는 코루틴 async_task를 실행하기 위해 이벤트 루프를 사용하는 기본적인 예시입니다.
asyncio.get_event_loop()는 이제 쓰지 마세요.
예전 자료에서는 아래와 같이 이벤트 루프를 직접 만들어 쓰는 코드를 자주 볼 수 있습니다.
loop = asyncio.get_event_loop() # 3.10부터 경고, 이후 버전에서 동작이 바뀌었습니다.
loop.run_until_complete(async_task())
loop.close()
파이썬 3.10부터 이 방식은 경고를 내고, 이후 버전에서는 실행 중인 루프가 없을 때 에러가 납니다. 지금은 asyncio.run()이 루프 생성부터 정리까지 알아서 처리해주니 이쪽을 사용해주세요.
1.2.3 이벤트 루프의 중요성
하나의 프로그램에는 보통 이벤트 루프가 하나만 돌아갑니다. 이 하나의 루프가 여러 코루틴을 번갈아 실행시키면서 전체 흐름을 조율합니다. 코루틴 하나하나가 각자 이벤트 루프를 갖는 것이 아니라는 점을 기억해주세요.
이러한 구조 덕분에 스레드를 여러 개 만들지 않고도 여러 작업을 동시에 진행하는 것처럼 처리할 수 있습니다. 특히 네트워크 요청처럼 기다리는 시간이 긴 작업에서 응답성을 크게 개선할 수 있습니다.