몽고디비(MongoDB)와 관계형 데이터베이스(RDBMS) 간의 모델링 차이

관계형 데이터베이스에서는 보통 데이터를 여러 테이블에 나누어 저장합니다. 예를 들어, "사용자(User)"와 "주문(Order)"이라는 두 개의 테이블이 있다고 가정해보겠습니다.

User 테이블
 user_id (Primary Key)
 name
 email
 ...

Order 테이블
 order_id (Primary Key)
 user_id (Foreign Key referencing User)
 product_name
 order_date
 ...

 

이러한 구조에서, 특정 사용자의 모든 주문을 조회하려면 두 테이블을 조인(join)해야 합니다. 이는 다음과 같은 SQL 쿼리로 표현될 수 있습니다.

SELECT u.name, o.product_name, o.order_date
FROM User u
JOIN Order o ON u.user_id = o.user_id
WHERE u.user_id = 1;

 

몽고디비(MongoDB)에서의 모델링

몽고디비에서는 데이터를 컬렉션(collection)에 문서(document) 형태로 저장합니다. 몽고디비에서는 두 개의 컬렉션을 사용하는 대신, 하나의 컬렉션에 데이터를 모두 넣을 수 있습니다. 

{
  "_id": 1,
  "name": "John Doe",
  "email": "john.doe@example.com",
  "orders": [
    {
      "order_id": 101,
      "product_name": "Laptop",
      "order_date": "2024-08-01"
    },
    {
      "order_id": 102,
      "product_name": "Smartphone",
      "order_date": "2024-08-02"
    }
  ]
}

이 경우, 특정 사용자의 주문 정보를 조회할 때 조인이 필요하지 않습니다. 단일 문서로 모든 관련 데이터를 가져올 수 있습니다. 

몽고디비에서는 관련된 데이터를 한 문서 안에 모두 포함시킬 수 있기 때문에, 조인 없이도 복잡한 구조의 데이터를 빠르게 조회할 수 있습니다.

몽고디비는 스키마리스(schema-less) 데이터베이스이므로, 데이터 구조를 유연하게 설계할 수 있으며, 필요한 경우 필드나 중첩된 구조를 쉽게 변경할 수 있습니다.

 

몽고디비에서 데이터를 쿼리할 때 find와 aggregate의 match 단계

1. find 메서드

find는 MongoDB에서 데이터를 조회할 때 가장 기본적인 메서드입니다. find 메서드를 사용하여 특정 조건에 맞는 문서를 찾을 수 있습니다.

// 컬렉션에서 name이 "John Doe"인 모든 문서를 찾기
db.collection.find({ name: "John Doe" })

// 컬렉션에서 age가 30 이상인 모든 문서를 찾기
db.collection.find({ age: { $gte: 30 } })

2. aggregate의 match 단계

aggregate 메서드는 몽고디비에서 데이터의 집계 작업을 수행할 때 사용됩니다.

// name이 "John Doe"이고 age가 30 이상인 문서를 필터링하는 집계 파이프라인
db.collection.aggregate([
  { $match: { name: "John Doe", age: { $gte: 30 } } }
])

 

MongoDB에서 배열 필드를 쿼리할 때 사용되는 연산자 $elemMatch

배열 내의 요소가 특정 조건을 만족하는지 검사하는 데 사용됩니다. 주로 배열의 각 요소가 객체일 때 유용합니다.

$elemMatch는 배열 필드에 대해 조건을 지정할 때 사용됩니다.

{
  "name": "John",
  "scores": [
    { "subject": "math", "score": 80 },
    { "subject": "history", "score": 90 },
    { "subject": "science", "score": 85 }
  ]
}

수학 점수가 80 이상이고, 과학 점수가 85 이상인 학생을 찾고 싶다고 가정하면 scores 배열에서 각 요소의 조건이 아니라 하나의 요소에서 subject가 "math"이고 score가 80 이상인지를 확인합니다.

db.students.find({
  scores: { 
    $elemMatch: { 
      subject: "math", 
      score: { $gte: 80 } 
    } 
  }
})

 

$addFields는 MongoDB의 집계 프레임워크에서 사용되는 연산자 중 하나로, 기존 문서에 새로운 필드를 추가하거나 기존 필드를 수정하는 데 사용

$addFields의 기본 개념

  • 새로운 필드 추가: 기존 문서에 계산된 값을 기반으로 새로운 필드를 추가
  • 기존 필드 수정: 기존 필드를 참조하여 값을 변경하거나 업데이트
  • 다양한 연산 사용: $addFields 내에서 여러 연산자($sum, $multiply, $concat, $ifNull 등)를 사용해 필드 값을 계산
db.collection.aggregate([
  {
    $addFields: {
      newField: <expression>,
      anotherNewField: <anotherExpression>,
      existingField: <newExpression>
    }
  }
])

학생들의 성적 데이터베이스에서 수학과 과학 점수의 합계를 계산하여 새로운 필드 totalScore를 추가하는 예제

