TSID를 사용해야하는가?
TSID는 보통 서버용 프로그래밍에서 사용하는 int, Long 타입의 auto increment에 비해 실행 중인 노드 정보 및 시간 정보를 포함하여 64비트 단위로 id를 생성하고 활용하는 방법이다.
대부분의 애플리케이션에서는 필요없다. DB가 분산시스템으로 2대 이상 존재하며 서로 싱크를 맞춰야하는(샤딩) 상황에서 필요하다.
TSID에 대한 개념은 이 글에서 적지 않는다. 좋은 참고자료들이 많다.
https://jsonobject.tistory.com/634
https://tech-monster.tistory.com/228
TSID를 사용해야할 때, 사용하는 장점
여러 시스템이 동시에 DB에 insert를 할 때도 id가 중복나는 경우가 거의 발생하지 않기 때문에 안전하다. 그리고 Long 타입으로 인코딩 변환하여 활용하면 정렬도 지원되므로 단순 유일성을 보장하는 uuid 대비해서도 유리하다.
Long(bigint), auto increment를 사용할 때 고려할 점
db lock
여러 대의 서버가 1대의 DB를 바라보고 ID를 생성하는 경우라도, DB에서 기본적으로 ID 생성 시 lock을 잡아서 유일성이 보장된다. 너무 많은 삽입이 동시에 일어난다면 해당 락 때문에 성능 저하가 걸릴 수도 있겠다. 그러나 이런 부하가 걸리는 경우라면 ID 생성 lock 외에도 레코드 삽입용 lock도 걸릴텐데, 이 부분이 더 큰 성능 저하 포인트가 될 수도 있을 것 같다. 참고로 TSID 생성 방식은 DB에서 ID 생성을 하도록 요청하는 것이 아니고 애플리케이션단에서 생성하기 때문에 DB의 ID 생성 락은 피할 수 있다.
bin, char 대신 Long(int, bigint)을 사용하는 이유
TSID 외에 bin, int, char 타입을 적용할 수 있다. char는 문자열이라 길게 저장할 수는 있으나 block에 저장하는 용량이 크고, bin은 사람이 탐색하기에는 불편한 점이 많으므로 int 타입을 사용하여 가성비가 좋게 설계한다.
int vs long
대표적인 64비트를 지원하는 언어들(자바, 자바스크립트, 파이썬 등)은 bigint 단위인 약 9경에 이르는 숫자까지 지원한다. 특히 파이썬은 9경 이상의 숫자를 사용하더라도 숫자 크기에 제한이 없기 때문에 오류가 발생하지 않는다고 한다. 따라서 long 타입을 가정하고 사용해도 된다. 다만 32비트 php 같은 구식 서버 같은 경우 int 단위인 약 21억 정도까지만 지원하기 때문에 long 타입으로 설계된 서버와의 통신을 위해서는 long 타입의 ID를 인코딩 및 디코딩하여 문자열로 주고받는 방식을 사용해야할 수 있다.
ULID
TSID도 좋을 수 있지만, TSID는 java 체계에서 지원하므로 javascript 등 일반적으로 client에서 사용하는 체계에 전달할려면 따로 변환이 필요하다. 그래서 ULID를 추천한다. ULID는 아래와 같은 장점들이 있다.
- key 내에 시간 base의 hashing 값이 들어가서 정렬 가능
- key 내에 인스턴스 구분자가 들어가므로 멀티 인스턴스 환경에서도 중복없이 사용 가능
- java 뿐 아니라 모든 언어에서 호환 가능하므로 별도 변환 필요없음
- DB의 sequence를 바라보는 것이 아니므로 DB 의존성과 상관없음. 혹시 마이그레이션을 하더라도 DB sequence를 신경쓸 필요가 없음
다만 TSID와 마찬가지로, 단순 Long 타입에 비해 길이가 길어짐에 따라 조회, 정렬 시 성능상의 손해(약 3~4배, 수~ 수십 ms 정도의 손해임), 인덱스 생성 크기 증가(이것도 3~4배)가 발생하는 점은 감수해야한다.
DB 별 특징
mongDB
기본적으로 TSID와 같은 개념으로 노드의 키값과 타임스탬프를 기반으로 하는 Object_ID라는 것을 사용한다. 애초에 디비 자체가 그런 분산 시스템을 위해 설계되었다.
MySQL
PK로 지정된 id 값이 클러스터링된다. 순서대로 auto increment되던 중 특정 id에 해당하는 레코드가 삭제되고, 그 이후에 다시 그 번호로 레코드가 삽입되면 재정렬되면서 pk의 정렬 순서를 항상 맞춘다.
PostgreSQL
MySQL과 달리 삭제되고 나중에 추가된 번호가 작은 id값이 재정렬되지 않는다. 조회 시마다 정렬을 해야해서 부하가 걸릴 수 있다.
ULID 사용 시 주의사항
문제:
순서대로 저장했음에도 디비에 저장된 순서는
table의 ID순서로 정렬을 했음에도 3번째 컬럼이 [0,7,14,21]순이 아니게 조회되는 현상

문제이유:
ULID의 데이터는 앞에 10자리는 시간( ms까지의 시간데이터) + 랜덤 문자로 구성이 되어있는데
ms까지 시간이 같아 순서대로 Insert가 되었음에도 시간값이 같아 뒤에 Random문자열로 정렬이 되어버리는 현상
→ 정렬을 하고 싶으면 특정 컬럼으로 다시 해야하는 번거로움 발생
시간의 ms 까지 같아져버리면 데이터는 순차적으로 들어갔으나, 쿼리를 통해 조회했을때 제대로 정렬이 안될 수 있다.
해결방안:
public class HibernateULIDGenerator implements IdentifierGenerator {
private static final ULID ULID = new ULID();
private static Value lastValue = null;
@Override
public synchronized Serializable generate(
SharedSessionContractImplementor session,
Object object) {
synchronized (HibernateULIDGenerator.class) {
if (lastValue == null) {
lastValue = ULID.nextValue();
} else {
lastValue = ULID.nextMonotonicValue(lastValue);
}
return lastValue.toString();
}
}
}
synchronized를 작성함으로써, 동시에 접근을 못하게 막고, Monotonic ULID를 사용하여 같은 ms 안에서는 이전 값보다 반드시 큰 시퀀스 값을 붙여 순서를 보장하도록 설정하였다.
같은 시각(ms 단위)일 때, 맨 끝자리가 순서대로 기입되도록 할 수 있게 된다.

-> 이 방식은 synchronized를 사용하기 때문에 분산 환경에서 동시성에 의한 한계가 있다. 또한 결국 유추가 가능하기 때문에 보안상 위험이 발생할 수도 있다.
추가내용
일반적으로 내부적으로는 DB Sequence에 의한 id를 그대로 사용하고, 추가로 uuid 컬럼을 두어 외부에 노출할 id는 uuid로 활용하는 방법도 있다.
'Programming-[Backend] > Database' 카테고리의 다른 글
| JPA, ddl-auto, MYSQL, index 등 테이블 생성 기초 정리 (0) | 2025.01.12 |
|---|---|
| id 설정하기. batchInsert, UUID, GenerationType.IDENTITY (0) | 2024.12.15 |
| [TIL] MySQL 사용 관련 주요 팁 모음: 타입, INET_ATON, FK 물리적으로 걸지 않기 (0) | 2024.08.26 |
| [경험 요약] Atomikos multi-database transaction 묶기 (0) | 2024.05.04 |
| [TIL][link] DB Connection Pooling, context manager (0) | 2023.09.18 |