안녕하세요.

이번 블로그를 통해서 제가 평소 MongoDB를 사용하면서 생각해왔던 성능 개선을 위한 팁을 몇가지 공유하고자합니다.

컬렉션 분리

굉장히 많은 데이터(제가 생각하는 많다의 기준은 1억건정도입니다.)가 쌓일 것으로 예상되는 컬렉션은 최대한 분리를 하는 것이 더 나은 성능을 이끌어 낼 수 있으며 서비스 중에 인덱스 추가등의 설정 변경을 진행하기 좋습니다.

MLD에서는 고객이 소유하고 있는 아이템 정보를 패션,얼굴,인테리어 각각 분리해서 저장하고 있으며, HSD에서는 하나의 컬렉션에 고객이 소유하고 있는 아이템 정보를 저장하고 있습니다.

MLD에서는 고객이 소지하고 있는 아이템 리스트를 모두 가져오기 위해서 3개 컬렉션을 모두 조회해야 하지만 HSD에서는 하나의 컬렉션만 조회하면 되기 때문에 개발 자체는 MLD가 더 용이합니다.

하지만 CCP 서비스 특성상 가장 많은 데이터가 쌓이는 곳이 아이템 관련 컬렉션이고 DAU 3,4만 정도의 서비스만되어도 1년정도 지나면 아이템 컬렉션의 Document수가 1억건이 넘어갑니다.

현재 너무 많은 데이터가 쌓여있는 HSD의 아이템 컬렉션은 점점 슬로우 쿼리의 빈도가 높아지는 상황이고 성능 개선을 위해서 새로운 인덱스 추가등의 변화를 주는 것도 매우 부담스러운 상황입니다.

임베디드 방식 사용

RDB만 사용하시던 개발자가 MongoDB를 처음 사용할때 DB 모델링을 RDB를 사용하던 방식으로 하는 경향이 있습니다.

개인적으로 Java 개발자에게 MongoDB의 가장 큰 장점은 Document 구조를 임베디드 방식으로 구현할 수 있다는 부분입니다.

잘 활용하면 2~3개 테이블로 구현해야 하는 로직을 1개 컬렉션에서 더 좋은 성능으로 구현할 수 있습니다.

contents가 수정되더라도 최초의 contents값을 가지고 있어야 하는 사양에 따라 contents,originContents에 임베디드 객체가 사용된 예입니다.

{
    "_id" : ObjectId("5ee31bbee51d6130fbf9e5fd"),
    "bbsId" : NumberInt(21),
    "seq" : NumberLong(11),
    "mid" : NumberLong(1045),
    "contents" : {
        "txt" : "블로그2",
        "imageUrl" : "https://.....2.png",
        "ct" : NumberLong(1594046565634)
    },
    ...
    "originContents" : {
        "txt" : "블로그1",
        "imageUrl" : "https://.....1.png",
        "ct" : NumberLong(1594013232233)
    }
}

만약 최초 contents 데이터를 별도 컬렉션에 저장할 경우 별도 쿼리를 통해서 조회해야 하지만 임베디드 구조를 통해서 하나의 Document에 관련 데이터를 모두 획득할 수 있습니다.

이렇게 잘 활용하면 필요한 데이터를 단순한 쿼리로 더 좋은 성능으로 조회할 수 있습니다.

물론 주의해야 하는 부분도 있습니다.

  1. document 최대 용량이 16MB이기 때문에 절대 최대 용량을 넘지 않도록 설계해야 합니다.
  2. 저장 과정에서 오류가 발생하면 데이터 정합성이 깨지기 쉽습니다.
  3. 업데이트에서는 성능 저하가 있을 수 있기 때문에 자주 변경되는 데이터의 경우는 피하는 것이 좋습니다.

적절한 인덱스 사용

MongoDB에 사용되는 인덱스 알고리즘은 RDB에서도 많이 사용되는 B-Tree(Balanced Tree)구조로 구현되었기 때문에 RDB에서 인덱스 사용하는 것과 크게 다르지 않습니다.

