Fastapi: XGBoost 집값 예측 모델 서빙해보기

Kim yoon beom·2025년 12월 19일

1. XGBoost 모델 파일 준비

원래 블로그에서 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 파일을 바로 다운로드해 사용했다.

“실무에서도 학습/서빙 담당이 나뉘는 경우가 많으니,
서빙 관점 연습이라고 생각하면 편하다.”

라는 마인드 장착.


2. 프로젝트 폴더 구조

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에 붙이는 역할


3. 의존성 설치

가상환경을 활성화한 뒤, XGBoost와 판다스, scikit‑learn을 설치했다.

pip install xgboost
pip install pandas
pip install scikit-learn

처음에는 ImportError: sklearn needs to be installed 같은 에러가 나왔는데
위 세 패키지 설치 후에는 정상적으로 동작했다.

매일 까먹는거 같다.


4. services/house.py – XGBoost 모델 로딩과 예측

먼저 서비스 레이어에서 모델을 불러오고, 예측하는 함수를 하나 만든다.

# 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 같은 값을 파라미터로 받아서 동적으로 바꾸는 버전도 만든다.


5. routers/house.py – /house/price/predict 엔드포인트

다음은 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를 붙이면서 완성된다.

6. routers/index.py – 라우터를 한 번에 묶기

기존에 /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가 매달린다.


7. main.py – 최상위 라우팅

마지막으로 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


8. 서버 실행 & 0.0.0.0 vs localhost 삽질

  • 서버는 아래 명령어로 실행했다.
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달러 정도라는 의미다.


9. Query 파라미터로 crim, room 받아 보기

위까지는 서버 코드에서 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()
  • 그리고 라우터 쪽에서는 GET + query 파라미터로 받도록 변경했다.
# 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 사이의 실수

값을 바꿔가며 실행해 보면, 예측 결과가 실제로 변하는 걸 확인할 수 있다.


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.

profile
나는.원한다.개발자

0개의 댓글