⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

MongoDB 쿼리 연산자 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 11. 27. · 0 Views

MongoDB 쿼리 연산자 완벽 가이드

MongoDB에서 데이터를 효율적으로 검색하고 필터링하는 쿼리 연산자를 알아봅니다. 비교, 논리, 배열 연산자부터 정규표현식, 프로젝션, 정렬까지 실무에서 바로 활용할 수 있는 내용을 다룹니다.


목차

  1. 비교_연산자
  2. 논리_연산자
  3. 배열_연산자
  4. 정규표현식_검색
  5. projection으로_필드_선택
  6. 정렬_제한_건너뛰기

1. 비교 연산자

김개발 씨는 오늘 처음으로 MongoDB를 사용하는 프로젝트에 투입되었습니다. 기존에 MySQL만 사용하던 그에게 MongoDB의 쿼리 문법은 낯설기만 합니다.

"WHERE age > 20 같은 조건은 MongoDB에서 어떻게 쓰는 거지?" 그가 혼잣말을 하자, 옆자리 박시니어 씨가 모니터를 슬쩍 바라봅니다.

비교 연산자는 문서의 필드 값을 특정 값과 비교하여 조건에 맞는 문서만 찾아내는 연산자입니다. 마치 도서관에서 "2020년 이후에 출판된 책만 찾아주세요"라고 요청하는 것과 같습니다.

$eq(같음), $gt(초과), $gte(이상), $lt(미만), $lte(이하), $ne(같지 않음) 등이 있으며, SQL의 WHERE 절과 비슷한 역할을 합니다.

다음 코드를 살펴봅시다.

// 사용자 컬렉션에서 나이가 정확히 25세인 사람 찾기
db.users.find({ age: { $eq: 25 } })

// 나이가 30세 초과인 사람 찾기
db.users.find({ age: { $gt: 30 } })

// 나이가 20세 이상, 30세 미만인 사람 찾기
db.users.find({
  age: {
    $gte: 20,  // 20 이상
    $lt: 30    // 30 미만
  }
})

// 상태가 'inactive'가 아닌 모든 사용자
db.users.find({ status: { $ne: "inactive" } })

김개발 씨는 입사 3개월 차 주니어 개발자입니다. 오늘 처음으로 MongoDB를 다루게 되었는데, 가장 기본적인 데이터 조회부터 막히고 말았습니다.

MySQL에서는 익숙하게 사용하던 WHERE age > 20 같은 조건을 MongoDB에서는 어떻게 표현해야 할지 막막했습니다. 선배 개발자 박시니어 씨가 다가와 화면을 살펴봅니다.

"아, MongoDB에서는 비교 연산자를 사용해야 해요. 문법이 조금 다르지만, 익숙해지면 오히려 더 직관적이에요." 그렇다면 비교 연산자란 정확히 무엇일까요?

쉽게 비유하자면, 비교 연산자는 마치 쇼핑몰에서 필터를 거는 것과 같습니다. "가격 5만원 이상", "평점 4점 초과"처럼 원하는 조건을 설정하면 그에 맞는 상품만 보여주는 것처럼, MongoDB의 비교 연산자도 조건에 맞는 문서만 골라서 반환합니다.

$eq는 equal의 약자로, 정확히 일치하는 값을 찾습니다. 사실 { age: 25 }와 { age: { $eq: 25 } }는 같은 결과를 반환합니다.

하지만 명시적으로 $eq를 쓰면 코드의 의도가 더 명확해집니다. $gt$lt는 각각 greater than(초과)과 less than(미만)을 의미합니다.

여기서 주의할 점이 있습니다. $gt는 "초과"이지 "이상"이 아닙니다.

만약 20세 이상을 찾고 싶다면 $gte(greater than or equal)를 사용해야 합니다. 위의 코드를 한 줄씩 살펴보겠습니다.

첫 번째 쿼리는 age 필드가 정확히 25인 문서를 찾습니다. 두 번째 쿼리는 age가 30보다 큰, 즉 31세 이상인 사람을 찾습니다.

