들어가며: 왜 멀티스테이지 아키텍처인가?

이커머스 홈페이지에 처음 방문한 사용자에게 '이 사람이 뭘 좋아할지' 맞추는 일은 난제입니다. 특히 제품 카탈로그가 수백만 개로 늘어나면 모든 아이템을 실시간으로 평가하는 건 불가능에 가깝죠.

이 글에서 소개할 시스템은 Two-Tower 모델로 후보를 빠르게 추리고(Retrieval), DLRM 랭커로 정밀하게 점수를 매긴 뒤(Ranking), 최종 재정렬(Reranking) 하는 전형적인 멀티스테이지 구조를 채택했습니다. 단순한 구조 설명이 아니라, 실제로 13M 인터랙션 데이터를 기반으로 Kubeflow, NVIDIA Triton, FAISS, Feast, ElastiCache 등을 활용해 엔드 투 엔드로 배포한 과정을 집중적으로 다룹니다.

이 글은 원문 Deploying a Multistage Multimodal Recommender System on Amazon EKS에서 다루는 내용을 한국 개발자 커뮤니티 맥락에 맞게 재구성했습니다.

Kubernetes cluster diagram showing multi-stage recommender system pipeline with Kubeflow and NVIDIA Triton System Abstract Visual

시스템 아키텍처: 4단계 파이프라인

전체 시스템은 크게 네 단계로 구성됩니다.

  1. Two-Tower 모델로 사용자-아이템 매칭 점수 계산 → 후보군 생성
  2. Bloom Filter로 사용자가 이미 본 아이템 제거
  3. DLRM 랭커가 사용자, 아이템, 컨텍스트(디바이스, 시간 등) 피처를 종합해 클릭 확률 예측
  4. 최종 재정렬(Softmax Sampling) 로 다양성 확보

핵심 컴포넌트

컴포넌트역할비고
Kubeflow Pipelines전체 학습 & 일일 파인튜닝 워크플로우 관리Kubernetes 기반
NVIDIA Merlin (NVTabular + Triton)GPU 가속 피처 엔지니어링 & 모델 서빙멀티스테이지 그래프를 단일 앙상블 모델로
FAISSANN 인덱스 기반 후보 검색Approximate Nearest Neighbor
Feast (오프라인/온라인 Feature Store)사용자/아이템 피처 관리ElastiCache for Valkey 백엔드
Amazon EKS컨테이너화된 ML 워크로드 오케스트레이션Karpenter + HPA 오토스케일링

데이터 흐름 (요청 처리)

1. 요청 도착 (user_id, device_type, timestamp)
2. context_preprocessor → 결측치 처리, 시간 피처 생성
3. feast_user_lookup → 사용자 피처 조회 (온라인 스토어)
4. nvt_user_transform → NVTabular 변환
5. query_tower → 사용자 임베딩 생성
6. faiss_retrieval → Top-K 후보 검색
7. filter_seen_items → Bloom Filter로 이미 본 아이템 제거
8. feast_item_lookup + nvt_item_transform + multimodal_embedding_lookup → 아이템 피처 + CLIP/SBERT 임베딩
9. unroll_features → 사용자/컨텍스트 피처를 후보 수만큼 타일링
10. dlrm_ranking → 점수 계산
11. softmax_sampling → 다양성 모드에 따라 Top-K 선택
12. item_id_decoder → 원래 item_id로 매핑 후 응답

이 과정에서 14개의 모델이 하나의 Triton Ensemble로 동작합니다. 이 부분이 특히 인상적인데, 각 모델은 Python 백엔드(피처 변환, 필터링) 또는 TensorFlow 백엔드(쿼리 타워, 랭커)로 구현되어 있고, Triton이 DAG(Directed Acyclic Graph)로 실행을 관리합니다.

NVIDIA Triton Inference Server ensemble model execution flow on Amazon EKS Software Concept Art

실무 적용 팁 & 주의사항

1. Cold-Start 처리: Feature Masking의 힘

새로운 사용자나 익명 사용자에게 정적인 기본 리스트를 보여주는 대신, 학습 데이터의 5%를 랜덤으로 마스킹해서 OOV(Out-Of-Vocabulary) 임베딩을 학습시켰습니다. 아래 코드가 핵심 로직입니다.

import cudf
import cupy
import os

ANONYMOUS_USER = -1
OOV_GENDER = -1
OOV_TOP_CATEGORY = -1
OOV_DEVICE = -1

masked_train_dir = "./masked_train"
os.makedirs(masked_train_dir, exist_ok=True)

