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, 엔티티 무결성, 참조 무결성, 대리 키, 자연 키, 멱등성, 분산 시스템, 이벤트 소싱