Spring/ETC

@Tsid 커스텀 생성 후 적용하기 (수정)

YoonJong 2024. 6. 3. 13:11
728x90

사용이유

  • 대규모 분산 시스템에서의 병렬처리 가능 및 고유성 보장
  • 8byte 로 공간 효율성 향상
  • 타임스탬프 값을 비트 가장 앞에 배치하여 정렬 순서 보장되어 데이터 삽입, 조회 시 성능 향상

 

As-Is

  • 기존 @GeneratedValue(strategy = GenerationType.IDENTITY) 사용
    • 동시성 문제 발생
    • 국가별 프로젝트의 모든 테이블을 하나의 테이블로 관리 시, ID 중복 발생

To-Be

  • TSID 를 이용하여 분산 환경에서 PK 고유 생성 보장.
    • 8bite 로 공간 효율
    • 순차적으로 증가(timestamp) 정렬 보장하여 성능 효율
  • UUID 생성 또한 고려하였지만, 선택 X
    • 36자리 문자열로 구성되어 있어 공간 효율성 문제
    • 데이터 삽입, 조회 시 성능 문제 (인덱스 비효율)

 

Gradle 의존성 추가

// https://mvnrepository.com/artifact/com.github.f4b6a3/tsid-creator
implementation group: 'com.github.f4b6a3', name: 'tsid-creator', version: '5.2.6'

 

Entity 클래스

@Entity
@Table(name = "member")
@NoArgsConstructor
public class MemberEntity  {

    @Id
    @RimanTsid
    private Long id;
}

 

@RimanTsid 커스텀 어노테이션

@IdGeneratorType(RimanTsidGenerator.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface RimanTsid {

    Class<? extends Supplier<TsidFactory>> value() default FactorySupplierCustom.class;

    @Slf4j
    public static class FactorySupplierCustom implements Supplier<TsidFactory> {

        private static int nodeBits = Integer.parseInt(System.getenv("ENV_NODE_BITS"));
        private static String clock = System.getenv("ENV_TZ");

        public static final FactorySupplierCustom INSTANCE = new FactorySupplierCustom();
        private final TsidFactory tsidFactory;

        public FactorySupplierCustom() {
            this.tsidFactory = TsidFactory.builder()
                    .withNodeBits(nodeBits)
                    .withClock(Clock.system(ZoneId.of(clock)))
                    .withRandomFunction(() -> ThreadLocalRandom.current().nextInt())
                    .build();
        }

        @Override
        public TsidFactory get() {
            return this.tsidFactory;
        }
    }
}

 

RimanTsidGenerator 클래스

public class RimanTsidGenerator implements IdentifierGenerator {

    @Override
    public Serializable generate(SharedSessionContractImplementor session, Object object) {
        RimanTsid.FactorySupplierCustom factorySupplier = RimanTsid.FactorySupplierCustom.INSTANCE;
        return factorySupplier.get().create().toLong();
    }
}

 

 

테스트 진행

  • 테스트 환경
    • 로컬 테스트 진행 ( 1ms 당 스레드가 접근하는 개수는 랜덤 )
    • 1000 개의 스레드가 동시에 접근
@Test
@DisplayName("멀티스레드 환경에서도 싱글톤으로 생성된다.")
void multiThreadSingleTonTest() throws InterruptedException {

    Set<Object> setSingleTonList = ConcurrentHashMap.newKeySet();

    int threadCount = 1000; // 병렬로 실행할 스레드 수
    ExecutorService executorService = Executors.newFixedThreadPool(threadCount);

    for (int i = 0; i < threadCount; i++) {
        executorService.execute(() -> {
            setSingleTonList.add(FactorySupplierCustom.INSTANCE.hashCode());
        });
    }

    // 스레드 풀 종료 대기
    executorService.shutdown();
    executorService.awaitTermination(10, TimeUnit.SECONDS);

    assertEquals(setSingleTonList.size(), 1);

}


@RepeatedTest(50)
@DisplayName("[Custom @RimanTsid Test] 스레드 n개가 동시에 접근할 경우에도 ID가 중복되지 않는다.")
void multiThreadTest2() throws InterruptedException {

    Set<Long> setIdList = ConcurrentHashMap.newKeySet();

    int threadCount = 1000; // 싱글 스레드 요청 개수
    ExecutorService executorService = Executors.newFixedThreadPool(threadCount);

    for (int i = 0; i < threadCount; i++) {
        executorService.execute(() -> {
            long customTsid = FactorySupplierCustom.INSTANCE.get().create().toLong();
            setIdList.add(customTsid);
        });
    }

    // 스레드 풀 종료 대기
    executorService.shutdown();
    executorService.awaitTermination(10, TimeUnit.SECONDS);

    assertEquals(setIdList.size(), threadCount);
}


@RepeatedTest(50)
@DisplayName("[Custom @RimanTsid Generate Entity Test] 스레드 n개가 동시에 접근할 경우에도 ID가 중복되지 않는다.")
void multiThreadTest() throws InterruptedException {

    Set<Long> setIdList = ConcurrentHashMap.newKeySet();

    int threadCount = 1000; // 병렬로 실행할 스레드 수
    ExecutorService executorService = Executors.newFixedThreadPool(threadCount);

    for (int i = 0; i < threadCount; i++) {
        executorService.execute(() -> {
            MemberEntity memberEntity = new MemberEntity();

            memberRepository.save(memberEntity);
            setIdList.add(memberEntity.getId());
        });
    }

    // 스레드 풀 종료 대기
    executorService.shutdown();
    executorService.awaitTermination(30, TimeUnit.SECONDS);

    System.out.println("setList.size() = " + setIdList.size());
    assertEquals(setIdList.size(), threadCount);
}



@Test
@DisplayName("TsidFactory 커스텀 시, WithNodeBits 설정이 정상 적용된다.")
void multiThreadTest3() throws InterruptedException {

    Set<Long> setList = ConcurrentHashMap.newKeySet();
    Map<Long, Integer> timestampCounts = new ConcurrentHashMap<>();

    int threadCount = 1000; // 싱글 스레드 요청 개수
    ExecutorService executorService = Executors.newFixedThreadPool(threadCount);

    for (int i = 0; i < threadCount; i++) {
        executorService.execute(() -> {
            long customTsid = FactorySupplierCustom.INSTANCE.get().create().toLong();
            setList.add(customTsid);
            long timestamp = customTsid >> 22; // 상위 42bits 추출 - TSID 는 64bits
            timestampCounts.merge(timestamp, 1, Integer::sum);
        });
    }

    // 스레드 풀 종료 대기
    executorService.shutdown();
    executorService.awaitTermination(10, TimeUnit.SECONDS);

    System.out.println("setList.size() = " + setList.size());
    assertEquals(setList.size(), threadCount);

    // DateTimeFormatter를 사용하여 타임스탬프를 보기 좋게 변환
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")
            .withZone(ZoneId.systemDefault());

    // 타임스탬프가 동일한 경우 출력
    timestampCounts.forEach((timestamp, count) -> {
        if (count > 0) {
            String formattedTimestamp = formatter.format(Instant.ofEpochMilli(timestamp));
            System.out.println("Timestamp: " + formattedTimestamp + ", Count: " + count);
        }
    });
}
728x90