2022. 11. 12. 04:02ㆍSpring 기초
개요
💡 강의에서 엔티티의 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라면 별도로 분리되어 있던 데이터들을 통합해도 문제없다.
- 하지만 pk가 UUID라면 별도로 분리되어 있던 데이터들을 통합해도 문제없다.
2. 기본 키를 UUID로 사용할 경우 단점
-
- UUID는 increment pk 보다 더 많은 저장 장소를 필요로한다. (UUID - 128 bits)
- 관계 테이블에서 fk로 UUID를 사용한다면, 더 많은 저장 공간을 사용하게 된다.
- 테이블과 인덱스의 크기가 커지므로, DB의 디스크와 메모리를 많이 사용하게 된다.
- 아무래도 버퍼방식으로 작동하는 부분에서 성능 문제가 발생할 듯 싶다. ex) 버퍼에 메모리가 금방 쌓이므로 랜덤 디스크 I/O 빈도가 상대적으로 늘어날 것
- 아무래도 버퍼방식으로 작동하는 부분에서 성능 문제가 발생할 듯 싶다. ex) 버퍼에 메모리가 금방 쌓이므로 랜덤 디스크 I/O 빈도가 상대적으로 늘어날 것
3. 둘 중 무엇을?
- 애플리케이션 내부용 키로는 자동증가 pk, 외부에 공개할 키로는 uuid를 사용하는 것을 권장한다.
- 애플리케이션 내부에서 자동증가 pk를 사용하면 성능과 저장 장소 측면에서 이점이 있다.
- 만약 식별 값이 외부로 노출될 수도 있는 서비스라면 UUID로 데이터를 식별하는 것이 좋다.
- 어떤 이유로든(외부 노출 등) UUID가 손상된다면 UUID를 변경해야 한다. PK를 변경하는 작업은 매우 값비싼데, UUID가 PK와 별개로 사용되는 경우 UUID를 변경하는 작업은 훨씬 저렴하다..
- 참고 링크
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/
UUID를 VARCHAR(36) 가변 문자열 타입으로 지정할 경우 number 타입보다 sorting 작업이 느리므로 권장하지 않는 듯 하다.
https://tomharrisonjr.com/uuid-or-guid-as-primary-keys-be-careful-7b2aa3dcb439
'Spring 기초' 카테고리의 다른 글
WebSocket & STOMP 그룹 채팅방 구현하기 (4) | 2023.05.16 |
---|---|
[SpringBoot] Annotation을 이용해 Slack에 Error log 남기기 (0) | 2023.03.18 |
순환 참조 문제(Setter 주입 vs 생성자 주입) (0) | 2022.11.02 |
수정자 DI, 생성자 DI (0) | 2022.11.02 |
IoC(Inversion of Control)란 (0) | 2022.11.01 |