엔티티 ID 설계 전략
TL;DR
- 이 글은 엔티티 ID 설계 전략의 핵심 개념과 실제 적용 포인트를 빠르게 정리한다.
- 왜 이 패턴/기법이 등장했는지 배경과 도입 이유를 함께 설명한다.
- 실무에서 바로 쓰기 위한 특징과 상세 내용을 원문 기반으로 정리한다.
1. 개념
엔티티 ID 설계 전략의 정의와 핵심 원리를 먼저 이해하면 뒤의 구현 전략을 훨씬 정확하게 판단할 수 있다.
2. 배경
기존 방식의 한계와 운영상의 문제를 해결하기 위해 이 접근이 발전했다.
3. 이유
확장성, 안정성, 유지보수성, 보안을 함께 높이기 위해 이 설계가 필요하다.
4. 특징
핵심 특징은 표준화된 구조, 명확한 책임 분리, 그리고 운영 관점에서의 예측 가능성이다.
5. 상세 내용
엔티티 ID 설계 전략
작성일: 2026-03-16 카테고리: Backend / Architecture / Database / DDD / API Design 포함 내용: Entity Identity, Primary Key, Surrogate Key, Natural Key, UUID v4, UUID v7, Snowflake ID, ULID, KSUID, NanoID, Stripe 접두사 ID, IDOR, 멱등성 키, 상관 ID, B-tree 성능, GraphQL Global ID, 내부/외부 ID 분리, 이벤트 소싱, 분산 시스템 ID 생성
1. 엔티티는 왜 정체성(Identity)이 필요한가
1-1. 객체 지향에서의 정체성
┌─────────────────────────────────────────────────────────────────┐
│ 객체의 세 가지 본질적 속성 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1) 상태 (State) ← 객체가 보유한 데이터 │
│ 2) 행동 (Behavior) ← 객체가 수행하는 연산 │
│ 3) 정체성 (Identity) ← 객체를 고유하게 식별하는 이름 │
│ │
│ Java에서의 두 가지 동일성 판단: │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 참조 동일성 (Reference Identity) │ │
│ │ a == b → 같은 메모리 주소를 가리키는가? │ │
│ │ │ │
│ │ 값 동등성 (Value Equality) │ │
│ │ a.equals(b) → 같은 값을 가지는가? │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 핵심 문제: │
│ 메모리 주소는 프로그램 실행 중에만 유효하다. │
│ 프로그램 종료, DB 저장, 네트워크 전송 시 사라진다. │
│ → 영속적(persistent) 식별자, 즉 ID가 필요해진다. │
│ │
└─────────────────────────────────────────────────────────────────┘
1-2. DDD에서의 엔티티 vs 값 객체
┌─────────────────────────────────────────────────────────────────┐
│ Evans Classification (Eric Evans, 2003) │
├────────────────────────────┬────────────────────────────────────┤
│ 엔티티 (Entity) │ 값 객체 (Value Object) │
├────────────────────────────┼────────────────────────────────────┤
│ │ │
│ "연속적인 정체성"으로 │ "무엇인가(what)"로 규정됨 │
│ 규정됨 │ │
│ │ Money(100, "USD") == │
│ 고객 이름이 바뀌어도 │ Money(100, "USD") │
│ 동일한 고객이다 │ → 완전히 교환 가능 │
│ │ │
│ 속성이 아닌 정체성이 │ equals() + hashCode() │
│ 객체를 규정한다 │ 반드시 오버라이드 │
│ │ │
│ ID가 반드시 필요 │ ID가 불필요 │
│ equals() 오버라이드 안함 │ 불변(immutable)이어야 함 │
│ │ │
└────────────────────────────┴────────────────────────────────────┘
“Many objects are not fundamentally defined by their attributes, but rather by a thread of continuity and identity.” — Eric Evans, Domain-Driven Design
1-3. ID 필요 여부 판단 기준
┌─────────────────────────────────────────────────────────────────┐
│ ID가 필요한가? 의사결정 트리 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 질문 Yes → No → │
│ ───────────────────────────────────────────────────── │
│ 이 객체를 독립적으로 참조할 것인가? ID 필요 불필요 │
│ 이 객체의 생애주기를 추적할 것인가? ID 필요 불필요 │
│ 다른 객체와의 관계를 가지는가? ID 필요 불필요 │
│ 개별적으로 업데이트할 것인가? ID 필요 불필요 │
│ 두 인스턴스가 같은 값이면 동일한가? ID 불필요 ID 필요 │
│ │
│ ID가 불필요한 대표 사례: │
│ ├── 값 객체: Money, DateRange, Address │
│ ├── NoSQL 내장 문서: 일대소 관계, 독립 참조 불필요 │
│ └── 불변 이벤트 페이로드의 속성값 │
│ │
└─────────────────────────────────────────────────────────────────┘
2. 데이터베이스 이론: 기본 키와 참조 무결성
2-1. Codd의 관계형 모델
┌─────────────────────────────────────────────────────────────────┐
│ Edgar F. Codd의 엔티티 무결성 원칙 (1970) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 규칙 1: 모든 관계(테이블)는 기본 키(Primary Key)를 가져야 한다 │
│ 규칙 2: 기본 키의 어떤 속성도 NULL이 될 수 없다 │
│ 규칙 3: 기본 키는 유일(unique)하고 불변이어야 한다 │
│ │
│ 보장된 접근 규칙 (Guaranteed Access Rule, Rule 2): │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ "모든 데이터 요소는 테이블 이름 + 기본 키 + 속성 이름의 │ │
│ │ 조합으로 논리적으로 접근 가능해야 한다." │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ → ID는 단순한 관행이 아니라 관계형 모델의 수학적 토대이다 │
│ │
│ NULL 기본 키가 불가능한 이유: │
│ NULL PK → 행을 고유하게 식별 불가 → 참조/조작 불가능 │
│ │
└─────────────────────────────────────────────────────────────────┘
2-2. 참조 무결성(Referential Integrity)
┌─────────────────────────────────────────────────────────────────┐
│ 외래 키와 참조 무결성 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ID가 없으면 테이블 간 관계가 성립하지 않는다. │
│ │
│ customers (id PK) │
│ │ │
│ └──< orders (id PK, customer_id FK) │
│ │ │
│ └──< order_items (id PK, order_id FK) │
│ │
│ 참조 무결성이 보장하는 것: │
│ ├── 고객이 없는 주문 생성 방지 │
│ ├── 연관된 주문이 있는 고객 삭제 방지 │
│ └── 여러 테이블에 걸친 업데이트의 일관된 적용 │
│ │
└─────────────────────────────────────────────────────────────────┘
2-3. 대리 키(Surrogate Key) vs 자연 키(Natural Key)
┌─────────────────────────────────────────────────────────────────┐
│ 현대에 대리 키가 지배적인 이유 │
├──────────────────┬──────────────────┬───────────────────────────┤
│ 문제 │ 자연 키의 취약점 │ 대리 키의 해결 │
├──────────────────┼──────────────────┼───────────────────────────┤
│ 비즈니스 규칙 │ 키 값 변경 시 │ 비즈니스 의미 없으므로 │
│ 변경 │ 모든 FK 연쇄 영향 │ 영향 없음 │
├──────────────────┼──────────────────┼───────────────────────────┤
│ 복합 자연 키 │ 여러 컬럼 조인 │ 단일 정수/UUID로 조인 │
│ │ → 성능 저하 │ → 성능 우수 │
├──────────────────┼──────────────────┼───────────────────────────┤
│ NULL 가능성 │ 후보 속성이 │ 시스템이 항상 생성 │
│ │ NULL일 수 있음 │ NULL 불가 │
├──────────────────┼──────────────────┼───────────────────────────┤
│ ORM 지원 │ 복잡한 매핑 필요 │ Rails, Hibernate 등 │
│ │ │ 기본 지원 │
├──────────────────┼──────────────────┼───────────────────────────┤
│ 합병/마이그레이션│ 중복 ID 충돌 위험 │ 시스템 내 충돌 없음 │
└──────────────────┴──────────────────┴───────────────────────────┘
“키는 관계형 스키마 내에서 결합의 주요 원천이며, 결과적으로 리팩터링이 어렵다.” — Scott Ambler, agiledata.org
3. 분산 시스템에서의 엔티티 정체성
3-1. 마이크로서비스와 ID
┌─────────────────────────────────────────────────────────────────┐
│ 분산 시스템에서 ID가 해결하는 문제들 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1) 조정 없는 생성 (Coordination-Free Generation) │
│ UUID는 각 서비스가 중앙 조율 없이 독립적으로 생성 가능 │
│ │
│ 2) 서비스 경계를 넘는 참조 │
│ 전역적으로 안정적인 ID = 서비스 독립성의 전제조건 │
│ │
│ 3) 추적성 (Traceability) │
│ 상관 ID (Correlation ID)로 요청을 종단간 추적 │
│ → OpenTelemetry의 trace_id, span_id │
│ │
│ 4) 멱등성 (Idempotency) │
│ ID가 중복 요청 감지/건너뛰기의 멱등성 키로 기능 │
│ → Stripe의 Idempotency-Key 헤더 │
│ │
└─────────────────────────────────────────────────────────────────┘
3-2. DDD 집합체(Aggregate) 규칙: ID로만 참조하라
┌─────────────────────────────────────────────────────────────────┐
│ Vaughn Vernon, "Implementing Domain-Driven Design" │
├─────────────────────────────────────────────────────────────────┤
│ │
│ "Prefer references to external Aggregates only by their │
│ globally unique identity, not by holding a direct object │
│ reference." │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ BAD: private Product product; // 직접 참조 │ │
│ │ GOOD: private ProductId productId; // ID로 참조 │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ 효과: │
│ ├── 자동으로 더 작은 집합체: 즉시 로드 방지 → 메모리 절약 │
│ ├── 무한에 가까운 확장성: 영속 상태 이동 → 분산 저장소 가능 │
│ └── 트랜잭션 결합 방지: 두 집합체의 같은 트랜잭션 수정 차단 │
│ │
└─────────────────────────────────────────────────────────────────┘
3-3. 이벤트 소싱과 CQRS에서의 ID
┌─────────────────────────────────────────────────────────────────┐
│ 이벤트 소싱에서 집합체 ID의 역할 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 이벤트 스트림: │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ aggregate_id: "order-abc-123" │ │
│ │ ├── OrderCreated {seq: 1, ...} │ │
│ │ ├── ItemAdded {seq: 2, ...} │ │
│ │ ├── PaymentReceived{seq: 3, ...} │ │
│ │ └── OrderShipped {seq: 4, ...} │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ 집합체 ID가 없으면: │
│ → 어느 이벤트가 어느 집합체에 속하는지 알 수 없다 │
│ → 이벤트 재생(replay) 자체가 불가능 │
│ → CQRS의 명령(Command) 라우팅이 불가능 │
│ │
└─────────────────────────────────────────────────────────────────┘
4. ID 포맷 비교
4-1. 주요 ID 포맷 개요
┌─────────────────────────────────────────────────────────────────┐
│ ID 포맷 스펙트럼 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 크기순: │
│ │
│ INT (32비트, 4B) │
│ └── 최대 ~21억. 단일 서버, 소규모 시스템 │
│ │
│ BIGINT (64비트, 8B) │
│ └── 최대 ~922경. Snowflake ID의 기반 │
│ │
│ UUID (128비트, 16B 바이너리 / 36자 텍스트) │
│ └── v4 (랜덤), v7 (시간순 정렬) │
│ │
│ ULID (128비트, 16B / 26자 Crockford Base32) │
│ └── 시간순 정렬, URL 안전. 비공식 스펙 │
│ │
│ KSUID (160비트, 20B / 27자) │
│ └── Segment 개발. Stripe 스타일 ID의 백엔드로 활용 │
│ │
│ NanoID (기본 21자, URL 안전) │
│ └── crypto.getRandomValues() 사용. 정렬 불가 │
│ │
└─────────────────────────────────────────────────────────────────┘
4-2. Auto-increment Integer
┌─────────────────────────────────────────────────────────────────┐
│ Auto-increment Integer │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 장점: │
│ ├── 단순성, 속도, 저장 공간 최소 (4-8 bytes) │
│ ├── 가독성 우수 (디버깅 시 직관적) │
│ └── B-tree에서 항상 끝에 삽입 → 페이지 분할 없음 │
│ │
│ 단점: │
│ ├── 보안: 열거 공격 가능 (IDOR), 비즈니스 지표 노출 │
│ ├── 분산 시스템에서 충돌, 단일 장애점(SPOF) │
│ └── 중앙 서버 없이 ID 생성 불가 │
│ │
│ 현재 사용처: │
│ ├── GitHub REST API의 databaseId │
│ ├── 소규모 내부 시스템 │
│ └── 레거시 시스템 │
│ │
└─────────────────────────────────────────────────────────────────┘
4-3. UUID v4 (랜덤)
┌─────────────────────────────────────────────────────────────────┐
│ UUID v4 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 128비트 완전 랜덤. 중앙 조정 없이 생성 가능. 추측 불가. │
│ │
│ 장점: │
│ ├── 충돌 확률 극히 낮음 (2^122 공간) │
│ ├── 분산 생성 가능, 보안성 우수 │
│ └── Google Spanner가 공식 권장 (랜덤 분포 = 핫스팟 방지) │
│ │
│ 치명적 단점 - B-tree 인덱스 파괴: │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 완전 랜덤 → 매번 인덱스의 다른 위치에 삽입 │ │
│ │ → 빈번한 페이지 분할 (page split) │ │
│ │ → 버퍼 캐시 오염 (cache pollution) │ │
│ │ → 저장 공간 낭비 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 예외: Spanner, DynamoDB처럼 키 범위 분산 구조에서는 │
│ 오히려 랜덤 UUID가 핫스팟 방지에 유리하다. │
│ │
└─────────────────────────────────────────────────────────────────┘
4-4. UUID v7 (시간순 정렬)
┌─────────────────────────────────────────────────────────────────┐
│ UUID v7 (RFC 9562, 2024년 5월) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 구조: │
│ ┌────────────────────────────────────────────────────┐ │
│ │ [48비트 Unix 타임스탬프(ms)] [4비트 ver=7] [나머지 랜덤]│ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ 핵심 특성: │
│ ├── 새 ID가 항상 이전 ID보다 크다 (단조 증가, monotonic) │
│ ├── B-tree 삽입이 항상 끝에서 발생 → v4 대비 30% 빠름 │
│ ├── 생성 시각을 ID에서 추출 가능 (디버깅 편의) │
│ └── 분산 생성 가능 (중앙 조정 불필요) │
│ │
│ 실측 성능 (Buildkite): │
│ ├── WAL 생성률 50% 감소 │
│ └── 쓰기 I/O 50% 감소 │
│ │
│ PostgreSQL 18 (2025): │
│ └── 네이티브 uuidv7() 함수 내장 │
│ │
│ 2024-2025 업계 컨센서스: 새 프로젝트의 기본 PK로 부상 │
│ │
└─────────────────────────────────────────────────────────────────┘
4-5. Snowflake ID (Twitter)
┌─────────────────────────────────────────────────────────────────┐
│ Snowflake ID (2010, Twitter) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 64비트 구조: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ [1비트: 부호] [41비트: 타임스탬프] [10비트: 머신ID] │ │
│ │ [12비트: 시퀀스] │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ 특성: │
│ ├── 초당 최대 4,096,000개 생성 가능 │
│ ├── 시간순 정렬 가능 │
│ ├── BIGINT에 저장 가능 (8 bytes) │
│ └── 단점: 중앙 워커 ID 관리 필요 │
│ │
│ 플랫폼별 변형: │
│ ┌──────────┬────────────┬──────────┬─────────┬──────────┐ │
│ │ 플랫폼 │ 타임스탬프 │ 머신 ID │ 시퀀스 │ 에포크 │ │
│ ├──────────┼────────────┼──────────┼─────────┼──────────┤ │
│ │ Twitter │ 41비트 │ 10비트 │ 12비트 │ 2010-11 │ │
│ │ Discord │ 41비트 │ 10비트 │ 12비트 │ 2015-01 │ │
│ │ Instagram│ 41비트 │ 13비트 │ 10비트 │ 자체 │ │
│ │ │ │ (샤드ID) │ │ │ │
│ └──────────┴────────────┴──────────┴─────────┴──────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
4-6. 성능 벤치마크 종합
┌─────────────────────────────────────────────────────────────────┐
│ PostgreSQL 벤치마크 (Ardent Performance, 2024, 100만 행) │
├──────────────────┬──────────────┬──────────────┬───────────────┤
│ 방식 │ 삽입 시간 │ 처리량(tps) │ 최종 크기 │
├──────────────────┼──────────────┼──────────────┼───────────────┤
│ UUID v4 (텍스트) │ 410초 │ 2,421 │ 4.31 GB │
│ UUID v4 (바이너리)│ 375초 │ 2,670 │ 2.65 GB │
│ UUID v7 │ 290초 │ 3,420 │ 2.47 GB │
│ BIGINT Identity │ 290초 │ 3,480 │ 1.97 GB │
└──────────────────┴──────────────┴──────────────┴───────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ B-tree 행동 원리와 ID 포맷의 관계 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ UUID v4 (랜덤): │
│ 매 INSERT → 인덱스의 랜덤 위치 → 페이지 분할 빈발 │
│ → 캐시 오염 → 높은 I/O → 성능 저하 │
│ │
│ UUID v7 / BIGINT (순차): │
│ 매 INSERT → 인덱스의 끝에 추가 → 페이지 분할 최소화 │
│ → 캐시 효율적 → 낮은 I/O → 성능 우수 │
│ │
│ 저장 공간 비교: │
│ ┌──────────────┬──────────────────────────────┐ │
│ │ ID 형태 │ 크기 │ │
│ ├──────────────┼──────────────────────────────┤ │
│ │ INT (32비트) │ 4 bytes │ │
│ │ BIGINT (64) │ 8 bytes │ │
│ │ UUID (128) │ 16 bytes (바이너리) / 36 (텍스트) │ │
│ │ ULID │ 16 bytes / 26자 │ │
│ │ KSUID │ 20 bytes / 27자 │ │
│ └──────────────┴──────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
5. 보안: ID가 만드는 취약점
5-1. IDOR (Insecure Direct Object Reference)
┌─────────────────────────────────────────────────────────────────┐
│ IDOR: 순차 ID가 만드는 보안 구멍 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 순차 정수 ID를 URL에 노출하면: │
│ GET /api/invoices/1246 │
│ GET /api/invoices/1247 ← 공격자가 순차적으로 스캔 │
│ GET /api/invoices/1248 │
│ │
│ 실제 침해 사례: │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ AT&T (2010) │ │
│ │ ICC-ID 일련번호로 10만 명 iPad 소유주 이메일 노출 │ │
│ │ │ │
│ │ Parler (2021년 1월) │ │
│ │ 순차 번호 + 인증 부재 + 속도 제한 없음 │ │
│ │ → 70TB 데이터 유출 (GPS 메타데이터 포함) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ OWASP 가이드라인: │
│ "복잡한 ID는 심층 방어(defense in depth)이지, │
│ 접근 제어(access control)의 대체가 아니다." │
│ │
│ → 불투명 ID + 서버 사이드 인가(authorization) 검증 필수 │
│ │
└─────────────────────────────────────────────────────────────────┘
5-2. ID를 통한 비즈니스 정보 누출
┌─────────────────────────────────────────────────────────────────┐
│ 순차 ID가 노출하는 비즈니스 지표 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 공격 시나리오: │
│ ├── 경쟁사가 1시간 간격으로 주문 2건 생성 │
│ │ 주문 #10245 (14:00) → 주문 #10312 (15:00) │
│ │ → 차이 67 → 시간당 약 67건 처리 역산 │
│ │ │
│ ├── 총 사용자 수 추정 │
│ │ 회원가입 → user_id: 1,247,891 │
│ │ → "약 124만 명 사용자" 추정 │
│ │ │
│ └── 성장률 추적 │
│ 매월 마지막 주문 ID 기록 → 월간 성장률 도출 │
│ │
│ 해결: 외부 노출용 ID를 불투명(opaque)하게 만들어야 한다 │
│ │
└─────────────────────────────────────────────────────────────────┘
6. API 설계에서의 ID 전략
6-1. REST와 리소스 식별
┌─────────────────────────────────────────────────────────────────┐
│ Roy Fielding의 REST (2000년 박사 논문) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ "A resource is a conceptual mapping to a set of entities, │
│ not the entity that corresponds to the mapping at any │
│ particular point in time." │
│ │
│ 모든 리소스는 URI로 식별되어야 하며, ID가 URI의 핵심이다: │
│ │
│ /customers ← 컬렉션 리소스 │
│ /customers/{customerId} ← 개별 리소스 (ID 필수)│
│ /customers/{customerId}/orders/{orderId} ← 중첩 리소스 │
│ │
│ HATEOAS: │
│ 서버가 응답에 관련 링크를 동적으로 포함 │
│ → 링크는 ID를 포함한 URI로 구성 │
│ → ID가 없으면 하이퍼텍스트 연결 구조가 성립하지 않는다 │
│ │
└─────────────────────────────────────────────────────────────────┘
6-2. Stripe 접두사 ID 패턴
┌─────────────────────────────────────────────────────────────────┐
│ Stripe의 접두사 + 랜덤 ID 패턴 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 접두사별 의미: │
│ ┌──────────────┬───────────────────────────┐ │
│ │ 접두사 │ 의미 │ │
│ ├──────────────┼───────────────────────────┤ │
│ │ ch_ │ Charge (결제) │ │
│ │ cus_ │ Customer (고객) │ │
│ │ pi_ │ PaymentIntent │ │
│ │ sk_live_ │ Secret Key (프로덕션) │ │
│ │ sk_test_ │ Secret Key (테스트) │ │
│ │ pk_ │ Public Key │ │
│ └──────────────┴───────────────────────────┘ │
│ │
│ 이점: │
│ ├── 즉각적 디버깅: 로그에서 접두사로 객체 타입 즉시 파악 │
│ ├── 오류 조기 발견: Customer ID를 Charge 파라미터에 넣으면 │
│ │ 접두사 불일치로 즉시 감지 │
│ ├── 우발적 노출 방지: Discord AutoMod로 sk_live_ 패턴 │
│ │ 자동 차단 가능 │
│ └── 단일 ID 공간: 모든 리소스의 ID가 전역적으로 유일 │
│ │
│ 채택 기업: Stripe, Clerk, Linear 등 │
│ │
└─────────────────────────────────────────────────────────────────┘
6-3. URL에서 Slug vs ID
┌─────────────────────────────────────────────────────────────────┐
│ Slug vs ID: 플랫폼별 전략 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Stack Overflow (하이브리드): │
│ /questions/11227809/why-is-processing-... │
│ → 슬러그 변경돼도 숫자 ID로 리디렉션 │
│ │
│ GitHub: │
│ /{owner}/{repo} (슬러그) │
│ 이슈는 저장소 내 순차 번호 │
│ │
│ Medium: │
│ 슬러그 + 짧은 해시 │
│ │
│ ┌──────────────────┬───────────────┬──────────────────────┐ │
│ │ 상황 │ 추천 방식 │ 이유 │ │
│ ├──────────────────┼───────────────┼──────────────────────┤ │
│ │ 블로그, 기사 │ 슬러그 │ SEO, 가독성 │ │
│ │ 사용자 프로필 │ 슬러그(username)│ 브랜딩 │ │
│ │ 주문, 결제 │ UUID │ 정보 노출 방지 │ │
│ │ 대규모 동적 콘텐츠│ ID+슬러그 혼합 │ 안정성 + 가독성 │ │
│ └──────────────────┴───────────────┴──────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
6-4. 외부 ID vs 내부 ID 분리
┌─────────────────────────────────────────────────────────────────┐
│ 내부 ID와 외부 ID의 분리 전략 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 원칙: │
│ 내부 ID → DB 성능 최적화 (auto-increment, 샤드 정보 인코딩) │
│ 외부 ID → API 소비자에 대한 안정적 계약 (불투명, 보안) │
│ │
│ 빅테크 사례: │
│ │
│ Stripe: │
│ ├── 외부: ch_xxxxx (접두사 + 랜덤) │
│ └── 내부: 샤드 정보 인코딩된 별도 식별자 │
│ │
│ Shopify: │
│ ├── REST 정수 ID: 12345678 │
│ ├── GraphQL GID: gid://shopify/Product/12345678 │
│ └── 고객용: order_number (사람이 읽을 수 있는 번호) │
│ → 3개 ID가 공존 │
│ │
│ GitHub: │
│ ├── REST: 정수 databaseId │
│ └── GraphQL: 불투명 node_id (Base64 → MessagePack 전환 중) │
│ │
│ Mark Seemann의 관점: │
│ REST 원칙상 클라이언트는 링크(href)만으로 리소스를 참조하면 │
│ 되므로, ID 노출 자체가 불필요할 수 있다. │
│ │
└─────────────────────────────────────────────────────────────────┘
6-5. GraphQL과 Relay 전역 객체 식별
┌─────────────────────────────────────────────────────────────────┐
│ Relay의 Node 인터페이스 패턴 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ interface Node { │
│ id: ID! │
│ } │
│ │
│ query { │
│ node(id: "VXNlcjoy") { ← 시스템 어디서든 어떤 객체든 │
│ ... on User { 페치 가능 │
│ name │
│ } │
│ } │
│ } │
│ │
│ Base64 인코딩: "User:42" → "VXNlcjoy" │
│ → 전역 고유성 + 불투명성 확보 │
│ │
│ Shopify GID 변형: │
│ gid://shopify/Product/123456789 │
│ → URI 형태로 타입과 ID를 명시적으로 인코딩 │
│ │
└─────────────────────────────────────────────────────────────────┘
7. 이벤트 기반 아키텍처에서의 ID 활용
7-1. 상관 ID (Correlation ID)
┌─────────────────────────────────────────────────────────────────┐
│ 마이크로서비스 종단간 추적 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 클라이언트 요청 │
│ │ X-Correlation-ID: "abc-123-def" │
│ ▼ │
│ API Gateway ──→ Order Service ──→ Payment Service │
│ │ │ │ │
│ │ trace_id: "abc-123-def" │ │
│ │ span_id: "span-001" │ │
│ │ trace_id: "abc-123-def" │
│ │ span_id: "span-002" │
│ ▼ │
│ 단일 요청의 전체 흐름을 하나의 ID로 추적 │
│ → OpenTelemetry의 trace_id + span_id │
│ │
└─────────────────────────────────────────────────────────────────┘
7-2. 멱등성 키 (Idempotency Key)
┌─────────────────────────────────────────────────────────────────┐
│ Stripe의 멱등성 키 패턴 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ POST /v1/charges │
│ Idempotency-Key: "key_abc123" │
│ { amount: 2000, currency: "usd" } │
│ │
│ 1차 요청: 정상 처리 → 결과 캐시 │
│ 2차 요청 (같은 키): 캐시된 응답 반환 → 이중 청구 방지 │
│ │
│ 네트워크 오류로 응답을 못 받았을 때: │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 클라이언트 → 같은 Idempotency-Key로 재시도 │ │
│ │ 서버: "이 키로 이미 처리됨" → 동일 응답 반환 │ │
│ │ → 결제가 두 번 되지 않음 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 핵심: ID가 "이 요청은 이전에 처리한 것과 같은 요청인가?"를 │
│ 판단하는 멱등성 키로 기능한다. │
│ │
└─────────────────────────────────────────────────────────────────┘
8. 복합 키(Composite Key) 전략
┌─────────────────────────────────────────────────────────────────┐
│ 복합 키 vs 대리 키 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Rails의 "모든 테이블에 auto-increment id" 관례가 │
│ 업계 전반에 영향을 미쳤다. (Rails 7.1+에서 복합 PK 공식 지원) │
│ │
│ Shopify 벤치마크: │
│ 단일 대리키 → 복합키 전환 시 INSERT 성능 약 10배 저하 │
│ │
│ 복합 키가 적합한 경우: │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 멀티테넌트: (tenant_id, resource_id) │ │
│ │ 이벤트 소싱: (aggregate_id, sequence_number) │ │
│ │ 연결 테이블: (user_id, role_id) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 대리 키가 적합한 경우: │
│ ├── ORM 중심 애플리케이션 (Hibernate, ActiveRecord) │
│ ├── 외래 키 참조가 빈번한 테이블 │
│ └── API 리소스 식별이 필요한 엔티티 │
│ │
└─────────────────────────────────────────────────────────────────┘
9. Big Tech 실제 관행 종합
┌─────────────────────────────────────────────────────────────────┐
│ 빅테크 ID 전략 비교 │
├─────────────┬───────────────────────────────────────────────────┤
│ 기업 │ ID 전략 │
├─────────────┼───────────────────────────────────────────────────┤
│ Google │ Spanner: UUID v4 공식 권장 │
│ (Spanner) │ 분산 아키텍처에서 랜덤 분포가 핫스팟 방지 │
├─────────────┼───────────────────────────────────────────────────┤
│ Meta │ TAO 분산 그래프 저장소에서 64비트 정수 ID │
│ (Facebook) │ object_id 안에 shard_id 인코딩 │
├─────────────┼───────────────────────────────────────────────────┤
│ Amazon │ DynamoDB: 높은 카디널리티 값을 파티션 키로 │
│ (DynamoDB) │ 단조 증가 키 금지 (핫 파티션 방지) │
│ │ UUID v4나 랜덤 ID 유리 │
├─────────────┼───────────────────────────────────────────────────┤
│ Stripe │ 접두사 + 랜덤 ID (ch_xxx, cus_xxx) │
│ │ 디버깅, 오류 감지, 보안 모두 고려 │
├─────────────┼───────────────────────────────────────────────────┤
│ GitHub │ REST: 정수 databaseId │
│ │ GraphQL: 불투명 node_id (MessagePack 전환 중) │
├─────────────┼───────────────────────────────────────────────────┤
│ Shopify │ REST 정수 ID → GraphQL GID 전환 │
│ │ gid://shopify/Product/123456789 │
│ │ legacyResourceId로 하위 호환 │
├─────────────┼───────────────────────────────────────────────────┤
│ Twitter │ Snowflake ID (64비트, 자체 에포크) │
│ Discord │ Snowflake ID 변형 (에포크 2015-01-01) │
│ Instagram │ Snowflake ID 변형 (샤드ID 13비트) │
├─────────────┼───────────────────────────────────────────────────┤
│ PlanetScale │ NanoID (21자, URL 안전) │
├─────────────┼───────────────────────────────────────────────────┤
│ Clerk │ KSUID + Stripe 스타일 접두사 │
└─────────────┴───────────────────────────────────────────────────┘
10. 상황별 권장 ID 전략
┌─────────────────────────────────────────────────────────────────┐
│ 2024-2025 업계 컨센서스 기반 권장 │
├────────────────────────────┬────────────────────────────────────┤
│ 상황 │ 권장 ID 형태 │
├────────────────────────────┼────────────────────────────────────┤
│ 새 프로젝트 기본 PK │ UUID v7 │
│ 단일 서버 소규모 앱 │ BIGINT auto-increment │
│ Google Spanner │ UUID v4 (핫스팟 방지) │
│ DynamoDB PK │ UUID v4 / 랜덤 ID │
│ 공개 API ID │ Stripe 스타일 접두사 + 랜덤 │
│ URL 내 짧은 ID │ NanoID │
│ 고성능 64비트 정수 │ Snowflake ID │
│ 이벤트 스트리밍/로그 │ ULID 또는 UUID v7 │
│ 멀티테넌트 테이블 │ 복합키 (tenant_id, resource_id) │
│ 연결 테이블 (M:N) │ 복합키 (entity_a_id, entity_b_id)│
└────────────────────────────┴────────────────────────────────────┘
11. 학술 및 업계 전문가 합의 요약
┌─────────────────────────────────────────────────────────────────┐
│ 핵심 원칙과 출처 │
├──────────────────────────┬──────────────────┬───────────────────┤
│ 원칙 │ 출처 │ 핵심 주장 │
├──────────────────────────┼──────────────────┼───────────────────┤
│ 엔티티 무결성 규칙 │ E.F. Codd (1970) │ 모든 행은 NULL이 │
│ │ │ 아닌 PK 필요 │
├──────────────────────────┼──────────────────┼───────────────────┤
│ 엔티티 vs 값 객체 구분 │ Eric Evans │ 연속적 정체성이 │
│ │ DDD (2003) │ 필요한 객체만 │
│ │ │ 엔티티 = ID 필요 │
├──────────────────────────┼──────────────────┼───────────────────┤
│ Identity Field 패턴 │ Martin Fowler │ ORM에서 DB-객체 │
│ │ PEAA (2002) │ 정체성 연결에 │
│ │ │ ID 필드 필수 │
├──────────────────────────┼──────────────────┼───────────────────┤
│ 집합체 간 ID 참조 │ Vaughn Vernon │ 외부 집합체는 │
│ │ IDDD (2013) │ ID로만 참조하라 │
├──────────────────────────┼──────────────────┼───────────────────┤
│ REST 리소스 식별 │ Roy Fielding │ 모든 리소스는 │
│ │ (2000) │ URI로 식별, │
│ │ │ ID가 URI의 핵심 │
├──────────────────────────┼──────────────────┼───────────────────┤
│ 분산 시스템 UUID │ IETF RFC 9562 │ UUID v7이 분산 │
│ │ (2024) │ 생성과 정렬 가능 │
│ │ │ 성을 모두 제공 │
└──────────────────────────┴──────────────────┴───────────────────┘
12. 전체 요약
┌─────────────────────────────────────────────────────────────────┐
│ 엔티티 ID 설계 전략 요약 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ WHY: 왜 ID가 필요한가 │
│ ├── OOP: 메모리 주소는 휘발성 → 영속 식별자 필요 │
│ ├── DDD: 엔티티는 "연속적 정체성"으로 규정됨 │
│ ├── DB: Codd의 엔티티 무결성 = 관계형 모델의 수학적 토대 │
│ ├── 분산: 서비스 경계를 넘는 참조, 멱등성, 추적성 │
│ └── REST: URI 기반 리소스 식별의 핵심 │
│ │
│ WHAT: 어떤 ID를 선택할 것인가 │
│ ├── UUID v7: 2024-2025 업계 기본값. 순차+분산+표준화 │
│ ├── UUID v4: Spanner/DynamoDB 등 분산 DB에서 핫스팟 방지 │
│ ├── Snowflake: 고성능 64비트. Twitter/Discord/Instagram │
│ ├── BIGINT: 소규모 단일 서버에서 여전히 최적 │
│ └── Stripe 접두사: 공개 API에서 디버깅+보안+타입 안전 │
│ │
│ HOW: 어떻게 설계할 것인가 │
│ ├── 내부/외부 ID 분리: DB 성능 vs API 안정성 모두 확보 │
│ ├── 보안: 불투명 ID + 서버 인가 검증 (IDOR 방지) │
│ ├── 멱등성 키: 이벤트 기반 아키텍처에서 중복 처리 방지 │
│ └── 상관 ID: 마이크로서비스 종단간 추적 │
│ │
└─────────────────────────────────────────────────────────────────┘
References
- E.F. Codd, “A Relational Model of Data for Large Shared Data Banks” (1970)
- Codd’s Twelve Rules - Red Gate Simple Talk
- Entity Integrity - Wikipedia
- Eric Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software (2003)
- Martin Fowler, Patterns of Enterprise Application Architecture (2002)
- Martin Fowler, “Evans Classification”, “Value Object”
- Vaughn Vernon, Implementing Domain-Driven Design (2013)
- Roy Fielding, REST Dissertation - Chapter 5 (2000)
- IETF RFC 9562 - UUID v7 (2024)
- Scott Ambler, Surrogate Key vs Natural Key - AgileData.org
- OWASP IDOR Prevention Cheat Sheet
- PortSwigger Web Security Academy: IDOR
- Salt Security: Parler Data Breach Analysis
- Ardent Performance Computing, UUID Benchmark War (2024)
- Buildkite, “Goodbye to sequential integers, hello UUIDv7!”
- The Nile, “UUIDv7 Comes to PostgreSQL 18”
- Twitter Engineering, “Announcing Snowflake”
- Instagram Engineering, “Sharding & IDs at Instagram”
- Stripe Blog, “Designing Robust APIs with Idempotency”
- Stripe Blog, “Payment API Design - The First 10 Years”
- DEV Community, “Designing APIs for humans: Object IDs”
- Clerk, “Generating Stripe-like IDs with KSUIDs”
- Google Cloud Spanner Primary Key Docs
- AWS DynamoDB Partition Key Best Practices
- Meta, “TAO: Facebook’s Distributed Data Store”
- Shopify Dev, “Global IDs in Shopify APIs”
- Shopify Engineering, “Composite Primary Keys in Rails”
- GitHub Blog, “New global ID format”
- Relay, GraphQL Global Object Identification Specification
- Mark Seemann, “Keep IDs Internal with REST”
- Microservices.io, “Idempotent Consumer Pattern”
- Azure Architecture Center, “Event Sourcing Pattern”
- Referential Integrity - Wikipedia
- Enterprise Craftsmanship, “Entity vs Value Object”
- MongoDB Embedded Data vs References
- ByteBase, “UUID vs Auto-Increment”
- segmentio/ksuid - GitHub
- Segment/Twilio Blog, “A Brief History of the UUID”
- PlanetScale, “Why we chose NanoIDs”
- ULID Specification
- RESTfulAPI.net, REST API URI Naming Conventions
- HATEOAS - Wikipedia
- Snowflake ID - Wikipedia
- UUIDs in Microservices: v4 vs v7
- Identifiers in Microservices Systems - Medium
관련 키워드
Entity ID, Primary Key, Surrogate Key, Natural Key, UUID v4, UUID v7, Snowflake ID, ULID, KSUID, NanoID, Identity, Entity Integrity, Referential Integrity, DDD, Domain-Driven Design, Entity, Value Object, Aggregate, IDOR, Insecure Direct Object Reference, Idempotency Key, Correlation ID, Stripe Prefixed ID, GraphQL Global ID, Relay Node Interface, B-tree, Page Split, RFC 9562, REST, HATEOAS, Composite Key, 엔티티 무결성, 참조 무결성, 대리 키, 자연 키, 멱등성, 분산 시스템, 이벤트 소싱