본문 바로가기
Spring/이론

Entity 식별자 전략: UUID vs Auto Increment vs Snowflake

by JuNo_12 2025. 10. 1.

목차

 

들어가며

Spring JPA에서 Entity의 식별자(ID)를 어떻게 설계할지는 시스템 아키텍처에서 중요한 결정사항입니다. 이 글에서는 세 가지 주요 식별자 전략(Auto Increment, UUID, Snowflake)의 특징과 장단점을 비교하고, 각 상황에 맞는 최적의 선택 방법을 알아보겠습니다.

 

1. Auto Increment (순차적 증가)

Auto Increment 개념

데이터베이스가 자동으로 1씩 증가시키는 정수형 ID를 생성하는 가장 전통적인 방식입니다.

@Entity
@Table(name = "p_user")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    private String email;
}

Auto Increment 장점

  • 단순하고 직관적: 구현이 쉽고 이해하기 쉬움
  • 작은 저장 공간: 숫자형으로 인덱스 크기가 작음 (4~8 bytes)
  • 빠른 조회 성능: B-Tree 인덱스에 최적화되어 있음
  • 순서 보장: 생성 순서를 자연스럽게 파악 가능
  • 디버깅 용이: 로그나 디버깅 시 가독성이 좋음

Auto Increment 단점

  • 보안 취약점: ID로 전체 데이터 개수나 생성 순서 추측 가능
  예: /api/users/1, /api/users/2, /api/users/3
  → 총 3명의 사용자가 있다는 것을 알 수 있음
  • 분산 환경 부적합: 여러 DB 서버에서 ID 충돌 가능성
  • 병합 어려움: 다른 시스템과의 데이터 병합 시 ID 중복 문제
  • 스케일링 제약: Sharding 시 ID 관리가 복잡해짐

Auto Increment 적합한 사용 사례

  • 단일 데이터베이스 환경
  • 작은~중간 규모의 애플리케이션
  • 보안이 크게 중요하지 않은 내부 시스템
  • 성능이 최우선인 경우
 

2. UUID (Universally Unique Identifier)

UUID 개념

128비트 길이의 고유 식별자로, 전 세계적으로 중복될 확률이 거의 없는 ID를 생성합니다.

@Entity
@Table(name = "p_payment")
public class Payment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;  // 내부 관리용 (옵션)
    
    @Column(name = "payment_public_id", nullable = false, unique = true, 
            updatable = false, columnDefinition = "BINARY(16)")
    private UUID paymentPublicId;  // 외부 노출용
    
    private Long userId;
    private Integer amount;
    
    @PrePersist
    public void generatePublicId() {
        if (this.paymentPublicId == null) {
            this.paymentPublicId = UUID.randomUUID();
        }
    }
}

UUID 예시:

550e8400-e29b-41d4-a716-446655440000

UUID 장점

  • 전역 고유성: 분산 환경에서도 충돌 없이 독립적으로 생성 가능
  • 보안 강화: 예측 불가능한 ID로 추측 공격 방지
  • 병합 용이: 여러 시스템의 데이터 병합 시 충돌 없음
  • 클라이언트 생성: DB 접근 없이 애플리케이션에서 미리 생성 가능
  • 프라이버시 보호: 데이터 개수나 순서 유추 불가

UUID 단점

  • 큰 저장 공간: 16 bytes로 Auto Increment 대비 2~4배
  • 성능 저하:
    • 인덱스 크기 증가로 메모리 사용량 증가
    • 무작위성으로 인한 인덱스 단편화
    • B-Tree 재조정 비용 증가
    • 페이지 분할 빈번 발생
  • 가독성 낮음: 디버깅이나 로그 확인 시 불편
  • URL 길이: RESTful API에서 URL이 길어짐

 

UUID 최적화 방법

1. 하이브리드 접근 (추천)

@Entity
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;  // DB 내부 관리 (조인, 인덱싱)
    
    @Column(columnDefinition = "BINARY(16)", unique = true)
    private UUID publicId;  // 외부 API 노출
    
    // publicId로 조회하는 메서드
    public static Order findByPublicId(UUID publicId) {
        // ...
    }
}

2. UUID v7 사용 (시간 기반)

// UUID v7은 시간 기반으로 정렬 가능 (Java 21+)
// 인덱스 단편화를 줄일 수 있음

3. BINARY(16)로 저장

// VARCHAR(36) 대신 BINARY(16) 사용하여 공간 절약
@Column(columnDefinition = "BINARY(16)")
private UUID id;

 

UUID 적합한 사용 사례

  • 마이크로서비스 아키텍처
  • 멀티 테넌트 시스템
  • 공개 API의 리소스 식별자
  • 보안이 중요한 결제, 개인정보 등
  • 여러 데이터 소스를 병합하는 경우
 

3. Snowflake ID

Snowflake 개념

Twitter에서 개발한 64비트 정수형 ID 생성 알고리즘으로, 시간 기반으로 정렬 가능하면서도 분산 환경에서 고유성을 보장합니다.

Snowflake 구조

┌─────────────────────────────────────────────────────────────────┐
│ 1bit │        41bit         │   10bit    │       12bit          │
│unused│     timestamp        │ machine ID │    sequence          │
│  0   │ 밀리초 단위 (69년)    │ (1024개)   │ 밀리초당 4096개       │
└─────────────────────────────────────────────────────────────────┘

구성 요소:

  • 1 bit: 부호 비트 (항상 0, 양수)
  • 41 bits: 타임스탬프 (밀리초 단위, 약 69년간 사용 가능)
  • 10 bits: 머신/데이터센터 ID (최대 1024개 서버 지원)
  • 12 bits: 시퀀스 번호 (밀리초당 최대 4096개 ID 생성)