세 번째 쿼리가 특히 중요한데, 하나의 필드에 여러 비교 연산자를 동시에 적용하는 방법을 보여줍니다. age: { $gte: 20, $lt: 30 }은 20세 이상 30세 미만, 즉 20대인 사람을 모두 찾아냅니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 전자상거래 서비스를 개발한다고 가정해봅시다.

"10,000원 이상 50,000원 이하 상품 중 재고가 0이 아닌 것"을 찾을 때 비교 연산자가 빛을 발합니다. 이런 필터링 기능은 거의 모든 서비스에서 필수적으로 사용됩니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 $gt와 $gte를 혼동하는 것입니다.

"20세 이상"을 찾고 싶은데 $gt: 20을 쓰면 20세는 제외됩니다. 반드시 "이상/이하"인지 "초과/미만"인지 확인하고 적절한 연산자를 선택해야 합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다.

"아, SQL이랑 크게 다르지 않네요. 문법만 조금 다를 뿐이군요!" 비교 연산자를 제대로 이해하면 MongoDB에서 원하는 데이터를 정확하게 조회할 수 있습니다.

이것이 모든 MongoDB 쿼리의 기초가 됩니다.

실전 팁

💡 - $gt/$lt는 "초과/미만", $gte/$lte는 "이상/이하"입니다. 경계값 포함 여부를 항상 확인하세요.

  • 하나의 필드에 여러 비교 연산자를 조합하면 범위 검색이 가능합니다.
  • 문자열에도 비교 연산자를 사용할 수 있으며, 알파벳 순서로 비교됩니다.

2. 논리 연산자

김개발 씨가 비교 연산자에 익숙해질 무렵, 새로운 요구사항이 들어왔습니다. "VIP 등급이면서 최근 30일 내 구매 이력이 있는 고객, 또는 총 구매액이 100만원 이상인 고객을 찾아주세요." 조건이 복잡해지자 김개발 씨는 다시 고민에 빠졌습니다.

이럴 때 필요한 것이 바로 논리 연산자입니다.

논리 연산자는 여러 조건을 조합하여 복잡한 쿼리를 만들 때 사용합니다. $and는 모든 조건을 만족해야 하고, $or는 하나라도 만족하면 됩니다.

$not은 조건을 부정하고, $nor는 모든 조건을 만족하지 않는 문서를 찾습니다. 마치 "빨간색이면서 사이즈가 M인 옷" 또는 "파란색이거나 초록색인 옷"을 찾는 것과 같습니다.

다음 코드를 살펴봅시다.

// $and: VIP이면서 활성 상태인 사용자
db.users.find({
  $and: [
    { grade: "VIP" },
    { status: "active" }
  ]
})

// $or: 서울 거주자이거나 부산 거주자
db.users.find({
  $or: [
    { city: "서울" },
    { city: "부산" }
  ]
})

// 복합 조건: (VIP이고 활성) 또는 구매액 100만원 이상
db.users.find({
  $or: [
    { $and: [{ grade: "VIP" }, { status: "active" }] },
    { totalPurchase: { $gte: 1000000 } }
  ]
})

// $not: 나이가 30 이상이 아닌 사용자 (30 미만)
db.users.find({ age: { $not: { $gte: 30 } } })

김개발 씨는 이제 비교 연산자를 자유자재로 사용할 수 있게 되었습니다. 그런데 기획팀에서 새로운 요청이 들어왔습니다.

"이번 프로모션 대상자를 추출해주세요. 조건은 좀 복잡해요." 조건을 듣고 보니 정말 복잡했습니다.

"VIP 등급이면서 최근 구매 이력이 있는 고객, 또는 일반 등급이지만 총 구매액이 100만원 이상인 고객"을 찾아야 했습니다. 단순한 비교 연산자만으로는 해결이 어려워 보였습니다.

박시니어 씨가 힌트를 줍니다. "이럴 때 논리 연산자를 쓰면 돼요.

SQL에서 AND, OR 쓰는 것처럼요." 논리 연산자란 무엇일까요? 쉽게 비유하자면, 논리 연산자는 마치 여러 개의 체를 조합하는 것과 같습니다.

