MLOps 구축기 (3) - MLflow 모델을 FastAPI로 서빙하기

2026. 3. 8. 03:19·AI

2편에서 하이퍼파라미터 튜닝으로 최적 모델을 찾고, 모델 레지스트리에 등록했다. 하지만 모델을 등록만 해두면 실제 서비스에서는 아무 의미가 없다. 외부 요청을 받아 추론 결과를 반환하는 API 서버가 있어야 비로소 모델이 "서비스"로 동작한다.

이번 편에서는 MLflow 레지스트리에 등록된 모델을 불러와 FastAPI로 추론 API를 만들고, Docker 컨테이너로 배포하는 과정을 정리해봤다.


전체 구조

1편에서 만든 MLflow 인프라 위에 서빙 서버가 하나 추가되는 구조다.

[ 클라이언트 (curl, 브라우저, 앱) ]
        │
        │  POST /predict (JSON)
        ▼
[ FastAPI 서빙 서버 (Docker) ]
        │
        │  모델 로드
        ▼
[ MLflow Server (Docker) ]
        │                │
        ▼                ▼
[ PostgreSQL ]     [ MinIO ]

서빙 서버는 시작될 때 MLflow 레지스트리에서 모델을 한 번 로드하고, 이후 추론 요청이 들어올 때마다 메모리에 올라온 모델로 예측을 수행한다.

매 요청마다 모델을 새로 불러오는 방식도 가능하지만, 그렇게 하면 아티팩트 저장소(MinIO/S3)에서 모델 파일을 계속 읽어야 해서 추론 latency가 크게 늘어날 수 있다. 그래서 일반적으로는 서버 시작 시 모델을 메모리에 올려두고 재사용하는 방식으로 구현한다.


FastAPI + Uvicorn

서빙에 FastAPI와 Uvicorn을 사용한다.

FastAPI는 "어떤 요청이 오면 뭘 할지" 정의하는 웹 프레임워크고, Uvicorn은 실제로 요청을 받아서 FastAPI에 전달하는 서버다. Java로 치면 FastAPI가 Spring, Uvicorn이 Tomcat에 해당한다. 별도 웹 서버를 따로 세팅하지 않아도 이 조합이면 바로 API 서버가 된다.


서빙 코드

# src/serve.py
import os
import mlflow
import numpy as np
from fastapi import FastAPI
from pydantic import BaseModel

mlflow.set_tracking_uri(os.getenv("MLFLOW_TRACKING_URI", "http://localhost:5001"))

# 서버 시작 시 레지스트리에서 모델 로드
model = mlflow.sklearn.load_model("models:/digits-classifier/1")

app = FastAPI(title="Digits Classifier API")

class PredictRequest(BaseModel):
    features: list[list[float]]

class PredictResponse(BaseModel):
    predictions: list[int]

@app.get("/health")
def health():
    return {"status": "ok"}

@app.post("/predict", response_model=PredictResponse)
def predict(req: PredictRequest):
    X = np.array(req.features)
    preds = model.predict(X).tolist()
    return PredictResponse(predictions=preds)

몇 가지 포인트만 짚어보면:

Tracking URI 설정

mlflow.set_tracking_uri(os.getenv("MLFLOW_TRACKING_URI", "http://localhost:5001"))

MLflow 서버 주소를 환경변수로 받도록 했다.

로컬에서 실행할 때는 http://localhost:5001을 사용하고, Docker 컨테이너 안에서는 http://mlflow:5000으로 바뀌기 때문에 주소를 코드에 하드코딩하면 환경에 따라 동작이 달라질 수 있다.


모델 로드 방식

model = mlflow.sklearn.load_model("models:/digits-classifier/1")

이 URI는 MLflow 모델 레지스트리에서 모델을 가져오는 방식이다.

models:/모델이름/버전 형태로 작성하면 MLflow가 내부적으로 아티팩트 위치(MinIO/S3)를 찾아 모델을 불러온다. 모델 파일이 실제로 어디에 저장돼 있는지 신경 쓸 필요가 없다.

예제에서는 단순하게 버전 1을 직접 지정했다.
실무에서는 보통 특정 버전 대신 Production 같은 stage를 사용해서 배포 모델을 관리한다.

예를 들면 이런 식이다.

