(’26) 머신러닝 플랫폼 엔지니어링 책 Study에서 책 4-5장 내용을 기반으로 본인 환경에서 실습한 내용을 정리하였습니다.
책 4–5장에서 언급하는 MLflow, Feast, Kubeflow Pipelines를 실제로 연결하는 과정에 대해 모델 파일과 피처 데이터가 어디에 저장되고 다음 단계가 그것을 어떻게 읽는지 직접 실습을 진행해보았다.
환경 준비와 실행 순서는 John Kim 님의 「04. 실험, 피처, 파이프라인 연결하기」와 ml-platform-book-env 저장소를 참고했다. 본인은 WSL2의 Ubuntu 26.04.1 LTS를 사용하기에 PowerShell 대신 Bash 환경에 맞게 바꾸어 실행해보았다. 즉, 책 내용을 해당 가이드에 따라 재현하면서 WSL에서 준비한 부분과 실제 확인한 결과를 블로그로 기록해보고자 하였다.
1. 실습 개요
전체 흐름부터 정리하면 다음과 같다. 특히 5장 파이프라인은 4장에서 학습해 MLflow에 등록한 모델을 가져와 배치 추론을 수행한다.
4장: CSV 읽기 → 모델 3개 학습 → MLflow에 기록 → 모델 등록
4장: 피처 parquet 업로드 → Feast 정의 등록 → 과거/온라인 피처 조회
5장: 대상 사용자 읽기 → Feast 피처 조회 → 등록 모델로 예측 → parquet 저장
‘배치 추론’은 여러 입력을 한 번에 읽어 예측 결과를 파일로 남기는 방식이다. 사용자 요청 한 건마다 바로 응답하는 API 서버와는 실행 형태가 다르다.
세 도구를 다음처럼 나눠 보니 역할이 더 분명해졌다.
| 도구 | 이번 실습에서 답하는 질문 | 실제 남는 것 |
|---|---|---|
| MLflow | 어떤 설정으로 학습했고, 어떤 모델 파일을 쓸까? | 실험 기록, 지표, 모델 버전 |
| Feast | 이 사용자의 피처를 어떤 데이터와 시각 기준으로 가져올까? | 피처 정의, 과거 조회 결과, Redis의 피처 값 |
| KFP | 예측 작업들을 어떤 순서로 실행하고 파일을 넘길까? | 실행 기록, 단계별 로그와 아티팩트 |
여기서 피처(feature)는 모델에 넣는 입력값이다. 이 예제에서는 학력, 직업, 관계 등의 정보를 사용한다. 아티팩트(artifact)는 실행 중 만들어진 파일을 뜻한다. 모델 파일, 그래프 PNG, 단계 사이에 전달되는 parquet 등이 여기에 해당한다.
2. WSL 환경 준비: 기본 Python과 실습 Python을 분리
본인이 사용하는 WSL 환경에 이미 설치된 Python 3.14.4, git, uv와 실습 환경 간 충돌이 일어나지 않고, Docker, kind, kubectl은 WSL 환경에서 설치하지 않았기에, Docker Desktop 대신 Ubuntu 안에 Docker Engine을 설치하는 방식으로 준비했다.
WSL 터미널에서 필요한 패키지를 설치한다.
sudo apt-get update
sudo apt-get install -y docker.io
sudo systemctl enable --now docker
sudo usermod -aG docker "$USER"
Docker 그룹 변경은 새 로그인 세션에서 반영된다. 기존 WSL 터미널을 계속 쓴다면 newgrp docker로 그룹이 적용된 새 셸에 들어간 뒤 docker info를 확인한다.
kind는 Docker 컨테이너 안에 작은 Kubernetes 클러스터를 만드는 도구다. 별도 서버 여러 대 없이도 Kubernetes의 Pod와 Service를 연습할 수 있다. kubectl은 만들어진 클러스터에 명령을 보내는 도구다.
mkdir -p ~/.local/bin
curl -fsSL https://kind.sigs.k8s.io/dl/v0.27.0/kind-linux-amd64 \
-o ~/.local/bin/kind
curl -fsSL https://dl.k8s.io/release/v1.32.2/bin/linux/amd64/kubectl \
-o ~/.local/bin/kubectl
chmod +x ~/.local/bin/kind ~/.local/bin/kubectl
export PATH="$HOME/.local/bin:$PATH"
이번 실행 환경은 아래와 같았다. Ubuntu 시스템 Python을 바꾸지 않고, 책 실습만 Python 3.11 가상환경으로 분리했다.
| 항목 | 확인한 값 |
|---|---|
| 운영체제 | WSL2 / Ubuntu 26.04.1 LTS |
| Docker Engine | 29.1.3 |
| kind | 0.27.0 |
| Kubernetes 노드 이미지 | kindest/node:v1.31.6 |
| kubectl 클라이언트 | 1.32.2 |
| uv | 0.12.13 |
| 실습 Python | 3.11.16 |
| MLflow 서버 / Python 클라이언트 | 3.0.0 / 3.0.0 |
| Feast | 0.40.1 |
| KFP 서버 / Python SDK | 2.4.1 / 2.9.0 |
| scikit-learn | 1.5.2 |

