🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

MLOps 모니터링 및 관찰성 완벽 가이드 - 슬라이드 1/8
A

AI Generated

2025. 12. 1. · 15 Views

MLOps 모니터링 및 관찰성 완벽 가이드

ML 시스템의 안정적인 운영을 위한 모니터링과 관찰성 구축 방법을 다룹니다. Prometheus, Grafana, 로그 분석, 모델 성능 추적, Alert Manager, 분산 트레이싱까지 실무에서 바로 적용할 수 있는 내용을 담았습니다.


목차

  1. Prometheus_메트릭_수집
  2. Grafana_Dashboard_구축
  3. 로그_분석_시스템
  4. ML_모델_성능_모니터링
  5. ML_메트릭_추적
  6. Alert_Manager_설정
  7. 분산_트레이싱_적용

1. Prometheus 메트릭 수집

어느 날 김개발 씨가 배포한 ML 모델이 갑자기 느려졌다는 연락을 받았습니다. 하지만 어디서 병목이 발생하는지 전혀 알 수 없었습니다.

"모니터링 시스템이 없으면 눈을 감고 운전하는 것과 같아요." 선배의 말에 김개발 씨는 고개를 끄덕였습니다.

Prometheus는 시계열 데이터베이스 기반의 모니터링 시스템입니다. 마치 병원에서 환자의 심박수, 혈압, 체온을 실시간으로 측정하는 생체 모니터처럼, 서버와 애플리케이션의 상태를 숫자로 기록합니다.

이를 통해 시스템의 건강 상태를 한눈에 파악할 수 있습니다.

다음 코드를 살펴봅시다.

from prometheus_client import start_http_server, Counter, Histogram, Gauge
import time

# 메트릭 정의 - 예측 요청 횟수를 카운트합니다
prediction_requests = Counter('ml_prediction_requests_total', 'Total prediction requests', ['model_name', 'status'])

# 예측 지연 시간을 히스토그램으로 측정합니다
prediction_latency = Histogram('ml_prediction_latency_seconds', 'Prediction latency', ['model_name'])

# 현재 로드된 모델 수를 게이지로 표시합니다
loaded_models = Gauge('ml_loaded_models', 'Number of loaded models')

def predict(model_name, input_data):
    with prediction_latency.labels(model_name=model_name).time():
        # 실제 예측 로직이 여기에 들어갑니다
        result = model.predict(input_data)
        prediction_requests.labels(model_name=model_name, status='success').inc()
        return result

# 메트릭 서버를 8000번 포트에서 시작합니다
start_http_server(8000)

김개발 씨는 입사 6개월 차 ML 엔지니어입니다. 어느 날 새벽, 갑자기 휴대폰이 울렸습니다.

"모델 서버가 이상해요. 응답이 너무 느려요." 운영팀의 긴급 연락이었습니다.

김개발 씨는 서버에 접속해 로그를 뒤지기 시작했습니다. 하지만 수십만 줄의 로그 속에서 문제의 원인을 찾기란 마치 건초더미에서 바늘 찾기와 같았습니다.

두 시간이 지나서야 겨우 메모리 부족이 원인임을 알아냈습니다. 다음 날, 선배 박시니어 씨가 말했습니다.

"모니터링 시스템을 구축해야 해요. Prometheus라는 도구를 써보세요." 그렇다면 Prometheus란 정확히 무엇일까요?

쉽게 비유하자면, Prometheus는 마치 자동차의 계기판과 같습니다. 속도계가 현재 속도를 알려주고, 연료 게이지가 남은 기름을 표시하듯이, Prometheus는 서버의 CPU 사용량, 메모리, 요청 수 같은 지표를 실시간으로 수집합니다.

Prometheus가 없던 시절에는 어땠을까요? 개발자들은 문제가 발생하면 수동으로 로그를 뒤져야 했습니다.

서버 상태를 확인하려면 일일이 SSH로 접속해서 top 명령어를 실행해야 했습니다. 더 큰 문제는 과거 데이터를 볼 수 없다는 것이었습니다.

"어제 이 시간에는 어땠지?"라는 질문에 답할 수 없었습니다. 바로 이런 문제를 해결하기 위해 Prometheus가 등장했습니다.

Prometheus는 Pull 방식으로 메트릭을 수집합니다. 각 서버가 메트릭을 노출하면, Prometheus가 주기적으로 가져가는 방식입니다.

마치 우체부가 각 가정의 우편함에서 편지를 수거해 가는 것과 비슷합니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 Counter는 누적 값을 기록합니다. 예측 요청이 들어올 때마다 1씩 증가하며, 절대 감소하지 않습니다.

웹사이트 방문자 수를 세는 카운터기와 같습니다. 다음으로 Histogram은 값의 분포를 기록합니다.

예측에 0.1초가 걸리는 요청이 몇 개인지, 1초가 걸리는 요청이 몇 개인지 버킷별로 분류합니다. 이를 통해 "전체 요청의 95%가 0.5초 이내에 처리된다"와 같은 분석이 가능해집니다.

마지막으로 Gauge는 현재 값을 나타냅니다. 온도계처럼 올라갔다 내려갔다 할 수 있습니다.

현재 로드된 모델 수, 현재 처리 중인 요청 수 같은 지표에 적합합니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 추천 시스템을 운영한다고 가정해봅시다. 사용자가 상품 페이지를 볼 때마다 추천 모델이 호출됩니다.

Counter로 총 호출 횟수를 세고, Histogram으로 응답 시간 분포를 측정하며, Gauge로 현재 대기 중인 요청 수를 파악할 수 있습니다. 하지만 주의할 점도 있습니다.

