2026년 1월 25일
[가상 면접 사례로 배우는 대규모 시스템 설계 기초] 13. 검색어 자동완성 시스템
1. 개요
- 13장에서는 검색어 자동완성 시스템 설계 문제를 다룬다.

2. 문제 이해 및 설계 범위 확정
2.1. 요구사항
- 빠른 응답 속도: 100ms 이내에 응답해야 한다.
- 연관성: 자동완성되어 출력되는 검색어는 사용자가 입력한 단어와 연관된 것이어야 한다.
- 정렬: 시스템의 계산 결과는 인기도(popularity) 등의 순위 모델(ranking model)에 의해 정렬되어 있어야 한다.
- 규모 확장성: 시스템은 많은 트래픽을 감당할 수 있도록 확장 가능해야 한다.
- 고가용성: 시스템의 일부에 장애가 발생하거나, 느려지거나, 예상치 못한 네트워크 문제가 생겨도 시스템은 계속 사용 가능해야 한다.
2.2. 개략적 규모 추정
- 일간 능동 사용자(DAU)는 천만 명으로 가정한다.
- 평균적으로 한 사용자는 매일 10건의 검색을 수행한다고 가정한다.
- 질의할 때마다 평균적으로 20바이트의 데이터를 입력한다고 가정한다.
- 문자 인코딩 방법으로는 ASCII를 사용한다고 가정할 것이므로, 1문자 = 1바이트이다.
- 질의문은 평균적으로 4개 단어로 이루어진다고 가정할 것이며, 각 단어는 평균적으로 다섯 글자로 구성된다고 가정할 것이다.
- 따라서 질의당 평균 4 * 5 = 20 바이트이다.
- 검색창에 글자를 입력할 때마다 클라이언트는 검색어 자동완성 백엔드에 요청을 보낸다.
- 평균적으로 1회 검색당 20건의 요청이 백엔드로 전달된다.
- 예를 들어 우리가 검색창에 boost라고 입력하면 다음의 5개 요청이 순차적으로 백엔드에 전송된다.
search?q=b search?q=bo search?q=boo search?q=boos search?q=boost
- 대략 초당 24,000건의 질의(QPS)가 발생할 것이다.
- (=10,000,000 사용자 * 10질의 / 일 * 20자 / 24시간 / 3600초)
- 최대 QPS = QPS * 2 = 대략 48,000
- 질의 가운데 20% 정도는 신규 검색어라고 가정할 것이다.
- 따라서 대략 0.4GB 정도다. (=10,000,000사용자 * 10질의 / 일 * 20자 * 20%)
- 매일 0.4GB의 신규 데이터가 시스템에 추가된다는 뜻이다.
3. 개략적 설계안 제시 및 동의 구하기
- 개략적으로 보면 시스템은 두 부분으로 나뉜다.
- 데이터 수집 서비스: 사용자가 입력한 질의를 실시간으로 수집하는 시스템이다.
- 질의 서비스: 주어진 질의에 다섯 개의 인기 검색어를 정렬해 내놓는 서비스이다.
3.1. 데이터 수집 서비스
- 질의문과 사용빈도를 저장하는 빈도 테이블(frequency table)이 있다고 가정해보자.
- 처음 테이블은 비어 있는데 사용자가 검색을 하면 다음과 같이 바뀌어 나가게 된다.

3.2. 질의 서비스
- 다음과 같은 빈도 테이블이 있는 상태라고 하자. 두 개의 필드가 있음을 볼 수 있을 것이다.
- query: 질의문을 저장하는 필드다.
- frequency: 질의문이 사용된 빈도를 저장하는 필드다.