models:/digits-classifier/Production

이렇게 해두면 모델을 교체할 때 코드 수정 없이 레지스트리 stage만 변경해서 배포 모델을 업데이트할 수 있다.


요청 / 응답 모델

class PredictRequest(BaseModel):
    features: list[list[float]]

class PredictResponse(BaseModel):
    predictions: list[int]

요청과 응답 스키마를 Pydantic 모델로 정의했다.
이렇게 하면 FastAPI가 입력 검증을 자동으로 처리하고, Swagger 문서도 함께 생성해준다.


로컬에서 테스트

Docker로 올리기 전에 로컬에서 먼저 실행해본다.

pip install fastapi uvicorn
cd ~/mlops-lab
uvicorn src.serve:app --host 0.0.0.0 --port 8000

새 터미널에서 추론 요청을 보내본다.

digits 데이터셋은 8×8 픽셀 이미지라서 총 64개의 feature가 필요하다.

curl -X POST http://localhost:8000/predict \
  -H "Content-Type: application/json" \
  -d '{"features": [[0,0,5,13,9,1,0,0,0,0,13,15,10,15,5,0,0,3,15,2,0,11,8,0,0,4,12,0,0,8,8,0,0,5,8,0,0,9,8,0,0,4,11,0,1,12,7,0,0,2,14,5,10,12,0,0,0,0,6,13,10,0,0,0]]}'

응답:

{"predictions":[0]}

숫자 0에 해당하는 픽셀 데이터를 보내면 모델이 정상적으로 0을 예측한다.

또는 http://localhost:8000/docs에 접속하면 FastAPI가 자동으로 생성한 Swagger UI에서 API를 직접 테스트할 수도 있다.


Docker 컨테이너화

로컬에서 동작을 확인했으니 Docker 컨테이너로 묶는다.
목표는 docker compose up 한 번으로 전체 환경이 올라오도록 만드는 것이다.


Dockerfile

# Dockerfile.serve
FROM python:3.11-slim

WORKDIR /app

RUN pip install mlflow scikit-learn fastapi uvicorn

COPY src/serve.py .

CMD ["uvicorn", "serve:app", "--host", "0.0.0.0", "--port", "8000"]

예제를 단순하게 하기 위해 직접 pip install을 사용했지만, 실제 프로젝트에서는 requirements.txt나 poetry로 의존성 버전을 고정하는 방식을 많이 사용한다.


docker-compose.yml에 서빙 서비스 추가

기존 services 블록에 서빙 컨테이너를 추가한다.

serving:
  build:
    context: .
    dockerfile: Dockerfile.serve
  ports:
    - "8000:8000"
  environment:
    MLFLOW_TRACKING_URI: http://mlflow:5000
  depends_on:
    mlflow:
      condition: service_started
  restart: unless-stopped

Docker 네트워크 내부에서는 localhost 대신 서비스 이름으로 접근할 수 있다.
그래서 MLflow 주소를 http://mlflow:5000으로 설정했다.


MLflow 3.x Host 헤더 보안

여기서 한 가지 주의할 점이 있다.

MLflow 3.x에서는 DNS rebinding 공격 방지를 위해 Host 헤더를 검증한다. Docker 내부에서 서빙 컨테이너가 http://mlflow:5000으로 요청하면 Host 헤더가 mlflow:5000이 되는데, MLflow 서버가 이걸 허용하지 않으면 403 에러가 발생한다.

mlflow.exceptions.MlflowException: API request failed with error code 403
Response body: 'Invalid Host header - possible DNS rebinding attack detected'

이 경우 MLflow 서버 실행 옵션에 허용 호스트를 추가해야 한다.

command: >
  mlflow server
  --host 0.0.0.0
  --port 5000
  --backend-store-uri postgresql://mlflow:mlflow@postgres:5432/mlflow
  --serve-artifacts
  --artifacts-destination s3://mlflow/
  --allowed-hosts mlflow,mlflow:5000,localhost,localhost:5001,0.0.0.0

이 설정이 없으면 서빙 컨테이너가 모델을 로드하지 못하고 계속 재시작한다.


빌드 및 실행

docker compose up -d --build

전체 서비스가 올라오면 다시 추론 요청을 보내본다.

