Pydantic & Backend 여러 기능들

DONGJIN IM·2022년 7월 12일
2

Product Serving 이론

목록 보기
10/10
post-thumbnail
post-custom-banner

Pydantic

Pydantic이란?

  • Fast API에서 Class를 사용할 때 활용하는 라이브러리

  • Data Validation / Setting Management 라이브러리

  • 파이썬 기본 Type(String, Int 등), List, Dict, Tuple에 대한 Validation 지원

    • 기존 Validation 라이브러리보다 빠름
    • 머신러닝 Feature Data Validation으로도 활용 가능
  • Config를 효과적으로 관리하게 도와줌

Pydantic Validation

  • 어디서 에러가 발생했는지 Location, Type, Message 등을 알려줌

  • Runtime에서 Type Hint에 따라 Validation Error 발생

  • Custom Type에 대한 Validation에도 쉽게 사용 가능

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)

rate = 11로 POST 요청 보냄

  • msg를 보면 "ensure this value is less than or equal to 10"이라고 나왔다. 즉, 값이 10이하가 아니라는 의미이다.

  • loc를 보면 "body"(Reqeust Body)의 "rate"에서 에러가 발생했다는 것을 알 수 있다

  • 즉, 이 2개를 종합하면 rate가 10 초과인 값이 요청으로 들어와 Validation을 통과했을 때 실패가 뜬다는 의미이다

    • 11을 보냈으니 정답!

url을 'www.naver'로 요청 보냄

  • msg에는 "invalid or missing URL Scheme"이라고 발생했다. 즉, 올바른 URL 형식이 아니라는 의미이다.

  • loc과 msg를 합치면 URL이 올바른 값이 아니라는 의미이며, 의도와 맞다

  • "https://www.naver.com" 으로 요청 보냈을 경우 : 성공(200 Code)

Pydantic Config

  • 애플리케이션에서 설정(Config) 값을 상수로 코드에 저장하는 경우가 있는데, 이는 Twelve-Factor를 위반함

    • 12-Factor : Heroku 플랫폼을 통해 방대한 앱의 개발, 운영, 확장 등을 관찰한 많은 사람들이 고안해낸 SaaS 개발 방법론
    • 12-Factor를 준수할 경우 아래 SaaS의 특징을 갖출 수 있음
    • 12-Factor는 애플리케이션이 Cloud 환경에서 올바르게 동작하기 위해서 지켜야 하는 12가지 규칙을 말함
  • 12-Factor App은 설정을 환경 변수(env / envvars라고도 부름)에 저장함

    • 환경 설정은 애플리케이션 코드에서 분리되어 관리되어야 함
  • 환경 변수는 코드 변경 없이 배포 때마다 쉽게 변경 가능해야 함

  • 이전에 활용한 방법

    • .ini, .yaml 파일 등으로 config 설정
      • 쉬운 환경 설정
      • 환경에 대한 설정 코드를 하드코딩하는 형태이므로 변경 사항이 생길 때 유연한 코드 변경이 어려움
      • (ex) 환경 변수를 하나 더 추가해야 할 경우, config 파일과 Python 파일도 동시에 수정해야 함
    • flask-style config.py
      • Config 클래스에서 yaml, ini 파일을 불러와 주입하는 과정을 미리 구현한 것
      • Config 클래스의 정보를 오버라이딩해서 활용
      • Validation, Overriding을 위해서는 코드량의 증가가 불가피
  • Pydantic Base Setting

    • BaseSettings를 상속한 클래스에서 Type Hint로 주입된 설정 데이터 검증 가능
      • BaseModel에서 자동으로 Validation으로 수행되듯이, BaseSettings도 조건을 달면 Validation을 수행함
    • Field 클래스의 env인자를 통해 환경변수로부터 필드를 오버라이딩 할 수 있음
      • env = 'db_host'일 때, os.environ['db_host'] = 'mysql' 명령어를 통해 원래 환경 변수값을 오버라이딩 할 수 있음
    • yaml, ini 파일들을 추가적으로 만들지 않고 .env 파일들을 환경별로 만들어 두거나 실행 환경에서 유연하게 오버라이딩 할 수 있음
  • 코드

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'로 바꾼다는 것을 알림

Event Handler

Event Handler란?

  • 이벤트가 발생했을 때 처리를 담당하는 함수

  • FastAPI에선 Application이 실행될 때와 종료될 때 함수를 실행할 수 있음

    • 시작할 때 : @app.on_event('startup')
    • 종료될 때 : @app.on_event('shutdown')

Event Handler 코드

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)
  • 시작할 때 "Hello!"를 출력하고, 종료될 때 "Bye!"를 출력하는 코드

  • 위 사진을 보면, 2번째 INFO 아래에 Hello!가 뜬 이후 'https://0.0.0.0:8080' 에 접속할 수 있음을 알렸고, 아래서 3번째 INFO에서 shutdown을 기다리고 있다는 알람이 뜬 이후 Bye!를 출력하고 Shutdown이 수행됨을 볼 수 있다

API Router

