프로그래밍/Database

💻 [MongoDB] 고급 파이프라인 연산자

프로그래민구찌 2025. 5. 2. 19:50

🎯 개요

MongoDB Aggregation은 단순히 그룹핑하고 계산하는 데 그치지 않고,
문서 구조를 바꾸거나 배열을 펼치거나, 특정 필드만 문서로 끌어올리는 등 다양한 데이터 전처리도 지원합니다.

이번 글에서는 $addFields, $unwind, $replaceRoot를 하나씩 실습해보며
복잡한 문서를 어떻게 원하는 형태로 바꿀 수 있는지 이해해보았습니다.


🧪 샘플 데이터 – surveys 컬렉션

db.surveys.insertMany([
  {
    respondent: "user1",
    age: 29,
    answers: [
      { question: "Q1", answer: "A" },
      { question: "Q2", answer: "B" }
    ]
  },
  {
    respondent: "user2",
    age: 34,
    answers: [
      { question: "Q1", answer: "C" },
      { question: "Q2", answer: "A" }
    ]
  }
])

🧮 고급 연산자 요약표

연산자 설명 예시
$addFields 기존 문서에 필드추가 { $addFields: { fullAge: "$age" } }
$unwind 배열 필드를 문서별로 펼침 { $unwind: "$answers" }
$replaceRoot 전체 문서를 특정 필드로 교체 { $replaceRoot: { newRoot: "$answers" } }

1️⃣ $addFields – 새 필드 추가

✔ 10년 후 나이 필드 추가

db.surveys.aggregate([
  {
    $addFields: {
      ageInTenYears: { $add: ["$age", 10] }
    }
  }
])

-- 결과
{ "_id" : ObjectId("6814212f9082a9a00776ebef"), "respondent" : "user1", "age" : 29, "answers" : [ { "question" : "Q1", "answer" : "A" }, { "question" : "Q2", "answer" : "B" } ], "ageInTenYears" : 39 }
{ "_id" : ObjectId("6814212f9082a9a00776ebf0"), "respondent" : "user2", "age" : 34, "answers" : [ { "question" : "Q1", "answer" : "C" }, { "question" : "Q2", "answer" : "A" } ], "ageInTenYears" : 44 }
처음엔 $project로도 비슷하게 할 수 있지 않나 싶었는데,
$addFields는 기존 필드 그대로 유지한 채 필요한 것만 추가할 수 있어서 더 간편했습니다.

2️⃣ $unwind – 배열 펼치기

✔ answers 배열을 하나씩 문서로 분해

db.surveys.aggregate([
  { $unwind: "$answers" }
])

-- 결과
{ "_id" : ObjectId("6814212f9082a9a00776ebef"), "respondent" : "user1", "age" : 29, "answers" : { "question" : "Q1", "answer" : "A" } }
{ "_id" : ObjectId("6814212f9082a9a00776ebef"), "respondent" : "user1", "age" : 29, "answers" : { "question" : "Q2", "answer" : "B" } }
{ "_id" : ObjectId("6814212f9082a9a00776ebf0"), "respondent" : "user2", "age" : 34, "answers" : { "question" : "Q1", "answer" : "C" } }
{ "_id" : ObjectId("6814212f9082a9a00776ebf0"), "respondent" : "user2", "age" : 34, "answers" : { "question" : "Q2", "answer" : "A" } }
aanswers 배열이 있으면 질문이 여러 개라서 분석이 어려웠는데,
$unwind를 쓰니까 배열 요소 하나하나가 독립 문서처럼 분리돼서 훨씬 다루기 쉬워졌습니다.

3️⃣ $replaceRoot – 문서 구조 재정의

✔ answers 항목을 최상위 문서로 설정

db.surveys.aggregate([
  { $unwind: "$answers" },
  { $replaceRoot: { newRoot: "$answers" } }
])

-- 결과
{ "question" : "Q1", "answer" : "A" }
{ "question" : "Q2", "answer" : "B" }
{ "question" : "Q1", "answer" : "C" }
{ "question" : "Q2", "answer" : "A" }
$unwind만 쓰면 문서 안에 answers가 남아있지만,
$replaceRoot까지 조합하면 아예 그 배열 요소 자체가 문서가 됩니다.
중첩 구조를 평탄화할 때 정말 유용하다고 느꼈습니다.

📌 실전 예제 조합

✔ 응답자별 Q1 답변만 추출

db.surveys.aggregate([
  { $unwind: "$answers" },
  { $match: { "answers.question": "Q1" } },
  {
    $project: {
      _id: 0,
      respondent: 1,
      question: "$answers.question",
      answer: "$answers.answer"
    }
  }
])

-- 결과
{ "respondent" : "user1", "question" : "Q1", "answer" : "A" }
{ "respondent" : "user2", "question" : "Q1", "answer" : "C" }
실제로는 조건 필터링과 가공을 함께 하게 되는데,
$unwind → $match → $project 조합이 가장 기본적인 데이터 추출 흐름처럼 느껴졌습니다.

🧠 회고 및 팁

  • $addFields는 기존 필드를 유지하면서 계산 필드를 추가할 때 쓰면 훨씬 편합니다.
  • $unwind는 배열 필터링과 분석에서 거의 필수 도구라고 느껴졌고,
  • $replaceRoot는 중첩된 문서를 평탄화해서 딱 필요한 데이터만 남기기 좋았습니다.
  • 세 연산자를 조합하면 복잡한 JSON 구조도 SQL 테이블처럼 편하게 다룰 수 있다는 확신이 생겼습니다.