UUID란, UUID vs 자동증가 pk

2022. 11. 12. 04:02Spring 기초

개요

  💡 강의에서 엔티티의 PK를 UUID로 사용하고 있다.

  • UUID란 무엇이며, 자동 증가 PK 와 비교했을 때 어떤 장단점이 있는지 알아보았다.
  • MySQL에 UUID를 저장하고 조회할 때 발생할 수 있는 문제를 간략하게 정리했다. 

UUID란

  • UUID는 정보 식별을 위해 사용되는 식별자로, 128-bit 숫자로 구성되어있다.
  • 네트워크 상에서 각 개체들을 식별하기 위해서는 각각의 고유한 이름이 필요하며, 이 이름은 유일성이 매우 중요하다.
    • 중복되는 이름의 개체가 존재하면 구별이 불가능하기 때문이다.

  • 중앙에서 관리시스템을 두어 고유한 이름을 부여해 주면 고유성을 확보할 수 있다.
    • 하지만 독립적으로 개발되는 시스템들은 중앙 관리 시스템으로 관리할 수 없다.

  • 따라서 개발 주체가 스스로 이름을 지으면서 유일성을 충족시키는 방법이 필요했고, 바로 그것이 범용고유식별자(UUID)이다.

Format

  • xxxxxxxx-xxxx-**M**xxx-**N**xxx-xxxxxxxxxxxx 의 8-4-4-4-12 형식을 띄며 각 그룹에 대한 설명은 아래 순서와 같다(간략히 보고 넘어가자).

 

  Length  
Name bytes hex  bits Contents
time_low 4 8 32 integer giving the low 32 bits of the time
time_mid 2 4 16 integer giving the middle 16 bits of the time
time_hi_and_version 2 4 16     4-bit "version" in the most significant bits, followed by the high 12 bits of the time
clock_seq_hi_and_res clock_seq_low 2 4 16 1 to 3-bit "variant" in the most significant bits, followed by the 13 to 15-bit clock sequence
node 6
12    
48 48비트 노드 id

UUID vs AUTO INCREMENT

1. 기본키로 UUID를 사용할 경우 얻는 이점

  • UUID는 데이터에 대한 정보를 노출하지 않기 때문에 보안상 안전하다.
    • AUTO INCREMENT PK는 키 값이 외부에 노출되기 쉬우며 의도치 않게 정보가 노출될 수 있다.
    • 예를들어 회원 정보를 조회하는 API가 /user/{pk}/와 같은 URL 패턴이라면, 다른 고객의 정보가 쉽게 노출될 수 있다. 뿐만아니라 pk로 데이터의 수를 유추할 수 있으므로 비즈니스에서 의미가 있는 수치가 노출될 수도 있다.

  • 데이터베이스가 여러 개인 경우에도 하나의 UUID는 여러 데이터베이스 중에서도 고유한 값이다.
    • UUID는 독립적으로 개발되는 시스템에서도 유일성을 갖는 범용고유식별자
    • 서로 다른 테이블에서 관리되던 데이터를 하나의 데이터 소스로 합치기 쉽다.
    • ex) A 콘텐츠 테이블이 1개가 있고, 이를 검색 엔진 (ElasticSearch)에 복제하고 있다고 가정하자. 잠시 후 B 콘텐츠 테이블이 필요하게 되어, 이 정보를 동일한 ElasticSearch에 추가해야하는 상황이다. 만약 A, B 둘 다 숫자 기반의 pk를 사용하고 있었다면 두 콘텐츠의 ID가 충돌나는 현상이 발생하게 된다.
      • 하지만 pk가 UUID라면 별도로 분리되어 있던 데이터들을 통합해도 문제없다.

2. 기본 키를 UUID로 사용할 경우 단점

    • UUID는 increment pk 보다 더 많은 저장 장소를 필요로한다. (UUID - 128 bits)
    • 관계 테이블에서 fk로 UUID를 사용한다면, 더 많은 저장 공간을 사용하게 된다.
    • 테이블과 인덱스의 크기가 커지므로, DB의 디스크와 메모리를 많이 사용하게 된다.
      • 아무래도 버퍼방식으로 작동하는 부분에서 성능 문제가 발생할 듯 싶다. ex) 버퍼에 메모리가 금방 쌓이므로 랜덤 디스크 I/O 빈도가 상대적으로 늘어날 것

3. 둘 중 무엇을?

  • 애플리케이션 내부용 키로는 자동증가 pk, 외부에 공개할 키로는 uuid를 사용하는 것을 권장한다.
    • 애플리케이션 내부에서 자동증가 pk를 사용하면 성능과 저장 장소 측면에서 이점이 있다.
    • 만약 식별 값이 외부로 노출될 수도 있는 서비스라면 UUID로 데이터를 식별하는 것이 좋다.
    • 어떤 이유로든(외부 노출 등) UUID가 손상된다면 UUID를 변경해야 한다. PK를 변경하는 작업은 매우 값비싼데, UUID가 PK와 별개로 사용되는 경우 UUID를 변경하는 작업은 훨씬 저렴하다..

 

 

UUID or GUID as Primary Keys? Be Careful!

You can use of UUIDs as the primary key to avoid database scale problems. But should you? I propose an alternative.

tomharrisonjr.com

 

Best practices on primary key, auto-increment, and UUID in SQL databases

