Simple Topics

UUID 의 세계(UUID 선택 가이드)

smileostrich 2023. 5. 15. 21:16

 

UUID 를 쓸 것이냐? ID 를 쓸 것이냐? 혹은 둘 다 쓸 것이냐?

오래된 논쟁이지만, 모든 상황에서 절대적으로 옳은 선택지가 존재하지 않는 논쟁이라고 생각하기에, 무의미한 논쟁에서 벗어나

내 시스템에는 어떤 선택지가 좋을까? 에 대해 같이 고민해봅시다 :)

 

들어가기전에

auto increment ID 는 DB 에서 PK 로 자주 사용됩니다.

하지만 ID 만 사용하게 되면 보안 문제가 생길 수 있습니다. 예를 들어, 일련번호와 같은 연속된 ID를 사용하는 경우, 공격자가 다른 사용자의 데이터에 접근할 가능성이 있습니다. 반면, UUID는 범용 고유 식별자로 128비트(아닌 경우도 있는데, 아래에서 다루겠습니다) 크기의 "거의" 중복되지 않는 값을 생성합니다.

ID 의 단점

ID 의 단점은 앞서 설명드린것 처럼, 보안상의 문제 뿐만 아니라 다른 문제들도 존재합니다.

대부분의 데이터베이스 시스템에서 ID 는 일반적으로 해당 DB 내에서만 고유하게 관리되는 값입니다. 즉, 서로 다른 DB 혹은 테이블에서는 같은 ID 값을 가진 데이터가 존재할 수 있습니다.

예를 들어, 데이터베이스 A와 B를 마이그레이션하는 상황을 가정해보겠습니다.
A에는 ID가 1인 데이터가 있고, B에도 ID가 1인 데이터가 있다면, 이 두 데이터를 가져올 때 데이터가 중복되는 문제가 발생합니다. 이는 ID collision 이라고 부르며, 이를 해결하기 위한 추가적인 작업이 필요합니다.

반면, UUID는 이러한 문제를 해결할 수 있습니다. UUID는 전역적으로 고유한 값을 가지므로, 서로 다른 데이터베이스 혹은 테이블에서 같은 UUID 값을 가진 데이터가 존재할 가능성이 극히 낮습니다. 따라서 UUID 를 사용하면, 데이터베이스 간의 데이터 이동이나 병합 시 collision 없이 안전하게 이동할 수 있습니다.

UUID 의 단점

그렇다면 UUID 의 단점은 없을까? 하면 크게 2가지를 꼽을 수 있을 것 같습니다.

 

  1. 크기
    auto increment 에 비해 저장 공간이 더 많이 필요합니다. 이것은 결국 디스크 공간 사용량을 증가시키며, 인덱스 크기 역시 증가시킵니다. 인덱스는 메모리 사용량을 증가시키고, 쿼리 성능을 저하시킬 있습니다.
  2. 랜덤성
    랜덤성이 왜 문제가 되지? 좋은거 아냐? 라고 의문을 품으실 수 있는데 "정렬될 수 없는 랜덤성"은 문제가 될 수 있습니다.
    새로운 UUID가 데이터베이스의 여러 위치에 분산되게 만들며, 이는 clustered index 에서 특히 문제가 될 수 있습니다. 새로운 row 가 계속 랜덤한 위치에 삽입되면, DB 는 페이지 분할을 수행할 것이고, 이는 성능에 부정적인 영향을 미칠 수 있습니다.
    또한, 이로 인해 WAL 에 많은 데이터가 쌓이게 되어 복구 시간이 증가하게 됩니다.

해결 방법은 없을까?

  1. 시간 기반 UUID 사용 : UUID v1 이나 v6 같은 시간 기반의 UUID 를 사용하면, 생성된 UUID 가 시간 순서대로 정렬되므로, 새로운 row 가 데이터베이스의 동일한 위치에 삽입됩니다. (인덱스와 디스크 공간의 사용량을 최적화하는 데에도 도움이 됨)
  2. 복합 키 사용 : UUID 와 시퀀스 번호 또는 타임스탬프를 결합하여 복합 키를 사용하는 방법입니다. 이렇게 하면 쿼리 성능을 개선하고, 동시에 UUID의 장점을 유지할 수 있습니다.
  3. UUID 압축 : UUID 를 바이너리 형식으로 저장하여 저장 공간을 줄일 수 있습니다. 이렇게 하면 UUID 의 크기를 16바이트로 줄일 수 있습니다 (물론 바이너리를 다시 핸들링 해줘야 하는 경우 비용 발생) (이외에도 base64uid 같은 방법도 있는데, 밑에서 자세하게 다루겠습니다)

