본문 바로가기
클라우드 저장소 만들기

클라우드 저장소 만들기 - UUID PK, 어떻게 만들어서 어떻게 저장할까?

by 정말반가워요 2025. 3. 2.

지난 이야기

클라우드 저장소의 '파일' 테이블의 PK를 UUID로 만들기로 정했다.

파일 식별자는 url에 직접 사용될 예정이므로, 예측 불가능하게 만들어야 하기 때문이다.


이제 '어떻게 만들어서 어떻게 저장할까?'에 대해서 생각해봤다.

Java 서버에 MySQL DB를 사용하는 상황.

가장 간단한 방식부터 시작해서 개선해 보자.

 

첫째 (프로젝트 초기 세팅 상황)

자바: UUID.randomUUID().toString(); 으로 UUID 생성

DB: VARCHAR에 저장

첫째 방식 문제점.

일단 자바 애플리케이션에서의 문제를 보자.

java.util.UUID.java의 randomUUID()는 UUIDv4를 반환한다.

 

UUIDv4는 완전 랜덤이다.

그리고 우리는 이 완전히 랜덤한 문자를 DB의 PK로 사용하려 한다.

DB 입장에서 무엇이 문제될까?

 

PK에는 인덱스가 걸린다.

인덱스는 일반적으로 B+Tree 자료구조이며, 여기서 봐야 할 건 인덱스가 '정렬된 상태를 유지'하려 한다는 점이다.

 

정렬 상태를 유지하는 자료구조에 랜덤한 값이 난입하게 되면 인덱스 재구성 비용이 크게 발생한다.

하지만 순차적 값(기존 리프노드의 모든 값과 비교했을 때 가장 큰 값)이 들어오면 인덱스 재구성 비용이 적다.

리프노드 최우측에 페이지를 하나 더 만들고 값을 추가한 다음 변경사항을 브랜치 노드로 전파하면 되기 때문이다.

 

첫째 문제(인덱스 재구성 부하)의 해결책

UUIDV7을 사용한다.

기존 시간순 정렬 UUID인 UUIDV1의 mac주소 보안문제를 개선한 UUID이다.

생성시간 기준 오름차순 정렬이 가능하다.

프로젝트에 UUIDv7 생성이 가능한 라이브러리를 추가해서 사용하기로 했다.

둘째.

자바: UUIDv7 생성

DB: VARCHAR에 저장

둘째 방식 문제점.

이젠 DB 컬럼의 용량 차지가 문제다.

애플리케이션 코드가 생성하는 UUID를 보자.

// 구분자 제외 총 32글자. 8-4-4-4-12 형태의 16진수 문자를 표시하는 문자열 반환
// 01afb2e0-c5ef-70a3-8b7f-c15e98d2a71f 이렇게!
UuidCreator.getTimeOrderedEpoch().toString()

(UTF-8기준)

UTF-8은 최대 4바이트를 차지하는 가변 길이 문자셋이며 ASCII문자 저장에는 1바이트로 충분하다.

01afb2e0-c5ef-70a3-8b7f-c15e98d2a71f

위 문자열의 모든 글자는 ASCII 체계에 포함된다.

따라서 불필요한  구분자를 제거하면 32바이트를 차지한다.

 

이게 최선일까?

각 문자는 16진수 문자이다. 

16진수 한 글자를 표현하려면 4비트면 충분한데, '문자로 표현'하기 위해 8비트(1바이트)를 쓰고 있는 상황이다.

DB에 굳이 '사람이 읽기 좋은' 형태로 데이터를 저장할 필요는 없다.

 

둘째 문제(인덱스 재구성 부하)의 해결책

DB에 Binary를 그대로 저장할 것이다. (컬럼타입 Binary(16))

그럼 byte 배열 변환을 해야 할텐데. 변환 API는 uuid-creator 깃허브 페이지의 wiki에 적혀 있었다.

그다음 저장한 걸 읽어올 땐 디코딩을 해야 한다. UUID로 바꾸고 싶을 땐 decode()를 호출하면 됨.

UUID uuid = //...
byte[] bytes = BinaryCodec.INSTANCE.encode(uuid);

 

최종 결과.

자바: UUIDv7 생성

DB: Binary(16)에 저장

종합적인 기대  결과

이번 결정으로 얻게 된 결과를 정리해보자.

1. UUIDv7로 인덱스 페이지 재구성 부하 감소

2. 컬럼 크기 최소화로 데이터 크기 감소

 

이러한 이득은 MySQL(InnoDB)과 같이 Clustered Index를 사용하는 DB일때 더 클 것이라 생각된다.

왜 그럴까?

1. 인덱스 재구성 부하 감소에 대해서 (출처가 확실한 정보가 아닌 내 추측)

Primary Index는 테이블 전체의 데이터를 가지고 있다.

