13장. 검색어 자동 완성 시스템
1단계: 문제 이해 및 설계 범위 확장
면접관과 대화를 통해 도출한 요구사항:
- 검색어 첫부분만 자동완성 지원
- 5개의 추천어를 보여줘야 함
- 검색 빈도가 높은 인기 검색어 순으로 추천
- 맞춤법 검사와 자동 수정 기능은 불필요
- 질의는 영어로
- 모든 질의는 영어 소문자로 이루어짐
- DAU는 천만명
추가 사항:
- 빠른 응답 속도: 100ms 이하의 빠른 응답 속도
- 연관성: 사용자 입력과 연관된 검색어가 자동완성되어야 함
- 정렬: 인기도 등의 순위 모델로 정렬되어야 함
- 규모 확장성: 많은 트래픽 감당 가능하도록 확장 가능해야 함
- 고가용성: 시스템 일부에 장애가 발생해도 시스템이 계속 사용 가능해야 함
개략적 규모 추정:
- DAU는 천만명으로 가정
- 한 사용자는 매일 10건의 검색을 수행한다고 가정
- 질의 마다 평균 20바이트의 데이터를 입력한다고 가정
- ASCII 방식으로는 1문자 = 1 바이트
- 질의문은 평균 4개 단어로 이뤄진다고 가정
- 각 단어는 평균 5글자로 이뤄진다고 가정
- 즉, 질의당 평균 4*5 = 20 바이트
- 검색창에 글자 입력할 때마다 클라이언트가 검색어 자동완성 백엔드에 요청을 보냄
즉, 평균 1회 검색당 20건의 요청이 벡엔드에 전달됨 - 대략 초당 24,000건의 질의(QPS)가 발생 (= (천만 사용자 * 10질의일 * 20자) / 24시간 / 36000초)
- 질의 중 20%는 신규 검색어
즉, 0.4GB의 신규 데이터가 시스템에 추가됨
2단계: 개략적 설계안 제시 및 동의 구하기
데이터 수집 서비스:
- 사용자의 질의를 수집하고 빈도수를 누적
- 예시: 질의문과 사용빈도를 저장하는 빈도 테이블을 가정, 사용자 검색에 따라 상태가 아래와 같이 변화함

질의 서비스:
- 주어진 질의에 다섯 개의 인기 검색어를 정렬해서 보여줌
- 두개의 필드를 가정:
- query: 질의문을 저장
- frequency: 질의문이 사용된 빈도를 저장

- 사용자가 "tw"를 입력한 경우 자동 완성 예시

- SQL 쿼리로 가장 많이 사용된 5개 검색어 계산 가능

3단계: 상세 설계
2단계의 방법은 데이터가 아주 많아지는 경우 DB에서 병목이 발생할 수 있음
컴포넌트를 상세히 설계하고 최적화 방법 논의 필요
트라이 자료구조:
- 관계형DB는 인기 질의 5개를 골라내는 데 비효율적이므로 트라이 자료구조 필요
- 트라이:
- 문자열 검색에 특화된 트리 형태의 자료구조
- 루트 노드는 빈 문자열을 나타냄
- 각 노드는 문자 하나를 저장하고, 최대 26개의 자식 노드를 가짐
- 각 노드는 하나의 단어 또는 접두어 문자열을 나타냄

- 각 노드에 빈도 정보를 저장하는 경우


- 용어 정리:
- p: 접두어(prefix)의 길이
- n: 트라이 안에 있는 노드 개수
- c: 주어진 노드의 자식 노드 개수
- 트라이로 가장 많이 사용된 질의어 k개 찾는 방법:
- 해당 접두어를 표현하는 노드를 찾음 (시간 복잡도 O(p))
- 해당 노드부터 시작하는 하위 트리를 탐색하여 모든 유효 노드를 찾음 (시간 복잡도 O(c))
- 유효 노드들을 정렬하여 가장 인기 있는 검색어 k개를 찾음 (O(clogc))
- 예제: k=2 이고 사용자가 'be'를 입력한 경우
- 접두어 노드 'be' 탐색
- 해당 노드부터 하위 트리 탐색하여 모든 유효 노트 찾음
[beer: 10], [best: 35], [bet: 29]가 유효 노드 - 유효 노드를 정렬하여 2개만 골라냄
[best: 35], [bet: 29]가 인기 검색어

