0. 개요
쇼핑몰 프로젝트를 진행하며 회원 테이블의 PK 생성 전략에 대해 고민했던 내용을 공유하려고 한다.
DB는 MySQL을 사용했다.
1. 문제
프로젝트 요구사항에서 DB는 확장 가능하고, 인증을 위해 회원의 PK는 외부에 노출되어도 문제 없어야 했다.
이를 위해 기본키 생성 전략으로 아래의 5가지 방법에 대해 고민했고, 결론부터 말하자면 TSID를 사용했다.
- Auto_Increment
- UUID
- ULID
- Snowflake ID
- TSID
2. 생성 전략들
1) Auto_Increment
개요
- 데이터베이스에서 자동 증가(시퀀스) 값을 사용하여 유일한 식별자(Primary Key) 를 생성하는 방식.
- 보통 INTEGER 또는 BIGINT 타입의 값을 사용하며, 새로운 레코드가 추가될 때마다 1씩 증가함.
특징
- 간단한 구현: 데이터베이스 자체 기능으로 제공됨.
- 정렬된 값: 오름차순으로 정렬되므로 클러스터링 인덱스 성능이 좋음.
- 충돌 없음: 기본적으로 한 테이블 내에서 중복되지 않음.
단점
- 분산 환경에서 충돌 발생: 여러 서버에서 동시에 데이터를 삽입하면 충돌 가능.
- 예측 가능성: 연속된 값이므로 보안상 예측이 가능함.
- 삭제 시 갭 발생: 중간 값이 삭제되면 숫자가 비게 됨. (한 번 사용한 숫자는 다시 사용 하지 않는다)
2) UUID
개요
- 128비트(16바이트) 크기의 고유 식별자로, 전 세계적으로 중복되지 않는 값을 생성할 수 있음.
- 보통 UUID v4 (랜덤) 또는 UUID v1 (시간 기반) 이 많이 사용됨.
특징
- 전역적으로 유일: 여러 서버에서 동시에 생성해도 중복 가능성이 거의 없음.
- 문자열 형태: 일반적으로 8-4-4-4-12 형식(36자 길이의 문자열)으로 표현됨.
ex) 550e8400-e29b-41d4-a716-446655440000 - MySQL, PostgreSQL 등의 DB에서 지원됨.
단점
- 긴 길이(16바이트): DB에서 많은 공간 차지.
- DB 인덱스 성능 저하: 프로젝트에서 MySQL을 사용하는데, MySQL은 클러스터드 인덱스 방식으로 B+ Tree를 사용한다. 따라서 정렬된 데이터를 저장하기 위해 데이터 삽입 시 항상 정렬하는데 UUID(보통 v4 사용함)는 무작위 값이기 때문에 삽입되는 위치가 예측할 수 없고 랜덤하다. 이로인해, 클러스터드 인덱스의 성능이 급격히 떨어진다.
- DB 저장 방식: DB에 저장할때, BYTE(16) 또는 CAHR(36) 중 선택하는데 byte 타입은 성능은 좋지만 가독성이 떨어져서 사람이 값을 보기 위해 추가 작업이 필요함.
3) ULID
개요
- UUID의 단점(정렬 불가능, 긴 길이)을 개선한 버전.
- 128비트이지만 가독성이 더 좋고 시간 정렬이 가능함.
특징
- 시간 기반, 정렬 가능: 처음 48비트는 Timestamp(1ms 단위) 기반으로 생성하기 때문에 시간순 정렬이 가능함.
- UUID보다 짧은 길이: 26자 길이의 문자열.
ex) 01H0ZKQH0YYAFB1JAG7P7N9S1X
단점
- DB 인덱스 성능 저하: 앞의 48비트가 Timestamp 방식으로 생성되어도 거의 동일한 시간 간격에 생성됐을 경우, 결국 뒤의 랜덤 16자리에 의해 정렬되므로 인덱스 성능 저하는 피할 수 없다.
4) Snowflake ID
개요
- 트위터(Twitter)에서 만든 분산 시스템용 고유 ID 생성 알고리즘.
- 64비트 크기의 정수 값으로 구성되며, 아래와 같은 비트 구조를 가짐.
특징
- 시간 정렬 가능: 41비트 타임스탬프 덕분에 삽입 순서 보장.
- 고유성 보장: 분산 환경에서 각 노드가 고유한 시퀀스 번호와 타임스탬프를 사용하여 고유성을 보장함. (약 69년까지)
- 짧은 길이: 64비트 정수로 설계됨.
단점
- 구현이 복잡함
- 시간대 문제가 발생할 수 있음: 서버의 시간이 잘못 설정되면, ID가 생성되는 순서가 꼬일 수 있습니다.
- 동일 밀리초 내에서 제한된 개수: 동일 밀리초 내에서 생성할 수 있는 ID 수가 4096개로 제한.
5) TSID
개요
- ULID와 Snowflake ID를 합쳐서 ID를 구성한 오픈소스 라이브러리.
- 64비트 크기의 시간 정렬 가능한 고유 ID.
- 랜덤성을 추가하여 예측 방지.
특징
- 시간 기반 (앞 42비트): 초 단위 타임스탬프 포함.
- 타입(Long/String) 선택 가능: 정수(Long) - 64비트로 저장, 문자열(String) - 13자의 문자열로 저장
- 랜덤성 보장: 마지막 22비트는 랜덤 값.
- 정렬 가능: ULID처럼 정렬 가능.
단점
- 타임스탬프의 한계: 현재 표준 방식은 밀리초 단위로 2020-01-01을 기준으로 하는 타임스탬프를 사용
(70년 또는 140년 동안만 유효하므로, 장기적인 사용에 제한)
3. 결론
- 사용하는 DB(MySQL)의 특성
- MySQL은 기본적으로 InnoDB 엔진을 사용하며, 이 엔진은 클러스터링 인덱스를 사용하며 B+트리 구조를 가진다. 클러스터링 인덱스에서는 레코드의 물리적인 저장 순서가 인덱스 순서와 동일하게 되도록 구성되어 있는데, 랜덤 값인 UUID를 기본 키로 사용할 경우 성능 저하가 발생할 수 있다.
- 랜덤 값 삽입: UUID는 무작위로 생성되기 때문에 데이터가 추가/수정/삭제될 때마다 클러스터링 인덱스가 재정렬되어야 하며, 이로 인해 디스크 I/O와 메모리 사용이 비효율적으로 증가합니다.
- B+ 트리 균형 문제: B+ 트리는 균형 잡힌 트리 구조로 검색 성능을 최적화하지만, UUID와 같은 무작위 값이 삽입되면 트리의 균형이 깨지며, 이로 인해 트리 높이가 증가하고 검색 성능이 저하됩니다.
- MySQL은 기본적으로 InnoDB 엔진을 사용하며, 이 엔진은 클러스터링 인덱스를 사용하며 B+트리 구조를 가진다. 클러스터링 인덱스에서는 레코드의 물리적인 저장 순서가 인덱스 순서와 동일하게 되도록 구성되어 있는데, 랜덤 값인 UUID를 기본 키로 사용할 경우 성능 저하가 발생할 수 있다.
- 구현 난이도
- 랜덤 값이 아닌 생성 전략 중 Snowflask ID vs TSID 비교 결과, 구현이 보다 간단한 TSID를 채택했다.
+ Spring에서 TSID 사용법
1. Maven 의존성 추가
<dependency>
<groupId>io.hypersistence</groupId>
<artifactId>hypersistence-utils-hibernate-60</artifactId>
<version>3.9.0</version>
</dependency>
2. @Tsid 어노테이션 추가
@NoArgsConstructor
@Getter
@Entity
@Table(name="members")
public class Member {
@Id
@Tsid
@Column(name="member_id")
private Long id;
참고
- 2024.10.03 - [NHN Java 백엔드 8기/Relational database] - [Relational database] 04. 파일 조직과 인덱스
- https://velog.io/@wndbsgkr/MySQL%EC%97%90%EC%84%A0-UUID%EB%A5%BC-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%82%AC%EC%9A%A9%ED%95%98%EC%A3%A0
- https://blog.chulgil.me/tsid/
- https://ksh-coding.tistory.com/157
- https://ksh-coding.tistory.com/123
'프로젝트 > 쇼핑몰 프로젝트' 카테고리의 다른 글
[쇼핑몰 프로젝트] Spring MVC 구조 - 단위 테스트 코드 작성하기 (0) | 2025.03.26 |
---|---|
[쇼핑몰 프로젝트] 분산 환경에서 데이터 캐싱 - Redis (1) | 2025.03.24 |
[쇼핑몰 프로젝트] 서버 다중화 환경에서 세션 기반 인증 문제 해결 – JWT 적용 (0) | 2025.03.21 |
[쇼핑몰 프로젝트] 외래 키(FK), NULL이어도 괜찮을까? (0) | 2025.02.02 |
[쇼핑몰 프로젝트] 프로젝트 후기 (0) | 2025.02.02 |