snowflake 기반 Id 생성 - ttasjwi/board-system GitHub Wiki
개요
- 데이터베이스에 데이터를 저장할 때 고유 식별자(PK)가 필요하다.
- PK 는 어떤 방식으로 생성하는게 좋을까?
자연키 vs 인공키
- 자연키 (Natural Key): 현실 세계에서 의미를 갖는 값으로 구성된 식별자
- 예: 주민등록번호, 학번, 이메일, 전화번호 등
- 인공키 (Surrogate Key): 현실적 의미와 무관하게 데이터베이스 내부에서 인위적으로 생성한 식별자
- 예: Auto Increment 숫자, UUID, Snowflake ID 등
자연키의 문제점
- 현실 변화에 취약
- 예: 주민등록번호는 법/정책 변경으로 수집이 제한되거나 변경 가능성이 있음
- 현실 속 식별자는 시간이 지나면서 변하거나 폐지될 수 있음
- 고유성 보장 실패 가능성
- 처음엔 중복되지 않던 자연키가 향후 중복될 가능성도 존재 (예: 이메일 재사용, 학번 재할당 등)
- 스키마 의존성 증가
- 자연키를 기본키로 설정하면 변경 시 관계형 데이터 구조 전체를 수정해야 할 수 있음 (외래키 제약 등)
- 결론
- 이러한 이유로, 현실 세계와 무관한 불변의 인공키를 기본키(PK)로 사용하는 것이 일반적으로 안전하고 유연함
- 자연키는 고유 제약조건(UNIQUE) 으로만 관리하고, 기본키로는 인공키 사용이 권장됨
인공키 종류
- DataBase Auto Increment 방식
- 키 생성을 DataBase 에서 해야하므로, 애플리케이션 로직이 DataBase 의존성을 가진다. (최초에 Id 가 없는 문제)
- 향후 데이터베이스를 물리적/논리적으로 분리해서(샤딩) 관리할 경우 PK 가 중복될 수 있어서 식별자 유일성이 보장되지 않을 수 있다.
- 클라이언트 측에 노출하면 보안상 문제가 될 여지가 있다. (예: 가입했더니 1000번 회원 -> 전체 회원이 1000명정도 있다는 것을 유추 가능)
- 그래도 간단하다는 장점이 있다.
- 순차적으로 생성되므로, 중간 삽입이 되지 않아 B+ Tree 재구성, 페이지 분할 드으이
- 유니크 임의 문자열, 숫자 방식
- UUID 또는 난수 방식으로 랜덤 데이터를 생성
- 키 생성을 애플리케이션에서 함으로서, 애플리케이션의 데이터베이스 의존성을 끊어낼 수 있음(이 특징은 아래의 유니크 정렬 문자열, 유니크 정렬 숫자도 공유한다.)
- 키 생성이 간단하다. (그냥 임의 데이터를 생성하면 된다.)
- 하지만 랜덤하다는 점때문에 성능저하가 발생할 수 있다.
- Clustered Index 는 정렬된 상태를 유지한다.
- 데이터 중간삽입 시, 필요한 인덱스 페이지가 가득찼다면 B+ Tree 재구성 및 페이지 재분할로 디스크 I/O가 증가한다.
- PK 를 사용한 범위 조회가 필요하다면, 디스크에서 랜덤 I/O 가 발생하기 때문에 순차 I/O 보다 성능이 저하된다.
- 유니크 정렬 문자열
- 분산환경에 대한 PK 중복 문제 해결
- 보안 문제 해결
- 랜덤 데이터에 의한 성능 문제 해결
- UUID v7, ULID 등의 알고리즘 : 일반적으로 알려진 알고리즘은 128비트를 사용한다.
- 데이터 크기에 따라 공간 및 성능 효율이 달라진다.
- Clustered Index 는 PK 를 기준으로 만들어진다.
- Secondary Index 는 데이터에 접근할 수 있는 포인터를 가지고 있다. (즉, PK 를 가지고 있다.)
- PK가 크면 클 수록, 데이터는 더 많은 공간을 차지하게 되고, 비교 연산에 의한 정렬/조회에 더 많은 비용을 소모하게 된다.
- 유니크 정렬 숫자
- 분산환경 pk 중복 문제 해결
- 보안 문제 해결
- 랜덤 데이터에 의한 성능 문제 해결
- snowflake, tsid 등
- 64비트 사용 (BIGINT)
- 정렬을 위해 타임스탬프를 나타내는 비트 수의 제한 문제가 있다.
- 키 생성을 위한 시간적인 한계가 있다. (snowflake 기준 41비트)
- 문자열 알고리즘에서도 동일한 문제가 있긴한데, 비트수가 많을 수록 이 제한은 덜 할 수 있다.
- 유니크 정렬 문자열 방식보다 적은 공간을 사용한다.
snowflake 키 생성
- X(Twitter)에서 개발
- 분산 시스템에서 고유한 64비트 ID를 생성하는 알고리즘
- [1비트][41비트: 타임스탬프][10비트: 노드 ID][12비트: 시퀀스 번호]
- 분산 환경에서도 중복 없이 순차적 ID 생성하기 위한 규칙
- 타임스탬프 : 순차성
- 노드ID : 고유성
- 시퀀스 번호 : 순차성, 고유성 (동일 시간, 동일 Node 에서 동시에 요청하더라도, 순차성 보장)
- 유니크, 시간 기반 순차성, 분산 환경에서의 높은 성능
/**
* 고유 식별자 생성을 위한 클래스
* X (구 Twitter)의 Snowflake 방식 사용
*
* 64비트 숫자 생성
* 1비트(사용) + 41비트(시간) + 10비트(현재 애플리케이션의 고유 식별자 : 랜덤) + 12비트(동일 애플리케이션 동시요청시 순번)
*/
class IdGenerator private constructor() {
companion object {
private const val UNUSED_BITS: Int = 1
private const val EPOCH_BITS: Int = 41
private const val NODE_ID_BITS: Int = 10
private const val SEQUENCE_BITS: Int = 12
private const val MAX_NODE_ID: Long = (1L shl NODE_ID_BITS) - 1
private const val MAX_SEQUENCE: Long = (1L shl SEQUENCE_BITS) - 1
private val NODE_ID = RandomGenerator.getDefault().nextLong(MAX_NODE_ID + 1)
// 2025-01-01T00:00:00+0900
private val START_TIME_MILLIS = 1735657200000
private var lastTimeMillis = START_TIME_MILLIS
private var sequence = 0L
fun create(): IdGenerator {
return IdGenerator()
}
}
@Synchronized
fun nextId(): Long {
// 현재 시간
var currentTimeMillis = System.currentTimeMillis()
// 만약 현재 시간이 lastTimeMillis보다 작다면, 시스템 클럭이 역행(rollback)된 것으로 판단하고 예외를 발생.
if (currentTimeMillis < lastTimeMillis) {
throw IllegalArgumentException("Invalid Time")
}
// 최근 아이디 발급 시점이, 지금 시각과 같을 때(같은 밀리초에서 여러 개의 ID 를 생성하는 경우)
// 그 안에서 sequence 순번을 매김
if (currentTimeMillis == lastTimeMillis) {
// 순서(sequence) 를 1 증가 시키고, 혹시 최대 자리를 초과할 경우를 다시 버림 처리
sequence = (sequence + 1) and MAX_SEQUENCE
if (sequence == 0L) {
currentTimeMillis = waitNextMillis(currentTimeMillis)
}
} else {
sequence = 0
}
lastTimeMillis = currentTimeMillis
return (((currentTimeMillis - START_TIME_MILLIS) shl (NODE_ID_BITS + SEQUENCE_BITS))
or (NODE_ID shl SEQUENCE_BITS)
or sequence)
}
/**
* 현재 시간이 lastTimeMillis 와 같거나 그 이전일 경우, lastTimeMillis 보다 더 현재 시간이 커질 때까지 대기
*/
private fun waitNextMillis(currentTimestamp: Long): Long {
var currentTimestampVar = currentTimestamp
while (currentTimestampVar <= lastTimeMillis) {
currentTimestampVar = System.currentTimeMillis()
}
return currentTimestampVar
}
}