Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

UUID vs ULID: 개발자가 알아야 할 고유 식별자의 모든 것

얼마 전 HB 팀이 잡담을 나누던 중, 데브옵스 전문가인 Ben이 "특정 시스템에 UUID 대신 ULID를 사용했더라면 좋았을 텐데"라고 언급한 적이 있습니다.

늘 그렇듯 베테랑 엔지니어다운 반응으로 저는 애매한 말만 중얼거린 뒤, 몰래 Google에 검색해 ULID가 도대체 무엇인지 알아보러 갔습니다.

두 시간 후, 저는 멍한 눈으로 돌아왔고, 고유 식별자의 세계가 상상했던 것보다 훨씬 넓고 흥미롭다는 사실을 깨달았습니다.

ULID를 본격적으로 다루기 전에, 기본으로 돌아가 UUID가 무엇인지 먼저 살펴보겠습니다.

일반적인(순차) ID에는 어떤 문제가 있을까?

데이터베이스를 사용하는 대부분의 웹 애플리케이션은 자동 증가하는 숫자 ID를 기본값으로 사용합니다. 예를 들어 Rails에서는 다음과 같은 동작을 볼 수 있습니다:

p1 = Person.create!
p1.id
# => 1

p2 = Person.create!
p2.id
# => 2

데이터베이스는 레코드 생성 시마다 1씩 증가하는 카운터를 저장하고 있기 때문에 순차적인 ID를 생성할 수 있습니다.

이 패턴은 데이터베이스 밖에서도 찾아볼 수 있습니다. 때로는 ID를 직접 할당해야 하는데, 이럴 때 Redis 인스턴스 같은 곳에 커스텀 카운터를 저장해 두곤 합니다.

순차 ID는 처리량이 적은 환경에서는 구현하기 쉽지만, 규모가 커질수록 여러 문제가 발생합니다:

  • 각 insert가 ID를 받기 위해 순서대로 기다려야 하므로 레코드를 동시에 생성하는 것이 불가능합니다.
  • 순차 ID를 요청하려면 네트워크 왕복이 필요할 수 있어 성능이 느려집니다.
  • 순차 ID를 제공하는 데이터 스토어는 수평 확장(scale-out)이 어렵습니다. 서로 다른 서버의 카운터가 동기화되지 않는 문제를 걱정해야 합니다.
  • 카운터를 가진 노드가 단일 장애 지점(SPOF)이 되기 쉽습니다.

순차 ID는 데이터를 노출한다는 문제도 있으며, 경우에 따라 보안 이슈가 될 수 있습니다:

  • 내 소유가 아닐 수 있는 리소스의 ID를 쉽게 추측할 수 있습니다.
  • 사용자를 생성했는데 ID가 20이라면, 해당 서비스에 사용자가 20명 있다는 사실을 알 수 있습니다.

UUID는 웹 스케일(Web-scale)

UUID는 순차 ID와 조금 다르게 생겼습니다. 128비트 숫자로, 일반적으로 32자리 16진수로 표현됩니다:

123e4567-e89b-12d3-a456-426655440000

UUID는 RFC 4122에 정의된 특정 알고리즘으로 생성되며, 순차 ID에서 발생하는 많은 문제를 해결합니다:

  • 노드 간 공유 상태나 조율 없이 어떤 노드에서든 UUID를 생성할 수 있습니다.
  • 순차 ID보다 추측하기 어렵습니다 (자세한 내용은 아래에서).
  • 데이터셋의 크기를 노출하지 않습니다.

단점은 두 노드가 독립적으로 동일한 ID를 생성할 작은 확률이 있다는 점입니다. 이를 "충돌(collision)"이라고 부릅니다.

다양한 종류의 UUID

RFC 4122에는 다섯 가지 UUID 알고리즘이 정의되어 있으며, 크게 두 가지 카테고리로 나뉩니다:

  • 시간 및 무작위성 기반 알고리즘: 실행할 때마다 새로운 UUID가 생성됩니다.
    • Type 4: 무작위로 생성된 ID입니다. 새 코드에는 아마 가장 좋은 선택입니다.
    • Type 1: 호스트의 MAC 주소와 현재 타임스탬프가 포함됩니다. 추측하기 너무 쉬워 더 이상 권장되지 않습니다.
    • Type 2: 흔하지 않은 편이며, 낡은 형태의 RPC를 위해 만들어진 것으로 보입니다.
  • 이름 기반 알고리즘: 주어진 입력에 대해 항상 동일한 UUID를 생성합니다.
    • Type 5: SHA-1 해시를 사용해 UUID를 생성합니다. 권장됩니다.
    • Type 3: MD5 해시를 사용하며, MD5가 너무 취약해 더 이상 권장되지 않습니다.

