본문 바로가기
DevOps

200ms latency- 실시간 개인화 시스템 아키텍처 Deep Dive

by Rainbound-IT 2026. 2. 26.
반응형

 

 

넷플릭스 앱을 열었다. 화면이 뜨는 순간 "오늘 이거 어때요?"라는 추천이 이미 펼쳐져 있다. 그 0.2초 동안 벌어진 일: 수십만 개의 콘텐츠를 훑고, AI가 점수를 매기고, 결과를 정렬했다. 이 글은 그 0.2초의 비밀에 대한 이야기다.

이 글은 NBC유니버설 글로벌 플랫폼 엔지니어링 부사장 Manoj Yerrasani의 ITWorld 기고를 바탕으로, 실전 아키텍처와 코드 레벨의 해설을 더해 구성했다.


1. 왜 하필 200ms인가

인간의 뇌는 200밀리초 이내의 응답을 "즉각적"으로 인식한다. 이건 UX 감성이 아니라 인지 심리학의 임계값이다.

 

응답 시간 사용자가 느끼는 것   비즈니스 임팩트
0~100ms "바로 나왔네" 최적의 경험
100~200ms "괜찮은데?" 수용 가능
200~500ms "좀 느린데…" 이탈률 증가 시작
500ms~1s "답답하다" 전환율 급감
1s 이상 "뭐지?" (앱 종료) 사용자 유실

Amazon의 유명한 연구가 이걸 돈으로 환산했다:

"지연 시간 100ms 증가 = 매출 1% 감소"

스트리밍 업계에서는 더 가혹하다. 추천이 느리면 사용자는 탐색을 포기하고 앱을 닫는다. 200ms는 "목표"가 아니라 "생존 조건"이다.


2. 투패스(Two-Pass) 아키텍처 — 핵심 설계

10만 개의 콘텐츠를 정밀한 AI 모델로 한꺼번에 평가하면? 200ms는 꿈도 못 꾼다. 해법은 두 단계로 쪼개는 것이다.

전체 흐름

 

Pass 1: 20ms 안에 10만 개를 500개로

첫 번째 관문의 핵심은 ANN(Approximate Nearest Neighbor) 검색이다. 정확한 최근접 이웃을 찾는 대신, "충분히 가까운" 후보를 빠르게 찾는다.

import hnswlib

# HNSW 인덱스 초기화 (Hierarchical Navigable Small World)
index = hnswlib.Index(space='cosine', dim=256)
index.init_index(max_elements=100000, ef_construction=200, M=16)
index.add_items(item_embeddings, item_ids)

# ef 파라미터로 정확도-속도 트레이드오프 조절
index.set_ef(50)
labels, distances = index.knn_query(user_embedding, k=500)

실무에서 자주 비교되는 ANN 라이브러리는 다음과 같다:

 

라이브러리  만든 곳 특징   지연 시간
HNSW hnswlib 그래프 기반, 높은 재현율 ~5ms
Faiss Meta GPU 가속, 대규모에 강함 ~3ms
ScaNN Google 양자화 기반, 균형적 ~4ms
Annoy Spotify 트리 기반, 가벼움 ~8ms

Pass 2: 500개에 집중하는 정밀 스코어링

후보가 500개로 줄어들면, 이제 무거운 모델을 꺼낼 차례다. 사용자 특성, 아이템 특성, 실시간 상호작용 신호까지 수백 개의 Feature를 동시에 고려해서 점수를 매긴다.

features = {
    "user_genre_affinity": [0.8, 0.3, 0.1, ...],  # 장르 선호도
    "watch_history_embedding": user_vec,             # 시청 이력
    "time_of_day": 21,                               # 시간대
    "content_popularity": 0.85,                      # 콘텐츠 인기도
    "freshness_score": 0.92,                         # 신선도
    "user_item_similarity": cosine_sim,              # 유사도
}

scores = ranking_model.predict(feature_matrix)
top_20 = sorted(zip(candidates, scores), key=lambda x: -x[1])[:20]

3. 콜드 스타트 — 처음 온 사용자에게 뭘 보여줄 것인가

아무 이력도 없는 신규 사용자. 추천 시스템의 가장 오래된 난제다.

NBC유니버설이 선택한 답은 세션 벡터(Session Vector) 전략이다. 과거가 아니라 "지금 이 순간"의 행동에 집중한다.

 

 

사용자가 3~5번만 상호작용해도 의미 있는 벡터가 만들어진다. "이 사람은 액션 영화를 클릭하고, 코미디를 건너뛰고, 한국 드라마를 검색했다" — 이 정도면 꽤 강력한 신호다.

 