We're designing a table for user entity. The only non-trivial requirement is that there should be a permanent URL to the user entity (for example their profile). There's a lot about int/long vs UUI...

stackoverflow.com

 

 


UUID 조회 시 주의할 점

UUID에도 타입이 있다

강의에서 JDBC를 이용해 MySQL에 저장, 조회 기능을 구현했다.

customer 테이블의 pk로 UUID를 사용하고 있는데, 문제는 customer를 저장할 때 넣어준 pk와 조회한 customer의 pk가 서로 다른 값이 나온다는 것이다.

아래는 JDBC로 데이터에 저장, 조회하는 코드다.

 

저장

public int insertCustomer(UUID customerId, String name, String email) {
        try ( Connection connection = DriverManager.getConnection(URL, NAME, PW);
              PreparedStatement statement = connection.prepareStatement(INSERT_SQL);
        ) {
            statement.setBytes(1, customerId.toString().getBytes());
            statement.setString(2, name);
            statement.setString(3, email);

            return statement.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
            logger.error("Got error while closing connection", e);
        }

        return 0;
    }

 

조회(id로 단건 조회) 

select * from customers where customer_id = UUID_TO_BIN(?) ****

일단 customer_id 값만 리턴하도록 만들었다.

public UUID findById(UUID uuid) {
        UUID id = null;

        try ( Connection connection = DriverManager.getConnection(URL, NAME, PW);
              PreparedStatement statement = connection.prepareStatement(SELECT_BY_ID_SQL);
        ) {
            statement.setBytes(1, uuid.toString().getBytes());

            try (ResultSet resultSet = statement.executeQuery();
            ) {
                while (resultSet.next()) {
                    String customerName = resultSet.getString("name");
                    id = UUID.nameUUIDFromBytes(resultSet.getBytes("customer_id"));
                    LocalDateTime createAt = resultSet.getTimestamp("create_at").toLocalDateTime();
                }
            }
        } catch (SQLException e) {
            e.printStackTrace();
            logger.error("Got error while closing connection", e);
        }

        return id;
    }

 

테스트 코드

JdbcCustomerRepository repository = new JdbcCustomerRepository();

    @Test
    void id로단건조회() {
        repository.deleteAllCustomers();

        UUID userId = UUID.randomUUID();
        repository.insertCustomer(userId, "kiseo", "aaa@aaa.aaa");

        UUID findOneId = repository.findById(userId);

        assertThat(findOneId).isEqualTo(userId);
    }

 

눈으로만 보면 당연히 성공할 것 테스트인데 실패가 발생한다.

0b48b22d로 시작하는 UUID는 도대체 어디에서 나온 것일까?

IntelliJ에서 제공하는 쿼리 콘솔에서 테이블을 조회해봐도 0b48b22d로 시작하는 UUID는 없다.

조회 쿼리를 날린 뒤, ResultSet에서 결과를 바인딩할 때 UUID.nameUUIDFromBytes 메소드를 사용했는데 이는 type3의 UUID를 반환한다.

 

하지만 우리가 insert할 때 사용한 UUID는 UUID.randomUUID()를 통해 생성하는데, 이 메소드는 type4의 UUID를 반환한다.

 

이렇게 타입이 다르므로, DB에서 UUID를 가져올 때 다른 방식으로 바인딩을 해야 한다. ByteBuffer 클래스의 wrap 메소드를 이용해서 128 bits 인 UUID를 버퍼에 저장한 뒤 8bytes씩 끊어서 가져오는 방식이다.

static UUID toUUID(byte[] bytes) {
        ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
        return new UUID(byteBuffer.getLong(), byteBuffer.getLong());
 }

여기서 사용한 getLong() 메소드는 현재 위치부터 8바이트씩 읽어들인다.


UUID 저장 시 주의할 점

강의에서는 UUID에 해당하는 컬럼의 타입을 BINARY(16)으로 지정했는데

(
    customer_Id BINARY(16) PRIMARY KEY,
    name VARCHAR(20) NOT NULL,
	  ...
);

UUID를 BINARY(255), VARCHAR(36) 등 다른 타입으로 지정할 경우 발생하는 문제점이 있다.
BINARY(255) 등 16바이트인 UUID 보다 큰 값으로 지정할 경우 MySQL은 패딩값을 넣어서 채운다고 한다..

https://helloworld.kurly.com/blog/jpa-uuid-sapjil/

 

JPA 덕분에 DB에서 삽질한 이야기

DB에 저장을 했는데, 조회가 안 돼요

helloworld.kurly.com

 

UUID를 VARCHAR(36) 가변 문자열 타입으로 지정할 경우 number 타입보다 sorting 작업이 느리므로 권장하지 않는 듯 하다.

https://tomharrisonjr.com/uuid-or-guid-as-primary-keys-be-careful-7b2aa3dcb439

 

UUID or GUID as Primary Keys? Be Careful!

You can use of UUIDs as the primary key to avoid database scale problems. But should you? I propose an alternative.

tomharrisonjr.com

http://chongmoa.com/sql/3611

 

C.m.A API 이야기(Mokulsha!)

구 분 데이터 형식 범위 및 특징 문 자 형 - MySQL 5.0은 문자 유닛의 문자 컬럼 정의문에 있는 길이 지정문을 해석한다.(이전 버전은, MySQL 길이를 바이트 단위로 해석한다.) - CHAR, VARCHAR, 그리고 TEXT

chongmoa.com