for i in range(train_days):
    day = cudf.read_parquet(f"./train_day_{i:02d}.parquet")
    n = len(day)
    
    # 5% 확률로 사용자 및 카테고리 정보 마스킹
    user_mask = cupy.random.random(n) < 0.05
    day.loc[user_mask, "user_id"] = ANONYMOUS_USER
    day.loc[user_mask, "gender"] = OOV_GENDER
    day.loc[user_mask, "top_category"] = OOV_TOP_CATEGORY
    
    # 5% 확률로 디바이스 타입 마스킹
    device_mask = cupy.random.random(n) < 0.05
    day.loc[device_mask, "device_type"] = OOV_DEVICE
    
    day.to_parquet(f"{masked_train_dir}/train_day_{i:02d}.parquet", index=False)

이렇게 하면 모델이 '모르는 사용자' 상황에서도 컨텍스트(디바이스, 시간)에 의존해 합리적인 추천을 할 수 있습니다. 국내 SI 프로젝트에서도 신규 사용자 온보딩 단계에서 이 기법을 적용하면 이탈률을 크게 낮출 수 있어요.

2. 성능 병목 해결: 인메모리 캐싱

프로파일링 결과, feast_item_lookup요청당 195ms를 소비하며 전체 지연 시간의 52% 를 차지했습니다. 해결책은 간단했습니다.

# 초기화 시 모든 아이템 피처를 한 번에 로드하여 NumPy 배열로 캐싱
import numpy as np

class ItemFeatureCache:
    def __init__(self, feast_client, item_ids):
        self.cache = {}
        for item_id in item_ids:
            features = feast_client.get_online_features(
                features=["item:feature1", "item:feature2"],
                entity_rows=[{"item_id": item_id}]
            ).to_dict()
            self.cache[item_id] = np.array([
                features["item:feature1"][0],
                features["item:feature2"][0]
            ])
    
    def lookup(self, item_id):
        return self.cache.get(item_id, self.default_vector)

결과: feast_item_lookup 지연 시간 99.7% 개선, 전체 지연 시간 54% 개선, 처리량 310% 향상. 아이템 속성이 자주 바뀌지 않는 환경에서는 이 정도 트레이드오프는 충분히 감수할 만합니다.

3. 오토스케일링 전략

EKS에서 Triton Inference Server를 운영할 때 Kubernetes HPA + Karpenter 조합을 사용했습니다. HPA는 30초 평균 큐 대기 시간을 커스텀 메트릭으로 사용합니다.

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: triton-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: triton-inference-server
  minReplicas: 1
  maxReplicas: 10
  metrics:
  - type: Pods
    pods:
      metric:
        name: triton_queue_latency_ms
      target:
        type: AverageValue
        averageValue: 100  # 100ms 초과 시 스케일 아웃

이 방식의 장점은 GPU 인스턴스가 부족할 경우 Karpenter가 자동으로 신규 노드를 프로비저닝한다는 점입니다. 트래픽이 갑자기 몰리는 블랙프라이데이 같은 상황에 특히 유용하죠.

Data pipeline architecture diagram for ecommerce recommender system with feature store and Bloom filter Algorithm Concept Visual

한계점과 다음 스텝

이 시스템은 분명히 프로덕션 레벨에 가깝지만, 몇 가지 아쉬운 점도 있습니다.

  1. 컨텍스트 피처가 랭커에만 사용됨 → 검색 단계에서도 컨텍스트를 반영하면 더 관련성 높은 후보를 뽑을 수 있음
  2. 세션 기반 모델 부재 → 현재는 top_category로 단기 관심사를 근사하지만, 트랜스포머 기반 세션 인코더로 대체하면 더 정교해짐
  3. 데이터 버저닝 & 실험 추적 미비 → Git 커밋으로는 재현 가능하지만, 데이터 스냅샷이 고정되지 않음. MLflow나 Weights & Biases 도입 필요
  4. 온라인 모델 모니터링 부재 → 성능 드리프트 감지가 안 됨. TFDV 같은 도구로 데이터 검증 파이프라인을 추가해야 함

다음 단계로 추천하는 학습 방향:

요약: 멀티스테이지 추천 시스템은 단순히 모델 성능만으로 완성되지 않습니다. 피처 스토어, 캐싱 전략, 오토스케일링, MLOps 파이프라인까지 인프라 전반을 고려한 설계가 필수입니다. 이 글이 여러분의 프로젝트에 실질적인 도움이 되길 바랍니다.

본 콘텐츠는 신뢰할 수 있는 출처를 바탕으로 AI 도구를 활용하여 초안이 작성되었으며, 편집자의 검토를 거쳐 발행되었습니다. 전문가의 조언을 대체하지 않습니다.