MongoDB의 Index 개선

byron1st·2021년 3월 20일
0

trust-chain-services 백엔드의 오랜 기술 부채는 바로 DB 인덱스였다. 개발 일정은 타이트한데, 아직 쓰는 사람이 많지는 않으니 자연스레 최적화를 등한시(?)하게 되고, 그 결과가 널부러진(?) DB 인덱스가 되었다. 그래서 지난주에 잠시 짬이 생겼을 때, DB 인덱스를 좀 살펴보았다.

MongoDB의 Go언어 공식 드라이버는 mongo-go-driver 이다. 난 별다른 외부 라이브러리 없이 공식 라이브러리를 쓴다. 이 공식 라이브러리에는 쿼리를 보내기 위해 쓸 수 있는 객체 타입이 몇가지 정의되어 있다. 예를 들어, JavaScript 라면, 아래와 같이 그냥 JSON 객체를 이용해 쿼리를 작성할 수 있다.

db.collections("students").find({status:"A"})

하지만 strongly typed 언어인 Go 언어에서는 {status:"A"} 를 표현하기 위한 별도의 타입 정의가 필요한데, 이를 위해 존재하는게 bson.M, bson.D, bson.A 이다. bson.Mbson.A 는 각각 map[string]interface{}[]interface{} 의 별칭 타입(alias)에 불과하지만, bson.D는 조금 다르다.

type D []E // bson.D는 bson.E의 배열

type E struct { // bson.E는 Key-Value 구조의 구조체
  Key string
  Value interface{}
}

사실 bson.Mbson.D는 모두 Key-Value 구조로 정의되는, 전형적인 JSON 형태의 구조체이다. 다만 다른점은 map 객체로 정의된 bson.M은 각 Key-Value 들 사이에 순서가 없지만, 배열 형태로 정의되는 bson.D 는 순서가 존재한다. 그래서 순서가 중요한 인덱스를 정의할 때는 반드시 bson.D 를, 순서가 필요 없는 쿼리 등을 정의할 때는 간편한 bson.M을 이용한다.

여기서 나의 궁금증. bson.M으로 순서없이 정의된 쿼리를 순서가 고정적인 인덱스가 지원할 수 있는가?

bson.M으로 정의된 쿼리가 인덱스의 지원을 받을 수 있나?

예를 들면, 다음과 같은 상황이다.

indexes := []mongo.IndexModel{
	{
		Keys: bson.D{
			{Key: "communityID", Value: 1},
			{Key: "isArchived", Value: 1},
		},
	},
...
}

위와같이, 인덱스는 우선 communityID 필드로 정렬하고, 그 후 같은 communityID 값을 갖는 값들을 다시 isArchived 값으로 정렬한다. 이때, 이 인덱스는 아래와 같은 쿼리를 지원할 수 있는가?

filter := bson.M{"isArchived": true, "communityID": communityID}

bson.M으로 정의되었기 때문에, 사실 위의 쿼리는 순서가 없다. 나의 궁금증은 인덱스의 지원을 받기 위해서 이 쿼리를 bson.D로 다시 짜줘야 하는 것인가 하는 문제였다. 그랬다면 아마 나의 그날 밤은 야근으로 물들었겠지(...) 다행히, 그렇지는 않다고 한다.

You may use bson.M for the filter, it usually results in shorter and clearer filter declaration, the order of fields doesn't matter, the MongoDB server is smart enough to find matching indices regardless of the used order. E.g. if you have a compound index with fields A and B, using a bson.D filter listing B first then A will not prevent the server to use the existing index. So in this case you may use bson.M and bson.D, it doesn't matter. (출처: Stackoverflow "bson.D vs bson.M for find queries")

정말 다행이다. 덕분에 나의 시간을 구할 수 있었다.

MongoDB의 인덱스 정의

