Python/Flask

Flask에서 async 기반 API 구현하기

검정비니 2023. 10. 9. 19:43
728x90
반응형

Flask에서 async API 만들기?

일반적으로 Flask는 wsgi와 연동되도록 synchronous하게 동작하는 것을 기대하고 구현되었기 때문에 async 함수 없이 구현하는 것이 일반적인 패턴이다.

 

그러나 경우에 따라 필요에 의해 Flask 서버에 비동기 non-blocking I/O 기반 기능을 추가해야 할 경우가 발생하게 된다. 애초에 async 함수로 구현되는 FastAPI에서는 그다지 할 필요가 없는 고민이긴 하다.

어찌되었든, Flask에서 비동기를 사용하고 싶다면 어떤 방법으로 문제를 해결할 수 있을까?

가장 간단한 방법으로는 `asyncio`를 사용해서 비동기적인 I/O 기능을 할 수 있도록 만들고, 이를 Flask 코드와 연결시키면 된다.

눈치 챈 사람들도 있겠지만, 파이썬에서 함수를 수정하지 않고도 유연하게 함수에 특정 동작을 추가하거나 작동 방식을 바꿀 수 있는 가장 간단한 방법으로는 `decorator`를 사용하는 것이다.

decorator란?

데코레이터는 함수를 수정하지 않은 상태에서 추가 기능을 구현할 때 사용한다.

데코레이터를 사용하면 타겟이 된 함수 자체를 매개변수로 삼아서 데코레이터 함수로 전달한다. 데코레이터 함수는 미리 정의된 코드로 매개변수로 주어진 함수를 wrapping한다.

이러한 기능이 가능한 이유는 파이썬 언어가 함수를 객체로 고려할 수 있는 First-Order Language의 특성을 가짐과 동시에, 파이썬에서 Closure(클로져: 함수를 다른 함수 내부에 선언하는 기능) 기능을 제공하기 때문이다.

우선 First-Order Language의 예제를 보도록 하자.

# First-Order Language이므로 함수를 객체나 변수로 사용할 수 있다.
def test():
print('hi')

a = test
a()


위의 코드를 실행하면 `hi`라는 메세지가 출력된다. 함수를 변수처럼 사용할 수 있기 때문에 변수 a가 함수 test를 가리키도록 만든 뒤 a를 호출하였기 때문에 `test()`와 같은 결과를 얻게 되는 것이다.

마찬가지로 아래 코드는 클로져에 대한 예제이다.

def outerFunction():
msg = 'hello from inner'
def innerFunction():
     print(msg)
    print('hello from outer')
    innerFunction()

outerFunction()

위의 코드를 실행시키면 `hello from outer`가 출력되고 `hello from inner` 메시지가 출력된다. 여기에서 innerFunction이 outerFunction 내의 변수에 접근 가능하도록 해주는 것이 바로 클로져 함수의 특징이다.

이제 간단한 데코레이터 예제 코드를 소개하도록 하겠다.

def decorator1(func):
    def wrapper():
        print('decorator1')
        func()
    return wrapper

def decorator2(func):
    def wrapper():
        print('decorator2')
        func()
    return wrapper

@decorator1
@decorator2
def hello():
    print('hello')

hello()

위의 예제를 실행하면 `decorator1`이 먼저 출력되고, `decorator2`가 출력된 후에 `hello`가 출력된다. 그 이유는 hello 함수를 decorator2가 먼저 wrapping하고, 그 함수를 decorator1 함수가 wrapping하기 때문이다.

위의 코드에서 각 decorator 함수 내에는 wrapper라는 이름의 함수가 있는데, 이 함수는 각 decorator 함수의 매개변수로 주어진 `func`라는 객체에 접근해서 적절한 시점에 이를 호출한다. 또한, 파이썬 인터프리터에 의해 타겟 함수는 자동으로 데코레이터 함수에게 매개변수로서 주어지게 된다.

즉, 파이썬 데코레이터 패턴은 First-Order Language의 특징과 Closure 함수의 특징을 모두 활용한 패턴이라고 할 수 있다.

 

asyncio를 활용한 비동기 데코레이터 만들기

이제 본 글의 주 목적인 "Flask에서 non-blocking I/O" 기능을 사용할 수 있도록 해주는 데코레이터 함수를 구현하도록 하자.

from functools import wraps
from flask import Flask
import asyncio

def async_action(f):
    @wraps(f)
    def wrapped(*args, **kwargs):
        return asyncio.run(f(*args, **kwargs))
    return wrapped

app = Flask(__name__)

@app.route('/')
@async_action
async def index():
    await asyncio.sleep(2)
    return 'Hello world !'

app.run()


위의 코드에서 async_action 함수는 wrapped 라는 함수를 포함하고 있으며, wrapped 함수에는 `@wraps(f)`라는 형태로 데코레이터가 추가되어 있는 것을 볼 수가 있을 것이다. 이는 `functools.wraps`를 활용한 것인데, `functools.wraps`는 커스텀 데코레이터를 구현할 때 자주 사용되는 기능으로 wrapping되는 타겟 함수의 매개변수들이나 keyword args 등까지 모두 전해줄 수 있도록 도와주기에 조금 더 고급 데코레이터 함수를 구현하는데 사용된다.

여기에서는 `asyncio.run` 함수를 통해서 비동기 coroutine을 생성하고, 이 coroutine 위에서 주어진 타겟 함수가 비동기적으로 실행될 수 있도록 도와준다.

위의 `async_action` 데코레이터를 적절히 활용하면 Flask 어플리케이션에 비동기 I/O 기능을 추가함으로서 성능 튜닝을 할 수 있을 것이다.

그러나 알다시피, coroutine은 결국 실제 스레드가 아닌 pseudo-thread이기 때문에 완전한 동시성을 보장하지는 않으며, GIL(Global Interpreter Lock)의 영향을 받을 수 밖에 없다.
따라서, 이러한 기능을 적용할 때에는 늘 충분한 테스트를 통해서 어느 정도의 성능 개선이 이루어지고 어떤 병목이 발생할 수 있는지 등을 충분히 확인해야 한다.

 

반응형