MLOps 실전 — Docker/Kubernetes 배포, 드리프트 탐지, MLflow로 모델 생애주기 관리하기

모델을 컨테이너로 배포하고 드리프트를 탐지하며 MLflow로 실험·레지스트리를 관리하는 MLOps 실무 구조와 코드 예시를 정리한다.

2026-08-12 · 최초 발행 2025-10-14

모델 학습이 끝나는 순간 프로젝트가 끝난다고 생각하면 곤란하다. 실제 서비스에 붙은 모델은 트래픽을 받아 예측을 내놓아야 하고, 시간이 지나면 입력 데이터의 분포가 바뀌어 성능이 떨어지고, 그러면 다시 학습해서 배포해야 한다. 이 전 과정 — 데이터에서 학습, 검증, 배포, 모니터링, 재학습까지 — 을 엔지니어링적으로 자동화하는 체계가 MLOps다. 컨테이너화, 오케스트레이션, 실험 추적, 모델 레지스트리, 관측성, 거버넌스가 전부 이 범위 안에 들어간다.

컨테이너로 배포한다는 것의 의미

모델과 의존성을 컨테이너 이미지로 표준화하면 "내 로컬에서는 되는데" 문제가 사라진다. Docker 단독으로도 이 표준화 효과는 얻을 수 있지만, 단일 노드 위에서 돌리는 한 스케일링은 수동이고 장애점도 하나다. Kubernetes를 얹으면 롤링 업데이트와 오토스케일링, 선언형 상태 기반의 자체치유, 블루/그린이나 카나리 배포로 점진적 전환이 가능해지는 대신 초기 설정의 복잡도가 확 올라간다. 이 트레이드오프 — 낮은 지연을 위한 단순 구성이냐, 트래픽 급증에 대응할 운영 자동화냐 — 는 팀의 성숙도와 트래픽 패턴을 보고 판단할 문제다.

서빙 방식도 워크로드에 따라 갈린다. 온라인 실시간 추론은 REST/gRPC, 배치는 스케줄 기반 오프라인 처리, 스트리밍은 Kafka 같은 메시지 큐를 통해 처리하는 식으로 패턴이 나뉘고, 각각 HPA/VPA 설정도 다르게 가져가야 한다.

다음은 하나의 추론 서버가 학습부터 배포, 모니터링, 재학습까지 거치는 전체 흐름이다.

log params/metricsregister modelNo Drift & SLO OKDrift or SLO BreachAutoRollbackDeploy FailRaw/Data & SchemaFeature EngineeringTrain & ValidateMLflow TrackingMLflow Model RegistryContainer Build (Docker)Push to RegistryKubernetes DeployMonitoring:Latency/Err/PSI/KSSteady StateActionRetrain Pipeline TriggerRollback/Canary Adjust

배포가 실패하면 헬스체크 실패 → 자동 롤백 → 알림 → 원인 로그 수집 순으로, 드리프트가 임계치를 넘으면 알림 → 재학습 잡 트리거 → 검증 통과 시 카나리 10%→50%→100% 순으로 진행하는 것이 일반적인 대응 절차다. 모델 레지스트리에서 새 버전을 승격할 때는 단일 작성자 정책과 단계적 롤아웃을 함께 적용해 동시 승격으로 인한 충돌을 막는다.

실제로 배포해보기

FastAPI/uvicorn으로 /predict 엔드포인트를 노출하고 프로메테우스 메트릭을 함께 공개하는 서버를 기준으로 한다. (Python 3.10+, Docker 24+, Kubernetes 1.26+, MLflow 2.x 기준.)