- 알고리즘의 시간 복잡도:
- O(p) + O(c) + O(clogc) = O(p + clogc)
- 최악의 경우에는 전체 트리를 다 탐색할 수 있으므로, 아래와 같이 해결
- 접두어의 최대 길이를 제한
- 각 노드에 인기 검색어를 캐시
- 접두어 최대 길이 제한
- 현실적으로 사용자가 검색창에 긴 검색어를 입력하는 일은 거의 없음
- p 값을 작은 정수 값(e.g. 50)으로 가정할 수 있음
- 접두어 노드를 찾는 시간 복잡도 O(p) = O(작은 상수) = O(1)
- 노드에 인기 검색어 캐시
- 각 노드에 인기 검색어 k개를 캐시해두면 전체 트라이 검색을 방지할 수 있음
- 5~10개 정도의 자동완성을 표시하면 충분하기 때문에 k는 작은 상수
- 공간이 많이 필요하지만, 빠른 성능이 중요하다면 가치 있음
- 예시: 각 노드에 인기 있는 검색어 5가지를 캐싱하게 하는 경우
- 시간 복잡도:
- 접두어 노드 찾는 시간 O(1)
- 인기 검색어 5개 찾는 시간 O(1)
- 전체 시간 복잡도 O(1)
- 시간 복잡도:

데이터 수집 서비스:
- 사용자가 타이핑을 할 때마다 실시간으로 데이터를 수정하는 방식은 실용적이지 않음
- 매일 입력되는 수천만 건의 질의 마다 트라이를 갱신하면 서비스가 심각하게 느려질 것
- 트라이가 일단 만들어진 후에는 인기 검색어가 자주 바뀌지 않을 것이므로 자주 갱신할 필요 없음
- 수정된 설계안:

- 데이터 분석 서비스 로그
- 검색창에 입력된 질의의 원본 데이터가 보관됨
- 추가는 발생하지만 수정은 발생하지 않음
- 인덱싱 되지 않음

- 로그 취합 서버
- 분석 로그는 크기가 매우 크고, 시스템이 바로 처리하기에는 데이터 형식이 제각각이므로 잘 취합해야 함
- 용례에 따라 집계 주기 조정:
- 실시간 검색이 중요한 트위터: 짧은 간격으로 집계
- 일반적인 검색 시스템: 주 1회 집계도 충분
- 취합된 데이터
- 예시:
- time: 해당 주가 시작한 날짜
- frequency: 해당 질의가 해당 주에 사용된 횟수의 합
- 예시:

- 작업 서버 :
- 정기적으로 비동기 작업을 수행하는 서버 집합
- 집계 데이터를 사용하여 트라이 구조를 생성하고, 이를 트라이 DB에 저장
- 트라이 캐시:
- 분산 캐시 시스템
- 트라이 데이터를 메모리 상에 유지해, 빠른 검색 응답을 제공
- 일반적으로 DB의 주간 스냅샷을 기반으로 데이터를 갱신
- 트라이 데이터베이스:
- 두 가지 저장 방식
- 문서 저장소
- 주기적으로 새 트라이를 생성하여 직렬화(저장 가능한 방식으로 변환) 후 저장
- e.g. MongoDB문서 저장소
- 키-값 저장소
- 트라이의 모든 노드를 해시 테이블 키로 변환
- 각 트라이 노드에 보관된 모든 데이터를 해시 테이블 값으로 변환
- 문서 저장소
- 두 가지 저장 방식

질의 서비스:
- 개략적 설계안에서 살펴본 DB를 사용한 질의 서비스를 개선한 설계안:
- 검색 질의가 로드밸런서로 전송됨
- 로드밸런서는 해당 질의를 API 서버로 전송
- API 서버는 트라이 캐시에서 데이터를 가져와 자동완성 검색에 제안을 구성
- 데이터가 트라이 캐시에 없는 경우에는 데이터를 데이터베이스에서 가져와 캐시에 채움
(다음에 같은 접두어 질의에 캐시 미스 발생하지 않고 대응하기 위함)

