본문 바로가기

Framework/FastAPI

[FastAPI] ASGI uvicorn Server 비동기 동작원리

Fast API는 기본적으로 ASGI의 한 종류인 uvicorn을 사용하고 있음
이로 인해 비동기적 프로그래밍이 가능한 것인데 어떻게 동작하는지 알아보자

ASGI(Asynchronous Server Gateway Interface)

: Python 웹 애플리케이션을 비동기식으로 처리하기 위한 웹 서버와 애플리케이션 간의 표준 인터페이스 기존 WSGI의 한계를 극복하기 위해 개발된 인터페이스

 

WSGI(Web Server Gateway Interface)

: Python 프로그램과 웹 서버 간의 표준 인터페이스 ASGI와 다르게 동기식으로 동작하여 동시성 처리에 어려움이 있다.

 

Uvicorn

: Python 웹 애플리케이션을 ASGI 서버로 실행할 수 있는 명령 줄 도구

 

 

비동기적 프로그래밍

: 프로그램의 흐름이 순차적이지 않고 여러 작업을 동시에 수행하거나 미래에 발생할 이벤트를 처리하는 특히 입출력 작업이나 네트워크 통신과 같이 시간이 오래 걸리는 작업을 효율적으로 다루는데 유용함

 

- 이벤트 기반: 비동기적 환경에서는 주로 이벤트 기반의 프로그래밍을 사용함 이벤트가 발생하면 해당 이벤트를 처리하는 콜백 함수나 핸들러를 실행하여 필요한 작업을 수행함

 

- 비동기 함수: 비동기 프로그래밍 언어와 라이브러리에서는 비동기 함수나 메서드를 사용하여 작업을 비동기적으로 실행함

 

- 스레드, 프로세스: 비동기 프로그래밍은 다중 스레드 또는 프로세스와 다르게, 단일 스레드 또는 프로세스에서 여러 작업을 동시에 처리함

 

 

이벤트 루프

: 비동기 프로그래밍 환경에서 비동기 함수를 실행하고 관리하는 핵심 메커니즘

 

- 이벤트 대기: 비동기 함수가 호출되면, 해당 함수 내의 비동기 작업(파일 읽기, 네트워크 통신 등)이 완료될 때까지 이벤트 루프틑 이벤트를 대기함

 

- 이벤트 처리: 비동기 작업이 완료되면 이벤트 루프에 해당 결과를 받아들이고 이 결과를 처리하기 위해 해당 비동기 함수의 실행을 재개함

 

- 작업 스케줄링: 여러 비동기 함수와 작업이 동시에 실행되어야 할 때, 이벤트 루프는 작업을 스케줄링하여 어떤 작업을 실행할지 결정함

 

위 과정으로 인해 비동기 함수들은 서로 간섭 없이 비동기적으로 실행이 가능함 동기 함수처럼 블로킹되지 않고 효율적으로 관리할 수 있으며 동시성과 높은 성능을 보장함

 

 

비동기 함수 사용방법

1) 선언

@app.get("/sleep")
async def sleep():
    await asyncio.sleep(1000000)
    return {"success"}

: async를 사용하여 /sleep이라는 엔드포인트를 비동기적으로 선언함

 

2) 동작

from fastapi import FastAPI
import time
import asyncio


app = FastAPI()

@app.get("/")
async def read_root():
    return {"message": "Hello, FastAPI"}

@app.get("/sleep")
async def sleep():
    await asyncio.sleep(1000000)
    return {"success"}

: 다음과 같이 두 개의 비동기 엔드포인트를 생성하고 /sleep이라는 엔드포인트에는 비동기적으로 sleep을 걸어 테스트 진행

 

Fast API에서는 기본적으로 swagger와 연동되어 쉽게 API 호출 테스트가 가능함

 

호출 테스트

위 테스트 결과를 확인해 보면 sleep이라는 API에서 호출이 완료되지 않아서 로딩 중이지만 기본 url의 get요청이 성공한 모습을 볼 수 있다.

 

3) 호출

@app.get("/")
async def read_root():
    user = await get_user()
    print(user)
    return {"message": "Hello, FastAPI"}

async def get_user():
    return {"user_id": "test", "password": "pass"}

async로 선언된 비동기 함수는 await 키워드를 활용하여 호출한다.

 

- 동기 내부에서 비동기 함수를 호출한다면?

@app.get("/test")
def test():
    user = get_user()

async def get_user():
    return {"user_id": "test", "password": "pass"}
fast-api-async/venv/lib/python3.11/site-packages/anyio/_backends/_asyncio.py:807: RuntimeWarning: coroutine 'get_user' was never awaited
  result = context.run(func, *args)
RuntimeWarning: Enable tracemalloc to get the object allocation traceback

: 동기 함수 생성 후 내부에서 비동기 함수를 실행하려 하자 위와 같은 에러가 발생하였다. get_user라는 함수는 비동기 이기 때문에 await 키워드 없이는 사용이 불가하기 때문이다.

 

- 그럼 await을 사용해서 호출하면?

await 오류

: 이 또한 불가하다 await이란 키워드는 async 내부에서만 사용 가능하기 때문이다.

 

- 그럼 동기 함수 내부에서 비동기함수를 실행시킬 방법은 없는 것 인가? -> No

@app.get("/test")
def test():
    user = asyncio.run(get_user())

async def get_user():
    return {"user_id": "test", "password": "pass"}

: asyncio라는 라이브러리를 활용하여 동기 함수 내부에서도 비동기 함수를 호출할 수 있다.

 

- asyncio.run(): 이 함수는 이벤트 루프를 생성해 주고 비동기 함수를 실행해 주는 역할을 한다.