ML CI/CD 파이프라인 — 테스트 게이트에서 드리프트 기반 재학습까지

데이터 계약·다계층 테스트·레지스트리 게이트·카나리 릴리스를 하나로 엮어 드리프트가 재학습을 자동으로 트리거하는 ML 전용 CI/CD 파이프라인을 구조와 코드로 정리한다.

2026-08-12 · 최초 발행 2024-04-29

모델 코드 한 줄을 고치는 것과 피처 파이프라인의 스키마가 바뀌는 것, 학습 데이터의 분포가 조금씩 밀리는 것은 전부 다른 종류의 변경이지만 결국 같은 질문으로 수렴한다 — 이 변경이 프로덕션에 나가도 되는가. 일반 소프트웨어의 CI/CD는 코드 아티팩트의 빌드·테스트·배포만 관리하면 되지만, ML 파이프라인은 여기에 데이터·피처·실험 메타데이터의 일관성과 모델 성능 회귀, 그리고 배포 이후에도 계속 움직이는 데이터 드리프트까지 얹어야 한다. 그래서 CI(테스트)와 CD(배포) 사이에 CT(Continuous Training) — 드리프트나 성능 저하가 감지되면 재학습을 자동으로 트리거하는 루프가 하나 더 들어간다.

전체 흐름을 그려보면 이렇다.

성공실패통과실패YesNo코드/피처/스키마 변경CI: Lint/Unit/Data TestsTrain & Evaluate: 재현가능한 실행Fail Fast: 알림/이슈 생성Package: Docker + ModelSignatureRegistry: 등록 + 메트릭 +Stage=StagingGates:성능/공정성/지연/비용CD:Canary/Shadow/Blue-GreenBlock + ReviewMonitoring: Data/Perf/Infra드리프트 또는 계약 위반?CT: Feature Store 기반재학습정상 상태 유지

레지스트리 승격은 Stage 변경과 서빙 리비전 업데이트를 원자적으로 처리해야 하고, 카나리 도중 장애가 나면 트래픽을 0%로 되돌리는 자동 롤백이 걸려 있어야 한다. 서빙 직전에는 피처·모델 버전 호환성 체크리스트를 자동으로 돌려 스큐를 미리 걸러낸다.

데이터 계약과 테스트 게이트

파이프라인 초입에서 막아야 할 건 코드 버그가 아니라 데이터 문제다. 스키마, 허용 범위, 결측·이상치 정책, SLO를 명시한 데이터 계약을 두고 위반 시 파이프라인을 즉시 실패 처리한다. 코드는 Git, 데이터는 DVC나 Delta, 실험은 MLflow의 참조 해시로 묶어야 "이 모델이 정확히 어떤 데이터·코드로 나왔는지"를 재현할 수 있다.

테스트는 세 층위로 나눠 돌린다. 전처리·피처 함수에 대한 단위 테스트, 스키마와 분포를 검증하는 데이터 테스트, 성능과 공정성을 보는 모델 테스트다. 여기에 p50/p95 레이턴시, 메모리·CPU·GPU 사용량, 동시성 부하 같은 리소스·성능 테스트를 더해 성능 회귀를 배포 전에 잡아낸다. 이 세 층위가 전부 배포 전 게이트로 자동화돼 있어야 사람이 매번 "이번 모델 괜찮은지" 눈으로 확인하는 일을 줄일 수 있다.

레지스트리 게이트를 통과해야 승격된다

테스트를 통과한 모델은 Docker나 Conda로 환경을 고정해 모델과 전처리 파이프라인을 함께 패키징한다. 이때 모델 시그니처와 입출력 스키마를 아티팩트에 내장해두면 서빙 시점의 호환성 문제를 미리 걸러낼 수 있다.

레지스트리에 등록될 때는 기준선 대비 상대·절대 임계치, 안정성·공정성 규칙, 통계적 유의성 검정을 게이트로 적용한다. 이 게이트를 통과해야 Staging에서 Production으로 승격되고, 실패하면 차단하고 리뷰로 넘긴다. 모델 레지스트리는 버전, 메타데이터, 메트릭, 아티팩트, 그리고 지금 이 모델이 어느 단계(Staging→Production)에 있는지를 관리하는 축이고, 피처 스토어는 온라인·오프라인 피처의 일관성과 백필·타임트래블을 담당하는 축이다. 이 둘이 없으면 "왜 서빙 시점의 피처 값이 학습 시점과 다른가" 같은 스큐 문제를 사후에 디버깅하게 된다.

