[FastAPI] FastAPI 란?
FastAPI 는 Online Serving 을 직접 구현할 때 사용하는 대표적인 Python 웹 프레임워크다. 굉장히 빠르게 업데이트 되고 있고, 최근에는 아래 그림과 같이 FastAPI 혹은 Django 중 두 가지 프레임워크로 많이 개발하게 된다.
이번 포스트에서는 FastAPI 와 활용을 위해 필요한 개념들을 알아보자.
FastAPI
-
FastAPI 는 High Performance, Easy, Productivity 의 특징을 가진다. 자세한 것은 아래에서 확인하자.
- 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 를 통해 서버가 구동된다.
- https://blog.naver.com/pjt3591oo/222772705407 에 굉장히 잘 설명되어 있다.
- 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
클래스를 사용해서 반환하는 것이 성능 상 유리하다.
- Request Body / Response Body 를 Pydantic
프로젝트 구조
- 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 인
GET
과POST
에 대해 알아보자. 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 설치 시
- 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
- 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 이 필요하다.
- Path parameter 방식은
- Resource 를 식별해야 하는 경우에는 Path Parameter 가 더 적합하다. 반면에 정렬, 필터링을 해야 하는 경우에는 Query Parameter 가 더 적합하다.
- 아래에서 각 parameter 방식을 더 자세하게 보자.
Path parameters
- Path Parameter 는 URL 경로의 일부를 동적으로 변경하여, API 가 다양한 리소스를 식별하고 처리할 수 있도록 한다. 즉 엔드포인트 경로에 포함된 파라미터를 뜻한다.
-
FastAPI 는 데코레이터로
GET
,POST
에 따른 동작을 구분할 수 있다.@app.get
,@app.post
-
정보를 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 는 아래와 같이 해당 데코레이터로 감싸진 함수의 인자로 들어간다. - 위 그림에서
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 를 같이 사용할 수 있다.
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 으로 정의한 내용을 볼 수 있다.
-
“Try it out” 버튼을 클릭하면
POST
기능을 시험해볼 수 있다. 아래와 같은 기본 설정 상태에서 Execute 버튼을 누른다. 이는 어떻게 요청하면 되는지를 알려주는 페이지로, test 할 때 좋다. -
Execute 를 누르면
curl
명령어와 그에 대한 Response 가 보인다. - Request Body 와 Response Body 가 Header 를 제외하고
-d
에 있는 것이 동일한 것을 볼 수 있다. 이는 위create_item
에서 item 을 그대로 반환하기 때문이다. - 이처럼 요청을 했을 때 response 가 어떻게 올 것인가, 데이터를 어떻게 전달할 것인가에 대해서는 Request Body 를 사용한다.
-
만약
float
타입으로 정의된tax
에 String 을 넣어서 Execute 하게 되면, 아래와 같이 Error 가 발생한다. 이것이 data Validation Check 다.
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 데이터 형식이고, ClassItemOut
이 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 데이터가 다른 것을 확인할 수 있다. - 실제로 이 방식은 웹에서 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/
에 접속하면 아래와 같은 오류가 뜨게 된다. 왜 그럴까? - 위 그림에서 볼 수 있는 ‘Method Not Allowed’ 는 HTTP 메소드가 서버에서 지원되지 않을 때 발생한다.
- 위 코드에서 구현한 것은
@app.post
로POST
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
함수를 실행시키게 된다. - 위 그림과 같이 제출 버튼을 누르면
POST
요청이 되어login
함수가 실행된다. - 참고로
FORM(...)
에서...
은 python ellipsis 로, required 필수 요소를 의미한다. 즉FORM
에서 username 은 필수로 입력되어야 한다는 것을 의미한다. 실제로 입력되지 않으면 실행되지 않는다. -
FastAPI 웹 서버를 실행한 후 Swagger(
/docs
) 로 이동하면 required 를 확인할 수 있다. - 또한 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 자료형을 이용한다.
댓글 남기기