인덱스는 첫째도 둘째도 쿼리다. 특히, Join 등의 기능이 부재한 NoSQL DB에서 쿼리들은 단순히 DB의 최적화 성능을 떠나 DB의 모델들, 인덱스, 심지어 도메인 모델까지 뒤흔들 중요한 요소라고 할 수 있다.

또한 인덱스는 결국 DB가 메모리에 잘 캐싱해두는 것이기 때문에, 또 너무 남발하는건 좋지 않다. 적당한 수준에서 타협을 해야 한다. 무슨 말이냐면, 쿼리에 필요한 모든 필드를 인덱스에 넣을 필요는 없다는 말이다. 특히, 자주 쓰지 않는 쿼리이거나(예를 들어, 관리자용 앱에서만 쓴다던가), 쓰기가 훨씬 자주 발생하는 영역이라면, 굳이 인덱스를 걸지 않는게 좋다. 쓰기가 발생하면 인덱스가 업데이트 되어야 하기 때문에 쓰기 성능에 영향을 줄 수 있기 때문이다.

그래서 인덱스를 개선하는 절차는 다음과 같다.

  1. 해당 콜렉션에 존재하는 모든 쿼리를 쭉 보기 좋게 나열해본다.
  2. 그 동안 너무 이상하게 짜지 않았다면(?) 대충 방향성이 보이는데, 대략 이런식이다:
  • '같은 커뮤니티 내에 존재하는 게시판들 중에서 보관처리 되지 않은 애들'
  • '같은 커뮤니티 내에서 존재하는 게시판들 중 보관처리 되지 않은 애들 중 ...'
  • '같은 커뮤니티 내에서 존재하는 ...'

이럴 경우, '같은 커뮤니티 내에서 보관처리 여부' 정도가 인덱스 대상이 되겠다.

  1. 튀는 쿼리들이 종종 있는데, 이럴 경우 해당 쿼리를 쓰는 API의 빈도수를 우선 확인해본다. 별볼일 없다면 그냥 무시하자. 별볼일 있다면(?), 이때 부터는 고민이 시작된다. 이를 기존 인덱스로 검색이 가능하도록 쿼리를 잘 고쳐보던지, 모델을 수정 가능하면 수정해보던지, 아니면, 어쩔수 없이 인덱스를 새로 생성하던지.

Unique Key에 대한 고민

DB 인덱스를 크게 고민하지 않던 때 짠 코드들 중에 이런 것들이 있다. 예를 들어, 어떤 사용자는 해당 커뮤니티에서 고유한 닉네임을 가져야 한다. 이 경우, 닉네임을 ID 로 하여, 콜렉션에 넣었는데, 이 때, _id 값을 닉네임_커뮤니티 이런 식으로 만들어서 넣었다. 만약 해당 커뮤니티에서 동일한 닉네임을 생성하려고 하면, 자동으로 생성되는 _id 에 대한 인덱스(unique가 걸려있다)에 의해 Duplicate key... 에러가 발생할테니 말이다.

그런데 사실 그냥 communityIDnickname에 대한 인덱스에 Unique 를 걸면 되는 것이었다. 이미 자동 생성되는 _id 인덱스 외에 별도의 인덱스를 만들어줘야 하지만, 제한이 많은 _id 필드를 임의로 건들지 않아도 되니 얼마나 좋은가.

사실 처음과 같은 코드를 짠건, 내가 NoSQL을 Hyperledger Fabric의 스마트컨트랙트 개발로 처음 접해서 그렇다. Hyperledger Fabric의 경우, LevelDB를 기본 DB로 사용하는데, 인덱스를 지원하지 않는다. 오직 키(MongoDB의 _id)를 정렬해주는게 전부다. 그러다보니, 키 값을 잘 조합해서 쓰는게 습관이 되어, MongoDB에 와서 코딩할 때도 저렇게 짜두었다.

하여튼, 개선의 여지가 있다고 할 수 있겠다.

profile
Fullstack software engineer specialized for Blockchain

0개의 댓글