쿼리 vs. 업데이트 성능: 효율적인 쓰기 작업 선택

쿼리와 쓰기 작업 비용을 비교하여 MongoDB 성능을 마스터하세요. 이 가이드는 MongoDB 쓰기 고려 사항이 내구성과 처리량을 어떻게 결정하는지 자세히 설명하고, 빠른 내부 문서 업데이트와 느린 문서 재작성의 중요한 차이를 설명합니다. 애플리케이션의 I/O 효율성을 최적화하고 데이터 요구 사항에 맞는 올바른 승인 수준을 선택하기 위한 실행 가능한 전략을 알아보세요.

쿼리 vs. 업데이트 성능: 효율적인 쓰기 작업 선택

MongoDB 쓰기 성능은 서버가 데이터를 얼마나 빨리 수용할 수 있는지에 관한 것만이 아닙니다. 쓰기의 형태, 유지해야 하는 인덱스, 영향을 받는 문서, 클라이언트가 기다리는 승인, 그리고 동일한 레코드가 동시에 많은 요청에 의해 처리되는지 여부에 관한 것입니다.

읽기와 쓰기는 서로 다른 방식으로 실패합니다. 잘못된 읽기는 종종 너무 많이 스캔합니다. 잘못된 업데이트는 먼저 스캔한 다음, 증가하는 문서를 다시 쓰고, 여러 인덱스를 업데이트하고, 복제를 기다리며, 동일한 핫 레코드에서 다른 작업을 차단할 수 있습니다. 이것이 올바른 쓰기 작업을 선택하는 것이 중요한 이유입니다.

핵심 트레이드오프: 읽기 속도 vs. 쓰기 내구성

모든 데이터베이스 시스템에서 데이터 안전(내구성) 보장과 높은 트랜잭션 속도(처리량) 달성 사이에는 본질적인 긴장이 있습니다. MongoDB는 쓰기 성능과 관련된 두 가지 주요 메커니즘인 쓰기 고려 사항과 쓰기 작업 자체의 유형(예: 단순 삽입 대 복잡한 업데이트)을 통해 이를 관리합니다.

쓰기 고려 사항 이해

쓰기 고려 사항은 MongoDB가 쓰기 작업을 성공으로 간주하기 전에 애플리케이션에 필요한 승인 수준을 정의합니다. 더 엄격한 쓰기 고려 사항은 내구성을 높이지만 클라이언트가 확인을 위해 더 오래 기다려야 하므로 쓰기 처리량이 감소하는 경우가 많습니다.

쓰기 고려 사항 수준 설명 내구성 지연 시간/처리량 영향
0 (Fire and Forget) 승인 필요 없음. 가장 낮음 가장 높은 처리량, 가장 낮은 지연 시간
majority 복제 세트 멤버 과반수의 쓰기 승인. 높음 적당한 지연 시간, 좋은 처리량
w: 'all' 모든 복제 세트 멤버의 쓰기 승인. 가장 높음 가장 높은 지연 시간, 가장 낮은 처리량

실용적인 예: 쓰기 고려 사항 설정

문서를 삽입할 때 드라이버 수준에서 쓰기 고려 사항을 설정합니다:

const options = { writeConcern: { w: 'majority', wtimeout: 5000 } };

db.collection('logs').insertOne({ message: "Critical Event" }, options, (err, result) => {
  // 과반수 확인 후에만 작업 완료
});

모범 사례: 가끔 손실이 허용되는 대용량 로깅 또는 중요하지 않은 데이터의 경우 w: 0을 사용하면 승인 지연 시간을 줄일 수 있지만, 비정상 종료 시 데이터 손실 위험이 있습니다.

쿼리 성능 특성

읽기(쿼리)는 일반적으로 본질적으로 내구성에 영향을 미치지 않으며 순수하게 검색 속도에 중점을 둡니다. 쿼리 성능은 주로 다음에 의해 결정됩니다:

  1. 인덱싱: 적절한 인덱싱은 가장 중요한 단일 요소입니다. 인덱스를 사용하는 쿼리는 거의 항상 컬렉션 스캔보다 성능이 뛰어납니다.
  2. 데이터 검색 크기: 더 적은 필드나 더 작은 문서를 가져오면 네트워크 전송 및 메모리 사용 속도가 빨라집니다.
  3. 쿼리 복잡성: 특히 $lookup(조인) 또는 많은 $group 작업을 포함하는 집계 파이프라인은 상당한 CPU 시간과 메모리를 필요로 하여 전체 서버 응답성에 영향을 미칩니다.