$and는 두 개의 체를 직렬로 연결해서 두 체를 모두 통과한 것만 남기고, $or는 병렬로 연결해서 어느 체든 하나만 통과하면 통과시킵니다. $and 연산자는 배열 안의 모든 조건을 만족하는 문서만 반환합니다.

재미있는 점은 같은 필드에 여러 조건을 걸 때 $and를 생략할 수 있다는 것입니다. 예를 들어 { age: { $gte: 20, $lt: 30 } }는 암묵적으로 $and가 적용된 것입니다.

$or 연산자는 배열 안의 조건 중 하나라도 만족하면 해당 문서를 반환합니다. "서울에 사는 사람 또는 부산에 사는 사람"처럼 여러 가능성 중 하나만 맞아도 되는 경우에 사용합니다.

위의 코드를 살펴보면 복합 조건의 예시가 눈에 띕니다. 세 번째 쿼리를 보면 $or 안에 $and가 중첩되어 있습니다.

이렇게 논리 연산자를 중첩하면 아무리 복잡한 비즈니스 로직도 하나의 쿼리로 표현할 수 있습니다. "VIP이면서 활성 상태인 사용자" 또는 "구매액이 100만원 이상인 사용자"를 한 번에 찾아내는 것입니다.

$not 연산자는 조금 특별합니다. 다른 논리 연산자들과 달리 배열을 받지 않고, 단일 조건을 부정합니다.

{ age: { $not: { $gte: 30 } } }는 "30세 이상이 아닌", 즉 30세 미만인 사용자를 찾습니다. 언뜻 보면 { age: { $lt: 30 } }과 같아 보이지만, null 값 처리에서 차이가 있으니 주의해야 합니다.

실무에서 논리 연산자는 정말 자주 사용됩니다. 회원 등급별 프로모션, 지역별 배송비 정책, 복합 검색 필터 등 거의 모든 비즈니스 로직에 논리 연산자가 필요합니다.

특히 관리자 페이지에서 복잡한 조건으로 데이터를 조회할 때 빛을 발합니다. 하지만 주의할 점이 있습니다.

논리 연산자를 너무 깊게 중첩하면 코드 가독성이 떨어지고, 쿼리 성능에도 영향을 줄 수 있습니다. 조건이 너무 복잡해진다면 애플리케이션 레벨에서 로직을 분리하는 것도 고려해보세요.

다시 김개발 씨의 이야기로 돌아갑니다. 복잡한 프로모션 대상자 쿼리를 논리 연산자로 깔끔하게 작성한 김개발 씨는 뿌듯한 미소를 지었습니다.

"이제 어떤 복잡한 조건도 무섭지 않아요!"

실전 팁

💡 - 같은 필드에 여러 조건을 걸 때는 암묵적 $and가 적용되므로 생략 가능합니다.

  • $or를 사용할 때는 인덱스 활용을 위해 각 조건에 해당하는 필드에 인덱스를 걸어두세요.
  • 너무 깊은 중첩은 피하고, 필요하다면 쿼리를 여러 개로 나누는 것을 고려하세요.

3. 배열 연산자

김개발 씨가 개발 중인 서비스에는 사용자의 관심 태그를 배열로 저장하는 기능이 있습니다. { name: "김개발", interests: ["JavaScript", "MongoDB", "React"] } 이런 식으로요.

"JavaScript에 관심 있는 사용자를 어떻게 찾지?" 배열 안의 값을 검색하는 방법이 궁금해졌습니다.

배열 연산자는 배열 타입 필드를 다룰 때 사용하는 연산자입니다. $in은 지정된 값 중 하나라도 포함하면 매칭되고, $all은 지정된 값을 모두 포함해야 매칭됩니다.

$elemMatch는 배열 내 요소가 여러 조건을 동시에 만족하는지 검사합니다. 마치 "사과 또는 배가 들어있는 바구니"($in)와 "사과와 배가 모두 들어있는 바구니"($all)를 찾는 것과 같습니다.

다음 코드를 살펴봅시다.

// $in: JavaScript 또는 Python에 관심 있는 사용자
db.users.find({
  interests: { $in: ["JavaScript", "Python"] }
})

