[FastAPI] FastAPI 란?


FastAPI 는 Online Serving 을 직접 구현할 때 사용하는 대표적인 Python 웹 프레임워크다. 굉장히 빠르게 업데이트 되고 있고, 최근에는 아래 그림과 같이 FastAPI 혹은 Django 중 두 가지 프레임워크로 많이 개발하게 된다.

Untitled

이번 포스트에서는 FastAPI 와 활용을 위해 필요한 개념들을 알아보자.

FastAPI

  • FastAPI 는 High Performance, Easy, Productivity 의 특징을 가진다. 자세한 것은 아래에서 확인하자.

    Untitled

  • FastAPI 에서는 API 명세 등을 Swagger 가 자동 생성해준다. 또한 Pydantic 을 이용해서 Config 파일 관리 및 data 에 대한 validation check 도 가능하다. 이에 대한 것들은 추후 자세하게 정리할 예정이다.
  • FastAPI 에 익숙해지기 위해서는 웹 서버를 직접 띄우고 점진적으로 기능을 추가해보는 것이 좋다.
  • 먼저 FastAPI 의 기본 기능에 익숙해지자. FastAPI 제공 기능들은 블록(컴포넌트)처럼 조합해서 사용할 수 있다. 즉 여러 블럭의 사용방법 익히고, 이들을 재조합해서 웹 서버를 띄워보자.
  • 특정 기능을 구현하기 위해 어떻게 설계해야 하는지 생각하고 구현하면서, 만들어 둔 모델들을 FastAPI 서버에 띄우면 웹 애플리케이션의 기초가 마련된다.

FastAPI 특징

  • Fast: 고성능 ASGI Server 인 uvicorn(uvloop 를 사용하는 경우) 덕분에 NodeJS 및 Go 와 동등한 매우 높은 성능을 제공한다. Starlette, Pydantic 을 통한 개발 편의성이 좋고, 가장 빠른 Python 프레임워크 중 하나다.
  • Fast to code: 기능 개발 속도를 약 200% 에서 300% 높인다.
  • Fewer bugs: 약 40%의 개발자가 만들 수 있는 버그를 줄인다.
  • Intuitive: auto-completion 에 대한 지원이 좋다. 이를 통해 디버깅하는 시간이 줄어든다.
  • Easy: 사용 및 학습하기 쉽도록 설계되었다. 이를 통해 문서를 읽는 시간이 줄어든다.
  • Short: 코드 중복을 최소화한다. 각 매개변수 선언에서 여러 기능을 제공하고, 버그가 적다.
  • Robust: 프로덕션에 적합한 코드를 얻을 수 있다. 자동 대화형 문서화가 가능하다.
  • Standard-based: 이전에는 Swagger 로 알려진, API 에 대한 오픈 표준인 OpenAPI 및 JSON Schema 를 기반으로 한다.

FastAPI 구성요소

  • FastAPI 는 Starlette 라고 하는 ASGI Python Web Framework 를 래핑하여 만들어졌다.
  • uvicorn 이라는 ASGI Server 를 통해 서버가 구동된다.
  • FastAPI 에서 입출력에 대한 데이터 검증은 Pydantic 라이브러리를 사용하여 검증할 수 있다.
    • Request Body / Response Body 를 Pydantic BaseModel 을 상속받아 구현할 수 있다.
    • 기존 1.x 버전에서는 속도가 느려 성능 이슈가 있었지만, 2.x 버전에서는 내부 로직을 rust 로 대체하여 성능이 개선 되었다.
    • 1.x 대비 빨라졌다고는 하나 가이드대로 사용하지 않으면 여전히 성능이 안좋을 수 있다.
    • Pydantic 은 중첩된(nested) BaseModel 에 대해 Json Encoding 성능이 매우 안좋은 이슈가 있다.
    • https://sawaca96.tistory.com/14에서 관련 내용을 자세히 알아볼 수 있다.
    • 따라서 Response Model 에만 Pydantic Class 로 정의해주고 실제 반환에서는 ORJSONResponse 클래스를 사용해서 반환하는 것이 성능 상 유리하다.

