규모가 작거나 개인 프로젝트를 진행하면 MongoDB를 자주 사용하게 되는것 같습니다. 아무래도 모델링을 스킵해도 되고 확장성도 좋아서 손이 가는것 같습니다. 처음엔 SQL구문과 다른 특유의 메서드가 번거로웠는데 나름 익숙해지니 오히려 편하네요 (Tailwind에 익숙해지기 시작했을때와 비슷한 느낌입니다)

 

최근 대용량 데이터를 MongoDB로 다루면서 성능 이슈가 발생했고 이를 해결했던 방법을 소개해보려고 합니다.

공식문서에 있는 최적화방법을 참고했습니다.

MongoDB Aggregation Pipeline

 

Aggregation 파이프라인은 Document들을 여러 단계의 처리 과정을 거쳐 원하는 결과를 도출해내는 데이터 처리 파이프라인입니다. 이전 단계의 출력이 다음 단계의 입력으로 전달됩니다.

단순한 쿼리 (find)로는 처리하기 어려운 복잡한 데이터의 집계, 변환, 분석 작업을 수행할 수 있습니다.

 

1. 기본 예시

아래 기본 예시를 보겠습니다.

orders collection에서 $match로 필터 후 _id로 그루핑하여 값을 반환합니다.

db.orders.aggregate( [
   { $match: { size: "medium" } }, // size가 "medium" 인 document 필터
   { $group: { _id: "$name" } } 
   // _id로 그루핑하여 document들을 반환
] )

 

각각의 파이프라인마다 단계적으로 처리하는 것이 특징입니다.

마치 방식이 JavaScript 고차함수를 사용하는것 같습니다.

const result = orders
  .filter(order => order.size === "medium") // $match와 동일한 기능
  .reduce((groups, order) => {
    // $group과 동일한 기능
    const key = order.name;
    if (!groups[key]) {
      groups[key] = { _id: key };
    }
    return groups;
  }, {});

const finalResult = Object.values(result);

 

2. 주요 연산자

$match, $project, $group, $sort, $unwind 등이 있습니다.

$match는 조건에 맞는 문서를 필터링을 하고, $group은 표현식을 기준으로 그룹화합니다. 

$project는 특정 필드만 포함하여 값을 반환하고 $sort는 정렬 후 값을 반환합니다. 

 

SQL구문과 비교하면 아래와 같습니다.

$match -> WHERE

$group -> GROUP BY

$project -> SELECT

$sort -> ORDER BY

 

아래는 $project와 $sort 예시입니다.

db.orders.aggregate([
  {
    $project: {
      _id: 1,
      price: 1,
      size: 0 // size 필드는 제외
    }
  }
])

// 결과
[
    { "_id": "Bob", "price": 3000 },
    { "_id": "Alice", "price": 2000 },
    { "_id": "Alice", "price": 1000 }
]

 

db.orders.aggregate([
  {
    $project: {
      _id: 1,
      price: 1,
      size: 0
    }
  },
  { $sort: { price: 1 } } // price 기준으로 오름차순
])

// 결과
[
    { "_id": "Alice", "price": 1000 },
    { "_id": "Alice", "price": 2000 },
    { "_id": "Bob", "price": 3000 }
]

 

 

$unwind 는 배열 필드를 풀어서 각 배열 요소마다 별도의 문서를 생성하는 연산자입니다. 자바스크립트의 normalize(정규화) 메서드와 유사합니다.

 

아래는 $unwind 사용 예시입니다

db.products.insertMany([
  {
    _id: 1,
    name: "티셔츠",
    sizes: ["S", "M", "L", "XL"]
  },
  {
    _id: 2,
    name: "청바지",
    sizes: ["30", "32", "34"]
  }
]);

db.products.aggregate([
  { $unwind: "$sizes" } // size로 정규화한다
]);

// 결과
[
    { "_id" : 1, "name" : "티셔츠", "sizes" : "S" }
    { "_id" : 1, "name" : "티셔츠", "sizes" : "M" }
    { "_id" : 1, "name" : "티셔츠", "sizes" : "L" }
    { "_id" : 1, "name" : "티셔츠", "sizes" : "XL" }
    { "_id" : 2, "name" : "청바지", "sizes" : "30" }
    { "_id" : 2, "name" : "청바지", "sizes" : "32" }
    { "_id" : 2, "name" : "청바지", "sizes" : "34" }
]

 

 

