랭킹 시스템 조회 성능 개선
문제 상황 및 목표
클라이언트에게 인기 채팅방을 반환하는 기능이 있었으나, 초기 구현은 평균 약 150초(약 2분 30초) 가 소요되어 사용자 경험에 큰 지장을 주고 있었습니다.
이에 20만 건의 더미 데이터를 사용해 쿼리 튜닝과 Redis(특히 Sorted Set) 도입으로 성능을 개선하는 것을 목표로 하였습니다.
인기 채팅방을 선별하는 기준은 다음과 같습니다.
현재 채팅방에 참여하는 인원수
한 시간 동안 발생한 채팅 개수
위 두 요소에 각각 가중치를 부여하고 정규화(예: min-max 또는 z-score) 후 종합 점수로 순위 결정
1. 문제가 되는 단순 구현

인기 채팅방을 선별하는 과정을 크게 3가지로 나눠볼 수 있다.
현재 토론이 시작된 채팅방을 모두 조회
채팅방에 참여한 사용자를 조회하고 각 사용자의 채팅 수를 조회
채팅 참여자 수와 1시간 동안 발생한 채팅 개수에 대해 가중치를 적용하고 정규화 진행
postman으로 확인했을 때, 위 코드의 실행 시간은 2m 25s라는 결과를 보여준다.
이를 개선하기 위해서는 어떤 과정에서 실행 시간이 오래걸리는지 확인하기 위해 측정해봤다.
1번 과정 : 4236ms
2번 과정 : 140865ms
3번 과정 : 4ms
현재 프로젝트에서 채팅방 참여 인원수는 최대 10명이고, 20만 개의 채팅방 더미 데이터가 생성된 상태이다.
여기서 참여 인원수를 임의로 설정하고 발생한 채팅 개수는 0으로 세팅되어 있다.
시간 복잡도를 계산해보면 O(20만 * 10 * 1시간 동안 발생한 채팅 개수)가 된다.
1시간 간격의 스케줄링을 사용하더라도, 이는 과도한 시간과 리소스를 소비하는 비효율적인 방식이다. 또한, 실시간 조회 라는 요구사항으로 변경 시 서버 응답에 2m 30s나 소요되는 것은 사용자 경험을 크게 저하시킬 수 있다. 따라서 더 효율적이고 신속한 데이터 처리 및 응답 방식이 필요로 했다.
2. 쿼리 개선을 통한 성능 개선
성능이 좋지 않은 코드에서 2번 과정은 140865ms라는 시간이 걸렸다.
2번 과정은 채팅방에 참여한 사용자의 수와 1시간 동안 발생한 채팅 개수를 종합해서 객체로 생성하는 과정이다.
🤔 이 과정에서 왜 많인 시간이 걸렸을까?.. 분석한 내용은 다음과 같다.
이중 루프 구조
주어진 코드에서는 모든 채팅방을 순회하면서 채팅방에 속한 멤버를 가져오고, 채팅 수를 카운트하기 위해 다시 루프를 돌고 있다. 데이터의 양이 많을 경우 성능에 큰 영향을 주는 과정이 될 수 있다.
DB 호출 횟수
각 채팅방(Agora)에 대해 멤버를 조회하고, 각 멤버에 대해 채팅 수를 조회하는 방식은 N+1 문제를 발생시키고 있다. 따라서 DB에서 많은 쿼리를 실행하면서 성능을 저하시킨다.
DB 성능
DB에서 조회하는 과정에서 적절한 인덱스가 설정되어 있지 않다면 쿼리 성능 저하로 이어질 수 있다.
객체 생성 비용
반복문을 순회하면서 계속해서 객체를 생성하기 때문에 이 과정에서 메모리와 CPU 자원을 많이 사용한다.
분석한 내용을 봤을 때 시간이 오래 걸린 1, 2번 과정을 하나의 쿼리로 실행할 수 있다면 개선할 수 있다고 생각했다.
쿼리 튜닝

