Back

MongoDB sequence 구현

MongoDB에는 내장 시퀀스 기능이 없다. 확장 가능하고 분산된 DB 설계 때문에, 그리고 개발자가 특정 요구 사항에 따라 다양한 방식으로 시퀀스 생성을 구현할 수 있는 유연한 데이터 모델을 제공하기 위해서라고 한다.

Triggers Treats and Tricks - Auto-Increment a Running ID Field | MongoDB

공식 문서에서 추천하는 시퀀스 구현은 위의 링크를 참고했다. counters collection을 만들고, 내부에 _id로 시퀀스 아이디를 만들어 조회와 동시에 시퀀스를 하나씩 늘려 관리하는 방법이다.

이 내용을 참고하여 db.collection.findOneAndUpdate()으로 시퀀스 관리를 했다.

시퀀스가 필요한 데이터를 생성할 때 내부적으로 사용할 시퀀스 아이디를 받아 함수를 따로 만들어 사용을 했다. 코드는 mongoose Schema를 사용한다.

const Counter = require('../models/counter');

exports.getNextSequence = async name => {
  const ret = await Counter.findOneAndUpdate(
    { _id: name },
    { $inc: { seq: 1 } },
    { new: true, upsert: true },
  );
  return ret.seq;
};

그런데 문득 document 생성 시점에 db.collection.countDocuments()로 조회 후 1을 더해 사용하면 어떨까 생각을 했다.

counters collection을 통해 따로 관리하는 것 보다 좋을 것 같은 이유들이 있었다.

  • 별도의 counters collection으로 시퀀스를 관리하면, 시퀀스가 포함된 collection 들이 counters collection을 의존한다. 개발을 진행하며 혹은 프로덕션 환경에서도 데이터의 수정이 일어나면 관련 시퀀스도 함께 수정해야하는 상황이 생길 수 있다. 개발을 진행하는 도중에는 collection 초기화 하는 경우가 많은데, 깔끔한 채번을 위해서는 매번 시퀀스도 같이 초기화를 해줘야한다.
  • 먼저 시퀀스를 조회해오고 insertOne()할 때, mongoose에서는 create() , insert 내에서 오류가 생겼을 때도 시퀀스는 1 늘어난다. (이는 트랜잭션 처리를 하거나, 예외처리를 완벽하게 하면 해결은 가능하나 countDocuments()로 관리하면 이런 상황이 생기지 않음)

그냥 countDocuments()를 사용하면, 위와 같은 데이터 불일치가 일어날 수 없다.

  • collection으로 별도로 관리하면 조회와 수정을 해야하나, countDocuments()의 경우에는 조회만 하기 때문에 리소스 비용이 더 낮을 것 같은 느낌이 든다.

그런데 또 느낌상 countDocuments()는 documents들이 많아질수록 점점 리소스 비용이 많아질 것 같다.

앞의 두가지 이유는 어느정도 개발이 안정이 된다면, 더이상 신경쓰지 않아도 될 문제다.

그런데 배포 이후를 생각했을 때는 마지막에 작성했던 성능에 대한 내용이 가장 중요했다. 확실한 검증이 필요했고, 뭐가 더 괜찮은 방법인지 찾아보게 되었다.


countDocuments()는 내장된 MongoDB 함수로 성능에 최적화 되어있다고는 한다. 별도의 counters collection을 사용했을 때, 현재 시퀀스 값을 가져오려면 추가 쿼리가 필요하기에 더 성능이 좋지 않다고 한다.

그런데 이는 생각했던대로 documents의 수가 많으면 적을 때와 비교했을 때 countDocuments()실행에서 성능 저하가 있다고 한다.

index를 통해 어느정도 카운트의 성능을 향상시킬 수는 있으나, 일반적으로 큰 규모의 collection의 경우에는 시퀀스 생성을 위해 저런 방법은 쓰지 않는 것 같다.

어느 정도의 규모부터 어떤 방법이 좋은지 정확하게 측정하는 방법은 잘 모르겠으나, 확장성을 생각했을 때는 기존의 별도의 counters collection을 사용하는 방법이 좋은 것 같다.

또한 시퀀스 관련하여 검색을 했을 때 시퀀스를 위해 countDocuments()를 사용한다는 경우는 찾지 못했고, 이렇게 하지 않는 어떤 이유가 있을 것이라고 생각한다.


두가지 이점을 더 찾았다.

동시성 문제 방지 가능

countDocuments()를 사용하며 동시성 문제를 해결하기 위해서는, 시퀀스가 업데이트가 되는 동안 collection을 잠가야 한다. 그렇지 않으면 여러 사용자가 동시에 insert 할 때 문제가 발생할 수 있다. 그런데, counter collection을 사용하면 $inc가 atomic하게 업데이트 작업을 하여 시퀀스 값을 증가시킬 수 있어 문제가 없다.

확장성

DB의 수평적 확장이 필요한 경우 별도의 카운터 컬렉션을 사용하는 것이 좋다. 별도의 collection을 사용하면 샤딩을 사용하여 시퀀스 생성 로드를 여러 서버에 분산시킬 수 있다. countDocuments()를 사용하면 새 시퀀스 값에 대한 모든 요청은 동일한 MongoDB 인스턴스로 이동해야 한다.