Skip to main content
← 블로그

보이는 문제는 미리 갈아엎는다

VauDium ·

북마크 기능이 잘 돌아가긴 했다. 그런데 북마크가 수천 개 쌓인 사용자에게서 망가질 것이 보였다. 그래서 그 자리에서 갈아엎었다.

보이는 문제는 미리 갈아엎는다

오늘 북마크 기능을 만들었다. 좋아요와 비슷한 패턴이라, 좋아요 팩토리를 보고 그대로 따라 만들었다. 그리고 북마크된 글만 모아 보는 화면이 있어야 했다.

처음 짠 방식은 단순했다.

  1. 사용자의 북마크 ID를 전부 가져온다
  2. overview_collection.find({ _id: {$in: [...]} }) 로 페이지네이션

데모용 데이터 몇 개에서는 잘 돌아갔다. 코드는 짧고 명확했다. 다음 작업으로 넘어가도 됐다.

근데 한 발자국만 더 가보면 보였다. 북마크가 수만 개 쌓이는 사용자가 생기면:

  • 매 요청마다 ID 배열 통째로 메모리 적재
  • $in은 ID 수가 커질수록 인덱스 효율이 떨어진다
  • 페이지 한 번 보려고 만 개 ID를 끌어오는 꼴

물어봤다. 지금 갈아엎을까, 나중에 갈아엎을까.

미리 갈아 엎어 놓자 문제가 있는데 넘어가면 나중에 발견하기 힘들어

맞는 말이다. 작동하는 코드를 다시 손대는 건 발견된 버그를 고치는 것보다 심리적으로 훨씬 어렵다. 나중에 갈아엎는다는 건 보통 안 갈아엎는다는 뜻이다.

갈아엎은 모양

쿼리 시작점을 뒤집었다. 더 이상 overview_collection에서 시작하지 않고, 북마크 컬렉션에서 시작해서 $lookup으로 overview를 붙이는 파이프라인.

match created_by → sort created_at desc
  → skip + limit
  → lookup overview by _id
  → unwind → replaceRoot

핵심은 skip + limit$lookup 앞에 둔 것. 페이지당 lookup 호출이 정확히 limit(12개)개로 제한된다. 만 개 북마크여도 페이지 보려고 만 번 lookup하지 않는다.

인덱싱

  • overview_bookmark_collection: (created_by:1, created_at:-1) — match와 sort를 한 인덱스로
  • overview_collection._id — implicit unique index. lookup은 O(log n) seek

필터(카테고리/언어 등)가 있으면 lookup이 사용자 북마크 수만큼 일어난다. 어쩔 수 없다 — overview 필드라 lookup 후에야 적용 가능. 다만 그 경우에도 정렬은 인덱스로, 각 lookup은 _id seek로 처리되니 선형 비용에 그친다.

회고

성능 문제는 발견된 후엔 탐정 일이고, 발견 전엔 설계의 일부다. 잘 돌아가는 코드라도 확실히 망가질 곳이 보이면 그게 바로 설계 문제고, 디자인 단계에 있을 때 고치는 게 압도적으로 싸다.

다음 사용자를 만나기 전에 갈아엎어서 다행이다.