메트릭을 너무 많이 만들면 Prometheus 서버에 부담이 됩니다. 또한 레이블(label)을 무분별하게 사용하면 카디널리티가 폭발할 수 있습니다.

예를 들어 사용자 ID를 레이블로 사용하면 수백만 개의 시계열이 생성되어 시스템이 멈출 수 있습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

Prometheus를 도입한 후, 김개발 씨는 더 이상 새벽에 로그를 뒤질 필요가 없어졌습니다. 대시보드 한 눈에 어느 부분이 문제인지 파악할 수 있게 되었습니다.

"역시 모니터링이 기본이구나!" 김개발 씨는 뿌듯하게 웃었습니다.

실전 팁

💡 - 메트릭 이름은 snake_case로 작성하고, 접두사로 서비스명을 붙이세요

  • 레이블의 카디널리티를 항상 고려하세요. 무한히 늘어날 수 있는 값은 레이블로 사용하지 마세요
  • /metrics 엔드포인트는 인증 없이 노출되므로 네트워크 보안에 주의하세요

2. Grafana Dashboard 구축

Prometheus로 메트릭을 수집하기 시작한 김개발 씨. 하지만 숫자만 쌓여가는 것을 보니 답답했습니다.

"이 숫자들을 한눈에 볼 수 있는 방법이 없을까요?" 박시니어 씨가 대답했습니다. "Grafana를 써보세요.

데이터가 그림으로 바뀌면 세상이 달라 보일 거예요."

Grafana는 데이터 시각화 도구입니다. 마치 스프레드시트의 숫자를 차트로 바꾸면 의미가 명확해지듯이, Grafana는 Prometheus의 메트릭을 아름다운 그래프와 대시보드로 변환합니다.

실시간으로 변화하는 시스템 상태를 직관적으로 파악할 수 있습니다.

다음 코드를 살펴봅시다.

# Grafana 대시보드를 코드로 정의하는 예제 (grafonnet 라이브러리 사용)
from grafanalib.core import Dashboard, Graph, Row, Target, YAxes, YAxis
import json

# ML 모델 예측 지연 시간 그래프를 정의합니다
prediction_latency_graph = Graph(
    title="예측 지연 시간 (95th percentile)",
    targets=[
        Target(
            expr='histogram_quantile(0.95, rate(ml_prediction_latency_seconds_bucket[5m]))',
            legendFormat="{{model_name}}",
            refId='A',
        ),
    ],
    yAxes=YAxes(left=YAxis(format='s')),  # 초 단위로 표시
)

# 초당 예측 요청 수 그래프를 정의합니다
requests_per_second = Graph(
    title="초당 예측 요청 수",
    targets=[
        Target(
            expr='rate(ml_prediction_requests_total[1m])',
            legendFormat="{{model_name}} - {{status}}",
            refId='A',
        ),
    ],
)

# 대시보드를 생성합니다
dashboard = Dashboard(title="ML Model Dashboard", rows=[Row(panels=[prediction_latency_graph, requests_per_second])])

김개발 씨는 Prometheus를 설정한 후 /metrics 엔드포인트를 열어보았습니다. 화면에는 수천 줄의 텍스트가 빼곡하게 나열되어 있었습니다.

"ml_prediction_requests_total 1523..."이런 숫자들이 끝없이 이어졌습니다. "이걸 어떻게 해석하라는 거지?" 김개발 씨는 한숨을 쉬었습니다.

숫자는 수집되고 있지만, 의미를 파악하기가 너무 어려웠습니다. 박시니어 씨가 다가와 화면을 보더니 말했습니다.

"Grafana를 연동해야 해요. 데이터 시각화의 마법을 보여드릴게요." 그렇다면 Grafana란 정확히 무엇일까요?

쉽게 비유하자면, Grafana는 마치 주식 차트 프로그램과 같습니다. 주가라는 숫자만 보면 오르는지 내리는지 감이 안 오지만, 차트로 보면 추세가 한눈에 들어옵니다.

Grafana도 마찬가지로 메트릭을 시각화해서 패턴과 이상 징후를 쉽게 발견할 수 있게 해줍니다. Grafana의 핵심은 PromQL입니다.

Prometheus Query Language의 약자로, 수집된 메트릭을 조회하고 가공하는 언어입니다. 위 코드에서 histogram_quantile(0.95, rate(ml_prediction_latency_seconds_bucket[5m]))라는 쿼리를 볼 수 있습니다.

이 쿼리는 "최근 5분간 예측 지연 시간의 95번째 백분위수"를 계산합니다. 왜 95번째 백분위수일까요?

평균값만 보면 함정에 빠질 수 있습니다. 대부분의 요청이 0.1초에 처리되더라도, 일부 요청이 10초씩 걸린다면 사용자 경험에 큰 문제가 됩니다.

95번째 백분위수는 "100명 중 95명은 이 시간 이내에 응답을 받는다"는 의미입니다. 위의 코드를 자세히 살펴보겠습니다.

Target은 하나의 PromQL 쿼리를 의미합니다. legendFormat은 그래프 범례에 표시될 텍스트입니다.

{{model_name}}처럼 중괄호 안에 레이블 이름을 넣으면 자동으로 값이 채워집니다. rate() 함수는 Counter 메트릭의 증가율을 계산합니다.

ml_prediction_requests_total이 1000에서 1060으로 증가했다면, 1분간 60개의 요청이 있었다는 뜻입니다. rate(ml_prediction_requests_total[1m])은 이를 초당 요청 수로 변환합니다.

약 1 req/s가 됩니다. 실제 현업에서 대시보드를 어떻게 구성할까요?

일반적으로 상단에는 핵심 지표를 배치합니다. 현재 RPS(초당 요청 수), 평균 지연 시간, 에러율 같은 숫자를 큰 글씨로 보여줍니다.