// $all: JavaScript와 React 모두에 관심 있는 사용자
db.users.find({
  interests: { $all: ["JavaScript", "React"] }
})

// $elemMatch: 점수가 80 이상이고 과목이 수학인 기록이 있는 학생
db.students.find({
  scores: {
    $elemMatch: {
      subject: "수학",
      score: { $gte: 80 }
    }
  }
})

// 배열 크기로 검색: 관심사가 정확히 3개인 사용자
db.users.find({ interests: { $size: 3 } })

김개발 씨는 사용자 프로필 기능을 개발하고 있었습니다. 각 사용자는 관심 분야를 여러 개 등록할 수 있고, 이 정보는 배열로 저장됩니다.

interests: ["JavaScript", "MongoDB", "React"] 이런 식으로요. 마케팅팀에서 요청이 왔습니다.

"JavaScript나 Python에 관심 있는 사용자들에게 새로운 강의 알림을 보내고 싶어요." 김개발 씨는 고민에 빠졌습니다. 배열 안에 특정 값이 있는지 어떻게 확인할 수 있을까요?

박시니어 씨가 지나가다 말합니다. "배열을 다룰 때는 배열 연산자를 써야 해요." $in 연산자부터 살펴봅시다.

$in은 "이 중에 하나라도 있으면"이라는 의미입니다. { interests: { $in: ["JavaScript", "Python"] } }은 interests 배열에 JavaScript가 있거나 Python이 있는 문서를 모두 찾습니다.

마치 "사과 또는 배가 들어있는 바구니를 모두 가져오세요"라고 하는 것과 같습니다. $all 연산자는 조금 다릅니다.

$all은 "이것들이 모두 있어야"라는 의미입니다. { interests: { $all: ["JavaScript", "React"] } }은 JavaScript와 React가 둘 다 interests 배열에 포함된 문서만 찾습니다.

"사과와 배가 모두 들어있는 바구니만 가져오세요"인 셈이죠. $elemMatch는 조금 더 복잡한 상황에서 빛을 발합니다.

학생의 성적을 저장한다고 가정해봅시다. scores: [{ subject: "수학", score: 85 }, { subject: "영어", score: 70 }] 이런 구조로요.

"수학 점수가 80점 이상인 학생"을 찾고 싶을 때, 단순히 { "scores.subject": "수학", "scores.score": { $gte: 80 } }로 검색하면 문제가 생깁니다. 왜냐하면 이 쿼리는 "scores 배열에 subject가 수학인 요소가 있고, scores 배열에 score가 80 이상인 요소가 있는" 문서를 찾기 때문입니다.

수학이 60점이고 영어가 90점인 학생도 매칭될 수 있습니다. $elemMatch를 사용하면 "하나의 요소가 모든 조건을 동시에 만족"하는지 확인할 수 있습니다.

수학이면서 80점 이상인 요소가 있는 문서만 정확하게 찾아냅니다. $size 연산자도 알아두면 유용합니다.

배열의 크기로 검색할 때 사용합니다. { interests: { $size: 3 } }은 관심사가 정확히 3개인 사용자를 찾습니다.

다만 $size는 정확한 크기만 검색할 수 있고, "3개 이상" 같은 범위 검색은 지원하지 않습니다. 실무에서 배열 연산자는 태그 시스템, 권한 관리, 다중 선택 필터 등에서 자주 사용됩니다.

예를 들어 게시글의 태그 기능을 구현할 때, "JavaScript 또는 MongoDB 태그가 있는 게시글"을 찾거나, "초보자 그리고 무료 태그가 모두 있는 강의"를 찾는 데 활용됩니다. 주의할 점은 $in과 $or의 차이입니다.

같은 필드에서 여러 값 중 하나를 찾을 때는 $in이 더 효율적입니다. { interests: { $in: ["A", "B"] } }와 { $or: [{ interests: "A" }, { interests: "B" }] }는 같은 결과를 반환하지만, $in이 더 깔끔하고 성능도 좋습니다.

김개발 씨는 마케팅팀의 요청을 $in 연산자로 간단히 해결했습니다. "배열도 이렇게 쉽게 검색할 수 있다니!"

실전 팁