프로젝트 구조

  • FastAPI 를 사용하거나, 다른 도구로 웹 애플리케이션을 개발하게 될 때 범용적인 프로젝트 구조가 있다.
  • 먼저 프로젝트 코드가 들어갈 모듈을 설정한다. 일반적으로 /app 이 되며, 대안으로 프로젝트 이름이나 /src 등이 가능하다.
  • __main__.py 는 간단하게 애플리케이션을 실행할 수 있는 entrypoint 역할을 한다. entrypoint 란 프로그래밍 언어에서 최상위 코드가 실행되는 시작점 또는 프로그램 진입점이다.
  • main.py 또는 app.py 에는 FastAPI 의 애플리케이션과 Router 를 설정한다. 점점 규모가 커질수록 다른 파일에 다른 설정들이 있을 수 있다.
  • model.py 는 ML model 에 대한 클래스와 함수를 정의한다.
  • 처음 FastAPI 를 다룰 때 컴포넌트가 복잡하면 어렵다. 따라서 간단하게 시작하고, 개발을 하면서 구조가 점점 다양하게 만드는 것이 좋다.
  • 아래는 iris classification 예시에 대한 일반적인 프로젝트 구조를 나타낸 것이다.

    ml_app/                            # 프로젝트 루트
    ├── app/                           # FastAPI 애플리케이션 코드
    │   ├── __init__.py
    │   ├── main.py                    # FastAPI 진입점 (uvicorn 실행 대상)
    │   ├── api/                       # API 라우터 구성 (endpoint 분리)
    │   │   ├── __init__.py
    │   │   └── v1/                    # API 버전 1
    │   │       ├── __init__.py
    │   │       ├── endpoint1.py       # /predict 등의 엔드포인트 정의
    │   │       └── endpoint2.py       # /predict 등의 엔드포인트 정의
    │   ├── models/                    # ML 모델 로딩/초기화 코드
    │   │   ├── __init__.py
    │   │   └── model.py               # 모델을 불러오고, 초기화하거나, 추론에 사용할 객체를 관리
    │   ├── schemas/                   # Pydantic 기반 요청/응답 스키마
    │   │   ├── __init__.py
    │   │   └── iris.py                # 예측용 입력/출력 데이터 구조
    │   ├── services/                  # 비즈니스 로직 (예: 추론 함수)
    │   │   ├── __init__.py
    │   │   └── iris_service.py        # 모델 예측 로직 정의
    │   └── core/                      # 설정, 공통 로직
    │       ├── __init__.py
    │       ├── config.py              # 환경 변수 및 설정값 관리
    │       └── utils.py               # 범용 유틸 함수
    ├── tests/                         # 테스트 코드
    │   ├── __init__.py
    │   └── test_endpoints.py          # API 엔드포인트 테스트
    ├── .env                           # 환경 변수 파일
    ├── pyproject.toml                 # Poetry 의존성 및 메타 설정
    ├── poetry.lock                    # 패키지 버전 잠금 파일 (자동 생성)
    ├── README.md                      # 프로젝트 설명 문서
    └── scripts/                       # 개발/운영용 스크립트 (선택사항)
        └── train_model.py             # ML 모델 학습 및 저장 스크립트
    

Poetry

  • Poetry 는 pip 를 대체하는 패키지 매니저다. Dependency Resolver 로 복잡한 의존성들의 버전 충돌을 방지한다.
  • 일반적으로 프로젝트에 사용한 패키지들은 requirements.txt 로 많이 관리하는데, 이를 다른 환경에 설치할 때 파일 내부에 라이브러리끼리 충돌이 날 수 있다.
    • 라이브러리 사이에 의존성이 있다면, 순차적으로 install 하기 때문에 앞에서 깔린 것의 버전을 바꿀 수 있다.
  • Poetry 에서 Virtualenv 를 생성해서 격리된 환경에서 빠르게 개발이 가능해진다.
  • 또한 기존 Python 패키지 관리 도구에서 지원하지 않는 build, publish 가 가능하다.
    • build 는 소스를 배포 가능한 형태(wheel, tarball)로 빌드한다.
    • publish 는 PyPI 에 배포할 수 있다.
  • pyproject.toml 을 기준으로 여러 툴들의 config 를 명시적으로 관리한다. pyproject.toml 은 프로젝트의 메타정보와 라이브러리 의존성을 관리해주는 파일이다.
  • Poetry 사용법은 아래와 같다.
    • 프로젝트 init : 해당 명령어를 CLI 에 입력한 후 프로젝트의 메타정보들을 입력하면 pyproject.toml 이 생성된다. 개발 환경마다 필요한 패키지를 분리할 수 있다.
    • poetry shell : pyproject.toml 에 기반한 가상환경을 활성화한다.
    • poetry install : 현재 프로젝트의 pyproject.toml 파일을 읽어서 의존성 패키지를 설치해준다. poetry.lock 파일이 없으면 만들어 주고, 있으면 해당 파일을 사용한다.
    • poetry add : 패키지 설정을 pyproject.toml 에 추가하고 설치한다.
    • poetry.lock : pyproject.toml 에 설정된 의존성들에 대한 lock 파일을 생성한다. 단, 설치하는 것은 아니다. 이 파일이 존재하면 작성하고 있는 프로젝트 의존성과 동일한 의존성을 가질 수 있다. 따라서 Github 레포에 꼭 커밋해주는 것이 좋다.
  • 추가적으로, Poetry 하나만으로 가상환경을 설치하고 사용할 수 있지만, 명시적인 Python 버전을 위해 pyenv 와 Poetry 를 조합할 수 있다. 이렇게 되면 여러 프로젝트에서 각기 다른 Python 버전을 사용할 수 있다.
  • 또한 여러 언어를 사용하는 프로젝트의 경우, 여러 언어의 버전을 통합 관리할 수 있는 asdf 와 Poetry 를 같이 사용할 수 있다.
  • 이러한 버전 및 패키지 관리에 대해서는 추후에 자세하게 정리해보자.