중간에는 시계열 그래프를 배치해 추세를 파악합니다. 하단에는 상세 정보나 로그를 표시합니다.

**변수(Variable)**를 활용하면 대시보드를 더 유연하게 만들 수 있습니다. 예를 들어 모델 이름을 변수로 설정하면, 드롭다운에서 모델을 선택할 때마다 해당 모델의 지표만 표시됩니다.

하나의 대시보드로 여러 모델을 모니터링할 수 있습니다. 하지만 주의할 점도 있습니다.

대시보드에 너무 많은 패널을 넣으면 로딩이 느려집니다. 각 패널마다 PromQL 쿼리가 실행되기 때문입니다.

또한 시간 범위를 너무 길게 잡으면 데이터 포인트가 많아져 브라우저가 버벅거릴 수 있습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

Grafana 대시보드를 완성한 김개발 씨는 감탄했습니다. 예전에는 숫자 더미에 불과했던 메트릭이 이제는 살아 움직이는 그래프로 변했습니다.

오후 2시에 트래픽이 급증하는 패턴, 특정 모델의 지연 시간이 서서히 늘어나는 추세가 한눈에 들어왔습니다. "데이터가 말을 하기 시작했어요!" 김개발 씨의 눈이 반짝였습니다.

실전 팁

💡 - 대시보드는 목적별로 분리하세요. 운영용, 디버깅용, 경영진 보고용 등

  • 패널에 적절한 임계값 라인을 추가하면 이상 징후를 빠르게 발견할 수 있습니다
  • 대시보드를 JSON으로 내보내고 Git으로 버전 관리하세요

3. 로그 분석 시스템

어느 날 ML 모델이 이상한 예측 결과를 내놓기 시작했습니다. 메트릭상으로는 에러율도 정상이고, 지연 시간도 괜찮은데 결과가 이상했습니다.

"메트릭만으로는 한계가 있어요. 로그를 분석해야 합니다." 박시니어 씨의 조언에 김개발 씨는 로그 분석 시스템 구축에 나섰습니다.

로그 분석 시스템은 애플리케이션이 남기는 텍스트 기록을 수집하고 검색하는 시스템입니다. 마치 의사가 환자의 차트를 보고 병력을 파악하듯이, 개발자는 로그를 통해 시스템에서 무슨 일이 일어났는지 상세히 알 수 있습니다.

메트릭이 "얼마나"를 알려준다면, 로그는 "무엇이, 왜"를 알려줍니다.

다음 코드를 살펴봅시다.

import logging
import json
from datetime import datetime
from pythonjsonlogger import jsonlogger

# 구조화된 JSON 로그를 설정합니다
logger = logging.getLogger("ml_service")
handler = logging.StreamHandler()

# JSON 형식으로 로그를 출력하도록 설정합니다
formatter = jsonlogger.JsonFormatter(
    fmt='%(asctime)s %(levelname)s %(name)s %(message)s',
    datefmt='%Y-%m-%dT%H:%M:%S'
)
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(logging.INFO)

def predict_with_logging(model_name, input_data, request_id):
    # 예측 시작 로그 - 컨텍스트 정보를 포함합니다
    logger.info("Prediction started", extra={
        "request_id": request_id,
        "model_name": model_name,
        "input_shape": str(input_data.shape),
        "event_type": "prediction_start"
    })

    result = model.predict(input_data)

    # 예측 완료 로그 - 결과 요약을 포함합니다
    logger.info("Prediction completed", extra={
        "request_id": request_id,
        "model_name": model_name,
        "prediction_value": float(result[0]),
        "event_type": "prediction_complete"
    })
    return result

김개발 씨는 곤란한 상황에 빠졌습니다. 추천 모델이 갑자기 이상한 상품을 추천하기 시작한 것입니다.

고양이 사료를 검색한 사용자에게 자동차 타이어를 추천하고 있었습니다. Grafana 대시보드를 열어봤지만, 에러율은 0%, 지연 시간도 정상이었습니다.

"메트릭은 정상인데 왜 이러지?" 김개발 씨는 머리를 쥐어뜯었습니다. 박시니어 씨가 다가와 말했습니다.

"메트릭은 숫자로 된 요약 정보예요. 세부적인 상황을 알려면 로그를 봐야 해요." 그렇다면 로그 분석 시스템이란 정확히 무엇일까요?

쉽게 비유하자면, 메트릭이 건강검진 결과표라면 로그는 의사의 진료 기록입니다. 건강검진 결과에서 "혈압 120/80"이라는 숫자만 보면 정상이지만, 진료 기록을 보면 "환자가 두통을 호소함, 스트레스로 인한 일시적 증상으로 보임"처럼 상세한 맥락을 알 수 있습니다.

과거에는 로그를 어떻게 관리했을까요? 각 서버에 텍스트 파일로 저장하고, 문제가 생기면 SSH로 접속해서 grep 명령어로 검색했습니다.

서버가 10대만 되어도 악몽이 시작됩니다. 각 서버를 일일이 접속해서 로그를 뒤져야 했기 때문입니다.

현대적인 로그 분석 시스템은 이 문제를 해결합니다. ELK 스택(Elasticsearch, Logstash, Kibana)이나 Loki같은 도구를 사용하면 모든 서버의 로그를 한 곳에 모아 검색할 수 있습니다.

마치 도서관의 중앙 검색 시스템처럼, 어떤 서버에서 발생한 로그든 한 번에 찾을 수 있습니다. 위의 코드에서 핵심은 구조화된 로그입니다.

예전 방식으로는 print("예측 완료: user123, model_a, 0.85")처럼 자유 형식으로 로그를 남겼습니다. 하지만 이런 로그는 파싱하기 어렵습니다.

