⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

MongoDB 집계 파이프라인 기초 - 슬라이드 1/7
A

AI Generated

2025. 11. 27. · 0 Views

MongoDB 집계 파이프라인 기초

MongoDB의 집계 파이프라인을 처음 접하는 개발자를 위한 가이드입니다. 데이터를 필터링하고, 그룹화하고, 변환하는 방법을 실무 예제와 함께 차근차근 알아봅니다.


목차

  1. 집계_파이프라인_개념
  2. $match로_필터링
  3. $group으로_그룹화
  4. $project로_필드_변환
  5. $sort와_$limit
  6. $count와_$skip

1. 집계 파이프라인 개념

어느 날 김개발 씨는 쇼핑몰 서비스의 주문 데이터를 분석해달라는 요청을 받았습니다. "이번 달 카테고리별 매출 합계를 뽑아주세요." 단순히 find()로는 해결할 수 없는 문제였습니다.

이때 선배 박시니어 씨가 다가와 말했습니다. "집계 파이프라인을 써보세요."

집계 파이프라인은 한마디로 데이터를 단계별로 가공하는 컨베이어 벨트입니다. 마치 공장에서 원재료가 여러 공정을 거쳐 완제품이 되는 것처럼, 문서들이 여러 단계를 통과하며 원하는 형태로 변환됩니다.

이것을 제대로 이해하면 복잡한 데이터 분석도 MongoDB 안에서 깔끔하게 처리할 수 있습니다.

다음 코드를 살펴봅시다.

// 기본적인 집계 파이프라인 구조
db.orders.aggregate([
  // 1단계: 2024년 주문만 필터링
  { $match: { year: 2024 } },

  // 2단계: 카테고리별로 그룹화하여 매출 합산
  { $group: {
    _id: "$category",
    totalSales: { $sum: "$amount" }
  }},

  // 3단계: 매출 높은 순으로 정렬
  { $sort: { totalSales: -1 } },

  // 4단계: 상위 5개만 출력
  { $limit: 5 }
])

김개발 씨는 입사 6개월 차 주니어 개발자입니다. MongoDB를 사용한 프로젝트에 투입된 지 얼마 되지 않았는데, 오늘 팀장님으로부터 특별한 미션을 받았습니다.

"이번 달 카테고리별 매출 현황을 대시보드에 보여줘야 해요. 데이터 좀 뽑아주세요." 김개발 씨는 먼저 익숙한 find() 메서드로 시도해봤습니다.

하지만 금방 벽에 부딪혔습니다. find()로는 데이터를 조회할 수는 있어도, 카테고리별로 합산하거나 평균을 구하는 건 불가능했기 때문입니다.

그때 옆자리의 박시니어 씨가 화면을 슬쩍 보더니 말했습니다. "그건 집계 파이프라인으로 해야 해요.

SQL의 GROUP BY 같은 건데, MongoDB에서는 훨씬 더 강력하죠." 그렇다면 집계 파이프라인이란 정확히 무엇일까요? 쉽게 비유하자면, 집계 파이프라인은 마치 자동차 조립 공장의 컨베이어 벨트와 같습니다.

원재료인 철판이 들어가면 첫 번째 공정에서 프레스로 찍고, 두 번째 공정에서 용접하고, 세 번째 공정에서 도색하는 식으로 단계를 거칩니다. 각 공정은 이전 공정의 결과물을 받아서 자기 일만 수행하고, 다음 공정으로 넘깁니다.

집계 파이프라인도 마찬가지입니다. 문서들이 각 단계를 통과하면서 필터링되고, 그룹화되고, 변환됩니다.

집계 파이프라인이 없던 시절에는 어땠을까요? 개발자들은 데이터베이스에서 원본 데이터를 전부 가져온 다음, 애플리케이션 코드에서 직접 반복문을 돌려가며 합산하고 평균을 구해야 했습니다.

데이터가 적을 때는 문제가 없었지만, 수십만 건이 넘어가면 이야기가 달라집니다. 네트워크 부하도 심해지고, 서버 메모리도 부족해지고, 무엇보다 코드가 복잡해집니다.

