{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "7f204b64",
   "metadata": {},
   "source": [
    "---\n",
    "marp: true\n",
    "theme: gaia\n",
    "class:\n",
    "  - lead\n",
    "  - invert\n",
    "---\n",
    "\n",
    "<!-- _class: lead -->\n",
    "<!-- _header: \"\" -->\n",
    "<!-- _footer: \"\" -->\n",
    "\n",
    "<style>\n",
    "h1 {\n",
    "    font-size: 2.5em;\n",
    "    font-weight: 600;\n",
    "}\n",
    "h2 {\n",
    "    font-size: 2em;\n",
    "    font-weight: 600;\n",
    "}\n",
    "h3 {\n",
    "    font-size: 1.6em;\n",
    "    font-weight: 600;\n",
    "}\n",
    "h4 {\n",
    "    font-size: 1.3em;\n",
    "    font-weight: 600;\n",
    "}\n",
    "h5 {\n",
    "    font-size: 1.1em;\n",
    "    font-weight: 600;\n",
    "}\n",
    "h6 {\n",
    "    font-size: 1em;\n",
    "    font-weight: 600;\n",
    "}\n",
    "p, li, ul, ol, table {\n",
    "    font-size: 28px;\n",
    "}\n",
    "pre, code {\n",
    "    font-size: 24px;\n",
    "}\n",
    "</style>\n",
    "\n",
    "# Part 8. FastAPI를 이용한 모델 서빙\n",
    "\n",
    "**⬅️ 이전 시간: [Part 7.5: 강화학습 (Reinforcement Learning)](../07_deep_learning/part_7.5_reinforcement_learning.md)**<br/>\n",
    "**➡️ 다음 시간: [Part 9: 프로덕션 레벨 API와 Docker](../09_production_ready_api/part_9_production_ready_api.md)**\n",
    "\n",
    "---\n",
    "\n",
    "## 1. 학습 목표 (Learning Objectives)\n",
    "\n",
    "이번 파트가 끝나면, 여러분은 다음을 할 수 있게 됩니다.\n",
    "\n",
    "- API의 개념을 '레스토랑 주문'에 비유하여 설명할 수 있습니다.\n",
    "- FastAPI를 사용하여 기본적인 웹 서버를 구축하고 `uvicorn`으로 실행할 수 있습니다.\n",
    "- `lifespan` 이벤트를 사용하여 서버 시작 시 머신러닝 모델을 효율적으로 로드할 수 있습니다.\n",
    "- Pydantic을 사용하여 API의 요청/응답 데이터 형식을 정의하고 자동으로 검증할 수 있습니다.\n",
    "- FastAPI의 자동 생성 문서(Swagger UI)를 통해 브라우저에서 직접 API를 테스트하고 디버깅할 수 있습니다.\n",
    "\n",
    "---\n",
    "\n",
    "## 2. 핵심 키워드 (Keywords)\n",
    "\n",
    "`모델 서빙(Model Serving)`, `API`, `FastAPI`, `uvicorn`, `Pydantic`, `경로 매개변수(Path Parameter)`, `요청 본문(Request Body)`, `Swagger UI`, `lifespan`\n",
    "\n",
    "---\n",
    "\n",
    "## 3. 도입 (Introduction)\n",
    "\n",
    "지금까지 우리는 Scikit-learn과 PyTorch로 강력한 AI 모델, 즉 세상에 없는 '비법 레시피'를 개발했습니다. 하지만 이 레시피를 내 서랍 속에만 둔다면 아무도 그 맛을 볼 수 없습니다.\n",
    "\n",
    "> **API 서버는 이 비법 레시피로 요리를 만들어 손님에게 판매하는 '레스토랑'을 차리는 과정입니다.**\n",
    "> 손님(클라이언트)은 주방(모델 서버)이 어떻게 돌아가는지 몰라도, 메뉴판(API 문서)을 보고 주문하면 맛있는 요리(예측 결과)를 받을 수 있습니다.\n",
    "\n",
    "이번 주차에는 파이썬의 최신 웹 프레임워크인 **FastAPI**를 이용해, 빠르고 세련된 'AI 레스토랑'을 여는 방법을 배워보겠습니다.\n",
    "\n",
    "> [!TIP]\n",
    "> 본 파트의 모든 예제 코드는 `../../source_code/part_8_fastapi_project` 폴더에서 직접 실행하고 수정해볼 수 있습니다.\n",
    "\n",
    "---\n",
    "\n",
    "## 4. FastAPI로 우리의 AI 레스토랑 개점하기\n",
    "\n",
    "> **🎯 1-2일차 목표:** FastAPI와 uvicorn으로 서버를 실행하고, Pydantic으로 데이터 형식을 정의합니다.\n",
    "\n",
    "### 4-1. 왜 FastAPI일까?\n",
    "- **엄청난 속도**: 주문이 밀려들어도 막힘없이 처리하는 '베테랑 직원'처럼 매우 빠릅니다.\n",
    "- **쉬운 메뉴 관리**: '레시피(코드)'가 간결하고 명확해서, 실수를 줄이고 새로운 메뉴를 빠르게 추가하기 쉽습니다.\n",
    "- **스마트 메뉴판 자동 생성**: 코드를 짜면 손님들이 직접 보고 테스트할 수 있는 자동 대화형 문서(Swagger UI)를 공짜로 만들어줍니다.\n",
    "\n",
    "### 4-2. 서버 실행과 데이터 형식 정의\n",
    "먼저 FastAPI와 서버 매니저 `uvicorn`을 설치합니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "434e9271",
   "metadata": {},
   "outputs": [],
   "source": [
    "pip install fastapi \"uvicorn[standard]\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "324d1f3b",
   "metadata": {},
   "source": [
    "\n",
    "이제 `main.py` 파일에 레스토랑의 기본 틀을 잡고, 손님의 '주문서 양식'을 **Pydantic**으로 정의합니다.\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b3ad3372",
   "metadata": {},
   "outputs": [],
   "source": [
    "# main.py\n",
    "from fastapi import FastAPI\n",
    "from pydantic import BaseModel\n",
    "from typing import List\n",
    "\n",
    "# 1. Pydantic으로 '주문서 양식' 정의\n",
    "class IrisFeatures(BaseModel):\n",
    "    features: List[float]\n",
    "\n",
    "    class Config: # 스마트 메뉴판에 보여줄 주문 예시\n",
    "        json_schema_extra = {\"example\": {\"features\": [5.1, 3.5, 1.4, 0.2]}}\n",
    "\n",
    "# 2. FastAPI 인스턴스 생성 (레스토랑 개점)\n",
    "app = FastAPI()\n",
    "\n",
    "# 3. API 엔드포인트 정의 (메뉴판에 메뉴 추가)\n",
    "@app.get(\"/\")\n",
    "def read_root():\n",
    "    return {\"message\": \"어서오세요! 붓꽃 품종 예측 레스토랑입니다!\"}\n",
    "\n",
    "# 주문(POST 요청)을 받는 엔드포인트 - 아직 내용은 비어있음\n",
    "@app.post(\"/predict\")\n",
    "def predict_iris(data: IrisFeatures):\n",
    "    return {\"주문 접수 완료\": data.features}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "37c07c1d",
   "metadata": {},
   "source": [
    "\n",
    "- `@app.get(\"/\")`: 손님이 가게 정문(`/`)으로 들어와 **\"여기는 뭐하는 곳인가요?\"(GET 요청)**라고 물어보면 응답합니다.\n",
    "- `@app.post(\"/predict\")`: `/predict` 창구에서는 **\"이 재료로 요리해주세요\"(POST 요청)**라는 주문을 받습니다.\n",
    "- `data: IrisFeatures`: 손님의 주문(JSON)을 우리가 정의한 `IrisFeatures` 양식에 맞춰 자동으로 확인하고, 형식이 틀리면 FastAPI가 알아서 오류를 보냅니다.\n",
    "\n",
    "이제 터미널에서 서버를 실행합니다. `--reload`는 코드가 바뀔 때마다 서버를 자동으로 재시작해주는 편리한 옵션입니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1c1c4eda",
   "metadata": {},
   "outputs": [],
   "source": [
    "uvicorn main:app --reload"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "034369e4",
   "metadata": {},
   "source": [
    "\n",
    "서버 실행 후, 웹 브라우저에서 `http://127.0.0.1:8000` 주소로 접속하여 환영 메시지를 확인하세요.\n",
    "\n",
    "---\n",
    "\n",
    "## 5. 예측 API 구현: 레스토랑의 시그니처 메뉴 만들기\n",
    "\n",
    "> **🎯 3-4일차 목표:** 6주차에서 만든 붓꽃 예측 모델을 FastAPI에 탑재하여, 실제 예측 결과를 반환하는 API를 완성합니다.\n",
    "\n",
    "### 5-1. 모델 로딩 최적화: `lifespan` 이벤트\n",
    "무거운 주방기구(모델)를 손님이 올 때마다 켜는 것은 비효율적입니다. 서버가 시작될 때 **단 한번만** 모델을 메모리에 로드하도록 `lifespan` 이벤트를 사용합니다.\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "949bab81",
   "metadata": {},
   "outputs": [],
   "source": [
    "# main.py (수정 및 추가)\n",
    "from contextlib import asynccontextmanager\n",
    "import joblib\n",
    "import numpy as np\n",
    "\n",
    "# ... (IrisFeatures 정의는 그대로) ...\n",
    "\n",
    "MODELS = {} # 모델을 저장할 딕셔너리\n",
    "\n",
    "@asynccontextmanager\n",
    "async def lifespan(app: FastAPI):\n",
    "    # 서버 시작 시 (가게 오픈)\n",
    "    print(\"레스토랑 오픈: 모델을 준비합니다.\")\n",
    "    MODELS[\"iris_model\"] = joblib.load(\"iris_model.pkl\")\n",
    "    print(\"모델 준비 완료!\")\n",
    "    \n",
    "    yield # 여기서 애플리케이션이 실행됨\n",
    "\n",
    "    # 서버 종료 시 (가게 마감)\n",
    "    MODELS.clear()\n",
    "    print(\"레스토랑 마감\")\n",
    "\n",
    "app = FastAPI(lifespan=lifespan)\n",
    "# ... (@app.get(\"/\") 정의는 그대로) ..."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4d4789c6",
   "metadata": {},
   "source": [
    "\n",
    "\n",
    "### 5-2. 예측 엔드포인트 완성\n",
    "이제 `/predict` 창구에서 실제 요리(예측)를 하고 결과를 반환하도록 코드를 완성합니다.\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9b576cc5",
   "metadata": {},
   "outputs": [],
   "source": [
    "# main.py (@app.post(\"/predict\") 수정)\n",
    "\n",
    "@app.post(\"/predict\")\n",
    "def predict_iris(data: IrisFeatures):\n",
    "    \"\"\"\n",
    "    붓꽃의 특징 4가지를 주문서로 받아, 예측된 품종과 신뢰도 점수를 반환합니다.\n",
    "    \"\"\"\n",
    "    model = MODELS[\"iris_model\"]\n",
    "    \n",
    "    # 주방(모델)이 알아듣는 형태로 재료 손질\n",
    "    model_input = np.array(data.features).reshape(1, -1)\n",
    "    \n",
    "    # 요리(예측) 시작\n",
    "    prediction_idx = model.predict(model_input)[0]\n",
    "    prediction_proba = model.predict_proba(model_input)[0]\n",
    "\n",
    "    # 결과 플레이팅\n",
    "    class_names = ['setosa', 'versicolor', 'virginica']\n",
    "    class_name = class_names[prediction_idx]\n",
    "    confidence = prediction_proba[prediction_idx]\n",
    "\n",
    "    return {\n",
    "        \"predicted_class\": class_name,\n",
    "        \"confidence\": float(confidence)\n",
    "    }"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5e52f42d",
   "metadata": {},
   "source": [
    "\n",
    "> **[필수]** 6주차 실습에서 저장한 `iris_model.pkl` 파일을 `main.py`와 같은 폴더에 준비하고, `scikit-learn`, `joblib`을 설치해주세요.\n",
    "> `pip install scikit-learn joblib numpy`\n",
    "\n",
    "---\n",
    "\n",
    "## 6. 캡스톤 프로젝트 연계 미니 프로젝트: 나만의 AI 모델 API로 서빙하기\n",
    "\n",
    "이론은 충분합니다. 이제 직접 'AI 레스토랑'의 오너가 되어, 여러분이 직접 만든 모델을 세상에 선보일 시간입니다. 이번 미니 프로젝트는 6장, 7장에서 직접 만들었던 모델 중 하나를 골라, FastAPI를 사용해 실제 호출 가능한 API로 만드는 전체 과정을 경험합니다.\n",
    "\n",
    "### 프로젝트 목표\n",
    "- 이전 챕터에서 직접 학습시킨 나만의 모델(Scikit-learn 또는 PyTorch)을 API로 서빙할 수 있습니다.\n",
    "- 모델의 입력과 출력에 맞는 Pydantic 모델을 직접 설계하고 정의할 수 있습니다.\n",
    "- `lifespan` 이벤트를 사용하여, Scikit-learn 모델(.pkl)과 PyTorch 모델(.pth)을 각각의 방식에 맞게 로드하는 코드를 작성할 수 있습니다.\n",
    "- FastAPI의 자동 문서(`/docs`)를 사용하여, 직접 만든 API를 테스트하고 다른 사람에게 사용법을 공유할 수 있습니다.\n",
    "\n",
    "### 선택 가능한 모델 (아래 중 1개 선택)\n",
    "1.  **[6장] 붓꽃 품종 분류 모델 (`iris_model.pkl`)**: 가장 간단한 옵션. 본문 예제와 거의 유사합니다.\n",
    "2.  **[7장] IMDB 영화 리뷰 감성 분석 모델 (`sentiment_model.pth`)**: PyTorch 모델을 서빙하는 실전 경험을 쌓을 수 있습니다.\n",
    "3.  **[7.4장] GNN 친구 추천 모델 (GCN 모델)**: 조금 더 도전적인 과제. 그래프 데이터를 입력으로 받아 추천 목록을 반환하는 API를 설계해봅니다.\n",
    "\n",
    "### 개발 과정 (IMDB 감성 분석 모델 기준)\n",
    "1.  **프로젝트 구조 설정**:\n",
    "    - `my_api/` 폴더를 만들고, 7장에서 학습시킨 `sentiment_model.pth`와 토크나이저(Tokenizer) 관련 파일(`vocab.pkl` 등)을 복사해 넣습니다.\n",
    "    - `main.py` 파일을 생성합니다.\n",
    "\n",
    "2.  **Pydantic 모델 정의 (`main.py`)**:\n",
    "    "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "419c52c9",
   "metadata": {},
   "outputs": [],
   "source": [
    "from pydantic import BaseModel\n",
    "\n",
    "    class ReviewRequest(BaseModel):\n",
    "        text: str\n",
    "\n",
    "        class Config:\n",
    "            json_schema_extra = {\"example\": {\"text\": \"This movie was fantastic!\"}}\n",
    "\n",
    "    class SentimentResponse(BaseModel):\n",
    "        sentiment: str\n",
    "        score: float"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cf34593b",
   "metadata": {},
   "source": [
    "\n",
    "\n",
    "3.  **PyTorch 모델 로딩 로직 구현 (`main.py`)**:\n",
    "    - 7장에서 사용했던 PyTorch 모델 클래스(`SentimentClassifier`)와 토크나이저 로딩 코드를 가져옵니다.\n",
    "    - `lifespan` 컨텍스트 매니저 안에 모델과 토크나이저를 전역 변수로 로드하는 코드를 작성합니다. `model.load_state_dict()`, `model.eval()`을 사용하는 것을 잊지 마세요.\n",
    "\n",
    "4.  **예측 엔드포인트 구현 (`main.py`)**:\n",
    "    "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f7b70b75",
   "metadata": {},
   "outputs": [],
   "source": [
    "@app.post(\"/predict\", response_model=SentimentResponse)\n",
    "    def predict_sentiment(request: ReviewRequest):\n",
    "        # 1. 입력 텍스트를 토크나이저로 전처리\n",
    "        # 2. 전처리된 데이터를 PyTorch 텐서로 변환\n",
    "        # 3. 모델에 입력하여 예측 수행 (with torch.no_grad():)\n",
    "        # 4. 모델의 출력(logit)을 확률(softmax)로 변환하고, 긍정/부정 레이블 결정\n",
    "        # 5. SentimentResponse 형식에 맞춰 결과 반환\n",
    "        ...\n",
    "        return SentimentResponse(sentiment=label, score=prob)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8a59db89",
   "metadata": {},
   "source": [
    "\n",
    "\n",
    "5.  **서버 실행 및 테스트**:\n",
    "    - `uvicorn main:app --reload`로 서버를 실행합니다.\n",
    "    - 브라우저에서 `http://127.0.0.1:8000/docs`에 접속합니다.\n",
    "    - `/predict` 엔드포인트의 `Try it out` 기능을 사용하여, 여러 가지 영화 리뷰 텍스트를 입력하고 모델이 감성을 잘 예측하는지 실시간으로 테스트합니다.\n",
    "\n",
    "### 캡스톤 프로젝트 연계 방안\n",
    "이 미니 프로젝트는 15장 캡스톤 프로젝트의 2단계, **'핵심 기능 개발'**의 축소판입니다. 여기서 작성한 코드는 여러분의 최종 프로젝트에서 그대로 재사용되거나 확장될 수 있습니다. 예를 들어, 지금 만든 감성 분석 API에 사용자 인증 기능을 추가하거나, 데이터베이스와 연동하여 예측 결과를 저장하는 식으로 발전시킬 수 있습니다.\n",
    "\n",
    "---\n",
    "\n",
    "## 7. 토론 주제 (Discussion Topics)\n",
    "\n",
    "FastAPI를 사용하면 매우 빠르게 API를 만들 수 있지만, 실제 프로덕션 환경에서는 간단한 예제 코드에서 보이지 않던 문제들이 발생합니다. 다음 상황들에 대해 함께 토론해보세요.\n",
    "\n",
    "1.  **`async` 키워드의 환상**\n",
    "    -   **상황**: 한 개발자가 무거운 머신러닝 모델의 예측 속도를 높이기 위해, 기존 `def predict(...):`를 `async def predict(...):`로 바꿨습니다. 그는 `async` 키워드만 붙이면 모델 예측이 자동으로 비동기 처리되어 서버의 다른 요청을 막지 않을 것이라고 기대했습니다. 하지만 여러 요청이 동시에 들어오자 서버는 여전히 응답이 느려졌습니다.\n",
    "    -   **토론**:\n",
    "        -   왜 `async` 키워드를 붙이는 것만으로는 CPU를 많이 사용하는 작업(CPU-bound, 예: `model.predict()`)이 빨라지지 않을까요? I/O-bound 작업과 CPU-bound 작업의 근본적인 차이는 무엇일까요?\n",
    "        -   무거운 모델 예측이 전체 서버를 차단(blocking)하는 것을 막기 위한 현실적인 해결책은 무엇이 있을까요? (예: `FastAPI.run_in_threadpool`의 역할, Celery와 같은 별도 작업 큐(Task Queue)의 필요성)\n",
    "\n",
    "2.  **모델 로딩 시점의 딜레마**\n",
    "    -   **상황**: 새로운 대용량 언어 모델(LLM)을 `lifespan` 이벤트에 추가했더니, 서버가 시작되는 데 2분이 넘게 걸리기 시작했습니다. 이로 인해 개발 환경에서 코드를 수정할 때마다 재시작을 기다리는 시간이 길어졌고, 클라우드 배포 시스템의 헬스 체크(health check) 시간 초과로 배포가 실패하는 문제가 발생했습니다.\n",
    "    -   **토론**:\n",
    "        -   서버 시작 시 모델을 로드하는 방식(`lifespan`)의 장점과 위와 같은 상황에서의 단점은 무엇일까요?\n",
    "        -   긴 모델 로딩 시간을 완화할 수 있는 전략들에는 어떤 것들이 있을까요? (예: 모델 최적화/양자화, 첫 요청 시 모델을 로드하는 'Lazy Loading' 패턴, Triton 같은 전문 추론 서버의 역할)\n",
    "\n",
    "3.  **전역 변수와 동시성 문제**\n",
    "    -   **상황**: 여러 버전의 모델을 서빙하기 위해, 실행 중에 모델을 교체하는 `/load_model`이라는 API 엔드포인트를 새로 만들었습니다. 이 엔드포인트는 전역 변수인 `MODELS` 딕셔너리의 모델 객체를 새로운 것으로 바꿉니다. 테스트 시에는 잘 동작했지만, 실제 서비스에서는 가끔 사용자들이 엉뚱한 버전의 모델로부터 예측 결과를 받는 문제가 발생했습니다.\n",
    "    -   **토론**:\n",
    "        -   웹 서버처럼 여러 요청이 동시에 처리되는 환경에서 `MODELS`와 같은 전역 변수를 직접 수정하는 것은 어떤 위험을 초래할까요? '경쟁 상태(Race Condition)'란 무엇일까요?\n",
    "        -   여러 모델을 안전하게 관리하고 서빙하기 위한 더 나은 설계 패턴은 무엇이 있을까요? (예: `lifespan`에서 모든 모델을 불러두고 경로 매개변수로 선택, 모델별로 별도의 서버/컨테이너로 배포)\n",
    "\n",
    "4.  **Pydantic을 생략했을 때의 비용**\n",
    "    -   **상황**: 개발 속도를 높이기 위해, 한 팀원이 새로운 엔드포인트를 만들면서 Pydantic 모델 정의를 생략하고, 대신 `await request.json()`으로 원시(raw) 데이터를 직접 다루기로 결정했습니다. 얼마 후, 이 엔드포인트는 간헐적으로 `KeyError`나 `TypeError`를 발생시키며 프로덕션 환경에서 실패하기 시작했습니다.\n",
    "    -   **토론**:\n",
    "        -   Pydantic은 단순히 데이터 타입을 변환해주는 것 외에, 우리를 위해 어떤 '보이지 않는' 중요한 일들을 해주고 있었을까요? (예: 필수 필드 검사, 타입 강제, 상세한 오류 메시지 리포팅 등)\n",
    "        -   Pydantic을 사용하여 명확한 데이터 계약(Data Contract)을 정의하는 것이 프론트엔드와 백엔드 개발자 간의 협업에 어떻게 도움이 될까요?\n",
    "\n",
    "---\n",
    "\n",
    "## 8. 되짚어보기 (Summary)\n",
    "\n",
    "이번 파트에서는 FastAPI를 사용하여 머신러닝 모델을 API로 서빙하는 방법을 배웠습니다. 주요 내용은 다음과 같습니다.\n",
    "- **FastAPI 기본**: `uvicorn`으로 서버를 실행하고, 경로 매개변수와 요청 본문을 처리하는 방법을 학습했습니다.\n",
    "- **Pydantic 모델**: API의 입출력 데이터 형식을 Pydantic으로 명확하게 정의하고, 데이터 유효성 검사를 자동화했습니다.\n",
    "- **모델 서빙**: `lifespan` 이벤트를 활용하여 서버 시작 시 모델을 효율적으로 로드하고, 예측 결과를 반환하는 API 엔드포인트를 구현했습니다.\n",
    "- **자동 API 문서**: FastAPI가 제공하는 Swagger UI를 통해 브라우저에서 직접 API를 테스트하고 사용법을 확인하는 방법을 익혔습니다.\n",
    "- **프로덕션 고려사항**: `async`의 올바른 사용법, 무거운 모델 로딩 문제, 동시성 이슈 등 실제 운영 환경에서 발생할 수 있는 문제점들을 토론했습니다.\n",
    "\n",
    "---\n",
    "\n",
    "## 9. 참고 자료 (References)\n",
    "\n",
    "- [FastAPI 공식 문서](https://fastapi.tiangolo.com/)\n",
    "- [Pydantic 공식 문서](https://pydantic-docs.helpmanual.io/)\n",
    "\n",
    "---\n"
   ]
  }
 ],
 "metadata": {},
 "nbformat": 4,
 "nbformat_minor": 5
}
