새소식

부스트캠프 AI Tech 4기

[Product Serving Part.5] FastAPI 기초-2 / Pydantic

  • -

Path Parameter vs Query Parameter

  • 웹에서 GET Method를 사용해 데이터 전송 가능
  • ID가 402인 사용자 정보를 가져오고 싶을 때는?
    • Path Parameter 방식
      • /users/402
      • 서버에 402라는 값을 전달하고 변수로 사용
      • 해당 경로에 존재하는 내용이 없으면 404 Error 발생
    • Query Parameter 방식
      • /users?id=402
      • Query String
      • API 뒤에 입력 데이터를 함께 제공하는 방식으로 사용
      • Query String은 Key, Value의 쌍으로 이루어지며 &로 연결해 여러 데이터를 넘길 수 있음
      • 데이터가 없는 경우 빈 리스트가 나옴 → Error Handling이 필요
  • 언제 어떤 방식을 사용해야 할까?
    • Resource를 식별해야 할 경우 → Path Parameter가 더 적합
    • 정렬, 필터링을 해야 하는 경우 → Query Parameter가 더 적합

 

 

Path Parameter

GET Method : 정보를 READ

유저 정보에 접근하는 API 만들기

  • Get Method의 인자로 있는 {user_id}가 함수의 값으로 입력
from fastapi import FastAPI
import uvicorn

app = FastAPI()

@app.get("/users/{user_id}") 
def get_user(user_id) 
	return {"user_id" : user_id}

if __name__ == "__main__":
	uvicorn.run(app, host="0.0.0.0", port=8000)

 

 

 

Query Parameter

GET Method : 정보를 READ

from fastapi import FastAPI
import uvicorn

# FastAPI 객체 생성
app = FastAPI()

fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]

@app.get("/items/")
def read_item(skip: int = 0, limit: int = 10):
    return fake_items_db[skip : skip + limit]

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)

 

  • URL 뒤에 ?를 붙이고 Key, Value 형태로 연결

 

  • skip=20일 경우는 fake_db_items[20:10]이므로 빈 리스트 출력

 

 

Optional Parameter

특정 파라미터는 선택적으로 설정하고 싶은 경우

  • typing 모듈의 Optional을 사용해 특정 파라미터가 Optional임을 명시
from fastapi import FastAPI
from typing import Optional
import uvicorn

# FastAPI 객체 생성
app = FastAPI()

fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]

@app.get("/items/{item_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)

q가 없는 경우
q가 있는 경우

 

 

 

Request Body

Request Body에 데이터를 보내고 싶다면 POST Method를 사용

  • Body의 데이터를 설명하는 Content-Type이란 Header 필드가 존재하고, 어떤 데이터 타입인지 명시해야 함
  • 대표적인 컨텐츠 타입
    • application/x-www-form-urlencoded : BODY에 Key, Value 사용, & 구분자 사용
    • text/plain : 단순 txt 파일
    • multipartform-data : 데이터를 바이너리 데이터로 전송

 

POST 요청으로 item 생성

  • pydantic으로 Request Body 데이터 정의
  • Type Hinting에 위에서 생성한 Class 주입

 

  • localhost:8000/docs로 이동
  • Schemas에서 pydantic으로 정의한 내용을 볼 수 있음

 

  • POST 클릭하고 Try it out 클릭

 

  • 기본 설정 상태에서 값을 바꿔보고 Execute

 

 

 

Request Body

Decorator의 response_model 인자로 설정 가능

  • Output Data를 해당 정의에 맞게 변형
  • 데이터 Validation
  • Response에 대한 Json Schema 추가
  • 자동으로 문서화

 

 

  • Request와 Response가 다름을 확인 가능

 

 

 

Form

Form(입력) 형태로 데이터를 받고 싶은 경우 python-multipart 설치 필요

pip install python-multipart

 

프론트도 간단히 만들기 위해 Jinja2 설치

pip install Jinja2

 

 

  • Form 클래스를 사용하면 Request의 Form Data에서 값을 가져옴
  • post로 username과 password를 입력하면 username을 반환하도록 설정
    그러나 이 부분만 작성하고 localhost:8000/login/으로 이동하면 안됨
    url 요청은 GET method로만 가능하기 때문
  • 프론트엔드를 사용하기 위해 다음과 같이 코드를 작성
    Jinja Template에선 {{ }} 표현을 사용해 데이터 사용 가능

login_form.html

 

 

 

  • localhost:8000/login/

  • 제출을 누르면 POST 요청이 실행

 

 

  • Form(...) 에서의 '...'는 무엇일까?
    • Python ellipsis : Required를 의미

 

 

File

  • File 업로드하고 싶은 경우
  • python-multipart를 설치

 

 

 

Pydantic

  • FastAPI에서 Class 사용할 때 보이던 Pydantic
  • Data Validation / Settings Management 라이브러리
  • Type Hint를 런타임에서 강제해 안전하게 데이터 핸들링 
  • 파이썬 기본 타입(String, Int 등) + List, Dict, Tuple에 대한 Validation 지원
  • 기존 Validation 라이브러리보다 빠름 (Benchmark)
  • Config를 효과적으로 관리하도록 도와줌
  • 머신러닝 Feature Data Validation으로도 활용 가능

 

1. Validation

Validation Check Logic

조건 1: 올바른 url을 입력 받음 (url)

조건 2: 1-10 사이의 정수 입력 받음 (rate)

조건 3: 올바른 폴더 이름을 입력 받음(target_dir)

 

사용할 수 있는 방법

1) 일반 Python Class를 활용한 Input Definition 및 Validation