JSON 형식으로 구조화하면 {"user_id": "user123", "model": "model_a", "score": 0.85}처럼 기계가 읽기 쉬운 형태가 됩니다. request_id는 매우 중요합니다.

하나의 요청이 여러 서비스를 거칠 때, 같은 request_id를 사용하면 전체 흐름을 추적할 수 있습니다. 사용자가 "3시에 이상한 결과가 나왔어요"라고 문의하면, 해당 시간대의 request_id로 관련 로그를 모두 찾을 수 있습니다.

실제 현업에서 로그 분석은 어떻게 활용될까요? 예를 들어 모델이 이상한 예측을 했다면, 해당 요청의 입력 데이터를 로그에서 찾아볼 수 있습니다.

"아, 입력 데이터에 null 값이 있었구나!" 하고 원인을 파악할 수 있습니다. 메트릭만으로는 절대 알 수 없는 정보입니다.

하지만 주의할 점도 있습니다. 로그에 민감한 정보를 남기면 안 됩니다.

사용자의 비밀번호, 신용카드 번호, 개인 식별 정보 등은 절대 로그에 포함되면 안 됩니다. 또한 로그를 너무 많이 남기면 저장 비용이 폭증합니다.

INFO 레벨은 적당히, DEBUG 레벨은 개발 환경에서만 사용하는 것이 좋습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

로그 분석 시스템을 도입한 후, 김개발 씨는 문제의 원인을 찾았습니다. 특정 시간대에 피처 스토어에서 잘못된 데이터가 반환되고 있었던 것입니다.

로그에 기록된 입력 데이터를 보니, 사용자의 최근 검색어가 전혀 다른 사용자의 것으로 바뀌어 있었습니다. "로그가 없었으면 영원히 못 찾았을 뻔했네요!" 김개발 씨는 안도의 한숨을 내쉬었습니다.

실전 팁

💡 - 로그 레벨을 적절히 사용하세요. ERROR는 즉시 조치 필요, WARNING은 주의, INFO는 정상 흐름

  • 민감한 정보는 마스킹 처리하세요. 이메일은 "j***@example.com" 형태로
  • 로그 보존 기간을 설정하고, 오래된 로그는 자동 삭제되도록 하세요

4. ML 모델 성능 모니터링

"모델 정확도가 떨어지고 있는 것 같아요." 데이터 분석팀에서 연락이 왔습니다. 김개발 씨는 당황했습니다.

배포할 때는 95%였던 정확도가 지금은 얼마인지 알 수가 없었습니다. "실시간으로 모델 성능을 모니터링해야 해요.

그래야 문제를 빨리 발견할 수 있죠." 박시니어 씨의 조언이 시작되었습니다.

ML 모델 성능 모니터링은 배포된 모델이 실제 환경에서 얼마나 잘 작동하는지 추적하는 것입니다. 마치 자동차를 출고한 후에도 리콜 여부를 판단하기 위해 성능을 모니터링하듯이, ML 모델도 배포 후 지속적으로 성능을 확인해야 합니다.

시간이 지나면 데이터 분포가 바뀌어 모델 성능이 저하될 수 있기 때문입니다.

다음 코드를 살펴봅시다.

from prometheus_client import Gauge, Histogram
import numpy as np
from scipy import stats

# 모델 성능 메트릭을 정의합니다
model_accuracy = Gauge('ml_model_accuracy', 'Model accuracy score', ['model_name', 'version'])
prediction_distribution = Histogram('ml_prediction_distribution', 'Distribution of predictions',
                                     ['model_name'], buckets=[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0])

# 데이터 드리프트 감지를 위한 메트릭입니다
data_drift_score = Gauge('ml_data_drift_score', 'Data drift detection score', ['model_name', 'feature'])

class ModelMonitor:
    def __init__(self, model_name, baseline_distribution):
        self.model_name = model_name
        self.baseline = baseline_distribution  # 학습 시점의 데이터 분포

    def check_data_drift(self, current_data, feature_name):
        # KS 테스트로 분포 변화를 감지합니다
        statistic, p_value = stats.ks_2samp(self.baseline[feature_name], current_data)
        data_drift_score.labels(model_name=self.model_name, feature=feature_name).set(statistic)

        # p_value가 0.05 미만이면 드리프트가 발생한 것입니다
        if p_value < 0.05:
            logger.warning(f"Data drift detected for {feature_name}",
                          extra={"p_value": p_value, "statistic": statistic})
        return statistic, p_value

김개발 씨는 6개월 전에 배포한 상품 추천 모델을 자랑스러워하고 있었습니다. 배포 당시 A/B 테스트에서 클릭률을 15%나 높였기 때문입니다.

그런데 어느 날 마케팅팀에서 이상한 리포트가 올라왔습니다. "추천 상품 클릭률이 계속 떨어지고 있어요." 김개발 씨는 모델 코드를 확인했습니다.

바뀐 게 없었습니다. 인프라도 정상이었습니다.

그런데 왜 성능이 떨어지는 걸까요? 박시니어 씨가 설명했습니다.

"ML 모델은 시간이 지나면 성능이 저하돼요. 이걸 모델 드리프트라고 해요.

세상이 변하니까요." 그렇다면 모델 드리프트란 정확히 무엇일까요? 쉽게 비유하자면, 1년 전에 만든 유행어 사전과 같습니다.

1년 전에는 정확했지만, 새로운 유행어가 계속 생기면서 사전의 정확도는 떨어집니다. ML 모델도 마찬가지입니다.

학습할 때의 세상과 지금의 세상이 다르기 때문에 성능이 저하됩니다. 드리프트에는 두 가지 종류가 있습니다.

