유성

실시간 검색어 순위 서비스 고도화: RDBMS에서 Redis 슬라이딩 윈도우까지 본문

Architecture

실시간 검색어 순위 서비스 고도화: RDBMS에서 Redis 슬라이딩 윈도우까지

백엔드 유성 2026. 1. 21. 14:57

최근 1시간 동안 가장 많이 검색된 키워드를 실시간으로 보여주는 기능은 단순해 보이지만, 트래픽이 몰릴수록 정교한 아키텍처 설계를 요구한다.

초기 모델부터 성능 최적화를 거쳐 O(1)의 복잡도를 달성하기까지의 과정을 정리해본다.

 

1. 초기 단계: RDBMS를 활용한 단순 그룹화

서비스 초기에는 가장 익숙한 RDBMS를 활용한다.

검색어가 입력될 때마다 이력 테이블에 저장하고, 조회 시점에 GROUP BY 연산을 수행하는 방식이다.

 

등록 로직

INSERT INTO real_time_search(id, terms, searched_at) VALUES (null, '검색어', now());

 

조회 로직

SELECT terms, COUNT(*) AS search_count
FROM real_time_search
WHERE searched_at > DATE_SUB(NOW(), INTERVAL 1 HOUR)
GROUP BY terms
ORDER BY search_count DESC
LIMIT 10;

 

이 방식은 구현이 매우 단순하다는 장점이 있지만, 데이터가 쌓일수록 성능 한계가 명확해진다.

 

2. 성장 단계: 캐싱(Caching)을 통한 부하 분산

트래픽이 증가하여 시간당 50만 건 이상의 검색이 발생하면, 매 요청마다 수십만 개의 로우를 그룹화하고 정렬하는 과정에서 2,000ms 이상의 지연 시간이 발생할 수 있다.

 

이를 해결하기 위해 DB 조회를 잠시 저장하는 Redis를 활용한 캐싱을 도입해볼 수 있다.

1시간 단위 통계 결과에 1~5분 정도의 TTL을 설정하여 저장해두면, DB 부하를 획기적으로 줄이고 응답 속도를 높일 수 있다.

 

3. 병목 단계: 연산 부하와 슬라이딩 윈도우의 필요성

사용자가 수백만 명으로 늘어나면 캐시가 만료되는 시점마다 DB에 발생하는 부하가 임계치를 넘게 된다.

이때 부터는 DB의 그룹화 연산 자체를 제거해야 한다.

 

이를 위해 슬라이딩 윈도우(Sliding Window) 개념을 도입할 수 있다.

모든 검색 이력을 큐(Queue)처럼 관리하며, 현재 시점부터 1시간 전까지의 범위(Window)만 유지하는 방식이다.

(아래는 과거 검색어, 위는 최신 검색어)

위 그림은 이력을 남기는 큐를 나타낸 것이다.

이 구조를 토대로 1시간 범위를 윈도우로 만들면 아래와 같이 만들어진다.

이렇게 1시간 단위 데이터를 윈도우로 잡고 해당 값들의 합계를 내어 정렬하면 된다.

 

하지만 단순히 윈도우 내 데이터를 매번 합산하고 정렬하는 방식은 여전히 연산 효율이 떨어진다.

이를 한번 더 캐싱해보자.

 

4. 고도화 단계: 윈도우 분리를 통한 효율화

연산량을 최소화하기 위해 이력과 합계를 분리해보자.

  1. 새로운 검색어 진입: 윈도우 합계에 즉시 +1을 수행한다.
  2. 시간이 지난 검색어 만료: 1시간이 경과한 윈도우 합계에서 -1을 수행하고 제거한다.

이처럼 검색어가 들어오고 나갈 때마다 합계를 실시간으로 갱신하면, 조회 시점에 별도의 합산 연산이 필요 없게 된다.

 

5. 구현 단계: Redis SortedSet 활용

이 아키텍처를 실무에서 구현하기 위해 가장 적합한 자료구조는 Redis의 SortedSet(ZSET)이다.

  • 윈도우 SortedSet (Score: 검색 횟수): 검색어 발생 시 ZINCRBY 명령어로 해담 검색어의 Score를 즉시 올린다.
    • 조회 시 ZREVRANGE를 사용하면 이미 정렬된 데이터를 가져오기만 하면 된다.
  • 이력 SortedSet (Scroe: 타임스탬프): 만료 처리를 위해 검색어와 시간을 저장한다.
    • 스케줄러가 1시간이 지난 데이터를 확인하여 윈도우 SortedSet의 Scroe를 낮춘다.

Java로 구현한다면 로컬 환경에서는 다음과 같은 자료구조 조합으로 유사하게 구현할 수 있다.

  • PriorityBlockigQueue: 만료 대상 추출 (이력 관리)
  • ConcurrentSkipListSet: 카운트 기준 정렬 관리 (윈도우 관리)
  • ConcurrentHashMap: 검색어별 Set 내부 객체 관리

 

6. 결과: O(1) 의 조회 복잡도 달성

최종적으로 윈도우를 분리하여 관리하게 되면, 실시간 검색어 순위 조회는 더 이상 계산이나 정렬 과정을 거치지 않는다.

이미 정렬되어 있는 윈도우 데이터를 필요한 만큼 끊어서 보여주기만 하면 되므로, 조회 복잡도는 사실상 O(1)에 수렴하게 된다.

 

이러한 고도화 과정은 단순한 기능 구현을 넘어, 대규모 트래픽 환경에서 시스템 안정성과 성능을 어떻게 확보할 것인가에 대한 핵심적인 접근법을 보여준다.

 

7. 마치며

"그렇다면 처음부터 가장 빠른 슬라이딩 윈도우를 도입하는게 정답 아닐까?" 라는 의문이 생길수도 있다.

하지만 저의 대답은 "아니오"이다. 모든 고도화된 아키텍처에는 그에 상응하는 '복잡성'이라는 비용이 따르기 때문이다.

 

데이터 정합성의 위험성

슬라이딩 윈도우 방식, 특히 Redis를 활용한 구조에서 가장 경계해야 할 것은 데이터 정합성 불일치이다.

예를 들어 다음과 같은 장애 시나리오를 가정해 보자.

  1. 검색 이력을 쌓고 윈도우 합계를 +1 했다.
  2. 1시간이 지나 이력을 제거하며 윈도우 합계를 -1 하려는 찰나, Redis 서버가 예기치 않게 다운되었다.
  3. 서버 복구 후 로직의 오작동이나 중복 처리로 인해 -1 연산이 두 번 수행된다면 어떤 일이 벌어질까?

결과적으로 해당 검색어의 검색량 기본값이 0이 아닌 -1로 유지되는 기현상이 발생한다.

아무도 검색하지 않았는데 오히려 검색 횟수가 마이너스가 되는 것이다.

이를 방지하기 위해 분산 락, 트랜잭션, 정합성 확인 스케줄러 등 보안 장치들을 추가할 수 있지만, 이는 곧 개발 및 운영 비용의 급격한 상승으로 이어진다.

 

만약 현재 서비스의 트래픽이 RDBMS의 단순 그룹화만으로도 충분히 소화 가능한 수준이라면, 첫 번째 모델인 RDBMS 방식이 모든 면에서 가장 훌륭한 정답일 수 있다.

RDBMS에서 위와 같은 문제가 발생했을 때 정합성 불일치 판단은 어려울 수 있으나, 시간이 지남에 따라 '최종적 일관성'을 지키게 될 수도 있다는 것이다.

 

개발은 비용대비 효율이니 가장 합리적인 방법을 찾아 적용하는 것이 바람직하다.