생성 예시:

ID: 1234567890123456789
→ 시간순 정렬 가능
→ 어느 서버에서 생성되었는지 식별 가능

Snowflake 장점

  • 시간 기반 정렬: 생성 순서대로 정렬 가능
  • 높은 성능: Long 타입으로 UUID보다 빠름
  • 분산 환경 지원: 여러 서버에서 동시 생성 가능
  • 적은 저장 공간: 8 bytes (UUID의 절반)
  • 높은 처리량: 초당 수백만 개의 ID 생성 가능
  • 메타데이터 포함: 타임스탬프와 머신 ID 정보 내장

Snowflake 단점

  • 시간 의존성: 서버 시간 동기화 필수 (NTP)
  • 시계 역행 문제: 시간이 뒤로 가면 ID 생성 실패 가능
  • 구현 복잡도: Auto Increment보다 구현이 복잡
  • 머신 ID 관리: 서버마다 고유 ID 할당 필요
  • 순서 노출: 대략적인 생성 시간 추측 가능

 

Snowflake 적합한 사용 사례

  • 대규모 분산 시스템
  • 높은 처리량이 필요한 경우
  • 시간순 정렬이 중요한 데이터 (주문, 로그 등)
  • UUID의 성능 문제를 해결하고 싶을 때
  • 샤딩(Sharding)된 데이터베이스
 

4. 전략 비교표

기준 Auto Increment UUID Snowflake
크기 8 bytes 16 bytes 8 bytes
성능 매우 빠름 보통 빠름
보안 낮음 매우 높음 보통
분산 환경 불가능 가능 가능
순서 보장 가능 불가능 가능
구현 복잡도 매우 낮음 낮음 높음
가독성 매우 높음 낮음 보통
인덱스 효율 매우 높음 낮음 높음
URL 길이 짧음 중간

 

성능 비교 (초당 처리량)

Auto Increment:  ~100,000 TPS (단일 DB)
UUID:            ~50,000 TPS (인덱스 단편화)
Snowflake:       ~1,000,000 TPS (분산 환경)
 

5. 실무 적용 가이드

시나리오별 추천 전략

소규모 스타트업 (MVP 단계)

추천: Auto Increment
이유: 빠른 개발, 단순한 구조, 충분한 성능

 

성장 중인 서비스

추천: Hybrid (Auto Increment + UUID)
이유: 내부는 성능, 외부는 보안

 

대규모 분산 시스템

추천: Snowflake
이유: 높은 처리량, 시간 정렬, 분산 지원

 

결제/금융 시스템

추천: UUID
이유: 최고 수준의 보안과 고유성

 

실전 패턴: 하이브리드 접근

@Entity
@Table(name = "p_payment")
public class Payment {
    
    // 내부용: 조인, 인덱싱에 사용
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    // 외부용: API 노출, 보안
    @Column(name = "payment_public_id", 
            columnDefinition = "BINARY(16)", 
            unique = true, 
            nullable = false)
    private UUID publicId;
    
    // ... 비즈니스 필드
    
    @PrePersist
    public void init() {
        if (this.publicId == null) {
            this.publicId = UUID.randomUUID();
        }
    }
}
 
// Controller에서는 UUID만 노출
@GetMapping("/payments/{publicId}")
public ResponseEntity<PaymentDto> getPayment(
        @PathVariable UUID publicId) {
    Payment payment = paymentService.findByPublicId(publicId);
    return ResponseEntity.ok(PaymentDto.from(payment));
}

// 내부 Service에서는 Long ID로 빠른 조인
@Service
public class OrderService {
    public void processOrder(Long paymentId) {
        // paymentId로 빠른 조인 처리
        Payment payment = paymentRepository.findById(paymentId);
        // ...
    }
}

 

성능 최적화 팁

UUID 사용 시

-- BINARY(16)로 저장 (36 bytes → 16 bytes)
CREATE TABLE p_payment (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    public_id BINARY(16) NOT NULL UNIQUE,
    ...
) ENGINE=InnoDB;

-- UUID를 문자열로 저장할 경우 인덱스 길이 제한
CREATE INDEX idx_public_id ON p_payment (public_id(16));

-- 피해야 할 패턴
-- VARCHAR(36)로 저장하면 공간 낭비

 

Snowflake 사용 시

// 시계 역행 대비
@Scheduled(fixedRate = 1000)
public void syncTime() {
    // NTP 서버와 시간 동기화 체크
}

// 머신 ID 관리
// application.yml
snowflake:
  machine-id: ${HOSTNAME:1}  # K8s Pod name 등 활용
 

마치며

의사결정 플로우차트

시작
  ↓
단일 DB? ────Yes──→ Auto Increment
  ↓ No
  ↓
높은 처리량 필요? ──Yes──→ Snowflake
  ↓ No
  ↓
보안 최우선? ──Yes──→ UUID
  ↓ No
  ↓
Hybrid 접근 (Auto Increment + UUID)

핵심 정리

  1. Auto Increment: 단순하고 빠르지만 분산 환경에 부적합
  2. UUID: 가장 안전하지만 성능 트레이드오프 존재
  3. Snowflake: 성능과 분산성의 균형, 구현 복잡도 높음
  4. Hybrid: 실무에서 가장 실용적인 접근

 

처음에는 Auto Increment로 시작하고, 필요할 때 UUID를 추가하는 하이브리드 방식이 가장 안전하다고 생각합니다!