- 더 빠른 질의 서비스를 위한 최적화 방안:
- AJAX 요청:
- 웹 애플리케이션에서는, 보통 브라우저가 AJAX 요청을 통해 자동완성 결과를 가져오는 방식 사용
- 장점: 요청/응답을 주고받더라도 웹 페이지 전체를 새로 고치지 않음
- 브라우저 캐싱:
- 대체로 자동완성 추천어는 짧은 시간 동안 자주 변하지 않으므로, 브라우저에 캐싱할 수 있음
- 데이터 샘플링:
- 모든 검색 요청을 로그로 남기는 대신, 1/N만 로깅
- AJAX 요청:
트라이 연산:
- 트라이 생성:
- 작업 서버가 담당
- 데이터는 분석 로그나 DB 취합된 데이터를 사용
- 트라이 갱신:
- 두 가지 방법:
- 매주 한 번 갱신:
- 새로운 트라이를 생성하고 기존 트라이를 대체
- 각 노드를 개별적으로 갱신:
- 트라이가 작을 때는 고려해볼만 하지만, 성능이 나쁨(느림)
- 특정 노드를 갱신할 때는 그 노드의 모든 조상 노드까지도 함께 갱신해야 함
- 매주 한 번 갱신:
- 두 가지 방법:

- 검색어 삭제
- 혐오성이 짙거나, 폭력적이거나, 성적이거나, 위험한 질의어 등은 제거해야 함
- 트라이 앞에 필터 계층을 두어, 부적절한 추천이 사용자에게 표시되지 않도록 함
- 다음 트라이 업데이트 시 비동기적으로 DB에서도 물리적으로 삭제

저장소 규모 확장
- 트라이가 너무 커져서 한 서버에 저장 불가하다면, 샤딩을 통해 확장 가능
- e.g. 첫 글자 기준 샤딩:
- 두 대일 경우:
- 서버 1에는 'a'~'m'로 시작하는 쿼리 저장
- 서버 2에는 'n'~'z'로 시작하는 쿼리 저장
- 세 대일 경우:
- 서버 1에 'a~'i'로 시작하는 쿼리 저장
- 서버 2에 'j'~'r'로 시작하는 쿼리 저장
- 서버 3에 's'~'z'로 시작하는 쿼리 저장
- 이 방식은 최대 26개의 서버까지 분할 가능
--> 샤딩을 첫n개 글자에 따라 계층적으로 하면 더 많은 서버 분할 가능
- 두 대일 경우:
- 데이터를 균등하게 분배하는 샤딩:
- 과거 질의 데이터 패턴을 분석하여 샤딩
- 검색어 대응 샤딩 관리자가 어느 검색어가 어느 저장소 서버에 저장되는지 관리
- e.g. 's'로 시작하는 검색어 양과 'u'~'z'로 시작하는 검색어 양이 같다면,
- 서버 1에 's'로 시작하는 쿼리 저장
- 서버 2에 'u'~'z'로 시작하는 쿼리 저장

4단계: 마무리
추가 논의 사항:
- 다국어 지원 확장: 트라이에 유니코드 데이터를 저장
- 국가별 인기 순위가 다른 경우: 국가별로 다른 트라이를 사용
- 실시간 검색 추이 반영:
- 현 설계안에는 부적합한 이유:
- 작업 서버가 매주 한 번만 갱신
- 실시간으로 트라이를 구성하는 데는 너무 많은 시간이 소요됨
- 개선방향:
- 샤딩을 통해 작업 대상 데이터 양을 줄임
- 순위 모델을 변경하여 최근 검색어에 높은 가중치를 주게 함
- 데이터가 스트리밍되는 경우를 고려한 특별한 시스템 필요
(아파치 하둡 맵리듀스, 아파치 스파크 스트리밍, 아파치 스톰, 아파치 카프카 등)
- 현 설계안에는 부적합한 이유:
'대규모 시스템 설계 기초' 카테고리의 다른 글
| 대규모 시스템 설계 기초 - 15장 (1) | 2025.04.26 |
|---|---|
| 대규모 시스템 설계 기초 - 14장 (0) | 2025.04.26 |
| 대규모 시스템 설계 기초 - 12장 (0) | 2025.04.20 |
| 대규모 시스템 설계 기초 - 11장 (1) | 2025.04.12 |
| 대규모 시스템 설계 기초 - 10장 (0) | 2025.04.12 |