FastAPI
in Web
본 글은 기존 Notion에서 이전된 글입니다.
I. 서론
1. FastAPI 정의
현대적이고, 빠르며(고성능), 파이썬 표준 타입 힌트에 기초한 Python3.6+의 API를 빌드하기 위한 웹 프레임워크
2. 주요 특징
- 빠름 : (Starlette과 Pydantic 덕분에) NodeJS 및 Go와 대등할 정도로 매우 높은 성능을 가지고 있으며, 사용 가능한 가장 빠른 파이썬 프레임워크 중 하나
- 빠른 코드 작성 : 약 200% ~ 300%까지 기능 개발 속도 증가
- 적은 버그 : 개발자에 의한 에러 약 40% 감소
- 직관적 : 훌륭한 편집기 지원, 모든 곳에서 자동완성, 적은 디버깅 시간
- 쉬움 : 쉽게 사용하고 배우도록 설계됨. 문서를 읽는데 적은 시간이 듦
- 짧음 : 코드 중복 최소화, 각 매개변수 선언의 여러 기능, 적은 버그
- 견고함 : 자동 대화형 문서와 준비된 프로덕션용 코드 사용 가능
- 표준 기반 : API에 대한 (완전히 호환되는) 개방형 표준 기반. OpenAPI(Swagger) 및 JSON 스키마
3. Reference
II. 본론
1. 설치 및 실행
1.1. FastAPI 설치
pip install fastapi
1.2. 프로덕션을 위한 Uvicorn 설치
pip install "uvicorn[standard]"
1.3. 서버 실행
uvicorn main:app --reload
- main : 모듈명을 의미
- main.py
- app : FastAPI로부터 생성된 인스턴스를 의미
- app=FastAPI()
- –reload : 소스코드를 수정했을 때 새로고침되도록 해주는 옵션
2. 경로 매개변수
- 파이썬 포맷 문자열에 사용되는 동일한 문법으로 “매개 변수” 혹은 “변수”를 URL로 받아올 수 있음
2.1. 일반 매개변수
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id):
return {"item_id": item_id}
- 경로 매개변수
item_id
의 값은 함수의item_id
인자로 전달됨 - 응답 결과
{"item_id":"foo"}
2.2. 타입이 있는 매개변수
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
**async def read_item(item_id: int):**
return {"item_id": item_id}
- 함수의 매개 변수
item_id
를int
로 선언 - 올바른 응답 결과
{"item_id":3}
**http://127.0.0.1:8000/items/foo
와 같이int
타입의 매개 변수를문자열
로 접근하였을 때 : 에러 발생**{ "detail": [ { "loc": [ "path", "item_id" ], "msg": "value is not a valid integer", "type": "type_error.integer" } ] }
- 경로 매개변수
item_id
는int
가 아닌"foo"
값이기 때문 int
대신float
의 경우에도 동일한 오류 발생- 파이썬 타입 선언을 통해 FastAPI는 데이터 검증을 할 수 있음
- 경로 매개변수
2.3. 매개 변수의 순서
from fastapi import FastAPI
app = FastAPI()
@app.get("/users/me")
async def read_user_me():
return {"user_id": "the current user"}
@app.get("/users/{user_id}")
async def read_user(user_id: str):
return {"user_id": user_id}
- 경로 동작은 순차적으로 동작하기 때문에
/users/{user_id}
이전에/users/me
를 먼저 선언해야 함- 그렇지 않다면
/users/{user_id}
는 매개변수user_id
의 값을"me"
라고 “생각하여”/users/{user_id}
로 동작함
- 그렇지 않다면
2.4. Enum
- 경로 매개변수를 받는 경로 동작이 있지만, 유효하고 미리 정의할 수 있는 경로 매개변수 값을 원할 때 파이썬 표준
Enum
을 사용할 수 있음 - Enum이란?
- enumerated type의 줄임말. 열거형이라고도 함
- 요소, 멤버라 불리는 명명된 값의 집합을 이루는 자료형
- 일반적으로 해당 언어의 상수 역할을 하는 식별자
- 언어에 따라 기본적으로 포함된 경우도 있음
- Enum의 장점
- IDE의 지원을 받을 수 있음
- 자동완성, 오타검증, 텍스트 리팩토리 등
- 허용 가능한 값들을 제한할 수 있음
- 리팩토링 시 변경 범위가 최소화 됨
- 내용을 추가해도 Enum 코드만 수정하면 됨
- 확실한 부분과 불확실한 부분을 분리할 수 있음
- 문맥(Context)을 담을 수 있음
- IDE의 지원을 받을 수 있음
**from enum import Enum**
from fastapi import FastAPI
**class ModelName(str, Enum):
alexnet = "alexnet"
resnet = "resnet"
lenet = "lenet"**
app = FastAPI()
@app.get("/models/{model_name}")
**async def get_model(model_name: ModelName):
if model_name is ModelName.alexnet:
return {"model_name": model_name, "message": "Deep Learning FTW!"}**
**if model_name.value == "lenet":
return {"model_name": model_name, "message": "LeCNN all the images"}**
**return {"model_name": model_name, "message": "Have some residuals"}**
- 파이썬 3.4 버전 이후부터 가능
- Enum 클래스 생성
Enum
을 임포트하고str
과Enum
을 상속하는 서브클래스를 만듦str
을 상속함으로서 API 문서는 값이string
형이어야 하는 것을 알게되고 제대로 렌더링 할 수 있게 됨
- 경로 매개변수 선언
- 생성한 열거형 클래스(
ModelName
)을 사용하는 타입 어노테이션으로 경로 매개변수 생성
- 생성한 열거형 클래스(
- 파이썬 열거형으로 소스코드 작성
- 열거체
ModelName
의 열거형 멤버를 비교할 수 있음
- 열거체
- 열거형 값 가져오기
model_name.value
혹은 일반적으로your_enum_member.value
를 이용하여 실제값을 가져올 수 있음ModelName.lenet.value
로도 값"lenet"
에 접근할 수 있음
- 열거형 멤버 반환
- return에서 중첩 JSON 본문(예 :
dict
) 역시 열거형 멤버를 반환할 수 있음 - 클라이언트에 반환하기 전에 해당 값으로 변환됨
- return에서 중첩 JSON 본문(예 :
응답 결과
{ "model_name": "alexnet", "message": "Deep Learning FTW!" }
2.5. 경로를 포함하는 매개변수
/files/{file_path}
가 있는 URL의 경우home/johndoe/myfile.txt
와 같이 path에 들어있는file_path
자체가 필요함- 해당 URL은
/files/home/johndoe/myfile.txt
가 되는데, 이 경우에는 테스트와 정의가 어려운 시나리오로 이어짐 - OpenAPI는 경로를 포함하는 경로 매개변수를 내부에서 선언하는 방법을 지원하지 않음 ⇒ FastAPI에서는 Starlette의 내부 도구 중 하나를 사용하여 구현 가능
- 경로 변환기
- Starlette에서 직접 옵션을 사용하여 아래와 같은 URL을 사용하여 pathㄹ르 포함하는 경로 매개변수를 선언할 수 있음
/files/{file_path:path}
- 이 경우 매개변수의 이름은
file_path
이고 마지막 부분:path
는 매개변수가 경로와 일치해야 함을 알려줌
- Starlette에서 직접 옵션을 사용하여 아래와 같은 URL을 사용하여 pathㄹ르 포함하는 경로 매개변수를 선언할 수 있음
소스코드
from fastapi import FastAPI app = FastAPI() **@app.get("/files/{file_path:path}")** async def read_file(file_path: str): return {"file_path": file_path}
- 매개변수가
/home/johndoe/myfile.txt
를 갖고 있어 슬래시로 시작(/
)해야함 - 이 경우 URL은:
/files//home/johndoe/myfile.txt
이며files
과home
사이에 이중 슬래시(//
)가 생김
- 매개변수가
3. 쿼리 매개변수
경로 매개변수의 일부가 아닌 다른 매개변수를 선언할 때, “쿼리” 매개변수로 자동 해석함
from fastapi import FastAPI app = FastAPI() fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] @app.get("/items/") **async def read_item(skip: int = 0, limit: int = 10):** return fake_items_db[skip : skip + limit]
- 쿼리는 URL에서
?
후에 나오고&
으로 구분되는 키-값 쌍의 집합 - 예제
http://127.0.0.1:8000/items/?skip=0&limit=10
skip
: 값0
을 가짐limit
: 값10
을 가짐
- URL의 일부이므로 기본적으로 문자열이지만 파이썬 타입과 함께 선언할 경우 해당 타입으로 변환되고 이에 대해 검증함
3.1. 기본값
2.3. Pydantic
모든 데이터 검증은 Pydantic에 의해 내부적으로 수행되고 관리받음
-
- @app.get(”/”)
- / : 경로
- get : HTTP의 GET 메서드(post, put, delete 등이 있음)
async : 비동기 함수
- 동기 방식 : 요청과 결과가 동시에 일어나는 방식, 요청을 보낸 후 응답을 받아야 다음 동작이 진행됨
비동기 방식 : 요청과 결과가 동시에 일어나지 않는 방식, 응답의 여부와 상관없이 다음 함수를 실행할 수 있음
# 비동기 함수 @app.get('/') **async** def read_results(): results = **await** some_library() return results # 동기 함수 @app.get('/') def results(): results = some_library() return results
2.2. 쿼리
from fastapi import FastAPI
app = FastAPI()
fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]
@app.get("/items/")
async def read_item(**skip: int = 0, limit: int = 10**):
return fake_items_db[**skip : skip + limit**]
- URL에서
?
후에 나오고&
로 구분되는 키-값 쌍의 집합- ex)
http://127.0.0.1:8000/items/?skip=0&limit=10
- ex)
2.3. 선택적 매개변수
from typing import Union
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id: str, q: Union[str, None] = None):
if q:
return {"item_id": item_id, "q": q}
return {"item_id": item_id}
- 기본값을 None으로 설정하여 선택적 매개변수를 선언할 수 있음
- 위 코드의 q를 Union을 통해 str 혹은 None으로 선언함으로서 필수값이 아니게 구현
- Union : 여러 개의 타입이 허용될 수 있는 상황에서 typing 모듈의 Union을 사용할 수 있음
- Optional[x] : X 또는 None을 의미함
2.4. 필수 쿼리 매개변수
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
async def read_user_item(item_id: str, needy: str):
item = {"item_id": item_id, "needy": needy}
return item
2.5. 여러 경로/쿼리 매개변수
from typing import Union
from fastapi import FastAPI
app = FastAPI()
@app.get("/users/{user_id}/items/{item_id}")
async def read_user_item(
user_id: int, item_id: str, q: Union[str, None] = None, short: bool = False
):
item = {"item_id": item_id, "owner_id": user_id}
if q:
item.update({"q": q})
if not short:
item.update(
{"description": "This is an amazing item that has a long description"}
)
return item
2.6. Query String VS Path Variable
- Query String
# writer가 nick인 게시글들을 가져옵니다. /board/list?writer=nick
- 일반적으로 리소스들을 정렬, 필터링 혹은 페이징하는 곳에 사용
- Path Variable
# id가 444인 게시글을 가져옵니다. /board/444
- 일반적으로 구체적인 리소스를 식별하는데 사용
3. 동기 vs 비동기
2.3. 동기 방식의 특징
- 장점 : 설계가 간단하고 직관적임
- 단점 : 요청에 대한 결과가 반환되기 전까지 대기해야 함
- 특정 기능을 실행하는 데 5분이 소요된다고 할 때, 5분동안 다른 작업을 수행할 수 없음
2.4. 비동기 방식의 특징
- 장점 : 요청에 대한 결과가 반환되기 전에 다른 작업을 수행할 수 있어서 자원을 효율적으로 사용할 수 있음
- 특정 기능을 실행하는 데 5분이 소요된다고 할 때, 5분동안 다른 작업을 수행할 수 있음
- 단점 : 동기 방식보다 설계가 복잡하고, 논증적임
4. HTTP 메시지
- 서버와 클라이언트 간 데이터를 주고 받는 방식
- HTTP/1.1의 경우 요청과 응답은 Start/Status line, Header, Body로 이루어져 있음
4.1. HTTP 요청(Request)
- 요청은 클라이언트가 특정 데이터를 받아올 수 있게끔 보내는 메시지
4.2. HTTP 요청 구성
- 메서드 : 클라이언트가 서버에 요청할 동작
- 프로토콜 : 사용되는 프로토콜과 버전
- 헤더 : 클라이언트 자체에 대한 자세한 정보
- empty-line : 헤더와 본문을 구별함
5. HTTP 요청 메서드
5.1. GET 메서드
- 특정 리소스를 가져오도록 요청하는 메서드
- 데이터를 가져올 때만 사용
- CRUD 개념으로 생각했을 때 Read에 속함
- URL 뒤에 데이터를 붙여 보냄
www.example.com/upper
5.2. POST 메서드
- 서버로 리소스를 제출하는 메서드
- 서버 상태의 변화를 일으킴
- 주로 새로운 리소스 생성(Create) 할 때 사용
- URL에 붙여 쓰는 방식이 아닌 Body에다 리소스를 넣어서 보냄
# URL
www.example.com
# Body
{
"name" : "jinwoo",
"age" : 26,
"school" : "A"
}
5.3. PUT 메서드
- POST와 유사하지만 연속적인 요청 시에도 같은 효과를 가져옴(멱등성, 멱등법칙)
- 기존 데이터를 교체(Update)하는 용도로 쓰임
5.4. DELETE 메서드
- 지정한 리소스를 삭제(delete) 요청 할 때 사용
6. HTTP 응답
- HTTP 요청에 대한 서버의 답변
6.1. HTTP 응답 구성
- 프로토콜 : 사용되는 프로토콜과 버전
- 상태 코드 : 요청에 대한 응답 상태
- 상태 메시지 : 상태 코드와 함께 전달되는 메시지
6.2. 상태 코드(State Code)
- HTTP 응답 상태코드는 요청에 대한 응답이 성공적으로 되었는지 알려줌
6.3. Content-Type
- 응답 안에 있는 Content-Type
- 클라이언트 안에 전달되는 데이터 유형을 알려줌
7. Pydantic
- 타입 애너테이션을 사용해서 데이터를 검증하고 설정들을 관리하는 라이브러리
- 입출력 항목의 갯수와 타입을 설정
- 입출력 항목의 필수값 체크
- 입출력 항목의 데이터 검증
- FastAPI 내부에서 Pydantic이 사용되어 있음
7.1. Pydantic 스키마 작성
import datetime
from pydantic import BaseModel
**class Question(BaseModel):**
id: int
subject: str
content: str
create_date: datetime.datetime
- BaseModel을 상속한 Questsion 클래스 작성
- pydantic의 BaseModel을 상속한 Question 클래스를 Question 스키마라고 칭함
- 총 4개의 출력항목을 정의하고 그 타입을 지정하였음
- 정해진 타입이 아닌 다른 타입의 자료형이 대입되면 오류가 발생
- 4개의 항목에는 디폴트 값이 없기 때문에 필수항목임을 나타냄
- 디폴트 값 설정
subject: str | None = None
- 디폴트 값 설정
7.2. 라우터에 Pydantic 적용
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from database import get_db
**from domain.question import question_schema**
from models import Question
router = APIRouter(
prefix="/api/question",
)
@router.get("/list", **response_model=list[question_schema.Question]**)
def question_list(db: Session = Depends(get_db)):
_question_list = db.query(Question).order_by(Question.create_date.desc()).all()
return _question_list
response_model=list[question_schema.Question]
의 의미는 question_list 함수의 리턴값은 Question 스키마로 구성된 리스트임을 의미- 하지만
_question_list
의 요소값이 딕셔너리가 아닌 Querstion 모델이기 대문에 Question 스키마로 자동변환되지 않음
- 하지만
따라서 아래와 같이 Question 스키마에 다음처럼 orm_mode 항목을 True로 설정하여 자동으로 Question 모델의 항목들이 Question 스키마로 매핑되도록 함
import datetime from pydantic import BaseModel class Question(BaseModel): id: int subject: str content: str create_date: datetime.datetime **class Config: orm_mode = True**
- Question 스키마에서 출력항목이 수정 및 제거될 경우 라우터의 question_list 메서드에서도 해당 항목이 제거되므로 실제 리턴되는 questionlist를 수정할 필요가 없어짐 ⇒ 스키마만 제거하면 되니 편리성이 증가함
Reference
- FastAPI 공식문서