이중 루프를 파히가 위해서 join을 활용하여 필요한 데이터를 한 번의 쿼리로 가져오는 방식으로 변경했다.
또, join과 집계 쿼리를 사용하여 한 번의 쿼리로 필요한 모든 데이터를 가져오면서 N+1 문제를 해결한다.
show index from table_name으로 설정된 인덱스를 확인하고, 필요한 인덱스들을 추가한다.
쿼리 튜닝이 끝난 SLQ과 서비스의 코드이다. 3가지 과정에서 2가지 과정으로 줄어들었다.


쿼리 개선 후, Postman을 통한 API 호출 테스트 결과, 성능이 크게 향상되었으며 실행 계획 분석에서도 쿼리 응답 시간이 평균 20~30ms로 단축되었다. 이는 스케줄링과 실시간 작업에 적합한 수준의 성능을 달성했다고 생각한다.
그러나 이 결과는 임의로 생성한 더미 데이터를 기반으로 한 것임을 고려할 때, 추가적인 성능 개선 방안을 고민해 볼 필요가 있었다. 그래서 Redis의 자료구조를 활용한다면 성능을 한층 더 향상시킬 수 있을것으로 예상했다.
하지만 데이터가 증가할수록 count query에 주의해야 하며 집계 시 임시 테이블을 활용하는 부분도 고민해야 합니다.
3. Redis SortedSet을 활용한 성능 개선
MySQL을 사용할 때, 20만의 더미 데이터에서 join을 통해 연산에 필요한 값을 가져오는 작업에 많은 비용이 든다고 생각했다. 이렇게 비즈니스에서 오래 걸리는 연산을 Redis를 활용해서 개선하기 위해 도입을 시도했다.
Redis를 아래와 같이 활용할 수 있다.
사용자가 채팅방에 입/퇴장 시 해당 채팅방의 ID에 대한 key의 value(count)를 증/감한다.
한 시간동안 발생한 채팅의 개수를 카운트하기 위해 시간 단위로 key를 생성한다.
위 과정을 적용하기 위해서는 Redis의 Hyperloglog와 SortedSet 중 어느 자료구조를 사용할 지 고민했다.
사전에 적합성을 판단했지만, 결과적으로 두 가지 모두 구현하여 요구사항을 충족할 수 있는지 직접 확인했다.
Hyperloglog
hyperloglog 자료구조는 대규모 집합의 고유 요소(카디널리티)를 추정하는 데 효과적이고 적은 메모리 사용고 빠른 연산 속도(O(1))를 제공하는 자료구조로 잘 알려져있다. hyperloglog는 약 0.81%의 표준 오차가 존재하지만 현재 프로젝트에서 채팅방 인기 순위를 제공하는 것에는 큰 영향을 주지 않을것이라 판단했다.
하지만 구현하면서 다음과 같은 이유로 적용할 수 없었다.
감소 연산 불가능 사용자가 채팅방에 입장하거나 퇴장할 때마다 카운트를 감소시키는 연산을 지원하지 않는다. 이는 실시간으로 참여 인원수를 집계하는데 제약이 되었다.
개별 키 관리의 어려움 HyperLogLog는 기본적으로 하나의 고유 요소 집합에 대해 추정을 수행하며, 이를 시간 단위로 여러 키에서 관리하려면 상당히 복잡한 구현이 필요하다고 느꼈다. 매 시간마다 발생하는 채팅 수를 분리하여 관리하는 것이 어려워, 이 요구 사항을 충족시키기에는 적합하지 않았다.
정렬 기능 부재 자체적인 정렬 기능을 제공하지 않는다.
SortedSet
Redis에는 데이터 구조를 가르키는 key가 존재한다. 이것이 SortedSet이다.
해시처럼 보이는 컬렉션이 존재하지만, 이를 key-value로 부르지 않고 member-socre라고 부른다.
모든 member는 score를 가지며 score는 항상 숫자이다.
각 요소는 고유한 특성이 있고 score를 기반으로 정렬이 가능하다.
이러한 Sorted Set의 특성은 Hyperloglog로 구현할 때의 한계를 극복할 수 있었다.
Last updated