데이터 드리프트는 입력 데이터의 분포가 바뀌는 것입니다. 예를 들어 코로나 이전에는 여행 상품이 인기였지만, 코로나 이후에는 집콕 용품이 인기가 되었습니다.

컨셉 드리프트는 입력과 출력의 관계가 바뀌는 것입니다. 예전에는 저렴한 상품을 좋아하던 고객이 이제는 프리미엄 상품을 선호하게 된 경우입니다.

위의 코드에서 KS 테스트(Kolmogorov-Smirnov test)를 사용합니다. 이 통계 검정은 두 분포가 같은지 다른지 판단합니다.

학습 시점의 데이터 분포와 현재 데이터 분포를 비교해서, p-value가 0.05 미만이면 "분포가 유의미하게 바뀌었다"고 판단합니다. prediction_distribution 히스토그램도 중요합니다.

모델의 예측값 분포를 추적합니다. 정상적인 상황에서는 예측값이 고르게 분포해야 합니다.

그런데 갑자기 모든 예측이 0.5 근처로 몰린다면? 모델이 제대로 작동하지 않는다는 신호입니다.

실제 현업에서는 어떻게 활용할까요? 매일 자동으로 드리프트 검사를 실행하고, 임계값을 넘으면 알림을 보냅니다.

드리프트가 감지되면 데이터 사이언티스트가 원인을 분석하고, 필요하면 모델을 재학습합니다. 이 과정을 자동화하면 **지속적 학습(Continuous Training)**이 됩니다.

하지만 주의할 점도 있습니다. 모든 분포 변화가 문제인 것은 아닙니다.

계절성이 있는 비즈니스라면 여름과 겨울의 데이터 분포가 다른 것은 당연합니다. 이런 경우는 드리프트가 아니라 정상적인 패턴입니다.

도메인 지식을 바탕으로 판단해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

모델 성능 모니터링을 도입한 후, 김개발 씨는 원인을 찾았습니다. 새로운 카테고리의 상품이 대량으로 추가되면서 데이터 분포가 바뀌었던 것입니다.

기존 모델은 새 카테고리를 전혀 학습하지 못했기 때문에 엉뚱한 추천을 하고 있었습니다. "정기적으로 재학습하는 파이프라인을 만들어야겠어요!" 김개발 씨는 다음 프로젝트를 계획하기 시작했습니다.

실전 팁

💡 - 모든 피처에 대해 드리프트를 검사하면 너무 많은 알림이 발생합니다. 중요한 피처만 선별하세요

  • 드리프트 감지와 모델 재학습을 자동화하면 MLOps 성숙도가 크게 높아집니다
  • A/B 테스트와 함께 사용하면 모델 업데이트의 실제 효과를 검증할 수 있습니다

5. ML 메트릭 추적

"이 모델 언제 학습한 거예요? 하이퍼파라미터가 뭐였죠?" 갑자기 날아온 질문에 김개발 씨는 당황했습니다.

3개월 전에 학습한 모델인데, 기록이 남아있지 않았습니다. "실험 추적 없이 ML을 하는 건, 실험 노트 없이 연구하는 것과 같아요." 박시니어 씨의 말에 김개발 씨는 MLflow를 도입하기로 결심했습니다.

ML 메트릭 추적은 모델 학습 과정의 모든 정보를 기록하고 관리하는 것입니다. 마치 요리사가 레시피 노트에 재료, 조리 시간, 온도를 꼼꼼히 기록하듯이, ML 엔지니어는 하이퍼파라미터, 메트릭, 데이터셋 버전을 체계적으로 기록해야 합니다.

이를 통해 실험을 재현하고, 최적의 모델을 찾을 수 있습니다.

다음 코드를 살펴봅시다.

import mlflow
from mlflow.tracking import MlflowClient

# MLflow 실험을 설정합니다
mlflow.set_experiment("recommendation_model_v2")

def train_model(params, train_data, val_data):
    # 실험 실행을 시작합니다
    with mlflow.start_run(run_name=f"lr_{params['learning_rate']}_bs_{params['batch_size']}"):
        # 하이퍼파라미터를 기록합니다
        mlflow.log_params(params)
        mlflow.log_param("data_version", "2024-01-15")

        model = train(params, train_data)

        # 학습 과정의 메트릭을 기록합니다
        for epoch in range(params['epochs']):
            loss = compute_loss(model, train_data)
            val_accuracy = evaluate(model, val_data)
            mlflow.log_metrics({"loss": loss, "val_accuracy": val_accuracy}, step=epoch)

        # 최종 모델을 저장합니다
        mlflow.sklearn.log_model(model, "model")

        # 추가 정보를 아티팩트로 저장합니다
        mlflow.log_artifact("feature_importance.png")
        mlflow.set_tag("model_type", "gradient_boosting")

        return model

김개발 씨는 지난 한 달간 50번이 넘는 실험을 했습니다. 학습률을 바꿔보고, 배치 사이즈를 조정하고, 피처를 추가하고 제거했습니다.

마침내 만족스러운 모델이 나왔습니다. 그런데 문제가 생겼습니다.

"이 모델의 학습률이 뭐였지?" 엑셀에 기록한 것 같은데 어떤 파일인지 모르겠습니다. "피처는 뭘 썼더라?" 노트에 적어둔 것 같은데 찾을 수가 없습니다.

결국 모델을 처음부터 다시 학습해야 했습니다. 박시니어 씨가 말했습니다.

"ML 실험은 재현 가능해야 해요. 같은 조건으로 다시 실행하면 같은 결과가 나와야 합니다.

그러려면 모든 것을 기록해야 해요." 그렇다면 ML 메트릭 추적이란 정확히 무엇일까요? 쉽게 비유하자면, 과학 실험의 실험 노트와 같습니다.

