본문 바로가기

MongoDB

[MongoDB] 보조적인 JOIN 기능 - $lookup

대부분의 NoSQL DBMS가 그러하듯이 MongoDB 서버도 조인( JOIN )을 지원하지 않는다.

 

그래서 MongoDB에서는 조인이 필요하다고 생각되면 하나의 도큐먼트에 조인 대상 데이터를 내장( Embed )할 것을 권장하고 있다.

 

하지만 이렇게 도큐먼트를 통합하는 방식은 모든 유즈케이스에 대한 해답이 될 수는 없을 것이다.

 

다행이 MongoDB 3.2 버전부터는 "$lookup"이라는 보조적인 조인 기능을 제공하고 있다.

 

초기 MongoDB에서는 이 기능을 유료 버전( 엔터프라이즈 버전 )의 MongoDB에만 적용할 것이라고 발표했지만,

 

커뮤니티의 반발로 인해서 무료 버전( 커뮤니티 버전 )에도 같이 포함돼서 배포되고 있다.

 

 

Lookup 기능은 어그리게이션( Aggregation ) 기능의 일부로 제공되고 있으며, Lookup 쿼리는 다음과 같이 사용할 수 있다.

 

mongo> db.orders.insert(
    {
    	"order_id" : 999, "order_user" : "matt", "product_id" : 123 
    }
);

monbo> db.orders.insert(
	[
    	  { "product_id" : 122, "product_name" : "Computer", "price" : 670000 }
    	, { "product_id" : 123, "product_name" : "Mouse", "price" : 30000 }
    	, { "product_id" : 123, "product_name" : "Keyboard", "price" : 50000 }
    ]
);

mongo> db.orders.aggregate(
	[
		{
			$lookup : {
				  from : "products"
                , localField : "product_id"
                , foreignField : "product_id"
                , as : "order_product"
			}
		}
	]
);
{ "product_id" : 122, "product_name" : "Computer", "price" : 670000, "order_product" : [ ] }
{ "product_id" : 123, "product_name" : "Mouse", "price" : 30000, "order_product" : [ ] }
{ "product_id" : 123, "product_name" : "Keyboard", "price" : 50000, "order_product" : [ ] }

 

위의 쿼리 예제는 주문 컬렉션( orders )의 모록을 조회하면서 주문 대상 상품의 상세 정보를 상품 컬렉션( products )에서 같이 조회하는 쿼리다.

 

이때 두 컬렉션을 조인한느 필드명을 "localField"와 "foreignField" 필드에 명시해서 조인을 수행하게 되며,

 

조인된 상품 컬렉션의 정보는 "as" 필드에 명시한 "order_product" 필드에 서브 도큐먼트로 반환된다.

 

재미있는 것은 MongoDB의 어그리게이션( Aggregation )은 여러 개의 스테이지( Stage )를 가질 수 있기 때문에

 

다음과 같이 "$lookup" 스테이지를 여러 번 나열하여 여러 컬렉션을 한 번에 조인할 수도 있다.

 

db.orders.aggregate(
  [
    {
        $lookup : {
              from : "products"
            , localField : "product_id"
            , foreignField : "product_id"
            , as : "order_product"
        }
    }
    , {
        $lookup : {
              from : "users"
            , localField : "order_user"
            , foreignField : "user_id"
            , as : "order_user"
        }
    }
  ]
);

 

MongoDB의 "$lookup" 기능은 기존의 RDBMS와 비교했을 때 몇 가지 차이점과 제약사항이 있다.

 

     ① INNER JOIN은 지원하지 않으며, OUTER JOIN만 지원한다.

     ② 조인되는 대상 컬렉션은 같은 데이터베이스( DB )에 있어야 한다.

     ③ 샤딩되지 않은 컬렉션만 "$lookup" 오퍼레이션을 사용할 수 있다.

 

첫 번째 제약 조건은 MongoDB의 어그리게이션( Aggregation )에 필터링 스테이지를 추가해서

 

드리븐 컬렉션에서 일치하는 도큐먼트를 찾지 못한 경우에는 제거할 수 있다.

 

그리고 두 번째 제약 사항은 모델링 시점에 충분히 피할 수 있다.

 

문제는 세 번째 제약사항이다.

 

이는 샤딩되지 않은 컬렉션만 $lookup의 대상 컬렉션으로 지정할 수 있다는 제약 사항인데,

 

만약 단일 레플리카 셋으로 MongoDB를 사용한다면 걱정할 바는 아니다.

 

하지만 일반적으로 MongoDB를 사용하는 이유가 샤딩인 것을 감안하면 치명적인 제약사항이다.

 

샤딩된 컬렉션을 "$lookup"의 대상 컬렉션으로 사용하지 못하는 이유는 $lookup 오퍼레이션이

 

Mongo 라우터에서 처리되는 것이 아니라 MongoDB 샤드 서버 단위로 처리되기 때문이다.

 

이 제약 사항은 같은 샤드 키로 샤딩된 컬렉션이라 하더라도 피할 수 없다.

 

같은 샤드 키로 샤딩됐다 하더라도 컬렉션이 다르면 청크가 서로 다르게 분산되기 때문이다.

 

 

$lookup 스테이지는 MongoDB 샤드에서 해당 DB의 프라이머리 샤드로 요청되며,

 

프라이머리 샤드는 우선 드라이빙 컬렉션을 각 샤드로 전송하여 그 결과를 수집한다.

 

그리고 프라이머리 샤드는 수집된 결과와 드리븐 컬렉션을 조인하게 되는데,

 

이때 드리븐 컬렉션은 프라이머리 샤드에 저장돼 있기 때문에 로컬 샤드에서 조인 처리를 수행할 수 있는 것이다.

 

그래서 $lookup을 사용한 조인 쿼리가 많아지면 해당 DB의 프라이머리 샤드 서버만 많은 처리를 담당하게 되므로

 

부하의 불균형이 심해질 수 있다는 점을 주의하자.