FastAPI 활용을 위한 개념

  • FastAPI 를 사용하기 위해 알아두어야 할 개념들을 정리해보자.
  • 이는 Online Serving 을 위한 웹 프로그래밍 포스트에서 정리한 내용들이 대부분이지만, FastAPI 를 중점적으로 보자.

API & Endpoint

  • API 가 두 시스템(애플리케이션)이 상호작용할 수 있게 하는 프로토콜의 총집합이라면, Endpoint 는 API 가 서버에서 리소스에 접근할 수 있도록 가능하게 하는 URL 이라 할 수 있다.
  • FastAPI 는 Endpoint 함수를 작성할 때 def 혹은 async def 로 정의할 수 있다.
  • async def 의 경우 비동기 함수를 정의하는 방법으로 Web Framework 로직을 작성할 때 사용하는 일반적인 방법이다.
    • 비동기 함수 내에서 blocking 을 유발하는 함수를 사용하는 경우, 비동기 스레드가 blocking 되어 해당 함수가 완료될 때 까지 다른 함수들을 처리할 수 없는 문제가 발생한다.
    • 따라서 비동기 함수 내에서 일부 blocking 연산을 처리해야하는 경우 starlette 의 run_in_threadpool 함수를 사용하거나 def 로 Endpoint 함수를 정의해야 한다.
    • 비동기 이벤트 스레드 루프에서 실행된다.
  • def의 경우 일반적인 함수를 작성하는 방법으로 blocking 을 유발할 수 있는 동기 함수를 포함하는 경우 사용한다.
    • 백엔드 로직을 작성할 때 기본적으로 비동기 프로그래밍을 해야하지만, 비동기 프로그래밍은 처음 백엔드 코드를 작성하는 개발자에게는 쉽지 않을 수 있다.
    • 따라서 처음 백엔드 코드를 작성하는 경우 def 로 먼저 구현하고, 추후 리팩토링을 통해 개선하는 것을 권장한다.
    • blocking 이란 대표적으로 I/O 작업을 말한다. I/O 는 CPU 가 연산하는 것보다 작업을 요청하고 대부분 기다리는 것으로 많은 시간을 소비한다.
    • 별도 스레드 풀에서 실행된다. 이 때 기본 스레드 수는 40개다.
  • @app.post(), @app.get(), @app.put(), @app.patch(), @app.delete() 로 HTTP 메소드를 정의할 수 있으며, 첫 번째 인자로 해당 Endpoint 르 호출하기 위한 경로(Path)를 작성한다.

    from typing import Union
    from fastapi import FastAPI
    
    app = FastAPI()
    
    @app.get("/")
    def read_root():
        return {"Hello" : "World"}
    
    @app.get("/items/{item_id}")
    async def read_item(item_id: int, q: Union[str, None] = None):
        return {"item_id" : item_id, "q" : q}
    