2) Dataclass를(python 3.7 이상 필요) 활용한 Input Definition 및 Validation

3) Pydantic을 활용한 Input Definition 및 Validation

 

 

Python Class 활용 시

  • Python Class로 Input Definition 및 Validation →  의미 없는 코드가 많아짐
  • 복잡한 검증 로직엔 Class Method가 복잡해지기 쉬움
  • Exception Handling을 어떻게 할지 등 커스텀하게 제어할 수 있는 있지만 메인 로직(Input을 받아서 Inference를 수행하는)에 집중하기 어려워짐
class ModelInput01:
    url: str
    rate: int
    target_dir: str

    def __init_(self, url: str, rate: int, target_dir: str):
        self.url = url
        self.rate = rate
        self.target_dir = target_dir
    
    ...
    
    def validate(self) -> bool:
        """클래스 필드가 올바른지 검증

        Returns:
            bool: 검증 실패/성공 여부
        """
        validation_results = [self._validate_url(self.url), 1 <= self.rate <= 10, self._validate_target_dir(self.target_dir)]
        return all(validation_results)

 

DataClass 활용 시

  • 인스턴스 생성 시점에서 Validation을 수행하기 쉬움
  • 여전히 Validation 로직들을 직접 작성해야 함
  • Validation 로직을 따로 작성하지 않으면, 런타임에서 type checking을 지원하지 않음
from dataclasses import dataclass

@dataclass # dataclass decorator => init method를 따로 작성할 필요 없음
class ModelInput02:
    url: str
    rate: int
    target_dir: str

    ...

    def __post_init__(self): # post init method => dataclass의 __init__ method와 같은 역할
        if not self.validate(): # 여전히 validate method를 따로 구현해야 함
            raise ValueError("Invalid input")

 

Pydantic 활용 시

  • 훨씬 간결해진 코드
  • 주로 쓰이는 타입들(http url, db url, enum 등)에 대한 Validation이 만들어져 있음
  • 런타임에서 Type Hint에 따라서 Validation Error 발생
  • Custom Type에 대한 Validation도 쉽게 사용 가능
  • 참고: https://pydantic-docs.helpmanual.io/usage/types/
from pydantic import BaseModel, HttpUrl, Field, DirectoryPath

class ModelInput03(BaseModel):
    url: HttpUrl
    rate: int = Field(ge=1, le=10)
    target_dir : DirectoryPath

 

 

2. Config 관리

  • 애플리케이션은 종종 설정을 상수로 코드에 저장함
    → Twelve-Factor를 위반
  • Twelve-Factor는 설정을 코드에서 엄격하게 분리하는 것을 요구함
  • Twelve-Factor App은 설정을 환경 변수(envvars나 env라고도 불림)에 저장함
  • 환경 변수는 코드 변경 없이 쉽게 배포 때마다 쉽게 변경할 수 있음
  • The Twelve-Factor App이라는 SaaS(Software as a Service)를 만들기 위한 방법론을 정리한 규칙들에 따르면, 환경 설정은 애플리케이션 코드에서 분리되어 관리되어야 함
    참고 글: https://12factor.net/ko/config

 

