MongoDB ObjectID로 알아보는 전역 키 관리 전략

개요

나는 지난 회사에서도, 토이/사이드 프로젝트를 진행할 때에도 주로 RDB를 사용해왔다. 그래서 그런지 항상 약속한듯이 Primary key 관리 전략으로 Auto-Increment Key(단조 증가하는 정수 키)를 사용해왔다.


그러던 중 이직을 하며 트래픽이 매우 큰 서비스를 다루게 되었는데, 이 서비스는 MongoDB라는 NoSQL DB를 사용한다. 그런데 이 DB에서는 ObjectID라는 난수 문자열같이 생긴 값을 Primary Key로 사용하는 것이 아닌가?

 

처음에는 왜 더 공간 차지도 많이 하고, 범위 정렬도 안 될 것 같은 이런 키를 사용할까? 라는 의문이 들었다. 하지만 이 ObjectID에 대해 학습하게 되면, MongoDB의 설계 철학과 왜 이런 키를 사용하게 되었는지를 이해할 수 있다.

 

따라서, 오늘은 이 ObjectID를 학습하며 MongoDB의 설계 철학을 이해하고, RDB에서는 이런 상황에서 어떤 안을 채택하는지 비교 분석까지 진행해보고자 한다.

 

 

MongoDB ObjectID란?

MongoDB ObjectID의 구성

위와 같이 구성되는, 12 bytes의 문자열이다.
눈으로 볼 때는 24자리의 16진수 문자열인 hex("문자열") 형태로 표시될 텐데, 이 값이 놀랍게도 의미 없는 난수 문자열이 아니라 3개 구역으로 나뉘고 각각이 의미를 가지는 값이라고 한다.

 

각 구역의 의미는 아래와 같다.

  • Timestamp (생성 시간, 4 bytes): ID의 생성 시점. 초 단위까지 저장하며 날짜 범위의 정렬과 탐색을 가능하게 만든다. (앞에서 범위 정렬도 안 될 것 같다고 얘기 했는데, 생성 시간이 가장 앞에 오기 때문에 대략적인 정렬이 가능하다!)
  • Random Value (무작위 값, 5 bytes): 말 그대로 무작위로 부여되는 값이다. 겹칠 확률은 1조분의 1 보다도 낮다. (2^40)
  • Counter (증가 카운터, 3 bytes): 노드 별로 랜덤 최초 값이 부여된 후 단조 증가하는 값이다. Timestamp가 초 단위까지만 기록하므로, 동일 애플리케이션 서버 내에서 밀리초 단위로 아이디를 생성할 때의 충돌을 막기 위한 값이라고 볼 수 있다.

여기까지 알고 나면 의미 없는 난수가 아니라 생각보다 잘 설계된 구조의 문자열임을 알 수 있다.
그러나 여전히 '왜 굳이 단조 증가 정수값'이라는 편안한 방법을 두고 이러한 난수를 사용하는가?에 대한 의문을 가질 수 있다. 이 내용은 아래에서 살펴본다.

 

 

왜 MongoDB는 Auto Increment Key를 사용하지 않을까?

이 내용을 이해하기 위해서는 MongoDB의 설계 철학을 이해해야 한다.


중앙 집중식의 단일 서버를 기본으로 하는 RDB와 달리, MongoDB는 분산 환경의 다중 서버를 전제로 제작되었다. 고가용성과 Scale-Out 용이성을 개발자가 복잡하게 구축할 필요 없이 지원하여, 고트래픽을 처리하는 애플리케이션에서 개발자가 애플리케이션에 더 집중할 수 있도록 하기 위함이다.

 

이런 이유로 MongoDB는 '다중 노드'를 기본 전제로 하는데, 다중 노드 환경에서 단조 증가 키를 사용하게 되면 키의 충돌 문제가 발생하게 된다.

이 키의 충돌 문제는 아래의 방향으로 해결할 수 있다.

  1. 노드 별로 할당 가능한 키 범위를 부여
  2. 중앙 집중식 키 관리 노드를 둠

 

하지만 각 방식은 아래와 같은 문제점을 가진다.

  1. 노드 별로 할당 가능한 키 범위를 부여: 노드의 추가/삭제 시마다 범위를 다시 고려해야 하므로 노드 개수 변경이 복잡해진다.
  2. 중앙 집중식 키 관리 노드를 둠: 부하 상황에서 키 관리 노드가 병목 지점이 될 수 있다.

따라서, 노드 별로 동기화하지 않아도 중복될 일이 없도록 정교하게 설계된 값을 사용하는 것이다. (물론 중복 확률이 0%는 아니지만 매우 미미하므로, 중복 키가 나오는 날에는 로또라도 사러 갈 것이다.)

 

