- 참고
(따라)한 원 블로그
#4. Fastapi로 모델 서빙부터 DevOps 까지 : XGBRegressor를 이용한 간단한 모델 구현 및 모델 서빙
- Colab에서 직접 학습하려 했는데 컬럼 문제로 모델 학습이 안 돼서,
작성자가 올려둔xgb_model.json만 다운받아 진행했다.
- 목표:
http://127.0.0.1:3000/api/house/price/predict 로 요청 → 보스턴 집값 예측 결과 받기.
원래 블로그에서 Colab으로 학습하는 코드
import xgboost as xgb
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
from sklearn.datasets import fetch_openml
import pandas as pd
import numpy as np
# Boston 집 값 데이터 로드
boston = fetch_openml(name="boston")
df = pd.DataFrame(boston.data, columns=boston.feature_names)
df["MEDV"] = boston.target
# 타입 변환
df["CHAS"] = df["CHAS"].astype("int")
df["RAD"] = df["RAD"].astype("int")
# X, y 분리
X = df.drop("MEDV", axis=1)
y = df["MEDV"]
# train/test 분할
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# 모델 생성
model = xgb.XGBRegressor(
objective="reg:squarederror",
colsample_bytree=0.3,
learning_rate=0.1,
max_depth=5,
alpha=10,
n_estimators=10,
)
# 학습
model.fit(X_train._get_numeric_data(), np.ravel(y_train, order="C"))
# 모델 저장
model.save_model("xgb_model.json")
여기서 컬럼 타입 이슈 때문에 학습이 계속 실패했다.
(1년전꺼 이긴 한데.. 모델 학습용 글이 아니라서..)
블로그에서 제공한 xgb_model.json 파일을 바로 다운로드해 사용했다.
“실무에서도 학습/서빙 담당이 나뉘는 경우가 많으니,
서빙 관점 연습이라고 생각하면 편하다.”
라는 마인드 장착.
FastAPI 쪽 프로젝트 구조는 이렇게 잡았다.
fastapi-test/
├── venv/
├── src/
│ ├── main.py
│ ├── models/
│ │ └── xgb_model.json
│ ├── routers/
│ │ ├── index.py
│ │ └── house.py
│ └── services/
│ └── house.py
└── requirements.txt