💡 - $in은 OR 조건, $all은 AND 조건으로 생각하면 이해하기 쉽습니다.

  • 배열 내 객체를 검색할 때는 $elemMatch를 사용해야 정확한 결과를 얻을 수 있습니다.
  • 배열 필드에는 인덱스를 걸어 검색 성능을 향상시킬 수 있습니다.

4. 정규표현식 검색

"이메일이 gmail.com으로 끝나는 사용자를 모두 찾아주세요." 운영팀의 요청을 받은 김개발 씨는 잠시 고민했습니다. 정확히 일치하는 값이 아니라 특정 패턴을 가진 값을 찾아야 하는 상황입니다.

SQL에서 LIKE 연산자를 쓰던 것처럼, MongoDB에서도 패턴 매칭이 가능할까요?

MongoDB는 **정규표현식(Regular Expression)**을 사용한 패턴 검색을 지원합니다. $regex 연산자를 사용하거나 /pattern/ 형태로 직접 정규표현식을 작성할 수 있습니다.

대소문자 구분 없이 검색하려면 $options: "i" 옵션을 추가합니다. SQL의 LIKE보다 훨씬 강력한 패턴 매칭이 가능합니다.

다음 코드를 살펴봅시다.

// gmail.com으로 끝나는 이메일 찾기
db.users.find({
  email: { $regex: "gmail\\.com$" }
})

// 이름이 '김'으로 시작하는 사용자
db.users.find({
  name: { $regex: "^김" }
})

// 대소문자 구분 없이 'javascript' 포함된 게시글
db.posts.find({
  title: {
    $regex: "javascript",
    $options: "i"  // case insensitive
  }
})

// 정규표현식 리터럴 사용
db.users.find({
  phone: /^010-\d{4}-\d{4}$/
})

// 특수문자가 포함된 검색 (이스케이프 필요)
db.products.find({
  name: { $regex: "C\\+\\+" }
})

김개발 씨는 운영팀의 요청을 받고 당황했습니다. "gmail.com으로 끝나는 이메일"을 찾으려면 모든 이메일을 일일이 비교해야 하는 걸까요?

그건 너무 비효율적입니다. 박시니어 씨가 힌트를 줍니다.

"MongoDB에서도 정규표현식을 쓸 수 있어요. SQL의 LIKE보다 훨씬 강력하죠." 정규표현식이란 무엇일까요?

정규표현식은 문자열의 패턴을 정의하는 특별한 문법입니다. 마치 "ㅇㅇㅇ-ㅇㅇㅇㅇ-ㅇㅇㅇㅇ" 형태의 전화번호를 찾는 것처럼, 구체적인 값이 아닌 패턴으로 검색할 수 있게 해줍니다.

MongoDB에서 정규표현식을 사용하는 방법은 두 가지입니다. 첫째, $regex 연산자를 사용하는 방법입니다.

{ email: { $regex: "gmail\.com$" } }처럼 작성합니다. 여기서 $는 "문자열의 끝"을 의미하고, \.은 점(.) 문자 자체를 의미합니다.

점은 정규표현식에서 "아무 문자"를 의미하는 특수문자이기 때문에 이스케이프 처리가 필요합니다. 둘째, 정규표현식 리터럴을 사용하는 방법입니다.

{ phone: /^010-\d{4}-\d{4}$/ }처럼 슬래시로 감싸서 작성합니다. 자바스크립트의 정규표현식 문법을 그대로 사용할 수 있어 편리합니다.

자주 사용되는 정규표현식 패턴을 알아봅시다. **^**는 문자열의 시작을 의미합니다.

{ name: { $regex: "^김" } }은 이름이 "김"으로 시작하는 사용자를 찾습니다. **$**는 문자열의 끝을 의미합니다.

"gmail.com$"은 gmail.com으로 끝나는 문자열을 찾습니다. $options 파라미터도 중요합니다.

"i" 옵션을 주면 대소문자를 구분하지 않습니다. { title: { $regex: "javascript", $options: "i" } }는 "JavaScript", "JAVASCRIPT", "javascript" 모두 매칭됩니다.

검색 기능을 구현할 때 거의 필수적으로 사용되는 옵션입니다. 특수문자 검색 시 주의할 점이 있습니다.

