MongoDB에서 배열 형태의 하위 문서(서브 도큐먼트) 안에 또 다른 하위 문서가 중첩되어 있을 때, 특정 조건에 맞는 요소만 골라내야 하는 경우가 종종 있습니다. 이럴 때는 aggregate() 메서드와 $unwind 연산자를 함께 사용하면 됩니다. 이번 글에서는 실제 예제를 통해 그 과정을 단계별로 살펴보겠습니다.
1. 샘플 데이터 준비하기
먼저 데모용 컬렉션을 만들고 문서를 삽입합니다. 아래 예제는 details1 배열 안에 details2 배열이 한 번 더 중첩된 구조를 가지고 있습니다.
> db.demo583.insert([
... {
... "details1" : [
... {
... "details2" : [
... {
... "isMarried" : true,
... "Name" : "Chris"
... },
... {
... "isMarried" : true,
... "Name" : "Bob"
... }
... ]
... },
... {
... "details2" : [
... {
... "isMarried" : false,
... "Name" : "Chris"
... },
... {
... "isMarried" : true,
... "Name" : "Mike"
... }
... ]
... }
... ]
... }
... ]);
BulkWriteResult({
"writeErrors" : [ ],
"writeConcernErrors" : [ ],
"nInserted" : 1,
"nUpserted" : 0,
"nMatched" : 0,
"nModified" : 0,
"nRemoved" : 0,
"upserted" : [ ]
})
2. 저장된 문서 확인하기
find() 메서드를 사용하면 컬렉션에 저장된 모든 문서를 조회할 수 있습니다.
> db.demo583.find();
위 명령을 실행하면 다음과 같은 결과가 출력됩니다.
{ "_id" : ObjectId("5e91d3c4fd2d90c177b5bcc1"), "details1" : [
{ "details2" : [
{ "isMarried" : true, "Name" : "Chris" },
{ "isMarried" : true, "Name" : "Bob" }
] },
{ "details2" : [
{ "isMarried" : false, "Name" : "Chris" },
{ "isMarried" : true, "Name" : "Mike" }
] }
] }
3. 하위 문서를 조건으로 필터링하는 쿼리
이제 하위 문서의 값을 기준으로 다시 하위 문서를 필터링하는 쿼리를 작성해 보겠습니다. 핵심 흐름은 다음과 같습니다. 먼저 $match로 조건에 맞는 문서를 걸러내고, $unwind로 중첩 배열을 개별 요소로 펼친 뒤, 마지막에 다시 $match를 적용해 원하는 하위 문서만 남기는 방식입니다.
> var q= [
... {
... "$match": {
... "details1.details2.isMarried": true,
... "details1.details2.Name": "Chris"
... }
... },
... {
... "$unwind": "$details1"
... },
... {
... "$unwind": "$details1.details2"
... },
... {
... "$match": {
... "details1.details2.isMarried": true,
... "details1.details2.Name": "Chris"
... }
... }
... ];
> db.demo583.aggregate(q).pretty();
4. 실행 결과
집계 파이프라인을 실행하면 isMarried 값이 true이면서 Name이 "Chris"인 하위 문서만 정확하게 추출됩니다.
{
"_id" : ObjectId("5e91d3c4fd2d90c177b5bcc1"),
"details1" : {
"details2" : {
"isMarried" : true,
"Name" : "Chris"
}
}
}
쿼리 동작 원리 정리
- 첫 번째 $match : 최상위 문서 수준에서 details1.details2 경로에 조건(isMarried: true, Name: "Chris")이 하나라도 존재하는 문서만 통과시킵니다.
- $unwind : details1 배열과 details2 배열을 차례로 개별 문서로 분해합니다. 이 과정을 거치면 중첩된 하위 문서 하나하나가 독립적인 레코드가 됩니다.
- 두 번째 $match : 분해된 개별 하위 문서에 다시 조건을 검사하여, isMarried가 true이고 Name이 "Chris"인 요소만 최종 결과로 남깁니다.
이처럼 $unwind로 배열을 펼친 후 $match를 다시 적용하면 부모 문서가 아니라 하위 문서 자체를 기준으로 정확하게 필터링할 수 있습니다. 중첩 배열 구조에서 필요한 데이터만 추출할 때 매우 유용한 패턴이니 꼭 기억해 두세요.