13장. REST API 와 FastAPI
실습 2 — FastAPI 로 예측 API 만들기
실습 1의 모델 파일을 쓰는 예측 API 를 out_delay_api.py 파일로 만들고, TestClient 로 서버 없이 불러 봅니다. 실습 4에서 이 파일을 실제 서버로 띄웁니다. fastapi 가 설치되어 있어야 하고, 실습 1을 먼저 실행해야 합니다.
1단계. API 파일 쓰기
from pathlib import Path
# 서버로 띄울 수 있게 파일로 만든다. 실습 3, 4가 이 파일을 쓴다
Path("out_delay_api.py").write_text('''"""배송 지연 예측 API. 실행: uvicorn out_delay_api:app --port 8013"""
from typing import Literal
import joblib
import pandas as pd
from fastapi import FastAPI
from pydantic import BaseModel, Field
BUNDLE = joblib.load("out_delay_model.joblib") # 서버가 뜰 때 한 번만 불러 둔다
PIPE = BUNDLE["파이프라인"]
app = FastAPI(title="배송 지연 예측 API", version=BUNDLE["판"])
class Order(BaseModel):
"""예측할 주문 하나. 칸마다 타입과 범위, 고를 수 있는 값을 정한다"""
주문시각: int = Field(ge=0, le=23, description="주문한 시각(0~23)")
거리km: float = Field(gt=0, le=1000)
무게kg: float = Field(ge=0, le=100)
지역: Literal["서울", "경기", "인천", "지방"]
상품분류: Literal["의류", "생활용품", "식품", "도서", "가전"]
날씨: Literal["맑음", "흐림", "비", "눈", "모름"] = "모름"
택배사: Literal["한결택배", "빠른별택배", "새벽길택배"]
class Batch(BaseModel):
주문들: list[Order] = Field(min_length=1, max_length=500)
def predict_rows(orders):
"""검사를 통과한 주문들을 학습 때와 같은 열의 표로 바꿔 확률을 계산한다"""
table = pd.DataFrame([o.model_dump() for o in orders])[BUNDLE["숫자열"] + BUNDLE["범주열"]]
probs = PIPE.predict_proba(table)[:, 1]
t = BUNDLE["기준값"]
return [{"지연확률": round(float(p), 4), "지연예상": bool(p >= t)} for p in probs]
@app.get("/health")
def health():
return {"상태": "ok", "판": BUNDLE["판"]}
@app.get("/model")
def model_info():
return {k: BUNDLE[k] for k in ("판", "기준값", "숫자열", "범주열", "시험성능", "sklearn")}
@app.post("/predict")
def predict(order: Order):
return {**predict_rows([order])[0], "기준값": BUNDLE["기준값"], "판": BUNDLE["판"]}
@app.post("/predict/batch")
def predict_batch(batch: Batch):
return {"건수": len(batch.주문들), "판": BUNDLE["판"], "결과": predict_rows(batch.주문들)}
''', encoding="utf-8")
print("API 파일:", Path("out_delay_api.py").stat().st_size, "바이트")API 파일: 2096 바이트2단계. 앱을 불러 시험 손님(TestClient) 만들기
import sys
sys.path.insert(0, ".") # 작업 폴더(data)의 out_delay_api.py 를 찾게 한다
from fastapi.testclient import TestClient
from out_delay_api import app
client = TestClient(app) # 서버를 띄우지 않고 앱에 요청을 보낸다
r = client.get("/health")
print(r.status_code, r.json())
print(client.get("/model").json())200 {'상태': 'ok', '판': 'delay-lr-1'}
{'판': 'delay-lr-1', '기준값': 0.5, '숫자열': ['주문시각', '거리km', '무게kg'], '범주열': ['지역', '상품분류', '날씨', '택배사'], '시험성능': {'정확도': 0.819, '재현율': 0.226, 'AUC': 0.695}, 'sklearn': '1.9.1'}3단계. 주문 하나 예측하기
order = {"주문시각": 22, "거리km": 197.6, "무게kg": 4.6, "지역": "지방", "상품분류": "도서", "날씨": "눈", "택배사": "빠른별택배"}
r = client.post("/predict", json=order) # json= 에 사전을 주면 JSON 본문과 Content-Type 을 알아서 붙인다
print(r.status_code, r.headers["content-type"])
print(r.json())
near = dict(order, 주문시각=9, 거리km=7.3, 지역="서울", 날씨="맑음", 택배사="한결택배")
print(client.post("/predict", json=near).json())200 application/json
{'지연확률': 0.6887, '지연예상': True, '기준값': 0.5, '판': 'delay-lr-1'}
{'지연확률': 0.0827, '지연예상': False, '기준값': 0.5, '판': 'delay-lr-1'}4단계. 자동으로 만들어진 문서 보기
spec = client.get("/openapi.json").json() # /docs 화면이 읽는 API 명세(OpenAPI)
for path, methods in spec["paths"].items():
for m in methods:
print(m.upper(), path)
fields = spec["components"]["schemas"]["Order"]["properties"]
print("Order 칸:", {k: v.get("enum", v.get("type")) for k, v in fields.items()})GET /health
GET /model
POST /predict
POST /predict/batch
Order 칸: {'주문시각': 'integer', '거리km': 'number', '무게kg': 'number', '지역': ['서울', '경기', '인천', '지방'], '상품분류': ['의류', '생활용품', '식품', '도서', '가전'], '날씨': ['맑음', '흐림', '비', '눈', '모름'], '택배사': ['한결택배', '빠른별택배', '새벽길택배']}이 실습이 뜻하는 것
이 실습은 밀키트를 꺼내 주문받는 창구를 여는 일과 같습니다. 창구의 메뉴와 주문서 양식을 적은 운영 안내서를 쓰고(1단계), 가게 문을 열지 않은 채 연습 손님을 들여 "영업 중인가요?"를 묻게 합니다(2단계). 연습 손님이 주문서 한 장을 내 결과를 받고(3단계), 창구가 저절로 만든 메뉴판을 읽어 봅니다(4단계). 여기서 창구가 API(Application Programming Interface, 프로그램끼리 주고받는 창구), 운영 안내서가 out_delay_api.py, 주문서 양식이 Pydantic 의 Order, 연습 손님이 TestClient, 메뉴판이 OpenAPI 명세입니다. 비유와 다른 점은, 진짜 손님은 가게까지 와야 하지만 TestClient 는 서버를 띄우지 않고 프로그램 안에서 바로 앱을 부른다는 것입니다.
| 단계 | 출력 | 뜻 |
|---|---|---|
| 1. 파일 쓰기 | API 파일: 2096 바이트 | API 코드가 파일 하나로 저장되었습니다. 실습 3, 4가 이 파일을 불러 씁니다 |
| 2. /health | 200 {'상태': 'ok', '판': 'delay-lr-1'} | 200 은 "잘 처리됨"이라는 상태 코드입니다. 창구가 열려 있고 어떤 판의 모델을 쓰는지 알려 줍니다 |
| 2. /model | '시험성능': {'정확도': 0.819, '재현율': 0.226, 'AUC': 0.695} | 실습 1에서 저장한 성능과 열 정보가 그대로 나옵니다 |
| 3. 예측 | 200 application/json, 0.6887 … True, 0.0827 … False | 밤 10시 지방 주문은 지연 확률 0.6887 로 기준 0.5 를 넘어 "지연 예상"이고, 아침 9시 서울 주문은 0.0827 로 지연이 아니라고 봅니다. 답은 JSON(JavaScript Object Notation) 꼴로 왔습니다 |
| 4. 명세 | GET /health … POST /predict/batch, Order 칸: {…} | 엔드포인트 넷과 주문서의 칸, 고를 수 있는 값이 따로 적지 않아도 명세에 들어갔습니다 |
한 줄로 줄이면, 모델 파일을 읽어 HTTP(HyperText Transfer Protocol) 요청으로 예측을 돌려주는 API 를 만들었고, 서버 없이도 시험할 수 있다는 실습입니다. 실습 3에서는 잘못된 주문서를 보내 보고, 실습 4에서는 진짜 서버로 띄웁니다. 여기서 본 확률 두 개는 주문 둘의 예일 뿐이고, 모델이 얼마나 잘 맞히는지는 실습 1의 성능이 보여 줍니다.
코드 설명
- 1단계는 API 를
out_delay_api.py파일로 씁니다. 모델은 파일을 불러올 때(서버가 뜰 때) 한 번만 읽고, 요청마다predict_rows()만 부릅니다.Literal로 학습 데이터에 있던 범주만 받게 했고,날씨는 기본값이 "모름"입니다. TestClient(app)는 서버 없이 앱에 HTTP 요청을 흉내 내 보냅니다./health는 200 과 판을,/model은 성능과 열 정보를 돌려줬습니다.- 3단계에서 밤 10시 지방 주문은 지연 확률 0.6887(지연 예상), 아침 9시 서울 주문은 0.0827 이었습니다. 응답의
content-type은application/json으로, 경로 함수가 돌려준 사전을 FastAPI 가 JSON 으로 바꿨습니다. /openapi.json은 FastAPI 가 경로 함수와 Pydantic 모델에서 만든 API 명세입니다. 엔드포인트 넷과Order의 칸이 들어 있고,Literal로 정한 칸은 고를 수 있는 값(enum)이 함께 적혔습니다. 브라우저의/docs화면이 이 명세를 읽어 그립니다.