바로 이런 문제를 해결하기 위해 MongoDB는 집계 파이프라인을 제공합니다. 집계 파이프라인을 사용하면 데이터베이스 서버에서 직접 연산이 수행됩니다.

네트워크로 전송되는 데이터 양이 획기적으로 줄어들고, 애플리케이션 코드도 깔끔해집니다. 게다가 MongoDB의 집계 엔진은 이런 작업에 최적화되어 있어서, 직접 코드를 작성하는 것보다 훨씬 빠릅니다.

위의 코드를 한 줄씩 살펴보겠습니다. aggregate() 메서드는 배열을 인자로 받습니다.

이 배열의 각 요소가 바로 파이프라인의 단계입니다. 첫 번째 단계인 $match는 2024년 주문만 걸러냅니다.

두 번째 단계인 $group은 카테고리별로 문서를 묶고 매출을 합산합니다. 세 번째 단계인 $sort는 결과를 내림차순으로 정렬하고, 마지막으로 $limit이 상위 5개만 남깁니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 쇼핑몰 대시보드를 만든다고 가정해봅시다.

일별 주문 건수, 카테고리별 매출, 시간대별 접속자 수 같은 통계가 필요합니다. 이런 것들을 모두 집계 파이프라인으로 처리할 수 있습니다.

실시간 분석부터 정기 리포트까지, 데이터와 관련된 거의 모든 작업에 활용됩니다. 하지만 주의할 점도 있습니다.

파이프라인의 단계가 너무 많아지면 성능이 떨어질 수 있습니다. 특히 $match 단계를 가능한 앞쪽에 배치해서 처리할 문서 수를 먼저 줄여주는 것이 좋습니다.

인덱스를 활용할 수 있는 단계를 앞에 두는 것도 중요한 최적화 기법입니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다. "아, 그러니까 데이터를 단계별로 가공하는 거군요!" 집계 파이프라인을 제대로 이해하면 복잡한 데이터 분석 요구사항도 MongoDB만으로 깔끔하게 해결할 수 있습니다.

실전 팁

💡 - $match 단계는 가능한 파이프라인 앞쪽에 배치하여 처리할 문서 수를 먼저 줄이세요

  • 복잡한 파이프라인은 단계별로 나눠서 테스트하면 디버깅이 쉬워집니다

2. $match로 필터링

김개발 씨가 집계 파이프라인의 기본 개념을 익힌 다음 날, 새로운 과제가 주어졌습니다. "프리미엄 회원의 주문 데이터만 분석해주세요." 수백만 건의 주문 중에서 특정 조건에 맞는 것만 골라내야 했습니다.

박시니어 씨가 말했습니다. "파이프라인의 문지기 역할을 하는 $match부터 배워보죠."

$match는 집계 파이프라인의 필터링 단계입니다. 마치 콘서트장 입구에서 티켓을 검사하는 것처럼, 조건에 맞는 문서만 다음 단계로 통과시킵니다.

find() 메서드에서 사용하는 쿼리 문법을 그대로 사용할 수 있어서 배우기도 쉽습니다. 파이프라인 초반에 배치하면 처리해야 할 데이터 양을 크게 줄일 수 있습니다.

다음 코드를 살펴봅시다.

// $match로 조건에 맞는 문서 필터링
db.orders.aggregate([
  // 여러 조건을 동시에 적용
  { $match: {
    status: "completed",           // 완료된 주문
    amount: { $gte: 10000 },       // 1만원 이상
    orderDate: {                   // 2024년 1월 주문
      $gte: new Date("2024-01-01"),
      $lt: new Date("2024-02-01")
    },
    "customer.type": "premium"     // 프리미엄 회원
  }},

  // 필터링된 데이터로 후속 작업 수행
  { $group: { _id: "$category", count: { $sum: 1 } }}
])

김개발 씨는 이제 집계 파이프라인의 기본 구조를 이해했습니다. 하지만 실제로 써보려니 막막했습니다.

