네부캠 그룹프로젝트 - 3. MongoDB 인덱스 설정 및 테스트

date
Dec 11, 2022
thumbnail
slug
naver-camp-project3
author
status
Published
tags
Project
summary
MongoDB 인덱스를 설정하며 했던 고민
type
Post
updatedAt
Jan 23, 2023 03:32 PM
boostcampwm-2022/web21-devrank
일단 MongoDB를 선택했던 이유는 아래와 같다.
Github 유저 정보를 보면 company, location, email 정보 등 사용자마다 갖는 정보의 구조가 다르다. 따라서 스키마리스한 구조가 적합하다. 또한 각 사용자의 프로필 데이터는 상호 독립적이다. 즉 어떠한 관계가 존재하지 않아서 정규화가 필요하지 않다. replica set이나 클러스터링 등 수평적인 확장으로 인해 고가용성을 보장하기가 RDBMS군보다 용이하다.
MongoDB 사용은 처음이라 공부를 해보니 전문 검색을 위한 인덱스 등 여러 인덱스 타입을 지원 하고, 심지어 RDBMS의 Clustered 인덱스같은 Clustered Collection을 지원하는 것을 보고 놀랐다.
인덱스 구조가 여타 RDBMS와 동일하게 B트리를 기반으로 되어있어 어떤 경우에 결합 인덱스를 못 타는 지, 어떤 경우 인덱스를 설정하는 게 효울적인지 등을 금방 배우고 적용할 수 있었다.

1️⃣ 현재 MongoDB에 저장하는 정보들

현재 MongoDB의 User Collection에 저장되는 정보는 아래와 같다.
  • Github에서 가져온 사용자 정보(id, username, following과 follower 수, 프로필사진 url 등)
  • 계산된 score 정보 (number 타입)
  • 일일 조회수 (number 타입)
  • 이전과 비교했을 때 score가 증가한 정도 (number 타입)

조회 시 필터로 사용되는 정보들

조회 시 필터로 사용되는 정보들은 아래와 같다.
  • username
  • score
  • 일일 조회수
  • score가 증가한 정도

2️⃣ Clustered Collection에 대한 고민

우리는 기본적으로 랭킹 서비스이다. 따라서 데이터는 score를 기반으로 sorting이 필요하다.여기서 MongoDB 5.3버전에 추가된 Clustered Collection이 고민이 되었다.
  • 대부분의 조회의 결과가 score를 기반으로 sort가 되어야 하므로 Clustered Collection을 사용해서 실제 물리데이터를 정렬된 상태로 유지하면 정렬 연산이 필요 없어 조회 시 얻는 이점이 크다.
  • 하지만 우리 서비스는 업데이트 또한 빈번하게 이루어진다. 하루에 1번은 전체 유저의 점수를 AWS Lambda같은 Cloud Function을 이용해 전체 유저의 score를 갱신할 예정이고, 버튼을 통해 120초의 delay를 갖는 단일 사용자의 업데이트 기능도 지원한다. 이 경우 Clustered Collection을 사용하면 갱신 시 부하가 상당할 것으로 예상된다.
일단 많은 데이터를 넣어서 테스트해봐야 알겠지만, 현재는 적용하지 않기로 했다. 이유는 위에서 말한 잦은 갱신 시 성능 문제가 있고, 또한 Mongoose에서 Clustered Collection에 대한 인터페이스를 아직 제공하지 않는다. 기존 코드들이 Mongoose로 이루어져 있는데, 이 계층을 걷어내고 테스트해보기에는 그룹 프로젝트 기간이 너무 짧다.

3️⃣ MongoDB의 단일 인덱스와 결합 인덱스 설정

현재 서비스에 사용되는 여러 API들이 존재한다.
notion image
  1. 일일 조회수를 기반으로, limit 쿼리 매개변수 개수의 최상위 유저들을 응답해주는 API
  1. score의 증가 정도를 기반으로 limit 쿼리 매개변수 개수의 최상위 유저들을 응답해주는 API
  1. username을 기반으로 limit 쿼리 매개변수 개수의 최상위 유저들을 응답해주는 API
  1. username, tier, limit를 쿼리로 받아 score순으로 정렬된 데이터 중에서 limit 수 만큼의 유저들을 응답해주는 API
notion image

단일 인덱스 설정

1, 2번 API의 경우는 간단하다. 단일 인덱스를 그저 설정해주면 되기 때문이다. mongoose를 이용한 설정은 아래와 같았다.
UserScheme.index({ score: -1 });
UserScheme.index({ scoreDifference: -1 });
UserScheme.index({ dailyViews: -1 });
문제는 3, 4번 API의 경우 username 인덱스를 어떻게 가져갈 지였다.

username필드에 일반적인 인덱스 vs Full-text 인덱스

처음에는 username에 text-index를 사용하여 전문 검색을 지원하려 했었다.
  • 이는 username으로만 필터링을 하는 API에서는 큰 문제가 없었다.
  • 하지만 위의 4번 API에서 text-index를 사용하면 username과 다른 필드들(tier, score)이 함께 필터링에 사용되는 경우, 결합 인덱스를 탈 방법이 없었다.
    • 트리 구조 인덱스 상, 문자열이라는 것은 앞에서 부터 정렬이 되어야하는데, 전문 검색은 역인덱싱이 반드시 필요하다. 하지만 역인덱싱은 B트리와는 다른 구조를 띈다.
결론적으로 서비스에 username 전문 검색이 반드시 필요한 경우가 아니므로 username필드에 일반적인 인덱스를 설정하여 prefix가 일치하는 문자열에 대해 인덱스를 타도록 설정하였다.

결합 인덱스 설정

최종적으로 생성한 결합 인덱스는 아래와 같다.
UserScheme.index({ username: 1, score: 1 });
UserScheme.index({ tier: 1, username: 1, score: 1 });
  • username만을 필터링에 사용하는 경우, { username: 1, score: 1 } 순으로 설정된 결합 인덱스를 타는 것이 가능하다.
  • 결합 인덱스의 특성상 앞에 필드가 일치하는 순으로 부분 필터링이어도 사용이 가능하기 때문이다.
  • 따라서 username에 단일로 인덱스를 설정함으로써 발생하는 불필요한 인덱스를 유지 비용을 줄일 수 있다.
  • 4번 API의 경우
    • username만 주어지는 경우, { username: 1, score: 1 } 결합 인덱스를 타게 된다.
    • tier만 주어지는 경우, { tier: 1, username: 1, score: 1 } 결합 인덱스를 타게 된다.
    • tier와 username이 모두 주어지는 경우, { tier: 1, username: 1, score: 1 } 결합 인덱스를 타게 된다.

4️⃣ 인덱스 테스트

MongoDB explain을 이용하여 각 API가 인덱스를 정상적으로 타는 지 검사해보았다.
  • score 순 정렬 API
notion image
  • score 상승폭 API
notion image
  • 일일 조회수 API
notion image
  • username API
notion image
  • tier (score순 정렬) API
notion image
  • username과 tier (score순 정렬) API
notion image
현재 테스트용 유저가 많지 않음에도, 따로 hint를 주지 않아도 인덱스를 제대로 탄다.