models/ :
Colab에서 만든(혹은 받아온) XGBoost 모델 파일
services/house.py :
모델 로딩 + 예측 로직
routers/house.py :
HTTP 엔드포인트 정의
routers/index.py :
라우터들을 한 번에 모아서 main에 붙이는 역할
가상환경을 활성화한 뒤, XGBoost와 판다스, scikit‑learn을 설치했다.
pip install xgboost
pip install pandas
pip install scikit-learn
처음에는 ImportError: sklearn needs to be installed 같은 에러가 나왔는데
위 세 패키지 설치 후에는 정상적으로 동작했다.
매일 까먹는거 같다.
먼저 서비스 레이어에서 모델을 불러오고, 예측하는 함수를 하나 만든다.
# src/services/house.py
from typing import List
import xgboost as xgb
import pandas as pd
# XGBRegressor 인스턴스 생성
loaded_model = xgb.XGBRegressor()
# 학습된 가중치 로드 (src 기준 경로)
loaded_model.load_model("src/models/xgb_model.json")
async def runModel() -> List[float]:
# 임시 입력 데이터 (나중엔 요청에서 받을 값)
dic = {
"CRIM": [3.7],
"ZN": [18.0],
"INDUS": [22.37],
"CHAS": [0],
"NOX": [0.145],
"RM": [4.533],
"AGE": [66.7],
"DIS": [4.291],
"RAD": [13],
"TAX": [333.333],
"PTRATIO": [21.0],
"B": [197.6],
"LSTAT": [23.4],
}
# dict → DataFrame
df = pd.DataFrame.from_dict(dic, orient="columns")
# 예측
pred = loaded_model.predict(df)
# numpy array → list
return pred.tolist()
여기서는 일단 dic 안의 값을 하드코딩해 두었다.
그래서 지금 단계에서는 어떤 요청을 보내든 항상 동일한 예측 값이 나온다.
뒤에서crim, room같은 값을 파라미터로 받아서 동적으로 바꾸는 버전도 만든다.
다음은 house 관련 라우터다.
# src/routers/house.py
from fastapi import APIRouter
from src.services.house import runModel
house_router = APIRouter()
@house_router.post("/price/predict")
async def get_prediction_of_house_price():
price = await runModel()
return price
@house_router.post("/price/predict")라는 데코레이터로
POST /house/price/predict요청을 처리하는 함수를 만든다.
- 실제 URL은
index_router와main에서prefix를 붙이면서 완성된다.
기존에 /user, /post를 쓰고 있었다면, house 라우터만 추가로 include해 주면 된다.
# src/routers/index.py
from fastapi import APIRouter
from src.routers.user import user_router
from src.routers.post import post_router
from src.routers.house import house_router
index_router = APIRouter()
index_router.include_router(user_router, prefix="/user")
index_router.include_router(post_router, prefix="/post")
index_router.include_router(house_router, prefix="/house")
이렇게 하면:
/user아래는 user 관련 API
/post아래는 post 관련 API
/house아래는 방금 만든 집값 예측 API가 매달린다.
마지막으로 main에서 index_router를 /api 아래에 붙인다.
# src/main.py
from fastapi import FastAPI
from src.routers.index import index_router
app = FastAPI(
docs_url="/docs",
openapi_url="/open-api-docs",
)
# /api 아래에 모든 라우터 묶기
app.include_router(index_router, prefix="/api")
@app.get("/")
async def root():
return {"message": "Hello, FastAPI"}
이제 최종 경로는 다음과 같이 결정된다.
main: /api
index: /house
house_router: /price/predict
즉 최종 URL은:
POST /api/house/price/predict
uvicorn src.main:app --host=0.0.0.0 --port=3000 --reload
Uvicorn running on http://0.0.0.0:3000 (Press CTRL+C to quit)
처음에는 이걸 그대로 보고 브라우저에 http://0.0.0.0:3000 을 찍었다가 접속이 안 됐는데,
실제로는 http://localhost:3000 또는 http://127.0.0.1:3000 로 접속해야 한다.
0.0.0.0 은 “서버가 모든 네트워크 인터페이스에서 요청을 받겠다”는 의미의 바인딩 주소이고,
클라이언트(브라우저)는 localhost 나 실제 IP를 사용한다.
Swagger UI는 다음 주소에서 확인할 수 있다.
http://localhost:3000/docs
여기서 /api/house/price/predict 엔드포인트를 찾은 뒤:
1. Try it out
2. Execute
버튼을 눌러 보면, 예측된 집값 리스트가 응답으로 내려온다.
값은
MEDV (천 달러 단위)이기 때문에,
예를 들어 [20.3] 이 나오면 대략 2만 300달러 정도라는 의미다.
위까지는 서버 코드에서 CRIM, RM 값을 하드코딩해서 항상 같은 결과만 나왔다.
블로그에서는 여기서 한 단계 더 나가서,
crim, room을 사용자가 입력하도록 수정한다.
services/house.py의 runModel을 이렇게 바꿀 수 있다.# src/services/house.py
async def runModel(crim: float, room: float) -> List[float]:
dic = {
"CRIM": [crim], # 0 ~ 20
"ZN": [18.0],
"INDUS": [22.37],
"CHAS": [0],
"NOX": [0.145],
"RM": [room], # 1 ~ 10
"AGE": [66.7],
"DIS": [4.291],
"RAD": [13],
"TAX": [333.333],
"PTRATIO": [21.0],
"B": [197.6],
"LSTAT": [23.4],
}
df = pd.DataFrame.from_dict(dic, orient="columns")
pred = loaded_model.predict(df)
return pred.tolist()
# src/routers/house.py
from fastapi import APIRouter
from src.services.house import runModel
house_router = APIRouter()
@house_router.get("/price/predict")
async def get_prediction_of_house_price(crim: float, room: float):
price = await runModel(crim, room)
return price
이제 Swagger에서 /api/house/price/predict 엔드포인트를 열어 보면
crim, room 두 개의 입력창이 생긴다.
crim:
0 ~ 20 사이의 실수
room:
1 ~ 10 사이의 실수
값을 바꿔가며 실행해 보면, 예측 결과가 실제로 변하는 걸 확인할 수 있다.
이번 포스팅에서는:
Colab에서 학습한(혹은 제공 받은) XGBoost Regressor 모델 파일(xgb_model.json)을 받아서
FastAPI 프로젝트에 models/ 폴더로 넣고,
services/house.py에서 모델을 로딩해 예측 함수로 감싼 뒤,
routers/house.py + index_router + main.py를 통해
POST /api/house/price/predict, GET /api/house/price/predict API로 서빙하는 것까지 해봤다.
모델 학습이 실패하더라도,
이렇게 “이미 학습된 모델 파일을 받아서 서빙하는 연습”만으로도
FastAPI Router구조, 서비스 레이어 분리,Swagger테스트 흐름을 꽤 잘 감 잡을 수 있다.
다음 단계에서는 튜토리얼 과제처럼:
- POST Body로 CRIM, RM 외의 모든 피처를 한 번에 받아서 예측하는 버전을 만들거나,
- 실제 도메인(콘텐츠 영향력 점수, PPL 가치 등)에 맞는 모델을 만들어 FastAPI로 서빙해 볼 계획이다.
다만 아직은 주 백엔드 개발자로 성장해 가는 과정이라,
배워야 할 것도, 정리하고 싶은 내용도 훨씬 더 많다.
그래서 이 시리즈는 잠시 여기서 숨 고르기를 하고,
실제 프로젝트에서 사용하거나 경험한 뒤에 다시 이어서 써 보려고 한다.
The end.