본격적으로 코드를 생성하여 테스트 데이터를 생성해보겠습니다.
public class Snowflake {
private static final int UNUSED_BITS = 1;
private static final int EPOCH_BITS = 41;
private static final int NODE_ID_BITS = 10;
private static final int SEQUENCE_BITS = 12;
private static final long maxNodeId = (1L << NODE_ID_BITS) - 1;
private static final long maxSequence = (1L << SEQUENCE_BITS) - 1;
private final long nodeId = RandomGenerator.getDefault().nextLong(maxNodeId + 1);
// UTC = 2024-01-01T00:00:00Z
private final long startTimeMillis = 1704067200000L;
private long lastTimeMillis = startTimeMillis;
private long sequence = 0L;
public synchronized long nextId() {
long currentTimeMillis = System.currentTimeMillis();
if (currentTimeMillis < lastTimeMillis) {
throw new IllegalStateException("Invalid Time");
}
if (currentTimeMillis == lastTimeMillis) {
sequence = (sequence + 1) & maxSequence;
if (sequence == 0) {
currentTimeMillis = waitNextMillis(currentTimeMillis);
}
} else {
sequence = 0;
}
lastTimeMillis = currentTimeMillis;
return ((currentTimeMillis - startTimeMillis) << (NODE_ID_BITS + SEQUENCE_BITS))
| (nodeId << SEQUENCE_BITS)
| sequence;
}
private long waitNextMillis(long currentTimestamp) {
while (currentTimestamp <= lastTimeMillis) {
currentTimestamp = System.currentTimeMillis();
}
return currentTimestamp;
}
}
위와 같은 코드를 작성하였다. 순서대로 이 코드를 읽어보면서 SnowFlake 전략에 대해서 설명해보겠습니다.
Snowflake는 전역적으로 고유한 ID를 생성하기 위한 알고리즘입니다.
64비트(8바이트) 크기의 ID를 다음과 같은 방식으로 구성합니다.
| 1비트(미사용) | 41비트(타임스탬프) | 10비트(노드 ID) | 12비트(시퀀스 번호) |
1비트: 항상 0 (미사용)
41비트: 기준 시간(startTimeMillis)부터 경과한 밀리초(ms)
10비트: 노드 ID (서버 또는 인스턴스 구분)
12비트: 같은 밀리초 내에서 증가하는 시퀀스 번호 (충돌 방지)
총 64비트로, 약 69년 동안 유니크한 ID를 생성할 수 있음
private static final int UNUSED_BITS = 1;
private static final int EPOCH_BITS = 41;
private static final int NODE_ID_BITS = 10;
private static final int SEQUENCE_BITS = 12;
if (currentTimeMillis == lastTimeMillis) {
sequence = (sequence + 1) & maxSequence;
if (sequence == 0) {
currentTimeMillis = waitNextMillis(currentTimeMillis);
}
} else {
sequence = 0;
}
lastTimeMillis = currentTimeMillis;
return ((currentTimeMillis - startTimeMillis) << (NODE_ID_BITS + SEQUENCE_BITS))
| (nodeId << SEQUENCE_BITS)
| sequence;
@Table(name = "article")
@Getter
@Entity
@ToString
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Article {
@Id
private Long articleId;
private String title;
private String content;
private Long boardId; // shard key
private Long writerId;
private LocalDateTime createdAt;
private LocalDateTime modifiedAt;
public static Article create(Long articleId, String title, String content, Long boardId, Long writerId) {
Article article = new Article();
article.articleId = articleId;
article.title = title;
article.content = content;
article.boardId = boardId;
article.writerId = writerId;
article.createdAt = LocalDateTime.now();
article.modifiedAt = article.createdAt;
return article;
}
public void update(String title, String content) {
this.title = title;
this.content = content;
modifiedAt = LocalDateTime.now();
}
}
@Service
@RequiredArgsConstructor
@Log4j2
public class ArticleService {
private final Snowflake snowflake = new Snowflake();
private final ArticleRepository articleRepository;
@Transactional
public ArticleResponse create(ArticleCreateRequest request){
Article article = articleRepository.save(
Article.create(snowflake.nextId(), request.getTitle(), request.getContent(), request.getBoardId(),
request.getWriterId())
);
return ArticleResponse.from(article);
}
}
다음과 같이 서비스 레이어를 만든 후 테스트 코드를 작성하여 실행이 되는지 확인해보겠습니다.
public class ArticleApi {
RestClient restClient = RestClient.create("http://localhost:9000");
@Test
void createTest(){
//given
ArticleResponse articleResponse = create(new ArticleCreateRequest("h1", "my content", 1L, 1L));
//when
//then
System.out.println("response = " + articleResponse);
}
ArticleResponse create(ArticleCreateRequest articleCreateRequest){
return restClient.post()
.uri("/v1/articles")
.body(articleCreateRequest)
.retrieve()
.body(ArticleResponse.class);
}
다음과 같이 만들고 결과값을 확인해보겠습니다.

정상적으로 테스트가 완료한 뒤에 articleId에 Snowflake전략을 통해 만들어진 ID값이 보입니다. 완성입니다!
그렇다면 대용량 데이터를 넣어봐야겠죠?
@SpringBootTest
public class DataInitializar {
@PersistenceContext
EntityManager entityManager;
@Autowired
TransactionTemplate transactionTemplate;
Snowflake snowflake = new Snowflake();
CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
static final int BULK_INSERT_SIZE = 2000;
static final int EXECUTE_COUNT = 6000;
@Test
void initialize() throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for(int i = 0; i < EXECUTE_COUNT; i++){
executorService.submit(() -> {
insert();
countDownLatch.countDown();
System.out.println("lat = " + countDownLatch.getCount());
});
}
countDownLatch.await();
executorService.shutdown();
}
void insert(){
transactionTemplate.executeWithoutResult(transactionStatus -> {
for(int i = 0; i < BULK_INSERT_SIZE; i++){
Article article = Article.create(
snowflake.nextId(),
"title" + i,
"content" + i,
1L,
1L
);
entityManager.persist(article);
}
});
}
}
다음과 같이 생성을 해보았습니다. 그렇다면 앞에서부터 하나씩 코드 분석에 들어가보겠습니다.
🔹 필드명 설명
Snowflake snowflake = new Snowflake();
-> ID 생성을 위한 Snowflake 알고리즘 사용
CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
-> 멀티스레드 작업 완료를 기다리는 동기화 도구
static final int BULK_INSERT_SIZE = 2000;
-> 한 번의 트랜잭션에서 삽입할 데이터 개수
static final int EXECUTE_COUNT = 6000;
-> 총 실행할 쓰레드 개수 (6000번 실행)
🔹 initialize 설명
@Test
void initialize() throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for(int i = 0; i < EXECUTE_COUNT; i++){
executorService.submit(() -> {
insert();
countDownLatch.countDown();
System.out.println("lat = " + countDownLatch.getCount());
});
}
countDownLatch.await();
executorService.shutdown();
}
🔹 이 메서드의 역할
멀티스레드 환경에서 6000번 insert() 실행
Executors.newFixedThreadPool(10)
스레드 풀을 10개 생성 → 동시에 10개 작업을 병렬 실행 가능
executorService.submit(() -> insert());
6000개의 작업을 executorService에 제출 → 10개씩 병렬 실행됨
countDownLatch.countDown();
각 작업이 끝날 때마다 카운트 감소 (6000 → 5999 → 5998 ... → 0)
countDownLatch.await();
모든 작업이 끝날 때까지 대기
executorService.shutdown();
스레드 풀 종료 (더 이상 작업을 받지 않음)
** 🔹 insert 설명
void insert(){
transactionTemplate.executeWithoutResult(transactionStatus -> {
for(int i = 0; i < BULK_INSERT_SIZE; i++){
Article article = Article.create(
snowflake.nextId(),
"title" + i,
"content" + i,
1L,
1L
);
entityManager.persist(article);
}
});
}
🔹 이 메서드의 역할
트랜잭션 내에서 2000개의 Article 삽입
transactionTemplate.executeWithoutResult(transactionStatus -> { ... })
트랜잭션을 시작 → 내부 코드 실행 → 자동으로 커밋
예외 발생 시 자동으로 롤백
entityManager.persist(article);
JPA의 EntityManager를 이용해 Article 엔티티 저장
데이터 삽입 예시
title0, content0 → ID: 123456789
title1, content1 → ID: 123456790
...
title1999, content1999 → ID: 123458789
다음과 같이 진행을 하였을 때 12000건의 테스트 데이터가 생성이 된다.
조회 속도

결과 값

이런 식으로 대용량의 데이터를 집어넣고 테스트를 진행 할 수 있게 되었다.
아래에 GitHub 를 통해서 코드를 더 자세히 볼 수 있습니다.