.yaml 파일 등으로 config 설정하기

  • yaml로 환경 설정을 관리할 경우, 쉽게 환경을 설정할 수 있지만, 환경에 대한 설정을 코드 하드코딩하는 형태
    → 변경 사항이 생길 때 유연하게 코드를 변경하기 어려움
import os
from typing import Dict, Any

dev_config_yaml = """
env: dev
db:
  username: user
  password: user
  host: localhost
  port: 3306
  database: dev
"""
with open("dev_config.yaml", "w") as f:
    f.write(dev_config_yaml)

# 1. .yaml 파일 기반 config 주입
from yaml import load, FullLoader


def load_config(config_path: str) -> Dict[str, Any]:
    """
    config YAML 파일을 로드
    Args:
        config_path: config YAML 파일 경로
    Returns:
        Dict[str, Any]: Config dictionary
    """
    with open(config_path, "r") as f:
        config = load(f, FullLoader)
    return config


config = load_config(config_path="dev_config.yaml")

assert config["env"] == "dev"
expected = {"username": "user", "password": "user", "host": "localhost", "port": 3306, "database": "dev"}
assert config["db"] ==

 

flask-style config.py

  • Config 클래스에서 yaml 파일을 불러와 python class 필드로 주입하는 과정을 구현
  • Config를 상속한 클래스에서는 Config 클래스의 정보를 오버라이딩해서 사용
  • 하지만 해당 파일의 데이터가 정상적인지 체크하거나(Validation) 또는 환경 변수로 부터 해당 필드를 오버라이딩(Overriding) 하려면 코드량이 늘어남
# 2. flask style config
class Config(object):
    ENV: str = None
    TESTING: bool = False
    DB: Dict[str, Any] = {}

    @classmethod
    def from_yaml(cls, config_path: str):
        import yaml

        with open(config_path, "r") as config_file:
            config = yaml.load(config_file, Loader=yaml.FullLoader)

        cls.ENV = config["env"]
        cls.DB = config["db"]
        return cls


class DevConfig(Config):
    pass


class ProdConfig(Config):
    pass


config = DevConfig.from_yaml("dev_config.yaml")
assert config.ENV == "dev"
assert config.DB == expected

 

 

pydantic setting

  • Validation처럼 Pydantic은 BaseSettings를 상속한 클래스에서 Type Hint로 주입된 설정 데이터를 검증할 수 있음 
  • Field 클래스의 env 인자로, 환경 변수로 부터 해당 필드를 오버라이딩 할 수 있음
  • yaml 파일들을 추가적으로 만들지 않고, .env 파일들을 환경별로 만들어 두거나, 실행 환경에서 유연하게 오버라이딩 할 수 있음

 

# 3. pydantic base settings
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")
    username: str = Field(default="user", env="db_username")
    password: str = Field(default="user", env="db_password")
    database: str = Field(default="dev", env="db_database")


class AppConfig(BaseSettings):
    env: ConfigEnv = Field(default="dev", env="env")
    db: DBConfig = DBConfig()


with open("dev_config.yaml", "r") as f:
    config = load(f, FullLoader)

config_with_pydantic = AppConfig(**config)

assert config_with_pydantic.env == "dev"
assert config_with_pydantic.db.dict() == expected

# 환경 변수로 필드를 오버라이딩합니다.
os.environ["ENV"] = "prod"
os.environ["DB_HOST"] = "mysql"
os.environ["DB_USERNAME"] = "admin"
os.environ["DB_PASSWORD"] = "SOME_SAFE_PASSWORD"

prod_config_with_pydantic = AppConfig()
assert prod_config_with_pydantic.env == "prod"
assert prod_config_with_pydantic.dict() != expected

# cleanup
os.remove("dev_config.yaml")

 

실무에서는 무조건 pydantic을 활용하기 보다는, 각 팀에서 맞는 방법을 따라가는 것이 좋음

728x90
Contents