Profile picture

[Python] asyncio를 사용한 비동기 프로그래밍

JaehyoJJAng2023년 06월 02일

문법

def 키워드로 선언하는 모든 함수는 파이썬에서 기본적으로 동기 함수로 동작한다

아래와 같이 선언된 함수는 동기 함수로 작동한다

def do_sync() -> str:
    return 'sync'

기존 def 키워드 앞에 async 키워드를 붙이면 이 함수는 비동기 처리되며, 이러한 비동기 함수를 파이썬에서는 코루틴(coroutine)이라고 부른다

async def do_async() -> str:
    return 'async'

이러한 비동기 함수는 일반 동기 함수가 호출하듯이 호출하면 코루틴 객체가 반한됨

print(type(do_async()))

따라서 비동기 함수는 일반적으로 async로 선언된 다른 비동기 함수 내에서 await 키워드를 붙여서 호출해야 함

async def main_async():
    await do_async()

async로 선언되지 않은 일반 동기 함수 내에서 비동기 함수를 호출하려면 asyncio 라이브러리의 이벤트 루프를 사용해야 한다

import asyncio

loop = asyncio.get_event_loop()
loop.run_until_complete(main_async())
loop.close()

파이썬 3.7 버전 이상부터는 다음과 같이 한 줄로 간단히 비동기 함수를 호출할 수 있다

asyncio.run(main_async())

실습 프로젝트

사용자 관리 애플리케이션을 데모로 실습 코드를 작성하면서 동기 처리하는 코드와 비동기 처리를 하는 코드를 비교해보도록 하게싿

**실습을 위한 가정은 아래와 같다 **

  • 애플리케이션은 사용자 데이터를 직접 보관하지 않고 외부 API를 호출해서 가져온다
  • 외부 API는 1명의 사용자 데이터를 조회하는데 1초가 걸리고, 한 번에 여러 사용자의 데이터를 조회할 수 없다
  • 각각 3명,2명,1명의 사용자 정보를 조회하는 요청 3개가 동시에 애플리케이션에 들어온다

동기 프로그래밍

사용자 데이터 조회를 동기 방식으로 처리해주는 find_users_sync 함수를 작성. 의도적으로 1초의 지연을 주기 위해 time.sleep() 함수 사용

import time

def find_users_sync(n: int) -> None:
    for i in range(1 , n + 1):
        print(f'{n}명 중 {i}번 째 사용자 조회 중')
        time.sleep(1.0)
    print(f'> 총 {n}명의 사용자 동기 조회 완료')

애플리케이션에 들어온 3개의 요청을 동기 처리하는 process_sync 함수 작성

def process_sync() -> None:
    start = time.time() 
    find_users_sync(n=3)
    find_users_sync(n=2)
    find_users_sync(n=1)
    end = time.time()
    print(f'>>> 동기 처리 총 소요 시간 : {end - start}')

if __name__ == '__main__':
    process_sync()

이 함수를 호출해보면 find_users_sync 함수가 총 6초 동안 3번 순차적으로 실행된다

3명 중 1번 째 사용자 조회 중
3명 중 2번 째 사용자 조회 중
3명 중 3번 째 사용자 조회 중
>3명의 사용자 동기 조회 완료
2명 중 1번 째 사용자 조회 중
2명 중 2번 째 사용자 조회 중
>2명의 사용자 동기 조회 완료
1명 중 1번 째 사용자 조회 중
>1명의 사용자 동기 조회 완료
>>> 동기 처리 총 소요 시간 : 6.01879096031189

싱글 쓰레드 기반 웹 서버가 위와 같이 동작하면 실제 사용자는 오랫동안 지연을 경험해야 할 것이다.


비동기 프로그래밍

위에서 동기 처리되도록 작성된 코드를 파이썬의 async/await 키워드를 사용해서 비동기 처리될 수 있도록 개선해보자. 기존의 함수 선언에 async 키워드를 붙여서 일반 동기 함수가 아닌 비동기 함수(coroutine)로 변경하고 time.sleep() 함수 대신에 asyncio.sleep() 함수를 사용하여 1초의 지연을 발생시키자

time.sleep() 함수는 기다리는 동안 CPU를 그냥 놀게하는 바면, asyncio.sleep() 함수는 CPU가 놀지 않고 다른 처리를 할 수 있도록 해준다. 여기서 주의할 점은 asyncio.sleep() 자체도 비동기 함수이기 때문에 호출할 때 반드시 await 키워드를 붙여야 한다

import asyncio

async def find_users_async(n: int) -> None:
    for i in range(1 , n + 1):
        print(f'{n}명 중 {i}번 째 사용자 조회 중')
        await asyncio.sleep(1.0)
    print(f'> 총 {n}명 사용자 조회 완료')

이제 파이썬의 asyncio 모듈을 사용해서 위에서 작성한 함수를 비동기로 실행해보자. 먼저 이벤트 루프가 3개의 함수 호출을 알아서 스케줄하여 비동기로 호출할 수 있도록 asyncio.gather 함수의 인자로 3개의 함수 반환값, 즉 코루틴 객체를 넘겨주도록 수정하자
그리고 이렇게 수정된 process_async 비동기 함수를 호출할 때도, 함수의 반환값인 코루틴 객체를 asyncio.run 함수에 넘겨주도록 하자

async def process_sync() -> None:
    start = time.time() 
    wait asyncio.gather(
        find_users_sync(n=3)
        find_users_sync(n=2)
        find_users_sync(n=1)
    )
    end = time.time()
    print(f'>>> 동기 처리 총 소요 시간 : {end - start}')

if __name__ == '__main__':
    process_sync()

비동기 처리되도록 재작성된 코드를 실행해보면 호출 순서와 무방하게 실행 시간이 짧을 수록 먼저 처리되는 것을 알 수 있었다. 총 소요시간 또한 6초에서 3초로 단축되었음을 확인할 수 있다

실제 사용자 관점에서 3초가 걸리는 요청을 기다리지 않고, 1초가 걸리는 요청은 1초만에 응답이 오고 2초가 걸리는 요청은 2초 만에 응답이 올테니 매우 이상적이라고 할 수 있다

3명 중 1번 째 사용자 조회 중
2명 중 1번 째 사용자 조회 중
1명 중 1번 째 사용자 조회 중
3명 중 2번 째 사용자 조회 중
2명 중 2번 째 사용자 조회 중
>1명의 사용자 동기 조회 완료
3명 중 3번 째 사용자 조회 중
>2명의 사용자 동기 조회 완료
>3명의 사용자 동기 조회 완료
>>> 비동기 처리 총 소요 시간 : 3.0035018920898438

Loading script...