Python FastAPI post 예제와 비동기(Asynchronous) async 함수에 대해서
포스팅 개요
본 포스팅은 Python FastAPI에 대해서 정리하는 FastAPI 포스팅 시리즈입니다.
FastAPI 포스팅은 아래와 같은 순서로 정리할 예정입니다.
- Python FastAPI 시작하기 - FastAPI란? 설치 방법과 기본 예제(FastAPI example) (https://lsjsj92.tistory.com/648)
- FastAPI post 간단 예제와 비동기(Asynchronous) async 함수에 대해서 ( 본 포스팅 )
- Python pydantic이란? Python에서 데이터 검증과 설정을 관리해보자(Feat. FastAPI)
- FastAPI router란? router 사용법과 예제(fastapi router example)
- Pytorch 딥러닝(deep learning) 모델과 FastAPI를 활용한 FastAPI 예제(example)
- Docker와 FastAPI를 활용해 pytorch 딥러닝 모델 배포하기(deploy pytorch model using docker, fastapi)
본 글은 그 중 두 번째 글인 FastAPI post 기본 예제와 비동기(asynchoronous) async 함수에 대해서 간단히 정리해보는 포스팅입니다.
해당 포스팅을 작성하면서 제가 참고한 글과 데이터는 다음과 같습니다.
- https://fastapi.tiangolo.com/
- https://github.com/tiangolo/fastapi
- https://grouplens.org/datasets/movielens/1m/
- https://fastapi.tiangolo.com/async/
- https://stackoverflow.com/questions/56729764/asyncio-sleep-vs-time-sleep
본 글에서 작성하는 모든 코드는 아래 github repository에 올려두었습니다.
포스팅 본문
포스팅 개요에서도 정리하였듯이 본 포스팅은 Python FastAPI post 통신에 대해서 살펴보고 asynchoronous(async, 비동기) 함수에 대해서 예제를 살펴볼 예정입니다.
FastAPI Post 방법에 대해서
지난번 포스팅에서는 FastAPI를 설치하고 get 방법으로 통신하는 것을 살펴보았습니다. 이번 포스팅에서는 Post 방법을 살펴보면서 Get method와의 차별점을 간단하게 살펴보겠습니다.
잠깐, HTTP Get과 Post의 차이는?
여기서 post와 get의 차이점을 간단하게 정리하자면, 다음과 같습니다.
일단, Get과 Post은 HTTP 방법이며 둘 다 브라우저가 서버에 '요청'하는 행위입니다.
Get 방법
- 서버에서 데이터를 받아오기 위해 자주 활용합니다.
- 요청할 때 데이터 전송시 URL 주소 끝에 파라미터 값으로 전송되며 이를 Query String이라고 합니다.
- 예를 들어서, https://lsjsj92.tistory.com/640?name=이수진 과 같이 제공하는 것입니다.
- HTTP Body에 담아서 전송하지 않습니다.
Post 방법
- 리소스를 생성/업데이트 하도록 설계된 방법입니다.
- 필요 데이터를 http body에 전송하므로 url로 데이터가 노출되지 않습니다.
본 포스팅은 Get과 Post 차이에 대해서 작성하는 글이 아니기 때문에 이렇게 간단하게 정리하고 넘어가려고 합니다. HTTP 통신 방법에 대한 자세한 것은 여러 좋은 자료들이 많으니 참고바랍니다!
다시 본문으로 돌아와서 먼저, 지난 포스팅에서 본 FastAPI get 방식을 복습해볼까합니다. 아래 코드는 skip과 limit이라는 값을 받아서 Python list slice를 통해 데이터를 받아오는 코드입니다.
from fastapi import FastAPI
app = FastAPI()
fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]
@app.get("/items/{item_id}")
def read_item(item_id: str, skip: int = 0, limit: int = 10):
return fake_items_db[skip : skip + limit]
skip이라는 값과 limit이라는 값을 받아서 skip부터 skip + limit의 범위에서 데이터를 가져옵니다.
그 결과는 다음 사진과 같습니다.
get 방식으로 받는 파라미터 값이기 때문에 url에다가 127.0.0.1:8000/items/1?skip=0&limit=2와 같이 데이터를 넘겨줍니다.
그리고 그에 해당하는 list slice 값을 return 받아 web 화면에 뿌려지는 것을 확인할 수 있습니다.
지난 포스팅에서 봤던 FastAPI swagger UI인 /docs에서도 확인할 수 있습니다.
해당 Swagger UI docs에서 값을 넘겨줄 수 있고 그 때의 결과도 확인할 수 있습니다.
그럼 Post는 어떻게 구성할 수 있을까요? FastAPI에서 post를 구성하는 방법은 간단합니다.
get으로 구성할 때는 @app.get으로 받아왔었는데요. post 일 때는 @app.post 형식으로 annotation을 구성하면 됩니다.
바로 post를 구성하는 기본 코드를 확인해볼게요
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def home():
return {"Hello": "GET"}
@app.post("/")
def home_post(msg: str):
return {"Hello": "POST", "msg" : msg}
위 코드는 FastAPI에서 get을 구성하는 방법과 post를 구성하는 방법 두 가지가 구현되어 있습니다.
get과 post 둘 다 / 경로로 받아오고 get일 경우 return이 hello, get이며 post는 msg에 받은 값을 활용해 return 해줍니다.
바로 fastapi docs를 통해 결과를 확인해보겠습니다.
swagger ui에서 get과 post 둘 다 나오는 것을 확인할 수 있으실태고 그 중 post를 눌러 값을 전송해보겠습니다.
msg에다가 "이수진입니다"라는 값을 넣어 전송하면 위처럼 결과가 나오게 됩니다. 아직 request body를 제대로 구성하지 않았기 때문에 curl 명령어만 봐도 -d 옵션에 값이 들어가지 않고 url 링크에 값이 들어가는 등 완벽하지는 않습니다.
그럼 아래와 같이 코드를 구성해볼까요?
from fastapi import FastAPI
from pydantic import BaseModel
from pydantic import Field
app = FastAPI()
class DataInput(BaseModel):
name: str
@app.get("/")
def home():
return {"Hello": "GET"}
@app.post("/")
def home_post(data_request: DataInput):
return {"Hello": "POST", "msg" : data_request.name}
위 코드는 Python에서 pydantic을 활용해 input 형태를 구성한 방법입니다.
pydantic에 대한 자세한 설명은 다음 포스팅에서 살펴보겠습니다. 일단, 현재는 해당 상태에서 FastAPI를 실행하면 다음과 같이 결과를 확인할 수 있습니다.
이렇게 구성하면 data에 name:이수진 값이 들어가면서 post 다운 통신을 하는 것을 볼 수 있습니다.
이러한 이유와 더불어 input, output format을 setting 하기 위해 pydantic을 많이 활용합니다. 관련 포스팅은 다음에 작성해두도록 하겠습니다!
또한, post로 열어진 FastAPI 서버에 Python requests를 활용해서 통신할 수도 있습니다.
위처럼 import requests로 requests 라이브러리를 가져와서 통신할 url, 그리고 값을 넣어주면 post 통신된 값이 넘어오는 것을 확인할 수 있습니다.
비동기 asynchoronous def 함수(async def 함수)에 대해서
본 파트에서는 FastAPI에서 자주 등장하는 async def 함수에 대해서 여러가지 테스트해보고 정리해보고자 합니다.
FastAPI async def와 dev의 차이점은? ( async def vs dev )
먼저, FastAPI에서 async def와 그냥 def 차이점에 대해서 정리하고 넘어가도록 하겠습니다.
FastAPI 공식 홈페이지 설명(https://fastapi.tiangolo.com/async/)을 보면 다음과 같이 설명이 쓰여져 있습니다.
" 만약, await을 호출하도록 가이드하는 third party 라이브러리를 사용하는 경우 async def를 사용하고 그렇지 않은 경우에는 def와 같이 정상적으로 사용하세요. 잘 모르겠으면 그냥 일반적인 정의 (def)를 사용하세요. 어떠한 경우에도 FastAPI는 비동기적으로 작동하고 매우 빠릅니다"
말이 어렵지만 결국 def건 async def건 둘 다 사용하면 비동기적으로 작동한다는 말입니다. 사실 파이썬은 비동기 코드를 코루틴으로 async와 await으로 지원하고 있는데 이러한 것들을 지원하기 위해 위처럼 설명하고 있는 것입니다.
아무튼 FastAPI에서의 async def와 def는 위와 같은 차이점과 특징이 있습니다.
자, 그럼 간단한 테스트를 해보면서 async def와 def의 차이점을 살펴볼까요?
먼저 아래와 같은 코드가 있다고 가정하겠습니다.
1. 올바르게 사용하지 않았을 경우
async def some_library(num: int, something:str):
s = 0
for i in range(num):
print(" something.. : ", something, i)
time.sleep(1)
s += 1
return s
app = FastAPI()
@app.post('/')
async def read_results(something:str):
s1 = await some_library(5, something)
return {'data' : 'data', 's1':s1}
위 코드는 FastAPI를 실행하면 somthine 이라는 어떤 값을 받아와서 some_library를 실행합니다. some_library 함수는 async def로 지정되어 있으며 time.sleep으로 1초마다 멈추었다가 다시 for문을 실행합니다.
위 FastAPI를 아래와 같이 실행해줍니다.
uvicorn async_example:app --port 8081 --reload
그럼 위 FastAPI를 호출하는 코드를 아래와 같이 구성해보겠습니다.
import requests
import random
url = "http://127.0.0.1:8081/"
params={'something':f'{random.randint(0, 100)}'}
res = requests.post(url, params=params)
print("status : ", res.status_code)
print(res.json())
여기서 something 값은 random.ranint 값으로 가져옵니다. 그럼 위 코드를 실행해볼까요??
위 이미지는 FastAPI 서버만 실행한 상태이고 아직 호출하는 파일을 실행하지 않은 상태입니다.
이제 아래 사진과 같이 req.py라는 파일을 동시에 실행해보겠습니다.
위 사진은 req.py를 동시에 2개를 실행시킨 결과입니다.
그 결과를 한 번 살펴보면
저는 왼쪽 commend에 있는 것을 먼저 실행했습니다. 그러니 왼쪽이 먼저 끝나게 됩니다.
왼쪽 것이 먼저 끝나고 이제 오른쪽 commend 가 실행됩니다. 즉, 순서대로 요청이 처리되고 이에 따라 총 10초에 시간이 걸리게 되는 것이죠. 이것이 바로 async-await을 잘못 사용했을 때의 경우입니다. time.sleep은 이런식으로 비동기적으로 처리할 수 없기 때문입니다. 관련 링크는 포스팅 최상단 참고 링크 중 stack overflow 링크를 참고해주세요
2. 올바르게 사용했을 경우
그럼 아래와 같이 코드를 구성하면 어떻게 되는지 살펴봅니다.
async def some_library(num: int, something:str):
s = 0
for i in range(num):
print(" something.. : ", something, i)
await asyncio.sleep(1)
s += int(something)
return s
app = FastAPI()
@app.post('/')
async def read_results(something:str):
s1 = await some_library(5, something)
return {'data' : 'data', 's1':s1}
이번에는 time.sleep을 사용하지 않고 asyncio.sleep을 사용했습니다. asyncio sleep은 time.sleep과 non-block 형식이기 때문에 앞선 방법과 결과가 다르게 나올 것입니다.
위 사진 결과를 보면 왼쪽과 오른쪽 터미널을 동시에 실행 시켰을 때 55를 값으로 받은 실행 프로세스와 4를 값으로 받은 실행 프로세스가 동시에 실행되는 것을 확인할 수 있습니다. 그리고 거의 동시에 종료도 되죠.
물론, 위 내용으로 async def와 normal def에 대한 차이점을 다 확인할 수 없습니다.
이 부분은 파이썬 코루틴에 대해서 공부가 필요하고 때론 헷갈리기까지 하는 내용입니다.
따라서, 위 예시는 참고로만 확인해주시길 바라며 전체를 이해하려하지 마시고 꼭 스스로 다른 다양한 코드를 살펴보면서 공부하시면 좋을 것 같습니다!
마무리
본 포스팅은 FastAPI 시리즈 포스팅 글의 두 번째 글입니다. 이번 글에서는 FastAPI의 post 방법에 대해서 살펴보았으며 간단하게 pydantic에 대해서도 살펴보았습니다. 또한, async def와 def의 차이점에 대해서도 간단한 예제로 살펴보았습니다.
pydantic에 대한 좀 더 자세한 내용은 다음 포스팅에서 다루도록 하겠습니다.
긴 글 읽어주셔서 감사합니다.