WSL 운영체제와 Python 환경을 확인한 JupyterLab 화면
가상환경은 이 프로젝트에서만 쓸 Python과 패키지를 모아 두는 폴더다. Ubuntu가 제공하는 Python과 섞지 않으면 책의 버전 조건을 맞추기 편하다.
3. 저장소와 클러스터 준비
Docker 이미지와 Python 패키지는 WSL의 Linux 파일시스템 안에 두었다. 이번 작업 위치는 ~/project/ml-platform-lab/ml-platform-book-env다. 재현할 때는 자신의 홈 디렉터리에 만들어도 된다.
mkdir -p ~/project/ml-platform-lab
cd ~/project/ml-platform-lab
git clone https://github.com/johklo/ml-platform-book-env.git
cd ml-platform-book-env
git clone https://github.com/practical-mlops/chapter-4.git upstream/chapter-4
git clone https://github.com/practical-mlops/chapter-5.git upstream/chapter-5
uv venv .venv-book --python 3.11
uv pip install --python .venv-book -r book/requirements.txt
source .venv-book/bin/activate
python bookenv.py check
python bookenv.py up
python bookenv.py up을 실행하면 kind 클러스터를 만들고 KFP, 저장소, PostgreSQL, Redis, MLflow 서버를 준비한다. 처음 실행할 때는 여러 컨테이너 이미지를 받으므로 기다릴 시간이 필요하다.
이번 실습은 Kubeflow 전체 설치가 아니라 KFP standalone 설치다. Kubeflow Notebook 서버나 통합 인증 화면까지 준비하는 구성은 아니다. JupyterLab은 WSL에서 직접 실행한다.
설치 중 Pod가 재시작했을 때
실행 과정 중 metadata-grpc-deployment에서 CrashLoopBackOff가 보였다. 이 상태는 컨테이너가 실패하고 다시 실행되기를 반복한다는 뜻이다.
kubectl --context kind-book-env -n kubeflow get pods
kubectl --context kind-book-env -n kubeflow \
logs deploy/metadata-grpc-deployment --tail=20
로그에는 다음 메시지가 있었다.
MySQL database was not initialized. Please ensure your MySQL server is running.
이때 MySQL Pod도 아직 ContainerCreating 상태였다. 이미지 다운로드와 데이터베이스 준비가 끝나기 전에 다른 서비스가 먼저 연결을 시도한 것이다. 바로 매니페스트나 DB 인증 설정을 바꾸기보다 의존 서비스가 준비됐는지 함께 확인했다.
MySQL이 준비된 뒤 재시작한 metadata 서비스도 1/1 Running으로 회복됐다. 이 경우 별도의 설정 수정이나 수동 Pod 재생성은 하지 않았다. 최종적으로 up의 마지막에 The environment is ready. Next: python bookenv.py forward가 출력됐다.

