[Spring Boot] PK 전략 (INCREMENT, UUID, TSID)

익선·2024년 9월 5일
0

스프링부트

목록 보기
8/8

1) AUTO INCREMENT

PK의 값 생성을 데이터베이스에 위임하여 INSERT 할 때마다 데이터베이스가 PK를 자동으로 1씩 증가시키도록 하는 전략입니다.

장점

정수형 데이터 타입을 사용하므로 인덱스의 크키가 작고 INSERT 성능이 좋습니다.

단점

분산형 시스템에서 여러 데이터베이스로 INSERT 작업을 하는 경우 각각의 데이터베이스에서 키 값이 증가되어 키 중복 문제가 생길 수 있습니다.

또한 키를 예측하기 쉬워 SQL Injection 공격에 취약합니다.


2) UUID

UUID'Universally Unique Identifier' 의 약자로 128bit의 고유 식별자로 32개의 16진수 숫자가 4개의 하이픈으로 나누어진 8-4-4-4-12 형태입니다 (9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d)

장점

전세계 어디서나 고유한 ID 생성이 가능합니다 (1조개의 UUID 중에 중복이 일어날 확률은 10억분의 1)

독립적으로 생성되기 떄문에 분산형 시스템에서도 충돌 없이 사용 가능하고 예측이 어려워 보안성이 높습니다.

단점

128비트의 크기로 저장 공간을 더 많이 차지하며 인덱스의 크기도 큽니다.

UUID를 사용하면 CHAR(32)의 필드를 PK로 사용하게 되는데, 일반적으로 사용하는 BIGINT(8바이트)보다 4배 크다.

이를 개선하기 위해 BINARY(16)형태로 변환해서 저장하면 크기를 줄일 수 있다.

하지만 postgreSQL 에서는 BINARY(16) 타입이 없습니다.

또한 서버에서 uuid 를 생성해야 하므로 insert 시 시간이 더 걸린다

PostgreSQLuuid_generate_v4() 함수와 같은 내장 함수로 UUID를 자동으로 생성할 수 있습니다.

순차적이지 않아 정렬이 비효율적일 수 있으며 가독성이 좋지 않습니다.

분산형 시스템의 경우 (다중 DB) 데이터 일관성을 위해 UUID 를 PK 로 선택하고 단일 DB를 사용하면 AUTO_INCREMENT 키를 사용하는 것이 좋다. (성능 메모리 측면에서 이득)

따라서 애플리케이션 내부용으로는 자동증가 PK, 외부에 공개할 키로 UUID 사용해볼 수 있습니다.


3) TSID

ULID와 스노우플레이크 기법을 결합한 형태, 42bit의 타임스탬프와 22bit의 랜덤 구성 요소로 이루어져있다 (시간순 정렬리 가능하며 최대 69년까지 사용 가능)

커스텀 필요성

User 엔티티에 @Id @Tsid 를 붙여준 PK 필드는 기본적으로 전역 상태를 유지하지 않는다

실제 운영 환경에서는 다중 사용자가 동시에 회원가입을 진행하게 되어, 여러 User 인스턴스 각각이 독립적으로 ID를 생성하게 되는데 이때 전역 상태를 유지하지 않는다면 ID 충돌이 발생할 수 있습니다.

따라서 Tsid 생성기를 싱글톤으로 구현하여 모든 요청이 단일 인스턴스를 통해 이루어지도록 해야합니다.

싱글톤 패턴이란?

싱글톤 패턴이란 객체의 인스턴스를 한개만 생성되게 하는 패턴이다.
이러한 패턴은 주로 프로그램 내에서 하나로 공유 해야하는 객체가 존재할때 해당 객체를 싱글톤으로 구현하여 모든 유저 또는 프로그램들이 해당 객체를 공유하며 사용하도록 할 때 사용됩니다.

build.gradle

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

User 엔티티