릴리스는 카나리·블루그린·섀도우 중에서

게이트를 통과했다고 바로 전체 트래픽에 내보내는 건 아니다. 점진적으로 트래픽을 늘리는 카나리, 스위치 한 번으로 전환하는 블루그린, 실제 응답에는 영향을 주지 않고 복제 추론만 돌려보는 섀도우 중에서 상황에 맞게 고른다. 서빙 방식도 REST/gRPC, 배치, 스트리밍·온라인을 혼합해 쓰는데, 피처 스토어를 온라인 서빙에 붙여두면 학습-서빙 스큐를 줄이는 데 도움이 된다.

아래는 일반 DevOps CI/CD와 비교했을 때 어디가 달라지는지 정리한 것이다.

항목 일반 DevOps CI/CD ML Ops CI/CD
성능 바이너리/서비스 성능 회귀 중심 모델 성능(AUC/F1) + 추론 레이턴시 동시 관리
확장성 스테이트리스 서비스 확장 위주 모델/피처 서비스 동시 확장, GPU/배치·온라인 이중화
일관성 코드 아티팩트 위주 코드·데이터·피처·실험 메타데이터 일관성 관리
안정성 롤백/헬스체크 표준화 카나리/섀도우로 통계적 유효성 확인 후 승격
운영 편의 빌드/배포 파이프라인 단순 데이터 테스트·드리프트·재학습 트리거 포함한 복합 파이프라인 운영

GitHub Actions로 짜보는 실제 파이프라인

전제조건은 GitHub Actions, Docker, MLflow 2.x, Python 3.10이다. CI 잡에서 데이터·단위 테스트를 돌리고 학습·평가를 거쳐 이미지를 빌드·푸시하면, CD 잡이 이어받아 카나리로 배포한다.

name: ml-ci-cd
on:
  push:
    branches: [ main ]
jobs:
  ci:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: '3.10' }
      - run: pip install -r requirements.txt
      - name: Data & Unit Tests
        run: pytest -m "unit or data"
      - name: Train & Evaluate
        run: python pipelines/train.py --config configs/prod.yaml
      - name: Package Image
        run: docker build -t ghcr.io/org/model:${{ github.sha }} .
      - name: Push Image
        run: echo "${{ secrets.GHCR_TOKEN }}" | docker login ghcr.io -u USER --password-stdin && docker push ghcr.io/org/model:${{ github.sha }}
  cd:
    needs: ci
    runs-on: ubuntu-latest
    steps:
      - name: Deploy Canary
        run: |
          kubectl set image deploy/ml-svc model=ghcr.io/org/model:${{ github.sha }}
          kubectl annotate deploy/ml-svc canary=true --overwrite

여기서 평가 게이트는 train.py 내부에서 처리한다 — 메트릭이 임계치를 못 넘기면 non-zero로 종료시켜서 이후 스텝(이미지 푸시, 카나리 배포)이 아예 진행되지 않게 막는다.

드리프트가 재학습을 부르는 방식

배포 이후에도 데이터는 계속 움직인다. 입력 분포 자체가 시간이 지나며 바뀌는 게 데이터 드리프트(평균·분산·범주 빈도 변화가 대표적 예), 입력과 타깃의 관계 자체가 바뀌는 게 개념 드리프트(사기 패턴이 바뀌면서 모델의 결정경계가 밀리는 경우)다. 여기에 학습 시점과 서빙 시점의 피처 생성·전처리가 어긋나는 스큐도 별도로 검증해야 한다.

지표는 네 계층으로 본다. 데이터 품질(누락·범위·스키마), 드리프트(PSI/KL/KS/ADWIN), 모델 성능(AUC/F1/MAE), 운영(SLA·오류율)이다. 이 중 드리프트 임계치를 넘거나 성능이 떨어지거나 데이터 계약을 위반하면 CT 파이프라인이 기동하고, 승인을 거쳐 다시 레지스트리로 들어가는 자동 배포 루프가 완결된다.

import pandas as pd
from evidently.report import Report
from evidently.metric_preset import DataDriftPreset