# Dockerfile
FROM python:3.10-slim
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py model.pkl .
ENV PORT=8080
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8080"]
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ml-inference
spec:
  replicas: 3
  selector: { matchLabels: { app: ml-inference } }
  template:
    metadata:
      labels: { app: ml-inference }
    spec:
      containers:
        - name: server
          image: <REGISTRY>/ml-inference:1.0.0
          ports: [{ containerPort: 8080 }]
          resources:
            requests: { cpu: "250m", memory: "256Mi" }
            limits: { cpu: "1", memory: "1Gi" }
          readinessProbe:
            httpGet: { path: /healthz, port: 8080 }
            initialDelaySeconds: 5
          livenessProbe:
            httpGet: { path: /healthz, port: 8080 }
            initialDelaySeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: ml-inference-svc
spec:
  selector: { app: ml-inference }
  ports: [{ port: 80, targetPort: 8080 }]
  type: ClusterIP

readinessProbe와 livenessProbe를 반드시 함께 두는 이유는 단순하다 — 모델 로딩이 끝나기 전에 트래픽이 들어오는 것과, 좀비 상태로 죽지 않고 남아 있는 파드를 각각 막아야 하기 때문이다.

드리프트를 어떻게 잡아내는가

서비스 수준 지표(지연 p95, 에러율, QPS)만으로는 모델이 "조용히" 나빠지는 걸 잡을 수 없다. 입력 분포 자체가 바뀌는 데이터 드리프트, 입력과 타깃의 관계가 바뀌는 컨셉 드리프트를 따로 봐야 한다. 실무에서는 PSI(Population Stability Index)와 KS(Kolmogorov-Smirnov) 검정을 가장 많이 쓴다.

절차는 대략 이렇다. 먼저 학습/검증 데이터의 분포를 베이스라인으로 저장해두고, 서빙 중인 입력·출력을 샘플링해 실시간으로 수집한 뒤, PSI/KS를 계산해 임계치(예: 경고 0.1, 심각 0.2)와 비교한다. 임계치를 넘으면 알림을 보내고 카나리를 중단하거나 롤백한 뒤 재학습 파이프라인을 돌리고, 새 모델이 검증을 통과하면 다시 승격하는 피드백 루프를 돈다.

# drift_detect.py
import numpy as np, pandas as pd
from scipy.stats import ks_2samp

def psi(expected, actual, bins=10):
    e_perc, a_perc = np.histogram(expected, bins=bins)[0], np.histogram(actual, bins=bins)[0]
    e_perc = e_perc / (e_perc.sum() + 1e-9)
    a_perc = a_perc / (a_perc.sum() + 1e-9)
    e_perc = np.where(e_perc == 0, 1e-6, e_perc)
    a_perc = np.where(a_perc == 0, 1e-6, a_perc)
    return np.sum((a_perc - e_perc) * np.log(a_perc / e_perc))

def detect_drift(df_ref, df_cur, num_cols, psi_th=0.2, ks_th=0.1):
    report = []
    for col in num_cols:
        p = psi(df_ref[col].values, df_cur[col].values)
        ks = ks_2samp(df_ref[col].values, df_cur[col].values, alternative='two-sided').pvalue
        report.append({"feature": col, "psi": float(p), "ks_pvalue": float(ks),
                       "drift": (p >= psi_th) or (ks <= ks_th)})
    return pd.DataFrame(report)

# 사용 예
# ref = pd.read_parquet("baseline.parquet"); cur = pd.read_parquet("serving_sample.parquet")
# print(detect_drift(ref, cur, num_cols=["age","income"]))

단변량 지표만으로는 여러 피처가 함께 움직이는 다변량 드리프트를 놓칠 수 있어서, 최종 스코어 분포나 이분류기 기반의 모델 기반 감지를 함께 쓰는 편이 안전하다. 서빙 데이터를 수집할 때는 샘플링·익명화로 개인정보를 보호하고, 피처 스토어나 Kafka 같은 메시지 큐로 수집 경로를 표준화해두는 게 나중에 편하다.

MLflow로 실험과 승격을 관리하기