- 이 상태에서 사용자가 “tw”를 검색창에 입력하면 아래의 “top 5” 자동완성 검색어가 표시되어야 한다.
twitter twitch twilight twin peak twitch prime
- 가장 많이 사용된 5개 검색어는 아래의 SQL 질의문을 사용해 계산할 수 있다.
SELECT * FROM frequency_table WHERE query LIKE 'prefix%' ORDER BY frequency DESC LIMIT 5
- 이 방법은 데이터의 양이 적을 때는 나쁘지 않은 설계안이다.
- 하지만 데이터가 아주 많아지면 데이터베이스가 병목이 될 수 있다.
4. 상세 설계
- 이번 절에서는 컴포넌트를 몇 개 골라 보다 상세히 설계하고 다음 순서로 최적화 방안을 논의할 것이다.
- 트라이(trie) 자료구조
- 데이터 수집 서비스
- 질의 서비스
- 규모 확장이 가능한 저장소
- 트라이 연산
4.1. 트라이 자료구조
- 개략적 설계안에서는 관계형 데이터베이스를 저장소로 사용했었다.
- 하지만 관계형 테이터베이스를 이용해 가장 인기 있었던 다섯 개 질의문을 골라내는 방안은 효율적이지 않다.
- 이 문제는 트라이(trie, 접두어 트리)를 사용해 해결할 수 있다.

- 트라이 자료구조의 핵심은 다음과 같다.
- 트라이는 트리 형태의 자료구조다.
- 이 트리의 루트 노트는 빈 문자열을 나타낸다.
- 각 노드는 글자(character) 하나를 저장하며, 26개(해당 글자 다음에 등장할 수 있는 모든 글자의 개수)의 자식 노드를 가질 수 있다.
- 각 트리 노드는 하나의 단어, 또는 접두어 문자열(prefix string)을 나타낸다.
- 이용 빈도에 따라 정렬된 결과를 내놓기 위해서는 노드에 빈도 정보까지 저장할 필요가 있다.

- 그렇다면 이 트라이로 검색어 자동완성은 어떻게 구현할 수 있을까?
- 알고리즘을 살펴보기 전에, 우선 용어만 몇 가지 정의하고 넘어가자.
- : 접두어(prefix)의 길이
- : 트라이 안에 있는 노드 개수
- : 주어진 노드의 자식 노드 개수
- 가장 많이 사용된 질의어 개는 다음과 같이 찾을 수 있다.
- 해당 접두어를 표현하는 노드를 찾는다. 시간 복잡도는 이다.
- 해당 노드부터 시작하는 하위 트리를 탐색하여 모든 유효 노드를 찾는다.
- 유효한 검색 문자열을 구성하는 노드가 유효 노드다. 시간 복잡도는 이다.
- 유효 노드들을 정렬하여 가장 인기 있는 검색어 개를 찾는다. 시간 복잡도는 이다.

- 예제를 살펴보자. 이고 사용자가 검색창에 ‘be’을 입력했다고 하자.
- 위의 알고리즘은 다음과 같이 동작할 것이다.
- 접두어 노드 ‘be’을 찾는다.
- 해당 노드부터 시작하는 하위 트리를 탐색하여 모든 유효 노드를 찾는다. 위 예제의 경우 [beer: 10], [best: 35], [bet: 29]가 유효 노드다.
- 유효 노드를 정렬하여 2개만 골라낸다. [best: 35]와 [best: 29]가 접두어 “tr”에 대해 검색된 2개의 인기 검색어다.
- 이 알고리즘의 시간 복잡도는 이다.
- 이 알고리즘은 직관적이지만 최악의 경우에는 k개 결과를 얻으려고 전체 트라이를 다 검색해야 하는 일이 생길 수 있다.
- 이 문제를 해결할 방법으로는 다음의 두 가지가 있다.
- 접두어의 최대 길이를 제한
- 각 노드에 인기 검색어를 캐시
- 접두어의 최대 길이를 제한
- 사용자가 검색창에 긴 검색어를 입력하는 일은 거의 없다.
- 따라서 값의 범위를 작게 둬도 된다. (예: )
- 이렇게 검색어의 최대 길이를 제한할 수 있다면 “접두어 노드를 찾는” 단계의 시간 복잡도는 에서 로 바뀔 것이다.
- 노드에 인기 검색어 캐시
- 각 노드에 개의 인기 검색어를 저장해 두면 전체 트라이를 검색하는 일을 방지할 수 있다.
- 5~10개 정도의 자동완성 제안을 표시하면 충분하므로, 는 작은 값이다.
- 하지만 각 노드에 질의어를 저장할 공간이 많이 필요하게 된다는 단점도 있다.
- 그러나 빠른 응답속도가 아주 중요할 때는 이 정도 저장공간을 희생할 만한 가치는 있다.