ref = pd.read_parquet("s3://bucket/data/ref.parquet")
cur = pd.read_parquet("s3://bucket/data/current.parquet")

report = Report(metrics=[DataDriftPreset()])
report.run(reference_data=ref, current_data=cur)

summary = report.as_dict()
drifted = summary["metrics"][0]["result"]["dataset_drift"]
psi = summary["metrics"][0]["result"]["share_of_drifted_columns"]

if drifted or psi > 0.2:
    # 재학습 트리거 또는 경보 발송
    print("DRIFT_ALERT"); exit(2)

전제조건은 Python 3.10, pandas, evidently 0.4+다. 스케줄드 잡과 실시간 샘플링을 혼합해서 돌리고, 주요 피처에는 가중치를 준 합성 드리프트 지표를 함께 쓰는 편이 단일 지표보다 안정적이다.

업종별로 어디에 무게를 두는지

전자상거래 추천 시스템은 계절·프로모션에 따라 클릭·구매 분포가 급변하고 카탈로그·가격 피처의 데이터 드리프트가 잦다. 카테고리별 PSI 임계치(예: 0.2)를 넘으면 CT를 트리거하고, 섀도우로 배포해 오프라인 점수와 온라인 CTR 리프트를 함께 본 뒤 카나리로 승격하는 식으로 대응한다.

금융 사기탐지는 공격자의 전략이 바뀌면서 개념 드리프트가 자주 일어나고, 라벨(사기 확정 여부)이 지연 도착한다는 제약이 있다. 라벨 지연을 보정하는 딜레이 버킷과 Precision@Top-K 같은 프록시 성능 지표를 함께 쓰고, KS 검정과 ADWIN을 병행하다가 임계치를 넘으면 규칙 기반 백업 모델로 즉시 폴백한 뒤 야간에 재학습을 돌린다.

제조 비전 품질검사는 조도·카메라 상태가 바뀌면서 이미지 분포가 이동하는데 라벨링 비용이 높다는 게 걸림돌이다. 엣지 대비·밝기 히스토그램 같은 피처 통계의 드리프트를 기준으로 능동 학습 샘플링을 돌리고, 블루그린 전환으로 다운타임 없이 모델을 교체한다.

보안·거버넌스에서 놓치기 쉬운 지점

컨테이너 서명(cosign)과 모델 아티팩트 해시·서명 저장, SBOM(Syft) 생성으로 서플라이체인 리스크를 관리해야 한다. 다만 서명·검증 단계를 추가하면 배포가 그만큼 늦어질 수 있어서 캐시와 병렬 검증으로 지연을 완화하는 게 보통이다.

PII 마스킹, 최소 권한 원칙, 기능별 서비스 계정 분리, 피처 스토어의 읽기·쓰기 권한 분리도 필요하다. 다만 권한을 세분화할수록 운영 복잡도가 올라가므로 IaC와 정책 코드화로 관리 부담을 흡수하는 편이 낫다.

실험 아티팩트 저장 정책과 스토리지 수명주기(Lifecycle) 설정도 미리 정해둬야 한다. 샘플링·서머리 통계만 아카이빙하면 저장 비용은 줄지만 나중에 감사·디버깅이 필요할 때 원본이 없어 곤란해질 수 있다 — 이 균형은 팀의 컴플라이언스 요구 수준에 따라 다르게 잡아야 한다.

이 체계를 갖추면 달라지는 것

배포 리드타임이 50~80% 단축되고, 장애가 나도 롤백까지 15분 이하로 끝난다. 성능 저하를 알아채는 시간(Time-To-Detect)이 24시간에서 2시간 수준으로 줄고, 스큐·데이터 계약 위반의 70% 이상을 사전에 차단해 품질 사고를 줄인다. 운영 SLA로는 p95 추론 지연 150ms 이하, 오류율 0.1% 이하를 유지하는 게 목표치로 흔히 쓰인다.

처음부터 이 전부를 갖추려 하지 말고, 모델 레지스트리·데이터 테스트·카나리 배포·드리프트 알림을 1단계로 먼저 세우는 편이 안전하다. 이후 피처 스토어, 공정성 모니터링, 능동 학습으로 범위를 넓혀가면 된다.

MLOpsCI/CD드리프트탐지모델레지스트리카나리배포