정규표현식에서 특별한 의미를 가진 문자들(. * + ?

^ $ [ ] { } | \ 등)을 검색하려면 백슬래시로 이스케이프해야 합니다. "C++"을 검색하려면 "C\+\+"로 작성해야 합니다.

이를 놓치면 예상치 못한 결과가 나올 수 있습니다. 실무에서 정규표현식은 검색 기능, 데이터 유효성 검사, 로그 분석 등에 널리 사용됩니다.

예를 들어 전화번호 형식이 올바른지 확인하거나, 특정 도메인의 이메일을 가진 사용자를 찾거나, 특정 키워드가 포함된 로그를 검색하는 데 활용됩니다. 하지만 정규표현식에도 단점이 있습니다.

정규표현식 검색은 일반 검색보다 느릴 수 있습니다. 특히 ^로 시작하지 않는 패턴(예: "gmail"만 포함)은 인덱스를 활용하기 어렵습니다.

대용량 데이터에서는 성능에 주의해야 합니다. 김개발 씨는 정규표현식으로 운영팀의 요청을 깔끔하게 처리했습니다.

"패턴 검색이 이렇게 쉬웠다니!"

실전 팁

💡 - ^로 시작하는 패턴은 인덱스를 활용할 수 있어 성능이 좋습니다.

  • 대소문자 구분 없이 검색하려면 $options: "i"를 꼭 추가하세요.
  • 사용자 입력을 정규표현식에 직접 넣을 때는 특수문자 이스케이프를 잊지 마세요.

5. projection으로 필드 선택

김개발 씨가 사용자 목록 API를 만들고 있습니다. 그런데 사용자 문서에는 비밀번호 해시, 개인정보 등 민감한 정보도 많이 들어있습니다.

"필요한 필드만 골라서 가져올 수는 없을까?" 모든 필드를 다 가져온 뒤 코드에서 필터링하는 건 비효율적으로 느껴졌습니다.

Projection은 쿼리 결과에서 원하는 필드만 선택하여 가져오는 기능입니다. SQL의 SELECT 절처럼 필요한 컬럼만 지정할 수 있습니다.

1은 해당 필드를 포함하고, 0은 제외한다는 의미입니다. 네트워크 트래픽을 줄이고 보안을 강화하는 데 필수적인 기능입니다.

다음 코드를 살펴봅시다.

// 이름과 이메일만 가져오기 (포함)
db.users.find(
  {},  // 모든 문서
  { name: 1, email: 1 }  // projection
)

// 비밀번호와 주민번호 제외하고 가져오기 (제외)
db.users.find(
  {},
  { password: 0, ssn: 0 }
)

// 조건과 projection 함께 사용
db.users.find(
  { status: "active" },
  { name: 1, email: 1, grade: 1 }
)

// _id 필드 제외하기 (기본적으로 _id는 항상 포함됨)
db.users.find(
  {},
  { name: 1, email: 1, _id: 0 }
)

// 중첩된 필드 선택
db.users.find(
  {},
  { name: 1, "address.city": 1 }
)

김개발 씨는 사용자 목록 API를 개발하고 있었습니다. 테스트를 해보니 응답 데이터가 너무 컸습니다.

사용자 문서에는 이름, 이메일 외에도 비밀번호 해시, 주민번호, 상세 주소 등 수십 개의 필드가 있었기 때문입니다. "프론트엔드에서는 이름이랑 이메일만 필요한데, 이걸 다 보내야 하나?" 김개발 씨가 혼잣말을 하자 박시니어 씨가 끼어들었습니다.

"Projection을 쓰면 필요한 필드만 가져올 수 있어요." Projection이란 무엇일까요? 프로젝션은 마치 스포트라이트와 같습니다.

무대 전체를 비추는 대신, 원하는 배우에게만 조명을 비추는 것처럼, 문서에서 원하는 필드만 선택적으로 가져올 수 있습니다. find() 메서드의 두 번째 인자로 projection 객체를 전달합니다.

