드디어 ID 생성기 구현을 마무리하습니다.
만약 이전글을 못보셨다면 이해를 위해 한 번 보고오시는걸 추천드려요.
아래는 낙서처럼 보일 수 있지만.. 고민을 위한 치열했던 흔적들이 고스란히 담겨있습니다.
아래는 가상 면접 사례로 배우는 대규모 시스템 설계 7장 : 유일 ID 생성기 설계
을 보고 목표 및 요구사항을 재정의해보았습니다.
목표
엔티티 ID 생성기를 구현한다
요구사항
- 생성된 ID 값은 고유해야 함 (unique) - 유일성
- 개발자가 보았을때 쉽게 식별이 가능해야 함 - 식별가능성
- 로직 과정이 노출되어도 누구의 ID인지 유추할 수 없어야 함 - 보안성
추가 요구사항
- 발급 날짜에 따라 정렬 가능해야 함
추가로 개선해야하는 내용
- 시간순 정렬
- 보안성
user
와 같이 entity 정보 앞에 와도 될 것 같음- MySQL가 클러스터 인덱스를 생성할때 조건을 설정이 가능한가 ?
- 가능하다면 생성시간 기준으로 정렬이 가능한가 ?
위 요구사항을 만족하기 위해 많은 시간과 노력을 들였습니다.
이전글에서 마지막 요구사항인 정렬을 제외한 나머지 요구사항(유일성, 식별가능성, 보안성)에 대해 구현을 완료하였어요.
그렇다면 오늘 정렬에 대한 요구사항을 어떻게 만족하였는지 알아볼게요.
저는 이 정렬에 대해 처음에는 어렵게 접근을 하였어요.
추가 요구사항을 보고서 우선 이전에 진행되었던 구현에 대해서 고민해볼게 있었어요.
"지금은 Instant로 시간을 받아 나노초를 사용하여 랜덤한 숫자를 통해 보안성을 유지하고 있어. 하지만 이 랜덤한 숫자를 시간이 아닌 랜덤숫자로 만들 수는 없을까 ?"
저는 시간순으로 정렬을 해야한다는 것을 고민해보고 이 시간을 지금처럼 사용하면 안되겠다는 생각을 하였어요. 왜냐하면 ID 생성하는 과정에서 보안과 관련된 값을 정렬값으로도 사용한다는 점에서 2곳에서 사용이 되면 안되겠다 라고 생각했기 때문입니다.
그래서 찾아보니 Java 17에서 난수를 생성하는 RandomGenerator API 가 릴리즈 되어 쉽게 사용할 수 있었어요. 그래서 이걸 활용해야겠다 싶었습니다.
시간 -> 정렬에 사용
난수 -> 보안에 사용
이렇게 생각이 정리되고 설계와 구현을 시작해보았습니다.
기존에 ID를 생성하기 위해 사용했던 값들은 총 3가지로
였어요.
저는 기존에 이름 + 나노초를 활용하여 유일한 값을 생성하고있었어요
여기서 저는 나노초 대신 난수를 사용한다고 결정하였으므로 보안과 관련된 값 (유일값) 을 생성하기 위해 기존에 랜덤한 숫자로 사용하던 나노초를 빼고 RandkmGenerator API를 사용하여 난수를 넣었습니다. 그리고 시간기준 정렬이 되어야하기 때문이 시간값은 정렬에 사용하려고 생각했어요.
난수 생성
RandomGenerator generator = RandomGenerator.of("L128X256MixRandom");
int pseudoNumber = generator.nextInt(10000);
이 과정에서 빈자리를 0으로 채워야 하는 로직을 넣어주었어요.
String SPseudo = String.format("%4s", pseudo).replace(' ', '0');
시간을 정렬에 사용하기 위해서 떠올린 첫번째 방법은 2진수로 나타내기 입니다.
2진수로 시도한 이유는
책에서 숫자로 ID가 구성되어야 한다는 조건
2진수로 구성되어있을시 탐색속도가 빠르지 않을까?
라고 생각하였기 때문입니다.
우선 2진수로 나타내기 위해서 십진수를 2진수로 나타내면 어떨까 ?
각 범위별 숫자들을 더하여 나온 결과값을 2진수로 변환하여 총 8bit로 나타내자!
였습니다. 그렇게 각 시간 범위(연도, 월, 일 등등 )별로 더한 뒤 값을 구해보니 잘 나왔습니다.
예를 들어 2024-04-29T00:00:00 일 경우 24 + 4 + 29 와 같은 방식으로요!
(연도는 앞의 20을 제외하였습니다. 이 로직을 앞으로 70년 넘게 사용할 것 같지는 않았어요)
이렇게 쉽게 성공을 했네 ?!
하지만 여기에는 치명적인 오류가 있었어요. 바로 하나밖에 없는 목표인 정렬
이 되지 않았습니다.
단순히 저는
각 값들은 시간이 지날 수록 점점 커지니까 십진수의 값도 커질테고 그러면 결국 그 값들을 2진수로 표현하였을때에도 정렬이 되지 않을까 ?
하는 생각에 위와 같이 시도를 하였지만 다시 한 번 잘 생각해봅시다..
아래 2가지 예시를 함께 볼게요.
2024-04-29T00:00:00
2024-06-01T00:00:00
(이해를 돕기 위해 시간, 분은 0으로 하였습니다.)
전체 값을 더해보면
24 + 4 + 29 = 57
24 + 6 + 1 = 31
가 나오게 됩니다.
4월 29일이 분명 더 빠른날짜이며 값이 작게 나와야 합니다. 그래야 정렬이 됩니다.. 하지만 결과는 그렇지 못했어요.
저 십진수 값을 가지고 2진수를 만들어 정렬을 한다면 정렬이 되지 않을거에요.
이를 발견하고서는 저렇게 생각한 제 자신이 웃기기도 하면서 어떤 다른 방법이 있을까 다시 고민을 해보았습니다.
그러면 각 시간 범위별로 나누어서 최대값을 구한 뒤 최대한 적은 bit를 사용하여 2진수로 나타내볼까 ?
와 같이 생각해보았어요. 왜냐하면 이전에 했던 실수는 각 시간 범위들을 모두 한꺼번에 더했기 때문입니다. 그래서 이번에는 그 점을 보완하고자
각 범위에서만 계산을 해보자!
라고 생각을 하였어요.
그래서 계산을 해보니
각 연도, 월, 일, 시간, 분, 초 별로 사용할 수 있는 최대값은
99 / 12 / 31 / 23 / 59 / 59
였으며
각 연도, 월, 일, 시간, 분, 초 별로 사용해야하는 bit수는 각각
7 / 4 / 5 / 5 / 6 / 6
이었습니다.
이를 모두 더해보면
7 + 4 + 5 + 5 + ... 까지 생각하고 더하려고 보니 .. 이건 아니다 싶었습니다.
정렬을 하기 위해 2진수로 20이 넘는 자리수를 사용해서 ID로 생성한다고 ?
정말 이런 비효율도 없었습니다. 왜냐하면 다른 중요한 내용들 (보안, 식별가능성, 고유성 등) 으로 사용될 값들도 충분히 많을텐데 단지 정렬만을 위해서 이러한 비용을 들게 한다는 건 정말 잘못된 설계 같다는 생각이 바로 들었어요 ..
그리고 실제로 상상을 해보면
010111000001010111110100000110-user-d14f1298 .....
좀 .. 이상하지 않나요 ? 저는 이상했습니다.
그래서 다른 방법을 다시 고민해보았어요.
시간을 모두 2진수로만 나타내야할까 ? 정렬만 되면 되는게 아닌가 ?
그래서 연도는 십진수를 그대로 사용하고, 나머지 월, 일, 시간, 분 에 대해서는 아스키코드를 활용하여 정렬을 진행해보는 방식으로 생각을 하였습니다.
계산해보니 나머지에서 나올 수 있는 최대값 범위는 분
으로 십진수 59 였습니다.
아스키 코드에서 사용할 수 있는 문자는 0-9 / A-Z / a-z
로 총 10 + 26 + 26 = 62
로 계산이 떨어져 59 < 62 로 일대일 매칭이 문제없이 가능하다고 생각이 되었고 HashMap 자료구조를 활용하였습니다.
그래서 위에 사진 보시면 별표를 엄청 쳐놨습니다 ..
이걸 발견하고 이거다 !! 생각이 들었어요.
그래서 바로 구현에 들어가였고 구현 및 로직자체는 매우 쉬워 금방 구현하였습니다.
최종적으로 나온 ID 값은 아래와 같았어요.
244C85-user-XNc9rQ4i
숫자는 식별자 ID의 인덱스를 의미합니다
[0 - 1] '24' 는 연도
[2 - 5] '4C85' 월, 일, 시간, 분 값의 아스키 코드 값
[7 - 10] 'user' 엔티티 식별자
[12 - 19] 'abcd1234' (난수 + 이름) 을 분해 결합 로직을 거친 유일한 값
만약
보기 편하게 엔티티 식별값이 앞에오면 안되나 ? 이렇게
user-244C85-XNc9rQ4i
그냥 ID 생성하고 뒤에 시간부분으로 정렬하면 되는거 아닌가요 ?
라고 질문을 해주신다면 정말 좋은 질문입니다 ! 저도 그렇게 생각했었어요. (정렬 그냥 기준만 있으면 되는거 아니야 ?)
만약 위와 같이 생각해보셨다면 SQL의 LIKE문에 대해 정리한 아래 블로그도 한 번 방문해주세요 !! :)
이렇게 하여 ID 생성기 구현을 마무리 하였습니다.
사실 대부분의 많은 개발자들이 토이프로젝트를 진행할때에는 Long 타입으로 선언하여 AutoInCrement를 사용하거나 UUID를 많이 사용하고 계십니다. 오히려 UUID나 다른 알고리즘이 훨씬 성능상 좋을 수 있습니다. 하지만 이렇게까지 구현을 해본 이유는 다양한 요구사항이 제시되었을때 그에 대해 유연하게 대처를 하고싶었기 때문입니다. (사실 그냥 궁금했습니다. 이게 되나 ? 왜 되지?)