Backend/Express

[Express] mongoose 로 쿼리할 때 lean을 이용해 메모리 사용량 줄이기

Satisfaction 2024. 1. 8. 20:09

 

 

const normalDoc = await MyModel.findOne();

 

mongoose를 이용하여 쿼리할 때 위와 같은 형태로 쿼리한다.

 

https://mongoosejs.com/docs/index.html

 

mongoose 의 quick start에서도 위와 같이 소개하고 있어 계속 이렇게 사용해왔지만,

클러스터링까지 사용하게 되면서 cpu, memory 사용량이 증가하여 조치를 취해야만 한다.

 

쿼리 시 어떤 데이터를 반환하는가?

const normalDoc = await MyModel.findOne();

 

이 코드를 사용했을 때 normalDoc에는 어떤 데이터가 할당될까?

 

util.inspect와 console.log를 이용해 normalDoc을 찍어보면

{
  _id: 659bc8cc415b117ab2379544,
  cacheDate: 2024-01-08T10:05:00.325Z,
  key: 'officialInfo',
  data: {
    notices: [ [Object], [Object], [Object], [Object], [Object] ],
    magazines: [ [Object], [Object], [Object] ],
    aces: [ [Object], [Object], [Object], [Object] ]
  },
  __v: 0
}

 

이런 식으로 되어있다.

 

__v 빼고는 내가 저장한 데이터이므로 그다지 메모리에 부담을 주지는 않는 것 같다.

 

과연 그럴까?

 

const normalDoc = await MyModel.findOne();

const v8= require('v8');

const getByteLength = (obj) => Buffer.byteLength(JSON.stringify(obj));
const getUsedMem= (obj)=> v8.serialize(target).length;

const byteLength= getByteLength(normalDoc);
const usedMem= getUsedMem(normalDoc);

console.log({byteLength, usedMem}); //{ usedMem: 3077, byteLength: 2634 }

 

normalDoc를 json으로 취급했을 때와 실제로 얼마나 메모리를 사용하는지를 확인해 보면 꽤 차이가 난다.

 

분명 console.log로 찍어보거나 JSON.stringify를 사용해 확인해보면 동일한 값을 내놓는데 어떻게 된 것일까?

 

normalDoc이 plain object가 아니기 때문이다.

 

console.log(normalDoc.__proto__);

//printed...
Model {
  db: NativeConnection {
    base: Mongoose {
      connections: [Array],
      models: [Object],
      modelSchemas: [Object],
      events: [EventEmitter],
      options: [Object],
      _pluralize: [Function: pluralize],
      Schema: [Function],
      model: [Function (anonymous)],
      plugins: [Array]
    },
    collections: {
    ...

 

normalDoc의 __proto__를 출력해 보면 알 수 있듯, 

쿼리가 반환하는 결과가 plain object가 아닌 Model 이라는 생성자 함수로부터 생성된 인스턴스인 것이다.

 

이는 단순히 메모리를 더 잡아먹는 것 뿐만이 아니라 인스턴스를 생성하기 위한 추가적인 cpu 자원까지 사용한다.

 

왜 plain object를 리턴하지 않을까?

mongoose에서는 쿼리 결과를 가지고 데이터를 추가하거나, 변경하거나, 저장하는 등의 행위가 가능하다.

 

이는 쿼리 결과가 plain object가 아닌, 다양한 유틸 함수와 데이터들을 가지고 있기 때문에 가능하다.

 

그러나 단순히 쿼리하고, 그 값을 사용할 뿐이라면 이것은 컴퓨팅 자원을 낭비하는 것이다.

 

추가 기능이 필요 없는 경우, 빠르고 작은 쿼리 결과 반환을 위해 lean() 을 사용하자

lean()을 사용하면 다음과 같이 쿼리 결과를 plain object로 반환한다.

 

const leanDoc = await MyModel.findOne().lean();

const v8= require('v8');

const getByteLength = (obj) => Buffer.byteLength(JSON.stringify(obj));
const getUsedMem= (obj)=> v8.serialize(target).length;

const byteLength= getByteLength(leanDoc);
const usedMem= getUsedMem(leanDoc);

console.log({byteLength, usedMem}); //{ usedMem: 2373, byteLength: 2634 }

 

실제로 leanDoc은 인스턴스가 아닌 plain object이기 때문에 __proto__가 비어있다.

 

그래서 usedMem값이 3077-> 2373으로 약 30%정도 경량화되었다.

 

따라서 불필요한 연산과 메모리 사용이 제거되어 훨씬 가볍고 빠르게 동작한다.

 

https://mongoosejs.com/docs/tutorials/lean.html

 

Mongoose v8.0.3: Mongoose Tutorials: Faster Mongoose Queries With Lean

Faster Mongoose Queries With Lean The lean option tells Mongoose to skip hydrating the result documents. This makes queries faster and less memory intensive, but the result documents are plain old JavaScript objects (POJOs), not Mongoose documents. In this

mongoosejs.com

 

얼마나 줄어들었을까?

 

CPU에는 별 차이가 없지만 MEM에서는 평균 25%-> 15%정도로 10%p 감소하였다.

 

프로세스 2개를 돌리고 있으므로, 개당 5%p가 감소하였으며 이는 약 100MB 정도이다.

 

다시 치솟지 않고 평균 MEM 15%를 유지 중이다.