이번엔 $unwind를 사용한 응용 예제입니다.

db.orders.insertMany([
  {
    _id: 1,
    customer: "김철수",
    items: [
      { product: "노트북", price: 1200000, quantity: 1 },
      { product: "마우스", price: 35000, quantity: 2 }
    ]
  },
  {
    _id: 2,
    customer: "이영희",
    items: [
      { product: "키보드", price: 89000, quantity: 1 },
      { product: "모니터", price: 350000, quantity: 1 },
      { product: "마우스", price: 35000, quantity: 1 }
    ]
  }
]);

db.orders.aggregate([
  { $unwind: "$items" }, // items로 정규화
  
  // 제품별로 그룹화
  { $group: {
    _id: "$items.product",
    totalQuantity: { $sum: "$items.quantity" },
    totalPrice: { $sum: { $multiply: ["$items.price", "$items.quantity"] } }
    // $multiply 는 배열 값을 곱합니다
  }},
  
  // 결과 정렬
  { $sort: { totalPrice: -1 } }
]);

// 결과
[
    { "_id" : "노트북", "totalQuantity" : 1, "totalPrice" : 1200000 },
    { "_id" : "모니터", "totalQuantity" : 1, "totalPrice" : 350000 },
    { "_id" : "키보드", "totalQuantity" : 1, "totalPrice" : 89000 },
    { "_id" : "마우스", "totalQuantity" : 3, "totalPrice" : 105000 }
]

 

3. 파이프라인 최적화

공식문서에는 파이프라인 최적화로 아래 방법을 소개하고 있습니다.

1) 필터링 우선 적용

  • $match를 파이프라인 앞부분에 배치하여 처리할 문서 수를 조기에 줄입니다.

2) 프로젝션 우선 적용

  • $project를 사용하여 필요한 필드만 선택하여 메모리 사용량을 줄입니다.

3) 인덱스 활용

  • $match, $sort 스테이지에서 인덱스 필드를 활용합니다.
  • 인덱스가 없는 경우 MongoDB는 컬렉션의 모든 문서를 스캔하여 쿼리 결과를 반환해야 합니다. 동일한 필드에서 반복적으로 쿼리를 실행하는 경우 해당 필드에 인덱스를 생상하여 성능을 개선할 수 있습니다.

4) 연산 최적화

  • $group 전 데이터 필터링을 통해 처리할 문서 수 줄이기

5) 불필요한 $unwind 피하기

  • 문서 수를 급격히 증가시킬 수 있는 $unwind를 지양

 

아래는 3) 인덱스 활용 예시입니다. 

db.users.insertMany([
  { name: "김철수", age: 25, city: "서울"},
  { name: "이영희", age: 30, city: "부산"},
  { name: "박지민", age: 28, city: "서울"},
]);

// age 필드에 인덱스 생성
db.users.createIndex({ age: 1 });

db.users.aggregate([
  // age 필드에 인덱스가 있으므로 빠르게 필터링됩니다
  { $match: { age: { $gt: 25 } } }
]);

 

쿼리 패턴을 고려해 복합 인덱스를 활용할 수 있습니다.

// 자주 사용되는 쿼리 패턴: city로 필터링 후 age로 정렬
db.users.createIndex({ city: 1, age: 1 });

db.users.aggregate([
  // city와 age 복합 인덱스 활용
  { $match: { city: "서울" } },
  { $sort: { age: 1 } }
]);

 

위 과정을 통해 최적화를 진행하기 전 후로 브라우저 랜더링 속도가 약 70% 감소했습니다.

pc 성능이 좋아져도 데이터가 복잡해지고 계산이 많아지면 체감할 만큼 성능이 안좋아지네요

 

요새는 간단한 코드는 GPT가 모두 작성해주니 공식문서를 읽고 고급기능을 잘 활용하는 능력이 더 필요해지는 것 같습니다.

얼마전에 에디터를 Vscode 에서 Windsurf 로 갈아탔는데 '딸깍'으로 할 수 있는일이 점점 많아져서 놀랐습니다.

곧 다가올 Ai 해일에 휩쓸리지 않으려면 미리 서핑하는법을 배워둬야겠다 싶은 요즈음입니다.🏄‍♂️

+ Recent posts