해보자
지난 글(SnowFlake 적용기 - Unique한 값)에서는 SnowFlake를 조사하게 된 계기(?)를 이야기 했다. 이번에는 실제로 적용해보자.
Twitter의 GitHub에 실제 채번을 위한 코드가 있지만, 이는 Scala로 작성되어 있었다.
감사하게도 누군가 실제로 구현한 코드가 있어서 이해한데로 간단하게 재구성하게 되었다.
재구성한 환경은 Spring Boot + JPA를 사용하는 프로젝트이며, Custom한 PK 생성을 위해서 수정하였다.
@MappedSuperclass
@ToString
@Getter
public class BaseEntity implements Serializable {
@Id
@GenericGenerator(name = "global_seq_id", strategy = "com.kgr.responsebodyadviceexample.core.component.SnowFlakeGenerator")
@GeneratedValue(generator = "global_seq_id")
private Long id;
}
Entity 전체에 BaseEntity를 상속하여 모든 ID 채번을 정의한 Generator로 하게끔 해준다.
strategy에 정의된 클래스파일이 실제 채번을 수행하는 Core가 되며, Entity에 상속시켜준다.
다음은 실제 채번이 이루어지는 Generator이다.
@Component
@NoArgsConstructor
public class SnowFlakeGenerator implements IdentifierGenerator {
private static final int CASE_ONE_BITS = 10;
private static final int CASE_TWO_BITS = 9;
private static final int SEQUENCE_BITS = 4;
private static final int maxSequence = (int) (Math.pow(2, CASE_ONE_BITS) - 1); // 2^5-1
private static final long CUSTOM_EPOCH = 1420070400000L; // 41bit
private volatile long sequence = 0L;
private int case_one = 10;
private int case_two = 0;
private volatile long lastTimestamp = -1L;
@Override
public Serializable generate(SharedSessionContractImplementor sharedSessionContractImplementor, Object o) throws HibernateException {
return nextId();
}
private static long timestamp() {
return Instant.now().toEpochMilli() - CUSTOM_EPOCH;
}
public synchronized long nextId() {
long currentTimestamp = timestamp();
if (currentTimestamp < lastTimestamp) {
throw new IllegalStateException("Invalid System Clock!");
}
if (currentTimestamp == lastTimestamp) {
sequence = (sequence + 1) & maxSequence;
if (sequence == 0) {
currentTimestamp = waitNextMillis(currentTimestamp);
}
} else {
sequence = 0;
}
lastTimestamp = currentTimestamp;
return makeId(currentTimestamp);
}
private Long makeId(long currentTimestamp) {
long id = 0;
id |= (currentTimestamp << CASE_ONE_BITS + CASE_TWO_BITS + SEQUENCE_BITS);
id |= (case_one << CASE_TWO_BITS + SEQUENCE_BITS);
id |= (case_two << SEQUENCE_BITS);
id |= sequence;
return id;
}
private long waitNextMillis(long currentTimestamp) {
while (currentTimestamp == lastTimestamp) {
currentTimestamp = timestamp();
}
return currentTimestamp;
}
}
Hibernate의 IdentifierGenerator를 implements 해서 생성방식을 Custom할 수 있도록 해준다. 실제로 @TableGenerator로 정의되어 있는 ID도 결국 IdentifierGenerator를 재구성하여 ID 값을 채번하도록 되어있다.
비트는 시간비트 41, 각각 커스텀할 비트로 CASE_ONE_BITS, CASE_TWO_BITS는 10과 9, 마지막 중복방지 Sequence비트로 4을 배정하였다. 로직에서 save가 일어나면, generate()를 타고 채번을 진행하게 되며, 채번을 진행하는 메서드인 nextId()에 있는 주요 과정을 살펴보자.
private static long timestamp() {
return Instant.now().toEpochMilli() - CUSTOM_EPOCH;
}
currentTimeStamp에 시간을 넣어주게된다. 의문이 들었던 것은 미리 지정해둔 CUSTOM_EPOCH를 빼주는 작업이었다. 왜 이작업을 해줄까 하며 생각해봤는데, 내린 결론은 41비트를 맞춰주기 위함이었다. 처음 toEpochMilli() 를 통해서 가져온 시간은 38비트 였는데, 저기에 41비트인 CUSTOM_EPOCH를 뺴줌으로써 비트를 맞춰주게 되었다.
if (currentTimestamp == lastTimestamp) {
sequence = (sequence + 1) & maxSequence;
if (sequence == 0) {
currentTimestamp = waitNextMillis(currentTimestamp);
}
} else {
sequence = 0;
}
private long waitNextMillis(long currentTimestamp) {
while (currentTimestamp == lastTimestamp) {
currentTimestamp = timestamp();
}
return currentTimestamp;
}
lastTimestamp에는 마지막으로 채번을 진행한 시간이 저장되어 어플리케이션실행중에 유지가 되는데, 이시간과 currentTimestamp가 같다는 것은 동일한시간에 들어온 요청으로 판단하여, sequence를 하나씩 올려줌으로써 채번을 진행할 수 있도록 한다. 이 때, 정해둔 sequence 의 최대값을 넘어설 경우, waitNextMillis()에서 그시점에 다시 현재시간을 받아서 처리할 수 있도록 해준다.
private Long makeId(long currentTimestamp) {
long id = 0;
id |= (currentTimestamp << CASE_ONE_BITS + CASE_TWO_BITS + SEQUENCE_BITS);
id |= (case_one << CASE_TWO_BITS + SEQUENCE_BITS);
id |= (case_two << SEQUENCE_BITS);
id |= sequence;
return id;
}
마지막으로 비트이동을 통해서 채번값을 반환해준다. 처음에 정의해준 비트만큼 이동해서 or 연산을 통해 값을 바꿔주며, 원하는 위치의 비트까지 이동시켜서 연산을 진행하게 된다. 최종적으로 반환된 ID값이 DB에 저장된다.
정리
지금은 비트 구성을 임시 네이밍으로 사용하였지만, 운영하고 있는 장비 구분값과 서비스 형태 등과 같은 네이밍으로 비트를 구성하게 된다면, 장비에 Unique한 채번값이 아닌 서비스 전체에 Unique한 채번값을 얻어낼 수 있다.
SnowFlake를 실제로 적용해보면서, 만들어놓은 코드가 있어서 그런지 생각보다 막막하지는 않았던 것 같았다. 혼자 했으면 비트연산도 생각을 좀 해봐야하고, 무조건 시간비트 41비트를 어떻게 맞추냐부터 머리가 아팠을 것 같다.
그런데 꼭 64비트가 아니어도 좀더 작은 비트수를 구성하여 채번을 진행할 수도 있을 것 같다. 세세하게 Unique한 값을 유지하고 싶다면 비트가 많이 필요하겠지만, 한두개의 케이스로도 구분할 수있다면 64비트보다 좀더 작은 비트를 사용해도 될 것같다. 물론 시간비트를 줄이게 된다면, 최대 사용할 수 있는 범위가 확실히 줄어들기 때문에 시간을 줄일 수는 없는 노릇이고.... 어쨌든 제공하는 서비스에 맞도록 전략을 잘 세우면 좋은 대안이 될 것 같다.
SnowFlake를 적용했다고 모든게 끝나는 것은 아니었다. 생각지도 못한 문제가 발생해서........
다음글에서는 적용하면서 겪었던 이슈들을 기록하려고 한다.
'Tech' 카테고리의 다른 글
Event가 동작할 것만 같지? - 잔여 이벤트 처리하기 (1) | 2023.05.01 |
---|---|
Event가 동작할 것만 같지? - Spring Event와 Domain Event (0) | 2023.05.01 |
Dead Code 분석도구 - Scavenger (Feat. DEVIEW 2023) (0) | 2023.03.27 |
SnowFlake 적용기 - JavaScript의 자료형 문제 (0) | 2023.01.10 |
SnowFlake 적용기 - Unique한 값 (0) | 2023.01.07 |