과학자가 실험할 때 사용한 시약의 양, 온도, 시간을 꼼꼼히 기록하듯이, ML 엔지니어도 학습률, 배치 사이즈, 에폭 수를 체계적으로 기록합니다. 나중에 "이 실험을 다시 해봐"라고 하면 노트만 보고 똑같이 재현할 수 있어야 합니다.

MLflow는 가장 널리 사용되는 ML 실험 추적 도구입니다. 세 가지 핵심 개념이 있습니다.

Experiment는 관련된 실험들의 묶음입니다. "추천 모델 v2 개발"처럼 프로젝트 단위로 생성합니다.

Run은 한 번의 실험 실행입니다. Artifact는 실험 과정에서 생성된 파일들입니다.

위의 코드를 자세히 살펴보겠습니다. mlflow.log_params()는 하이퍼파라미터를 기록합니다.

학습률, 배치 사이즈 같은 설정값입니다. 한 번 기록하면 변경되지 않습니다.

mlflow.log_metrics()는 성능 지표를 기록합니다. step 파라미터로 에폭별 변화를 추적할 수 있습니다.

mlflow.sklearn.log_model()은 학습된 모델 자체를 저장합니다. 나중에 이 모델을 그대로 불러와서 예측에 사용할 수 있습니다.

mlflow.log_artifact()는 그래프, 설정 파일 등 추가 자료를 저장합니다. 실제 현업에서는 어떻게 활용할까요?

MLflow UI에서 모든 실험을 한눈에 비교할 수 있습니다. "학습률 0.01과 0.001 중 어떤 게 더 좋았지?"라는 질문에 클릭 몇 번으로 답할 수 있습니다.

차트로 학습 곡선을 비교하고, 표로 최종 성능을 정렬할 수 있습니다. 모델 레지스트리는 더 발전된 기능입니다.

실험 단계의 모델 중 프로덕션에 배포할 모델을 선택하고, 버전을 관리합니다. "v1.2.0을 프로덕션에 배포하고, v1.1.0은 스테이징으로 내리자"와 같은 워크플로우가 가능합니다.

하지만 주의할 점도 있습니다. 모든 것을 기록하려고 하면 오버헤드가 커집니다.

빠른 프로토타이핑 단계에서는 가볍게, 본격적인 실험 단계에서는 꼼꼼하게 기록하는 것이 좋습니다. 또한 데이터 버전 관리도 함께 해야 합니다.

같은 코드, 같은 파라미터라도 데이터가 다르면 결과가 달라지기 때문입니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

MLflow를 도입한 후, 김개발 씨의 삶이 바뀌었습니다. "이 모델 어떻게 만들었어요?"라는 질문에 MLflow 링크 하나로 답할 수 있게 되었습니다.

실험 결과를 팀원들과 공유하기도 쉬워졌습니다. "이제야 제대로 된 ML 엔지니어가 된 것 같아요!" 김개발 씨는 뿌듯하게 웃었습니다.

실전 팁

💡 - 실험 이름은 명확하게 지으세요. 나중에 찾기 쉽도록 날짜나 목적을 포함하면 좋습니다

  • Git 커밋 해시도 함께 기록하면 코드 버전도 추적할 수 있습니다
  • 대용량 데이터는 log_artifact 대신 외부 스토리지 링크를 기록하세요

6. Alert Manager 설정

새벽 3시, 모델 서버가 다운되었습니다. 하지만 아무도 몰랐습니다.

아침에 출근해서야 밤새 서비스가 멈춰있었다는 것을 알게 되었습니다. "모니터링만 하면 뭐해요.

문제가 생기면 알려줘야죠!" 박시니어 씨의 질책에 김개발 씨는 알림 시스템 구축에 나섰습니다.

Alert Manager는 모니터링 시스템에서 이상 징후를 감지하면 담당자에게 알림을 보내는 도구입니다. 마치 가정의 화재 경보기처럼, 문제가 발생하면 즉시 경고음을 울려 빠른 대응을 가능하게 합니다.

아무리 좋은 모니터링 대시보드가 있어도, 24시간 지켜보고 있을 수는 없기 때문입니다.

다음 코드를 살펴봅시다.

# alertmanager.yml - Alert Manager 설정 파일
global:
  slack_api_url: 'https://hooks.slack.com/services/xxx/yyy/zzz'

route:
  receiver: 'slack-notifications'
  group_by: ['alertname', 'model_name']
  group_wait: 30s      # 같은 그룹의 알림을 30초간 모읍니다
  group_interval: 5m   # 5분마다 그룹 알림을 보냅니다
  repeat_interval: 4h  # 해결되지 않으면 4시간마다 반복합니다

  routes:
    - match:
        severity: critical
      receiver: 'pagerduty-critical'  # 심각한 문제는 PagerDuty로
    - match:
        severity: warning
      receiver: 'slack-notifications'

receivers:
  - name: 'slack-notifications'
    slack_configs:
      - channel: '#ml-alerts'
        title: '{{ .GroupLabels.alertname }}'
        text: '{{ range .Alerts }}{{ .Annotations.description }}{{ end }}'

  - name: 'pagerduty-critical'
    pagerduty_configs:
      - service_key: 'your-pagerduty-key'

# prometheus_rules.yml - 알림 규칙 정의
groups:
  - name: ml_alerts
    rules:
      - alert: HighPredictionLatency
        expr: histogram_quantile(0.95, rate(ml_prediction_latency_seconds_bucket[5m])) > 1
        for: 5m
        labels:
          severity: warning
        annotations:
          description: '모델 {{ $labels.model_name }}의 95% 지연 시간이 1초를 초과했습니다'