{ name: 1, email: 1 }처럼 가져오고 싶은 필드에 1을 지정하면, 해당 필드들만 결과에 포함됩니다. 반대로 { password: 0, ssn: 0 }처럼 0을 지정하면, 해당 필드들을 제외한 나머지가 반환됩니다.

중요한 규칙이 하나 있습니다. 포함(1)과 제외(0)를 동시에 사용할 수 없습니다.

{ name: 1, password: 0 }처럼 섞어서 쓰면 오류가 발생합니다. 단, _id 필드는 예외입니다.

_id는 기본적으로 항상 포함되기 때문에, 다른 필드를 포함할 때 _id만 제외하는 것은 허용됩니다. { name: 1, _id: 0 }은 유효한 projection입니다.

중첩된 필드도 선택할 수 있습니다. address 객체 안에 city, street, zipcode 등이 있을 때, "address.city"만 가져오고 싶다면 { "address.city": 1 }로 지정합니다.

점 표기법을 사용해서 깊이 중첩된 필드도 접근할 수 있습니다. 실무에서 projection은 여러 상황에서 중요합니다.

첫째, 보안 측면에서 민감한 정보(비밀번호, 개인정보 등)가 실수로 클라이언트에 노출되는 것을 방지합니다. 둘째, 성능 측면에서 네트워크 트래픽을 줄이고 응답 속도를 개선합니다.

수십 개 필드 중 3개만 필요하다면, 나머지를 전송하지 않는 것이 효율적입니다. API 설계 시 projection을 적극 활용하세요.

목록 조회 API에서는 요약 정보만, 상세 조회 API에서는 전체 정보를 반환하는 패턴이 일반적입니다. 같은 컬렉션이라도 상황에 따라 다른 projection을 적용하는 것이죠.

주의할 점도 있습니다. Projection은 서버에서 필드를 필터링하므로 성능에 도움이 되지만, 문서를 먼저 읽은 후에 필드를 걸러내는 방식입니다.

즉, 디스크 I/O 자체는 줄어들지 않을 수 있습니다. 정말 큰 필드(대용량 텍스트나 바이너리)가 있다면 별도 컬렉션으로 분리하는 것도 고려해보세요.

김개발 씨는 projection을 적용하여 API 응답 크기를 80%나 줄였습니다. "보안도 챙기고 성능도 챙기고, 일석이조네요!"

실전 팁

💡 - API에서는 기본적으로 민감한 필드를 제외하는 습관을 들이세요.

  • 포함(1)과 제외(0)를 혼용하면 안 되지만, _id: 0은 예외적으로 허용됩니다.
  • 목록 조회와 상세 조회에 다른 projection을 적용하여 효율성을 높이세요.

6. 정렬 제한 건너뛰기

"게시글을 최신순으로 10개만 보여주세요. 아, 페이지네이션도 구현해야 해요." 프론트엔드 개발자의 요청을 받은 김개발 씨는 고민에 빠졌습니다.

데이터베이스에서 정렬하고, 개수를 제한하고, 페이지별로 건너뛰는 기능은 어떻게 구현할까요?

MongoDB는 sort(), limit(), skip() 메서드로 결과를 정렬하고 페이지네이션을 구현합니다. **sort()**는 오름차순(1) 또는 내림차순(-1)으로 정렬하고, **limit()**은 반환할 문서 수를 제한하며, **skip()**은 앞에서부터 지정한 수만큼 건너뜁니다.

이 세 가지를 조합하면 페이지네이션을 쉽게 구현할 수 있습니다.

다음 코드를 살펴봅시다.

// 최신순 정렬 (내림차순)
db.posts.find().sort({ createdAt: -1 })

// 오래된 순 정렬 (오름차순)
db.posts.find().sort({ createdAt: 1 })

// 여러 필드로 정렬 (인기순 -> 최신순)
db.posts.find().sort({ likes: -1, createdAt: -1 })

// 상위 10개만 가져오기
db.posts.find().sort({ createdAt: -1 }).limit(10)

// 페이지네이션: 2페이지 (한 페이지당 10개)
const page = 2
const pageSize = 10
db.posts.find()
  .sort({ createdAt: -1 })
  .skip((page - 1) * pageSize)  // 10개 건너뛰기
  .limit(pageSize)              // 10개 가져오기