UUID 와 ID 를 같이 쓴다면?

ID와 UUID를 함께 사용하면, 복잡도가 높아지는 것은 사실입니다. 하지만 감당하기 힘들 정도는 아니며, 서비스가 성장하고 나서 마이그레이션을 할 때도 편리합니다.

예를 들어, 서비스가 성장하면서 데이터베이스 시스템을 변경하거나 분산 시스템으로 전환해야 할 때가 있습니다. 이런 경우, UUID 를 사용하면 ID collision 부담감이 줄어들고, UUID 는 전체 시스템에서 고유함을 보장하기 때문에, 여러 데이터베이스나 서비스에서 데이터를 병합하거나 이동시키는 것이 더욱 용이해집니다.

 

따라서 저희는 ID 만 사용하는것이 아닌 UUID 혹은 ID 와 UUID 를 모두 사용하는 선택지로 진행해봅시다 :)

(우선, UUID 종류를 살펴보고 그 다음 상황마다 어떤 선택지가 좋을지 살펴볼게요!)

 

UUID 의 종류

UUIDv1 ~ UUIDv8, ULID, snowflake, baidu 등 다양한 선택지가 있습니다.

하나씩 차례로 살펴봅시다 : )

UUIDv1

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                          time_low                             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|       time_mid                |         time_hi_and_version   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|clk_seq_hi_res |  clk_seq_low  |         node (0-1)            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                         node (2-5)                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

이 버전의 UUID는 MAC 주소와 현재 시간을 결합하여 생성됩니다. 이 방식의 장점은 시간을 기반으로 하므로 시간 순서대로 UUID를 생성할 수 있습니다. (배치로 UUID 를 생성해야 하는 경우에는 카운터(counter)를 사용하여 각각의 UUID가 유일하게 생성되도록 할 수 있습니다)

UUIDv2

이 버전은 POSIX UID와 GID를 포함하여 생성됩니다. 일반적으로는 잘 사용되지 않기에 넘어가겠습니다

UUID v3

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                            md5_high                           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          md5_high             |  ver  |       md5_mid         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var|                        md5_low                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                            md5_low                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

이 버전은 MD5 해시와 이름 공간 (예: URL, 도메인 이름, 등등)을 결합하여 이름 기반의 UUID를 생성합니다.
따라서, UUID v3는 항상 같은 입력에 대해 같은 UUID를 생성합니다. (마찬가지로 자주 사용되지 않기에 넘어가겠습니다)

UUID v4

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           random_a                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          random_a             |  ver  |       random_b        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var|                       random_c                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           random_c                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+


이 버전은 난수를 기반으로 UUID를 생성합니다. 이로 인해 생성된 UUID 는 거의 고유하며, 임의성이 높습니다(물론, 이론적으로는 충돌 가능성 존재)

UUID v5

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                            sha_high                           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          sha_high             |  ver  |       sha_mid         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var|                        sha_low                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                            md5_low                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

이 버전은 UUID v3 와 비슷하지만, SHA-1 해시 알고리즘을 사용하여 생성됩니다.

(UUIDv3 와 마찬가지로 같은 입력에 대해 항상 같은 UUID 를 생성합니다)

UUID v6

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           time_high                           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|           time_mid            |      time_low_and_version     |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|clk_seq_hi_res |  clk_seq_low  |         node (0-1)            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                         node (2-5)                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

버전은 UUIDv1 과 유사한 방식을 사용하지만 표준화된 방식을 사용합니다.

바로, 타임스탬프를 UUID 시작 부분에 배치하여 시간 순서대로 정렬할 있게 해줍니다. 이 점이 바로 v1 과의 가장 차이점으로 이를 통해 DB 에서 효율적인 인덱싱이 가능합니다.

UUIDv7

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           unix_ts_ms                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          unix_ts_ms           |  ver  |       rand_a          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var|                        rand_b                             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                            rand_b                             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

이 방식은 비교적 최근에 제안된 UUID 입니다. 1582년 10월 15일 00:00:00.00 부터 100 나노초 단위로 계산하는 대신, 1970년 1월 1일 0시(UTC)부터 밀리초 단위로 계산하는 유닉스 시간을 사용합니다 (따라서 유닉스 시간을 사용하는것이 장점이자 단점)