설치 완료 뒤 kubectl get pods로 확인한 서비스 상태
READY 1/1은 Pod의 컨테이너 하나가 준비됐다는 뜻이다. RESTARTS는 지금 장애라는 뜻이 아니라, 지금까지 재시작한 횟수다. 따라서 상태와 준비 여부를 함께 읽었다.
4. Windows 브라우저에서 WSL 서비스를 열기
별도 WSL 터미널에서 가상환경을 활성화하고 포트포워딩을 실행한다.
cd ~/project/ml-platform-lab/ml-platform-book-env
source .venv-book/bin/activate
python bookenv.py forward
포트포워딩은 PC의 특정 포트와 클러스터 안의 서비스 사이에 통로를 만드는 작업이다. 브라우저가 Kubernetes 서비스 주소를 직접 몰라도 localhost로 접속할 수 있게 해 준다.
| 브라우저 / 클라이언트 주소 | 용도 |
|---|---|
http://localhost:8080 |
KFP UI |
http://localhost:5000 |
MLflow UI 및 API |
http://localhost:8888/buckets/ |
SeaweedFS 저장소 화면 |
localhost:9000 |
S3 API |
localhost:6379 |
Redis |
JupyterLab은 저장소 화면의 8888과 겹치지 않도록 8890을 사용한다.
jupyter lab --notebook-dir book --port 8890 --ip 127.0.0.1 --no-browser
출력되는 토큰 포함 주소를 Windows 브라우저에서 열면 된다. 이번에는 WSL 서비스를 Windows Chrome에서 열어 확인했다.
버킷은 왜 여러 개 만들까
버킷은 S3 저장소에서 파일을 묶는 최상위 공간이다. 같은 저장소를 쓰더라도 학습 CSV, 모델 파일, 피처 정의, 최종 결과를 나눠 두면 어떤 단계가 무엇을 읽고 쓰는지 확인하기 쉽다.
python bookenv.py buckets
| 버킷 | 이번 실습에서 저장하는 것 |
|---|---|
mlflow-datasets |
학습·테스트 CSV |
mlflow-artifacts |
모델, 그래프, 입력 열 목록 |
feature-data-sets |
피처 원천 parquet, 예측 대상 |
feature-registry |
Feast 정의와 접속 설정 |
inference-datasets |
최종 예측 결과 |
mlpipeline |
KFP 단계 사이의 중간 파일 |

이번 클러스터에 생성한 실습 버킷
5. 4장 MLflow: 학습 결과를 다시 찾을 수 있게 기록하기
JupyterLab에서 ch04/01_mlflow_tracking.ipynb를 위에서부터 실행했다.
첫 부분은 데이터 경로와 저장소 접속 설정을 준비한다. 이후 소득 분류 CSV를 읽고 범주별 분포를 살펴본다. 이번 데이터는 30,162행, 9개 열이었다.
MLflow의 income-classifier Experiment 안에는 데이터 탐색 run 한 개와 모델 학습 run 세 개가 기록된다. Experiment는 관련 실행을 모아 두는 폴더와 비슷하고, Run은 그 안의 한 번의 실행 기록이다.
이번에 학습한 모델은 Decision Tree, Random Forest, XGBoost다. 학습 알고리즘은 scikit-learn과 XGBoost가 담당하고, MLflow는 설정·점수·파일을 기록한다.
| 모델 | MLflow run | 테스트 ROC AUC | 테스트 정확도 |
|---|---|---|---|
| Decision Tree | fortunate-goat-977 |
0.815114 | 0.818333 |
| Random Forest | fearless-moose-660 |
0.857340 | 0.826786 |
| XGBoost | abrasive-crow-116 |
0.718263 | 0.821813 |
ROC AUC는 분류기가 두 클래스를 얼마나 잘 구분하는지 살펴보는 지표다. 여기서는 값이 높을수록 좋게 본다. 다만 원본 코드는 데이터 분할과 일부 모델의 난수 시드를 고정하지 않는다. 같은 명령을 실행해도 이 글이나 참고 가이드와 점수가 정확히 일치하지 않을 수 있다.
또한 원본 XGBoost 셀은 마지막에 binary:hinge 모델의 0/1 예측으로 AUC를 계산한다. 다른 두 모델의 확률 기반 AUC와 같은 조건의 비교라고 해석하기는 어렵다. 이번 점수표는 책 코드가 남긴 결과를 확인한 기록이다.