주문 컬렉션에는 수백만 건의 문서가 있는데, 이걸 전부 처리하면 시간이 얼마나 걸릴지 걱정이 됐습니다. 박시니어 씨가 다가와 화면을 보더니 말했습니다.

"걱정하지 마세요. 파이프라인의 첫 번째 단계에서 필요한 데이터만 걸러내면 됩니다.

그게 바로 $match의 역할이에요." $match 단계를 쉽게 비유하자면, 도서관에서 책을 찾는 상황과 같습니다. 도서관에 10만 권의 책이 있다고 해서 전부 훑어볼 필요는 없습니다.

먼저 "컴퓨터 과학" 코너로 가고, 그다음 "데이터베이스" 분야를 찾고, 마지막으로 "MongoDB" 관련 책을 고르면 됩니다. $match도 마찬가지로, 조건에 맞는 문서만 골라서 다음 단계로 보냅니다.

$match가 없다면 어떻게 될까요? 파이프라인의 모든 단계가 전체 문서를 대상으로 작업해야 합니다.

100만 건의 문서가 있고 그중 1,000건만 필요한 상황인데, 100만 건 전체를 그룹화하고 정렬한다고 생각해보세요. 엄청난 자원 낭비입니다.

더 심각한 건 메모리 문제입니다. MongoDB는 기본적으로 한 번의 집계 작업에 100MB의 메모리 제한이 있습니다.

$match를 사용하면 이런 문제를 예방할 수 있습니다. 필터링 조건에 맞는 문서만 다음 단계로 전달되기 때문에, 후속 작업의 부담이 크게 줄어듭니다.

게다가 $match는 인덱스를 활용할 수 있습니다. 적절한 인덱스가 있다면 디스크의 모든 데이터를 읽지 않고도 빠르게 필터링할 수 있습니다.

위의 코드를 살펴보겠습니다. $match 안에는 여러 조건을 동시에 넣을 수 있습니다.

**status: "completed"**는 완료된 주문만 선택합니다. $gte 연산자는 "크거나 같다"를 의미하므로, amount가 10000 이상인 문서를 필터링합니다.

날짜 범위를 지정할 때는 시작일에 $gte, 종료일에 $lt를 사용하는 것이 일반적입니다. 내장 문서의 필드는 점 표기법으로 접근합니다.

실무에서 $match는 거의 모든 파이프라인에 등장합니다. 로그 분석을 할 때 특정 기간의 에러 로그만 추출하거나, 사용자 분석을 할 때 활성 사용자만 필터링하거나, 매출 보고서를 만들 때 취소되지 않은 주문만 선택하는 식입니다.

$match 없는 집계 파이프라인은 거의 찾아보기 힘들 정도입니다. 주의할 점이 있습니다.

$match를 파이프라인 중간이나 끝에 배치하면 인덱스를 활용할 수 없습니다. 최대한 앞쪽에 배치해야 성능상 이점을 얻을 수 있습니다.

또한 정규 표현식이나 $where 같은 연산자는 인덱스를 타지 않으므로 주의해야 합니다. 김개발 씨는 $match를 파이프라인 맨 앞에 배치하고 쿼리를 실행해봤습니다.

수백만 건의 데이터가 순식간에 수천 건으로 줄어드는 것을 보고 감탄했습니다. "이렇게 하면 후속 작업이 훨씬 빨라지겠네요!" $match를 제대로 활용하면 효율적인 집계 파이프라인의 첫걸음을 뗄 수 있습니다.

실전 팁

💡 - $match는 가능한 파이프라인의 첫 번째 단계로 배치하여 인덱스를 활용하세요

  • 여러 조건이 필요하면 하나의 $match에 모두 넣는 것이 여러 $match로 나누는 것보다 효율적입니다

3. $group으로 그룹화

$match로 필터링하는 법을 익힌 김개발 씨에게 새로운 요청이 들어왔습니다. "카테고리별 총 매출, 평균 주문 금액, 주문 건수를 한 번에 볼 수 있게 해주세요." 단순 필터링으로는 불가능한, 데이터를 묶어서 계산해야 하는 작업이었습니다.