전략  필요한 데이터  응답 속도  개인화 품질
인기 기반 없음 즉시 낮음
세션 벡터 3~5회 상호작용 ~10ms 중간~높음
협업 필터링 수십 회 ~50ms 높음
딥러닝 개인화 수백 회 ~100ms 매우 높음

4. 사전 계산 vs 적시 추론 — 모든 걸 실시간으로 돌릴 필요는 없다

파레토 법칙이 여기서도 작동한다. 상위 20%의 인기 콘텐츠가 전체 조회의 대부분을 차지한다. 이걸 매번 실시간으로 계산하는 건 GPU 낭비다.

헤드 콘텐츠 (상위 20%) → 사전 계산

인기 콘텐츠의 추천 결과는 미리 계산해서 캐시에 넣어둔다.

# 배치 파이프라인: 15분마다 사전 계산
def precompute_recommendations(user_segment, items):
    top_items = ranking_model.batch_predict(user_segment, items)
    redis.setex(f"recs:{user_segment}", timedelta(minutes=15), json.dumps(top_items))

# 서빙: O(1) 조회, < 5ms
def get_recommendations(user_id):
    cached = redis.get(f"recs:{get_user_segment(user_id)}")
    return json.loads(cached) if cached else fallback_recommendations()

저장소는 Redis, DynamoDB, Cassandra 같은 초저지연 스토어를 사용한다. 응답 시간은 5ms 이내.

테일 콘텐츠 (하위 80%) → 적시 추론(JIT)

롱테일 영역은 사전 계산의 가성비가 떨어진다. 여기는 요청이 들어올 때 GPU 기반으로 실시간 추론한다. 대신 최신 사용자 행동을 즉시 반영할 수 있다는 장점이 있다.


5. 모델 최적화 — FP32에서 INT8로, 속도 2배

200ms 안에 딥러닝 모델을 돌리려면 **양자화(Quantization)**가 필수다. 32비트 부동소수점을 8비트 또는 4비트 정수로 압축하는 기법이다.

"모델 크기는 최대 1/4로 줄고, GPU 메모리 대역폭 사용량도 크게 감소한다. 정확도 손실은 0.5% 미만이면서 속도는 2배 향상된다."

PyTorch에서는 몇 줄이면 적용 가능하다:

from torch.quantization import quantize_dynamic

quantized_model = quantize_dynamic(
    original_model,
    {torch.nn.Linear},
    dtype=torch.qint8
)
# FP32: ~45ms → INT8: ~22ms (약 2x 향상)

양자화 외에도 다양한 경량화 기법이 있다:

 

기법  크기 감소 속도 향상   난이도 언제 쓰나
양자화 (INT8) 4x 2x 낮음 가장 먼저 적용
프루닝 2~10x 1.5x 중간 모델이 과도하게 클 때
지식 증류 가변 3~5x 높음 소형 모델로 대체할 때
ONNX Runtime - 1.5~2x 낮음 프레임워크 독립 서빙
TensorRT - 2~4x 중간 NVIDIA GPU 환경

6. 복원력 설계 — 실패는 반드시 온다

분산 시스템에서 "실패하지 않는 시스템"은 존재하지 않는다. 중요한 건 실패했을 때 사용자가 눈치채지 못하는 것이다.

서킷 브레이커 패턴

전기 회로의 차단기와 같은 원리다. 추천 서비스가 연속으로 실패하면 회로를 끊고, 캐시된 기본 목록으로 즉시 전환한다.

 

 

핵심 설정값:

  • 타임아웃: 150ms (전체 200ms에서 50ms 여유)
  • 차단 기준: 연속 3회 실패
  • 복구: 30초 후 자동으로 반개방 시도
from circuitbreaker import circuit

@circuit(failure_threshold=3, recovery_timeout=30, expected_exception=TimeoutError)
def get_personalized_recommendations(user_id):
    with timeout(0.15):  # 150ms
        return recommendation_service.rank(user_id)

def get_recommendations_safe(user_id):
    try:
        return get_personalized_recommendations(user_id)
    except CircuitBreakerError:
        return get_cached_popular_items()  # 폴백

4단계 Graceful Degradation

폴백도 한 종류가 아니다. 장애 수준에 따라 단계적으로 품질을 낮추면서도 서비스는 유지한다.

 