김개발 씨는 Grafana 대시보드를 열심히 만들었습니다. 예쁜 그래프와 유용한 지표가 가득했습니다.

하지만 문제가 있었습니다. 대시보드는 누군가 봐야 의미가 있는데, 모든 시간 지켜볼 수는 없었습니다.

어느 날 새벽, 모델 서버의 메모리가 가득 차서 서비스가 중단되었습니다. 대시보드에는 빨간 경고등이 켜져 있었지만, 보는 사람이 없었습니다.

아침에 출근한 김개발 씨는 6시간이나 서비스가 멈춰있었다는 것을 알고 충격을 받았습니다. 박시니어 씨가 말했습니다.

"대시보드는 사후 분석용이에요. 실시간 대응이 필요하면 알림 시스템을 구축해야 해요." 그렇다면 Alert Manager란 정확히 무엇일까요?

쉽게 비유하자면, 건물의 화재 경보 시스템과 같습니다. 연기 감지기가 연기를 감지하면 경보가 울리고, 소방서에 자동으로 신고가 갑니다.

Alert Manager도 마찬가지로 Prometheus가 이상 징후를 감지하면 담당자에게 알림을 보냅니다. 위의 설정 파일을 살펴보겠습니다.

route 섹션이 핵심입니다. 알림을 어디로 보낼지 결정합니다.

group_by는 같은 종류의 알림을 묶어줍니다. 모델 A, B, C에서 동시에 지연 시간 알림이 발생하면, 세 개의 알림을 하나로 묶어서 보냅니다.

group_wait은 알림을 모으는 시간입니다. 30초 동안 같은 그룹의 알림을 모아서 한 번에 보냅니다.

이렇게 하지 않으면 알림 폭탄을 맞을 수 있습니다. 서버 10대에서 동시에 문제가 생기면 10개의 알림이 연속으로 오기 때문입니다.

severity 레이블이 중요합니다. 모든 알림을 똑같이 처리하면 안 됩니다.

서비스가 완전히 멈춘 critical 상황과, 약간 느려진 warning 상황은 다르게 처리해야 합니다. critical은 새벽에도 전화가 가도록 PagerDuty로, warning은 Slack으로 보내는 식입니다.

알림 규칙도 살펴보겠습니다. expr은 PromQL 조건입니다.

"95% 지연 시간이 1초를 초과하면"이라는 조건입니다. for: 5m은 이 조건이 5분 이상 지속되어야 알림을 보낸다는 뜻입니다.

순간적인 스파이크로 거짓 알림이 오는 것을 방지합니다. 실제 현업에서는 어떤 알림을 설정할까요?

ML 시스템에서는 예측 지연 시간 증가, 에러율 급증, 모델 로딩 실패, 데이터 파이프라인 지연, 드리프트 감지 등이 일반적입니다. 비즈니스 메트릭도 포함할 수 있습니다.

추천 클릭률이 갑자기 떨어지면 알림을 보내는 식입니다. 하지만 주의할 점도 있습니다.

알림 피로를 조심해야 합니다. 알림이 너무 많으면 무시하게 됩니다.

양치기 소년 효과입니다. 정말 중요한 알림이 왔을 때도 "또 오류겠지"하고 넘어갈 수 있습니다.

임계값을 적절히 조정하고, 불필요한 알림은 제거해야 합니다. **런북(Runbook)**을 함께 작성하면 좋습니다.

알림이 왔을 때 어떻게 대응해야 하는지 문서화한 것입니다. "HighPredictionLatency 알림이 오면, 1) 모델 서버 리소스 확인, 2) 최근 배포 이력 확인, 3) 트래픽 급증 여부 확인..." 처럼 대응 절차를 미리 정해두면 새벽에 잠결에도 대응할 수 있습니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. Alert Manager를 설정한 후, 다시 새벽에 문제가 발생했습니다.

하지만 이번에는 달랐습니다. 휴대폰으로 Slack 알림이 왔고, 런북을 보며 10분 만에 문제를 해결했습니다.

"이제 마음 편히 잘 수 있겠어요!" 김개발 씨는 안도의 한숨을 내쉬었습니다.

실전 팁

💡 - 알림 임계값은 처음에 느슨하게 설정하고, 점점 타이트하게 조정하세요

  • 모든 알림에 런북 링크를 포함하세요. 대응 시간이 크게 줄어듭니다
  • 주기적으로 알림을 검토하고, 한 달간 대응이 필요 없었던 알림은 제거하거나 수정하세요

7. 분산 트레이싱 적용

"이 요청이 왜 이렇게 느린지 모르겠어요." 김개발 씨가 고민에 빠졌습니다. 모델 서버는 빠른데 전체 응답 시간이 느렸습니다.

API 게이트웨이, 피처 스토어, 모델 서버, 후처리 서비스... 어디서 병목이 생기는지 알 수 없었습니다.

"마이크로서비스 환경에서는 분산 트레이싱이 필수예요." 박시니어 씨가 해결책을 알려주었습니다.

분산 트레이싱은 여러 서비스를 거치는 요청의 전체 흐름을 추적하는 기술입니다. 마치 택배 추적 시스템처럼, 요청이 어디를 거쳐서 어디에서 얼마나 머물렀는지 한눈에 볼 수 있습니다.

마이크로서비스 환경에서 성능 병목을 찾고 문제를 디버깅하는 데 필수적인 도구입니다.

다음 코드를 살펴봅시다.

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.requests import RequestsInstrumentor

# 트레이싱 프로바이더를 설정합니다
provider = TracerProvider()
processor = BatchSpanProcessor(OTLPSpanExporter(endpoint="http://jaeger:4317"))
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)

