서비스를 개발할 때 대부분의 프로젝트는 AUTO_INCREMENT로 시작한다.
구현이 간단하고, 단일 DB 환경에서는 충분히 안정적이기 때문이다.
하지만 트래픽이 증가하고, 서버가 확장되기 시작하면
ID 생성 방식이 생각보다 빠르게 병목 지점이 된다.
이번 글에서는 Snowflake ID를 실제로 적용하면서 느낀 점을 중심으로
왜 필요한지, 언제 사용하는 것이 적절한지 정리해 본다.
Auto Increment의 특징과 한계
Auto Increment는 단일 DB 환경에서는 매우 효율적이다.
ID 생성 로직을 따로 구현할 필요가 없고, 순차 증가하기 때문에 정렬도 자연스럽다.
멀티 서버 환경에서도 기본적으로 문제없이 동작한다.
여러 애플리케이션 서버가 하나의 DB를 바라보는 구조라면
ID 생성은 DB가 담당하기 때문에 충돌 없이 관리된다.
다만 구조적으로 보면 중요한 특징이 있다.
ID 생성이 항상 DB를 통해 이루어진다는 점이다.
이 구조는 트래픽이 증가할수록 다음과 같은 문제로 이어질 수 있다.
- INSERT 요청 증가에 따른 DB write 부하 집중
- PK 생성 과정에서의 동시성 제어 비용 증가
- connection 증가 및 대기 시간 증가
- 특정 시점 lock 대기 발생
초기에는 문제가 없지만, 트래픽이 커질수록
DB가 ID 생성까지 책임지는 구조가 점점 부담이 된다.
Snowflake ID의 접근 방식
Snowflake는 접근 자체가 다르다.
ID를 DB가 아니라 애플리케이션에서 직접 생성한다.
ID는 다음 요소들을 조합해서 만들어진다.
- timestamp (시간)
- datacenterId (데이터센터)
- workerId (서버)
- sequence (동일 시간 내 증가값)
이 구조 덕분에 중앙 관리 없이도
여러 서버에서 동시에 고유한 ID를 생성할 수 있다.
구현 구조 핵심
Snowflake 구현에서 중요한 포인트는 세 가지다.
첫 번째는 시간 기반이다.
완전한 순차는 아니지만, 정렬 기준으로 충분한 수준의 순서를 보장한다.
두 번째는 서버 구분 값이다.
datacenterId + workerId 조합으로 서버를 구분한다.
세 번째는 시퀀스다.
같은 밀리초 내에서 여러 ID 생성 시 중복을 방지한다.
실제 적용 방법
이제 중요한 부분이다.
“그래서 어떻게 쓰는가”
1. Snowflake ID 생성기 구현
실무에서는 단순 synchronized 방식보다
CAS 기반(AtomicLong) 구조가 더 안정적이다.
@Slf4j
public class SnowflakeIdGenerator {
private final long workerId;
private final long datacenterId;
private final long sequenceMax = 4095L;
private final long workerIdShift = 12;
private final long datacenterIdShift = 17;
private final long timestampLeftShift = 22;
private final long twepoch = 1672531200000L;
private final AtomicLong lastTimestamp = new AtomicLong(-1L);
private final AtomicLong sequence = new AtomicLong(0L);
public SnowflakeIdGenerator(long workerId, long datacenterId) {
if (workerId < 0 || workerId > 31) {
throw new IllegalArgumentException("workerId must be 0~31");
}
if (datacenterId < 0 || datacenterId > 31) {
throw new IllegalArgumentException("datacenterId must be 0~31");
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
public long nextId() {
while (true) {
long currentTimestamp = System.currentTimeMillis();
long lastTime = lastTimestamp.get();
if (currentTimestamp < lastTime) {
throw new IllegalStateException("Clock moved backwards.");
}
if (currentTimestamp == lastTime) {
long seq = (sequence.incrementAndGet()) & sequenceMax;
if (seq == 0L) {
currentTimestamp = waitNextMillis(lastTime);
sequence.set(0L);
}
} else {
sequence.set(0L);
}
if (lastTimestamp.compareAndSet(lastTime, currentTimestamp)) {
long seq = sequence.get();
return ((currentTimestamp - twepoch) << timestampLeftShift)
| (datacenterId << datacenterIdShift)
| (workerId << workerIdShift)
| seq;
}
}
}
private long waitNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}
핵심은 이거다:
- synchronized 대신 CAS로 성능 확보
- 밀리초 단위 충돌 → sequence로 해결
- overflow → 다음 시간으로 대기
2. Spring Bean으로 등록
환경에 따라 workerId를 다르게 주입하는 구조가 중요하다.
@Configuration
public class SnowflakeConfig {
@Value("${snowflake.worker-id}")
private long workerId;
@Value("${snowflake.datacenter-id}")
private long datacenterId;
@Bean
public SnowflakeIdGenerator snowflakeIdGenerator() {
return new SnowflakeIdGenerator(workerId, datacenterId);
}
}
이 구조의 의미:
- 서버마다 설정만 다르게 하면 됨
- 코드 수정 없이 확장 가능
3. 서비스에서 사용
@Service
@RequiredArgsConstructor
public class OrderService {
private final SnowflakeIdGenerator idGenerator;
public void createOrder() {
Long id = idGenerator.nextId();
// 엔티티 생성 후 저장
}
}
기존과 차이:
- @GeneratedValue 제거
- 저장 전에 ID 생성
적용 이후 변화
실제로 적용하면 가장 먼저 체감되는 건 DB 부하다.
ID 생성이 애플리케이션으로 이동하면서
DB는 순수한 저장 역할에 집중하게 된다.
그 결과:
- write 트래픽 상황에서도 DB 안정성 증가
- connection 경합 감소
- lock 대기 감소
또한 서버 확장 시에도 구조 변경이 필요 없다.
workerId만 다르게 주면 끝이다.
실무에서 반드시 고려할 점
이 부분이 빠지면 글이 얕아진다.
1. workerId 충돌 금지
- 절대 중복되면 안 됨
- 보통:
- 환경 변수
- 서버별 설정
- Kubernetes pod index
2. 시간 역행 문제
- 서버 시간 밀리면 예외 발생
- 대응:
- NTP 동기화 필수
- 또는 rollback 처리 전략
3. ID 타입 전략
- 내부: Long 유지 (성능)
- 외부 노출:
- 그대로 사용 가능
- 또는 인코딩(base62 등)
언제 Snowflake를 고려해야 할까
다음 상황이면 고민할 시점이다.
- DB write 부하가 눈에 띄게 증가한 경우
- 서버를 여러 대로 확장한 경우
- 샤딩 또는 MSA 구조를 고려하는 경우
- ID를 외부에 노출해야 하는 경우
반대로 초기 서비스라면 Auto Increment로 충분하다.
정리
Auto Increment는 가장 빠른 시작점이다.
하지만 서비스가 성장하면 한계가 명확해진다.
Snowflake는 그다음 단계에서 자연스럽게 선택되는 방식이다.
핵심 차이는 하나다.
“DB가 ID를 생성하느냐, 애플리케이션이 생성하느냐”
이 차이가 결국
성능과 확장성을 결정한다.
'백엔드 개발' 카테고리의 다른 글
| Spring Boot Security 4.x 인증/인가 구현 (Resource · Menu · Action 기반 권한 설계) (0) | 2026.04.13 |
|---|---|
| JPA MyBatis 집계 한계와 Spring Batch로 해결하는 대용량 집계 처리 (0) | 2026.04.09 |
| Spring에서 @Autowired를 지양해야 하는 이유와No qualifying bean 오류가 발생하는 근본 원인 (0) | 2026.04.07 |
| Spring Boot 3.x → 4.x 마이그레이션 (실무 기준 변경 포인트 정리) (0) | 2026.04.06 |
| Spring 동시성 문제 해결 (synchronized부터 Redis Lock, Lua, Redisson까지 정리) (0) | 2026.04.06 |