예: 효율적인 쿼리 구조

쿼리 조건자에서 항상 인덱싱된 필드를 선호하세요:

// 'status' 필드가 인덱싱되었다고 가정
db.items.find({ status: 'active', lastUpdated: { $gt: yesterday } }).limit(100);

업데이트 성능 영향

업데이트는 기본적으로 쓰기 작업이며 삽입과 동일한 내구성 고려 사항이 적용됩니다. 그러나 업데이트는 문서 구조나 크기를 수정하는지 여부에 따라 복잡성이 발생합니다.

내부 업데이트 vs. 재작성

MongoDB는 가능할 때마다 내부에서 업데이트를 수행하려고 시도합니다. 문서의 디스크 위치가 변경되지 않기 때문에 내부 업데이트는 훨씬 빠릅니다. 이는 다음과 같은 경우 가능합니다:

  1. 업데이트된 필드가 문서의 현재 할당된 저장 공간을 초과하지 않는 경우.
  2. 업데이트 작업이 내부 재구성이 필요한 방식으로 문서 크기를 변경하지 않는 경우.

업데이트로 인해 문서가 현재 할당된 공간보다 커지면 MongoDB는 문서를 디스크의 새 위치로 재작성해야 합니다. 이 재작성 작업은 상당한 I/O 오버헤드를 생성하고 문서를 더 오랜 시간 동안 잠가 동시성이 높은 시나리오에서 성능을 심각하게 저하시킵니다.

재작성 최소화

업데이트를 최적화하려면:

  • 공간 사전 할당: 특정 필드가 크게 증가할 것이라는 것을 알고 있다면(예: 배열에 요소 추가), 초기에 충분한 공간을 확보하기 위해 해당 필드를 플레이스홀더 데이터로 초기화하는 것을 고려하세요.
  • 과도한 업데이트 방지: 문서 크기가 자주 조정되는 경우, 참조로 연결된 별도의 더 작은 문서를 사용하도록 스키마를 재구성하는 것을 고려하세요.

업데이트 수정자와 속도

다른 업데이트 연산자는 서로 다른 성능 비용을 수반합니다:

  • 원자적 연산 ($set, $inc): 일반적으로 내부 업데이트로 이어지면 빠릅니다.
  • 배열 조작 ($push, $addToSet): 배열 증가로 인해 문서 재작성이 반복적으로 발생하면 특히 느릴 수 있습니다.
  • 문서 교체 (replaceOne): 전체 문서를 교체하는 것은(replaceOne 또는 전체 문서를 덮어쓰는 findAndModify와 함께 { upsert: true, multi: false } 사용) 재작성을 강제하므로 신중하게 사용해야 하며, 업데이트가 필요한 이전 위치를 가리키는 기존 인덱스를 무효화합니다.

쿼리 vs. 쓰기 성능 비교

쿼리는 일반적으로 내구성 오버헤드를 피하기 때문에 쓰기보다 빠르지만, 비교는 미묘합니다:

작업 유형 주요 성능 동인 내구성 오버헤드 최악의 시나리오
쿼리 (읽기) 인덱스 효율성, 네트워크 지연 시간. 없음 (오래된 복제본에서 읽지 않는 한). 인덱스 누락으로 인한 전체 컬렉션 스캔.
업데이트 (쓰기) 쓰기 고려 사항 확인, 내부 vs. 재작성. 높음 (w 설정에 따라 다름). 클러스터 전체에서 빈번한 문서 재작성.

실행 가능한 통찰력: 애플리케이션이 쓰기 바운드인 경우 먼저 업데이트 필터, 핫 문서, 문서 증가 및 인덱스 유지 관리를 확인하세요. 쓰기 고려 사항은 유용한 레버이지만 내구성을 낮추는 것은 제품 결정이지 반사적인 행동이 되어서는 안 됩니다.

쓰기 고려 사항뿐만 아니라 쓰기 형태 선택

쓰기 고려 사항은 MongoDB가 클라이언트에 쓰기가 승인되었음을 알리는 시점을 제어합니다. 비효율적인 업데이트 패턴을 수정하지는 않습니다. 두 쓰기가 동일한 w: "majority" 설정을 사용하더라도 하나는 작은 필드를 건드리고 다른 하나는 핫 문서 내에서 큰 배열을 계속 증가시키기 때문에 비용이 매우 다를 수 있습니다.

일반적인 예는 계속 증가하는 events 배열이 있는 사용자 문서입니다:

db.users.updateOne(
  { _id: userId },
  { $push: { events: { type: "login", at: new Date() } } }
)

처음에는 편리합니다. 나중에는 사용자 문서가 커지고, 모든 로그인이 동일한 문서를 변경하며, 업데이트가 사용자 프로필 읽기와 경쟁하기 시작합니다. 더 나은 모델은 종종 별도의 user_events 컬렉션입니다:

db.user_events.insertOne({
  userId,
  type: "login",
  at: new Date()
})

이제 프로필 문서는 작게 유지되고 이벤트 쓰기는 하나의 증가하는 문서를 반복적으로 수정하는 대신 새 문서를 추가합니다. 최근 활동 화면을 위해 { userId: 1, at: -1 }을 인덱싱하고 데이터가 영구적이지 않은 경우 TTL 인덱스로 오래된 이벤트를 만료시킬 수 있습니다.

또 다른 패턴은 카운터입니다. 모든 요청이 하나의 전역 문서를 증가시키면 해당 문서는 쓰기 핫스팟이 됩니다:

db.metrics.updateOne(
  { _id: "page_views" },
  { $inc: { count: 1 } },
  { upsert: true }
)

트래픽이 적은 경우 괜찮습니다. 트래픽이 많은 경우 분당, 테넌트, 경로 또는 샤드 키당 하나의 문서와 같은 버킷 카운터를 사용하세요. 약간의 읽기 시간 집계를 훨씬 더 나은 쓰기 분배와 교환하는 것입니다.

db.metrics.updateOne(
  { metric: "page_views", minute: "2026-05-24T10:31Z" },
  { $inc: { count: 1 } },
  { upsert: true }
)

Upsert는 특별한 주의가 필요합니다. Upsert는 먼저 일치하는 문서를 찾아야 합니다. 필터가 인덱싱되지 않은 경우 쓰기 경로는 읽기 스캔과 쓰기로 바뀝니다. 예를 들어 멱등성 결제 콜백의 경우 고유 인덱스 키를 원합니다:

db.payment_events.createIndex({ providerEventId: 1 }, { unique: true })

db.payment_events.updateOne(
  { providerEventId },
  { $setOnInsert: { providerEventId, receivedAt: new Date(), payload } },
  { upsert: true }
)

이렇게 하면 컬렉션을 스캔하거나 중복 레코드를 생성하지 않고 재시도를 안전하게 할 수 있습니다. 또한 애플리케이션에 중복 키 경합을 처리하는 깔끔한 방법을 제공합니다.

대량 쓰기는 또 다른 유용한 레버입니다. 10,000개의 상태 변경을 가져오는 경우 업데이트당 하나의 네트워크 왕복은 일반적으로 낭비입니다. bulkWrite를 사용하면 배치를 보낼 수 있으며, 순서가 없는 배치는 작업에 허용되는 경우 개별 실패 후에도 계속될 수 있습니다.

db.orders.bulkWrite(
  updates.map(({ id, status }) => ({
    updateOne: {
      filter: { _id: id },
      update: { $set: { status, updatedAt: new Date() } }
    }
  })),
  { ordered: false }
)

속도를 쫓아 쓰기 고려 사항을 무턱대고 완화하지 마십시오. majority에서 w: 1로 이동하면 지연 시간이 줄어들 수 있지만 장애 조치 중에 발생할 수 있는 상황도 변경됩니다. w: 0으로 이동하면 클라이언트가 쓰기가 전혀 실패했는지 알 수 없음을 의미합니다. 이는 일회성 원격 측정에는 허용될 수 있습니다. 주문, 계정 변경 또는 사용자가 확인되기를 기대하는 모든 항목에는 좋지 않은 선택입니다.

더 나은 질문은 다음과 같습니다: 쓰기를 더 작고, 더 목표 지향적이며, 덜 경쟁하고, 재시도하기 쉽게 만들 수 있습니까? 하나의 필드만 변경된 경우 전체 문서를 교체하는 대신 $set, $inc, $unset$setOnInsert를 사용하세요. 자주 업데이트되는 문서에서 무제한 배열을 유지하지 마십시오. 읽기 필터뿐만 아니라 업데이트 필터에 대한 인덱스를 추가하십시오. 중복 요청이 중복 효과를 생성하지 않도록 고유 키를 중심으로 재시도를 설계하십시오.

자신을 속이지 않고 쓰기 성능 측정