구조는 다음과 같습니다.

  • 처음 48비트는 유닉스 시간을 밀리초 단위로 표현한 것입니다.
  • 다음 4비트는 버전 비트로서 (0111), 이어서 12비트는 pseudo-random 데이터입니다.
  • 다음은 2비트 변형 비트, 마지막으로 62비트는 pseudo-random 데이터입니다.

UUIDv8

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           custom_a                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          custom_a             |  ver  |       custom_b        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var|                       custom_c                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           custom_c                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

UUIDv8 는 UUIDv4 와 비슷한 방식을 사용하지만, 더 많은 사용자 정의 옵션을 제공합니다. UUIDv8 의 122비트 중 일부는 시간 기반으로 생성되지만, 나머지 비트는 사용자가 원하는 방식으로 생성할 수 있습니다.

(사용자 정의 옵션이 많기 때문에, 특정 어플리케이션의 요구 사항에 맞게 UUID 생성할  있습니다. 하지만 결국 어플리케이션 간에 UUIDv8  구현이 일관되지 않을  있다는 단점으로 작용할 수 있음)

구조는 다음과 같습니다

  • 48비트는 사용자 정의 구현에 따라 달라집니다.
  • 다음 4비트는 버전 비트로서 (1000), 이어서 12비트는 사용자 정의 구현에 따라 달라집니다.
  • 다음은 2비트 변형 비트, 마지막으로 62비트는 사용자 정의 구현에 따라 달라집니다.

Snowflake

Snowflake 는 분산 시스템의 특성을 최대한 활용하기 위해 설계되었습니다.
Snowflake ID는 64비트로 구성되며, 시간 순서, 서버 ID, 그리고 시퀀스 숫자를 포함합니다. 이러한 방식은 분산 시스템에서 동시에 생성되는 ID 의 중복을 방지하고 일관성을 유지합니다.

구조는 다음과 같습니다

  • 앞 41 비트는 밀리세컨드 단위의 타임스탬프입니다.
  • 다음 10 비트는 워커 노드 또는 데이터 센터의 식별자입니다.
  • 마지막 12 비트는 같은 타임스탬프에서 생성될 수 있는 연속된 ID 를 구분하기 위한 시퀀스 번호입니다.

 

주요 장점

  • 분산 환경 지원 : 각 노드는 독립적으로 ID를 생성할 수 있으며, 이 ID는 전체 시스템에서 고유합니다.
  • 충돌방지 : worker ID 와 시퀀스 번호를 결합하여 각 ID가 시스템 전체에서 고유함을 보장합니다
  • 64비트 정수 ID : 64비트 정수 ID를 생성합니다. 이는 저장 공간을 효율적으로 활용하며, 정수 기반의 ID는 처리가 빠르고, 데이터베이스에서 쿼리를 수행하기도 편리합니다.
  • 순차적 증가 : Snowflake에 의해 생성된 ID는 (특히 시간 부분에 대해) 크게 보면 순차적으로 증가합니다. 이는 데이터베이스 인덱스의 효율성을 향상시키는데 도움이 됩니다.
  • 고성능과 확장성 : 초당 수만 건의 ID를 생성할 수 있으며, 서비스의 확장성을 유지하면서 더 많은 요청을 처리할 수 있습니다.
  • 시간 역행 문제 방지 : 시스템 시계가 뒤로 가는 경우를 처리하기 위한 로직을 포함하고 있기에, "시간 역행" 문제를 방지합니다.

Snowflake의 장점은 고유하며 시간 순서대로 생성되는 ID 를 빠르게 생성할 수 있다는 것입니다. 또한, 분산 시스템에서 사용하기 위해 설계되어 동시성 문제를 잘 처리합니다.

 

마지막으로 간단한 예제 코드를 살펴보고 넘어가겠습니다 : ) 

