비동기란?
asyncio를 알기 전에 먼저 비동기에 대해서 먼저 알아야 한다. 사실 우리는 대부분 비동기적으로 행동하고 있다. 만약 요리와 세탁과 TV 시청을 한다고 해보자. 아마 대부분 세탁이 다 될 때까지 기다렸다가 세탁이 끝나면 요리를 하고, 요리가 끝나면 TV를 보는 비효율적인 인생을 살지 않을 것이다. 모든 사람들이 세탁기를 돌려놓고 요리를 하면서 TV를 볼 것이다. 바로 이것이 비동기의 핵심이다. 컴퓨터에서 비동기는 여러 작업을 동시에 하는 것과 같은 행동을 하는 것이다.
asyncio란?
asyncio는 이러한 비동기 방식을 파이썬에서 사용할 수 있도록 하는 라이브러리이다. asyncio는 기본적으로 이벤트 루프와 코루틴이라는 개념 위에서 동작하는데 이에 대해서 정확하게 알아야 asyncio를 제대로 사용할 수 있다.
이벤트 루프란?
이벤트 루프는 작업들에 대해 루프를 돌면서 하나씩 실행시키는 역할을 한다. 이렇게 말하면 무슨 기능을 하는지 알기가 쉽지 않다. 이를 제대로 이해하기 위해서는 다시 한 번 비동기 함수의 정의을 살펴보아야 한다. 아까 비동기 함수는 어떤 식으로 작동한다고 했었는가? 어떤 작업을 할 때 작업을 돌려놓고 다른 작업을 하러 가는 것이 비동기 함수의 핵심이다. 이벤트 루프는 바로 이러한 비동기 방식을 실행하는 역할을 한다.
이벤트 루프가 작업들에 대해 반복문을 돌며 하나씩 실행시키면, 실행한 작업(①)이 데이터를 요청하고 응답을 기다리는 동안 다른 작업(②)에게 이벤트 루프에 대한 권한을 넘겨받는다. 권한을 받은 작업(②)은 똑같이 데이터를 요청하는데 이때 아까 요청을 보냈던 작업①이 요청에 대한 응답을 받았다고 해보자. 그러면 작업①은 작업을 마무리하기 위해 대기하는데 만약 작업②에 대한 요청이 끝났다면 다시 이벤트 루프에게 권한을 받아 작업을 마무리한다.
정확한 표현인지는 모르겠지만 이벤트 루프는 감독관이고 작업들은 인부들이다. 인부들은 감독관에게 권한을 받아 작업을 하고 잠시 쉴 때는 다시 권한을 감독관에게 넘기고 감독관은 받은 권한을 다시 다른 인부에게 주어 작업을 하게 한다.
코루틴이란?
코루틴(coroutine)은 cooperative routine의 줄임말로 말 그대로 협력하는 루틴이라는 뜻이다. 협력하는 루틴이 무슨 말일까? 이를 위해선 루틴과 협력에 대해 알아야 한다. 먼저 루틴이 뭔지부터 알아보기 위해 아래와 같은 함수가 있다고 생각해보자.
def add(a, b):
c = a + b # add 함수가 끝나면 변수와 계산식은 사라짐
print(c)
print('add 함수')
def calc():
add(1, 2) # add 함수가 끝나면 다시 calc 함수로 돌아옴
print('calc 함수')
calc()
add()는 calc()라는 함수 아래에서 작동한다. calc()를 실행하면 add()가 호출되고 add를 마무리하였다면 다시 calc() 함수로 돌아와 동작을 마무리한다. 여기서 calc()는 메인 루틴, add()는 서브 루틴이라고 한다. 이 둘의 동작 과정을 그림으로 나타내면 아래와 같다. 당연하게도 서브 루틴은 동작이 끝나면 변수나 계산식 등 모든 내용이 사라진다.
우리가 알아볼 코루틴은 이와 다르게 동작한다. 여기서 협력에 대한 부분이 나오는데 코루틴은 메인 루틴과 서브 루틴처럼 종속된 관계가 아니라 대등한 입장에서 서로를 호출한다. 아래 그림처럼 서로를 번갈아가며 호출하는 형태를 가진다. 당연히 코루틴 함수는 계속해서 내용이 유지되고 코드를 여러 번 호출할 수 있다.
asyncio에서는 이벤트 루프와 코루틴 이 두 개념을 가지고 비동기적인 동작을 수행할 수 있다. 본격적으로 asyncio에 대해서 알아보도록 하자!
asyncio 사용법 알아보기
기본적으로 우리가 사용하는 파이썬의 def 함수는 동기적으로 처리된다. 따라서 이를 비동기로 바꾸기 위해서는 조치가 필요하다. asyncio에서는 이를 위해 def 앞에 비동기를 알리는 async를 붙인다. 이렇게 되면 파이썬에서는 해당 함수가 비동기로 동작하는데 이 함수가 앞에서 살펴보았던 코루틴이다. 아래는 더하기 연산을 동기와 비동기 방식으로 나타낸 함수이다.
# 동기 함수
def sync_add():
print(1+2)
# 비동기 함수 = 코루틴
async def async_add():
print(1+2)
이제 두 함수 모두 실행을 시키면 3이 출력될까? 안타깝게도 코루틴은 3이 출력되지 않는다. 코루틴은 일반 함수처럼 호출한다고 동작하지 않고 대신 코루틴 객체를 리턴한다. 그렇다면 코루틴을 실행하기 위해서 무엇을 해야할까? 코루틴을 실행하려면 다른 비동기 함수 내에서 await을 붙여서 호출해야 한다. 이렇게 하면서 코루틴이 다른 코루틴을 부를 수 있게 되는 것이다.
async def main():
await async_add()
await는 단어 그대로 기다리는 역할을 한다. 따라서 I/O가 발생하거나 해당 코드가 실행되는 동안 권한을 다른 코드에게 넘겨 또 다른 작업을 수행하고 싶은 함수의 앞에 붙이면 된다. 참고로 await 뒤의 함수는 반드시 코루틴으로 작성된 함수여야 한다. 동기 함수를 사용한다면 아무리 await를 붙여도 동기적으로 실행이 되니 주의하도록 하여야 한다.
위에서 await로 async_add()를 실행시키는 것까지는 알겠다. 그러면 main()은 어떻게 실행시킬까? 코루틴 안에 코루틴은 await로 실행시킬 수 있지만 그냥 코루틴은 await로는 실행을 시킬 수가 없다. 이때 이벤트 루프를 사용하는 것이다. 따지고 보면 await 뒤의 함수도 이벤트 루프를 사용하는 것이긴 하지만 main()을 실행시키기 위해서는 이벤트 루프 객체를 생성하고 main()에게 권한을 부여해 작업을 실행해야 한다. 이를 위해서 asyncio에서는 get_event_loop()와 run_until_complete()를 이용한다.
먼저 get_event_loop()를 이용해서 이벤트 루프 객체를 생성한다. 그리고 해당 객체에서 run_until_complete()를 호출하여 코루틴을 실행시킨다. run_until_complete은 이름 그대로 끝날 때까지 실행한다는 뜻으로 코루틴이 이벤트 루프에서 실행되도록 예약하고 해당 코루틴이 끝날 때까지 기다린다. 그리고 해당 작업이 끝나면 close()를 통해 이벤트 루프를 종료한다.
간단하게 지식 하나를 더 채우고 가자면 run_forever()라는 함수도 있다. 이는 영원히 실행하라는 뜻인데, 혹시 짐작이 가는지 모르겠다. run_until_complete은 이벤트 루프가 끝나면 모든 코루틴이 중지된다. 하지만 run_forever는 명시적으로 loop.stop()을 해주지 않는다면 영원히 루프를 돈다. 자세한 내용을 확인하고 싶다면 여기 스택오버플로우 질문을 참고하면 될 거 같다!
import asyncio
async def async_func():
pass
loop = asyncio.get_event_loop()
loop.run_until_complete(async_func())
loop.close()
하지만 이벤트 루프를 생성하고 여기에 또 함수를 호출하는 것은 조금 귀찮다. 그래서 파이썬 3.7 버전부터는 조금 더 간단하게 비동기 함수를 사용할 수 있게 되었다. asyncio.run()를 이용하면 이 둘을 한 번에 처리할 수 있는데 사용 예는 아래와 같다.
import asyncio
async def async_func():
pass
asyncio.run(async_func())
이번 포스팅에서는 asyncio에 대해 전반적인 개념을 이해해보는 시간을 가졌다. 하지만 아직 asyncio에 대해 다 배웠다고 하기에는 넘어야 할 산이 많다. 다음 시간에는 asyncio를 정말로 비동기적으로 처리하는 방법, 태스크, 퓨처 객체 등 asyncio에 대해서 더 자세하게 알아볼 것이다.
참고
[Python] 비동기 프로그래밍 동작 원리 (asyncio)
'개발 > 개발 지식' 카테고리의 다른 글
Anaconda와 JupyterLab 알아보기 (0) | 2022.07.10 |
---|---|
파이썬 DB 커넥터(psycopg2) 간단한 사용법/executor과 commit 차이 (0) | 2022.04.06 |
NFS란? 정의/장단점 (0) | 2022.04.04 |
REST vs Websocket - 차이/작동방식/적절한 비유 (1) | 2022.03.29 |
Vagrant 어떤 용도일까? 정의/사용이유/Docker와 비교 (0) | 2022.03.23 |