HTTP Method

  • FastAPI 로 endpoint 들을 정의하게 되는데, 이 때 HTTP Method 마다 어떻게 동작할지를 구현해야 한다. 따라서 대표적인 HTTP Method 인 GETPOST 에 대해 알아보자.
  • GET
    • 대표적으로 웹 페이지에 접근할 때 GET Method 를 쓰게 된다.
    • URL 에 데이터가 노출된다. 예를 들어 localhost:8080/login?id=bkkhyunn 과 같은 식이다.
    • 데이터의 위치는 Header 에 들어있다.
  • POST
    • 대표적으로 웹 페이지에 FORM 이나 FILE 등을 제출 할 때 사용하게 된다.
    • URL 에 데이터가 노출되지 않는다. 즉 localhost:8080/login 과 같은 식이다.
    • 데이터의 위치는 Body 에 들어있다.
  • 아래는 루트(/)로 접근했을 때 Hello World 가 출력되는 웹 서버 예시다.

    from fastapi import FastAPI
    
    # FastAPI 객체 생성
    app = FastAPI()
    
    @app.get("/")    # 루트(/)로 접근하면 return 을 보여준다.
    def read_root():
        return {"Hello" : "World"}
    
  • 해당 웹 서버를 실행시키기 위해서, Poetry 를 사용한다고 가정하고, poetry add uvicorn 를 통해 uvicorn 을 설치한다.
    • uvicorn 설치 시 [standard] 를 추가하여 설치하면 Python 기본 비동기 라이브러리인 asyncio 의 이벤트 루프가 uvloop 라는 더 성능이 좋은 Cython 기반의 이벤트 루프를 사용할 수 있다.
    • poetry add "uvicorn[standard]"
    • 해당 링크(https://www.uvicorn.org/#quickstart)를 참고하자.
  • uvicorn 은 Python 으로 작성된 ASGI(Asynchronous Server Gateway Interface) 규격을 구현한 비동기 웹 서버다.
    • ASGI 는 WSGI 의 비동기 버전이라고 생각할 수 있다. WSGI 가 싱글 스레드 요청 처리에 초점을 맞춘 반면에, ASGI 는 비동기 I/O 를 통해 여러 요청을 동시에 처리할 수 있는 구조를 제공한다.
    • uvicorn 은 ASGI 3.0 사양을 완벽하게 구현하며 이로 FastAPI, Django 등 python 웹 프레임워크와 원활하게 호환 가능하다.
    • 굉장히 강력하고 유연한 ASGI 서버이며 특히 FastAPI와 같은 최신 Python 웹 프레임워크와 함께 많이 사용하며 프로덕트 환경에서도 웹서버로 사용하거나, Nginx 같은 리버스 프록시 뒤에 배치하여 사용 가능하다.
  • CLI 상에서 uvicorn simple_webserver:app —reload 와 같이 사용할 수 있다.
  • 만약 위와 같이 터미널에서 unvicorn 실행을 하기 번거롭다면, 코드 내 uvicorn.run 을 추가할 수 있다.

    from fastapi import FastAPI
    import uvicorn
    
    # FastAPI 객체 생성
    app = FastAPI()
    
    @app.get("/")    # 루트(/)로 접근하면 return 을 보여준다.
    def read_root():
        return {"Hello" : "World"}
    
    if __name__ == '__main__':
        uvicorn.run(app, host="0.0.0.0", port=8000)
    

Swagger

  • localhost:8000/docs 와 같이 만든 웹 서버의 docs 로 이동하면 Swagger 문서를 확인할 수 있다. 또한 localhost:8000/redoc 에서는 Redoc 을 확인할 수 있다. Redoc 또한 Swagger 문서와 같이 쓸 수 있는 API 문서화 도구다.
  • 아래 그림에서 왼쪽이 Swagger UI 이고, 오른쪽이 Redoc UI 다.

  • Swagger vs. Redoc
    • Swagger
      • Swagger 는 API 설계, 빌드, 문서화 및 사용에 대한 강력한 오픈소스 도구다.
      • Swagger 도구 세트에는 Swagger Editor, Swagger UI, Swagger Codegen 등이 포함된다. 가장 인기 있는 도구는 Swagger UI 로, 웹 기반 UI 를 제공하여 개발자가 RESTful 웹 서비스를 시각적으로 탐색하고, API 엔드포인트에 대한 요청을 보내고 응답을 받을 수 있게 한다.
      • 이 UI 는 API 의 모든 부분을 상세히 보여주며, 직관적으로 API 를 테스트할 수 있는 기능을 제공한다.
    • Redoc
      • ReDoc 역시 API 문서화를 위한 오픈 소스 도구다.
      • Swagger 와 비슷하게 ReDoc 은 API 스펙의 시각적인 표현을 제공하지만, 더 깔끔하고 직관적인 인터페이스로 설계되어 있다.
      • ReDoc 은 특히 읽기 쉽고 깨끗한 문서 레이아웃을 제공하는 데 중점을 둔다. 이 도구는 복잡한 API 스펙을 쉽게 탐색할 수 있도록 다단계 네비게이션과 상세한 검색 기능을 지원한다.
    • ReDoc 은 사용자 친화적인 디자인과 깔끔한 인터페이스를 제공하는 반면, Swagger 는 더 많은 상호작용과 사용자 정의 옵션을 제공한다.
    • Swagger 는 API 를 테스트하고 상호작용하는 데 필요한 다양한 도구와 통합 옵션을 제공하는 반면, ReDoc 은 주로 문서의 시각화와 구조화에 집중한다.
  • Swagger 와 같은 API 문서화 도구는 아래와 같은 경우에 용이하다.
    • 협업 과정에서 설계한 API 를 클라이언트에서 호출하는 경우에 어떻게 되는지 물어본다면 해당 Swagger UI 문서를 보면 된다고 답할 수 있다.
    • 이 때 해당 문서를 사람이 작성하면 유지보수해야 하는데, FastAPI 에서는 Swagger 로 문서를 자동으로 만들어준다.
    • 어떻게 Request 해야 하는지 질문할 때, Swagger UI 를 보여주면서 어떤 파라미터를 넣어서 요청하면 되는지 답할 수 있다.
  • FastAPI 와 Pydantic 을 이용해 정의된 Endpoint 코드는 자동으로 Open API 스펙에 맞는 API Docs 를 생성해준다.
  • Usecase
    • REST API 설계 및 문서화할 때
    • 다른 개발팀과 협업하는 경우
    • 구축된 프로젝트를 유지보수하는 경우
    • API 디자인, API 빌드, API 문서화, API 테스팅 기능 사용
    • Swagger 로 API 명세를 공유
  • 정리하면, Swagger 는 백엔드 개발자와 클라이언트 개발자 간의 소통을 위한 문서로, 각 기능들에 대한 Endpoint 경로, Path/Query 파라미터(필수 값 유무), Request / Response Body, 각 기능에 대한 설명 등을 확인할 수 있다.

URL parameters

  • 웹에서 GET Method 를 사용해서 데이터를 전송할 수 있다.
  • 예를 들어 https://search.naver.com/search.naver?where=nexearch&sm=top_hty&fbm=0&ie=utf8&query=광진구 와 같은 URL 에서, ? 뒤에 & 로 연결된 key=value 형태가 모두 URL 파라미터다.
  • 이렇게 URL 에 파라미터를 주는 방식은 2 가지가 있다.
  • Path parameter 방식
    • /users/402 와 같은 형식이다.
    • 서버에 402 라는 값을 전달하고 변수로 사용할 수 있다.
  • Query parameter 방식
    • /users?id=402 와 같은 형식이다. API 뒤에 입력 데이터를 함께 제공하는 방식으로 사용한다.
    • 이 때 Query String 을 이용한다. Query String 은 Key, Value 의 쌍으로 이루어지며 & 로 연결해 여러 데이터를 넘길 수 있다.
  • 언제 어떤 방식을 사용하느냐는 상황에 따라 다르다.
  • 예를 들어 어떤 Resource 를 식별하고 싶은데, 해당 Resource 가 없는 경우를 보자.
    • Path parameter 방식은 users/bkkhyunn 으로 접근한다. 그러나 존재하는 내용이 없으므로 404 Error 가 발생하게 된다.
    • Query parameter 방식은 users?name=bkkhyunn 으로 접근한다. 이 때 Query 방식은 optional 이기 때문에 데이터가 없는 경우 빈 리스트가 나온다. 따라서 추가적인 Error Handling 이 필요하다.
  • Resource 를 식별해야 하는 경우에는 Path Parameter 가 더 적합하다. 반면에 정렬, 필터링을 해야 하는 경우에는 Query Parameter 가 더 적합하다.
  • 아래에서 각 parameter 방식을 더 자세하게 보자.

Path parameters

  • Path Parameter 는 URL 경로의 일부를 동적으로 변경하여, API 가 다양한 리소스를 식별하고 처리할 수 있도록 한다. 즉 엔드포인트 경로에 포함된 파라미터를 뜻한다.
  • FastAPI 는 데코레이터로 GET, POST 에 따른 동작을 구분할 수 있다. @app.get, @app.post

    Untitled

  • 정보를 read 하기 위한 GET Method 를 사용하여 유저 정보에 접근하는 API 를 만들어보자.

    from fastapi import FastAPI
    import uvicorn
    
    # FastAPI 객체 생성
    app = FastAPI()
    
    @app.get("/users/{user_id}")
    def get_user(user_id: int):
        return {"user_id" : user_id}
    
    if __name__ == '__main__':
        uvicorn.run(app, host="0.0.0.0", port=8000)
    
  • 위와 같이 데코레이터 안에 { } 로 표현하면 변수가 된다. 즉 위 예시에서 /users/ 다음에 user_id 를 넣을 수 있고, 이는 접근 URL 마다 계속 바뀔 수 있는 값이 된다.
  • 또한 엔드포인트 함수의 인자를 정의할 때, Type Hint 로 값에 대한 Validation 을 수행할 수 있다.
  • 웹 서버는 계속 동작하는 상태로 둬야 한다. 그렇게 되면 터미널에서 request 로그가 남는다. 이를 통해 method, status code 등을 확인할 수 있다.

Query parameters

  • Query Parameter 는 API 에 추가적인 정보를 제공하기 위해 사용되며, URL 뒤에 ? 를 사용하여 데이터를 전달한다. 즉 엔드포인트 경로에 포함되어 있지 않은 파라미터를 뜻한다.
  • 경로에는 존재하지 않고, 엔드포인트 함수의 인자에 정의되어 있다면 해당 파라미터는 Query 파라미터가 된다. 아래 예시 코드를 보자.

    from fastapi import FastAPI
    import uvicorn
    
    app = FastAPI()
    items_db = [{"item_name" : "Foo"}, {"item_name" : "Bar"}, {"item_name" : "Baz"}]
    
    @app.get('/items/')
    def read_item(skip: int = 0, limit: int = 10):
        return items_db[skip:skip+limit]
    
    if __name__ == '__main__':
        uvicorn.run(app, host="0.0.0.0", port=8000)
    
  • Path parameters 에서는 @app.get 의 endpoint URL 뒤에 {user_id} 를 통해 변수로 사용할 수 있었다. 반면에 Query Parameter 는 아래와 같이 해당 데코레이터로 감싸진 함수의 인자로 들어간다.

    Untitled

  • 위 그림에서 items/ 은 어떤 parameter 도 전달하지 않았기 때문에 default 값이 사용되어 items_db 의 전체를 가져오게 된다.
  • URL 뒤에 ? 를 붙이고 Key=Value 형태의 파라미터를 & 로 연결하는 방식으로 함수에 인자를 건네줄 수 있다.
    • 만약 localhost:8000/items?skip=20 이라면 items_db[20:30] 으로 빈 리스트 [] 가 출력된다.
  • Query parameter 는 이처럼 유효하지 않은 parameter 가 들어왔을 때를 위한 Error Handling 이 필요하다.
  • Path & Query Parameter 에 기본 값을 설정할 수 있지만, Path Parameter 에 기본 값을 설정하는 것은 예상하지 못한 엔드포인트가 호출될 수 있기 때문에 적절하지 않을 수 있다. 따라서 가능하면 Query Parameter 에만 기본 값을 설정하는 것이 좋다.

Optional parameters

  • 특정 파라미터는 Optional(선택적)로 사용할 수 있다. typing 모듈의 Optional 을 사용하면 된다.
  • 아래와 같이 Optional 을 통해 해당 파라미터는 선택적임을 명시한다.

    from typing import Optional
    from fastapi import FastAPI
    import uvicorn
    
    app = FastAPI()
    items_db = [{"item_name" : "Foo"}, {"item_name" : "Bar"}, {"item_name" : "Baz"}]
    
    @app.get('/items/{user_id}')
    def read_item(item_id: str, q: Optional[str] = None):
        if q:
            return {"item_id" : item_id, "q" : q}
        return {"item_id" : item_id}
    
    if __name__ == '__main__':
        uvicorn.run(app, host="0.0.0.0", port=8000)
    
  • Optional parameter 를 통해 Path parameter 와 Query parameter 를 같이 사용할 수 있다.

    Untitled

Request Body

  • 클라이언트에서 API 를 통해 서버에 데이터를 보낼 때 Request Body(=Payload)를 사용한다. 반대로 서버에서 클라이언트로 응답을 보낼 때는 Response Body 를 사용한다.
  • Request Body 에 데이터가 항상 포함되어야 하는 것은 아니다. 만약 Request Body 에 데이터를 태워 보내고자 한다면 POST Method 를 사용한다. 참고로 GET Method 는 URL, 즉 Request Header 로 데이터를 전달한다.
  • Body 에는 Body 에 포함된 데이터를 설명하는 Content-type 이라는 Header 필드가 존재한다. 따라서 데이터를 보낼 때, 어떤 데이터 타입인지 명시해줘야 한다.
  • 대표적인 Content-type 에는 아래와 같은 것들이 있다.
    • application/json : FastAPI 가 기본적으로 사용하는 타입이다. Body 가 JSON 형태임을 의미한다.
    • application/x-www-form-urlencoded : Body 에 Key, Value 를 사용한다. & 구분자를 사용한다.
    • text/plain : 단순 txt 파일임을 의미한다.
    • multipart/form-data : 데이터를 바이너리 데이터로 전송한다.
  • Request Body 를 통해 데이터를 보낼 때, Content-type 을 명시하는 예제를 보자. 다음은 POST 요청으로 item 을 생성하는 예제다.
  • FastAPI 에서 많이 다룰 기능인 Pydantic 을 이용하여 Request Body 의 데이터를 정의한다. 이는 다음 포스트에서 다룰 예정이다.
  • 아래와 같이 Pydantic 의 BaseModel 을 상속받은 Class 를 Endpoint 인자로 정의해주어 Request Body 를 정의할 수 있다.

    from typing import Optional
    from fastapi import FastAPI
    import uvicorn
    
    from pydantic import BaseModel
    
    class Item(BaseModel):
        name: str
        description: Optional[str] = None
        price: float
        tax: Optional[float] = None
    
    app = FastAPI()
    
    @app.post('/items/')
    def create_item(item: Item):
        return item
    
    if __name__ == '__main__':
        uvicorn.run(app, host="0.0.0.0", port=8000)
    
  • 위와 같이 pydantic.BaseModel 로 데이터를 정의한 뒤, 함수 create_item 의 인자에 대한 Type Hinting 에 Class 를 주입한다.
  • 이는 Request Body 데이터를 Validation 하는 기능을 한다. 즉 Hinting 으로 데이터를 정의한 Class 를 줬기 때문에, Request Body 의 데이터 형태는 name, description, price, tax 로 구성된다는 것을 전달한다. 여기서 Optional 이 아닌 것은 항상 기입해줘야 한다.
    • 다시 한번, BaseModel Class 의 속성 값으로 Type Hint 와 함께 정의하면 해당 필드가 Request Body 의 Json 입력으로 들어왔는지 혹은 타입이 정상인지 검사하게 된다.
    • 각 속성 값에 (defualt)값이 할당되어 있다면, 해당 필드는 필수 값이 아닌 선택 값이 된다. Return 시 item.name, item.price 와 같이 반환할 수도 있다.
  • 이제 curl 등으로 POST 를 실행해볼 수 있다.
  • 더 쉬운 방법으로 Swagger API 문서 확인을 위해 localhost:8000/docs 로 이동해보자.
  • UI 상에서 Schemas 를 확인할 수 있고, 그 안에 item 이 있다. 이를 클릭하면 pydantic 으로 정의한 내용을 볼 수 있다.

    Untitled

  • “Try it out” 버튼을 클릭하면 POST 기능을 시험해볼 수 있다. 아래와 같은 기본 설정 상태에서 Execute 버튼을 누른다. 이는 어떻게 요청하면 되는지를 알려주는 페이지로, test 할 때 좋다.

    Untitled

  • Execute 를 누르면 curl 명령어와 그에 대한 Response 가 보인다.

    Untitled

  • Request Body 와 Response Body 가 Header 를 제외하고 -d 에 있는 것이 동일한 것을 볼 수 있다. 이는 위 create_item 에서 item 을 그대로 반환하기 때문이다.
  • 이처럼 요청을 했을 때 response 가 어떻게 올 것인가, 데이터를 어떻게 전달할 것인가에 대해서는 Request Body 를 사용한다.
  • 만약 float 타입으로 정의된 tax 에 String 을 넣어서 Execute 하게 되면, 아래와 같이 Error 가 발생한다. 이것이 data Validation Check 다.

    Untitled

Response Body

  • Response Body 는 API 요청에 따른 응답이 클라이언트로 넘어갈 때 데이터가 담긴다.
  • 위 예제와 같이 API response 가 Request Body 의 데이터 전체가 아니라 특정 데이터만 response 하도록 할 수 있다.
  • 이는 아래와 같이 데코레이터에서 response_model 인자로 주입할 수 있다.

    from typing import Optional
    from fastapi import FastAPI
    import uvicorn
    
    from pydantic import BaseModel
    
    class ItemIn(BaseModel):
        name: str
        description: Optional[str] = None
        price: float
        tax: Optional[float] = None
    
    class ItemOut(BaseModel):
        name: str
        price: float
        tax: Optional[float] = None
    
    app = FastAPI()
    
    @app.post('/items/', response_model=ItemOut)
    def create_item(item: ItemIn):
        return item
    
    if __name__ == '__main__':
        uvicorn.run(app, host="0.0.0.0", port=8000)
    
  • Class ItemIn 이 Request Body 데이터 형식이고, Class ItemOut 이 Response Body 데이터 형식이 된다. description 이 빠진 것처럼, 원하는 데이터만 response 할 수 있다.
  • ItemOut 은 Output Data 를 해당 클래스에 정의된 것에 맞게 변형해준다. Request 가 오면 그걸 토대로 변형해서 Response 로 바꿔주는 것이다.
  • 이를 통해 Response 에 대한 JSON Schema 를 추가한 것이기 때문에, 데이터 Validation 의 일환이라고 볼 수 있다.
  • FastAPI 에서 위와 같이 Request, Response Body 를 정의하면 Swagger 로 자동으로 문서화된다.
  • 아래와 같이 /docs 로 들어가 위와 같이 “Try it out” 및 “Execute” 버튼을 누르면 Request 데이터와 Response 데이터가 다른 것을 확인할 수 있다.

    Untitled

  • 실제로 이 방식은 웹에서 Feature 요청할 때 request 에 담아 보내고, response 에서 feature 를 다시 내려줘야 하는지 여부에 따라 활용할 수 있다.
  • Feature 를 그대로 내려주면 response_model 을 따로 지정할 필요 없이 동일하게 내려주면 되고, 필요한 데이터만 응답하고자 한다면 response_model 을 사용하면 된다.

Form

  • Form 은 입력 형태로 데이터를 받고 싶은 경우 사용한다. 회원가입, 로그인 창이 모두 Form 형태다.
  • Form 을 사용하기 위해서 python-multipart 를 설치해야 한다. 또한 프론트엔드를 간단히 만들기 위해 Jinja2 도 설치해보자.
  • Form 클래스를 사용하면 Request 의 Form Data 에서 값을 가져오게 된다. 즉 웹에서 입력하고 제출을 누르면 데이터가 전송되는 것이다.
  • 아래의 예제 코드에서 Form(...) 은 Form 에서 데이터를 가지고 오겠다는 것을 의미한다.

    from fastapi import FastAPI, Form
    import uvicorn
    
    app = FastAPI()
    
    @app.post('/login/')
    def login(username: str = Form(...), password: str = Form(...)):
        return {"username" : username}
    
    if __name__ == '__main__':
        uvicorn.run(app, host="0.0.0.0", port=8000)
    
  • 이제 localhost:8000/login/ 에 접속하면 아래와 같은 오류가 뜨게 된다. 왜 그럴까?

    Untitled

  • 위 그림에서 볼 수 있는 ‘Method Not Allowed’ 는 HTTP 메소드가 서버에서 지원되지 않을 때 발생한다.
  • 위 코드에서 구현한 것은 @app.postPOST Method 다. 그러나 login 으로 접근하는 것은 GET Method 가 요청된다.
  • 웹 브라우저에서 URL 을 입력하거나 링크를 클릭할 때 기본적으로 발생하는 것은 GET 요청이다. 앞서 보았듯, GET 은 서버에 정보를 요청할 때 사용되고, 데이터를 검색하거나 특정 페이지를 요청할 때 사용된다.
  • 위 예제에서 작성한 POST 는 클라이언트가 서버에 데이터 혹은 Form 을 제출할 때 주로 사용된다. 이를 통해 데이터 생성 혹은 업데이트를 한다.
  • 즉 로그인 Form 에 사용자 이름과 비밀번호를 입력하고 제출 버튼을 누를 때, 이 정보는 POST 요청을 통해 서버로 전송되는 것이다.
  • 정리하면, 웹사이트에 접근하는 것은 GET 요청이고, 사용자가 데이터를 제출하는 행위는 POST 요청이다.
  • 이처럼 웹 페이지를 만들 때 GET, POST Method 를 많이 사용하게 된다. 특히 API 를 만들어서 ML 모델을 활영하여 예측을 제공할 때는 POST 요청이 많다.
  • 이제 위 오류를 해결하기 위해 GET 을 구현해보자. 로그인 Form 을 위한 프론트엔드를 사용하여 간단하게 리팩토링 한다.

    from fastapi import FastAPI, Form, Request
    from fastapi.templating import Jinja2Templates
    
    import uvicorn
    
    app = FastAPI()
    templates = Jinja2Templates(directory='./')
    
    @app.get('/login/')
    def get_login_form(request: Request):
        return templates.TemplateResponse('login_form.html', context={'request':request})
    
    @app.post('/login/')
    def login(username: str = Form(...), password: str = Form(...)):
        return {"username" : username}
    
    if __name__ == '__main__':
        uvicorn.run(app, host="0.0.0.0", port=8000)
    
  • /login/ 에 접근하면 FastAPI 가 제공하는 Request 객체로 Request 를 받는다.
  • Jinja2Templates 는 Python 에서 사용할 수 있는 템플릿 엔진으로 프론트엔드를 구성한다. templates.TemplateResponse 을 사용하여 login_form.html HTML 파일로 데이터를 보낸다.
  • login_form.html 은 아래와 같이 간단하게 구현한다. Jinja Template 에서는 {{ }} 표현을 사용해서 데이터를 사용할 수 있다.

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Sample Login Form </title>
    </head>
    <body>
    <form method="post">
        <input type="string" name="username" value="{{  username  }}"/>
        <input type="password" name="password" value="{{  password  }}"/>
    
        <input type="submit">
    </form>
    </body>
    </html>
    
  • 위 HTML 을 보면 {{ }} 안에 username 과 password 가 있다. 데이터가 들어오면 {{ }} 에 저장되어 변수로 활용될 수 있다.
  • 이제 /login/GET 으로 접근할 때는 HTML 을 반환하고 POST 요청을 했을 때는 login 함수를 실행시키게 된다.

    Untitled

  • 위 그림과 같이 제출 버튼을 누르면 POST 요청이 되어 login 함수가 실행된다.
  • 참고로 FORM(...) 에서 ... 은 python ellipsis 로, required 필수 요소를 의미한다. 즉 FORM 에서 username 은 필수로 입력되어야 한다는 것을 의미한다. 실제로 입력되지 않으면 실행되지 않는다.
  • FastAPI 웹 서버를 실행한 후 Swagger(/docs) 로 이동하면 required 를 확인할 수 있다.

    Untitled

  • 또한 Form 형태로 데이터를 지정하기 때문에, Content-type 이 application/x-www-form-urlencoded 로 되어있다.

File

  • Form 이외에, csv 나 이미지 등 File 을 업로드할 수 있다. 이렇게 File 을 사용할 때도 python-multipart 를 설치해야 한다.
  • 아래와 같이 FastAPI 에서 제공하는 UploadFile 을 사용한다.

    from typing import List
    
    from fastapi import FastAPI, File, UploadFile
    from fastapi.responses import HTMLResponse
    
    import uvicorn
    
    app = FastAPI()
    
    @app.post('/files/')
    def create_files(files: List[bytes] = File(...)):
        return {"file_sizes" : [len(file) for file in files]}
    
    @app.post('/uploadfiles/')
    def create_upload_files(files: List[UploadFile] = File(...)):
        return {"filenames" : [file.filename for file in files]}
    
    @app.get('/')
    def main():
        content = """
        <body>
        <form action="/files/" enctype="multipart/form-data" method="post">
        <input name="files" type="file" multiple>
        <input type="submit">
        </form>
        <form action="/uploadfiles/" enctype="multipart/form-data" method="post">
        <input name="files" type="file" multiple>
        <input type="submit">
        </form>
        </body>
        """
        return HTMLResponse(content=content)
    
    if __name__ == '__main__':
        uvicorn.run(app, host="0.0.0.0", port=8000)
    
  • /GET 으로 접근할 때 보여줄 HTML(content) 를 보면 <form action=> 이 있다. 이는 해당 form 을 action 하면 할당된 POST 함수를 실행하겠다는 것을 의미한다. 즉 HTML 에서 action 으로 넘기는 것이다.
  • 위 예제에서는 두 개의 form 이 있고, 첫번째 form 을 제출했을 때 /files/POST 요청을 하여 create_files 함수가 실행되고, 두번째 form 이 제출되면 /uploadfiles/POST 요청되어 create_upload_files 함수가 실행된다.
  • 파일은 bytes 로 표현하고, 여러 파일은 List 에 담게 된다.
    • Bytes 자료형은 컴퓨터가 읽을 수 있는 형식으로, 1 과 0 으로 구성되는 바이너리 포맷으로 저장한다.
    • 다양한 파일(텍스트, 이미지, 오디오)은 기본적으로 바이너리 데이터로 구성된다.
  • 업로드하는 파일들 대부분이 Byte 자료형을 이용한다.

Untitled

맨 위로 이동 ↑

댓글 남기기