또한, Object ID 방식을 사용하게 되면 클라이언트 사이드(애플리케이션 서버 내 Mongo Driver)에서 ID를 만들 수 있다는 장점도 있다.
DB 서버에서 아이디를 생성하게 되면 DB가 이를 위한 리소스를 사용하게 되고, 생성한 아이디를 다시 클라이언트에게 전달하기 위한 추가적 통신이 발생하게 되는데, 클라이언트 측에서 아이디를 생성하게 되면 DB 서버의 부담을 줄이고 불필요한 통신을 제거할 수 있다는 이점이 있다.

이 때문에 MongoDB 드라이버 구현체 마다 아이디 생성 방식이 달라 언어 종속성이 생기는 건 아닌가 했는데, 모든 MongoDB 공식 드라이버가 엄격한 BSON 표준에 따라 아이디를 생성 및 사용하도록 되어 있기 때문에 언어가 달라져도 영향을 받지는 않는다고 한다.

 

 

RDB로 다중 노드를 구축하고 싶을 땐 어떻게 할까?

위에서 잠깐 언급했듯, RDB는 중앙 집중식의 단일 노드 서버를 기본으로 하기 때문에, 다중 노드로 구축했을 때 단조 증가 키의 충돌 문제가 발생할 위험이 있다.
하지만 엄격한 스키마 제한과 복잡한 JOIN도 필요한데 대용량 트래픽 처리도 필요하다면? 이런 상황을 위해 RDB용 고유 ID 생성 기법도 추후 등장했다. 바로 한 번쯤은 들어봤을 Snowflake ID 이다. (등장 시기는 ObjectID가 더 앞섰다고 한다.)


이 글은 ObjectID에 대한 글이므로 Snowflake ID에 대한 자세한 설명은 생략하지만, 두 기술 모두 "분산 환경에서 중앙 서버 병목 없이 고유한 ID를 어떻게 만들 것인가?" 라는 난제에 대한 해결안임에도 각 기술의 목적에 따라 아래와 같은 차이점이 있다는 정도는 알아두면 좋다.

 

ObjectID는 단순한 구조가 주는 관리 용이성도 챙기되 충돌을 막는 기술이다.

  • 중앙 관리자 없이 클라이언트가 ID를 생성할 수 있어 편리하다.
  • 워커 노드의 개수에 변화가 있어도 별도 조치가 필요 없다.
  • 서버 간 시계가 미세하게 어긋나더라도 ID 생성에 문제가 없으며, 각 노드가 1초 당 약 160만개 (2^24) 정도의 고유 아이디를 만들어낼 수 있다.
  • 하지만 Snowflake ID에 비해 더 많은 공간을 소모하고, 더 느리다. (ObjectID는 96 bits 문자열, SnowflakeID는 64 bits 정수)

그에 반해 Snowflake는 인프라 복잡도를 감수하더라도 최소한의 용량, 최대한의 정밀도를 보장하고자 하는 기술이다.

  • 극강의 공간 효율, 극강의 성능을 보장한다.
  • 하지만 Snowflake ID 방식은 워커 노드 관리를 위한 중앙 코디네이터가 필요하다. 이는 단일 장애 지점을 유발할 수 있고, 워커 노드 변경에 영향을 받으므로 관리가 까다롭다.
  • 분산 시스템에서는 서버 간 클럭 불일치로 인한 충돌 문제가 잦아서, 밀리초 단위의 아이디는 관리하기 어려울 수 있다.

 

 

시대에 맞춰 진화하는 ObjectID - 과거에는 구조가 달랐다?

이 부분은 옛날 얘기이므로 바쁜 사람은 넘겨도 된다. 그냥 신기해서 함께 정리한다.

 

위에서 Random Value 부분이 말그대로 무작위 값으로 구성된다고 설명했는데, 이 값이 과거에는 머신 ID 3 bytes와 프로세스 ID 2 bytes로 구성되었다고 한다.

 

그런데 지금은 왜 무작위 값으로 바뀌었을까? 그 이유는, 과거의 방식이 클라우드와 컨테이너 기술(Docker & Kubernetes)이 자리잡음에 따라 잦은 id 충돌 문제를 야기했기 때문이다.

 

클라우드/컨테이너 기술 환경에서는 서로 다른 애플리케이션이어도 논리적으로만 분리될 뿐 물리적으로는 동일한 머신에서 동작하는 경우가 많다. 또한, 도커 컨테이너 내부에서 실행되는 메인 애플리케이션은 거의 첫 번째 프로세스로서 PID 1을 부여받는 경우가 많다.
이렇게 되다 보니 머신 ID와 프로세스 ID가 동일할 확률이 높아졌고, 충돌 확률이 올라가고 말았던 것이다.

 

따라서 현대에는 Random Value 부분이 무작위 값으로 변경되었으며, 인프라의 진화에 잘 대응한 스펙 변경이라고 볼 수 있겠다.