이번 실행에서 MLflow에 기록된 탐색과 학습 run
모델 파일은 어디에 있을까
MLflow 화면에서 보이는 실행 이름, 설정, 지표 등은 PostgreSQL에 기록된다. 실제 모델과 그래프 파일은 mlflow-artifacts 버킷에 저장된다. 화면이 열린다고 모델 파일까지 정상적으로 저장됐다는 뜻은 아니므로 아티팩트도 확인해야 한다.
원본 노트북은 범주형 입력을 숫자 열로 바꾸고 column_list.pkl에 입력 열 순서를 저장한다. 예를 들어 학력과 직업을 여러 0/1 열로 바꾸면, 추론에서도 같은 열을 같은 순서로 넣어야 한다. 이 파일은 나중에 5장 추론 코드가 내려받는다.
모델 레지스트리에 등록
노트북 마지막 셀은 테스트 AUC가 높은 run을 찾아 random-forest-classifier라는 이름으로 등록한다. 이어서 가이드가 추가한 셀을 실행해 stage를 Production으로 지정한다.
이번에 선택된 원천 run은 fearless-moose-660였고, 테스트 AUC는 0.857340이었다. 모델은 버전 1 / Production으로 등록됐다. 원천 run ID는 489e1a27bc8b428c9b6f8fe9af0248e3다.

등록 모델의 버전과 원천 run
Production은 이번 코드가 모델 버전을 찾을 때 쓰는 표식이다. 이를 지정했다고 예측 API가 자동으로 배포되는 것은 아니다. MLflow 3에서는 stage 사용에 대한 FutureWarning이 나오지만, 이번 책 코드와의 연결에서는 실제로 동작하는지 확인했다.
6. 4장 Feast: 피처 정의와 피처 값을 구분하기
다음으로 ch04/02_feast_feature_store.ipynb를 실행했다.
학습 CSV를 읽는 것과 Feast를 사용하는 것은 서로 다른 과정이다. 이 책 예제의 학습 노트북은 CSV를 직접 읽고, Feast는 그 뒤에 따로 준비한다. 따라서 “학습도 처음부터 Feast로 했다”고 설명하면 이번 실행과 맞지 않는다.
이번 Feast 흐름은 다음과 같다.
원천 parquet 업로드
→ Entity / FeatureView 정의 등록
→ 과거 시점 피처 조회
→ Redis로 값 적재
→ 온라인 피처 조회
| 용어 | 쉽게 풀어 보면 | 이번 예제 |
|---|---|---|
| Entity | 누구의 값인지 찾는 키 | user_id |
| FeatureView | 함께 다룰 피처의 정의 | demographic, relationship, occupation |
| Registry | 피처의 이름·타입·위치 등을 기록한 목록 | S3의 Registry 파일 |
| Offline store | 과거 피처를 읽는 원천 | S3의 parquet |
| Online store | 빠르게 조회할 최신값을 저장하는 곳 | Redis |
apply와 materialize는 다른 작업
feast apply는 피처 정의를 Registry에 등록하는 명령이다. 실행 로그에 세 FeatureView를 준비하는 메시지가 나타났다.
Deploying infrastructure for relationship
Deploying infrastructure for demographic
Deploying infrastructure for occupation
그렇다고 모든 피처 값이 Redis에 들어간 것은 아니다. feast materialize가 원천 데이터를 읽어 온라인 저장소로 옮긴다.
이번에는 원본 README와 같은 기간을 사용했다.
feast materialize 2022-09-16T00:00:00 2023-09-17T00:00:00
이 명령을 직접 실행한다면 노트북이 만든 book/ch04/feature_repo에서 실행해야 한다. 노트북의 도우미 셀은 이 작업 위치를 자동으로 지정한다.
과거 조회와 온라인 조회는 왜 나눌까
과거 조회는 사용자 ID와 예측하려는 기준 시각을 함께 사용한다. 그 시점에 맞는 피처를 가져오기 위해서다. 온라인 조회는 Redis에 적재된 최신값을 빠르게 가져오는 데 사용한다.
과거의 예측을 재현하면서 오늘의 최신값을 넣으면, 그때는 알 수 없었던 정보가 섞일 수 있다. 5장의 배치 추론이 get_historical_features()를 사용하는 이유도 이 구분과 연결된다.
실제 과거 조회는 사용자 9f2ac416-06e1-44a0-87bd-d4787c85bf66의 2023-01-30 10:59:42+00:00 기준으로 Sex=Male을 반환했다. 온라인 조회는 원본 스크립트에 지정된 다른 사용자 74b0638a-5742-4d1a-a03f-483794d7aa7c에 대해 Sex=Male을 반환했다. 두 예제가 같은 사용자의 값 변화를 비교한 것은 아니다. 출력 중 None은 print(df.info())가 반환값을 출력한 부분이며, Sex가 결측이라는 의미가 아니다.

