파이썬 비동기 프로그래밍

배진오

@baealex

소비적인 일보단 생산적인 일을 좋아합니다.

이 글은 sleep(1)에 관한 의문으로 시작되었다.

  • "sleep(1)은 왜 비효율적이라고 할까?"
  • "setTimeout(1000)과 sleep(1)은 근본적으로 어떤 차이가 있는 걸까?"

위 의문이 단지 동기와 비동기라는 것의 차이라면 정확히 비동기라는 녀석이 어떻게 생겨먹고 굴러가고 있는지 궁금했다. 공부를 지속하고 있지만 아직까지 정확히 '비동기를 이해하고 있다'고 생각하진 않는다. 되려 혼란스러운 상태다.



JavaScript

우리는 자바스크립트를 비동기 언어라고 인식하지만 자바스크립트는 순수 비동기 언어가 아니다. 무한루프를 돌려보자(?) 비동기 로직을 사용할 수 있을 뿐 비동기 언어는 아니다. 데이터베이스를 호출하는 등 네트워크 요청이나 파일 입출력, setTimeout 같이 시간이 오래 걸리는 로직들은 요청만하고 다음 로직을 지속적으로 처리해 나간다. 내부적으로 어떻게 동작하고 있을까?

자바스크립트엔 Call StackEvent Loop, Task Queue라는 녀석들이 있다. 모든 함수는 호출시 Call Stack에 등록되며 LIFO 방식으로 처리된다. Call Stack은 대부분의 프로그래밍 언어가 동일하게 동작한다. 비동기 함수는 이때 Call Stack에 등록되었다가 실행한 후 Call Stack에서 사라진다. 그리고 처리해야 할 다음 함수들을 처리한다.

setTimeout 같은 경우에는 WebAPI에 등록되어 타이머를 측정한다. 이후 타이머가 끝나면 Task Queue에 콜백 함수가 등록되며 Event LoopCall Stack에 내용이 없다는 것을 확인하면 Task Queue에 있는 함수를 FIFO 방식으로 Call Stack에 등록한다.

아직까지 궁금한 점은 비동기 함수를 호출했을 때 이 비동기 함수가 내부적으로 어떻게 처리되고 있는지 궁금하다. 결국 스레드를 생성하여 처리되는 건 아닌지... 그렇다면 비동기가 동기 멀티 스레드와 근본적으로 어떻게 차이가 있는 것일까. 관련 글을 찾아보면 '커피집 알바생' 얘기만하니 답답해 미칠 노릇이다.



Python

여하튼 비동기 처리의 성능이 매우 훌륭하다는 것은 익히 알려진 사실이며 파이썬 3.4에도 asyncio라는 비동기 표준 라이브러리를 추가하였다. 3.5에선 asyncawait라는 예약어가 추가되었다.

def do_sync(): #일반 함수
    pass
async def do_async(): #코루틴 함수
    pass

파이썬에서 비동기 함수는 코루틴 함수라고 불려진다. 코루틴 함수를 직접 호출하면 노드에서 프로미스를 호출하듯 코루틴 함수가 반환된다. 기본적으로 코루틴 함수는 다른 코루틴 함수에서 await 키워드를 붙여서 호출해야 한다.

async def main_async():
    await do_async()

일반 함수에서 코루틴 함수를 호출하려면 asyncio 라이브러리 이벤트 루프를 이용해야 한다.

3.6 이하에서

import asyncio

async def do_async():
    pass

async def main_async():
    await do_async()

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main_async())
    loop.close()

3.7 이상에서

import asyncio

async def do_async():
    pass

async def main_async():
    await do_async()

if __name__ == '__main__':
    asyncio.run(main_async())


비동기 파일 IO

비동기 방식으로 프로그래밍하려면 비동기 라이브러리를 사용해야 하는데 비동기 파일 입출력은 aiofiles라는 라이브러리를 이용하여 처리할 수 있다.

import asyncio
import aiofiles

async def main():
    async with aiofiles.open('test.txt', 'w') as f:
        await f.write('Hello World!')

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())
    loop.close()


비동기 네트워크 IO

비동기 네트워크는 aiohttp라는 라이브러리를 사용하여 처리할 수 있다. 아래는 비동기 네트워크 라이브러리를 이용해 python의 유명한 라이브러리인 requests와 비슷한 문법으로 요청할 수 있도록 만들어 본 클래스다.

import asyncio
import aiohttp

class async_requests:
    async def get(url):
        async with aiohttp.ClientSession() as session:
            async with session.get(url) as res:
                return {
                    'text': await res.text(),
                    'status_code': res.status,
                    'headers': res.raw_headers
                }

    async def post(url, data=None, json=None):
        async with aiohttp.ClientSession() as session:
            async with session.post(url, data=data, json=json) as res:
                return {
                    'text': await res.text(),
                    'status_code': res.status,
                    'headers': res.raw_headers
                }