// 조건 + 정렬 + 제한 조합
db.posts.find({ status: "published" })
  .sort({ views: -1 })
  .limit(5)

김개발 씨는 게시판 API를 개발하고 있었습니다. 프론트엔드 팀에서 구체적인 요구사항을 전달해왔습니다.

"게시글을 최신순으로 정렬해서 한 페이지에 10개씩 보여주세요. 페이지 번호를 클릭하면 해당 페이지의 게시글이 나와야 해요." 아주 흔한 요구사항이지만, MongoDB에서 어떻게 구현해야 할지 막막했습니다.

박시니어 씨가 차근차근 설명해줍니다. "sort, limit, skip 세 가지만 알면 돼요." sort() 메서드부터 알아봅시다.

정렬 방향은 1(오름차순)과 -1(내림차순)로 지정합니다. { createdAt: -1 }은 생성일 기준 내림차순, 즉 최신순입니다.

{ createdAt: 1 }은 오래된 순이 됩니다. 숫자뿐 아니라 문자열도 정렬할 수 있는데, 문자열은 알파벳/가나다 순으로 정렬됩니다.

여러 필드로 정렬할 수도 있습니다. { likes: -1, createdAt: -1 }은 먼저 좋아요 수로 내림차순 정렬하고, 좋아요 수가 같으면 최신순으로 정렬합니다.

쇼핑몰의 "인기순" 정렬이나 검색 엔진의 "관련순" 정렬에서 이런 다중 정렬을 많이 사용합니다. limit() 메서드는 반환할 문서 수를 제한합니다.

limit(10)은 최대 10개의 문서만 반환합니다. "TOP 10", "최근 게시글 5개" 같은 기능을 구현할 때 필수입니다.

성능 측면에서도 중요한데, 수만 건의 데이터 중 필요한 만큼만 가져오므로 응답 시간이 크게 단축됩니다. skip() 메서드는 앞에서부터 지정한 수만큼 건너뜁니다.

skip(10)은 처음 10개를 건너뛰고 그 다음 문서부터 반환합니다. 페이지네이션의 핵심이 바로 이 skip입니다.

페이지네이션 공식을 살펴봅시다. 페이지 번호가 page이고 한 페이지당 문서 수가 pageSize일 때, skip((page - 1) * pageSize).limit(pageSize)를 사용합니다.

예를 들어 2페이지를 보려면 skip(10).limit(10)으로 처음 10개를 건너뛰고 그 다음 10개를 가져옵니다. 하지만 skip에는 성능 문제가 있습니다.

skip은 내부적으로 건너뛸 문서들도 모두 읽습니다. skip(10000)이면 10000개를 읽고 버린 후 원하는 문서를 반환하는 것입니다.

데이터가 많아지면 페이지 번호가 높을수록 느려집니다. 대안으로 커서 기반 페이지네이션이 있습니다.

마지막으로 본 문서의 _id나 timestamp를 기준으로 "이 값보다 작은 것" 중 N개를 가져오는 방식입니다. { _id: { $lt: lastId } }처럼요.

skip 없이 인덱스를 활용하므로 데이터가 많아도 일정한 성능을 유지합니다. 실무에서 이 세 가지 메서드는 거의 항상 함께 사용됩니다.

find().sort().skip().limit() 순서로 체이닝하는 패턴을 기억하세요. 조건 검색, 정렬, 페이지네이션을 한 번에 처리할 수 있습니다.

김개발 씨는 페이지네이션을 깔끔하게 구현했습니다. "sort, limit, skip만 알면 어떤 목록 API도 만들 수 있겠네요!"

실전 팁

💡 - 정렬에 사용되는 필드에는 반드시 인덱스를 생성하세요. 성능 차이가 큽니다.

  • 대용량 데이터에서는 skip 대신 커서 기반 페이지네이션을 고려하세요.
  • sort().limit() 조합은 "TOP N" 쿼리에 최적화되어 있어 매우 빠릅니다.

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#MongoDB#QueryOperators#Database#NoSQL#DataFiltering#MongoDB,Database,NoSQL

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.