Feast 원본 노트북에서 확인한 조회 결과
노트북을 마치면 feature-registry에 로컬용 registry_local.db와 feature_store.yaml이 남는다.
7. WSL의 localhost와 Pod의 localhost는 다름
이번 실습에서 가장 기억할 만한 연결 지점은 주소였다.
WSL에서 실행하는 노트북은 포트포워딩을 통해 localhost:5000과 localhost:9000으로 접속한다. 하지만 Kubernetes Pod 안에서 localhost는 해당 Pod 자체를 가리킨다.
| 실행 위치 | MLflow 접속 주소 |
|---|---|
| WSL의 노트북 | http://localhost:5000 |
| KFP 작업 Pod | http://mlflow-service.mlflow.svc.cluster.local:5000 |
S3 API도 Pod에서는 minio-service.kubeflow.svc.cluster.local:9000을 쓴다.
그래서 로컬용 Feast 설정을 그대로 파이프라인에 넘기지 않는다. 5장 준비 과정에서 클러스터용 Registry를 새로 등록하고, feature_store_k8s.yaml이라는 별도 파일을 업로드한다.
두 설정을 분리해 두면 “노트북에서는 되는데 파이프라인에서는 안 된다”는 문제를 살펴볼 때 실행 위치부터 확인할 수 있다.
8. 5장 KFP: 네 단계의 배치 추론 실행
먼저 가이드가 제공하는 준비 스크립트와 Dockerfile을 사용한다.
python book/ch05/prepare.py
docker build -t book-ch5/feast-job:0.1 \
-f book/ch05/Dockerfile.feast-job upstream/chapter-5/scripts/feast
docker build -t book-ch5/components:0.1 \
-f book/ch05/Dockerfile.components upstream/chapter-5
python bookenv.py load book-ch5/feast-job:0.1
python bookenv.py load book-ch5/components:0.1
prepare.py는 원본 컴포넌트의 이미지 이름과 Redis 설정을 이 환경에 맞춘다. Docker로 이미지를 만드는 것과 kind 노드에 넣는 것은 별개다. 그래서 빌드 다음에 load를 실행했다.
참고 가이드의 Dockerfile은 오래된 Debian buster 기반 이미지를 bookworm으로 바꾸고, 추론 환경의 scikit-learn을 학습 환경과 같은 1.5.2로 맞춘다. 이 보완은 참고 가이드에 이미 들어 있는 변경이며, 이번 실습에서 새로 발견해 고친 것으로 소개하지 않는다.
클러스터용 Feast 정의와 입력 파일
kubectl --context kind-book-env apply -f book/ch05/feast-job.yaml
kubectl --context kind-book-env -n kubeflow wait \
--for=condition=complete --timeout=300s job/feast-job
kubectl --context kind-book-env -n kubeflow logs job/feast-job
python book/ch05/upload_inputs.py
cd upstream/chapter-5
python inference_pipeline.py
cd ../..
grep 'image:' upstream/chapter-5/income_classifier_pipeline.yaml
feast-job은 Pod에서 사용할 피처 정의를 등록한다. upload_inputs.py는 클러스터용 설정과 예측 대상 파일을 올린다. 마지막 Python 명령은 파이프라인 정의를 실행용 YAML로 컴파일한다.
이번 이미지 참조는 네 컴포넌트 모두 book-ch5/components:0.1이었다. YAML이 만들어졌다는 것만으로 작업이 실행된 것은 아니다. 실제 실행은 KFP 서버에 등록하고 run을 시작한 뒤 확인해야 한다.
파이프라인 등록과 실행
참고 가이드는 UI에서 YAML을 업로드하고 실행한다. 이번에는 WSL의 Python SDK로 같은 YAML과 파라미터를 등록·제출하고, KFP UI에서 생성된 run과 그래프를 확인했다. 캐시를 끄고 실제로 네 컴포넌트를 실행했다.
import json
from pathlib import Path
import kfp
# 저장소 최상위 폴더에서 실행
client = kfp.Client(host="http://localhost:8080")
pipeline_id = client.get_pipeline_id("income-classifier-pipeline")
if not pipeline_id:
pipeline = client.upload_pipeline(
"upstream/chapter-5/income_classifier_pipeline.yaml",
pipeline_name="income-classifier-pipeline",
)
pipeline_id = pipeline.pipeline_id
version = client.list_pipeline_versions(pipeline_id).pipeline_versions[0]
experiment = client.create_experiment("income-classifier")
params = json.loads(Path("book/ch05/run-parameters.json").read_text())
run = client.run_pipeline(
experiment.experiment_id,
"income-classifier-wsl-run-1",
pipeline_id=pipeline_id,
version_id=version.pipeline_version_id,
params=params,
enable_caching=False,
)
print(run.run_id)
SDK로 등록된 파이프라인을 실행하는 과정에서는 다음 오류도 만났다.
both `pipeline_id` and `version_id` are required
내 제출 코드에서 파이프라인 ID만 전달했기 때문이다. 등록된 파이프라인의 버전 ID도 조회해서 run_pipeline()에 함께 전달하니 run이 만들어졌다. 이 오류는 이번에 추가한 SDK 제출 코드의 문제였으며, 책의 추론 컴포넌트가 실패한 것은 아니다.
핵심 파라미터는 다음과 같다. 나머지 값은 가이드의 book/ch05/run-parameters.json을 사용했다.
| 파라미터 | 값 |
|---|---|
| 대상 파일 | feature-data-sets/entity_df.parquet |
| Feast 설정 | feature-registry/feature_store_k8s.yaml |
| 모델 이름 / stage | random-forest-classifier / Production |
| 출력 파일 | inference-datasets/income_predictions.parquet |
KFP의 Experiment와 MLflow의 Experiment는 이름이 같아도 별개다. 전자는 파이프라인 실행을, 후자는 모델 학습 기록을 묶는다.
실제로 실행되는 네 단계는 아래와 같다.
| 단계 | 무엇을 하는가 |
|---|---|
read-data-from-minio |
예측 대상 사용자와 기준 시각을 읽는다 |
retrieve-features-from-feast |
대상에 맞는 과거 피처를 조회한다 |
model-inference |
MLflow 등록 모델과 입력 열 목록을 받아 예측한다 |
write-data-to-minio |
예측 결과를 최종 버킷에 저장한다 |
KFP는 앞 단계의 Python 메모리를 다음 단계와 공유하지 않는다. 각 작업은 별도 Pod에서 실행되고 중간 결과는 파일로 전달된다. 화면의 화살표는 작업 순서뿐 아니라 파일 입력·출력의 연결로 읽어야 한다.
이번 run ID는 06e5dad7-2ae6-4e46-95aa-1379f72e5a58다. KFP 화면에서는 네 컴포넌트가 모두 초록색 체크로 바뀌었고, kubectl get workflows에서는 income-classifier-pipeline-j4b4j이 Succeeded로 표시됐다.
파이프라인이 실행 중일 때는 첫 컴포넌트가 동작하는 모습을 확인할 수 있었다.

