데이터베이스 설계에서 유저 ID는 시스템의 근간이 되는 선택입니다.
이 결정은 성능, 보안, 확장성에 직접적인 영향을 미칩니다. 본 글에서는 long (정수형), string (문자열), UUID의 장단점을 실제 사례와 벤치마크 데이터를 통해 비교하고, 여러분의 프로젝트에 가장 적합한 선택을 돕겠습니다.
Instagram은 왜 Snowflake ID를 선택했을까요? Stripe는 왜 접두사가 붙은 문자열 ID를 사용할까요? 그리고 2024년 새로운 RFC 표준인 UUIDv7이 왜 주목받는지 알아봅시다.
Long (정수형 ID): 성능의 왕, 보안의 약점
정수형 ID는 AUTO_INCREMENT나 SERIAL을 사용하는 가장 전통적인 방식입니다. 1, 2, 3처럼 순차적으로 증가하는 간단한 구조죠.
장점: 압도적인 성능과 효율성
저장 공간이 매우 작습니다. INT는 4바이트, BIGINT는 8바이트만 차지합니다. UUID의 16바이트나 문자열의 36바이트 이상과 비교하면 절반 이하입니다.
1천만 건의 레코드 기준으로 PostgreSQL에서 BIGINT 인덱스는 214MB인 반면, UUID 인덱스는 856MB에 달합니다. 이는 4배의 저장 공간 차이를 의미합니다.
데이터베이스 성능이 탁월합니다. B-tree 인덱스는 순차적인 데이터 삽입에 최적화되어 있습니다. KCCoder의 MySQL 벤치마크에 따르면, 2,800만 건의 레코드 삽입 시 정수형 ID는 약 8분이 소요된 반면, UUID(CHAR(36))는 25시간 이상 걸렸습니다.
이는 무려 80배의 성능 차이입니다. PostgreSQL에서도 정수형 ID의 조인 연산은 2.72초인 반면 UUID는 3.07초로 약 13% 느렸습니다.
인간이 읽고 디버깅하기 쉽습니다. "사용자 ID 12345"는 한눈에 이해되지만, "550e8400-e29b-41d4-a716-446655440000"은 그렇지 않습니다. 개발 중 디버깅이나 로그 추적이 훨씬 간편합니다.
단점: 심각한 보안 취약점
열거 공격(Enumeration Attack)에 취약합니다. 이것이 정수형 ID의 가장 큰 문제입니다. 공격자는 /users/1, /users/2, /users/3처럼 단순히 숫자를 증가시키며 모든 사용자를 탐색할 수 있습니다.
OWASP(Open Web Application Security Project)는 이를 "Insecure Direct Object Reference (IDOR)" 취약점으로 분류하며, 2021년 Top 10 보안 위험 중 하나로 꼽았습니다.
실제 사건: Parler 데이터 유출(2021년). 소셜 미디어 플랫폼 Parler는 순차적인 게시물 ID를 사용했고, API에 속도 제한도 없었습니다. 결과적으로 해커들은 단순히 숫자를 증가시키며 70테라바이트 이상의 데이터와 114,000명 이상의 사용자 정보를 스크래핑했습니다.
Salt Security의 분석에 따르면, "순차 식별자는 공격자가 패턴을 파악하고 스크립트를 통해 API와 URL을 무차별 대입할 수 있게 합니다."
실제 사건: AT&T iPad 보안 침해(2010년). AT&T 웹사이트는 순차적인 ICC-ID(SIM 카드 식별자)를 사용자 이메일 주소와 매핑했습니다.
Goatse Security 해커 그룹은 순차적인 ICC-ID를 추측하여 114,000명 이상의 iPad 사용자 이메일 주소를 수집했습니다. 피해자 중에는 뉴욕 시장 Michael Bloomberg도 포함되었습니다.
비즈니스 지표가 노출됩니다. 주문 ID가 12345라면, 공격자는 최소 12,344건의 주문이 있었다는 것을 알 수 있습니다. 경쟁사는 테스트 계정을 만들어 비즈니스 성장률을 추적할 수 있습니다.
이는 "German Tank Problem"이라는 통계 기법으로, 샘플 관찰만으로 전체 모집단을 추정할 수 있습니다.
분산 시스템에 부적합합니다. 여러 데이터베이스 서버에서 동시에 ID를 생성하려면 조정이 필요합니다. 이는 단일 장애 지점(single point of failure)을 만들고 확장성을 제한합니다.
언제 사용해야 할까?
정수형 ID는 내부 전용으로만 사용해야 합니다. 외부 API나 URL에 절대 노출하지 마세요. 최선의 접근은 듀얼 식별자 아키텍처입니다:
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY, -- 내부: 빠른 조인용
public_id UUID NOT NULL UNIQUE, -- 외부: API 노출용
email VARCHAR(255),
created_at TIMESTAMP
);
내부적으로는 정수형의 성능 이점을 누리고, 외부적으로는 UUID로 보안을 확보하는 방식입니다.
UUID: 보안과 분산성의 선택
UUID(Universally Unique Identifier)는 128비트(16바이트)로 구성된 전역적으로 고유한 식별자입니다.
UUID 버전 이해하기
UUIDv4 (랜덤): 가장 널리 사용되는 버전으로, 122비트의 무작위 데이터를 포함합니다. 예: 550e8400-e29b-41d4-a716-446655440000. 초당 10억 개의 UUID를 생성해도 85년 동안 충돌 확률이 50%에 불과할 정도로 안전합니다.
UUIDv7 (시간 기반, 2024년 RFC 9562 표준): 최신 표준으로, 48비트 타임스탬프 + 74비트 무작위 데이터로 구성됩니다. 시간순 정렬이 가능하면서도 충분한 무작위성을 제공합니다. 현대적인 시스템에서 가장 추천되는 선택입니다.
장점: 보안과 분산성
예측 불가능하여 열거 공격을 방지합니다. 122비트의 무작위 공간에서 다음 ID를 추측하는 것은 사실상 불가능합니다. OWASP는 UUID를 IDOR 방어 수단 중 하나로 권장합니다.
비즈니스 정보가 노출되지 않습니다. UUID에서는 총 사용자 수나 성장률을 알 수 없습니다. 프라이버시와 비즈니스 기밀 보호에 유리합니다.
조정 없이 전역적으로 고유합니다. 마이크로서비스, 샤딩된 데이터베이스, 분산 시스템에서 각 노드가 독립적으로 UUID를 생성할 수 있습니다. 중앙 조정이나 락(lock)이 필요 없습니다.
GDPR/CCPA 준수에 유리합니다. 순차 ID보다 데이터 익명화와 삭제가 용이하여 개인정보 보호 규정 준수에 도움이 됩니다.
단점: 성능과 저장 공간
데이터베이스 성능 저하 (특히 UUIDv4). MariaDB 벤치마크에서 UUID 인덱스 조회는 INT 인덱스보다 5~20배 느렸습니다. 5,000건의 레코드 조회 시 UUID는 0.396초, INT는 0.074초가 소요되었습니다.
무작위 삽입으로 인한 인덱스 단편화 (UUIDv4). B-tree 인덱스는 순차 삽입을 기대하지만, UUIDv4는 무작위로 분산됩니다. 이는 페이지 분할(page split)을 유발하고 인덱스를 비대화시킵니다. PostgreSQL 연구에 따르면, UUIDv4는 페이지 분할이 정수형보다 10배 많이 발생합니다.
더 큰 저장 공간. 16바이트는 BIGINT(8바이트)의 두 배입니다. 문자열로 저장하면 36바이트 이상이 됩니다. 1천만 건 기준으로 UUIDv4 테이블은 768MB인 반면 BIGINT는 절반 수준입니다.
UUIDv7의 등장: 게임 체인저
2024년 5월 발표된 RFC 9562는 UUIDv7을 표준화했습니다. UUIDv7은 UUIDv4의 보안성과 정수형의 성능을 결합했습니다.
DEV Community 벤치마크 (PostgreSQL, 1천만 건):
- UUIDv4 삽입 시간: 191.1초
- UUIDv7 삽입 시간: 124.5초 (34.8% 빠름)
- UUIDv4 인덱스 크기: 774MB
- UUIDv7 인덱스 크기: 600MB (22% 작음)
Scaling Postgres 벤치마크 (2천만 건 기준, 백만 건 추가 삽입):
- BIGINT: 290초
- UUIDv7: 290초 (동일!)
- UUIDv4: 375초 (29% 느림)
핵심 통찰: UUIDv7은 타임스탬프 기반 접두사로 순차적 삽입을 유지하면서도 74비트의 무작위성으로 보안을 확보합니다. PostgreSQL 18+에서 네이티브 지원되며, Buildkite 같은 회사들이 이미 모든 새 테이블에 UUIDv7을 채택했습니다.
실제 사례: Buildkite의 UUIDv7 마이그레이션
Buildkite는 2023년 정수형 ID에서 UUIDv7로 전환했습니다. 그들의 결정 이유:
"분산 데이터베이스 환경에서 정수형 ID를 기본 키로 사용하는 것이 곧 부담이 될 것임을 빠르게 인식했습니다.
UUIDv7의 시간 기반 특성은 무작위 접두사 UUIDv4에 비해 훨씬 나은 DB 성능을 제공합니다."
결과: Write-Ahead Log(WAL) 비율 50% 감소, Write I/O 50% 감소.
언제 사용해야 할까?
UUIDv7 (최우선 추천):
- 2024년 이후 새로운 프로젝트
- 마이크로서비스 아키텍처
- 분산 시스템 또는 샤딩 계획이 있는 경우
- 시간순 정렬이 필요한 이벤트/로그
UUIDv4:
- 보안 토큰이나 세션 ID (예측 불가능성이 중요)
- 레거시 시스템 (이미 사용 중인 경우)
- 소규모 애플리케이션
PostgreSQL UUIDv7 구현 예시:
CREATE EXTENSION IF NOT EXISTS pg_uuidv7;
CREATE TABLE events (
id UUID DEFAULT uuidv7() PRIMARY KEY,
user_id UUID REFERENCES users(id),
event_type TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
String (문자열 ID): 유연성과 가독성
문자열 ID는 사용자명, 슬러그(slug), 또는 커스텀 형식의 식별자를 의미합니다.
실제 사례: Stripe의 접두사 ID 시스템
Stripe는 독특한 접두사 문자열 ID를 사용합니다:
- pi_3LKQhvGUcADgqoEM3bh6pslE (PaymentIntent)
- cus_MNlbRsTWfvcJ01 (Customer)
- pm_1LaXpKGUcADgqoEMl0Cx0Ygg (PaymentMethod)
Stripe 엔지니어 Paul Asjes가 밝힌 이유:
- 사람이 읽을 수 있음: 접두사만 봐도 객체 타입을 즉시 알 수 있습니다.
- 디버깅 효율성: ID를 트리플 클릭하면 전체가 선택되어 복사/붙여넣기가 쉽습니다.
- 다형성 지원: 하나의 파라미터에 여러 객체 타입을 받을 수 있습니다.
- 보안 감지: sk_live_ 같은 실제 키가 Discord 메시지에 포함되면 자동으로 차단할 수 있습니다.
장점
의미 있고 가독성이 좋습니다. /user/johndoe는 /user/550e8400-e29b-41d4-a716-446655440000보다 훨씬 직관적입니다.
SEO에 유리합니다. URL에 의미 있는 키워드를 포함할 수 있어 검색 엔진 최적화에 도움이 됩니다.
유연한 길이와 형식. 필요에 따라 커스터마이징할 수 있습니다.
단점
가변 크기로 인한 성능 저하. VARCHAR는 고정 크기 타입보다 느립니다. 문자열 비교는 숫자 비교보다 CPU 비용이 큽니다.
열거 가능성 (사용자명 등). 일반적인 사용자명은 사전 공격(dictionary attack)에 취약할 수 있습니다.
개인정보 노출 위험. 이메일 주소나 실명을 ID로 사용하면 프라이버시 문제가 발생합니다.
대소문자/인코딩 문제. User와 user를 같은 것으로 처리할지, UTF-8 문자를 어떻게 다룰지 등의 복잡성이 있습니다.
언제 사용해야 할까?
적합한 경우:
- 공개 프로필 URL (/user/johndoe)
- 블로그 게시물 슬러그 (/posts/uuid-vs-integer-ids)
- 민감하지 않은 리소스
부적합한 경우:
- 민감한 데이터 (의료 기록, 금융 정보)
- 고성능이 중요한 경우
- 기본 키 (보조 식별자로만 사용 권장)
추천 구조:
CREATE TABLE articles (
id UUID PRIMARY KEY DEFAULT uuidv7(),
slug VARCHAR(255) UNIQUE, -- 외부 URL용
title TEXT,
content TEXT
);
-- URL: /articles/how-to-choose-user-ids (slug)
-- 내부: UUID로 조회
대안적 접근: 하이브리드 솔루션들
Instagram/Twitter: Snowflake ID (64비트 정수)
Instagram과 Twitter는 커스텀 Snowflake ID를 사용합니다:
구조 (64비트):
- 41비트: 타임스탬프 (밀리초)
- 13비트: 샤드 ID (Instagram) 또는 10비트 머신 ID (Twitter)
- 10비트: 시퀀스 번호
장점:
- 8바이트 (UUID의 절반)
- 시간순 정렬 가능
- 밀리초당 4,096개 ID 생성 가능
- BIGINT로 네이티브 저장
단점:
- 머신 ID 조정 필요
- 69년 후 타임스탬프 오버플로 (커스텀 epoch 사용 시)
- UUID보다 낮은 엔트로피
Instagram의 선택 이유: "Twitter의 Snowflake가 가장 근접했지만, ID 서비스를 운영하는 추가 복잡성이 단점이었습니다. 대신 PostgreSQL 내부로 가져왔습니다."
ULID (Universally Unique Lexicographically Sortable Identifier)
형식: 26자 대소문자 구분 없는 문자열
구조: 48비트 타임스탬프 + 80비트 무작위
예시: 01HQF2QXSW5EFKRC2YYCEXZK0N
장점:
- UUID보다 짧음 (26자 vs 36자)
- URL 안전 (특수 문자 없음)
- 문자열로도 시간순 정렬 가능
- 128비트 (UUID 호환)
언제 사용?
- 이벤트 스트리밍 시스템
- NoSQL 데이터베이스 (문자열 기반 정렬)
- URL 친화적 ID가 필요한 API
NanoID
형식: 21자 URL 친화적 무작위 문자열
예시: V1StGXR8_Z5jdHi6B-myT
장점:
- 매우 작음 (라이브러리 118바이트)
- 가장 빠른 생성 속도
- 커스터마이징 가능한 알파벳과 길이
- 충돌 확률 UUIDv4와 유사
단점:
- 시간 정렬 불가 (완전 무작위)
- 데이터베이스 네이티브 지원 없음
언제 사용?
- URL 단축기
- 클라이언트 측 ID 생성
- 짧은 토큰이 중요한 경우
성능 비교표
저장 공간 (건당)
타입 바이트 문자열 길이
| INT | 4 | 10자 |
| BIGINT | 8 | 19자 |
| UUID (binary) | 16 | 36자 |
| UUID (string) | 36+ | 36자 |
| ULID | 16 | 26자 |
| Snowflake | 8 | 19자 |
삽입 성능 (MySQL InnoDB, 2,800만 건)
타입 시간 비고
| Auto-increment INT | 8분 | 기준선 |
| UUIDv4 (random) | 25+ 시간 | 80배 느림 |
| UUIDv7/Snowflake | ~10분 | 순차적 삽입 |
인덱스 크기 (PostgreSQL, 1천만 건)
타입 인덱스 크기
| BIGINT | 214 MB |
| UUIDv7 | 428 MB |
| UUIDv4 | 856 MB |
결정 가이드: 어떤 것을 선택할까?
신규 프로젝트 (2024년 이후)
UUIDv7을 사용하세요. 이유:
- 현대적인 RFC 표준 (2024년)
- 정수형에 근접한 성능
- 보안과 분산성 확보
- 미래 지향적 선택
CREATE TABLE users (
id UUID DEFAULT uuidv7() PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL
);
초기 스타트업 (< 100만 건)
듀얼 식별자 패턴을 사용하세요:
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY, -- 내부: 빠른 조인
public_id UUID UNIQUE, -- 외부: API
email VARCHAR(255)
);
성능은 최대한 유지하면서 보안도 확보하는 실용적 접근입니다.
마이크로서비스 / 분산 시스템
UUIDv7 또는 Snowflake ID를 사용하세요. 각 서비스가 독립적으로 ID를 생성할 수 있어 조정 오버헤드가 없습니다.
공개 API (개발자 경험 중시)
Stripe 스타일 접두사 ID를 고려하세요:
usr_xYz123abc # 사용자
txn_aB34c56d # 트랜잭션
tok_mnOp78q # 토큰
디버깅과 지원이 훨씬 쉬워집니다.
레거시 시스템 (이미 정수형 사용 중)
마이그레이션하지 마세요. 대신:
- UUID 컬럼을 추가
- 외부 API에만 UUID 노출
- 내부 조인은 정수형 유지
완전한 마이그레이션은 복잡하고 위험합니다. 하이브리드 접근이 더 실용적입니다.
보안 체크리스트
ID 타입과 관계없이 반드시 구현해야 할 사항:
✅ 접근 제어 검증: 모든 요청마다 권한을 확인하세요. ID 난독화는 보안이 아닙니다.
# 취약한 코드
user = User.find(params['id'])
# 안전한 코드
user = current_user.users.find(params['id']) # 현재 사용자의 리소스만
✅ API 속도 제한: Parler 사건처럼 무제한 스크래핑을 방지하세요.
✅ 순차 ID를 외부에 노출하지 마세요: 공개 API, URL, 쿠키에 절대 사용하지 마세요.
✅ 로깅과 모니터링: 연속적인 ID 스캔 패턴을 감지하세요.
✅ 이진 형식으로 UUID 저장: CHAR(36) 대신 BINARY(16) 또는 네이티브 UUID 타입을 사용하세요.
결론: 실용적 권장사항
대부분의 현대적 웹 애플리케이션:
→ UUIDv7 (2024년 표준, 최선의 균형)
초기 단계 스타트업:
→ BIGINT + 공개 UUID (성능과 보안 모두)
고성능 분산 시스템:
→ Snowflake ID (Instagram/Twitter 검증됨)
개발자 친화적 API:
→ Stripe 스타일 접두사 ID (DX 최적화)
레거시 시스템:
→ 현상 유지 + UUID 추가 (점진적 개선)
ID 선택은 되돌리기 어려운 결정입니다. 초기에 올바르게 선택하면, 나중에 Parler처럼 70TB의 데이터 유출이나 복잡한 마이그레이션을 겪지 않을 수 있습니다. 보안과 성능, 그리고 미래의 확장성을 모두 고려하여 신중히 결정하세요. 2024년 현재, UUIDv7이 새로운 표준으로 자리 잡고 있으며, 대부분의 경우 가장 합리적인 선택입니다.
'자료구조' 카테고리의 다른 글
| 자료구조 4장 - 스택 (0) | 2025.09.03 |
|---|---|
| 자료구조 3장 - 배열 (0) | 2024.08.14 |
| 자료구조 2장 (0) | 2024.08.13 |
| 자료구조 1장 (0) | 2024.08.12 |
