MongoDB에서 $lookup을 사용하는 방법

MongoDB는 NoSQL 데이터베이스로서, 컬렉션이라는 구조에 데이터를 저장하는 데 널리 사용됩니다. MongoDB 컬렉션은 JSON 형식으로 이루어진 하나 이상의 문서로 구성되어 있습니다. 여기서 문서는 기존의 관계형 SQL 데이터베이스의 행과 유사하며, 컬렉션은 테이블과 비슷하다고 볼 수 있습니다.

데이터베이스의 핵심적인 기능 중 하나는 데이터베이스에 저장된 정보를 검색하는 쿼리 기능입니다. 쿼리를 통해 특정 정보 탐색, 데이터 분석, 보고서 생성, 그리고 데이터 통합 등의 작업을 수행할 수 있습니다.

효율적인 데이터베이스 쿼리를 위해서는, SQL 또는 NoSQL 데이터베이스를 막론하고 여러 테이블 또는 컬렉션에 분산된 데이터를 하나의 결과 집합으로 결합할 수 있어야 합니다.

MongoDB에서는 $lookup 연산자를 사용하여 쿼리 시점에 두 컬렉션의 정보를 통합할 수 있습니다. 이는 SQL 데이터베이스의 왼쪽 외부 조인과 유사한 기능을 수행합니다.

$lookup의 활용 및 목적

데이터베이스의 중요한 역할은 원시 데이터를 가공하여 의미 있는 정보를 추출하는 것입니다.

예를 들어, 음식점 사업을 운영하는 경우, 일별 수입, 주말 인기 메뉴, 시간대별 커피 판매량 등을 파악하기 위해 데이터 분석을 수행할 수 있습니다.

이러한 분석에는 단순한 데이터베이스 쿼리만으로는 충분하지 않으며, 저장된 데이터를 대상으로 고급 쿼리를 실행해야 합니다. MongoDB는 이러한 요구를 충족시키기 위해 집계 파이프라인이라는 기능을 제공합니다.

집계 파이프라인은 데이터를 처리하여 최종 집계 결과를 도출하는 일련의 단계로 구성된 시스템입니다. 이러한 단계에는 $sort, $match, $group, $merge, $count, 그리고 $lookup 등이 포함됩니다.

이러한 단계들은 집계 파이프라인 내에서 자유롭게 순서를 변경하여 적용할 수 있으며, 각 단계마다 파이프라인을 통해 전달되는 데이터에 대해 서로 다른 작업이 수행됩니다.

따라서 $lookup은 MongoDB 집계 파이프라인의 한 단계로, 데이터베이스 내 두 컬렉션 간에 왼쪽 외부 조인을 수행하는 데 사용됩니다. 왼쪽 외부 조인은 왼쪽 컬렉션의 모든 문서와 오른쪽 컬렉션에서 일치하는 문서를 결합합니다.

이해를 돕기 위해, 아래와 같이 표 형식으로 표현된 두 컬렉션을 예시로 들어 보겠습니다.

주문_컬렉션:

order_id customer_id order_date total_amount
1 100 2022-05-01 50.00
2 101 2022-05-02 75.00
3 102 2022-05-03 100.00

고객_컬렉션:

customer_num customer_name customer_email customer_phone
100 John [email protected] [email protected]

order_collection에 있는 customer_id 필드를 기준으로 두 컬렉션 간에 왼쪽 외부 조인을 수행하면 order_collection이 왼쪽 컬렉션, customers_collection이 오른쪽 컬렉션이 됩니다. 최종 결과에는 Orders 컬렉션의 모든 문서와 Orders 컬렉션의 customer_id 값과 일치하는 customer_num 값을 가진 Customers 컬렉션의 문서들이 포함됩니다.

주문고객 컬렉션에 대한 왼쪽 외부 조인 결과는 표 형식으로 표시하면 다음과 같습니다.

Customers 컬렉션에 일치하는 customer_num 값이 없는 Orders 컬렉션의 customer_id 값이 101인 경우, 고객 테이블에서 누락된 해당 값은 null로 채워집니다.

$lookup은 필드 간의 정확한 일치 비교를 수행하며, 일치하는 필드뿐만 아니라 일치하는 전체 문서를 검색합니다.

$lookup 구문

$lookup의 구문은 다음과 같습니다.

{
   $lookup:
     {
       from: <join할 컬렉션>,
       localField: <입력 문서의 필드>,
       foreignField: <"from" 컬렉션의 문서 필드>,
       as: <출력 배열 필드>
     }
}

