✅ 기본키를 UUID로
제시된 스키마와 시나리오를 통해 커피주문서비스 백엔드 서버를 구현하는 첫번째 프로젝트를 진행하던 도중 제시된 스키마의 기본키 타입이 binary(16)으로 제시된걸 발견
CREATE TABLE products
(
product_id BINARY(16) PRIMARY KEY,
-- 기타 컬럼 생략..
);
이는 UUID를 기본키로 할 때 사용된다는 것을 알 수 있었다.
UUID를 기본키로 구현해보는건 처음이라 고민하고 있던 찰나에 조원분께서 공유해주신 자료…
🟡 UUID 사용시 주의점
1️⃣
java.util에서 제공하는 UUID클래스는 UUIDv4 인데 해당 방식은 단순 랜덤으로 값을 생성한다.
그런데 MySQL에선 기본적으로 B-Tree로 데이터를 관리하기때문에 항상 정렬된 상태로 유지한다.
기본적으로 삽입되는 데이터의 기본키를 기준으로 구조를 재배치하는데 auto_increment와 같은 순차적인 값을 넣을 때는 재배치를 하지않지만 UUID와 같은 순서가 없는 무작위값은 값을 넣을때마다 재배치를 하게된다.
2️⃣
6670ab71-6ccd-11ef-a031-6ff42f42c220
그리고 UUID는 위와 같이 기본적으로 16진수 32개로 이루어진 16바이트의 크기를 가지지만(중간 필드 구분을 위한 ‘-’ 제외) 이를 데이터베이스에 그대로 문자열로 저장한다면 32자리이기 때문에 32바이트가 된다.
이는 가장 많이 기본키로 사용되는 bigint auto_incremet(8byte)보다 큰 용량을 차지하게 된다.
따라서 UUID를 기본키로 사용하려면 위의 블로그 내용처럼 몇가지 최적화 과정을 거쳐야한다.
🟡 최적화
1️⃣ UUIDv4가 아닌 UUID v1 사용해 순차적인 값 생성하기
UUID v1은 UUID v4와는 다르게 Timestamp를 기반으로 값을 생성한다.
따라서 순차적인 값을 생성하려면 UUID v1을 사용해야 한다.
UUID v1을 사용하려면 다음과 같은 의존성을 추가해야 한다.
[build.gradle]
// uuid 라이브러리 추가
// <https://mvnrepository.com/artifact/com.fasterxml.uuid/java-uuid-generator>
implementation 'com.fasterxml.uuid:java-uuid-generator:4.3.0'
UUID 생성
for (int i = 0; i < 10; i++) {
// UUIDv1 10개 생성
System.out.println(Generators.timeBasedGenerator().generate());
}
/**
22ac6ce0-6ccf-11ef-a2bd-e1442e2a1c50
22ac6ce1-6ccf-11ef-a2bd-a5f69ee8a63c
22ac6ce2-6ccf-11ef-a2bd-9732b7c79d2d
22ac6ce3-6ccf-11ef-a2bd-158564ee9aef
22ac6ce4-6ccf-11ef-a2bd-ef7862ba573e
22ac6ce5-6ccf-11ef-a2bd-1d4ebbd9126c
22ac6ce6-6ccf-11ef-a2bd-0daf84a8b183
22ac6ce7-6ccf-11ef-a2bd-fb732b7afb28
22ac6ce8-6ccf-11ef-a2bd-55701f78e934
22ac6ce9-6ccf-11ef-a2bd-ef03b97bc749
*/
UUID v1의 각 필드는 다음과 같은 의미를 가진다.
Timestamp - Timestamp - Timestamp & Version - Variant & Clock Sequence - Node id
시간에 대한 정보는 첫번째 필드, 두번째 필드, 세번째 필드의 뒷 세자리(앞에는 버전 정보가 들어감)에 들어간다.
특이한 점은 하위비트가 첫번째 필드에 들어간다는 것이다.
이말은 앞 쪽 필드일수록 시간 단위가 작다는 것을 의미한다.
시간에 대한 완벽한 순차적인 값을 얻어내려면 앞에가 큰 단위가 와야 하는데 UUID v1에선 반대인 셈이다.
따라서 필드의 순서를 다음과 같이 바꿔줘야 한다.
→ 3 - 2 - 1 - 4 - 5
for (int i = 0; i < 10; i++) {
UUID uuidV1 = Generators.timeBasedGenerator().generate();
String[] uuidArr = uuidV1.toString().split("-");
String sequentialUUID = uuidArr[2]+"-"+uuidArr[1]+"-"+uuidArr[0]+"-"+uuidArr[3]+"-"+uuidArr[4];
System.out.println(sequentialUUID);
}
/**
11ef-6cd2-9b614625-99e5-1f0e2818858c
11ef-6cd2-9b627ea6-99e5-e18bbd583ade
11ef-6cd2-9b627ea7-99e5-5370cf5f1b7e
11ef-6cd2-9b627ea8-99e5-83a30ccd4277
11ef-6cd2-9b627ea9-99e5-b5cf78504024
11ef-6cd2-9b627eaa-99e5-edec967b21f4
11ef-6cd2-9b627eab-99e5-7f45ea3e74ff
11ef-6cd2-9b627eac-99e5-01a44342ff9a
11ef-6cd2-9b627ead-99e5-4f332750ee61
11ef-6cd2-9b627eae-99e5-9918de5281ae
*/
완벽하게 순차적인 값이 나오는 것을 알 수 있다.
부가적인 이야기이긴 하지만 이 타임스탬프가 한번 순환하는데 까지 17592년이 걸린다고 한다.
여기서 한 가지 의문
🤔 한번 순환하는데 17592년이 걸리면 앞에 TimeStamp정보만 있으면 되는거 아닌가?
의문에 대한 해답
💡결론은 4.5번째 필드도 필요하다.
시간자체는 17592년이 지나야 한번 순환하기 때문에 겹치기 매우 어렵지만, 분산시스템의 경우 여러 서버 또는 장치가 동시에 UUID를 생성할 수 있기 때문에 시간정보로만 유일성을 보장하기 어렵다
2️⃣ UUID를 binary 형태로 저장하기
11ef-6cd2-9b614625-99e5-1f0e2818858c
UUID는 16진수 32개로 이루어져 있기 때문에 실제 크기는 16바이트이지만, 문자열로 저장하면 문자하나당 1바이트이기 때문에 32바이트가 된다.
따라서 문자열 형식이 아닌 binary형태로 저장해야 한다.
MySQL에서는 binary라는 타입을 제공한다. 크기까지 정할 수 있는데 binary(16)은 16바이트를 의미한다.
따라서 binary(16) 타입으로 UUID를 저장해야 한다.
문자열 형태의 UUID를 byte[] 배열로 변환하는 Java 코드
public static byte[] hexToBytes(String uuid) {
ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
bb.putLong(Long.parseUnsignedLong(uuid.substring(0, 16), 16));
bb.putLong(Long.parseUnsignedLong(uuid.substring(16), 16));
return bb.array();
}
✅ JPA @Entity의 @id의 타입 고민
BINARY(16)은 JPA Entity에서 어떤 타입으로 매핑하는게 좋을까?
크게 두 가지 후보가 존재한다
1️⃣ UUID
@Id
private UUID productId;
2️⃣ byte[]
@Id
private byte[] productId;
1️⃣ UUID
JPA는 BINARY(16)을 UUID로 변환해주는 기능이 존재한다. 그렇기 때문에 UUID로 하는것도 괜찮아보인다.
@Entity(name = "products")
public class Product {
@Id
private UUID productId;
}
하지만 이 변환되는 UUID는 UUID v4로 생성된다. 현재 나는 UUID v1을 사용하고 있기 때문에 별도의 변환과정이 필요하다.
변환시키는 방법은 @Converter를 정의해주고 @Id에 @Convert를 적용시켜주는 것이다.
@Converter 클래스 정의
AttributeConverter인터페이스를 구현한다.
@Converter(autoApply = true)
public class UUIDConverter implements AttributeConverter<UUID, byte[]> {
@Override
public byte[] convertToDatabaseColumn(UUID uuid) {
if (uuid == null) {
return null;
}
String[] uuidV1Parts = uuid.toString().split("-");
String sequentialUUID = uuidV1Parts[2]+uuidV1Parts[1]+uuidV1Parts[0]+uuidV1Parts[3]+uuidV1Parts[4];
String sequentialUuidV1 = String.join("", sequentialUUID);
ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
bb.putLong(Long.parseUnsignedLong(sequentialUuidV1.substring(0, 16), 16));
bb.putLong(Long.parseUnsignedLong(sequentialUuidV1.substring(16), 16));
return bb.array();
}
@Override
public UUID convertToEntityAttribute(byte[] dbData) {
if (dbData == null) {
return null;
}
// BINARY(16) 데이터를 다시 UUID로 변환
ByteBuffer bb = ByteBuffer.wrap(dbData);
long high = bb.getLong();
long low = bb.getLong();
return new UUID(high, low);
}
}
@Convert 적용
@Id
@Convert(converter = UUIDConverter.class)
private UUID productId;
뭔가 번거롭다는 생각이 들었다 그래서 다른 방법이 없을까 고민해봤다.
2️⃣ byte[]
UUID로 하게 되면 별도의 컨버터 클래스를 정의해야 했다.
그래서 컨버터 클래스를 정의하지 않도록 byte[]로 해서 바로 매핑되도록 했다.
@Id
private byte[] productId;
이렇게 되면 MySQL의 binary(16)자체가 byte[]이기 때문에 별도의 컨버팅 과정을 거치지 않아도 된다.
byte[]로 하기로 결정하였다.
'데브코스 > 실습 & 프로젝트' 카테고리의 다른 글
[1차 프로젝트] - Toss Payments 결제 API를 통해 Spring boot 서버 구현하기 (4) | 2024.09.12 |
---|---|
[1차 프로젝트] 코드리뷰 피드백 반영하기 (0) | 2024.09.09 |
게시판 만들기 - 데이터 계층 리팩토링 (JPA 도입) (2) | 2024.09.02 |
게시판 만들기 - 기능 추가: 댓글(대댓글) 기능 추가 (0) | 2024.08.30 |
게시판 만들기 - 기능추가: Pagination(페이지네이션) (0) | 2024.08.27 |