실행 중인 KFP 파이프라인

이번 WSL 클러스터에서 실행한 KFP 배치 추론 그래프
UI와 kubectl을 함께 확인
실행 중과 완료 뒤에는 아래 명령도 조회했다. Workflow 이름은 이번 실행에서 생성된 실제 이름이다. 다른 실행에서는 get workflows 결과에서 자신의 이름을 확인해 넣으면 된다.
kubectl --context kind-book-env -n kubeflow get workflows
kubectl --context kind-book-env -n kubeflow get pods \
-l workflows.argoproj.io/workflow=income-classifier-pipeline-j4b4j
kubectl --context kind-book-env -n kubeflow get jobs

kubectl에서 확인한 Workflow 성공과 작업 Pod 완료
작업 Pod들은 0/2 Completed로 표시됐다. 실패해서 준비가 안 된 것이 아니라, 두 컨테이너가 할 일을 마치고 종료됐기 때문이다. MLflow 서버처럼 계속 실행되는 서비스 Pod와 다르게 읽어야 한다.
모델 추론 작업은 main 컨테이너 로그를 확인했다.
kubectl --context kind-book-env -n kubeflow logs \
income-classifier-pipeline-j4b4j-system-container-impl-3969511708 \
-c main --tail=18

kubectl logs로 확인한 모델 파일 다운로드와 결과 업로드
로그에서는 아티팩트 다운로드, 추론 결과 파일 업로드, publish success, sub-process exited와 error="<nil>"을 확인했다. 특정 로그 한 줄만으로 성공을 판단하지 않고, Workflow 상태와 최종 결과 파일까지 함께 확인했다.
9. 성공 표시 다음에 실제 파일 확인
KFP 화면에 초록색 체크가 나타나도, 마지막 버킷에 원하는 결과가 저장됐는지까지 확인해야 한다.
python book/ch05/show_predictions.py
이번 파일을 읽은 결과는 다음과 같았다.
rows, columns: (30162, 91)
model input columns: 90
user_id in output: False
missing prediction values: 0
파일 크기는 282,569바이트였다. 예측값의 범위는 0.0–1.0, 평균은 약 0.2494였다. 평균값은 이 파일의 요약 통계이며, 정확도나 모델 품질 지표는 아니다.
참고 가이드에는 92열이라고 되어 있지만 이번 실행은 91열이었다. 저장된 column_list.pkl에는 모델 입력 열이 90개 있고, 결과는 그 입력 열에 예측 열 하나가 더해진 구조였다. 참고 글의 숫자를 옮기지 않고 이번 파일에서 측정한 값으로 기록했다.