# HTTP 요청을 자동으로 추적합니다
RequestsInstrumentor().instrument()

tracer = trace.get_tracer("ml-prediction-service")

def predict_with_tracing(request_id, user_id, input_data):
    # 최상위 스팬을 생성합니다
    with tracer.start_as_current_span("prediction_request") as span:
        span.set_attribute("request_id", request_id)
        span.set_attribute("user_id", user_id)

        # 피처 조회 스팬
        with tracer.start_as_current_span("fetch_features"):
            features = feature_store.get_features(user_id)
            span.set_attribute("feature_count", len(features))

        # 모델 예측 스팬
        with tracer.start_as_current_span("model_inference"):
            result = model.predict(features)
            span.set_attribute("prediction_score", float(result))

        # 후처리 스팬
        with tracer.start_as_current_span("post_processing"):
            final_result = apply_business_rules(result)

        return final_result

김개발 씨의 ML 서비스는 점점 복잡해지고 있었습니다. 처음에는 하나의 서버에서 모든 것을 처리했지만, 이제는 여러 서비스로 분리되었습니다.

API 게이트웨이가 요청을 받고, 인증 서비스가 사용자를 확인하고, 피처 스토어에서 사용자 정보를 가져오고, 모델 서버에서 예측하고, 후처리 서비스에서 결과를 다듬습니다. 어느 날, 응답 시간이 느리다는 불만이 접수되었습니다.

김개발 씨는 각 서비스의 메트릭을 확인했습니다. 모두 정상이었습니다.

모델 서버는 50ms, 피처 스토어는 30ms, 후처리 서비스는 20ms... 그런데 전체 응답 시간은 2초가 걸렸습니다.

"어디서 나머지 시간이 소비되는 거지?" 박시니어 씨가 설명했습니다. "각 서비스만 보면 안 돼요.

요청의 전체 흐름을 봐야 해요. 그게 분산 트레이싱이에요." 그렇다면 분산 트레이싱이란 정확히 무엇일까요?

쉽게 비유하자면, 택배 추적 시스템과 같습니다. 택배를 보내면 "서울 물류센터 도착 - 대전 허브 경유 - 부산 배송센터 도착 - 배송 완료"처럼 전체 경로를 추적할 수 있습니다.

어디서 얼마나 머물렀는지도 알 수 있습니다. 분산 트레이싱도 마찬가지로 요청이 어떤 서비스를 거쳤고, 각 서비스에서 얼마나 시간이 걸렸는지 추적합니다.

핵심 개념은 트레이스스팬입니다. 트레이스는 하나의 요청에 대한 전체 기록입니다.

택배 한 건의 전체 배송 기록과 같습니다. 스팬은 트레이스 안의 각 단계입니다.

"피처 조회에 30ms", "모델 예측에 50ms"처럼 각 작업의 시작과 끝을 기록합니다. 위의 코드에서 OpenTelemetry를 사용합니다.

분산 트레이싱의 표준 프레임워크입니다. tracer.start_as_current_span()으로 스팬을 생성하고, with 문으로 시작과 끝을 자동 관리합니다.

span.set_attribute()로 추가 정보를 기록합니다. 컨텍스트 전파가 중요합니다.

요청이 서비스 A에서 서비스 B로 넘어갈 때, 트레이스 ID가 함께 전달되어야 합니다. HTTP 헤더에 트레이스 ID를 담아서 보내고, 받는 쪽에서 이를 추출해서 같은 트레이스에 스팬을 추가합니다.

OpenTelemetry의 RequestsInstrumentor가 이를 자동으로 처리해줍니다. 실제 현업에서는 어떻게 활용할까요?

JaegerZipkin 같은 UI에서 트레이스를 시각화합니다. 워터폴 차트로 각 스팬의 시작 시간과 지속 시간을 볼 수 있습니다.

"아, 피처 스토어 호출이 순차적으로 세 번 일어나서 느렸구나. 병렬로 바꾸면 되겠다!" 이런 인사이트를 얻을 수 있습니다.

샘플링도 중요한 개념입니다. 모든 요청을 트레이싱하면 오버헤드가 너무 큽니다.

보통 1%나 10%만 샘플링합니다. 에러가 발생한 요청은 100% 샘플링하는 전략도 있습니다.

하지만 주의할 점도 있습니다. 트레이싱은 성능에 영향을 줍니다.

스팬을 너무 세밀하게 나누면 오버헤드가 커집니다. 또한 민감한 정보가 스팬 속성에 포함되지 않도록 주의해야 합니다.

사용자의 입력 데이터나 예측 결과를 그대로 기록하면 안 됩니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

분산 트레이싱을 도입한 후, 김개발 씨는 드디어 병목을 찾았습니다. 피처 스토어 호출 후 캐시 미스가 발생하면 데이터베이스까지 가는데, 이때 커넥션 풀이 부족해서 대기 시간이 발생하고 있었습니다.

트레이스에서 "db_connection_wait" 스팬이 1.5초나 걸리는 것을 확인했습니다. 커넥션 풀 사이즈를 늘리자 응답 시간이 200ms로 줄어들었습니다.

"트레이싱 없었으면 절대 못 찾았을 거예요!" 김개발 씨는 감탄했습니다.

실전 팁

💡 - 모든 서비스에 일관된 트레이싱을 적용하세요. 하나라도 빠지면 전체 흐름을 볼 수 없습니다

  • 에러 발생 시 스팬에 에러 정보를 기록하면 디버깅이 쉬워집니다
  • 샘플링 비율은 트래픽에 따라 조정하세요. 트래픽이 많으면 낮게, 적으면 높게

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Python#Prometheus#Grafana#MLOps#Observability#MLOps,Monitoring,Observability

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.