본문 바로가기
Java

데이터를 식별하기 위한 Unique한 ID생성하기 (트위터 스노우플레이크의 java구현)

by 졸린개발자 2022. 10. 12.

안녕하세요. 졸린개발자입니다.

 

오늘은, 데이터를 저장할 때 왜 Unique한 ID를 왜 생성해야하고,

Unique한 ID를 어떻게 생성하는지에 대해 설명하기 위해, 포스트를 작성합니다.

 

추가적으로, 제가 생각하는 가장 좋은 방법인

트위터 스노우 플레이크의 java구현을 마지막으로 해보겠습니다.

 

 

데이터를 저장하는데, 왜 Unique한 ID가 필요할까요?

당연한 말로 들릴 수도 있는데, 하나하나의 데이터를 식별하기 위해서 입니다.

 

예를 들어볼까요?

데이터베이스에 (철수, 2), (영희, 10), (길동, 15)가 저장되어 있다고 합시다.

이 데이터는 (이름, 나이)입니다.

 

이때, 철수의 나이를 찾으려면 어떻게 해야할까요?

이름에 철수가 있는 데이터 부분의 2번째에 나이가 존재합니다.

 

여기서 여러분들은 방금, 철수라는 ID를 가지고, 나이라는 값을 찾아낸 것입니다.

 

하지만, 이러한 ID가 Unique하지 않다면 어떻게 될까요?

다시 예를 들어봅시다

데이터베이스에 (철수, 2), (철수, 8), (영희, 10), (길동, 15)가 저장되어 있다고 합시다.

 

그럼 철수의 나이는 어떻게 될까요?

2일까요? 8일까요?

이렇게 하나하나의 데이터를 식별하기 위해서 Unique한 ID를 생성하는 것이 필요할 때가 있습니다.

 

여러 SQL팁을 보다보면, 왠만해선 PK를 생성하라는 것이,

Unique한 ID를 붙여, 서로 다른 데이터를 식별하기 위함입니다.

 

이제 Unique한 ID를 생성하는 다양한 전략에 대해서 살펴봅시다.

 

 

DB에서 제공하는 방법 (AUTO_INCREMENT, Sequence 등등......)

가장 손쉬운 방법입니다.

 

일단, AUTO_INCREMENT나 Sequence나 특정 숫자부터 차례대로 채번한다는 매커니즘은 같기 때문에

AUTO_INCREMENT로 통일해서 작성하겠습니다.

 

가장 쉬운 이유는, 데이터를 하나하나 넣을때 마다 DB에서 자동으로 채번을 해주어,

Unique함을 보장해 주기 때문입니다.

 

추가적으로, 동시성에도 강하기 때문에, 편하면서도 강력한 기능입니다.

 

하지만, 단점도 있습니다.

 

첫번째로, 분산 시스템에서는 절대로 사용할 수 없습니다.

분산 시스템에서는, DB가 여러대로 나뉘어지는 것이 보통입니다.

이때 여러 DB에서 AUTO_INCREMENT를 쓴다면, 당연히 같은 값이 겹칠 수 밖에 없겠죠?

 

두번째는, ID를 통한 정보 노출의 가능성입니다.

간단한 예를 들어봅시다.

어떤 서비스에서 User의 고유한 ID가 100만 단위의 ID가 존재하면,

그 서비스에서는 당연히 100만의 유저 숫자가 있음을 쉽게 유추할 수 있습니다.

이렇게 분석을 당할 수도 있죠.

 

 

UUID를 사용하는 방법

UUID를 사용하는 방법도 아주 간단합니다.

 

모르시는 분들을 위해 간단하게 설명하면,

UUID는 unique한 ID를 생성하는 규약입니다.

 

여러분이 사용하시는 대부분의 언어에는 이러한 UUID를 생성할 수 있는

패키지나 모듈이 존재합니다.

 

실제로는 UUID가 겹칠 가능성이 아주 없지는 않는데,

그럴 일은 거의 없다고 생각하고 배제하고 개발을 하는것이 보통입니다.

 

