

안녕하세요. 지난 포스트에서는 개인화 추천 시스템의 여러 요소 중, 실시간 추론에 필요한 모델을 어떻게 배포하고 운영하는지에 대한 '모델 서빙'을 다루었습니다. 잘 만들어진 모델이 제 역할을 하려면, 추론 시점에 정확한 입력 데이터를 빠르게 전달받는 것이 무엇보다 중요합니다.
이번 포스트에서는 바로 그 모델의 '입력 값'인 피처(Feature)를 어떻게 효율적으로 관리하고 제공하는지에 대한 이야기를 하고자 합니다. 피처 스토어가 없던 시절 추천팀이 겪었던 어려움부터 시작해 도입 이후 무엇이 그리고 어떻게 바뀌었는지, 나아가 시스템이 성장하며 새롭게 마주한 현실적인 운영 이슈를 공유해 드리고자 합니다.
피처 스토어 도입 계기 : 우리는 왜 변화가 필요했나?
피처 스토어라는 시스템을 구축하기로 결정하기까지 저희는 여러 성장통을 겪고 있었습니다. 먼저 기존 시스템의 구조와 그로 인해 발생한 문제점들을 먼저 살펴보겠습니다.
기존 시스템 : Airflow와 Redis를 이용한 피처 관리
추천 모델의 추론 방식은 크게 두 가지로 나뉩니다. 사용자의 실시간 반응을 반영하지 않고 주기적으로 대량의 추천 결과를 미리 계산해 두는 배치(Batch) 추론과, 사용자 요청이 들어오는 즉시 실시간으로 추천 결과를 계산하는 온라인(Online) 추론입니다.
- 배치 추론 환경 : 비교적 간단합니다. 학습에 사용된 데이터가 저장된 데이터 웨어하우스(Hive 등)의 테이블을 조회하여 피처를 가져오고, 추론 결과를 다시 테이블에 저장하면 됩니다.
- 온라인 추론 환경 : 여기서부터 복잡성이 증가합니다. 수 ms 내에 응답해야 하는 실시간 서빙 환경에서는 데이터 웨어하우스를 직접 조회할 수 없습니다. 따라서 빠른 조회가 가능한 인메모리 DB, 저희의 경우 Redis에 피처 값들을 미리 저장해두어야 합니다.
피처 스토어 도입 전, 저희는 이 과정을 다음과 같이 처리했습니다.
- 서빙에 필요한 피처들을 프로토 메시지(Protobuf Message) 형식으로 미리 정의합니다.
- Airflow를 이용해 주기적으로 데이터 웨어하우스에서 피처 값을 읽어옵니다.
- 읽어온 데이터를 정의된 프로토 메시지 형태로 가공하여 Redis에 저장합니다.
- 추천 서버에서는 사용자 요청이 오면 Redis에서 해당 피처를 조회하여 모델 추론에 사용합니다.
우리가 마주했던 문제점들
처음에는 이 방식이 잘 동작하는 것처럼 보였습니다. 하지만 실시간 추론이 필요한 모델의 수가 늘어나고 피처의 종류가 다양해지면서 여러 어려움이 발생하기 시작했습니다.
- 강한 결합과 낮은 재사용성 : 피처를 Redis에 '저장하는 쪽(Airflow DAG)'과 '사용하는 쪽(추천 서버)' 모두가 피처의 출처(어느 테이블의 어떤 컬럼인지)와 저장 방식(어떤 Proto 필드에 어떻게 저장되는지)을 정확히 알고 있어야 했습니다. 새로운 모델에서 기존 피처를 재사용하려면 이 모든 로직을 다시 파악하고 구현해야 했습니다. 피처가 중앙에서 관리되지 않고 각자의 파이프라인에 흩어져 있어 재사용이 어려웠습니다.
- 비효율적인 피처 추가/관리 : 새로운 피처를 하나 추가하려면 항상 프로토 파일을 수정하고, 관련 코드를 전부 재배포해야 했습니다. 이는 매우 번거롭고 실수를 유발하기 쉬운 작업이었습니다. 또한, 현재 어떤 피처들이 누구에 의해 어떻게 사용되고 있는지 현황을 파악하기 어려워 관리가 되지 않았습니다.
- 학습-서빙 데이터의 불일치 : 모델의 성능을 결정하는 가장 중요한 원칙 중 하나는 '학습에 사용한 피처와 서빙에 사용한 피처가 동일해야 한다'는 것입니다. 하지만 기존 방식에서는 학습용 피처를 생성하는 로직과 서빙용 피처를 Redis에 저장하는 로직이 분리되어 있어, 두 값이 항상 같다고 보장하기 어려웠습니다. 미묘한 차이가 모델 성능 저하의 원인이 되기도 했습니다.
- 모델러의 역할 부담 증가 : 모델을 연구하고 개발해야 할 ML 엔지니어가 피처를 서빙하기 위한 백엔드 작업까지 처리하게 되었습니다. 이는 모델 개발의 속도를 저하시키는 큰 병목이었습니다.