$group은 문서들을 특정 기준으로 묶어서 집계 연산을 수행하는 단계입니다. SQL의 GROUP BY와 비슷하지만, 여러 집계 함수를 동시에 적용할 수 있어서 더 강력합니다.

합계, 평균, 최댓값, 최솟값, 개수 등 다양한 연산을 한 번의 그룹화로 모두 처리할 수 있습니다.

다음 코드를 살펴봅시다.

// $group으로 데이터 그룹화 및 집계
db.orders.aggregate([
  { $match: { status: "completed" } },

  { $group: {
    _id: "$category",              // 그룹화 기준
    totalSales: { $sum: "$amount" },      // 총 매출
    avgAmount: { $avg: "$amount" },       // 평균 주문 금액
    orderCount: { $sum: 1 },              // 주문 건수
    maxOrder: { $max: "$amount" },        // 최대 주문 금액
    minOrder: { $min: "$amount" },        // 최소 주문 금액
    customers: { $addToSet: "$customerId" } // 고유 고객 목록
  }},

  { $sort: { totalSales: -1 } }
])

김개발 씨는 이제 필터링은 자신 있게 할 수 있게 됐습니다. 하지만 오늘 요청받은 건 차원이 다른 문제였습니다.

카테고리별로 데이터를 묶어서 여러 가지 통계를 한꺼번에 계산해야 했습니다. 박시니어 씨가 말했습니다.

"이제 집계 파이프라인의 진짜 핵심인 $group을 배울 차례네요. 이걸 알면 SQL에서 하던 GROUP BY 작업을 MongoDB에서도 할 수 있어요." $group 단계를 비유하자면, 마트에서 영수증을 정리하는 것과 같습니다.

한 달 치 영수증이 잔뜩 쌓여 있을 때, 이걸 "식료품", "생활용품", "의류" 같은 카테고리별로 분류합니다. 그리고 각 카테고리마다 총 지출액, 평균 구매 금액, 구매 횟수를 계산합니다.

$group이 바로 이 작업을 해줍니다. $group이 없다면 어떻게 해야 할까요?

애플리케이션에서 모든 문서를 가져온 다음, 반복문을 돌면서 직접 카테고리별 객체를 만들고, 각각의 합계와 평균을 계산해야 합니다. 코드가 길어지고, 실수하기 쉽고, 무엇보다 성능이 좋지 않습니다.

$group을 사용하면 이 모든 작업을 데이터베이스에서 한 번에 처리합니다. 코드를 자세히 살펴보겠습니다.

$group에서 가장 중요한 필드는 _id입니다. 이것이 그룹화의 기준이 됩니다.

"$category"라고 쓰면 category 필드의 값이 같은 문서끼리 묶입니다. 달러 기호는 "이 필드의 값을 가져와라"라는 의미입니다.

그 아래에는 집계 함수들이 나옵니다. **$sum: "$amount"**는 각 그룹 내 모든 amount 값을 더합니다.

$sum: 1은 문서 개수를 세는 트릭입니다. 각 문서마다 1을 더하니까 결국 문서 수가 됩니다.

$avg는 평균, $max$min은 최댓값과 최솟값을 구합니다. 특히 유용한 것이 $addToSet입니다.

이것은 중복 없이 고유한 값들만 배열로 모읍니다. 위 예제에서는 각 카테고리를 구매한 고유 고객 목록을 만듭니다.

실무에서 $group은 정말 다양하게 활용됩니다. 일별 가입자 수 집계, 상품별 리뷰 평점 계산, 지역별 매출 분석, 시간대별 트래픽 집계 등 데이터 분석이 필요한 거의 모든 곳에서 사용됩니다.

대시보드를 만들 때 필수적인 기능이라고 할 수 있습니다. 몇 가지 주의할 점이 있습니다.