빈 로컬 데이터베이스에 작은 문서를 삽입하는 벤치마크는 프로덕션 쓰기 성능에 대해 많은 것을 알려주지 않습니다. 실제 쓰기는 인덱스, 복제, 저널링, 백그라운드 작업 및 다른 클라이언트와 경쟁합니다. 업데이트가 많은 경로를 테스트하는 경우 실제 문서처럼 보이는 문서와 프로덕션과 일치하는 인덱스에 대해 테스트를 실행하십시오.

애플리케이션 지연 시간, MongoDB 명령 기간, 복제 지연 및 쓰기 오류 또는 시간 초과의 네 가지 숫자를 최소한 추적하십시오. 평균 지연 시간을 개선하지만 복제 지연을 생성하는 변경은 단순히 고통을 세컨더리로 옮기는 것일 수 있습니다. w: 1에서 빠르게 보이는 변경은 제품이 실제로 필요로 하는 내구성 요구 사항을 충족하지 못할 수 있습니다.

인덱스는 쓰기 비용의 일부입니다. 인덱싱된 필드를 변경하는 모든 삽입 또는 업데이트는 관련 인덱스 항목을 업데이트해야 합니다. 이것이 인덱스가 나쁘다는 것을 의미하지는 않습니다. 사용되지 않는 인덱스가 무료가 아니라는 것을 의미합니다. 수년간의 기능 작업 동안 생성된 많은 인덱스가 있는 컬렉션이 있는 경우 실제 쿼리를 여전히 지원하는지 검토하십시오. 사용되지 않는 인덱스를 삭제하면 쓰기 속도가 향상되고 저장 공간이 줄어들 수 있지만 쿼리 로그를 확인하고 롤백 계획을 테스트한 후 신중하게 수행하십시오.

일반적인 애플리케이션 작업을 위한 작업 선택

프로필 편집 양식의 경우 사용자가 변경한 필드에 $set을 사용하십시오. 오래된 클라이언트 복사본에서 전체 사용자 문서를 교체하지 마십시오. 다른 프로세스에 의해 추가된 필드를 실수로 지울 수 있기 때문입니다.

재고 예약의 경우 조건부 업데이트를 사용하여 확인과 변경이 함께 발생하도록 하십시오:

db.inventory.updateOne(
  { sku, available: { $gte: quantity } },
  { $inc: { available: -quantity, reserved: quantity } }
)

그런 다음 matchedCountmodifiedCount를 확인하십시오. 이렇게 하면 두 클라이언트가 동일한 사용 가능한 수를 읽고 둘 다 예약할 수 있다고 결정하는 경합을 방지할 수 있습니다.

소프트 삭제의 경우 deletedAt 필드를 $set하고 일반 읽기가 이를 필터링하는지 확인하십시오. 활성 레코드를 자주 쿼리하는 경우 해당 필드를 관련 인덱스에 포함시키십시오. 대량 하드 삭제의 경우 배치로 삭제하여 워크로드의 나머지 부분을 방해하는 장기 실행 작업을 만들지 마십시오.

백그라운드 마이그레이션의 경우 체크포인트가 있는 작은 배치를 선호하십시오. 단일 대규모 updateMany는 간단할 수 있지만 복제 압력을 생성하고 롤백을 더 어렵게 만들 수 있습니다. 한 번에 1,000개 또는 5,000개의 문서를 업데이트하고, 진행 상황을 기록하며, 복제 지연이 증가할 때 대기하는 마이그레이션은 덜 극적이고 일반적으로 더 안전합니다.

이러한 경우 패턴은 동일합니다: 데이터베이스가 하나의 정확한 원자적 변경을 수행하도록 하고, 재시도를 안전하게 만들며, 핫 문서를 영원히 증가시키지 마십시오.

실용적인 마무리 참고 사항: 성능 튜닝 전략

MongoDB에서 효율적인 쓰기 작업을 선택하는 것은 애플리케이션 요구 사항을 데이터베이스 기능과 일치시키는 데 달려 있습니다. 높은 내구성 요구 사항(w: 'all' 사용)은 본질적으로 높은 처리량 요구 사항(w: 0 사용)보다 느립니다. 동시에 개발자는 할당된 저장 공간을 초과하는 업데이트로 인해 문서가 디스크에 다시 쓰도록 강제하여 발생하는 성능 저하를 방지해야 합니다.

데이터 중요성에 따라 쓰기 고려 사항을 신중하게 선택하고 내부 수정을 선호하도록 업데이트를 구성함으로써 현대 애플리케이션의 높은 동시성 요구 사항과 강력한 데이터 지속성을 효과적으로 균형을 맞출 수 있습니다.