학습을 돌릴 때마다 파라미터·메트릭·아티팩트(모델, 피처 중요도, SHAP 값)를 기록해두지 않으면 "지난주에 왜 성능이 더 좋았는지"를 재현할 방법이 없다. MLflow Tracking이 이 역할을 하고, 모델 서명(signature)과 입력 예시를 함께 저장해두면 서빙 시점의 호환성 문제도 미리 걸러낼 수 있다.

# train_and_register.py
import mlflow, mlflow.sklearn
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import roc_auc_score

mlflow.set_tracking_uri("http://<mlflow-tracking>:5000")
mlflow.set_experiment("fraud-detector")

X, y = load_breast_cancer(return_X_y=True, as_frame=True)
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.2, random_state=42)

with mlflow.start_run() as run:
    model = RandomForestClassifier(n_estimators=200, random_state=42)
    model.fit(Xtr, ytr)
    auc = roc_auc_score(yte, model.predict_proba(Xte)[:,1])

    mlflow.log_param("n_estimators", 200)
    mlflow.log_metric("auc", auc)
    mlflow.sklearn.log_model(model, "model", registered_model_name="fraud-detector")

    print("AUC:", auc)

Model Registry는 여기서 한 단계 더 나아가 Staging 환경에 자동 배포하고, 승인 후 Production으로 승격하며, 변경 이력과 심사 로그를 남긴다. CI/CD 파이프라인에서 기준 메트릭 회귀 검증을 통과했을 때만 자동 승격시키고 실패하면 차단하는 구성도 가능하다. 다만 이 자동화 수준은 팀의 리스크 허용도에 달려 있다 — 수동 승인으로 두면 안전성이 올라가고, 자동화하면 배포 속도가 빨라진다. 아티팩트 스토어(S3/GCS/Azure Blob)는 권한을 분리해 접근을 통제하고, 트래킹 메타데이터는 관리형 DB(RDS 등)에 두어 신뢰성을 확보하는 편이 좋다.

보안과 운영에서 놓치기 쉬운 것들

이미지 스캔(trivy)과 서명(cosign), 시크릿 관리(Secrets Manager/KMS), NetworkPolicy는 모델 서빙 인프라에도 똑같이 적용된다. PII가 섞인 데이터를 다룬다면 마스킹과 최소 권한 원칙을 별도로 신경 써야 한다. 서빙, 피처 스토어, 트래킹 서버는 분리 배치하고 Readiness/Liveness, PodDisruptionBudget, HPA를 갖춰야 하며, 지연이 문제라면 캐싱이나 배치 프리컴퓨트로 완화할 수 있다. SLO(예: p95 100ms 미만, 에러율 1% 미만)를 정의하고 에러 버짓 기준으로 배포 빈도를 조절하며, 블루/그린을 기본으로 하되 카나리로 미세 조정하고 롤백 경로를 상시 검증해두는 것이 사고를 줄이는 가장 확실한 방법이다.

어떤 상황에 이 구조가 필요한가

고QPS·저지연이 필요한 실시간 사기 탐지는 PSI 기반 입력 분포 감시로 이벤트 변화에 대응해야 하고, 추천 시스템은 컨셉 드리프트에 대응하기 위한 주기적 오프라인 재학습과 온라인 A/B 검증이 필요하다. 수요 예측처럼 배치로 도는 파이프라인은 스케줄링과 모델·데이터 버전 고정으로 회귀(regression)를 막는 쪽에 무게가 실린다. 세 사례 모두 결국 "언제 모델이 나빠지는지 어떻게 알고, 어떻게 안전하게 되돌리는지"라는 같은 질문에 답하는 구조라는 점은 같다.

이런 체계를 갖추면 실험에서 배포까지의 리드타임이 30~70% 줄고, 자동 롤백·승격 게이트로 장애 회복 시간이 단축되며, 감사 로그와 버전 이력 덕분에 컴플라이언스 대응도 쉬워진다.

MLOpsKubernetesMLflow드리프트탐지모델배포