그래서 이러한 UUID로 ID를 생성하면

550e8400-e29b-41d4-a716-446655440000

이러한 형태가 나오게 되죠.

 

이렇게 되면 장점으로는

첫번째, 분산 시스템에도 아주 적당합니다.

각 시스템에서 자신의 ID를 생성하면 되기 때문이죠.

 

두번째로, 정보 유출의 가능성도 거의 없습니다.

차례차례 부여되는 숫자가 아니니, 그럴 가능성도 없죠.

 

하지만 단점도 당연히 존재합니다.

 

첫번째로는, UUID가 겹칠 가능성입니다.

아주 없다고는 말 못하죠.

 

두번째로는, UUID는 숫자가 아니라 문자라는 것입니다.

보통 Unique한 ID를 저장할때, 문자가 아니라, 숫자로 저장하는 것을 생각해보면

추가적인 변환 과정이 더 필요하게 되겠죠.

 

 

트위터 스노우 플레이크

마지막 방법입니다.

 

이 방법은 트위터에서 사용하는 방법입니다.

우선, 트위터에서는 64비트로 스노우 플레이크를 구성한다고 합니다.

 

첫 1비트는 sign을 나타내구요

그 다음 41비트는 TimeStamp

그 다음 5비트는 데이터센터ID

그 다음 5비트는 서버ID
그 다음 12비트는 일련번호를 사용합니다

 

TimeStamp는 기본적으로 epoch time을 생각하시면 됩니다.

하지만 epoch time과 다른점은 시작시간이죠.

 

1970년 1월 1일부터 시작하는 epoch time과 달리,

여기서 TimeStamp는 바로 오늘, 2022년 10월 12일 오후 8시 51분 부터 시작해도 됩니다.

 

epoch time을 그대로 사용하면 41비트밖에 사용못해서 금방 고갈이 될 것입니다.

 

일련번호는, 같은 초에 여러개의 요청이 들어오면, 증가하게 되는 값입니다.

그리고 TimeStamp의 1초마다 초기화 되는 값입니다.

그래서 1초에 12비트, 즉 4095개의 데이터가 생성되지만 않으면 같은 숫자가 될 수 없습니다.

 

이렇게 트위터 스노우 플레이크 방법을 사용하게 되면,

절대로 같은 숫자가 겹칠 일이 없고, 분산 시스템에서도 사용할 수 있습니다.

 

그럼 코드로 구현해 봅시다.

 

 

getUniqueId

public static long getUniqueId(SequenceIdGenerator sequenceIdGenerator, int dataCenterId, int serverId) {
    long timeStamp = Instant.now().toEpochMilli() & 2_199_023_255_551L;
    long sequenceId = sequenceIdGenerator.getSequence() & 4095;

    long result = 0;
    result |= sequenceId;
    result |= (serverId << 12);
    result |= (dataCenterId << 17);
    result |= (timeStamp << 22);

    return result;
}

트위터 스노우플레이크 방법으로, Unique한 ID를 생성해주는 함수입니다.

 

기본적으로 3개의 인자를 받습니다.

1초마다 0으로 초기화되는 일련번호를 생성해줄 generator (이 Generator는 아래에 구현 코드가 존재합니다.)

그리고 DataCenter와 Server의 ID입니다.

 

코드를 위에서부터 차례대로 설명하겠습니다.

long timeStamp = Instant.now().toEpochMilli() & 2_199_023_255_551L;

현재 시간의 epoch time을 받아, 하위 41비트만을 사용하기 위해

41비트를 전부 1로 채운 값과 and 연산해줍니다.

 

long sequenceId = sequenceIdGenerator.getSequence() & 4095;

그리고, sequenceIdGenerator로부터 일련번호를 받은 뒤, 하위 12비트만 사용하기 위해,

12비트를 전부 1로 채운값인 4095를 and연산해줍니다.

 

long result = 0;
result |= sequenceId;
result |= (serverId << 12);
result |= (dataCenterId << 17);
result |= (timeStamp << 22);
return result;

