개요
ML 모델을 프로덕션에 배포할 때 가장 흔한 문제 중 하나가 **Training-Serving Skew**다. 학습 시 사용한 피처와 서빙 시 사용하는 피처가 달라져서 모델 성능이 떨어지는 현상이다. **Feature Store**는 이 문제를 근본적으로 해결하는 인프라 컴포넌트로, 피처의 정의·저장·서빙을 중앙에서 관리한다.
**Feast**(Feature Store)는 가장 널리 사용되는 오픈소스 피처 스토어로, 오프라인(배치 학습)과 온라인(실시간 서빙) 두 가지 경로를 모두 지원한다. 이 글에서는 Feast를 활용해 피처 파이프라인을 구축하는 전 과정을 다룬다.
Feature Store가 필요한 이유
Training-Serving Skew 문제
학습 시 (오프라인)
features = pd.read_sql("""
SELECT user_id,
AVG(purchase_amount) as avg_purchase,
COUNT(*) as purchase_count
FROM transactions
WHERE timestamp < '2026-01-01'
GROUP BY user_id
""", conn)
서빙 시 (온라인) - 다른 로직으로 계산하면 Skew 발생!
features = redis_client.get(f"user:{user_id}:features")
학습과 서빙에서 같은 피처를 다른 코드로 계산하면 미묘한 차이가 생기고, 모델 성능이 오프라인 실험과 달라진다. Feature Store는 **하나의 피처 정의**에서 오프라인/온라인 모두 일관된 값을 제공한다.
Feature Store의 핵심 기능
| 기능 | 설명 |
| ---------------------- | ----------------------------------------------- |
| **피처 레지스트리** | 피처의 메타데이터, 스키마, 소유자 관리 |
| **오프라인 스토어** | 배치 학습용 대량 피처 조회 (Point-in-Time Join) |
| **온라인 스토어** | 실시간 서빙용 저지연 피처 조회 |
| **피처 서비스** | gRPC/HTTP API로 피처 서빙 |
| **Point-in-Time Join** | 시간 기준으로 정확한 피처 값 조인 |
Feast 설치 및 프로젝트 초기화
설치
기본 설치
pip install feast
PostgreSQL 온라인 스토어 사용 시
pip install feast[postgres]
Redis 온라인 스토어 사용 시
pip install feast[redis]
전체 의존성
pip install feast[postgres,redis,aws,gcp]
프로젝트 초기화
프로젝트 생성
feast init my_feature_store
cd my_feature_store
디렉토리 구조
my_feature_store/
├── feature_repo/
│ ├── feature_store.yaml # Feast 설정
│ ├── example_repo.py # 피처 정의 예제
│ └── data/ # 샘플 데이터
└── README.md
feature_store.yaml 설정
project: my_feature_store
registry: data/registry.db
provider: local
online_store:
type: sqlite
path: data/online_store.db
offline_store:
type: file
entity_key_serialization_version: 2
프로덕션 환경에서는 다음과 같이 변경한다:
project: my_feature_store
registry:
registry_type: sql
path: postgresql://user:pass@host:5432/feast_registry
provider: local
online_store:
type: redis
connection_string: redis://localhost:6379
offline_store:
type: file # 또는 bigquery, redshift, snowflake
피처 정의
데이터 소스 및 엔티티 정의
feature_repo/features.py
from datetime import timedelta
from feast import Entity, FeatureView, Field, FileSource, PushSource
from feast.types import Float32, Int64, String
데이터 소스 정의
user_transactions_source = FileSource(
path="data/user_transactions.parquet",
timestamp_field="event_timestamp",
created_timestamp_column="created_timestamp",
)
엔티티 정의 (피처의 기준이 되는 키)
user = Entity(
name="user_id",
join_keys=["user_id"],
description="사용자 고유 ID",
)
Feature View 정의
오프라인 + 온라인 피처 뷰
user_transaction_features = FeatureView(
name="user_transaction_features",
entities=[user],
ttl=timedelta(days=7), # 온라인 스토어에서 7일 후 만료
schema=[
Field(name="total_purchases", dtype=Int64, description="총 구매 횟수"),
Field(name="avg_purchase_amount", dtype=Float32, description="평균 구매 금액"),
Field(name="last_purchase_amount", dtype=Float32, description="최근 구매 금액"),
Field(name="purchase_frequency", dtype=Float32, description="구매 빈도 (건/일)"),
Field(name="user_segment", dtype=String, description="사용자 세그먼트"),
],
online=True,
source=user_transactions_source,
tags={"team": "ml-platform", "version": "v1"},
)
On-Demand Feature View (실시간 변환)
from feast import on_demand_feature_view, RequestSource
요청 시점에 동적으로 계산되는 피처
input_request = RequestSource(
name="purchase_request",
schema=[
Field(name="current_amount", dtype=Float32),
],
)
@on_demand_feature_view(
sources=[user_transaction_features, input_request],
schema=[
Field(name="amount_vs_avg_ratio", dtype=Float32),
Field(name="is_high_value", dtype=Int64),
],
)
def purchase_analysis(inputs: dict) -> dict:
"""현재 구매 금액과 평균 구매 금액의 비율 계산"""
df = pd.DataFrame(inputs)
df["amount_vs_avg_ratio"] = df["current_amount"] / (df["avg_purchase_amount"] + 1e-6)
df["is_high_value"] = (df["amount_vs_avg_ratio"] > 2.0).astype(int)
return df[["amount_vs_avg_ratio", "is_high_value"]]
샘플 데이터 생성
scripts/generate_data.py
from datetime import datetime, timedelta
np.random.seed(42)
n_users = 1000
n_records = 5000
user_ids = [f"user_{i:04d}" for i in range(n_users)]
records = []
for _ in range(n_records):
user_id = np.random.choice(user_ids)
ts = datetime(2026, 1, 1) + timedelta(
days=np.random.randint(0, 60),
hours=np.random.randint(0, 24),
)
records.append({
"user_id": user_id,
"total_purchases": np.random.randint(1, 100),
"avg_purchase_amount": round(np.random.uniform(10, 500), 2),
"last_purchase_amount": round(np.random.uniform(5, 1000), 2),
"purchase_frequency": round(np.random.uniform(0.1, 5.0), 3),
"user_segment": np.random.choice(["bronze", "silver", "gold", "platinum"]),
"event_timestamp": ts,
"created_timestamp": ts,
})
df = pd.DataFrame(records)
df.to_parquet("feature_repo/data/user_transactions.parquet", index=False)
print(f"Generated {len(df)} records for {n_users} users")
python scripts/generate_data.py
Generated 5000 records for 1000 users
Feast 워크플로우
1. Apply — 피처 정의 등록
cd feature_repo
feast apply
Created entity user_id
Created feature view user_transaction_features
Created on demand feature view purchase_analysis
Deploying infrastructure for my_feature_store...
2. Materialize — 오프라인 → 온라인 스토어 동기화
특정 기간의 데이터를 온라인 스토어로 적재
feast materialize 2026-01-01T00:00:00 2026-03-01T00:00:00
증분 적재 (마지막 materialize 이후 ~ 현재)
feast materialize-incremental $(date -u +"%Y-%m-%dT%H:%M:%S")
Materializing 1 feature views from 2026-01-01 to 2026-03-01
user_transaction_features:
100%|████████████████████████| 1000/1000 [00:03<00:00, 312.45it/s]
3. 오프라인 피처 조회 (학습용)
from feast import FeatureStore
store = FeatureStore(repo_path="feature_repo")
학습 데이터 생성을 위한 엔티티 데이터프레임
entity_df = pd.DataFrame({
"user_id": ["user_0001", "user_0042", "user_0100", "user_0500"],
"event_timestamp": pd.to_datetime([
"2026-02-01", "2026-02-15", "2026-01-20", "2026-02-28"
]),
})
Point-in-Time Join으로 피처 조회
training_df = store.get_historical_features(
entity_df=entity_df,
features=[
"user_transaction_features:total_purchases",
"user_transaction_features:avg_purchase_amount",
"user_transaction_features:last_purchase_amount",
"user_transaction_features:purchase_frequency",
"user_transaction_features:user_segment",
],
).to_df()
print(training_df.head())
user_id event_timestamp total_purchases avg_purchase_amount ...
0 user_0001 2026-02-01 45 234.56 ...
1 user_0042 2026-02-15 12 89.30 ...
2 user_0100 2026-01-20 78 456.78 ...
3 user_0500 2026-02-28 33 167.42 ...
**Point-in-Time Join**이 핵심이다. 각 엔티티의 `event_timestamp` 시점에서 가장 최신의 피처 값을 가져온다. 이를 통해 데이터 누수(data leakage) 없이 정확한 학습 데이터를 구성할 수 있다.
4. 온라인 피처 조회 (서빙용)
실시간 서빙에서 피처 조회
online_features = store.get_online_features(
features=[
"user_transaction_features:total_purchases",
"user_transaction_features:avg_purchase_amount",
"user_transaction_features:user_segment",
"purchase_analysis:amount_vs_avg_ratio",
"purchase_analysis:is_high_value",
],
entity_rows=[
{"user_id": "user_0001", "current_amount": 750.0},
{"user_id": "user_0042", "current_amount": 50.0},
],
).to_dict()
print(online_features)
{
"user_id": ["user_0001", "user_0042"],
"total_purchases": [45, 12],
"avg_purchase_amount": [234.56, 89.30],
"user_segment": ["gold", "silver"],
"amount_vs_avg_ratio": [3.199, 0.560],
"is_high_value": [1, 0],
}
Feature Service로 피처 그룹 관리
from feast import FeatureService
추천 모델에 필요한 피처 묶음
recommendation_service = FeatureService(
name="recommendation_features",
features=[
user_transaction_features[["total_purchases", "avg_purchase_amount", "user_segment"]],
purchase_analysis,
],
tags={"model": "recommendation-v2"},
)
사기 탐지 모델에 필요한 피처 묶음
fraud_detection_service = FeatureService(
name="fraud_detection_features",
features=[
user_transaction_features,
purchase_analysis,
],
tags={"model": "fraud-detection-v1"},
)
Feature Service로 조회
features = store.get_online_features(
features=store.get_feature_service("recommendation_features"),
entity_rows=[{"user_id": "user_0001", "current_amount": 750.0}],
).to_dict()
Push Source로 실시간 피처 업데이트
from feast import PushSource
Push 소스 정의
user_realtime_source = PushSource(
name="user_realtime_push",
batch_source=user_transactions_source,
)
실시간 이벤트 발생 시 피처 업데이트
store.push(
push_source_name="user_realtime_push",
df=pd.DataFrame({
"user_id": ["user_0001"],
"total_purchases": [46],
"avg_purchase_amount": [240.12],
"last_purchase_amount": [750.0],
"purchase_frequency": [2.1],
"user_segment": ["gold"],
"event_timestamp": [pd.Timestamp.now()],
"created_timestamp": [pd.Timestamp.now()],
}),
)
Feature Server 배포
로컬 Feature Server 실행
feast serve -h 0.0.0.0 -p 6566
HTTP API로 피처 조회
curl -X POST http://localhost:6566/get-online-features \
-H "Content-Type: application/json" \
-d '{
"features": [
"user_transaction_features:total_purchases",
"user_transaction_features:avg_purchase_amount"
],
"entities": {
"user_id": ["user_0001", "user_0042"]
}
}'
Docker로 Feature Server 배포
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install feast[redis]
COPY feature_repo/ feature_repo/
WORKDIR /app/feature_repo
Registry 적용 & 서버 실행
CMD feast apply && feast serve -h 0.0.0.0 -p 6566
docker-compose.yml
services:
feast-server:
build: .
ports:
- '6566:6566'
depends_on:
- redis
environment:
- REDIS_URL=redis://redis:6379
redis:
image: redis:7-alpine
ports:
- '6379:6379'
Airflow와 연동 (자동 Materialize)
dags/feast_materialize.py
from airflow import DAG
from airflow.operators.bash import BashOperator
from datetime import datetime, timedelta
default_args = {
"owner": "ml-platform",
"retries": 2,
"retry_delay": timedelta(minutes=5),
}
with DAG(
dag_id="feast_materialize",
default_args=default_args,
schedule_interval="0 */6 * * *", # 6시간마다
start_date=datetime(2026, 1, 1),
catchup=False,
) as dag:
materialize = BashOperator(
task_id="materialize_incremental",
bash_command=(
"cd /opt/feature_repo && "
"feast materialize-incremental $(date -u +'%Y-%m-%dT%H:%M:%S')"
),
)
마무리
Feast를 활용한 피처 파이프라인의 핵심 포인트를 정리하면:
- **일관된 피처 정의**: 학습과 서빙에서 동일한 피처 정의 사용으로 Training-Serving Skew 방지
- **Point-in-Time Join**: 시간 기준 정확한 피처 조인으로 데이터 누수 방지
- **오프라인/온라인 이중화**: 배치 학습은 오프라인 스토어, 실시간 서빙은 온라인 스토어
- **Feature Service**: 모델별 피처 그룹 관리로 재사용성 향상
- **Push Source**: 실시간 이벤트 기반 피처 업데이트 지원
Feature Store는 ML 모델이 1~2개일 때는 과하게 느낄 수 있지만, 모델이 늘어나고 팀이 커지면 필수적인 인프라가 된다. 특히 여러 모델이 같은 피처를 공유할 때 그 가치가 극대화된다.
퀴즈
학습 시 사용한 피처와 서빙 시 사용하는 피처가 달라서 모델 성능이 저하되는 현상이다. 피처 계산
로직의 불일치, 데이터 소스의 차이, 시간 기준의 불일치 등이 원인이다.
기준으로 그 시점 이전의 가장 최신 피처 값을 조인한다. 이를 통해 미래 데이터가 학습에 사용되는
데이터 누수(data leakage)를 방지한다.
오프라인 스토어는 대량의 히스토리컬 피처를 저장하여 배치 학습에 사용되고(파일, BigQuery 등),
온라인 스토어는 최신 피처 값만 저장하여 저지연 실시간 서빙에 사용된다(Redis, DynamoDB 등).
오프라인 스토어의 피처 데이터를 온라인 스토어로 동기화(적재)하는 작업이다. 지정된 시간 범위의 최신
피처 값을 온라인 스토어에 저장하여 실시간 조회가 가능하게 한다.
일반 Feature View는 사전에 계산된 피처를 저장하지만, On-Demand Feature View는 요청 시점에 동적으로
피처를 계산한다. 요청 파라미터와 기존 피처를 조합한 실시간 변환에 사용된다.
모델별로 필요한 피처를 논리적으로 그룹화하여 관리할 수 있다. 어떤 모델이 어떤 피처를 사용하는지
명확히 추적 가능하고, 피처 조회 시 일관된 인터페이스를 제공한다.
온라인 스토어에서 피처 값의 유효 기간을 지정한다. TTL이 지난 피처는 조회 시 null로 반환되어,
오래된(stale) 피처 값이 서빙에 사용되는 것을 방지한다.
실시간 이벤트(결제, 클릭 등)가 발생할 때 즉시 온라인 스토어의 피처를 업데이트해야 하는 경우에
사용한다. 배치 materialize의 주기적 갱신 사이에 최신 상태를 유지할 수 있다.
현재 단락 (1/293)
ML 모델을 프로덕션에 배포할 때 가장 흔한 문제 중 하나가 **Training-Serving Skew**다. 학습 시 사용한 피처와 서빙 시 사용하는 피처가 달라져서 모델 성능이...