단계  조건  제공하는 추천  품질
Level 0 정상 AI 모델 + 실시간 특성 최상
Level 1 모델 서버 지연 캐시된 이전 결과 재사용 양호
Level 2 캐시 미스 사용자 세그먼트별 추천 보통
Level 3 전체 장애 에디터 큐레이션 인기 목록 최소

사용자 입장에서 Level 3까지 가도 "추천이 좀 뻔하네" 정도지, "서비스가 죽었다"는 느끼지 못한다. 이게 핵심이다.


7. 데이터 계약 — 쓰레기가 들어가면 쓰레기가 나온다

ML 모델은 입력 데이터 품질에 극도로 민감하다. 스키마 검증 없이 파이프라인에 데이터를 흘리면 모델이 오염된다.

Protobuf 스키마로 입구를 지킨다

message UserEvent {
  string user_id = 1;        // 필수, UUID 형식
  string event_type = 2;     // 필수, "click" | "view" | "search"만 허용
  int64 timestamp_ms = 3;    // 필수, 현재 시간 ± 24시간
  string content_id = 4;
  map<string, string> metadata = 5;
}

"데이터가 파이프라인에 들어오기 전에 스키마 검증을 강제한다."

검증을 통과하지 못한 이벤트는 **DLQ(Dead Letter Queue)**로 격리한다. ML 모델에는 절대 도달하지 못하게 막는 것이다.

 

 


8. 옵저버빌리티 — 평균의 함정을 피하라

"평균 지연 시간은 허상에 가까운 지표이다."

평균이 100ms라고 안심하면 안 된다. 상위 1%의 사용자가 2초 이상 기다리고 있을 수 있다. 이 1%가 가장 충성도 높은 파워 유저일 수도 있다.

진짜 봐야 할 지표

 

지표  의미  목표  초과 시 액션
p50 절반의 사용자 < 50ms 트렌드 추적
p95 20명 중 1명 < 150ms 팀 알림
p99 100명 중 1명 < 200ms 즉시 대응
p99.9 1,000명 중 1명 < 500ms 인시던트 선언

Prometheus + Grafana로 구성하면 이런 알림 규칙이 된다:

groups:
  - name: recommendation-slo
    rules:
      - alert: RecommendationP99TooHigh
        expr: |
          histogram_quantile(0.99,
            rate(recommendation_latency_seconds_bucket[5m])
          ) > 0.2
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "추천 p99 지연이 200ms SLO를 초과했습니다"

폴백 비율도 중요한 지표다. 정상 상태에서 1% 이상이면 어딘가 문제가 있다는 신호다.


9. 미래 — 에이전틱 아키텍처로의 전환

지금까지의 추천 시스템은 결국 **"콘텐츠 목록을 정렬하는 것"**이다. UI는 고정되어 있고, AI는 목록 안의 순서만 바꾼다.

하지만 미래는 다르다. AI 에이전트가 UI 자체를 능동적으로 구성하는 방향으로 진화하고 있다.

 

 

같은 콘텐츠라도 어떤 사용자에게는 큰 썸네일로, 다른 사용자에게는 텍스트 리스트로, 또 다른 사용자에게는 자동 재생 프리뷰로 — 보여주는 방식 자체가 개인화되는 것이다.


핵심 정리

 

지연 시간 200ms = 사용자 인지 임계값 100ms 지연 → 매출 1%↓
아키텍처 두 단계로 쪼갠다 10만 → 500 → 20개
콜드 스타트 과거 이력 대신 현재 행동 3~5회 상호작용이면 충분
모델 최적화 양자화가 첫 번째 수단 크기 1/4, 속도 2배
복원력 실패해도 서비스는 살린다 150ms 타임아웃 + 4단계 폴백
모니터링 평균 보지 말고 p99를 봐라 평균은 허상이다

마치며

200밀리초. 눈을 한 번 깜빡이는 시간의 절반이다.

그 안에서 ANN 인덱스가 벡터 공간을 탐색하고, 양자화된 모델이 수백 개의 Feature를 처리하고, 서킷 브레이커가 장애에 대비하고, 데이터 계약이 파이프라인을 지키고 있다.

사용자는 이 모든 걸 모른 채 "추천이 꽤 괜찮네"라고 생각한다.

그게 잘 만든 시스템의 증거다.


참고 자료

  • 원문: ITWorld Korea — 200밀리초의 벽을 지켜라
  • 저자: Manoj Yerrasani, NBC유니버설 글로벌 플랫폼 엔지니어링 부사장
  • Amazon Latency Study (100ms = 1% revenue loss)
  • Malkov & Yashunin (2018), Efficient and robust approximate nearest neighbor search using HNSW
반응형

댓글