그리고, 차례차례 비트 연산으로 result에 넣어 return해줍니다.

 

 

 

SequenceIdGenerator

public class SequenceIdGenerator extends Thread {

    private int sequence;

    @Override
    public void run() {
        while(true) {
            try {
                Thread.sleep(1000);
                sequence = 0;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public int getSequence() {
        return this.sequence++;
    }

}

간단한 일련번호 generator입니다.

 

1초마다 0으로 초기화 되어야 하기 때문에,

간단하게 무한 loop로 1초마다 0이 되게 설정합니다.

 

그리고, 이러한 무한 loop는 독립적인 실행흐름이 필요하므로,

Thread를 상속해서 run에 초기화 코드를 넣어줍니다.

 

그리고 getSequence를 호출할때마다 일련번호를 return해주고 하나씩 증가시킵니다.

 

한번 간단하게 실행해본 결과입니다.

now: 2022-10-12T21:23:17.822066200 SequenceId : 0
now: 2022-10-12T21:23:18.138015 SequenceId : 1
now: 2022-10-12T21:23:18.447946400 SequenceId : 2
now: 2022-10-12T21:23:18.756924 SequenceId : 3
now: 2022-10-12T21:23:19.067859800 SequenceId : 0
now: 2022-10-12T21:23:19.376907700 SequenceId : 1
now: 2022-10-12T21:23:19.685543200 SequenceId : 2
now: 2022-10-12T21:23:19.999339400 SequenceId : 0
now: 2022-10-12T21:23:20.310122500 SequenceId : 1
now: 2022-10-12T21:23:20.620894100 SequenceId : 2

1초마다 0으로 초기화 되는 것을 볼 수 있으시죠?

 

 

테스트

public static void main(String[] args) throws InterruptedException {
    SequenceIdGenerator sequenceIdGenerator = new SequenceIdGenerator();
    sequenceIdGenerator.start();

    while(true) {
        System.out.println("now: " + LocalDateTime.now() + " ID: " + getUniqueId(sequenceIdGenerator, 1, 1));
        Thread.sleep(300);
    }
}

 

간단한 테스트입니다.

무한 Loop를 돌면서, 300millis만큼 쉬면서 ID를 생성해줍니다.

 

 

결과

now: 2022-10-12T21:25:17.496864500 ID: 6985938443943677952
now: 2022-10-12T21:25:17.816231 ID: 6985938445285855233
now: 2022-10-12T21:25:18.129607400 ID: 6985938446598672386
now: 2022-10-12T21:25:18.430350600 ID: 6985938447861157891
now: 2022-10-12T21:25:18.740333500 ID: 6985938449161392128
now: 2022-10-12T21:25:19.052087700 ID: 6985938450470014977
now: 2022-10-12T21:25:19.363798900 ID: 6985938451774443522
now: 2022-10-12T21:25:19.672381 ID: 6985938453070483456
now: 2022-10-12T21:25:19.983182 ID: 6985938454374912001
now: 2022-10-12T21:25:20.294231500 ID: 6985938455679340546
now: 2022-10-12T21:25:20.605424900 ID: 6985938456983769088

 

Unique한 ID를 생성해 주는 것을 확인해 보실 수 있습니다.

 

 

결론

저도 사실 개인적인 프로젝트를 하면서,

구현이 간단하다는 이유만으로 AUTO_INCREMENT를 즐겨 썼었습니다.

 

하지만 오늘 이렇게 Unique한 ID를 생성해주는 Gererator에 대해 공부하면서,

더 좋은 방법에 대해 알 수 있게 되어 기분이 좋네요.

 

여러분도 좋은 하루 보내시기 바라겠습니다.

 

 

참고자료

https://blog.twitter.com/engineering/en_us/a/2010/announcing-snowflake

https://darkstart.tistory.com/147

'Java' 카테고리의 다른 글

Java에서의 정규표현식 Flag  (0) 2022.09.30
Java classpath란?  (0) 2022.05.18