Ruby에서는 uuidtools 젬을 통해 UUID를 생성할 수 있습니다. 미스터리한 Type 2를 제외한 모든 타입을 지원합니다:

# uuidtools readme에서 가져온 코드입니다 :)
require "uuidtools"

# Type 1
UUIDTools::UUID.timestamp_create
# => #<UUID:0x2adfdc UUID:64a5189c-25b3-11da-a97b-00c04fd430c8>

# Type 4
UUIDTools::UUID.random_create
# => #<UUID:0x19013a UUID:984265dc-4200-4f02-ae70-fe4f48964159>

# Type 3
UUIDTools::UUID.md5_create(UUIDTools::UUID_DNS_NAMESPACE, "www.widgets.com")
# => #<UUID:0x287576 UUID:3d813cbb-47fb-32ba-91df-831e1593ac29>

# Type 5
UUIDTools::UUID.sha1_create(UUIDTools::UUID_DNS_NAMESPACE, "www.widgets.com")
# => #<UUID:0x2a0116 UUID:21f7f8de-8051-5b89-8680-0195ef798b6a>

ULID로 넘어가기

참고: 이 글의 원래 버전에서 ULID 스펙 링크를 빠뜨렸었습니다. 공식 ULID 스펙 문서에서 확인할 수 있으며, Ruby를 비롯한 다양한 언어의 구현체 링크도 함께 제공됩니다.

ULID는 고유 식별자에 대한 유용한 새로운 접근 방식입니다. 가장 눈에 띄는 차이점은 생김새가 조금 다르다는 것입니다:

01ARZ3NDEKTSV4RRFFQ69G5FAV

ULID는 base32로 인코딩된 두 개의 값으로 구성됩니다. UNIX 타임스탬프 뒤에 무작위(random) 값이 이어집니다. 스펙에 정의된 구조는 다음과 같습니다:

01AN4Z07BY      79KA1307SR9X4MV3

|----------|    |----------------|
 Timestamp          Randomness
   48bits             80bits

이 구조는 매우 흥미롭습니다! UUID는 타임스탬프 또는 무작위성 중 하나에 의존하는 반면, ULID는 타임스탬프 무작위성을 모두 사용합니다.

그 결과 ULID는 몇 가지 흥미로운 특성을 갖습니다:

  • 사전순(lexicographically)으로 정렬 가능합니다.
  • 타임스탬프가 밀리초 단위까지 정확합니다.
  • UUID보다 보기 좋습니다 :)

이런 특성 덕분에 몇 가지 멋진 활용이 가능합니다:

  • 날짜별로 데이터베이스를 파티셔닝하는 경우, ULID에 포함된 타임스탬프로 올바른 파티션을 선택할 수 있습니다.
  • 밀리초 정밀도로 충분하다면, 별도의 created_at 컬럼 대신 ULID로 정렬할 수 있습니다.

물론 잠재적인 단점도 있습니다:

  • 타임스탬프 노출이 애플리케이션에 부적합하다면, ULID가 최선의 선택이 아닐 수 있습니다.
  • 밀리초 미만의 정밀도가 필요하다면 sort by ulid 방식이 동작하지 않을 수 있습니다.
  • 인터넷에 따르면 일부 ULID 구현체는 완벽하지 않다고 합니다.

결론

UUID는 현재이자 앞으로도 표준으로 남을 것입니다. 오랫동안 사용되어 왔고, 상상할 수 있는 거의 모든 프로그래밍 언어에서 라이브러리를 찾을 수 있습니다. 하지만 분산 시스템이 점점 더 세상을 지배해 가는 지금, 새로운 접근 방식도 살펴볼 가치가 충분합니다. 새로운 고유 ID 방식은 RFC4122가 발표되던 시절에는 드물었던 문제들을 해결하는 데 도움을 줄 수 있습니다.