여러 필드로 그룹화하려면 _id에 객체를 넣으면 됩니다. 예를 들어 **_id: { category: "$category", year: "$year" }**처럼 쓰면 카테고리와 연도의 조합별로 그룹화됩니다.

또한 $group 후에는 원본 문서의 개별 필드에 접근할 수 없습니다. 필요한 필드는 집계 함수를 통해 명시적으로 가져와야 합니다.

김개발 씨는 $group을 사용해서 한 번의 쿼리로 카테고리별 매출, 평균, 건수를 모두 계산해냈습니다. "예전에는 코드로 직접 반복문 돌렸는데, 이렇게 깔끔하게 되다니!" $group을 마스터하면 복잡한 데이터 분석도 간단한 쿼리 하나로 해결할 수 있습니다.

실전 팁

💡 - _id: null로 설정하면 전체 문서를 하나의 그룹으로 처리하여 전체 합계나 평균을 구할 수 있습니다

  • $addToSet과 $push의 차이를 기억하세요: $addToSet은 중복을 제거하고, $push는 모든 값을 포함합니다

4. $project로 필드 변환

김개발 씨가 만든 집계 결과를 본 기획자가 말했습니다. "결과는 좋은데, 필드 이름이 너무 기술적이에요.

그리고 퍼센트로 표시하면 안 될까요?" 데이터는 잘 뽑았지만, 원하는 형태로 가공하는 작업이 남아있었습니다.

$project는 출력 문서의 형태를 원하는 대로 바꾸는 단계입니다. 필드를 선택하거나 제외하고, 이름을 바꾸고, 새로운 계산 필드를 추가할 수 있습니다.

마치 엑셀에서 열을 숨기거나 수식으로 새 열을 만드는 것처럼, 결과 데이터의 모양을 자유롭게 디자인할 수 있습니다.

다음 코드를 살펴봅시다.

// $project로 필드 변환 및 계산
db.orders.aggregate([
  { $match: { status: "completed" } },
  { $group: {
    _id: "$category",
    total: { $sum: "$amount" },
    count: { $sum: 1 }
  }},

  { $project: {
    _id: 0,                              // _id 필드 제외
    categoryName: "$_id",                 // 필드 이름 변경
    totalSales: "$total",
    orderCount: "$count",
    avgPerOrder: {                        // 새 필드 계산
      $round: [{ $divide: ["$total", "$count"] }, 0]
    },
    salesInMillions: {                    // 단위 변환
      $concat: [
        { $toString: { $round: [{ $divide: ["$total", 1000000] }, 2] } },
        "M"
      ]
    }
  }}
])

김개발 씨는 $group으로 훌륭한 집계 결과를 만들어냈습니다. 하지만 기획자의 피드백을 받고 나니, 데이터의 "내용"뿐 아니라 "형태"도 중요하다는 걸 깨달았습니다.

기술자가 아닌 사람이 봐도 이해할 수 있는 결과가 필요했습니다. 박시니어 씨가 조언했습니다.

"$project 단계를 추가하면 돼요. 데이터를 원하는 모양으로 다듬을 수 있죠." $project를 비유하자면, 원석을 보석으로 가공하는 과정과 같습니다.

광산에서 캐낸 원석은 그 자체로 가치가 있지만, 불필요한 부분을 깎아내고 면을 다듬어야 비로소 반지에 끼울 수 있는 보석이 됩니다. $project도 마찬가지로, 집계 결과의 불필요한 부분을 제거하고 원하는 형태로 다듬습니다.

$project 없이 결과를 가공하려면 어떻게 해야 할까요? 애플리케이션 코드에서 결과를 받아서 다시 변환해야 합니다.

필드 이름을 바꾸고, 계산식을 적용하고, 불필요한 필드를 제거하는 작업을 일일이 해야 합니다. 프론트엔드와 백엔드 양쪽에서 데이터 변환 코드가 중복되기 쉽습니다.

코드를 살펴보겠습니다. _id: 0은 _id 필드를 결과에서 제외한다는 의미입니다.