최종 버킷에 저장된 이번 실행의 예측 파일

parquet를 읽어 확인한 결과와 데이터 크기
parquet는 테이블 데이터를 저장하는 바이너리 형식이다. 웹 화면에서 파일을 텍스트로 열었을 때 깨진 문자나 PAR1이 보인다고 곧바로 파일이 손상됐다고 판단하면 안 된다. pandas 등으로 실제 테이블을 읽어 확인하는 편이 정확하다.
실행 성공과 결과를 업무에 쓸 수 있다는 것은 별개
이번 원본 결과에는 user_id가 없었다. 추론 코드가 모델 입력에서 ID를 제거하고 결과에 다시 붙이지 않기 때문이다. 모델 입력에서 ID를 빼는 것과 예측 결과의 사용자를 식별할 수 없게 만드는 것은 다른 문제다.
실제 서비스 결과라면 사용자 ID, 기준 시각, 구체 모델 버전도 함께 남기도록 보완해야 한다.
예측 열 이름은 Predicted_Income_Class지만 원본 코드는 predict_proba()의 두 번째 클래스 값을 넣는다. 따라서 0.7 같은 값은 클래스 번호가 아니라 확률 추정치다. 확률을 0/1 판단으로 바꾸는 임계값 설정은 이번 코드에 포함되어 있지 않다.
원본 코드는 테스트 점수로 모델을 선택한다. 운영용 모델 선택에서는 검증셋으로 후보를 고르고 테스트셋은 최종 확인에 사용하는 방식을 별도로 검토할 필요가 있다. 이번 실습에서 예측 파일을 만들었다는 사실만으로 모델의 일반화 성능이나 업무 적합성까지 검증한 것은 아니다.
10. 이번 WSL 실습에서 확인한 점
이번 실행은 책의 모델 학습, 피처 준비, 배치 추론 흐름을 WSL에서도 연결할 수 있는지 확인하는 데 초점을 맞췄다.
Ubuntu 안에 Docker Engine과 CLI 도구를 준비하고, Python 3.11 가상환경에서 책의 노트북 두 개를 실행했다. MLflow의 네 run과 등록 모델, Feast의 과거·온라인 조회, KFP의 네 작업 성공, S3의 최종 예측 파일까지 확인했다. WSL에서 추가로 필요한 준비와 실행 위치에 따른 주소 차이를 함께 확인할 수 있었다.
내가 다시 설명할 수 있어야 할 포인트는 다음과 같다.
- MLflow는 모델을 대신 학습하는 도구가 아니라, 학습 설정·결과·모델 파일을 추적하는 도구다.
- Feast의 Registry에 정의가 있다고 Redis에 값까지 들어 있는 것은 아니다.
apply와materialize를 구분해야 한다. - 과거 조회에는 기준 시각이 필요하다. 최신 온라인 값과 과거 시점의 값은 사용 목적이 다르다.
- WSL 노트북과 Kubernetes Pod는 접속 주소가 다르다.
localhost를 복사하기 전에 실행 위치를 확인해야 한다. - KFP 성공 표시 이후에도 최종 파일의 크기, 내용, 사용자 식별 열을 확인해야 한다.
세 도구를 각각 설치하는 것보다, 모델과 데이터가 어디에 남고 다음 단계가 어떤 주소와 파일 이름으로 가져가는지 확인하는 과정이 이번 스터디의 핵심이었다.
다시 실행하거나 종료할 때
환경은 결과를 다시 확인할 수 있도록 남겨 두었다. 다시 접속하려면 클러스터 상태를 확인하고 forward와 JupyterLab을 실행한다.
cd ~/project/ml-platform-lab/ml-platform-book-env
source .venv-book/bin/activate
python bookenv.py status
python bookenv.py forward
다른 터미널에서:
cd ~/project/ml-platform-lab/ml-platform-book-env
source .venv-book/bin/activate
jupyter lab --notebook-dir book --port 8890 --ip 127.0.0.1 --no-browser
Ctrl+C로 포트포워딩이나 JupyterLab을 멈추면 접속 프로세스가 종료된다. 클러스터를 삭제하는 python bookenv.py down은 내부 실습 기록도 지우므로, 이 글의 결과를 확인하기 위해 이번에는 실행하지 않았다.
참고 자료
- 책: 『Machine Learning Platform Engineering』 4–5장
- 실습 안내: John Kim 님의 「04. 실험, 피처, 파이프라인 연결하기」
- 환경 저장소: johklo/ml-platform-book-env, 커밋
e9048a3cdc32da84782be3682617754db3fe8f5f - 책 4장 코드: practical-mlops/chapter-4, 커밋
b67a8228336781c933dd68bb7dc520bd484c3371 - 책 5장 코드: practical-mlops/chapter-5, 커밋
332915b2f617e7fed8646b7c096653c87eed9d30
'MLOps & LLMOps' 카테고리의 다른 글
| [MLOps 플랫폼 스터디] AWS MLOps 성숙도와 쿠버네티스 역할 (feat. Amazon EKS) (0) | 2026.10.25 |
|---|