예제 코드

    private static final int EPOCH_BITS = 41;
    private static final int NODE_ID_BITS = 10;
    private static final int SEQUENCE_BITS = 12;
    private static final long MAX_NODE_ID = (1L << NODE_ID_BITS) - 1;
    private static final long MAX_SEQUENCE = (1L << SEQUENCE_BITS) - 1;
    private static final long DEFAULT_CUSTOM_EPOCH = 1680000000000L;

    private volatile long lastTimestamp = -1L;
    private volatile long sequence = 0L;

    private final long nodeId;
    private final long customEpoch;

    public Snowflake(long nodeId, long customEpoch) {
        if (nodeId < 0 || nodeId > MAX_NODE_ID)
            throw new IllegalArgumentException(String.format("invalid-node-id : node id must be between %d and %d", 0, MAX_NODE_ID));

        this.nodeId = nodeId;
        this.customEpoch = customEpoch;
    }

    public Snowflake(long nodeId) {
        this(nodeId, DEFAULT_CUSTOM_EPOCH);
    }

    public Snowflake() {
        this.nodeId = createNodeId();
        this.customEpoch = DEFAULT_CUSTOM_EPOCH;
    }

    public synchronized long nextId() {
        long currentTimestamp = timestamp();

        if (currentTimestamp < lastTimestamp)
            throw new IllegalStateException("invalid-system-clock");

        if (currentTimestamp == lastTimestamp) {
            sequence = (sequence + 1) & MAX_SEQUENCE;
            if (sequence == 0)
                currentTimestamp = waitNextMillis(currentTimestamp);
        } else
            sequence = 0;

        lastTimestamp = currentTimestamp;

        return currentTimestamp << (NODE_ID_BITS + SEQUENCE_BITS)
                | (nodeId << SEQUENCE_BITS)
                | sequence;
    }

    private long timestamp() {
        return System.currentTimeMillis() - customEpoch;
    }

    private long waitNextMillis(long currentTimestamp) {
        while (currentTimestamp == lastTimestamp)
            currentTimestamp = timestamp();

        return currentTimestamp;
    }

    private static long createNodeId() {
        long nodeId;
        try {
            StringBuilder sb = new StringBuilder();
            Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
            while (networkInterfaces.hasMoreElements()) {
                NetworkInterface networkInterface = networkInterfaces.nextElement();
                byte[] mac = networkInterface.getHardwareAddress();

                if (mac != null)
                    for (byte macPort : mac)
                        sb.append(String.format("%02X", macPort));
            }
            nodeId = sb.toString().hashCode();
        } catch (Exception ignored) {
            nodeId = new SecureRandom().nextInt();
        }

        return nodeId & MAX_NODE_ID;
    }

    public long[] parse(long id) {
        long maskNodeId = ((1L << NODE_ID_BITS) - 1) << SEQUENCE_BITS;
        long maskSequence = (1L << SEQUENCE_BITS) - 1;
        long timestamp = (id >> (NODE_ID_BITS + SEQUENCE_BITS)) + customEpoch;
        long nodeId = (id & maskNodeId) >> SEQUENCE_BITS;
        long sequence = id & maskSequence;

        return new long[] {timestamp, nodeId, sequence};
    }

    @Override
    public String toString() {
        return "My Custom Snowflake [EPOCH_BITS=" + EPOCH_BITS + ", NODE_ID_BITS=" + NODE_ID_BITS
                + ", SEQUENCE_BITS=" + SEQUENCE_BITS + ", CUSTOM_EPOCH=" + customEpoch
                + ", NodeId=" + nodeId + "]";
    }

 

Baidu의 UID Generator

이는 Baidu 에서 개발한 분산 시스템용 uuid generator 로, 시간 기반의 생성 전략을 사용합니다.

구조는 다음과 같습니다

  • 앞 28비트는 시간을 초 단위로 표현합니다.
  • 다음 22비트는 워커 노드의 식별자입니다.
  • 마지막 13비트는 같은 초에 생성될 수 있는 연속된 ID 를 구분하기 위한 시퀀스 번호입니다.

이러한 방식은 고유하고 시간 순서대로 생성하며, 분산 시스템에서 동시성 문제를 잘 처리할 수 있지만, (시간 기반이기에 당연히) 시간 동기화에 대한 요구사항이 있으며(사실 분산시스템에서 시간 동기화는 정말 어려운 문제입니다. 이 내용도 언제 기회가 되면...) 초 단위로 ID를 생성하기 때문에 초당 ID 생성 수에 제한이 생길 수 있습니다.

또한 RingBuffer 를 사용하면 CPU 캐시 매커니즘으로 인해, 읽기 성능을 향상시킬 수 있습니다. 하지만 False Sharing 이라는 사이드 이팩트도 존재하는데, baidu 는 이 문제를 해결하기 위해 캐시 라인 패딩을 적용했습니다.

 

