Fast API에서 Class를 사용할 때 활용하는 라이브러리
Data Validation / Setting Management 라이브러리
파이썬 기본 Type(String, Int 등), List, Dict, Tuple에 대한 Validation 지원
Config를 효과적으로 관리하게 도와줌
어디서 에러가 발생했는지 Location, Type, Message 등을 알려줌
Runtime에서 Type Hint에 따라 Validation Error 발생
Custom Type에 대한 Validation에도 쉽게 사용 가능
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl, Field, DirectoryPath
import uvicorn
app = FastAPI()
# Request Body
# url, rate에 대해 1개씩 잘못된 값을 보내 에러 문구가 달라짐을 보이기 위해 Optional 활용
class ModelInput(BaseModel):
url: Optional[HttpUrl] = None # URL 링크 형식이여야 함
rate: int = Field(ge=1, le=10) # 1이상 10이하여야 함
@app.post('/')
def get_login_form(input: ModelInput):
return input
if __name__=='__main__':
uvicorn.run(app, host='0.0.0.0', port = 8000)
msg를 보면 "ensure this value is less than or equal to 10"이라고 나왔다. 즉, 값이 10이하가 아니라는 의미이다.
loc를 보면 "body"(Reqeust Body)의 "rate"에서 에러가 발생했다는 것을 알 수 있다
즉, 이 2개를 종합하면 rate가 10 초과인 값이 요청으로 들어와 Validation을 통과했을 때 실패가 뜬다는 의미이다
msg에는 "invalid or missing URL Scheme"이라고 발생했다. 즉, 올바른 URL 형식이 아니라는 의미이다.
loc과 msg를 합치면 URL이 올바른 값이 아니라는 의미이며, 의도와 맞다
"https://www.naver.com" 으로 요청 보냈을 경우 : 성공(200 Code)
애플리케이션에서 설정(Config) 값을 상수로 코드에 저장하는 경우가 있는데, 이는 Twelve-Factor를 위반함
12-Factor App은 설정을 환경 변수(env / envvars라고도 부름)에 저장함
환경 변수는 코드 변경 없이 배포 때마다 쉽게 변경 가능해야 함
이전에 활용한 방법
Pydantic Base Setting
env = 'db_host'
일 때, os.environ['db_host'] = 'mysql'
명령어를 통해 원래 환경 변수값을 오버라이딩 할 수 있음코드
from pydantic import BaseSettings, Field
from enum import Enum
class ConfigEnv(str, Enum):
DEV = "dev"
PROD = "prod"
class DBConfig(BaseSettings):
host: str = Field(default='localhost', env='db_host')
port: int = Field(Default=3306, env='db_port')
class AppConfig(BaseSettings):
env: ConfigEnv = Field(default="dev", env="env")
db: DBConfig = DBConfig()
config_with_pydantic = AppCnofig()
os.environ['ENV'] = 'prod'
# AppConfig를 보면 env 이름을 가진 것은 ConfigEnv임을 알 수 있음
# ConfigEnv는 default로 'dev'를 가지고 있는데, 이를 'prod'로 바꾼다는 것을 알림
이벤트가 발생했을 때 처리를 담당하는 함수
FastAPI에선 Application이 실행될 때와 종료될 때 함수를 실행할 수 있음
@app.on_event('startup')
@app.on_event('shutdown')
from fastapi import FastAPI
import uvicorn
app = FastAPI()
@app.on_event("startup")
def start():
print("Hello!")
@app.on_event("shutdown")
def start():
print("Bye!")
if __name__=='__main__':
uvicorn.run(app, host='0.0.0.0', port = 8000)
큰 애플리케이션에서 많이 활용되는 기능
API Endpoint를 정의
기존에 사용하는 @app.get
, @app.post
를 활용하지 않고 router파일을 따로 설저앟여 app에 import해서 활용
실제로 Router를 활용할 때는 하나의 파일에 저장하지 않고, 1개의 파일마다 1개의 Router를 만들어서 각각 저장하는 경우가 많음
네이버를 예를 들어보자.
네이버는 blog에 대한 사이트는 naver.com/blog/~로 링크가 되어 있으며, Cafe에 대한 사이트는 naver.com/cafe/~ 형태로 링크가 구성되어 있다.
그렇다면, 이 기능은 blog와 연관이 있느니 get('/blog/~')로 만들고, cafe와 연관이 있으면 get('/cafe/~')로 일일히 다 타이핑할까?
물론, 하면 되긴 한다. 하지만 사람은 언제나 실수를 하는 존재로, 그러다 Cafe와 연관이 있는데 /cafe를 앞에 붙이지 않거나 하는 실수가 일어날 수 있고, 이런 경우 문제가 꼬이고 꼬여서 폭발해버릴 수도 있을 것이다.
개발자는 생각했다.
"아 애초에 그냥 Cafe와 연관된 기술은 /cafe를 앞에 자동으로 붙이는 기술을 만들면 되지 않을까?"
그렇게 해서 만들어진, Endpoint(위 예시에선 /blog, /cafe일 것이다)를 자동으로 정의해주는 기능이 API Router이며, 이런 실수를 방지하는데 매우 효과적이다
from fastapi import FastAPI, APIRouter
import uvicorn
app = FastAPI()
blog = APIRouter(prefix='/blog')
cafe = APIRouter(prefix='/cafe')
@blog.get('/', tags=['blog'])
def read_blog():
return [{"Game Blog":"StarCraft", "Movie Blog":"Avengers"}]
@blog.get('/game', tags=['blog'])
def read_game():
return "StarCraft"
@cafe.get('/')
def read_cafe():
return [{"Game Blog":"League of Legends", "Movie Blog":"Batman"}]
@cafe.get('/game')
def cafe_game():
return "League of Legends"
if __name__=='__main__':
app.include_router(blog)
app.include_router(cafe)
uvicorn.run(app, host='0.0.0.0', port = 8000)
APIRouter(prefix='/blog')
명령어를 통해 '/blog'라는 Endpoint를 지정해 줄 수 있음app.include_router()
명령어를 통해 생성한 APIRouter를 FastAPI 객체에 추가시켜 줘야 함tags=['blog']
가 필수적일까?
답부터 말하자면 "아니오"이다.
tags가 없는 /cafe, /cafe/game 모두 정상적으로 GET Method를 통해 접근할 수 있다.
단지, tags가 없다면 Document를 쓸 때나 위처럼 Swagger를 활용할 때 default라는 이름으로 지정된다.
즉, 해당 URI가 무엇에 대한 URI인지 알 수가 없다.
따라서, tags로 URI가 "어떤 작업에 대한" URI인지를 명시하여 Document가 이해하기 쉽게 만들어지도록 도와주는 보조 역할을 한다고 생각하면 된다
Python 파일을 보면 "/", "/game" 밖에 존재하지 않지만, APIRouter를 통해 prefix를 각각 "/blog", "/cafe"로 지정해줬기 때문에 존재하는 링크도 모두 prefix가 앞에 붙어있음을 알 수 있다.
참고로, localhost:8000을 입력하면 Error가 발생할 것이다(pefix가 없는 주소이므로, 존재하지 못하는 주소)
웹 서버를 안정적으로 위해 반드시 수행해야 하는 행동
서버에서 Error가 발생했을 경우 Error 내용을 파악할 수 있어야 하고 요청한 Client에 Error에 대한 정보를 전달하여 대응할 수 있어야 함
서버 개발자는 모니터링 도구를 활용해 Error Log를 수집해야 함
발생하는 오류를 빠르게 수정할 수 있도록 예외 처리를 잘 해야 함
(ex) 회원가입 시 404 Error가 발생했다! 라는 것만 알린다면, 유저는 사이트를 잘못 접속한것인지, 입력값이 잘못된 것인지 알 수가 없으므로 User 입장에서는 어떤 부분을 고쳐야할지 모르기 때문에 제품 만족도가 많이 떨어짐
즉, 에러 메시지와 에러의 이유를 보안을 지키는 선에서 Client에게 전달할 수 있도록 코드를 작성해야 하며, 이를 Error Handling이라 함
from fastapi import FastAPI, HTTPException
import uvicorn
app = FastAPI()
items = {
1: "Math",
2: "Korean",
3: "English"
}
@app.get("/item/{item_id}")
async def find_by_id(item_id:int):
try:
item = items[item_id]
except KeyError:
raise HTTPException(status_code=404, detail=f"{item_id}번째 Item이 없습니다.
Item은 총 3개 존재 하고, 1 ~ 3중 1개의 값을 선택 해야 합니다!")
return item
if __name__=='__main__':
uvicorn.run(app, host='0.0.0.0', port = 8000)
item_id가 1,2,3이 아닌 다른 값이 올 경우 에러가 발생할 것이다. 따라서 이 부분에 대한 Error Handling이 필요하다
raise HttpException
Fast API는 Starlett이라는 비동기 프레임워크를 래핑하므로, Background에서 작업을 수행시킬 수 있음
Background Tasks : 시간이 오래 걸리는 작업들을 Background에서 실행하도록 하는 것
Online Serving시에는 CPU 사용이 많은 작업들을 Background Task로 사용하면 클라이언트는 작업 완료를 기다리지 않고 즉시 Response를 받을 수 있음
Background Task를 통해 시간이 오래 걸려 수행된 결과물을 DB 등에 저장하고, 나중에 Search를 통해 DB에서 결과물을 찾아오는 형식으로 Task 완료 여부를 확인할 수 있음
from time import sleep
from fastapi import FastAPI, HTTPException
import uvicorn
from starlette.background import BackgroundTasks
app = FastAPI()
def waiting(wait_time:int):
sleep(wait_time)
print("Wake Up!!!!!!!")
@app.get("/task")
async def find_by_id(background_tasks:BackgroundTasks):
background_tasks.add_task(waiting, 4)
print("I want to return this first!")
return "hi"
if __name__=='__main__':
uvicorn.run(app, host='0.0.0.0', port = 8000)
starlette.background import BackgroundTasks
: import를 fastapi에서 하는게 아닌 starlette에서 하는 점을 주의하자background_tasks.add_task(function, parameters)
I want to return this first! 문구가 나중에 실행되도록 코드 상에는 구현되어 있지만, waiting function은 Background에서 실행되도록 설정해놨으므로 먼저 출력되고, 이후 waiting() 메서드가 모두 실행되었을 때 Wake Up!!!!!이 출력되는 것이다
Parameter는 int형이므로 4를 전달해줬고, 4초 이후 Wake Up!!! 문구가 출력된다