
실제 업무 기반 회고이며, 서비스/테이블/필드명은 식별 방지를 위해 익명화했습니다.
tenantOrgId: 로그인 사용자가 접근 가능한 조직(테넌트) 식별값 buyerOrgId: 주문의 구매 주체(고객사/구매사) 조직 식별값 catalogItemId: 주문에 연결된 상품 식별값 fulfillSiteId: 주문 처리/출고 거점 식별값 buyerLocationId: 구매 주체의 지점/위치 식별값 removedAt: 소프트 삭제 시각(없으면 활성 데이터) eventYear/eventMonth/eventDay: 이력 이벤트 시점 search-order-records: 주문 이력 검색 API(익명화) orderRecords: 주문 이력 컬렉션(익명화) catalogItems: 상품 마스터 컬렉션(익명화) organizations: 조직 마스터 컬렉션(익명화) locations: 지점/거점 마스터 컬렉션(익명화)관리자 화면은 페이지당 20건을 보여줍니다.
겉으로는 20 / 10,000,000 구조라서 간단해 보이지만, 실제 서버 동작은 그렇지 않았습니다.
핵심 문제는:
즉, 느린 이유는 “표시 건수”가 아니라 “처리 건수”였습니다.
기존 경로를 단순화하면 아래와 비슷했습니다.
tenantOrgId 기준으로 접근 가능한 상품 후보(catalogItems)를 넓게 확보 catalogItemId 목록으로 orderRecords를 다시 필터링 organizations/locations와 조합해 화면 데이터 및 필터 옵션 생성 countDocuments)도 자주 수행즉,
상품 후보를 크게 만든 뒤 -> 이력에서 다시 거르고 -> 조직/지점 정보를 붙이는
다단계 구조였습니다.
orderRecords에서 큰 group/lookup 집계 반복cnt 중심으로 계산catalogItems 선로딩(preload) 범위가 넓음hasMore 중심)includeExactTotal=false로 두고limit + 1 조회로 hasMore만 판단orderRecords에서 페이지 20건 확정catalogItemId만 모아서 catalogItems 조회조회 패턴에 맞춰 인덱스 축 정리:
catalogItemId + removedAt + eventYear/eventMonth/eventDayfulfillSiteId + removedAt + eventYear/eventMonth/eventDaybuyerLocationId + removedAt + eventYear/eventMonth초기 무필터에서는 대형 이력 집계를 바로 타지 않고:
locations 직접organizations 직접catalogItems.buyerOrgId 그룹 + organizations 조합성능 개선 중 일부 분기에서 “항상 전체 옵션”이 노출되는 문제가 있었고,
scopedBaseFilter 경로로 강제 전환catalogItems 대량 후보 확보
-> orderRecords 재필터링
-> organizations/locations 조합
-> count/필터 집계 반복
조건 기반으로 스코프 먼저 축소
-> orderRecords 페이지 결과 20건 먼저 확정
-> 필요한 catalogItems/organizations/locations만 후로딩
-> 기본은 hasMore 중심 응답
핵심 차이:
“큰 후보를 만들고 줄이는 방식” -> “처음부터 좁은 범위로 시작하는 방식”
예: buyerOrgIds를 많이 선택한 상황에서도
buyerOrgIds -> buyerLocationIds로 먼저 압축 tenantOrgId, fulfillSiteId, removedAt 조건으로 범위 제한 hasMore 중심 $group/$lookup이나 광범위 count를 동시에 강제하면 비용이 커질 수 있음성능 개선의 본질은 “DB를 더 세게 쓰는 것”이 아니라:
hasMore 중심인가?