다만 RDB에서보다 인덱스가 더 중요한 이유는 비슷한 양의 데이터라도 인덱스를 타지 않고 풀스캔이 이루어지는 경우 구조적인 특성상 MongoDB에서 훨씬 많은 시간이 소모되기 때문입니다.

또한 인덱스에는 많은 용량이 사용되기 때문에 불필요한 인덱스 사용을 피하면서 용량을 낭비하지 않도록 해야 합니다.

가장 일반적으로 사용하는 인덱스는 단일(Single)인덱스와 복합(Compound)인덱스입니다.(이외에 Multikey Index, Geospatial Index, Text Index, hashed Index도 있습니다.)

말 그대로 하나의 키만을 사용한다면 단일인덱스이고 여러 개의 키가 사용되면 복합인덱스입니다.

인덱스는 하나만 사용되지 두개 이상의 인덱스가 사용되지는 못합니다.

TEST라는 컬렉션에 아래와 같이 두개의 단일인덱스가 걸려 있는데 db.TEST.find({mid:1,seq:10}) 와 같이 두개 이상의 키가 사용된다고 해서 mid, seq 인덱스가 함께 사용되는 것이 아니라 쿼리 옵티마이저가 그 중 제일 효율적인 인덱스를 선택하는데, 이 선택이 가장 좋은 결과를 보장하는 것은 아니기 때문에 explain을 통해서 어떤 인덱스가 사용되는지 확인할 필요가 있습니다.
image

이번 경우에는 seq 인덱스가 사용 되었습니다.

image

대상의 범위가 좁을 수록 좋은 결과가 나올수 있는 인덱스입니다.

예를들어 mid:1에 해당하는 Document가 100개이고 seq:10에 해당하는 Document가 1000개라면 mid 인덱스에서 더 좋은 결과가 기대되고, mid:1에 해당하는 Document가 1000개이고 seq:10에 해당하는 Document가 100개라면 seq 인덱스에서 더 좋은 결과가 기대됩니다.

복합인덱스는 index prefix를 지원하는데 이 부분을 정확히 이해하고 있어야 불필요한 인덱스 설정을 줄일 수 있습니다.

index prefix 개념은 예를들어 설명드리겠습니다.

TEST컬렉션에 a필드,b필드가 키로 지정되어 있는 경우 a필드는 별도로 인덱스를 지정할 필요가 없습니다.

db.TEST.createIndex({ a필드: 1, b필드: 1})
{ a필드: 1 } // OK

a필드,b필드,c필드 순서로 인덱스가 지정된 경우에는 아래 인덱스는 별도로 지정할 필요가 없습니다.

db.TEST.createIndex({ a필드: 1, b필드: 1, c필드:1})
{ a필드: 1 } // OK
{ a필드: 1, b필드: 1 } // OK

그리고 복합인덱스의 경우 순서가 중요한데 a필드,b필드 순으로 인덱스를 지정한 경우 a필드로 조회할 경우에는 인덱스를 사용하지만, b필드로 조회할 경우에는 인덱스를 타지 않습니다.

db.TEST.createIndex({ a필드: 1, b필드: 1})
{ a필드: 1 } // OK
{ b필드: 1} // 지원하지 않음

실제 HSD의 USER_ITEM_M에서 확인해 보겠습니다.

아래와 같이 mid,seq순으로 복합인덱스가 생성되어 있는 것을 확인하실 수 있습니다.

image

mid로 데이터를 조회하면 인덱스를 사용하지만

image

seq로 데이터를 조회하면 인덱스를 사용하지 않고 풀스캔을 하는 것을 확인 하실 수 있습니다.

image

마치며

처음 블로그를 작성하려고 마음 먹었을 때는 다양한 생각들이 있었는데 막상 블로그로 쓰려고 하니 쉽지 않네요. 생각보다 깊게 들어가지는 못했지만 MongoDB를 사용하시는 분들이 기본적으로 알고 있어야 하는 내용들이니 개발하면서 염두해 두셨으면 좋겠습니다.