집계 결과에서 MongoDB가 자동으로 추가하는 _id가 필요 없을 때 유용합니다. 반대로 포함시키려면 _id: 1로 쓰거나 그냥 생략하면 됩니다.

**categoryName: "$_id"**는 필드 이름을 바꾸는 방법입니다. _id 필드의 값을 categoryName이라는 새 이름으로 출력합니다.

클라이언트 코드에서 이해하기 쉬운 이름을 쓸 수 있습니다. 가장 강력한 기능은 계산 필드입니다.

$divide로 나눗셈을 하고, $round로 반올림하고, $concat으로 문자열을 연결할 수 있습니다. 위 예제에서는 주문당 평균 금액을 계산하고, 매출을 "3.5M" 같은 형태로 변환합니다.

실무에서 $project는 API 응답을 만들 때 특히 유용합니다. 데이터베이스의 필드 이름과 API 스펙의 필드 이름이 다른 경우가 많습니다.

데이터베이스에서는 snake_case를 쓰는데 API는 camelCase를 요구한다든지 하는 상황이죠. $project로 변환하면 별도의 매핑 레이어 없이 원하는 형태로 바로 출력할 수 있습니다.

주의할 점이 있습니다. $project에서 명시하지 않은 필드는 결과에 포함되지 않습니다.

단, _id는 예외로 명시적으로 0을 지정해야 제외됩니다. 또한 너무 복잡한 계산은 성능에 영향을 줄 수 있으니, 정말 필요한 변환만 수행하는 것이 좋습니다.

김개발 씨는 $project를 추가하여 기획자가 원하는 형태로 결과를 다듬었습니다. "이제 비개발자도 바로 이해할 수 있는 데이터가 됐네요!" $project를 활용하면 집계 결과를 용도에 맞게 완벽하게 가공할 수 있습니다.

실전 팁

💡 - 많은 필드 중 일부만 제외하고 싶다면 $unset 단계가 더 간편합니다

  • $addFields는 기존 필드를 유지하면서 새 필드만 추가할 때 유용합니다

5. $sort와 $limit

집계 결과가 점점 완성되어 갔습니다. 하지만 기획자가 또 한 가지를 요청했습니다.

"매출 상위 10개 카테고리만 보여주세요. 그리고 높은 순으로 정렬해서요." 결과를 원하는 순서로 정렬하고 개수를 제한하는 방법이 필요했습니다.

$sort는 결과를 특정 필드 기준으로 정렬하고, $limit은 결과 개수를 제한합니다. 이 두 단계는 짝꿍처럼 함께 쓰이는 경우가 많습니다.

"상위 N개", "최근 N개" 같은 요구사항을 처리할 때 필수적인 조합입니다.

다음 코드를 살펴봅시다.

// $sort와 $limit으로 상위 결과 추출
db.orders.aggregate([
  { $match: { status: "completed", year: 2024 } },

  { $group: {
    _id: "$category",
    totalSales: { $sum: "$amount" },
    orderCount: { $sum: 1 }
  }},

  // 매출 기준 내림차순 정렬 (높은 것부터)
  { $sort: { totalSales: -1 } },

  // 상위 10개만 선택
  { $limit: 10 },

  // 순위 필드 추가 (선택사항)
  { $group: {
    _id: null,
    items: { $push: "$$ROOT" }
  }},
  { $unwind: { path: "$items", includeArrayIndex: "rank" } },
  { $replaceRoot: {
    newRoot: { $mergeObjects: ["$items", { rank: { $add: ["$rank", 1] } }] }
  }}
])

김개발 씨가 만든 카테고리별 매출 데이터는 이제 꽤 쓸만해졌습니다. 하지만 카테고리가 수십 개나 되다 보니, 중요한 정보가 묻혀버리는 문제가 있었습니다.

기획자는 핵심만 빠르게 파악하고 싶어 했습니다. 박시니어 씨가 말했습니다.

"정렬과 제한을 추가하면 돼요. $sort와 $limit, 이 두 단계만 알면 원하는 결과를 뽑을 수 있습니다." $sort$limit을 비유하자면, 마라톤 경기의 결과 발표와 같습니다.

