TL;DR
| 항목 | FastAPI (기존) | BentoML (신규) |
|---|---|---|
| 핵심 파일 | app/main.py, routes/predict.py | service.py |
| 모델 로드 시점 | 종종 전역 또는 Dependency Injection | 서비스 인스턴스 초기화 시 (__init__) |
| 배포 패키징 | Dockerfile 직접 작성 필요 | bentofile.yaml을 통한 자동화 |
| 성격 | 범용 HTTP 서버 | ML 추론 최적화 서비스 |
제게는 FastAPI가 익숙하고 빠르기 때문에 FastAPI를 사용한 AI 서빙을 소개하였습니다. 하지만 프로젝트의 성격이 일반적인 '웹 서비스'가 아니라 '모델 추론' 그 자체에 집중되어 있다면, 범용 웹 프레임워크보다는 ML Serving 전용 프레임워크를 고민해 볼 수 있습니다.
이번 글에서는 기존의 FastAPI 기반 손글씨 숫자 인식 백엔드를 BentoML 구조로 전환하여, 더욱 AI 모델 서빙에 최적화된 구조로 개선하는 방법을 살펴봅니다.
FastAPI는 매우 훌륭한 범용 웹 API 프레임워크입니다. 하지만 모델 서빙 관점에서 보면 다음과 같은 차이가 있습니다.
Runner, 마이크로서비스 확장을 위한 배치(Batch) 처리, 모델 버전 관리 등을 별도 설정 없이 바로 사용할 수 있습니다.bentofile.yaml 하나로 의존성과 환경을 정의하고 Docker 이미지까지 쉽게 빌드할 수 있습니다.결국, 지금 프로젝트는 웹 서비스라기보다 모델 추론 서비스에 가깝기 때문에 BentoML로 옮겨볼 명분이 충분하다고 생각합니다.
기존 FastAPI 구조는 웹 애플리케이션의 관례를 따르느라 파일이 여러 갈래로 찢어져 있었습니다.
backend/
├─ app/
│ ├─ api/routes/predict.py # 엔드포인트
│ ├─ core/config.py # 설정
│ ├─ schemas/prediction.py # Pydantic 모델
│ └─ services/
│ ├─ image_preprocessing.py # 전처리 로직
│ └─ inference.py # 모델 로드 및 추론
├─ models/
│ └─ digit_model_28.joblib
└─ main.py
digit-recognition/
├─ bentoml_backend/
│ ├─ service.py # 핵심 서빙 로직 (통합)
│ ├─ preprocessing.py # 전처리 (재사용)
│ ├─ bentofile.yaml # 패키징 설정
│ ├─ requirements.txt # 의존성
│ └─ models/
│ └─ digit_model_28.joblib
└─ frontend/
| 기존 FastAPI 구조 | BentoML 대체 구조 |
|---|---|
app/main.py | service.py |
api/routes/predict.py | service.py 내 @bentoml.api |
schemas/prediction.py | service.py 내 Pydantic 모델 |
services/inference.py | service.py 내 모델 로직 |
services/image_preprocessing.py | preprocessing.py |
핵심은 service.py입니다. 기존에 흩어져 있던 책임을 하나의 클래스로 모읍니다.
from __future__ import annotations
import base64
import io
from pathlib import Path
import bentoml
import joblib
import numpy as np
from PIL import Image
from pydantic import BaseModel
# 기존 전처리 로직 재사용
from preprocessing import preprocess_canvas_png_to_28x28
MODEL_PATH = Path(__file__).resolve().parent / "models" / "digit_model_28.joblib"
class ImageRequest(BaseModel):
image: str # Base64 data URL
class PredictionResponse(BaseModel):
digit: int
@bentoml.service(
name="digit-recognizer",
traffic={"timeout": 10},
)
class DigitRecognizerService:
def __init__(self) -> None:
# 서비스 시작 시 모델을 한 번만 로드하여 메모리에 유지
self.model = joblib.load(MODEL_PATH)
@bentoml.api
def predict(self, request: ImageRequest) -> PredictionResponse:
# 1. Base64 이미지 디코딩
_, encoded = request.image.split(",", 1)
image_data = base64.b64decode(encoded)
image = Image.open(io.BytesIO(image_data))
# 2. 전처리 (28x28 grayscale 변환 등)
data_28 = preprocess_canvas_png_to_28x28(image)
# 3. 모델 입력 규격($1 \times 784$)에 맞게 변형
values = data_28.flatten().reshape(1, -1)
# 4. 추론
prediction = self.model.predict(values)[0]
return PredictionResponse(digit=int(prediction))
preprocessing.py)이 프로젝트의 품질을 결정하는 전처리 로직(Crop, Resize, Center of Mass 등)은 프레임워크와 무관한 순수 로직이므로 파일명만 바꿔서 그대로 재사용합니다.
bentofile.yamlBentoML의 강력함은 이 설정 파일에서 나옵니다. 어떤 파일을 포함할지, 어떤 라이브러리가 필요한지 명시합니다.
service: "service:DigitRecognizerService"
labels:
project: "digit-recognition"
framework: "bentoml"
include:
- "*.py"
- "models/*.joblib"
python:
packages:
- bentoml
- joblib
- numpy
- pillow
- scikit-learn
- pydantic
기존 uvicorn 대신 bentoml serve 명령어를 사용합니다.
# 개발 모드 실행
bentoml serve service:DigitRecognizerService --reload
이 경우, http://localhost:3000/ 로 접속 시 아래의 Swagger 페이지를 확인할 수 있습니다.

Vue.js 등)의 대응이번 BentoML의 변경에서 가장 좋은 점은 프론트엔드 코드를 거의 고칠 필요가 없다는 것입니다. BentoML도 동일하게 POST /predict 엔드포인트를 생성하며, 우리가 정의한 ImageRequest 스키마가 기존 FastAPI의 스키마와 동일하다면 HTTP 계약(Contract)이 유지되기 때문입니다.
포트 번호나 API 베이스 URL 정도만 새 서버 주소에 맞게 업데이트하면 됩니다.

안전한 전환을 위해 다음 순서를 권장합니다.
image_preprocessing.py를 독립된 파일로 추출합니다.service.py를 작성하고 모델 로드 테스트를 진행합니다.bentoml serve를 띄운 후, Postman이나 curl로 기존과 동일한 JSON 응답이 오는지 확인합니다.VITE_API_BASE_URL 등을 수정하여 실제 캔버스와 연동합니다.app/, main.py 등 FastAPI 관련 파일을 제거합니다.FastAPI에서 BentoML로의 교체는 단순히 라이브러리를 바꾸는 작업이 아닙니다. 이것은 프로젝트의 중심을 "범용 웹 API"에서 "전문화된 모델 서비스"로 옮기는 작업입니다.
이렇게 구조를 정리해두면, 나중에 모델을 업데이트하거나, 다른 모델(예: PyTorch 기반)로 교체하거나, 대규모 트래픽을 처리하기 위해 스케일 아웃을 할 때 훨씬 유연하게 대응할 수 있습니다.