목차
들어가며
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)
핵심 정리
- Auto Increment: 단순하고 빠르지만 분산 환경에 부적합
- UUID: 가장 안전하지만 성능 트레이드오프 존재
- Snowflake: 성능과 분산성의 균형, 구현 복잡도 높음
- Hybrid: 실무에서 가장 실용적인 접근
처음에는 Auto Increment로 시작하고, 필요할 때 UUID를 추가하는 하이브리드 방식이 가장 안전하다고 생각합니다!
'Spring > 이론' 카테고리의 다른 글
| Redis를 활용한 데이터베이스 캐싱 전략 (0) | 2025.10.23 |
|---|---|
| Spring Boot 환경에서의 직렬화(Serialization) 이해하기 (3) | 2025.10.22 |
| Spring Boot 검증 처리 아키텍처 전략: 컨트롤러에서 BindingResult를 직접 처리하지 말아야 하는 이유 (0) | 2025.09.16 |
| 데이터베이스 인덱스의 구조와 특징 (0) | 2025.06.18 |
| JPA 엔티티에서 @NoArgsConstructor(access = AccessLevel.PROTECTED)를 사용하는 이유 (0) | 2025.06.16 |