PK에 순차적 데이터가 아닌 랜덤한 데이터가 들어온다면,

 

랜덤한 데이터가 들어옴으로 인해 발생하는

'페이지 분할 비용'과

'분할된 이후 페이지의 잉여 공간이 남는 문제'가

'테이블 풀 스캔의 성능하락'으로까지 이어질 확률이 높을 것이다.

 

2. 데이터 크기 감소에 대해서

아래 블로그에서 PK 크기를 작게 유지할 때의 이점에 대해서 말하고 있다.

If the UUID has to be a primary  key, the gain is even greater, 
as in InnoDB the primary key value is copied into all secondary index values.

출처: https://dev.mysql.com/blog-archive/storing-uuid-values-in-mysql-tables/

Secondary Index는 인덱스의 맨 끝에 PK를 함께 보관한다(이 PK를 단서로 인덱스 스캔 이후 Primary Index를 찾아간다).

그러므로  'PK 용량 절약'은 'Secondary Index의 개수'만큼 이득을 본다.

반대로 PK 용량 낭비는 그만큼의 손해가 될 것이다.

 

 

MySQL 공식문서에서 '웬만하면 PK는 Auto Increment 쓰세요' 하는 문장을 본 적이 있다(문장이 정확하지 않을 수 있음).

그 이유는 아마 위 2가지 이유가 크지 않을까 싶었다.


 

부록 - 조금 더 자세한 B+Tree 페이지 분할 과정

리프 노드에 데이터를 저장하려고 할 때, 공간이 모자랄 경우 발생하는 상황은 다음과 같다.

 

1. 페이지를 2개로 분할해서 기존의 데이터를 나눠 담는다.
2. 중간값을 상위 노드에 복사하여 올린다 (이정표로 사용하기 위함) 
3. 값을 전달받은 상위 노드 또한 공간이 부족하면 페이지를 분할하고 상위 노드에 변경사항을 전파하는데, 

    이는 루트 노드까지 연쇄적으로 이루어질 수 있다.
4. 1번 과정에서 페이지 분할이 발생함과 동시에 '데이터는 거의 그대로인데 페이지는 2개가 되는, 

    '채워지지 않은 페이지'' 상황이 발생한다.

    실제 데이터 용량보다 데이터를 담기 위한 그릇의 크기가 커지는 것이다.
5. 따라서 인덱스는 삽입과 삭제가 많이 일어날 경우 '채워지지 않은 페이지'가 B+Tree 곳곳에 산재되는 문제가 발생할 수 있으며, 

    이는 저장공간 비효율과 스캔 성능 저하로 이어질 수 있다.
6. 5번의 문제 때문에 인덱스의 KEY는 '가능하다면' 1, 2, 3...과 같이 순차적인 값이 들어오게 하는 것이 좋다. 

    '채워지지 않은 페이지' 문제가 'B+Tree의 맨 오른쪽'에서만 발생하기 때문이다.

 

    또한 B+Tree의 리프노드는 연결리스트 형태로 구성된다.

    '페이지를 찢고 데이터 분할 + 앞뒤 리프노드와 연결하는 비용' 

    보다는 '페이지를 하나 더 추가하고 연결하는 비용'이 저렴할 것이다. 이 부분에서도 추가적인 이득이 있을 것 같다.


뭔가 이렇게 보면 UUIDv7이 짱짱맨 같은데, 쓰면 안 되는 상황도 있지 않을까?

일단 쉽게 생성시점 추적이 가능한 문제가 있다.

생성한 UUID를 구글링해서 찾은 변환 코드에 넣으니 생성시점이 정확하게 추출되었다. 

 

(생성시점이 노출되면 안되면서 + Auto Increment처럼 순서 추적도 불가능하게) 해야 할 때도 있을 것이다.

구글링 결과 reddit에서 여러 의견들을 보았다.

PK는 Auto Increment로 쓰고, url 노출은 UUIDv4로 사용하는 전략을 추천하는 사람도 있었다(2종류의 식별자 사용).

개발을 하면서 어떤 자원의 식별자를 url에 노출시킬 때

노출되어도 괜찮은 데이터인지 한번씩 생각해볼 필요는 있을 것 같다.


이렇게 UUID에 대해서 탐구하고,

UUID를 DB에 저장하는 전략과 그 이점에 대해서 알 수 있게 되었다.

이 글 내용 중에 문자셋과 Binary 저장 부분은 내가 예전엔 이해하지 못했었는데

이젠 이해할 수 있게 되어서 기쁘다. 나름 내공이 어느정도 쌓였나보다.

 

기반 지식에 도움을 주신 유튜버 널널한개발자님

디비안 SQL 서적의 저자님

검색중 발견한 레딧의 고수님

늘 함께하는 AI님에게도 감사감사.