이러한 문제들을 해결하고 모델러가 오직 모델링에만 집중할 수 있는 환경을 만들고자 저희는 피처 스토어 구축을 시작했습니다.
목표 : 우리가 꿈꾸는 피처 관리 시스템
피처 스토어 구축을 통해 달성하고자 했던 목표는 명확했습니다.
"모델러는 학습에만 집중하면, 서빙은 자동으로!"
이를 실현하기 위한 세부 목표는 다음과 같습니다.
- 재사용성 : 모든 피처를 중앙에서 관리하고, 한 번 정의된 피처는 누구나 쉽게 가져다 쓸 수 있도록 공유합니다.
- 일관성 : 피처 생성 로직을 단일화하여 학습 환경과 서빙 환경에 항상 동일한 피처 값을 제공합니다.
- 신속성 : ML 엔지니어가 피처를 탐색하고, 개발하며, 실제 모델에 활용하기까지 걸리는 시간을 단축시킵니다.
Ohouse Feature Store, 아키텍처와 도입 과정
피처 스토어를 처음부터 전부 만드는 것은 엄청난 리소스가 필요한 일입니다. 저희는 우선 가장 널리 알려진 오픈소스 피처 스토어인 Feast를 기반으로 시스템을 구축하기로 결정했습니다. Feast는 Entity, Feature View, Feature Service와 같은 잘 정립된 개념과 피처의 명세를 중앙에서 관리하는 레지스트리(Registry) 방식을 제공하여, 우리가 추구하던 목표를 설계하는 데 효과적인 참고 모델이 되었습니다. (컨셉에 대한 자세한 내용은 Feast 공식 문서를 참고하시는 것을 추천해 드립니다.)
하지만 실제 도입 과정은 순탄치만은 않았습니다. 당시 Feast는 아직 성숙하지 않아 크고 작은 버그가 있었고, 저희가 해결하려던 몇몇 문제는 Feast의 기본 기능만으로는 대응하기 어려웠습니다. 따라서 저희는 다음과 같은 전략을 세웠습니다.
“Feast의 ‘레지스트리 관리 방식’은 차용하고, 핵심 기능은 오늘의집 데이터 플랫폼과 추천팀 환경에 맞춰 직접 구현한다.”
즉, 저희는 Feast의 장점을 그대로 가져오면서도 기존에 이미 자리 잡고 있던 인프라(Hive, Spark, Flink 등)를 적극적으로 활용하는 하이브리드 접근 방식을 선택했습니다. 덕분에 새로운 기능을 빠르게 개발할 수 있었고, 동시에 오늘의집 서비스에 꼭 맞는 성능과 안정성까지 확보할 수 있었습니다.
아래의 다이어그램은 이러한 접근 방식을 기반으로 한 오늘의집 피처 스토어 인프라 구성을 대략적으로 보여줍니다.