수천 명의 참가자가 결승선을 통과했지만, 시상식에서는 기록순으로 정렬한 다음 상위 3명만 호명합니다. 모든 참가자의 순위를 발표할 필요는 없으니까요.

$sort가 기록순 정렬이고, $limit이 상위 3명 선택입니다. 이 두 단계가 없다면 어떻게 될까요?

모든 결과를 클라이언트로 전송한 다음 JavaScript의 sort()와 slice()로 처리해야 합니다. 결과가 많으면 네트워크 트래픽도 늘고, 클라이언트의 메모리 사용량도 증가합니다.

서버에서 필요한 만큼만 정제해서 보내는 것이 훨씬 효율적입니다. 코드를 살펴보겠습니다.

$sort에서 -1은 내림차순, 1은 오름차순입니다. 매출이 높은 순으로 정렬하려면 -1을 사용합니다.

여러 필드로 정렬하려면 **{ totalSales: -1, orderCount: -1 }**처럼 객체에 여러 필드를 나열하면 됩니다. 먼저 적은 필드가 우선순위가 높습니다.

$limit: 10은 결과를 10개로 제한합니다. $sort 다음에 $limit을 배치하면 "상위 10개"가 됩니다.

순서가 중요합니다. $limit을 먼저 쓰면 정렬되지 않은 상태에서 10개를 자르기 때문에 원하는 결과가 나오지 않습니다.

아래쪽 코드는 순위를 매기는 고급 기법입니다. 모든 결과를 배열로 모은 다음, $unwind로 다시 펼치면서 인덱스를 붙입니다.

필수는 아니지만 "1위, 2위, 3위" 같은 표시가 필요할 때 유용합니다. 실무에서 이 조합은 정말 자주 사용됩니다.

인기 상품 Top 10, 최근 가입자 20명, 매출 상위 지역 5개 등 "상위 N개" 패턴은 어디서나 등장합니다. 대시보드, 랭킹 페이지, 요약 보고서 등에서 필수적인 기능입니다.

주의할 점이 있습니다. $sort는 메모리를 많이 사용합니다.

대용량 데이터를 정렬할 때는 allowDiskUse: true 옵션을 사용하거나, $match로 먼저 데이터 양을 줄이는 것이 좋습니다. 또한 정렬 필드에 인덱스가 있으면 성능이 크게 향상됩니다.

김개발 씨는 $sort와 $limit을 추가하여 매출 상위 10개 카테고리를 깔끔하게 뽑아냈습니다. "이제 한눈에 핵심 정보가 보이네요!" $sort와 $limit을 적절히 활용하면 방대한 데이터에서 핵심만 추려낼 수 있습니다.

실전 팁

💡 - 대용량 데이터 정렬 시 aggregate() 옵션에 { allowDiskUse: true }를 추가하세요

  • $sort 다음에 $limit을 배치해야 "상위 N개"가 제대로 동작합니다

6. $count와 $skip

마지막으로 기획자가 페이지네이션 기능을 요청했습니다. "전체 카테고리가 몇 개인지 알려주고, 페이지별로 10개씩 보여주세요." 전체 개수를 세고, 원하는 위치부터 데이터를 가져오는 기능이 필요했습니다.

$count는 파이프라인을 통과한 문서의 총 개수를 세고, $skip은 지정한 개수만큼 문서를 건너뜁니다. 페이지네이션을 구현할 때 $skip과 $limit을 함께 사용하면 "2페이지의 10개 항목" 같은 요구사항을 처리할 수 있습니다.

다음 코드를 살펴봅시다.

// $count로 전체 개수 세기
db.orders.aggregate([
  { $match: { status: "completed" } },
  { $group: { _id: "$category" } },
  { $count: "totalCategories" }  // 결과: { totalCategories: 42 }
])

// $skip과 $limit으로 페이지네이션 구현
const page = 2;
const pageSize = 10;