- 앞의 두 가지 최적화 기법을 적용하면 시간 복잡도는 다음과 같이 변한다.
- 접두어 노드를 찾는 시간 복잡도는 로 바뀐다.
- 최고 인기 검색어 5개를 찾는 질의의 시간 복잡도도 로 바뀐다. (검색 결과가 이미 캐시됨)
4.2. 데이터 수집 서비스
- 검색창에 타이핑을 할 때마다 실시간으로 데이터를 수집하는 방법은 효율적이지 않다.
- 매일 수천만 건의 질의가 입력될텐데 그때마다 트라이를 갱신하면 질의 서비스는 심각하게 느려질 것이다.
- 일단 트라이가 만들어지고 나면 인기 검색어는 그다지 자주 바뀌지 않을 것이다.
- 아래는 데이터 분석 서비스의 수정된 설계안이다. 각 컴포넌트가 어떤 역할을 하는지 차례로 살펴보자.

- 데이터 분석 서비스 로그
- 데이터 분석 서비스 로그에는 검색창에 입력된 질의에 관한 원본 데이터가 보관된다.
- 새로운 데이터가 추가될 뿐 수정은 이루어지지 않으며 로그 데이터에는 인덱스를 걸지 않는다.

- 로그 취합 서버
- 데이터 분석 서비스로부터 나오는 로그는 보통 그 양이 엄청나고 데이터 형식도 제각각인 경우가 많다.
- 로그 취합 서버는 이 데이터를 잘 취합하여 우리 시스템이 쉽게 소비할 수 있도록 한다.
- 취합 주기는 서비스의 성격에 맞게 설정한다. (본 책에서는 일주일 주기로 취합한다고 가정)
- 취합된 데이터
- 다음 표는 매주 취합된 데이터의 예시이다.
time필드는 해당 주가 시작한 날짜를 나타내고,frequency필드는 해당 질의가 해당 주에 사용된 횟수의 합이다.

time 필드를 둬서 주(week)를 구분한 이유는 무엇일까?
- 작업 서버
- 작업 서버(worker)는 주기적으로 비동기적 작업(job)을 실행하는 서버 집합이다.
- 트라이 자료구조를 만들고 트라이 데이터베이스에 저장하는 역할을 담당한다.
- 트라이 캐시
- 트라이 캐시는 분산 캐시 시스템으로 트라이 데이터를 메모리에 유지하여 읽기 연산 성능을 높인다.
- 매주 트라이 데이터베이스의 스냅샷을 떠서 갱신한다.
- 트라이 데이터베이스
- 트라이 데이터베이스는 지속성 저장소다.
- 트라이 데이터베이스로 사용할 수 있는 선택지로는 크게 두 가지가 있다.
- 문서 저장소(document store): 새 트라이를 매주 만들 것이므로, 주기적으로 트라이를 직렬화하여 데이터베이스에 저장할 수 있다. 이때 몽고디비 같은 문서 저장소를 활용하면 이런 데이터를 편리하게 저장할 수 있다.
- 키-값 저장소: 트라이는 아래 로직을 적용하면 해시 테이블 형태로 변환 가능하다.
- 트라이에 보관된 모든 접두어를 해시 테이블 키로 변환
- 각 트라이 노드에 보관된 모든 데이터를 해시 테이블 값으로 변환

4.3. 질의 서비스
- 아래는 데이터베이스를 활용한 질의 서비스에서 설계의 비효율성을 개선한 새 설계안이다.