@Getter
@Entity
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@Table(name = "users")
public class User extends BaseEntity {
    @Id @Tsid
    private Long id;
    private UUID uuid;
    private String email;
    private String password;
    private String username;
    private String phone;
    private String nickname;
    private LocalDate birthday;
    private String profileImg;
    private String businessNum; 
    private Role role;

    @Enumerated(EnumType.STRING)
    private BusinessStatus businessStatus;

    @Enumerated(EnumType.STRING)
    private SocialType socialType;

    @OneToMany(mappedBy = "user")
    private List<TradeItem> usedTradeItem;

    @OneToOne(mappedBy = "user")
    private Shop shop;
}

Tsid 커스텀 인터페이스

NODE는 각 서버가(혹은 노드)가 고유하게 식별될 수 있도록 하는 값이다. 여러 대의 서버가 함께 동작할 때, 각 서버를 구분하기 위해 이 값을 설정한다.
만약 서버가 3개라면 각각의 서버에 NODE=1, NODE=2 처럼 설정하여 구별이 가능

NODEBITS는 노드를 식별하는데 사용할 비트 수를 정의한다. 비트 수는 노드를 식별하는데 필요한 고유한 값을 표현하는 범위를 결정한다. 예를 들어 NODEBITS=2 는 2비트를 사용하여 최대 4개의 노드를 표현할 수 잇다는 의미이다.

LazyHolder 패턴을 사용X

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

    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 int nodeBits = 2;
        // private static String clock = System.getenv("ENV_TZ");
        private static String clock = "Asia/Seoul";

        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;
        }
    }
}

TsidGenerator

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

UserRepositoryTest

@DataJpaTest
@ActiveProfiles("dev") // application-test.yaml 과 같은 파일을 자동으로 찾음
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Import(QuerydslConfig.class)
public class UserRepositoryTest {
    @Autowired
    private UserRepository userRepository;
    
	@Test
    @DisplayName("싱글톤 테스트")
    public void testSingletonTest() {
        Tsid.FactorySupplierCustom instance1 = FactorySupplierCustom.INSTANCE;
        TsidFactory factory1 = instance1.get();
        Tsid.FactorySupplierCustom instance2 = FactorySupplierCustom.INSTANCE;
        TsidFactory factory2 = instance2.get();

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

        assertSame(factory1, factory2);
    }


  @Test
  @DisplayName("멀티 스레드 환경에서 TsidFactory 싱글톤 테스트")
  public void testTsidFactorySingletonInMultiThread() throws InterruptedException {
      Set<TsidFactory> factories = ConcurrentHashMap.newKeySet();
      // 테스트할 스레드 수
      int threadCount = 10;

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

      for (int i = 0; i < threadCount; i++) {
          executorService.execute(() -> {
              Tsid.FactorySupplierCustom instance = FactorySupplierCustom.INSTANCE;
              TsidFactory factory = instance.get();
              factories.add(factory);
          });
      }

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

      // 모든 스레드에서 가져온 TsidFactory가 동일한 인스턴스인지 검증
      assertEquals(1, factories.size());
  }

4) 결론

기존에는 create_at 필드를 사용하여 상품 등록순 정렬이라던지 사용자 등록순 정렬을 해왔지만, PK 만으로도 시간순 정렬이 된다는 점이 매력적으로 다가와 Tsid 를 적용해보았다. 정리하자면 pk는 tsid, 외부로 노출시킬 키로 사용할 uuid 별도로 구성하였습니다.

이번 Tsid를 사용하면서 처음으로 멀티스레드 환경에서의 동작을 테스트해봤는데

멀티 스레드 환경의 애플리케이션을 테스트 하는 방법에 대해서 알았고, 싱글톤 패턴에 대해서도 알게 되었다.

출처
MySql Auto_INCREMENT vs UUID 장단점 비교 및 결과
@Tsid 커스텀 생성 후 적용하기
@Tsid 커스텀 생성 후 적용하기 (수정)
[개발] id(PK) 직접할당 전략 - Random, UUID, TSID 각각에 대한 비교분석

profile
꾸준히 기록하는 사람

0개의 댓글