본문 바로가기
Coding/Python

Python async/await로 비동기 프로그래밍하기

by Hide­ 2019. 5. 23.
반응형

토이 프로젝트를 진행하면서 어떤 웹 프레임워크를 사용할까 고민을 많이 했었다. 기존에는 Django 또는 Flask로 대부분의 작업을 진행했는데, 요즘 파이썬 비동기 프로그래밍에 관심이 생겨서 관련 프레임워크를 찾아봤다. 많은 프레임워크가 있었고 그 중 aiohttp, sanic, vibora 총 3가지를 후보군으로 뒀다.


먼저 aiohttp는 주변에서 추천해주길래 한번 써봤는데 나쁘진 않았다. 하지만 sanic과 vibora보다 퍼포먼스가 좋지 않았다. 두번째로 vibora는 가장 속도가 빨랐다. 하지만 깃헙 스타가 5000개밖에 되지 않았다. 결과적으로 sanic을 사용하기로 결정했는데 그 이유는 다음과 같다. 


첫번째로 형태가 Flask와 비슷했다. 나는 보통 프로젝트를 시작할 때 boilerplate등을 검색해보며 일반적으로 사용하는 프로젝트 구조 또는 패턴등을 먼저 익히는 편이다. 애초에 비동기 웹 프레임워크가 Django/Flask등과 비교하여 많이 쓰이지 않기에 이럴수록 신중을 기하는 편이 좋다고 생각했다. 마음에 드는 boilerplate가 없다면, Flask 스타일로 구조를 짜려고 했다. 두번째로 vibora보다는 높은 12000의 깃헙 스타가 있었다. 2배 이상의 사람들이 사용하고 있기에 퍼포먼스가 vibora보다 떨어져도 트러블 슈팅 시 많은 레퍼런스를 찾을 수 있을 것이라고 판단했다.


파이썬 3.4부터 async/await를 지원한다고 알고만 있었지 실제로 써본적은 없었다. 또한 이번 토이 프로젝트에서도 비동기 프로그래밍을 공부하고 시작한것이 아닌 무작정 비동기 웹 프레임워크로 구조부터 짜자 라는 마인드로 시작했기에, 사용하며 막히는 부분이 많았다. 따라서 간단하게나마 이렇게 정리하기로 마음먹었다.


먼저 파이썬의 코루틴에 대해서는 많은 자료가 있으므로 설명하지 않도록 하고 내가 생각하는 가장 중요한 핵심은 다음과 같다.


"여러개의 코루틴을 한번에 스케줄링하면 순서대로 실행되면서 await 가 등장하면 다른 코루틴으로 제어권이 넘어간다."


await가 등장하면 다른 코루틴으로 제어권이 넘어간다..만 잘 기억해도 코루틴에 대해 쉽게 이해할 수 있으리라 생각한다. 이제 main하나의 함수를 통해 3개의 작업을 동시에 진행해보자. (엄밀히 따지면 멀티 프로세스처럼 병렬로 실행되는것은 아니다)


import asyncio


def main():
loop = asyncio.get_event_loop()
func_set = asyncio.gather(test_1(), test_2())
loop.run_until_complete(func_set)

async def test_1():
print('test 1 start')
await asyncio.sleep(2)
print('test 1 end')
return 'test1'

async def test_2():
print('test 2 start')
await asyncio.sleep(1)
print('test 2 end')
return 'test2'


if __name__ == '__main__':
main()


main()부터 살펴보자면 asyncio.get_event_loop() 를 통해 먼저 이벤트 루프를 정의한다. 그리고 여러개의 함수를 실행시키기 위해 asyncio.gather() 를 사용한다. 그리고 실행과 동시에 모든 작업이 완료될 때 까지 기다리기 위해 run_until_complete() 함수를 사용했다. 위 함수를 실행시켜보면 결과는 아래와 같다.


test 1 start

test 2 start

test 2 end

test 1 end

[Finished in 2.1s]


test_1()이 가장 먼저 실행됐다. 하지만 await를 걸어준 sleep을 만났기 때문에 2초를 sleep함과 동시에 test_2()로 제어권이 넘어갔다. test_2()에서 프린트문을 실행하고 sleep했다. test_1()보다 먼저 sleep이 끝났으므로 test 2 end를 출력하고 test_2() 함수는 종료된다. 그리고 sleep을 마친 test_1()도 test 1 end를 출력하고 종료됐다. 실제 시간만 봐도 2.1초로써 병렬적으로 실행되는것과 거의 동일한 결과라고 볼 수 있다. 이러한 과정을 쉽게 풀어쓰자면 다음과 같다.


1. test_1() 함수 실행

2. test_1() - "test 1 start" 프린트문 출력

3. test_1() - sleep(2)초를 함과 동시에 await로 인해 제어권이 test_2()로 넘어감

4. test_2() 함수 실행

5. test_2() - "test 2 start" 프린트문 출력

6. test_2() - sleep(1)

7. test_2() - "test 2 end" 프린트문 출력

8. test_1() - "test 1 end" 프린트문 출력


위 코드를 보면 test_1(), test_2() 모두 마지막에 값을 return한다. 만약 이렇게 함수의 리턴값을 받고싶다면 어떻게 해야할까? 그냥 단순히 run_until_complete를 변수에 담으면 된다. 변수에 담으면 리스트 형태로 리턴값이 넘어온다. 아래의 코드를 보자.


import asyncio


def main():
loop = asyncio.get_event_loop()
func_set = asyncio.gather(test_1(), test_2())
test1, test2 = loop.run_until_complete(func_set)
print(f'test_1 return value = {test1} / test_2 return value = {test2}')

async def test_1():
print('test 1 start')
await asyncio.sleep(2)
print('test 1 end')
return 'test1'

async def test_2():
print('test 2 start')
await asyncio.sleep(1)
print('test 2 end')
return 'test2'


if __name__ == '__main__':
main()


위 함수를 실행시켜보면 다음과 같은 결과가 나온다.


test 1 start

test 2 start

test 2 end

test 1 end

test_1 return value = test1 / test_2 return value = test2

[Finished in 2.2s]


추가적으로 await뒤에 오는 함수 역시 코루틴으로 작성되어 있어야 한다. 하지만 만약 코루틴으로 짜여있지 않은 함수를 비동기적으로 이용하고 싶다면 run_in_executor() 함수를 사용하면 된다. 예를 들어 위 코드에서 sleep을 위해 asyncio.sleep()을 사용했지만 time.sleep()처럼 코루틴이 아닌 함수를 사용하고 싶다면 아래와 같은 형태로 진행하면 된다.


import asyncio
import time


def main():
loop = asyncio.get_event_loop()
func_set = asyncio.gather(test_1(), test_2())
test1, test2 = loop.run_until_complete(func_set)
print(f'test_1 return value = {test1} / test_2 return value = {test2}')

async def test_1():
print('test 1 start')
# await asyncio.sleep(2)
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, time.sleep, 2)
print('test 1 end')
return 'test1'

async def test_2():
print('test 2 start')
# await asyncio.sleep(1)
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, time.sleep, 1)
print('test 2 end')
return 'test2'


if __name__ == '__main__':
main()


test_1(), test_2() 각 함수에서 마찬가지로 get_event_loop() 를 통해 이벤트 루프를 가져오고 run_in_executor() 를 통해 실행시켰다. 위 코드를 실행시키면 처음 코드와 동일한 결과를 가질 수 있다.