- 검색 질의가 로드밸런서로 전송된다.
- 로드밸런서는 해당 질의를 API 서버로 보낸다.
- API 서버는 트라이 캐시에서 데이터를 가져와 해당 요청에 대한 자동완성 검색어 제안 응답을 구성한다.
- 데이터가 트라이 캐시에 없는 경우에는 데이터를 데이터베이스에서 가져와 캐시에 채운다.
- 최적화 방안
- AJAX 요청: 요청을 보내고 받기 위해 페이지를 새로고침 할 필요가 없다.
- 브라우저 캐싱: 대부분의 경우 자동완성 검색어 제안 결과는 짧은 시간 안에 자주 바뀌지 않는다.
- 데이터 샘플링: 모든 질의 결과를 로깅하는 대신 N개 요청 가운데 1개만 로깅하도록 한다.
4.4. 트라이 연산
- 트라이는 검색어 자동완성 시스템의 핵심 컴포넌트다.
- 트라이 생성
- 트라이 생성은 작업 서버가 담당하며, 데이터 분석 서비스의 로그나 데이터베이스로부터 취합된 데이터를 이용한다.
- 트라이 갱신
- 트라이를 갱신하는 데는 두 가지 방법이 있다.
- 매주 한 번 갱신하는 방법: 새로운 트라이를 만든 다음에 기존 트라이를 대체한다. (추천)
- 트라이의 각 노드를 개별적으로 갱신하는 방법: 노드를 갱신할 때 모든 상위 노드도 갱신해야 하는데, 노드에 캐시도 보관되기 때문에 성능이 좋지 않다. (비추천)
- 검색어 삭제
- 서비스 정책을 위반하는 질의어는 자동완성 결과에서 제거해야 한다.
- 트라이 캐시 앞에 필터 계층을 둑 부적절한 질의어가 반환되지 않도록 한다.
- 데이터베이스에서 해당 검색어를 물리적으로 삭제하는 것은 다음번 업데이트 사이클에 비동기적으로 진행하면 된다.

- 저장소 규모 확장
- 트라이의 크기가 한 서버에 넣기엔 너무 큰 경우에는 어떻게 대응할까?
- 영어만 지원하면 되기 때문에, 간단하게는 첫 글자를 기준으로 샤딩(sharding)을 고려해볼 수 있다.
- 검색어를 보관하기 위해 두 대 서버가 필요하다면 ‘a’부터 ‘m’까지 글자로 시작하는 검색어는 첫 번째 서버에 저장하고, 나머지는 두 번째 서버에 저장한다.
- 세 대 서버가 필요하다면 위와 같이 적절히 삼등분해서 세 개의 서버에 저장한다.
- 이 방법을 쓰는 경우 사용 가능한 서버는 최대 26대로 제한된다. 영어 알파벳에는 26자 밖에 없기 때문이다.
- 이 이상으로 서버 대수를 늘리려면 샤딩을 계층적으로 해야 한다.
- 가령 검색어의 첫 번째 글자는 첫 번째 레벨의 샤딩에 쓰고, 두 번째 글자는 두 번째 레벨의 샤딩에 쓰는 것이다.
- 예를 들어, ‘a’로 시작하는 검색어를 네 대 서버에 나눠 보관하고 싶다고 해보자. 그러면 ‘aa’부터 ‘ag’까지는 첫 번째 서버에, ‘ah’부터 ‘an’까지는 두 번째 서버에, ‘ao’부터 ‘au’까지는 세 번째 서버에, 나머지는 네 번째 서버에 보관하면 될 것이다.
- 다만, ‘c’로 시작하는 단어가 ‘x’로 시작하는 단어보다 많다는 것을 감안하면 좋은 방법은 아니다.
- 그래서 과거 질의 데이터의 패턴을 분석하여 샤딩하는 방법을 추천한다.
- 예를 들어, ‘s’로 시작하는 검색어의 양이 ‘u’ ~ ‘z’로 시작한느 검색어를 전부 합친 것과 비슷하다면, ‘s’에 대한 샤드 하나와 ‘u’부터 ‘z’까지의 검색어를 위한 샤드 하나를 두어도 충분할 것이다.

5. 마무리
- 만약 다국어 지원이 가능하도록 시스템을 확장해야 한다면 어떻게 해야 할까?
- 비영어권 국가에서 사용하는 언어를 지원하려면 트라이에 유니코드(unicode) 데이터를 저장해야 한다.
- 국가별로 인기 검색어 순위가 다르다면 어떻게 해야 할까?
- 국가별로 다른 트라이를 사용한다.
- 트라이를 CDN에 저장하여 응답속도를 높이는 방법도 고민해볼 수 있다.
- 실시간으로 변하는 검색어의 추이를 반영하려면 어떻게 해야 할까?
- 실시간 검색어 자동완성 시스템을 구축하는 것은 복잡한 문제로 이 책에서 다룰 수 있는 범위를 넘어선다.