API Router란?

  • 큰 애플리케이션에서 많이 활용되는 기능

  • API Endpoint를 정의

  • 기존에 사용하는 @app.get, @app.post를 활용하지 않고 router파일을 따로 설저앟여 app에 import해서 활용

  • 실제로 Router를 활용할 때는 하나의 파일에 저장하지 않고, 1개의 파일마다 1개의 Router를 만들어서 각각 저장하는 경우가 많음

    • cafe에 관한 Router, Blog에 관한 Router를 만들 때 cafe에 관한 Router와 연결된 함수는 Cafe에 관련된 메서드만 존재할 것이고, Blog에 관한 Router와 연결된 함수는 Blog에 관련된 메서드만 존재할 것이다.
      즉, 모듈화라는 측면에서 봤을 때 아예 다른 Python 파일에서 코딩하는 것이 좋은 것이다

API Router 활용 이유

네이버를 예를 들어보자.
네이버는 blog에 대한 사이트는 naver.com/blog/~로 링크가 되어 있으며, Cafe에 대한 사이트는 naver.com/cafe/~ 형태로 링크가 구성되어 있다.
그렇다면, 이 기능은 blog와 연관이 있느니 get('/blog/~')로 만들고, cafe와 연관이 있으면 get('/cafe/~')로 일일히 다 타이핑할까?
물론, 하면 되긴 한다. 하지만 사람은 언제나 실수를 하는 존재로, 그러다 Cafe와 연관이 있는데 /cafe를 앞에 붙이지 않거나 하는 실수가 일어날 수 있고, 이런 경우 문제가 꼬이고 꼬여서 폭발해버릴 수도 있을 것이다.
개발자는 생각했다.
"아 애초에 그냥 Cafe와 연관된 기술은 /cafe를 앞에 자동으로 붙이는 기술을 만들면 되지 않을까?"
그렇게 해서 만들어진, Endpoint(위 예시에선 /blog, /cafe일 것이다)를 자동으로 정의해주는 기능이 API Router이며, 이런 실수를 방지하는데 매우 효과적이다

API Router 코드

  • 여기에선 Router의 코드 이해를 극대화하기 위해 2개의 Router를 1개의 Python에 모두 담았다
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 모듈을 import 하고 활용해야 함
    • APIRouter(prefix='/blog') 명령어를 통해 '/blog'라는 Endpoint를 지정해 줄 수 있음
    • app.include_router() 명령어를 통해 생성한 APIRouter를 FastAPI 객체에 추가시켜 줘야 함
      • Fast API 측면에서도 해당 Router를 활용해야한다는 것을 인지해야 함

  • 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 Handling

Error Handling 개념

  • 웹 서버를 안정적으로 위해 반드시 수행해야 하는 행동

  • 서버에서 Error가 발생했을 경우 Error 내용을 파악할 수 있어야 하고 요청한 Client에 Error에 대한 정보를 전달하여 대응할 수 있어야 함

  • 서버 개발자는 모니터링 도구를 활용해 Error Log를 수집해야 함

  • 발생하는 오류를 빠르게 수정할 수 있도록 예외 처리를 잘 해야 함

  • (ex) 회원가입 시 404 Error가 발생했다! 라는 것만 알린다면, 유저는 사이트를 잘못 접속한것인지, 입력값이 잘못된 것인지 알 수가 없으므로 User 입장에서는 어떤 부분을 고쳐야할지 모르기 때문에 제품 만족도가 많이 떨어짐

  • 즉, 에러 메시지와 에러의 이유를 보안을 지키는 선에서 Client에게 전달할 수 있도록 코드를 작성해야 하며, 이를 Error Handling이라 함

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 ~ 31개의 값을 선택 해야 합니다!")

    return item

if __name__=='__main__':
    uvicorn.run(app, host='0.0.0.0', port = 8000)
  • item_id가 1,2,3이 아닌 다른 값이 올 경우 에러가 발생할 것이다. 따라서 이 부분에 대한 Error Handling이 필요하다

  • raise HttpException

    • Erorr Handling을 해주는 부분
    • status_code : Client에게 반환해 줄 응답 코드
    • detail : Client에게 보여줄 에러 메시지


Background Tasks

Background Tasks 개념

  • Fast API는 Starlett이라는 비동기 프레임워크를 래핑하므로, Background에서 작업을 수행시킬 수 있음

  • Background Tasks : 시간이 오래 걸리는 작업들을 Background에서 실행하도록 하는 것

  • Online Serving시에는 CPU 사용이 많은 작업들을 Background Task로 사용하면 클라이언트는 작업 완료를 기다리지 않고 즉시 Response를 받을 수 있음

    • (ex) A 작업과 B 작업이 큰 연관이 없는데 A -> B 순으로 코딩이 되어 있고 A가 시간이 오래 걸릴 경우, A작업을 Background에서 실행시키고 B 작업을 Front에서 실행시켜 먼저 결괌물을 내도록 함
  • Background Task를 통해 시간이 오래 걸려 수행된 결과물을 DB 등에 저장하고, 나중에 Search를 통해 DB에서 결과물을 찾아오는 형식으로 Task 완료 여부를 확인할 수 있음

Background Tasks 코드

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)
      • function : Background Task로 처리할 Function
      • Parameters : function에 줄 Parameter들

  • I want to return this first! 문구가 나중에 실행되도록 코드 상에는 구현되어 있지만, waiting function은 Background에서 실행되도록 설정해놨으므로 먼저 출력되고, 이후 waiting() 메서드가 모두 실행되었을 때 Wake Up!!!!!이 출력되는 것이다

  • Parameter는 int형이므로 4를 전달해줬고, 4초 이후 Wake Up!!! 문구가 출력된다

profile
개념부터 확실히!
post-custom-banner

0개의 댓글