지난 글을 통해 fastapi에서도 pymysql을 이용해서 DB와 통신이 가능하다는걸 확인했습니다.
이어서 CRUD를 구현해야 하는데요,
fastapi에서 파라미터를 다루는 것에 대해 정리된 곳이 많지 않아 한 번 정리가 필요할 것 같습니다.
기존에 다루던 방법
Fastapi는 구현 철학에 따라, 대충 만들어도 '그냥 작동되는' 구조를 띄고 있습니다.
하지만 더욱 안정적으로 구성하고, 추후 문서화 작업에서 이해하기 쉽도록 하기 위해선 더 좋은 방법이 있습니다.
1. path(type)
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
def read_item(item_id):
return {"item_id": item_id}
위처럼 매우 단순하게 path 파라미터로써 item_id값을 받고 그대로 사용한다고 정의해도, 큰 문제 없이 동작합니다.
다만 위 경우 입력되는 값이 어느 타입인지 지정되어 있지 않아 추후에 문제가 될 여지가 큽니다.
(예를 들어, 반드시 int형 자료가 들어와야 하는데 문자열이 들어올 수도 있습니다.)
또한, 아무런 타입이 지정되어 있지 않으므로 {"item_id" : "foo"}와 같이 반드시 문자형으로 반환되게 됩니다.
만약 item_id가 101이여서 정수 101이 반환되길 기대했어도, 의도되지 않은 값인 "101"이 반환되게 되겠죠?
따라서 최소한 type에 대한 지정이 필요합니다.
@app.get("/items/{item_id}")
def read_item(item_id : int):
return {"item_id": item_id}
위와 같이 type을 지정해주게 되면 아래의 두 장점이 생기게 됩니다.
1. IDE(Vscode 등)에서 type이 지정된 것을 파악하여 디버깅 작업이 더욱 수월하게 됩니다.
2. int로써 데이터를 받는다고 선언하게 되어 fastapi 내부적으로 입력된 값을 int형으로 변환하게 됩니다. 따라서 반환값 역시 int형이 됩니다.
따라서 http://127.0.0.1:8000/items/100은 {"item_id":100}을 반환하게 됩니다. "100"이 아니라 100이 된걸 볼 수 있습니다.
또한 2번에 이어지는 장점으로써, 형 변환이 불가능한 데이터(예를 들어, 위 item_id에 "foo"같은 숫자로 변환 불가능한 문자열)가 입력된 경우 내부적으로 오류처리를 내 줍니다.
{
"detail": [
{
"loc": ["path", "item_id"],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
]
}
위 예시는 http://127.0.0.1:8000/items/foo에 접근하게 되었을 때에 나오는 오류입니다.
위와 같이, 오류 메세지에 원인을 명확하게 알려주는 것을 알 수 있습니다. 디버깅에 유용하겠죠?
2. query(required vs optional)
query 파라미터에 대해서도 위의 내용은 동일하게 적용됩니다.
다만 반드시 어떠한 값이 요구되는 path 파라미터와 달리 query 파라미터는 반드시 요구되지 않을 수 있습니다.
따라서 각 파라미터가 required인지, optional인지 명시해야 의도대로 동작됩니다.
fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]
@app.get("/items/")
def read_item(skip: int, limit: int):
return fake_items_db[skip : skip + limit]
위 코드의 경우 skip과 limit 파라미터는 반드시 제공되어야 하므로, 각 변수의 type을 지정해주기만 하였습니다.
http://127.0.0.1:8000/items?skip=0&limit=2는 [{"item_name": "Foo"}, {"item_name": "Bar"}]를 반환하고,
http://127.0.0.1:8000/items?skip=1 등은 에러를 내게 됩니다.
그렇다면 어떻게 optional한 파라미터를 표현할 수 있을까요?
@app.get("/items/{item_id}")
def read_item(item_id: str, q: str = None):
if q != None:
return {"item_id": item_id, "q": q}
else:
return {"item_id": item_id}
위의 q와 같이 변수의 기본 값을 지정해주면, optional한 값을 지정해줄 수 있습니다.
http://127.0.0.1:8000/items/foo?q=bar는 {"item_id":"foo", "q":"bar"}를 반환하고,
http://127.0.0.1:8000/items/foo는 {"item_id":"foo"}를 반환하게 됩니다.
q 파라미터의 값이 주어지지 않아도 에러 없이 올바르게 처리되는 것을 볼 수 있습니다.
다만, 파이썬의 문법에 의해 아래와 같은 기술은 금지됩니다.
기본 값을 갖는 모든 파라미터는, 반드시 입력되어야 하는 파라미터 뒤에 배치되어야 합니다.
따라서 위에서 optional한 파라미터인 q는 반드시 item_id 뒤에 위치해야 함에 유의하세요.
좀 더 올바르게 다루기
위의 두 가지만 유의해도 기초적인 구현은 가능합니다.
하지만 fastapi의 가장 큰 장점인 '자동 api 문서 생성'을 제대로 활용하기 위해선 조금 더 올바르게 다룰 필요가 있습니다.
위의 사진은 fastapi가 자동 생성한 swagger api 문서 페이지의 내용입니다.
위와 같이 해당 값이
어떤 파라미터인지(path, query 등), 어떤 type(string, int 등)인지, 설명은 어떻게 되는지, 예시 값은 어떻게 되는지
표시해주기 위해선 어떻게 해야 할까요?
파라미터를 받는 쪽에서 조금의 추가 처리가 있으면 가능합니다.
from fastapi import FastAPI
from fastapi import Path, Query, Body, File
from typing import Optional
app = FastAPI()
@app.get("/cctvInfo")
def get_cctvInfo(
cctv_id: Optional[str] = Query(None, description="CCTV의 ID, Form : [CCTV의 MAC주소]_[IndexNum]", example="F02F74235F38_01"),
):
위의 import하는 부분을 보시면 두 줄이 추가된 것을 알 수 있습니다.
1. Path, Query, Body, File
Query(None, description="CCTV의 ID, Form : [CCTV의 MAC주소]_[IndexNum]", example="F02F74235F38_01")
각각 해당 파라미터가 어떤 형태로 입력되었는지를 알려줍니다.
위의 경우 Query string으로 입력되기를 기대하였으므로 Query를 사용하였습니다.
또, Query() 안에 첫 파라미터값을 None을 넣은 것을 알 수 있는데요,
이는 default 값으로써, 해당 파라미터가 주어지지 않은 경우 None으로 다루기 위해 넣어주었습니다.
또한 description과 example을 제공하여 더욱 알아보기 쉽도록 할 수 있고, 해당 내용은 자동 생성된 api 문서에도 포함됩니다.
2. Optional
해당 파라미터가 optional한지 알려줍니다.
Optional[type]
사용법은 위처럼 Optional[]의 대괄호 안에 해당 파라미터의 type을 작성해주면 됩니다.
위의 경우 string 타입을 기대하므로, Optional[str]과 같이 기술하였습니다.
Optional이 사용되지 않은 파라미터는 required로 인식됩니다.
활용
위의 내용을 활용하면 아래와 같은 사용도 가능합니다.
1. required한 path 파라미터를 string 형태로 받기
@app.get("/download_image/{cctv_id}")
def download_image(
cctv_id: str = Path(description="CCTV의 ID, Form : [CCTV의 MAC주소]_[IndexNum]", example="F02F74235F38_01")
):
위의 Path() 내부를 보시면 Query()때와 달리 None등이 없는 것을 볼 수 있는데요,
path 파라미터는 반드시 required이므로 기본 값을 지정해주지 않아도 됩니다.
2. required, optional한 query 파라미터를 string 형태로 받기
@app.get("/cctvInfo/update")
def update_cctvInfo(
cctv_id: str = Query(description="CCTV의 ID, Form : [CCTV의 MAC주소]_[IndexNum]", example="F02F74235F38_01"),
location: Optional[str] = Query(None, description="CCTV의 위치", example="hana_lounge_1")
):
3. Path, Query섞어서 사용
@app.get("/cctvInfo/create/{cctv_id}")
def create_cctvInfo(
cctv_id: str = Path(description="CCTV의 ID, Form : [CCTV의 MAC주소]_[IndexNum]", example="F02F74235F38_01"),
location: Optional[str] = Query(None, description="CCTV의 위치", example="hana_lounge_1")
):
물론 위처럼 섞어 쓰는것도 가능합니다.
4. 파일 업로드
from fastapi import FastAPI, UploadFile
from fastapi import Path, Query, Body, File
from typing import Optional
app = FastAPI()
@app.post("/upload_image/{cctv_id}")
def upload_image(
cctv_id: str = Path(description="CCTV의 ID, Form : [CCTV의 MAC주소]_[IndexNum]", example="F02F74235F38_01"),
img_file: UploadFile = File(..., description="JPEG 이미지파일, Form : IMG_NAME.jpg", example="./cctv_image")
):
UploadFile type을 이용하면 파일 업로드도 처리 가능합니다!
장점은?
그냥 되는걸 왜 굳이 코드 길어지게 이러냐고 그럴 수도 있습니다.
하지만 위와 같이 작성하게 되면, 코드의 직관성도 더 높아지고, api 문서에 자동으로 설명이 들어가므로 협업할 때 도움이 됩니다.
사실 fastapi는 위 단계에서 한 단계 더 나아가 아예 Class를 생성해서 다루는 것을 권장합니다...만!
거기까지 나아가면 class까지 다뤄야하니 여기까지만 해도 최소한은 되는 것 같습니다. 기회가 된다면 class로 다루는 것은 다음번에 다뤄보도록 하겠습니다.
개발에 도움이 되셨기를 빕니다!
'개발팁' 카테고리의 다른 글
Nginx와 Docker Compose로 무중단 배포하기 (0) | 2023.08.20 |
---|---|
Certbot HTTPS용 SSL 인증서 발급 / Nginx로 적용하기 (2) | 2023.08.15 |
스프링 부트 CORS 설정법 정리 (0) | 2023.08.12 |
Spring Boot + Redis Cache 사용법 정리 (0) | 2023.08.11 |
[Fastapi]sqlalchemy말고, pymysql로 데이터베이스(Mysql)와 통신하기 (4) | 2023.02.23 |