#데이터
{
  "name": "Alice",
  "math": 85,
  "science": 90
}

#쿼리
db.students.aggregate([
  {
    $addFields: {
      totalScore: { $sum: ["$math", "$science"] }
    }
  }
])


#결과
{
  "name": "Alice",
  "math": 85,
  "science": 90,
  "totalScore": 175
}

더 복잡한 파이프라인에서 $addFields 사용

#데이터
{
  "name": "John",
  "salary": 70000,
  "bonus": 20000
}

#쿼리
db.employees.aggregate([
  {
    $addFields: {
      totalCompensation: { $sum: ["$salary", "$bonus"] },
      isHighEarner: { $gte: [{ $sum: ["$salary", "$bonus"] }, 100000] }
    }
  }
])

#결과
{
  "name": "John",
  "salary": 70000,
  "bonus": 20000,
  "totalCompensation": 90000,
  "isHighEarner": false
}

부적절하게 사용할 경우 몇 가지 단점

  • $addFields는 각 문서에 대해 새로운 필드를 계산하여 추가하기 때문에, 계산이 복잡할수록 더 많은 리소스를 소비
  • 많은 필드 추가나 복잡한 연산이 포함된 $addFields를 반복적으로 사용하면, CPU와 메모리 사용이 급격히 증가

MongoDB에서 문자열을 정렬

숫자 정렬 (numericOrdering: true 옵션)

numericOrdering 옵션을 사용하면 문자열 내의 숫자를 자연스럽게 정렬할 수 있습니다. 일반적인 사전순 정렬에서는 "10"이 "2"보다 먼저 오지만, numericOrdering: true를 설정하면 "2"가 "10"보다 앞에 오도록 정렬시킬 수 있습니다.

db.collection.find().sort({ name: 1 }).collation({ locale: "en", numericOrdering: true });

 

$unwind는 MongoDB의 집계 파이프라인에서 사용되는 연산자

하나의 문서에 있는 배열 필드의 각 요소가 개별 문서로 확장됩니다.

{
  "_id": 1,
  "name": "John",
  "hobbies": ["reading", "swimming", "cycling"]
},
{
  "_id": 2,
  "name": "Alice",
  "hobbies": ["painting", "dancing"]
}

이 데이터를 hobbies 필드를 기준으로 $unwind 연산자를 사용하여 확장하면 다음과 같은 결과가 나옵니다.

db.collection.aggregate([
  { $unwind: "$hobbies" }
])
{
  "_id": 1,
  "name": "John",
  "hobbies": "reading"
},
{
  "_id": 1,
  "name": "John",
  "hobbies": "swimming"
},
{
  "_id": 1,
  "name": "John",
  "hobbies": "cycling"
},
{
  "_id": 2,
  "name": "Alice",
  "hobbies": "painting"
},
{
  "_id": 2,
  "name": "Alice",
  "hobbies": "dancing"
}

혼합된 문서에서 특정 필드를 제외하고 특정 정보만 가져오고 싶을 때, $project 연산자를 사용

이 데이터에서 transactionId, transactionAmount, transactionDate와 같은 트랜잭션 관련 필드를 제외하고 어카운트 정보만 가져오고 싶다고 가정해보겠습니다. 

{
  "_id": 1,
  "accountId": "A12345",
  "accountName": "John Doe",
  "balance": 1000,
  "transactionId": "T98765",
  "transactionAmount": 100,
  "transactionDate": "2024-08-01"
},
{
  "_id": 2,
  "accountId": "A67890",
  "accountName": "Jane Smith",
  "balance": 2500,
  "transactionId": "T54321",
  "transactionAmount": 200,
  "transactionDate": "2024-08-02"
}

$project 연산자로 특정 필드 제외하기

$project 연산자를 사용하여 트랜잭션 관련 필드를 제외하고 어카운트 정보만 남기는 쿼리를 작성합니다.

db.collection.aggregate([
  {
    $project: {
      transactionId: 0,
      transactionAmount: 0,
      transactionDate: 0
    }
  }
])

결과

{
  "_id": 1,
  "accountId": "A12345",
  "accountName": "John Doe",
  "balance": 1000
},
{
  "_id": 2,
  "accountId": "A67890",
  "accountName": "Jane Smith",
  "balance": 2500
}

추가 쿼리 작성

예를 들어, 잔액(balance)이 1500 이상인 어카운트만 필터링하고 싶다면, 다음과 같이 쿼리를 작성할 수 있습니다.

db.collection.aggregate([
  {
    $project: {
      transactionId: 0,
      transactionAmount: 0,
      transactionDate: 0
    }
  },
  {
    $match: {
      balance: { $gte: 1500 }
    }
  }
])

최종 결과

이 쿼리의 결과는 다음과 같이 잔액이 1500 이상인 어카운트 정보만 반환됩니다.

{
  "_id": 2,
  "accountId": "A67890",
  "accountName": "Jane Smith",
  "balance": 2500
}