1. 들어가며
이번 패키 업데이트에 웹 버전을 출시하여 앱을 설치하지 않은 사람도 웹으로 선물박스를 확인할 수 있도록 할 예정이다.
웹 출시 이전에는 아래 플로우에 따라 카카오톡으로 받은 선물박스를 앱에서만 확인할 수 있었다.
- 클라이언트는 선물박스 만들기 API의 응답값으로 받은 선물박스 ID를 사용하여 선물박스를 열 수 있는 딥링크를 생성한다.
- 생성된 딥링크이 추가된 카카오톡 템플릿을 생성한다.
- 선물박스를 받은 유저는 카카오톡에서 '패키 앱에서 열어보기' 버튼을 눌러 선물박스를 앱에서 확인할 수 있다.
이번 업데이트에서는 2, 3번 과정에서 딥링크 대신 제작한 웹 링크( packyforyou.com?box={giftBoxId} )를 넣고, 웹 사이트 내에 디퍼드 링크를 넣어 웹에서 앱으로 유저를 이동시키도록 하려고 한다.
기존에 앱 서비스만 운영할 때는 선물박스 식별 값을 PK 값인 id를 사용하였다. 현재 PK 생성 전략으로는 auto increment 전략을 사용하고 있다.
만약 웹 링크에서도 id를 사용하게 된다면 다른 선물박스 id를 쉽게 유추하여 다른 선물박스를 열람할 수 있게 된다.
이전에 선물박스 만들기 API를 만들 때 응답값으로 id와 함께 UUID를 넘기도록 구현해두었기 때문에, giftBoxId로 UUID 값을 사용하고자 했다.
2. 문제 상황
But . . .
클라이언트 개발자님의 개인 사정으로 어플리케이션 단 코드를 수정하지 못한다는 제약이 발생하였다.
카카오톡 템플릿에 들어가는 URL은 '딥링크 → 패키 웹 도메인'으로 변경이 가능하지만, 어플리케이션 → 카카오톡으로 데이터를 전달하는 것은 id로 구현되어 있기 때문에 이것을 UUID로 변경하지 못하는 상황이다.
>>> 💌 Mission: 선물박스의 ID 값을
1. 숫자로 이루어져야 함
2. 고유성을 지켜야 함
3. 쉽게 유추할 수 없어야 함
이라는 조건을 지키는 값으로 변경해야 한다 !
숫자로 이루어져야 하는 이유는 클라이언트 단 데이터 모델에서 선물박스의 id를 Int 또는 Long으로 받기 때문이다.
추가로 선물박스 만들기 API의 응답값(DTO)에 들어가는 값만 난수로 변경하면 되지 않느냐?라는 생각도 해보았지만 선물박스 생성 이후 단계에서 id 값을 Query Parameter로 다른 API를 호출하기 때문에 서버 단 코드의 수정 양이 어마어마해진다 🫠
즉! 위 조건을 충족하도록 선물박스 엔티티의 PK 생성 전략을 수정해야 한다 !!!
다양한 PK 생성 전략을 찾아보았고, 그중 내가 최종적으로 선택한 것은 TSID(Time-Sorted Unique Identifier)이다.
내가 TSID를 선택한 이유는 '유추 불가능하며 숫자로만 구성됨'이 핵심이긴 하지만, TSID는 이뿐만 아니라 대규모 분산 시스템에서 고유성과 성능을 보장할 수 있는 기법이라는 장점도 있기 때문에 이 부분도 함께 알아보자.
TSID는 ULID와 트위터의 스노우 플레이크(snowflake) 아이디어를 결합한 것이므로, 이 2가지에 대해 먼저 간략히 알아보자.
3. TSID
3-1. ULID
48bit의 Timestamp(UNIX Time, 밀리초 단위)와 80bit의 랜덤 문자열로 이루어져 있다.
밀리초 단위의 정밀도를 가지며, 밀리초 내에 동시 생성되면 무작위성에 따라 순서가 달라진다.
- ULID(26문자)는 Base32 인코딩을 사용하여 UUID(Base16 인코딩, 36문자)보다 짧은 문자열을 가짐
- 특수문자, 대소문자 구분 X
- 시간순 정렬 가능
- 128bit로 상대적으로 용량이 큼
자세한 내용은 ULID 공식 깃허브를 참고하자.
3-2. 트위터의 스노우 플레이크(snowflake)
초당 6,000개 이상의 트윗이 생성되는 트위터는 고가용성 방식으로 초당 수만 개를 생성할 수 있으며 대략적으로 정렬이 가능한 ID 전략이 필요했고, 이를 위해 스노우 플레이크 기법을 제시하였다.
Sign, Timestamp, Worker Number, Sequence Number 4부분으로 구성되어 있다.
- Sign (1bit): 음수와 양수를 구별
- Timestamp (41bit): UNIX Time 사용, 밀리초 단위
- 오버 플로우 방지를 위해 시작 시간을 1288834974657(2010년 11월 4일 10시 42분 54초)을 기준으로 함
- 최댓값은 69년 (2^41 - 1 = 2199023255551ms)
- 시간 순으로 정렬 가능
- Worker Number (10bit): 데이터센터(5bit)와 서버(5bit)를 구분하는 역할
- 각각 32개의 데이터센터와 서버를 지원할 수 있음
- 시스템 시작 시 결정되며, 일반적으로 시스템 운영 중에는 바뀌지 않음
- 시작 시 Zookeeper를 통해 선택됨
- Sequence Number(12bit): 각 서버에서 ID를 생성할 때마다 1씩 증가
- 1밀리초가 경과할 때마다 0으로 초기화
- 1밀리초 내에 두 개 이상의 ID가 생성되지 않는 한 일반적으로 0
분산 환경에서는 유일한 키를 생성하기 위해 티켓 서버(Ticket Server)를 두는데, 이는 SPOF(Single Point of Failure)라는 단점이 있다.
스노우플레이크 기법은 Worker Number를 두기 때문에 특정 시스템에 의존한다는 티켓 서버의 문제점을 해결하였으며, 나아가 어떤 데이터센터나 서버에서 생성한 ID인지 알 수 있게 되었다.
초당 생성 가능한 ID 수도 약 4,096,000개로 넉넉하며 타임스탬프를 앞 쪽에 배치하여 시간 순 정렬도 가능하다.
3-3. TSID
TSID는 ULID와 스노우플레이크 기법을 결합한 아이디어로, 42bit의 타임스탬프와 22bit의 랜덤 구성 요소로 이루어져있다.
랜덤 구성 요소는 node와 counter로 이루어져 있다.
노드는 인스턴스를 식별하는 번호이며, 카운터는 스노우플레이크의 sequence number와 동일하게 각 노드에서 생성할 때마다 1씩 증가하며 밀리초가 지나면 0으로 초기화된다.
노드는 기본값이 10bit(1024개)이며, 직접 설정하지 않으면 랜덤한 값으로 설정된다. 노드의 bit 수와 값은 직접 조절할 수 있다.
TSID 또한 시간 순 정렬이 가능하며, 최대 69년 사용 가능하다.
4. Spring Boot에서 TSID 사용하는 방법
이제 Spring Boot에서 TSID를 사용하는 방법을 알아보자.
먼저 build.gradle에 의존성을 추가한다. 이때, Hibernate 버전에 따라 다른 버전을 사용하므로 자신에게 맞는 버전을 사용하자.
아래에 나오는 코드는 Hibernate 6을 기준으로 작성되었다.
implementation 'io.hypersistence:hypersistence-utils-hibernate-63:3.7.6'
라이브러리를 추가한 후 '@Tsid' 어노테이션을 사용하여 쉽게 사용할 수 있다!
public class GiftBox {
@Id
@Tsid
private Long id;
// 중략 ...
}
만약 노드 bit, epoch 기준 시간 등을 변경하고 싶으면 Supplier<TSID.Factory>를 확장하는 커스텀 클래스를 생성하고, @Tsid 어노테이션의 파라미터로 전달하면 된다.
커스텀 가능한 값들은 TSID.Factory 구현 코드에서 더 확인 가능하다.
public static class CustomTsidSupplier implements Supplier<TSID.Factory> {
⠀
@Override
public TSID.Factory get() {
return TSID.Factory.builder()
.withNodeBits(1)
.build();
}
}
public class GiftBox {
@Id
@Tsid(CustomTsidSupplier.class)
private Long id;
// 중략 ...
}
5. 자바스크립트 관련 주의사항
자바스크립트에서 정수의 표현은 53비트로 제한되어 있어, 아래 콘솔 결과처럼 뒷 숫자의 정확도를 잃을 수 있다.
그러므로 생성된 아이디를 문자열로 다뤄주어야 한다.
6. 마치며
@GeneratedValue의 GenerationType에서 제공하는 기본 키 생성 전략의 경우 특정 DB에 의존적이거나, 성능 이슈가 있고 PK 생성 전략을 직접 설계하기는 막막하였는데 TSID를 사용하여 현재 상황에 맞게 PK 생성 전략을 간편하게 수정할 수 있었다.
단일 서버로 서비스를 운영해왔기 때문에 분산 시스템에서의 고유성 문제는 생각해보지 않은 영역이였는데 이번에 TSID에 대해 조사하며 좋은 인사이트를 얻었다.
Reference
- https://github.com/f4b6a3/tsid-creator
- https://ssdragon.tistory.com/162
- https://vladmihalcea.com/tsid-identifier-jpa-hibernate/
- https://jeong-pro.tistory.com/251
- https://velog.io/@ssssujini99/%EA%B0%9C%EB%B0%9C-idPK-%EC%A7%81%EC%A0%91%ED%95%A0%EB%8B%B9-%EC%A0%84%EB%9E%B5-Random-UUID-TSID-%EA%B0%81%EA%B0%81-%EB%B9%84%EA%B5%90%EB%B6%84%EC%84%9D#82-tsid-%EC%9D%B4%EC%9A%A9%EC%8B%9C%EC%97%90-%EB%B0%9C%EC%83%9D%ED%95%9C-%EA%B8%B0%EC%96%B5%EC%97%90-%EB%82%A8%EC%95%98%EB%8D%98-%EC%9D%B4%EC%8A%88