$lookup은 네 개의 매개변수를 가집니다.

  • from: 조인하려는 컬렉션을 지정합니다. 이전 예시에서는 orders_collectioncustomers_collection을 사용할 때, customers_collection을 이 값으로 설정했습니다.
  • localField: from 컬렉션(이 예시에서는 customer_collection)의 필드와 비교하는 데 사용되는 작업 컬렉션 또는 기본 컬렉션의 필드입니다. 위 예시에서는 orders_collection 내의 customer_id가 이에 해당합니다.
  • foreignField: from에서 지정한 컬렉션에서 비교하려는 필드입니다. 이 예시에서는 customer_collection에서 찾은 customer_num이 해당됩니다.
  • as: localFieldforeignField 간의 일치 결과를 담을 문서에 표시될 새 필드 이름을 지정합니다. 일치하는 모든 항목은 이 필드의 배열에 저장됩니다. 일치하는 항목이 없으면 이 필드에는 빈 배열이 할당됩니다.

이전의 두 컬렉션에서 orders_collection을 기본 컬렉션으로 사용하여 두 컬렉션에 대해 $lookup 작업을 수행하려면 다음 코드를 사용할 수 있습니다.

{
    $lookup: {
      from: "customers_collection",
      localField: "customer_id",
      foreignField: "customer_num",
      as: "customer_info"
   }
}

as 필드에는 임의의 문자열 값을 사용할 수 있습니다. 그러나 작업 문서에 이미 존재하는 이름을 지정하면 해당 필드의 값이 덮어씌워집니다.

여러 컬렉션의 데이터 결합

MongoDB의 $lookup은 집계 파이프라인에서 매우 유용한 단계입니다. 이 단계가 필수는 아니지만, 여러 컬렉션에서 데이터를 조인해야 하는 복잡한 쿼리를 수행할 때 중요한 역할을 합니다.

$lookup 단계는 두 컬렉션 간의 왼쪽 외부 조인을 수행하여 새로운 필드를 생성하거나 기존 필드의 값을 다른 컬렉션의 문서들을 포함하는 배열로 대체합니다.

이러한 문서들은 비교 중인 필드의 값과 일치하는 값이 있는지에 따라 선택됩니다. 최종 결과는 일치하는 항목이 발견된 경우 문서 배열을 포함하는 필드가 되고, 일치하는 항목이 없는 경우 빈 배열이 됩니다.

다음은 직원프로젝트 컬렉션을 예시로 들어 보겠습니다.

아래 코드를 사용하여 두 컬렉션을 결합할 수 있습니다.

db.projects.aggregate([
   {
      $lookup: {
         from: "employees",
         localField: "employees",
         foreignField: "_id",
         as: "assigned_employees"
      }
   }
])

이 작업의 결과는 두 컬렉션이 합쳐진 형태입니다. 결과는 각 프로젝트와 해당 프로젝트에 할당된 모든 직원 정보를 보여주며, 직원은 배열 형태로 표시됩니다.

$lookup과 함께 사용할 수 있는 파이프라인 단계

앞서 언급했듯이, $lookup은 MongoDB 집계 파이프라인의 한 단계이며, 다른 집계 파이프라인 단계들과 함께 사용될 수 있습니다. $lookup과 함께 이러한 단계들을 사용하는 방법을 설명하기 위해, 다음 두 컬렉션을 예시로 사용하겠습니다.

MongoDB에서는 JSON 형식으로 저장됩니다. MongoDB에서 위 컬렉션의 모습은 다음과 같습니다.

$lookup과 함께 사용할 수 있는 집계 파이프라인 단계의 몇 가지 예시는 다음과 같습니다.

$match

$match는 주어진 조건을 충족하는 문서만 집계 파이프라인의 다음 단계로 전달되도록 문서 스트림을 필터링하는 데 사용되는 집계 파이프라인 단계입니다. 이 단계는 파이프라인 초기에 불필요한 문서를 제거하여 집계 파이프라인을 최적화하는 데 유용합니다.

앞서 언급한 두 컬렉션을 사용하여 $match$lookup을 다음과 같이 결합할 수 있습니다.

db.users.aggregate([
   {
      $match: {
         country: "USA"
      }
   },
   {
      $lookup: {
         from: "orders",
         localField: "_id",
         foreignField: "user_id",
         as: "orders"
      }
   }
])

$match는 미국 사용자들을 필터링하는 데 사용됩니다. $match의 결과는 $lookup과 결합되어 미국 사용자들의 주문 정보를 가져옵니다. 위 작업의 결과는 다음과 같습니다.

$project

$project는 문서에 포함, 제외 또는 추가할 필드를 지정하여 문서를 재구성하는 데 사용되는 단계입니다. 예를 들어, 각각 10개의 필드를 가진 문서를 처리하지만 문서의 4개의 필드에만 데이터 처리에 필요한 데이터가 포함된 경우, $project를 사용하여 불필요한 외부 필드를 필터링할 수 있습니다.

