FastAPI

본 글은 기존 Notion에서 이전된 글입니다.


I. 서론

1. FastAPI 정의

현대적이고, 빠르며(고성능), 파이썬 표준 타입 힌트에 기초한 Python3.6+의 API를 빌드하기 위한 웹 프레임워크

2. 주요 특징

  • 빠름 : (Starlette과 Pydantic 덕분에) NodeJS 및 Go와 대등할 정도로 매우 높은 성능을 가지고 있으며, 사용 가능한 가장 빠른 파이썬 프레임워크 중 하나
  • 빠른 코드 작성 : 약 200% ~ 300%까지 기능 개발 속도 증가
  • 적은 버그 : 개발자에 의한 에러 약 40% 감소
  • 직관적 : 훌륭한 편집기 지원, 모든 곳에서 자동완성, 적은 디버깅 시간
  • 쉬움 : 쉽게 사용하고 배우도록 설계됨. 문서를 읽는데 적은 시간이 듦
  • 짧음 : 코드 중복 최소화, 각 매개변수 선언의 여러 기능, 적은 버그
  • 견고함 : 자동 대화형 문서와 준비된 프로덕션용 코드 사용 가능
  • 표준 기반 : API에 대한 (완전히 호환되는) 개방형 표준 기반. OpenAPI(Swagger) 및 JSON 스키마

3. Reference

FastAPI

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_idint로 선언
  • 올바른 응답 결과
    {"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)을 담을 수 있음
**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 버전 이후부터 가능
  1. Enum 클래스 생성
    1. Enum을 임포트하고 strEnum을 상속하는 서브클래스를 만듦
    2. str을 상속함으로서 API 문서는 값이 string형이어야 하는 것을 알게되고 제대로 렌더링 할 수 있게 됨
  2. 경로 매개변수 선언
    1. 생성한 열거형 클래스(ModelName)을 사용하는 타입 어노테이션으로 경로 매개변수 생성
  3. 파이썬 열거형으로 소스코드 작성
    1. 열거체 ModelName의 열거형 멤버를 비교할 수 있음
  4. 열거형 값 가져오기
    1. model_name.value 혹은 일반적으로 your_enum_member.value를 이용하여 실제값을 가져올 수 있음
    2. ModelName.lenet.value로도 값 "lenet"에 접근할 수 있음
  5. 열거형 멤버 반환
    1. return에서 중첩 JSON 본문(예 : dict) 역시 열거형 멤버를 반환할 수 있음
    2. 클라이언트에 반환하기 전에 해당 값으로 변환됨
  6. 응답 결과

    {
      "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는 매개변수가 경로와 일치해야 함을 알려줌
  • 소스코드

    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

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 비동기

Untitled

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 요청 구성

Untitled

  • 메서드 : 클라이언트가 서버에 요청할 동작
  • 프로토콜 : 사용되는 프로토콜과 버전
  • 헤더 : 클라이언트 자체에 대한 자세한 정보
  • 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 응답 구성

Untitled

  • 프로토콜 : 사용되는 프로토콜과 버전
  • 상태 코드 : 요청에 대한 응답 상태
  • 상태 메시지 : 상태 코드와 함께 전달되는 메시지

6.2. 상태 코드(State Code)

  • HTTP 응답 상태코드는 요청에 대한 응답이 성공적으로 되었는지 알려줌

Untitled

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 공식문서