+ Baidu uid generator 는 비록 snowflake 에 비해 복잡도가 높지만, 여기에 미처 다 소개하지 못한 여러 재미있는 아이디어들이 많아, 꼭 한번 읽어보시길 바랍니다 :)

 

이제까지 다양한 선택지들을 살펴보았습니다. 이러한 방법 중 어느 것을 선택할지는 여러 가지 요인을 고려해야 합니다.

주요 요인으로는 시스템 별로 요구사항이 있을 수 있고, 분산 처리 요구사항, 생성 속도, ID 의 시간 순서 등이 있는데,

아래 상황별 유즈케이스를 통해 살펴봅시다 :)

 

상황별 유즈케이스

만약, UUID 의 길이가 부담이라면?

원칙적으로는 RFC 2616 (Hypertext Transfer Protocol HTTP / 1.1) 섹션 3.2.1 에 따라

HTTP 프로토콜은 URI 길이에 대한 사전 제한을 두지 않습니다. 서버는 제공하는 모든 리소스의 URI 를 처리할 수 있어야하며 이러한 URI 를 생성 할 수있는 GET 기반 양식을 제공하는 경우 무제한 길이의 URI 를 처리 할 수 있어야합니다 (SHOULD). URI가 서버가 처리 할 수있는 것보다 길면 서버는 414 (Request-URI Too Long) 상태를 반환해야합니다 (섹션 10.4.15 참조).

와 같지만, 사실 대부분의 브라우저는 2000(2048, 2083 등) 자 제약이 존재합니다.

이 제약조건이 애매하게 걸쳐져 있는 경우엔 base64uid 와 같은 선택지도 고려해볼 수 있습니다.

monolithic 인 상황이라면?

사실 모놀리식 서버를 운영중인 상황이라면 문제는 심플한 편이기에 선택지가 많아집니다.

snowflake, baidu 와 같은 선택지가 아니더라도 기본 UUID 로도 충분한 경우가 많기에 니즈를 쪼개서 살펴볼게요!

    • (흔치는 않지만) 아주 높은 속도로 UUID 를 생성해야하는 경우
      -> UUIDv4 (랜덤 기반)를 사용하는 것이 가장 간단하고 효율적일 수 있습니다. (다만, 위에서 설명 드린 것처럼 성능 문제가 있음)
    • 서비스가 급속도로 성장하는 경우
      서비스가 급격히 성장하면서 데이터의 삽입 속도가 빨라질 경우, UUIDv7(여기에서도 UUIDv7 을 권장하고 있음) 또는 ULID 와 같은 시간 기반의 UUID 스킴을 고려해 볼 수 있습니다. 위 스킴은 생성된 순서대로 UUID를 부여하므로, 삽입 시 인덱스 재조정을 최소화하고 성능을 향상시킬 수 있습니다. (이 경우, 배치로 UUID 를 생성해야 하는 경우에는 카운터(counter)를 사용하여 각각의 UUID가 유일하게 생성되도록 할 수 있습니다)

분산 환경인 경우

분산 환경인 경우 고려해야 할 제약조건들이 많아집니다. 물론 Baidu 도 괜찮은 선택지일 수 있지만, 개인적으로는 Snowflake 가 제너럴한 케이스에서 더 괜찮은 선택지라고 생각합니다. 앞선 내용을 토대로 봤을 때, UUIDv1 혹은 v6 의 경우 발생할 수 있는 (시간 역행 문제를 비롯한 몇가지) 문제점들을 snowflake 가 우아한 방법으로 해결했기에 가장 많이 선택되고 있습니다. 

+ 다만, 대부분의 기업들은 엄청난 트래픽을 다루는것은 아니기에, ULID 로도 충분하다는 의견을 주셨다.

Snowflake 를 직접 구현해서 쓰셨다는 생생한 경험담을 얘기해주셨는데,

직접 인프라운영할 때와 aws 에서 돌리는게 운영 차이가 있기도하고,

중앙 집중식 보다 차라리 겹칠 경우 fail & retry 가 더 나았다는 경험담을 전해주셨다)

(귀중한 경험담을 공유해주신 @CharSyam 님께 다시한번 감사 말씀 드립니다)

 

참고자료

 

혹시 틀린 부분이 있다면, 언제든 편하게 지적해주세요!

 

이메일 ian.ilminmoon@gmail.com