본문 바로가기
Spring/ETC

@Tsid 커스텀 생성 후 적용하기 (사용X)

by YoonJong 2024. 5. 21.
728x90

6/3 

멀티스레드 테스트 도중 중복값이 발생하는 문제가 발생했다.

기존 라이브러리를 사용해서 커스텀하였는데, 중복값 발생하는 이슈가 올라왔는데 TSID 제작자가 그냥 이슈를 닫아버린 히스토리가 있었다. 

따라서, 라이브러리 문제로 판단하고 TSID Generater 라이브러리를 사용해 새롭게 커스텀했다.

https://josteady.tistory.com/963

 

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

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

josteady.tistory.com

 

---

 

왜 UUID 나 Random 값을 쓰지 않고 TSID 를 사용하는지 장단점을 살펴보고 싶다면 아래 블로그를 참고.

https://velog.io/@ssssujini99/%EA%B0%9C%EB%B0%9C-idPK-%EC%A7%81%EC%A0%91%ED%95%A0%EB%8B%B9-%EC%A0%84%EB%9E%B5-Random-UUID-TSID-%EA%B0%81%EA%B0%81-%EB%B9%84%EA%B5%90%EB%B6%84%EC%84%9D

 


왜 진행했는가?

현재 새로운 프로젝트 진행 중 Entity 에 @GeneratedValue 가 아닌 새로운 방식으로 ID 생성 값을 진행하는 것으로 확정했다.

 

이유는 현재 프로젝트의 구조와 기획에 있다.

간단히 정리하면,

1. 한국뿐만이 아닌 n개의 나라에 배포해야할 프로젝트 

2. 분산시스템

3. DataLake 사용

등이 있다.

 

TSID 는 아래와 같은 구조를 가진다.

https://velog.io/@ssssujini99/%EA%B0%9C%EB%B0%9C-idPK-%EC%A7%81%EC%A0%91%ED%95%A0%EB%8B%B9-%EC%A0%84%EB%9E%B5-Random-UUID-TSID-%EA%B0%81%EA%B0%81-%EB%B9%84%EA%B5%90%EB%B6%84%EC%84%9D

 

 

TSID 를 커스텀 하는 이유는 서버의 개수를 판단하여 현재 프로젝트에 맞게 더욱 세밀하게 적용시키기 위해서이다.

가장 중요한 것은, 해당 커스텀은 POJO를 기준으로 생성하였다.

처음에는 Config 파일을 만들어서 따로 커스텀 어노테이션을 만들어 적용했다.

 

해당 방법으로는 Entity 에서 사용할 수는 있으나, DDD 로 설계했을 경우 도메인(POJO) 을 통해서 ID 를 수동으로 생성해야 하기 때문에 Spring 에서 제공하는 기능을 제외하고 재작업하였다.

커스텀 하는 방법은 ?

먼저, @Tsid 를 적용하는 방법은 아래와 같다.

습관적으로 작성하던 @GeneratedValue 대신 @Tsid 를 넣어주면 된다.

@Id
@Tsid 
private Long id;

 

 

@Tsid 를 커스텀하기 위해 어떤 값을 설정해주어야 하는지 확인해보자.

 

Suppier<Factory> 를 구현하는 클래스를 넣어줄 수 있다.

 

아래 TsidFactory 라는 커스텀 클래스를 만들어준다.

LazyHolder 를 사용한 이유는 멀티스레드 환경에서 싱글톤 객체를 보장하기 위해서이다.

(싱글톤 멀티스레드 라고 검색하면 여러 방법이 나온다)

@Slf4j
public class TsidFactory implements Supplier<TSID.Factory> {

    private static int node = Integer.parseInt(System.getenv("ENV_NODE")); // 1
    private static int nodeBits= Integer.parseInt(System.getenv("ENV_NODEBITS")); // 2
    private static String clock = System.getenv("CLOCK"); // 3

    public TsidFactory() {
    }

    private static TSID.Factory createTSIDFactory() { // 4
        log.info("node={}", node);
        log.info("nodeBits={}", nodeBits);
        log.info("clock={}", clock);

        return TSID.Factory.builder()
                .withRandomFunction(TSID.Factory.THREAD_LOCAL_RANDOM_FUNCTION) // 보안강화
                .withNode(node) 
                .withNodeBits(nodeBits) 
                .withClock(Clock.system(ZoneId.of(clock))) // 나라에 맞는 시스템 시간 사용
                .build();
    }

    // 호출하는 순간 Class 로딩되며 초기화
    public static TSID.Factory getFactory() {
        return LazyHolder.INSTANCE;
    }

