로컬 주피터 노트북(Jupyter Notebook) 환경에서는 완벽하게 동작하던 모델이 프로덕션 환경에 배포되는 순간 ModuleNotFoundError를 뱉어내거나, 추론(Inference) 속도가 현저히 느려지는 현상은 매우 흔한 문제입니다. 더 심각한 것은 배포 직후에는 성능이 좋다가 시간이 지날수록 정확도가 하락하는 '성능 부식(Performance Decay)' 현상입니다. 이는 단순히 모델링의 문제가 아니라, 모델을 둘러싼 인프라스트럭처와 운영 파이프라인의 부재에서 기인합니다. Google의 논문 <Hidden Technical Debt in Machine Learning Systems>에서 지적했듯, ML 시스템에서 실제 ML 코드는 매우 작은 부분이며 나머지는 서빙 인프라, 모니터링, 데이터 검증 등이 차지합니다.
1. CI/CD를 넘어 CT(Continuous Training)로
전통적인 소프트웨어 엔지니어링의 DevOps가 코드(Code)의 통합과 배포에 집중한다면, MLOps는 코드, 데이터(Data), 그리고 모델(Model)이라는 세 가지 축을 관리해야 합니다. 따라서 CI(Continuous Integration)와 CD(Continuous Deployment) 외에도 CT(Continuous Training)라는 개념이 필수적으로 요구됩니다.
CT 파이프라인은 새로운 데이터가 유입되거나 모델 성능이 임계치 이하로 떨어졌을 때, 자동으로 재학습을 트리거(Trigger)하고 검증하여 배포하는 프로세스를 의미합니다. 이를 수동으로 처리하는 것은 재현성(Reproducibility)을 해치고 휴먼 에러를 유발하는 가장 큰 원인입니다.
재현성을 확보하기 위해서는 코드 버전(Git SHA), 데이터 버전(DVC 등), 하이퍼파라미터, 실행 환경(Docker Image Digest)이 하나의 스냅샷으로 관리되어야 합니다. 단순히 랜덤 시드(Seed)를 고정하는 것만으로는 충분하지 않습니다.
2. 오케스트레이션 도구 선택: Kubeflow vs MLflow
MLOps 파이프라인 구축 사례를 살펴보면 가장 많이 거론되는 두 도구가 Kubeflow와 MLflow입니다. 이 둘은 상호 배타적이기보다 보완적인 관계일 수 있으나, 아키텍처 설계 시 명확한 장단점 비교가 필요합니다.
| Feature | Kubeflow | MLflow |
|---|---|---|
| 기반 인프라 | Kubernetes Native (K8s 의존성 높음) | Python Library (인프라 독립적) |
| 주요 강점 | 복잡한 워크플로우 오케스트레이션, 확장성 | 실험 추적(Experiment Tracking), 모델 레지스트리 |
| 진입 장벽 | 매우 높음 (K8s, Manifest, Istio 이해 필요) | 낮음 (pip install mlflow) |
| 적합한 조직 | 엔터프라이즈, 전담 플랫폼 팀 존재 | 데이터 사이언스팀 중심, 빠른 프로토타이핑 |
대규모 트래픽을 처리하고 GPU 리소스를 동적으로 할당해야 하는 환경이라면 Kubeflow Pipelines(KFP)가 사실상 표준입니다. KFP는 각 단계를 컨테이너 단위로 격리하여 실행하므로 의존성 충돌(Dependency Hell)을 원천 차단합니다.
3. Kubeflow Pipelines(KFP) 구현 패턴
KFP를 사용하여 데이터 전처리부터 모델 학습까지 연결하는 DAG(Directed Acyclic Graph)를 구성할 때, 재사용 가능한 컴포넌트(Reusable Components) 설계가 중요합니다. 아래는 KFP SDK v2를 사용한 파이프라인 정의 예시입니다.
from kfp import dsl
from kfp.dsl import Input, Output, Dataset, Model
# 전처리 컴포넌트 정의
# Base Image를 명시하여 실행 환경을 고정해야 함
@dsl.component(base_image='python:3.9', packages_to_install=['pandas', 'scikit-learn'])
def preprocess_data(
raw_data_path: str,
train_dataset: Output[Dataset],
test_dataset: Output[Dataset]
):
import pandas as pd
from sklearn.model_selection import train_test_split
# 데이터 로드 및 전처리 로직
df = pd.read_csv(raw_data_path)
train, test = train_test_split(df, test_size=0.2)
# Artifact로 저장 (KFP가 경로 자동 관리)
train.to_csv(train_dataset.path, index=False)
test.to_csv(test_dataset.path, index=False)
# 파이프라인 정의
@dsl.pipeline(
name='production-training-pipeline',
description='Continuous Training Pipeline with Drift Check'
)
def training_pipeline(raw_data_url: str):
# 컴포넌트 연결
preprocess_task = preprocess_data(raw_data_path=raw_data_url)
# 캐싱(Caching) 전략: 동일한 입력에 대해 재실행 방지하여 리소스 절약
preprocess_task.set_caching_options(True)
# 학습 컴포넌트 (생략된 train_model 함수 연결)
# train_task = train_model(train_data=preprocess_task.outputs['train_dataset'])
파이프라인의 각 단계마다 무거운 베이스 이미지를 사용하면 Pod 초기화 시간(Cold Start)이 길어집니다. 자주 사용하는 라이브러리는 미리 빌드된 커스텀 도커 이미지를 사용하여 런타임 설치 시간을 줄여야 합니다.
4. Data Drift와 Concept Drift 모니터링
모델 배포는 끝이 아니라 시작입니다. 머신러닝 모델 모니터링 방법 중 가장 중요한 것은 시스템 메트릭(CPU/Memory)이 아니라 데이터 분포의 변화를 감지하는 것입니다. 이를 무시하면 모델은 정상적으로 응답하지만(HTTP 200), 비즈니스 가치는 하락하는 '침묵의 실패'가 발생합니다.
- Data Drift (Covariate Shift): 입력 데이터 $P(X)$의 분포가 학습 데이터와 달라지는 현상입니다. 예를 들어, 사용자 연령층이 20대에서 50대로 이동한 경우입니다.
- Concept Drift: 입력과 출력 사이의 관계 $P(Y|X)$가 변하는 현상입니다. 예를 들어, '스팸 메일'의 정의나 패턴 자체가 변하는 경우입니다.
이를 감지하기 위해 프로덕션 환경에서는 KS Test(Kolmogorov-Smirnov Test)나 KL Divergence(Kullback-Leibler Divergence)와 같은 통계적 기법을 사용하여 입력 데이터의 분포 거리를 실시간으로 계산해야 합니다. TensorFlow Data Validation(TFDV)나 Evidently AI 같은 도구를 서빙 레이어의 사이드카(Sidecar) 패턴으로 배치하여 Drift 발생 시 즉시 Alert를 보내도록 구성하십시오.
결론 및 아키텍처 제언
성공적인 MLOps 파이프라인 구축은 도구의 도입보다 프로세스의 정립이 우선입니다. 초기 단계에서는 MLflow와 같은 경량 도구로 실험 추적을 자동화하고, 모델의 복잡도와 팀 규모가 커짐에 따라 Kubeflow 또는 매니지드 서비스(AWS SageMaker, Vertex AI)로 전환하는 점진적 마이그레이션 전략을 권장합니다. 무엇보다 모델의 생애 주기를 코드 레벨에서 통제하고, Drift 감지를 통한 지속적인 재학습 루프를 완성하는 것이 안정적인 AI 서비스 운영의 핵심입니다.
Post a Comment