SnowFlake를 이용한 대용량 데이터 생성 (2)

haru·2025년 1월 31일
0

스프링 프로젝트

목록 보기
3/3

SnowFlake를 사용한 데이터 생성

본격적으로 코드를 생성하여 테스트 데이터를 생성해보겠습니다.

🔥 SnowFlake 코드


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 전략에 대해서 설명해보겠습니다.

1️⃣ Snowflake ID란?

Snowflake는 전역적으로 고유한 ID를 생성하기 위한 알고리즘입니다.
64비트(8바이트) 크기의 ID를 다음과 같은 방식으로 구성합니다.

| 1비트(미사용) | 41비트(타임스탬프) | 10비트(노드 ID) | 12비트(시퀀스 번호) |

1비트: 항상 0 (미사용)
41비트: 기준 시간(startTimeMillis)부터 경과한 밀리초(ms)
10비트: 노드 ID (서버 또는 인스턴스 구분)
12비트: 같은 밀리초 내에서 증가하는 시퀀스 번호 (충돌 방지)

총 64비트로, 약 69년 동안 유니크한 ID를 생성할 수 있음

2️⃣ 필드값

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;
  • UNUSED_BITS = 1 → 사용되지 않는 비트 (항상 0)
  • EPOCH_BITS = 41 → 기준 시간(Epoch)으로부터 경과한 밀리초를 저장하는 비트 수
  • NODE_ID_BITS = 10 → Snowflake를 사용하는 서버(노드) ID를 저장하는 비트 수 (최대 1024개 노드 가능)
  • SEQUENCE_BITS = 12 → 같은 밀리초 내에서 충돌 방지를 위한 시퀀스 값 저장 (한 밀리초당 최대 4096개 생성 가능)

3️⃣ ID의 생성 로직 (1)

if (currentTimeMillis == lastTimeMillis) {
    sequence = (sequence + 1) & maxSequence;
    if (sequence == 0) {
        currentTimeMillis = waitNextMillis(currentTimeMillis);
    }
} else {
    sequence = 0;
}
  • 같은 밀리초(currentTimeMillis == lastTimeMillis)라면 시퀀스 값을 증가
  • 시퀀스 값이 4095를 넘어가면 밀리초가 변경될 때까지 대기

4️⃣ ID의 생성 로직 (2)

lastTimeMillis = currentTimeMillis;

return ((currentTimeMillis - startTimeMillis) << (NODE_ID_BITS + SEQUENCE_BITS))
        | (nodeId << SEQUENCE_BITS)
        | sequence;
  • 현재 시간 - 기준 시간(startTimeMillis)
    → 41비트를 차지
  • 노드 ID를 왼쪽으로 12비트(SEQUENCE_BITS) 이동
    → 10비트를 차지
  • 시퀀스를 그대로 추가
    → 12비트를 차지

🔥 Entity 생성


@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 레이어 생성

@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 를 통해서 코드를 더 자세히 볼 수 있습니다.

https://github.com/Jjd3109/micro-board

0개의 댓글