데이터베이스 식별자 타입 전략
by mooowu데이터베이스 설계에서 기본키(Primary Key) 선택은 시스템의 성능과 확장성을 결정하는 핵심 결정 중 하나다.
Primary Key 유형과 특성
데이터베이스에서 사용되는 주요 Primary Key 유형은 크게 세 가지로 나눌 수 있다. Auto Increment는 데이터베이스가 자동으로 순차적인 정수 값을 생성하는 방식이다. UUID는 128비트 길이의 고유 식별자로 중앙 집중식 조정 없이도 전 세계적으로 유일한 값을 생성할 수 있다. 마지막으로 시간 기반 순차적 ID는 ULID나 Snowflake ID처럼 시간 정보를 포함하면서도 순차성을 보장하는 하이브리드 방식이다.
Auto Increment의 장점과 한계
Auto Increment는 뛰어난 성능을 보장한다. 순차적 삽입으로 인해 인덱스 분할이 최소화되고, 4바이트 또는 8바이트의 작은 저장 공간을 사용한다. 설정이 단순하고 인덱스 크기와 메모리 사용량을 예측하기 쉽다는 장점도 있다.
하지만 현대적 애플리케이션 개발에서는 여러 한계가 드러난다. 식별자 생성이 데이터베이스에 의존적이어서 분산 환경에서 문제가 된다. 미리 ID를 알 수 없어 테스트 코드 작성이 복잡해지고, 순차적 패턴으로 인한 보안 취약성도 존재한다. 특히 URL 경로에서 사용자 ID나 주문 번호가 노출되면 쉽게 추측 가능하며, 가입자 수나 주문량 같은 비즈니스 정보까지 유추될 수 있다.
UUID의 이점과 트레이드오프
UUID는 Auto Increment의 한계를 해결하는 강력한 대안이다. 애플리케이션에서 미리 ID를 생성할 수 있어 데이터베이스 독립성을 확보할 수 있다. 고정된 ID로 테스트를 작성할 수 있어 개발 편의성이 높아지고, 추측 불가능한 랜덤 값으로 보안성도 향상된다. 여러 시스템에서 충돌 없이 생성할 수 있어 분산 환경에 적합하다.
하지만 UUID 사용에는 명확한 비용이 따른다. 랜덤 삽입으로 인한 인덱스 분할이 빈번하게 발생하고, 16바이트 또는 36바이트의 큰 저장 공간이 필요하다. 더 큰 인덱스 크기로 인해 메모리 사용량이 증가하고, 디버깅 시 식별이 어려워진다는 가독성 문제도 있다.
MySQL에서 UUID 사용 시 성능 문제의 원인
MySQL InnoDB 엔진에서 UUID 사용 시 심각한 성능 저하가 발생하는 이유는 Clustered Index 구조에 있다. InnoDB는 Primary Key Index와 실제 데이터를 물리적으로 동일한 구조로 저장한다. 즉, 테이블의 데이터가 Primary Key 순서대로 물리적으로 정렬되어 디스크에 저장된다.
UUID처럼 랜덤한 값이 중간에 삽입되면 전체 데이터 행들이 물리적으로 재배치되어야 한다. 이는 대량의 디스크 I/O를 발생시키고, 메모리에 캐시된 페이지들을 대량으로 무효화시킨다. 또한 모든 보조 인덱스가 Primary Key 변경의 영향을 받아 추가적인 업데이트가 필요하다. 실제 성능 테스트에서 AUTO_INCREMENT 대비 약 300-400%의 성능 저하가 발생한다.
PostgreSQL에서의 다른 양상
PostgreSQL은 MySQL과 근본적으로 다른 저장 구조를 사용한다. Heap Table 구조에서는 데이터와 인덱스가 물리적으로 분리되어 있다. 데이터는 삽입 순서대로 저장되고, Primary Key Index는 별도의 구조로 관리된다.
PostgreSQL에서 UUID가 삽입될 때는 새 행이 파일 끝에 추가되고, Primary Key 인덱스에서만 B-Tree 재정렬이 발생한다. 실제 데이터 행의 물리적 이동은 발생하지 않는다. 따라서 성능 저하가 SERIAL 대비 약 20-30% 수준으로 MySQL 대비 훨씬 적다.
Page Split과 데이터베이스 내부 동작
데이터베이스 성능을 이해하기 위해서는 Page Split 개념을 알아야 한다. PostgreSQL은 데이터를 8KB 크기의 Page 단위로 관리한다. Page Split은 새로운 데이터를 삽입할 때 해당 Page에 공간이 부족한 경우 발생한다.
Page Split 과정은 다음과 같다. 먼저 공간 부족을 감지하고, 시스템에서 새로운 8KB Page를 할당한다. 기존 Page의 튜플을 두 Page로 분할하고, 상위 노드의 포인터 정보를 수정한다. 마지막으로 Write-Ahead Logging에 변경사항을 기록한다.
이 과정에서 CPU 오버헤드, I/O 증가, Lock 경합, Buffer Pool 압박 등의 성능 영향이 발생한다. Auto Increment는 항상 마지막 Page에 추가되어 Split이 거의 발생하지 않지만, UUID는 랜덤 삽입으로 인해 Split이 빈번하게 발생한다.
PostgreSQL의 MVCC 특성도 중요하다. 삭제된 튜플이 즉시 제거되지 않고 "dead tuple"로 남고, UPDATE도 새 행 삽입 방식으로 처리된다. 이로 인해 물리적 재정렬 부담이 원래부터 적은 구조를 갖는다.
Stripe의 실제 구현 사례
Stripe는 외부 인터페이스의 보안성과 가독성, 그리고 내부 성능 최적화를 동시에 달성한 훌륭한 설계 사례를 보여준다. Stripe의 ID는 리소스 타입을 명확히 식별하는 접두사와 16자리 영숫자 조합으로 구성된다. 고객은 'cus_', 결제는 'pi_', 구독은 'sub_' 등의 접두사를 사용한다.
이러한 설계는 여러 장점을 제공한다. 타입 안정성을 확보하여 컴파일 타임에 오류를 감지할 수 있고, 로그에서 즉시 리소스 타입을 파악할 수 있어 디버깅이 용이하다. 추측 불가능하면서도 의미 있는 구조로 보안성을 확보하고, 새로운 리소스 타입을 쉽게 추가할 수 있는 확장성도 갖췄다.
Stripe는 내부적으로 이중 구조를 사용한다. 데이터베이스에서는 성능을 위해 Auto Increment를 사용하고, 외부 API에서는 의미 있는 식별자를 제공한다. 내부 조인은 정수 ID로 처리하여 성능을 최적화하면서, 외부 인터페이스에서는 보안성과 가독성을 확보한다.
현대적 해결책: 애플리케이션 레벨 ID 생성
MySQL 8.0에서 제공하는 순차적 UUID 생성 기능은 여전히 데이터베이스 의존적이라는 한계가 있다. 이를 해결하기 위해 애플리케이션 레벨에서 동일한 효과를 낼 수 있는 방법들이 개발되었다.
ULID는 48bit 타임스탬프와 80bit 랜덤으로 구성되어 밀리초 정밀도로 자연스럽게 시간순 정렬된다. Base32 인코딩으로 가독성이 좋고 사전식 정렬이 가능하다.
Snowflake ID는 41bit 타임스탬프, 10bit 머신 ID, 12bit 시퀀스로 구성된다. 분산 환경에서 머신 ID로 충돌을 방지하고, 시간순 정렬을 보장한다.
이러한 애플리케이션 레벨 ID 생성은 여러 장점을 제공한다. 데이터베이스 독립성을 확보하고, 시간순 정렬로 MySQL 성능을 최적화할 수 있다. 머신 ID로 분산 환경에서의 충돌을 방지하고, 미리 정해진 ID로 테스트를 작성할 수 있다. 여러 관련 테이블에 동시 삽입도 가능하다.
실무에서의 선택 기준
데이터베이스 식별자 전략은 시스템의 요구사항과 데이터베이스의 특성을 종합적으로 고려해 선택해야 한다. 단일 데이터베이스 환경에서는 Auto Increment가 성능상 유리하지만, 분산 환경이나 마이크로서비스 아키텍처에서는 UUID나 시간 기반 순차적 ID가 더 적합하다.
PostgreSQL에서는 아키텍처 특성상 UUID 사용의 페널티가 상대적으로 적어 현실적인 선택이 가능하다. MySQL에서는 성능 저하가 크므로 신중한 고려가 필요하다.
최적의 접근법은 Stripe처럼 계층별 분리 전략을 사용하는 것이다. 내부적으로는 성능을 위한 Auto Increment를 유지하고, 외부 인터페이스에서는 UUID나 애플리케이션 레벨에서 생성한 순차적 ID를 사용한다. 이를 통해 성능과 편의성, 보안성을 모두 확보할 수 있다.
- https://kenwagatsuma.com/blog/postgresql-btree-is-non-balanced
PostgreSQL B-Tree is non-balanced - Ken Wagatsuma's Homepage
An exploration of PostgreSQL's non-balanced B-tree implementation, explaining how its unique approach to handling page splits and key distribution differs from textbook balanced B-trees while still maintaining optimal performance characteristics.
kenwagatsuma.com
- https://www.postgresdba.com/bbs/board.php?bo_table=C05&wr_id=274
블로그의 정보
블로그
mooowu