async def main():
    res = await async_requests.get('https://blex.me')
    print(res['text'])

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())
    loop.close()

근데 생각해보면 비동기 네트워크 IO라는게... 클라이언트 입장에선 효율적이겠지만 상대측이 동기 멀티 스레드로 구현되어 있다면 고문일 것 같다...


비동기 웹 프레임워크

비동기 로직에 대한 의문이 왜 여기까지 왔는지 모르겠지만 결과적으로 이걸 학습하는 목적은 현재 이 BLEX도 그렇고 적어도 파이썬으로 웹 개발을 한다면 최대한 비동기 프로그래밍을 활용하고자 한다.


Vibora

Vibora는 파이썬의 비동기 웹 프레임워크이다. 파이썬 웹 프레임워크 중에 가장 빠르다고 알려졌으며(Flask대비 5-6배 초당 응답량 많음) Flask 스타일을 지향한다. Vibora가 빠른 이유는 내부적으로 가능한 Cpython을 주로 사용하며 Event Loopuvloop로 사용하기 때문인 것으로 보인다. uvloopCpythonlibuv로 만들어졌다.

공식 홈페이지에선 3.6이상을 사용하라고 권장하나 Python 3.7에선 Syntax에러가 발생하여 사용이 불가능하다. 해당 프레임워크를 사용해보고 싶은 경우 3.6 버전을 설치해야 하므로 pyenv를 활용하도록 하자.

from vibora import Vibora, Request
from vibora.responses import JsonResponse

app = Vibora()

@app.route('/')
async def home(request: Request):
    return JsonResponse({'hello': 'world'})

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=8000)

Flask와 문법이 전체적으로 동일하나 모든 라우트 함수 앞에 async라는 키워드가 추가되었다.


Async ORM

해당 라이브러리의 이름은 orm으로 지어져 있으나. 이는 실제 널리 사용되는 이름이므로 혼동이 생길 수 있어 Async ORM으로 부르도록 하겠다. 필자는 간단하게 테스트를 위해서 사용하므로 sqlite를 사용한다.

pip install orm~=0.1
pip install aiosqlite

라이브러리 개발자가 말하길 개발중인 상태이므로 위와같이 설치하는 것을 권장한단다.

import databases
import orm
import sqlalchemy
import asyncio

database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()

class Note(orm.Model):
    __tablename__ = "notes"
    __database__ = database
    __metadata__ = metadata

    id = orm.Integer(primary_key=True)
    text = orm.String(max_length=100)
    completed = orm.Boolean(default=False)

# Create Database
engine = sqlalchemy.create_engine(str(database.url))
metadata.create_all(engine)

async def create():
    await Note.objects.create(text="Buy the groceries.", completed=False)
    await Note.objects.create(text="Call Mum.", completed=True)
    await Note.objects.create(text="Send invoices.", completed=True)

async def get():
    notes = await Note.objects.all()
    return notes

개쩐다. 장고의 ORM 문법과 거의 동일하다. 장고의 내부 함수를 많이 사용하지 않았다면 마이그레이션도 간단할 것 같다. 여하지간 요걸 Vibora랑 섞으면 이런 느낌이 된다.

import databases
import orm
import sqlalchemy
import asyncio
import re

from vibora import Vibora, Request
from vibora.responses import Response, JsonResponse

database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()

class Note(orm.Model):
    __tablename__ = "notes"
    __database__ = database
    __metadata__ = metadata

    id = orm.Integer(primary_key=True)
    text = orm.String(max_length=100)
    completed = orm.Boolean(default=False)

app = Vibora()

@app.route('/create_database')
async def note_create(request: Request):
    engine = sqlalchemy.create_engine(str(database.url))
    metadata.create_all(engine)

@app.route('/notes')
async def note(request: Request):
    notes = await Note.objects.all()
    return JsonResponse(list(map(lambda x: {x.id: x.text}, notes)))

@app.route('/create_notes')
async def create():
    await Note.objects.create(text="Buy the groceries.", completed=False)
    await Note.objects.create(text="Call Mum.", completed=True)
    await Note.objects.create(text="Send invoices.", completed=True)
    return JsonResponse({'state': 'done'})

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=8000)

상당히 Node 스럽네...

curl http://localhost:8000/notes
[{"1":"Buy the groceries."},{"2":"Call Mum."},{"3":"Send invoices."}]


ETC.

Vibora를 사용하는 것은 시기상조다.

  • 문서가 상태가 상당히...
  • 자잘한 오류가 정말 많다.
  • 가장 큰 문제는 이 자잘한 오류가 이슈에도 등재되어 있으며 전혀 고쳐지지 않고 있다.
😥 작성된 댓글이 없습니다!
댓글을 작성하기 위해 로그인이 필요합니다.