기존 방식
나는 Spring Boot의 JPA를 사용해서 프로젝트를 다수 진행했다.
기존에 보통 Entity의 키를 결정하던 방식은 Auto Increment 였다.
@Entity
@Getter
@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED)
public class Problem {
@Id
@Column(name = "problem_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
}
이는 기본 키 생성을 DB에 위임하는 전략이다..
기존 방식의 장점
Clustered Index
Primary Key는 MySQL에서 Clustered Index 로 설정된다. Clustered Index로 설정된다는 건, 데이터의 물리적인 위치가 PK의 순서로 결정된다는 것이다. 이 부분에서 Auto Increment는 다음 2가지의 장점을 가지게 된다.
1. 기존 데이터를 건들이지 않고 뒤에 붙이기만 하면 된다. 만약 중간에 삽입된다면 디스크의 위치를 정렬하는 외부 정렬이 일어날 것이다.
2. PK가 특별한 의미를 가지지 않기 때문에 수정할 일이 거의 없다. 수정을 하면 물리적인 위치가 모두 수정될 위험이 있다.
WAS가 여러 개 있을 경우
WAS가 여러 개 있고 WAS에서 PK를 결정하게 된다면 DB에 insert시 충돌이 될 수도 있고, 물리적인 위치가 모두 수정될 위험도 있다.
3. 단일 DB 환경이라면, DB에서 Key를 모두 관리하므로 충돌의 위험이 없다.
기존 방식의 단점
성능
만약 많은 인원들이 북마크를 동시에 많이 한다면, 수많은 데이터가 insert 될 것이다. 이 때, PK를 설정하는 과정에서 DB에 더 긴 Lock이 걸리게 된다. 또한 여러 테이블이 외래키로 연결된 경우라면 연쇄적으로 많은 테이블에 Lock을 걸고, 해제하는 과정이 반복될 것이다.
따라서 DB에게 PK를 결정하도록 하는 것은 특정 상황에서 성능에 문제가 생길 수 있다.
장점은 그대로 가져가되, 단점은 해결할 방법을 찾아보자.
해결방안 1. UUID
DB에게 PK를 결정하지 않게 하고, 보안적인 문제도 해결하기 위해 WAS에서 PK를 결정해 직접 넣어주기로 했다.
따라서 기본적인 UUID를 우선 생각해볼 수 있다.
PK값이 수정될 가능성도 없고, UUID가 충돌될 가능성도 굉장히 적다. 하지만 다음의 단점이 있다.
- Auto Increment의 장점 1번을 버리게 된다. PK를 랜덤으로 정하게 되니 삽입할 때 물리적인 위치가 제일 뒤가 아니다. 따라서 insert 시 대대적인 위치 수정이 일어나게 된다. 성능에 치명적인 단점을 가져오게 된다.
- UUID의 크기는 16바이트이다. 기존에 사용하던 자료형보다 2배 큰 크기를 가진다. 마찬가지로 성능에 좋지 않다.
해결방안 2. 티켓 서버
PK만을 생성하는 서버를 둔다. 구현하기 쉬울 수도 있지만, SPOF가 발생한다. 이를 해결하기 위해서 티켓 서버를 여러 대 둔다면 또 다른 동기화 문제가 발생한다.
https://code.flickr.net/2010/02/08/ticket-servers-distributed-unique-primary-keys-on-the-cheap/
해결방안 3. Tsid
Twitter의 Snowflake와 ULID를 합친 ID 생성기이다.
- 우선, ULID는 UUID의 몇 가지 단점을 해결한 랜덤 생성기이다. 나는 이것보다 Twitter Snowflake를 중점적으로 살펴보겠다.
https://github.com/ulid/spec
Twitter Snowflake
위의 모든 것들을 해결하기 위해 WAS에서 PK를 결정하지만, 다음과 같이 64비트의 값으로 PK를 생성한다.
- 사인(sign) 비트
- 1비트를 할당한다. 지금으로서는 쓰임새가 없지만 나중을 위해 유보해둔다. 음수와 양수를 구별하는데 사용할 수 있을 것이다.
- 타임스탬프(timestamp)
- 41비트를 할당한다. 기원시각(epoch) 이후로 몇 밀리초(millisecond)가 경과했는지를 나타내는 값이다.
- 사인 비트를 제외하고 제일 왼쪽에 있기 때문에, 결국 모든 PK가 밀리초 단위에서는 시간순으로 정렬될 것이다.
- 데이터센터ID
- 5비트를 할당한다. 따라서 2^5 =32개 데이터센터를 지원할 수 있다. 이 값은 시스템 운영 중에 바뀌지 않는다.
- 서버ID
- 5비트를 할당한다. 따라서 데이터 센터 당 32개 서버를 사용할 수 있다. 이 값은 시스템 운영 중에 바뀌지 않는다.
- 일련번호
- 12비트를 할당한다. 각 서버에서는 ID를 생성할 때 마다 이 일련 번호를 1 만큼 증가시킨다. 이 값은 1 밀리초가 경과할 때 마다 0으로 초기화 (reset)된다.
- 다시 말해, 한 서버에서 1 밀리초에 2개 이상의 ID를 생성할 때만 0이 아니게 된다.
조금 더 자세히
이 방식은 다중 WAS 환경에서도 각각의 WAS가 PK를 생성해서 삽입하므로 Auto Increment의 성능상 단점을 해결했다.
그럼 남은 것은 Auto Increment의 장점을 유지하는가 이다. 아까 언급했던 Auto Increment의 장점을 다시 한 번 살펴보자.
1. 기존 데이터를 건들이지 않고 뒤에 붙이기만 하면 된다. 만약 중간에 삽입된다면 디스크의 위치를 정렬하는 외부 정렬이 일어날 것이다.
2. PK가 특별한 의미를 가지지 않기 때문에 수정할 일이 거의 없다. 수정을 하면 물리적인 위치가 모두 수정될 위험이 있다.
3. 단일 DB 환경이라면, DB에서 Key를 모두 관리하므로 충돌의 위험이 없다.
여기서 2번 항목은 성립된다. Tsid로 생성된 것 역시 PK로서의 의미만 지니고 있다.
3번 항목도 성립된다. 각 서버마다 ID가 따로 있고 1 밀리초에 2^12 개의 일련번호도 할당되므로 충돌될 여지가 없다.
그럼 1번 항목을 살펴보자. 일어날 수 있는 경우의 수를 살펴보고 충족되는지 보면 된다.
- 다른 밀리초에 생성된 2개의 PK: 나중에 생성된 PK가 타임스탬프 값이 더 클 것이므로 자연스럽게 뒤에 붙는다.
- 같은 데이터센터, 서버에서 같은 밀리초에 생성된 2개의 PK: 나중에 생성된 PK가 일련번호가 더 클 것이므로 자연스럽게 뒤에 붙는다.
- 다른 데이터센터, 서버에서 같은 밀리초에 생성된 2개의 PK: 만약 서버 ID가 더 작은 서버가 더 나중에 PK를 생성했다면, 문제가 일어난다. 이러면 외부 정렬이 일어난다. 하지만 희귀한 경우이고 정렬할 데이터가 크지 않아 성능에 큰 영향을 미치지 않는다.
마무리
이 원리를 이용해 Tsid는 만들어졌다. 이제 처음의 장점은 모두 취하고, 단점은 모두 해결했다.
관련 라이브러리를 설정한 후 다음과 같이 적용하면 된다.
@Id
@Tsid
@Column
private Long id;
주의해야 할 점
JavaScript/ECMAScript 수준의 정밀도에는 정수의 경우 53비트로 제한되어 있다.
기존 Auto Increment의 경우 53비트를 넘을 경우가 거의 없어 괜찮았지만, Tsid는 64비트를 모두 사용해 문제가 생길 수 있다.
따라서 Response해줄 때 Long을 String으로 바꿔서 전달하는 방식을 사용했다.
'Spring Boot' 카테고리의 다른 글
Spring의 새로운 동기식 HTTP Client, RestClient (0) | 2024.03.09 |
---|---|
JWT 총정리 (2) - Access Token, Refresh Token (1) | 2024.03.08 |
JWT 총정리 (1) - JWT의 원리와 흐름 (1) | 2023.10.09 |
Git in IntelliJ - 인텔리제이에서 Git의 모든 것 (2) (0) | 2023.02.03 |
Git in IntelliJ - 인텔리제이에서 Git의 모든 것 (1) (0) | 2023.01.27 |