이렇게 하면 집계 파이프라인의 다음 단계로 불필요한 데이터가 전달되는 것을 방지할 수 있습니다.

다음과 같이 $lookup$project를 결합할 수 있습니다.

db.users.aggregate([
   {
      $lookup: {
         from: "orders",
         localField: "_id",
         foreignField: "user_id",
         as: "orders"
      }
   },
   {
      $project: {
         name: 1,
         _id: 0,
         total_spent: { $sum: "$orders.price" }
      }
   }
])

위 코드는 $lookup을 사용하여 사용자 및 주문 컬렉션을 결합한 후, $project는 각 사용자의 이름과 각 사용자가 지출한 총 금액만 표시하는 데 사용됩니다. $project는 결과에서 _id 필드를 제거하는 데에도 사용됩니다. 위 작업의 결과는 다음과 같습니다.

$unwind

$unwind는 배열 필드의 각 요소에 대한 새로운 문서를 생성하여 배열 필드를 해체하거나 해제하는 데 사용되는 집계 단계입니다. 이 단계는 배열 필드 값에 대한 집계 작업을 실행하려는 경우 유용합니다.

예를 들어, 아래 예시에서 취미 필드에 대한 집계 작업을 실행하려는 경우, 배열이므로 직접 집계를 수행할 수 없습니다. 그러나 $unwind를 사용하여 해제한 다음 결과 문서에서 집계 작업을 수행할 수 있습니다.

사용자와 주문 컬렉션을 사용하여 $lookup$unwind를 다음과 같이 함께 사용할 수 있습니다.

db.users.aggregate([
   {
      $lookup: {
         from: "orders",
         localField: "_id",
         foreignField: "user_id",
         as: "orders"
      }
   },
   {
      $unwind: "$orders"
   }
])

위 코드에서 $lookuporders라는 배열 필드를 반환합니다. 그런 다음 $unwind를 사용하여 이 배열 필드를 해제합니다. 이 작업의 결과는 다음과 같습니다. Alice는 주문이 두 개였으므로 두 번 나타납니다.

$lookup 사용 사례 예시

데이터 처리 작업을 수행할 때 $lookup은 매우 유용한 도구입니다. 예를 들어, 유사한 데이터를 가진 컬렉션의 필드를 기반으로 결합하려는 두 컬렉션이 있을 수 있습니다. 간단한 $lookup 단계를 사용하여 이를 수행하고 다른 컬렉션에서 가져온 문서가 포함된 기본 컬렉션에 새 필드를 추가할 수 있습니다.

다음은 사용자와 주문 컬렉션을 예시로 보여줍니다.

$lookup을 사용하여 두 컬렉션을 결합하면 다음과 같은 결과를 얻을 수 있습니다.

$lookup을 사용하여 더 복잡한 조인 작업도 수행할 수 있습니다. $lookup은 두 컬렉션 간의 결합 작업에만 국한되지 않으며, 여러 개의 $lookup 단계를 구현하여 세 개 이상의 컬렉션에서 조인을 수행할 수 있습니다. 아래는 세 가지 컬렉션을 예시로 보여줍니다.

아래 코드를 사용하여 세 가지 컬렉션에서 보다 복잡한 조인 작업을 수행하여 이루어진 모든 주문과 주문된 제품에 대한 상세 정보를 가져올 수 있습니다.

다음 코드를 사용하면 가능합니다.

db.orders.aggregate([
   {
      $lookup: {
         from: "order_items",
         localField: "_id",
         foreignField: "order_id",
         as: "order_items"
      }
   },
   {
      $unwind: "$order_items"
   },
   {
      $lookup: {
         from: "products",
         localField: "order_items.product_id",
         foreignField: "_id",
         as: "product_details"
      }
   },
   {
      $group: {
         _id: "$_id",
         customer: { $first: "$customer" },
         total: { $sum: "$order_items.price" },
         products: { $push: "$product_details" }
      }
   }
])

위 작업의 결과는 다음과 같습니다.

결론

여러 컬렉션과 관련된 데이터 처리 작업을 수행할 때, $lookup은 데이터를 결합하고 여러 컬렉션에 저장된 데이터를 기반으로 결론을 도출하는 데 매우 유용합니다. 데이터 처리는 하나의 컬렉션에만 의존하는 경우가 거의 없습니다.

데이터에서 의미 있는 결론을 도출하려면 여러 컬렉션에서 데이터를 결합하는 것이 핵심적인 단계입니다. 따라서 MongoDB 집계 파이프라인에서 $lookup 단계를 활용하여 데이터를 더욱 효과적으로 처리하고 컬렉션에 저장된 원시 데이터에서 의미 있는 통찰력을 얻을 수 있도록 하는 것이 좋습니다.

MongoDB 명령어와 쿼리에 대해서도 더 알아보는 것을 추천합니다.