    private static class LazyHolder {
        // Class를 로딩하고 초기화하는 시점은 thread-safe를 보장
        // java 메모리에 올라와있기 때문에 싱글톤 객체 하나만 생성 (JVM 보장)
        private static final Factory INSTANCE = createTSIDFactory();
    }

    @Override
    public Factory get() {
        return getFactory();
    }
}

 

// 1, 2, 3 은 이후 도커 및 쿠버네티스를 이용해서 환경변수를 설정해줄 예정이기 때문에 테스트를 위해 .env 파일을 작성해 넣어주었다.

// 4 는 로그를 출력해서 정상적으로 env 값을 가져오는지 확인한다.

 

TSID.Facotry 를 이용해서 만들 때 커스텀하는 이유가 나온다.

.withNode()
.withNodeBits()
.withClock()

 

여러 Builder 값중 3개의 값이 가장 중요하다고 생각한다.

가장 위에 있는 블로그에 성능 테스트를 해주신 도표와 해당 값의 설명이 있으니 참고하면 좋을 것 같다.

 

env 파일은 아래와 같다.

ENV_NODE=22
ENV_NODEBITS=8
CLOCK=Asia/Seoul

 

정상적으로 싱글톤 객체인지, 멀티스레드 환경에서는 중복되지 않는지 테스트.

// 싱글톤 테스트
@Test
public void testSingletonBehaviorTest() {
    // 첫 번째 호출
    TSID.Factory factory1 = TsidFactory.getFactory();
    // 두 번째 호출
    TSID.Factory factory2 = TsidFactory.getFactory();

    System.out.println("factory1 = " + factory1);
    System.out.println("factory2 = " + factory2);

    // 두 객체가 같은지 확인
    assertSame(factory1, factory2);
}

factory1 = io.hypersistence.tsid.TSID$Factory@3ddc6915
factory2 = io.hypersistence.tsid.TSID$Factory@3ddc6915

--

// 멀티스레드 테스트 
 @Test
public void testSingletonBehaviorMultiTreadTest() throws InterruptedException {
    // 테스트할 스레드 수
    int threadCount = 10;

    // 스레드 풀 생성
    ExecutorService executorService = Executors.newFixedThreadPool(threadCount);

    // Runnable 객체 생성하여 스레드 풀에 추가
    for (int i = 0; i < threadCount; i++) {
        executorService.execute(() -> {
            TSID.Factory factory = TsidFactory.getFactory();
            System.out.println("Thread ID: " + Thread.currentThread().getId() + ", Factory: " + factory);
        });
    }

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

    // 스레드 풀에서 생성된 팩토리 인스턴스가 모두 동일한지 확인
    for (int i = 0; i < threadCount - 1; i++) {
        assertSame(TsidFactory.getFactory(), TsidFactory.getFactory());
    }
}
    
Thread ID: 26, Factory: io.hypersistence.tsid.TSID$Factory@6bb01223
Thread ID: 25, Factory: io.hypersistence.tsid.TSID$Factory@6bb01223
Thread ID: 18, Factory: io.hypersistence.tsid.TSID$Factory@6bb01223
Thread ID: 22, Factory: io.hypersistence.tsid.TSID$Factory@6bb01223
Thread ID: 27, Factory: io.hypersistence.tsid.TSID$Factory@6bb01223
Thread ID: 23, Factory: io.hypersistence.tsid.TSID$Factory@6bb01223
Thread ID: 19, Factory: io.hypersistence.tsid.TSID$Factory@6bb01223
Thread ID: 20, Factory: io.hypersistence.tsid.TSID$Factory@6bb01223
Thread ID: 21, Factory: io.hypersistence.tsid.TSID$Factory@6bb01223
Thread ID: 24, Factory: io.hypersistence.tsid.TSID$Factory@6bb01223

 

+ 추가 

커스텀 클래스에서 로그가 찍히는지 확인해본다.

 

정상적으로 콘솔에 출력되는 것을 볼 수 있다.

 

위의 결과는 애플리케이션을 실행시키고 Spring 애플리케이션 초기화 시 TsidFactory 도 같이 초기화 되는 것을 알 수 있다.

초기화되는 이유는 Entity 설정에 적용되어 있기 때문이다.

@Id
@Tsid(TsidFactory.class)
private Long id;

 

배운점 

@Tsid 에 대해서 처음알게 되었고, 커스텀에 관한 자료는 국내뿐만 아니라 해외사이트에도 찾지 못했다.

회사 선임분의 힌트를 받아서 커스텀을 하게 되었는데 좋은 기회가 되었다.

또한, Spring 에 의존하지 않고 따로 싱글톤을 구현하여 도메인에서 ID 를 수동으로 생성하도록 구현할 수 있는 새로운 접근방식을 알게 되었다.

728x90

댓글