curl -X POST http://localhost:8000/predict ...

정상적으로 응답이 오면 Docker 환경에서도 MLflow → MinIO → 서빙까지 전체 흐름이 연결된 것이다.


최종 구성

지금까지 구성된 서비스는 다음과 같다.

[ docker-compose.yml ]
├── postgres       ← 실험 메타데이터 저장
├── minio          ← 모델 아티팩트 저장 (S3 호환)
├── create-bucket  ← MinIO 버킷 초기화
├── mlflow         ← 실험 추적 서버 + 모델 레지스트리
└── serving        ← FastAPI 추론 API 서버

학습은 로컬에서 실행하고 MLflow에 기록한다.
최적 모델을 레지스트리에 등록하면 서빙 서버가 해당 모델을 불러와 API 형태로 제공한다.

이렇게 학습 → 추적 → 비교 → 등록 → 서빙 흐름이 하나의 파이프라인으로 연결된다.


정리

3편에 걸쳐서 간단한 MLOps 환경을 만들어봤다. 1편에서 Docker Compose로 MLflow 실험 추적 서버를 구축하고, 2편에서 하이퍼파라미터 튜닝 후 최적 모델을 레지스트리에 등록하고, 3편에서 그 모델을 FastAPI로 서빙했다.

지금 만든 환경은 작은 실험용이지만, 대부분의 MLOps 플랫폼도 이 기본 구조 위에서 확장된다.

여기에서 더 나아가려면 CI/CD(GitHub Actions로 학습 → 테스트 → 배포 자동화), 모니터링(Prometheus + Grafana), 데이터/모델 버전 관리, 자동 재학습 파이프라인 같은 요소들을 추가할 수 있다. 이런 기능들을 하나씩 붙이면 점점 실무에 가까운 MLOps 환경이 된다.


사용한 도구

  • FastAPI + Uvicorn (추론 API 서버)
  • MLflow 3.10.0 (모델 레지스트리)
  • Docker Compose (인프라 오케스트레이션)
  • scikit-learn (digits 데이터셋, RandomForest 모델)

'AI' 카테고리의 다른 글

온디바이스 AI 경량화 (1) — INT8 양자화로 CIFAR-10 모델 92% 줄이기 (TFLite PTQ)  (0) 2026.04.17
MLOps 구축기 (5) - MLflow 기반 MLOps 파이프라인 전체 정리  (1) 2026.03.13
MLOps 구축기 (4) - GitHub Actions CI/CD와 Prometheus + Grafana 모니터링  (0) 2026.03.13
MLOps 구축기 (2) - MLflow 하이퍼파라미터 실험 관리와 Model Registry  (0) 2026.03.08
MLOps 구축기 (1) - MLflow Tracking Server 구축 (Docker Compose + PostgreSQL + MinIO)  (0) 2026.03.05
'AI' 카테고리의 다른 글
  • MLOps 구축기 (5) - MLflow 기반 MLOps 파이프라인 전체 정리
  • MLOps 구축기 (4) - GitHub Actions CI/CD와 Prometheus + Grafana 모니터링
  • MLOps 구축기 (2) - MLflow 하이퍼파라미터 실험 관리와 Model Registry
  • MLOps 구축기 (1) - MLflow Tracking Server 구축 (Docker Compose + PostgreSQL + MinIO)
João Jin
João Jin
모바일 · 보안 · AI 기록
  • João Jin
    João Jin - 모바일 · 보안 · AI
    João Jin
  • 전체
    오늘
    어제
    • 분류 전체보기 (30)
      • 프로젝트 (3)
      • 개발기 (8)
      • 모바일 (8)
      • 보안 (2)
      • AI (8)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • GitHub
    • X
  • 공지사항

  • 인기 글

  • 태그

    MLOps
    온디바이스AI
    JNI
    mcp-fence
    Docker
    LLM 보안
    MCP
    Docker Compose
    MLFlow
    LINE WORKS
    AI 에이전트 보안
    MCP 보안
    Android
    Native
    AI
    머신러닝
    FastAPI
    ndk
    model context protocol
    안드로이드 NDK
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
João Jin
MLOps 구축기 (3) - MLflow 모델을 FastAPI로 서빙하기
상단으로

티스토리툴바