넷플릭스 앱을 열었다. 화면이 뜨는 순간 "오늘 이거 어때요?"라는 추천이 이미 펼쳐져 있다. 그 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 | 양자화 기반, 균형적 | ~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
'DevOps' 카테고리의 다른 글
| 왜 여러 회사에서 AKS는 불안정하고 EKS는 안정적으로 느껴졌을까 (0) | 2026.02.05 |
|---|---|
| kubernetes에서 Pod requests / limits Tunning (1) | 2025.12.30 |
| 오픈소스 검색엔진 비교: OpenSearch vs Meilisearch vs Typesense (0) | 2025.11.06 |
| Argo CD에서 GitHub Apps로 보안 강화하기 (0) | 2025.10.29 |
| Githup Apps 이란? 활용방법과 token과 비교 (0) | 2025.10.29 |
댓글