db.orders.aggregate([
  { $match: { status: "completed" } },
  { $group: {
    _id: "$category",
    totalSales: { $sum: "$amount" }
  }},
  { $sort: { totalSales: -1 } },

  // 2페이지: 첫 10개를 건너뛰고 다음 10개 가져오기
  { $skip: (page - 1) * pageSize },  // 10개 건너뛰기
  { $limit: pageSize }                // 10개 가져오기
])

김개발 씨는 이제 집계 파이프라인의 핵심 기능을 대부분 익혔습니다. 마지막 과제는 페이지네이션이었습니다.

카테고리가 수십 개나 되니 한 화면에 다 보여줄 수 없었고, 전체 개수도 알려줘야 했습니다. 박시니어 씨가 말했습니다.

"$count와 $skip을 배우면 페이지네이션도 깔끔하게 구현할 수 있어요." $count를 비유하자면, 도서관에서 특정 장르의 책이 몇 권인지 세는 것과 같습니다. 책 내용은 필요 없고, 그냥 개수만 알면 됩니다.

$count는 파이프라인의 마지막에 위치해서 "여기까지 살아남은 문서가 몇 개야?"라고 물어보는 역할을 합니다. $skip은 대기열에서 앞에 있는 사람들을 건너뛰는 것과 같습니다.

VIP 입장권이 있으면 앞의 100명을 건너뛰고 바로 들어갈 수 있듯이, $skip은 지정한 수만큼의 문서를 무시하고 그다음부터 결과를 반환합니다. 코드를 살펴보겠습니다.

**$count: "totalCategories"**는 이전 단계까지 통과한 문서 수를 세서 지정한 필드명으로 반환합니다. 결과는 { totalCategories: 42 } 형태의 단일 문서입니다.

간단하지만 "전체 N개 중" 같은 정보를 표시할 때 유용합니다. 페이지네이션은 $skip$limit의 조합으로 구현합니다.

2페이지에서 10개씩 보여주려면, 먼저 1페이지의 10개를 건너뛰고($skip: 10), 다음 10개를 가져옵니다($limit: 10). 공식은 $skip: (page - 1) * pageSize입니다.

실무에서 페이지네이션은 거의 모든 목록 화면에 필요합니다. 상품 목록, 주문 내역, 검색 결과 등 데이터를 나눠서 보여주는 곳이라면 어디든 사용됩니다.

보통 API에서 page와 size 파라미터를 받아서 집계 파이프라인에 적용합니다. 중요한 주의사항이 있습니다.

$skip은 성능상 좋지 않은 점이 있습니다. $skip: 10000이면 10,000개의 문서를 실제로 읽고 버려야 합니다.

데이터가 많아질수록 뒤쪽 페이지의 성능이 나빠집니다. 대용량 데이터에서는 커서 기반 페이지네이션을 고려해보세요.

마지막으로 본 문서의 ID나 정렬 기준 값을 기억해두고, 그다음부터 가져오는 방식입니다. 전체 개수와 페이지 데이터를 한 번에 가져오려면 $facet을 사용할 수 있습니다.

하나의 파이프라인에서 여러 갈래로 분기하여 각각의 결과를 얻는 고급 기법입니다. 김개발 씨는 $count와 $skip을 활용하여 페이지네이션 기능을 완성했습니다.

"이제 집계 파이프라인의 기본은 다 배운 것 같아요!" 박시니어 씨가 미소 지으며 말했습니다. "맞아요.

이 여섯 가지 단계만 잘 조합해도 웬만한 데이터 분석은 다 할 수 있어요." $count와 $skip까지 마스터하면 실무에서 필요한 집계 파이프라인의 기초를 완성한 것입니다.

실전 팁

💡 - 대용량 데이터에서는 $skip 기반 페이지네이션 대신 커서 기반 페이지네이션을 고려하세요

  • 전체 개수와 페이지 데이터를 동시에 가져오려면 $facet 단계를 활용해보세요

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#MongoDB#Aggregation#Pipeline#Database#NoSQL#MongoDB,Database,NoSQL

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.