이 구조에서 가장 핵심적인 요소는 중앙의 Feature Store Registry입니다. 피처 스토어를 구축하면서 저희가 가장 중요하게 생각했던 요구사항 중 하나는 “어디서든 동일한 피처를 안정적으로 가져올 수 있어야 한다”는 점이었습니다.
Feast의 apply 기능을 통해 생성되는 이 Registry 파일은 바로 그 역할을 합니다. 모든 피처 정의가 한곳에 모여 있어 어떤 피처가 어디에서 만들어졌는지, 그리고 어떤 모델이 어떤 피처를 사용하는지 기록됩니다. 말 그대로 피처 스토어의 단일 진실 공급원(Single Source of Truth) 역할을 하게 되는 것이죠.
feature_views {
spec {
name: "product_features"
project: "recommendation"
entities: "product"
features {
name: "selling_cost"
value_type: DOUBLE
}
...
batch_source {
timestamp_field: "timestamp"
name: "product_feature_source"
spark_options {
query: "select selling_cost, ... from product_features_table"
}
}
entity_columns {
name: "product_id"
value_type: INT64
}
}
...
feature_services {
spec {
name: "product_feature_service"
project: "recommendation"
features {
feature_view_name: "product_features"
feature_columns {
name: "selling_cost"
value_type: INT64
}
...
}
features {
feature_view_name: "user_features"
feature_columns {
name: "age"
value_type: INT32
}
}
...
description: "상품 추천 모델용 feature service"
}
}
그리고 이 Registry는 단순히 정의만 모아둔 문서가 아니라, 실제로 Offline Store와 Online Store의 모든 동작을 연결하는 출발점이 됩니다. Offline Store에서는 Registry를 기반으로 학습 데이터셋을 만들고, Online Store에서는 동일한 Registry를 이용해 실시간으로 피처를 가져옵니다. 아래에서는 각각의 영역에서 피처 스토어가 어떤 역할을 하며, 그 과정에서 Registry가 어떻게 활용되고 있는지 조금 더 구체적으로 소개하겠습니다.
Offline Store : 학습 데이터셋 생성
오늘의집 추천팀은 기존에 PySpark를 config 기반으로 실행하도록 시스템을 구성해 두었습니다. 공용으로 Pyspark UDF (User Defined Function)를 정의해두면 어디서든 쉽게 재사용하고, 여러 UDF를 조합해서 유연하게 데이터를 처리할 수 있는 방식입니다. 데이터셋을 생성하는 것도 이와 유사한 인터페이스로 손쉽게 할 수 있도록 피처 스토어용 UDF를 구현하였습니다.
데이터셋 생성은 기본적으로 피처와 라벨 데이터를 조인하는 것입니다. 따라서 사용자가 데이터셋에 필요한 피처들의 이름만 입력하면, UDF 내부에서 Feature Store Registry를 참조해 각 피처의 출처를 찾아 자동으로 데이터를 가져오고 조인하도록 설계했습니다.
이 과정에서 중요한 점은 데이터 누수(Data Leakage)를 방지하는 것입니다. 예를 들어, 특정 사용자가 홈 화면에서 콘텐츠 3개를 조회하고 그중 2개에 “좋아요”를 남긴 뒤 09:00:00에 하나의 콘텐츠를 클릭했다고 가정해 보겠습니다. 이때 클릭 이벤트가 라벨(label)이 되며, 학습용 데이터셋에서는 이 클릭 이전에 발생한 유저의 조회·좋아요 이력만을 기반으로 피처를 조인해야 합니다. 다시 말해, “09:00:00 클릭”이라는 라벨을 기준으로 09:00:00 이전까지의 행동 로그만 유효한 피처가 됩니다. 이러한 요구사항을 반영하기 위해, 데이터셋 생성 시 필요한 피처 이름과 라벨 timestamp 컬럼을 config에 정의하면 UDF 내부에서 point-in-time join을 수행하여 정확한 학습 데이터를 구성하도록 하였습니다.

아래는 이러한 방식으로 데이터셋을 구성하기 위한 예시 config입니다.
dataset_generation:
load: -- 라벨 데이터를 가져오기 위한 쿼리
sql: |
SELECT ..., click_timestamp
FROM
click_label_table
transform:
- name: feature_join -- Registry에 등록된 feature 이름만 입력하면 데이터를 모두 조인해주는 커스텀 함수
join_columns: [user_id]
features: [
IMPRESSION_LIST,
LIKE_LIST,
age,
...
]
timestamp_col: click_timestamp
save:
output_tb_name: "dataset_table"
이처럼 config 기반으로 데이터셋 생성 과정을 단순화함으로써, 학습용 데이터셋 제작이 일원화되고 데이터 누수 방지와 피처 관리의 일관성까지 확보할 수 있게 되었습니다.
Online Store : 실시간 추론 대응
실시간 추론을 위해서는 아래 두 가지 단계를 거치게 됩니다.
첫 번째 단계는 오프라인(Offline) 피처를 온라인(Online)으로 옮기는 과정입니다. 학습 시 사용했던 피처 테이블에는 각 엔티티(entity, 예: 사용자 ID, 상품 ID 등)별로 시간에 따른 여러 피처 값이 저장되어 있습니다. 이 중 가장 최신의 값을 온라인 스토어로 옮겨주는 작업을 Feast에서는 “materialize” 라고 부릅니다. materialize 과정에서는 Feature Registry에 저장된 피처 테이블 정보를 기반으로 어떤 테이블에서 데이터를 가져올지, 어떤 컬럼이 entity(기본 키)인지, 그리고 어떤 timestamp 컬럼을 기준으로 최신 값을 선택할지를 결정합니다. 이를 통해 학습 시점의 데이터와 동일한 스키마와 로직으로 온라인 스토어에 최신 상태의 피처를 적재할 수 있습니다.
두 번째 단계는 실시간 추론 시점에서 피처를 조회하는 과정입니다. 서버는 추론 요청이 들어오면 Feature Store Registry에 정의된 Feature Service를 참조합니다. Feature Service는 모델별로 어떤 Feature View에서 어떤 피처들을 묶어 사용할지를 정의한 구성단위로, 이를 통해 서버는 필요한 피처 목록을 정확하게 파악하고 Online Store에서 해당 피처들을 조회합니다. 이러한 구조 덕분에 동일한 이름의 피처가 여러 Feature View에 존재하더라도 현재 필요한 피처가 “어떤 테이블의 어떤 Feature View에 정의되어 있는 피처인지” 가 명확하게 구분됩니다. 결과적으로 학습과 서빙 시점의 데이터 불일치 문제가 시스템적으로 방지되며, 온·오프라인 간 데이터 정합성이 보장됩니다.

개발 과정에서 발생한 이슈들
설계할 때와 달리 시스템을 실제로 개발하고 도입하면서 저희는 예상하지 못한 여러 현실적인 제약과 기술적 문제를 마주했으며, 이를 해결하는 과정에서 시스템에 많은 변화가 있었습니다.
1. Python 기반의 Feast 서버에 의한 네트워크 지연 문제
Feast에서는 Python 기반의 Feature Server를 제공합니다. 초기에는 이를 그대로 활용하려 했지만, 오늘의집 추천 시스템은 Go로 구현되어 있어 HTTP 요청에 따른 지연 시간이 부담이 되었습니다. 당시 Feast의 공식 Go Feature Server는 안정성이 낮고 일부 기능이 미지원 상태였기 때문에, 온라인 피처 조회 로직을 저희가 Go로 직접 구현하는 방식을 선택했습니다. 이로써 Feast의 인터페이스 구조는 그대로 유지하되, 내부 로직은 시스템 환경에 맞게 최적화할 수 있었습니다. 쿼리를 개선하여 응답 속도를 높였고, Feast에서 제공하지만 저희가 사용하지 않는 기능들은 제거하여 불필요한 오버헤드를 줄였습니다.

2. Feast와 Redis 조합으로 인한 구조적 제약
초기에는 Redis Cluster를 Online Store로 사용하며, 피처를 hash 타입으로 저장하고 있었습니다. key에는 entity ID를, field에는 피처 이름을 저장하는 구조였습니다. 그러나 이 방식에는 여러 단점이 있었습니다. 당시 사용 중이던 Redis 버전에서는 hash 타입에 대해 TTL을 개별 필드 단위가 아닌 key 단위로만 적용할 수 있었기 때문에, 사용하지 않는 피처 데이터가 자동으로 정리되지 않는 문제가 있었습니다. 더불어 피처의 개수가 빠르게 증가함에 따라 ElastiCache 비용이 급격히 증가했고, 클러스터를 자주 증설해야 하는 비효율도 존재했습니다. 이러한 구조적 제약을 근본적으로 해결하기 위해 Online Store를 Redis에서 ScyllaDB로 교체하는 대규모 마이그레이션을 진행했습니다. ScyllaDB는 디스크 기반 스토리지를 사용하기 때문에 확장성과 비용 효율성이 뛰어나며, 앞서 언급한 Redis의 단점도 모두 해소되었습니다. 조회 속도가 Redis에 비해 다소 느려지기는 했지만, 내부적으로 쿼리 최적화와 조회 방식 개선을 병행하여 서빙 지연 시간을 최소화할 수 있었습니다. 그 결과, 인프라 비용을 약 1/8 수준으로 절감하면서도 안정적인 온라인 피처 서빙 성능을 유지할 수 있었습니다.
3. 유연한 인터페이스 도입 및 스택 변경을 통한 시스템 고도화
저희는 기존 Online Store로 사용하던 Redis를 ScyllaDB로 교체하는 과정을 진행했습니다. 기본적으로 Feast는 다양한 스택의 Offline 및 Online Store 백엔드를 지원하기 때문에 피처를 적재하는 부분은 Feast 설정 파일만 수정하면 되었습니다. 그러나, 피처를 조회하는 로직은 직접 구현했기 때문에 일부 코드 변경이 필요했습니다. 이를 계기로, 스토어 간 교체를 유연하게 할 수 있도록 스토리지 인터페이스를 새롭게 정의했습니다. 현재는 Redis와 ScyllaDB를 모두 지원할 수 있는 형태로 구현되어 있으며, 향후 새로운 타입의 스토리지를 추가하거나 기존 스토리지를 교체할 때도 코드 변경을 최소화하면서 손쉽게 확장할 수 있게 되었습니다. Offline Store에서 Online Store로 데이터를 적재하는 과정에서도 점점 커지는 데이터 규모에 대응하기 위해 기존의 Amazon Athena 쿼리 엔진을 사용하던 것을 Spark 기반으로 전환했습니다. 이를 통해 대규모 피처 조인이 필요한 학습용 데이터셋 생성 과정에서도 확장성을 챙길 수 있었습니다.
도입 이후와 앞으로의 계획
지금까지 오늘의집 피처 스토어의 도입배경 부터 아키텍처, 그리고 운영 과정에서 겪었던 이슈들까지 소개해드렸습니다. 현재는 수백 개의 피처를 서빙하고 있으며 수십 개의 모델에서 사용되는 피처들을 안정적으로 공급하고 있습니다. 이러한 성과는 단순히 시스템 안정화에 그치지 않고, 조직 전반에 아래와 같은 긍정적인 변화를 가져왔습니다.
긍정적 변화
1. 업무 효율성 증대
피처 스토어 도입 이후, ML 엔지니어의 업무 방식이 바뀌었습니다. 기존에는 새로운 피처를 학습하고 서빙하기 위해 복잡한 데이터 파이프라인을 구성하고, 서버 코드 작성부터 배포까지 여러 단계를 거쳐야 했습니다. 이 과정은 많은 시간이 소요될 뿐 아니라 파이프라인 간 중복 코드나 관리 부담도 적지 않았으나, 이제는 서빙 환경으로 피처를 제공하기 위해 필요한 과정이 크게 단순화되었습니다. 엔지니어는 피처의 데이터 소스와 변환 로직만 선언적으로 작성하면 되고, 나머지 파이프라인 생성·배포 과정은 자동화되어 처리됩니다. 덕분에 새로운 피처를 실제 서비스에 손쉽게 적용할 수 있게 되었습니다.
2. 투명성 확보
어떤 모델이 어떤 피처를 사용하고 있는지, 해당 피처가 어디서 왔고 어떤 값을 의미하는지 누구나 쉽게 파악할 수 있게 되었습니다. 물론 모든 피처의 명세는 YAML 파일로 관리되지만, 텍스트만으로는 피처의 현황과 상태를 직관적으로 파악하는 데 한계가 있었습니다. 이를 위해 저희는 이러한 모든 정보를 시각적으로 탐색할 수 있는 피처 카탈로그(Feature Catalog)를 구축했습니다. 이 카탈로그를 통해 사용자는 피처의 설명이나 데이터 타입뿐만 아니라, 분포나 값의 범위 같은 주요 통계 정보까지 한눈에 확인할 수 있습니다.

3. 안정성
모든 피처가 중앙 Registry를 기준으로 관리되면서, 학습과 서빙 단계 모두 동일한 피처 정의를 참조하도록 구조화되었습니다. 덕분에 데이터가 잘못 들어가거나 파이프라인에 문제가 발생했을 때, 원인을 빠르게 파악하고 대응할 수 있게 되었습니다. 피처 적재 과정과 서빙 상태를 모니터링할 수 있는 시스템도 함께 추가하였는데, 이상 상황을 신속히 감지하고 조치할 수 있어 모델 성능 저하를 예방할 수 있습니다. 이처럼 중앙 관리와 체계적 모니터링을 통해 운영 안정성을 크게 높일 수 있었습니다.
4. 관리 편의성
피처 스토어는 Git 기반의 선언적 관리 방식을 따르고 있습니다. 새로운 피처를 추가하거나 수정·삭제할 때마다 YAML 정의 파일을 변경하고, 이 변경사항은 Pull Request(PR) 형태로 관리되어 코드 리뷰를 거칩니다. 이를 통해 모든 변경 내역이 명확히 기록되고, 누가 언제 어떤 피처를 수정했는지를 투명하게 추적할 수 있습니다. 또한 CI/CD 파이프라인과 연동되어 리뷰가 승인되면 자동으로 배포가 이루어지기 때문에, 사용자는 수동 배포나 환경 설정에 신경 쓰지 않고 피처 개발에만 집중할 수 있습니다.
5. 효율적인 저장소 선택
피처 스토어를 도입하면서 데이터 저장과 조회 구조를 재정비했습니다. 이 과정에서 피처의 접근 패턴(읽기 빈도, 피처 형태, 데이터 크기 등)에 따라 적합한 저장소와 접근 방식을 선택하도록 하였습니다. 예를 들어, 읽기 빈도는 높지만 데이터 크기가 크지 않은 피처의 경우 여전히 Redis를 사용해 빠른 응답 속도를 유지하고 있습니다. 이처럼 피처의 특성에 따라 저장소를 유연하게 선택하면서도, 모든 피처를 중앙에서 일관되게 관리함으로써 중복 적재나 불필요한 파이프라인 생성을 방지할 수 있습니다.
Future work
피처 스토어는 한 번의 구축으로 완성되는 것이 아니라 끊임없이 개선하고 발전시켜야 하는 '현재 진행형' 프로젝트입니다. 저희는 더 나은 피처 스토어를 만들기 위해 아래와 같은 여러 개선 작업을 계획하고 있습니다.
1. 실시간으로 피처 제공
현재는 대부분 분석용 테이블에 저장된 값을 배치 작업으로 온라인 스토어에 옮기고 있으며, 일부 피처에 대해서만 실시간으로 피처를 저장하고 사용 중입니다. 앞으로는 서비스 운영 DB에서 데이터가 변경되는 순간 이를 감지하고 피처로 활용할 수 있도록, CDC(Change Data Capture) 기술 기반의 실시간 피처 파이프라인을 구축할 예정입니다. 이를 통해 최신 데이터를 기반으로 추론할 수 있고, 사용자에게 제공되는 추천 결과의 정확도와 신속성을 높일 수 있습니다.
2. 실시간으로 모델 학습하기
현재 대부분의 모델 학습은 하루 단위로 이루어지고 있습니다. 향후에는 피처 스토어에서 제공되는 실시간 피처를 학습용 데이터셋 생성에도 활용할 수 있도록 확장하려고 합니다. 실시간으로 업데이트되는 피처와 기존 오프라인 데이터를 함께 조인하여 원하는 형식의 학습 데이터셋을 생성할 수 있도록 할 예정입니다. 이렇게 된다면 모델 학습 주기를 한 시간 단위 혹은 그 이하로 단축할 수 있으며, 결과적으로 더 빠르게 최신 데이터를 반영하여 모델 성능 향상에도 기여할 수 있을 것입니다.
오늘의집 피처 스토어의 여정은 이제 막 중요한 변곡점을 지났습니다. 저희의 경험이 피처 스토어 도입을 고민하거나 운영 중인 많은 분들께 작은 도움이 되기를 바랍니다. 앞으로도 더 똑똑하고 효율적인 MLOps 환경을 만들어나가는 오늘의집의 여정에 많은 관심과 응원 부탁드립니다.
