Skip to content

Commit 6409c22

Browse files
add post 'LCG를 활용한 ID 셔플링'
Signed-off-by: jonghoonpark <dev@jonghoonpark.com>
1 parent 92e39a0 commit 6409c22

File tree

4 files changed

+310
-0
lines changed

4 files changed

+310
-0
lines changed

_config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ baseurl: ""
1515
highlighter: rouge
1616
kramdown:
1717
input: GFM
18+
math_engine: mathjax
1819
syntax_highlighter_opts:
1920
default_lang: html
2021
css_class: "syntax"

_includes/head.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,15 @@
5959
<meta name="theme-color" content="#070707" />
6060

6161
<script src="/assets/js/mobile.min.js" type="text/javascript"></script>
62+
63+
<!-- MathJax -->
64+
<script>
65+
MathJax = {
66+
tex: {
67+
inlineMath: [['$', '$'], ['\\(', '\\)']],
68+
displayMath: [['$$', '$$'], ['\\[', '\\]']]
69+
}
70+
};
71+
</script>
72+
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" async></script>
6273
</head>

_posts/2026-02-21-LCG.md

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
---
2+
layout: "post"
3+
title: "LCG를 활용한 ID 셔플링"
4+
categories:
5+
- "개발"
6+
tags:
7+
- "LCG"
8+
- "Linear Congruential Generator"
9+
- "ID 셔플링"
10+
- "Base34"
11+
- "의사난수"
12+
- "ID 난독화"
13+
- "단축 URL"
14+
date: "2026-02-21 00:00:00"
15+
toc: true
16+
---
17+
18+
단일 데이터베이스를 쓸 경우 무난하게 사용할 수 있는 ID 생성 방식은 순차적으로 오르는 ID 를 사용하는 것이다. 이 방식의 아쉬운 점은 이전, 이후 id 가 유추가 가능하다는 것이다.
19+
20+
이럴 때, 오늘 알아볼 LCG를 활용한 ID 셔플링을 활용하면 간단한 알고리즘으로 좀 더 데이터를 은닉할 수 있게 된다. (아래에서 설명하겠지만 완벽한 보안을 보장하는 것이 아니다.)
21+
22+
## LCG란 무엇인가?
23+
24+
**LCG(Linear Congruential Generator, 선형 합동 생성기)**는 의사난수(pseudo-random number)를 생성하는 알고리즘 중 하나이다. 가장 오래되고 널리 알려진 PRNG(Pseudo-Random Number Generator) 중 하나이다.
25+
26+
"의사난수"라는 이름에서 알 수 있듯이, LCG는 진정한 난수가 아닌 결정론적(deterministic) 방식으로 난수처럼 보이는 수열을 생성한다. 동일한 시드(seed) 값을 사용하면 항상 동일한 수열이 생성된다.
27+
28+
## LCG의 수학적 원리
29+
30+
원리는 간단한 편이다.
31+
32+
LCG는 다음과 같은 점화식(recurrence relation)을 사용한다:
33+
34+
$$
35+
X_{n+1} = (a \cdot X_n + c) \mod m
36+
$$
37+
38+
각 변수의 의미는 다음과 같다:
39+
40+
| 변수 | 이름 | 설명 |
41+
| --------- | ---------------- | ------------------------- |
42+
| $X_n$ | 현재 값 | 현재 난수 값 |
43+
| $X_{n+1}$ | 다음 값 | 생성될 다음 난수 값 |
44+
| $a$ | 승수(multiplier) | 곱해지는 상수 |
45+
| $c$ | 증분(increment) | 더해지는 상수 |
46+
| $m$ | 모듈러(modulus) | 나머지 연산에 사용되는 값 |
47+
| $X_0$ | 시드(seed) | 초기값 |
48+
49+
### 작동 예시
50+
51+
간단한 예시로 LCG가 어떻게 동작하는지 살펴보자.
52+
53+
- $a = 5$, $c = 3$, $m = 16$, $X_0 = 7$ 로 설정하면:
54+
55+
| n | 계산 과정 | $X_n$ |
56+
| --- | --------------------------------------- | ----- |
57+
| 0 | 초기값 | 7 |
58+
| 1 | $(5 \times 7 + 3) \mod 16 = 38 \mod 16$ | 6 |
59+
| 2 | $(5 \times 6 + 3) \mod 16 = 33 \mod 16$ | 1 |
60+
| 3 | $(5 \times 1 + 3) \mod 16 = 8 \mod 16$ | 8 |
61+
| 4 | $(5 \times 8 + 3) \mod 16 = 43 \mod 16$ | 11 |
62+
63+
이런 식으로 연속적인 난수 수열이 생성된다.
64+
65+
## 좋은 파라미터 선택 기준
66+
67+
LCG 를 사용할 때에는 적절한 파라미터(a, c, m) 선택이 중요하다.
68+
69+
### 주기(Period)란?
70+
71+
LCG는 모듈러 연산을 사용하기 때문에 생성되는 수열은 결국 반복된다. **주기(period)**란 수열이 반복되기 전까지 생성되는 서로 다른 값의 개수를 말한다.
72+
73+
예를 들어, 위의 예시($a = 5$, $c = 3$, $m = 16$)에서 수열을 계속 생성하면 어느 순간 이전에 나왔던 값이 다시 나타나고, 그 이후로는 같은 패턴이 반복된다.
74+
75+
### 최대 주기(full period)
76+
77+
최대 주기는 가능한 모든 값(0부터 $m-1$까지)이 한 번씩 나타나는 것을 의미한다. 즉, 주기가 $m$과 같을 때 최대 주기를 갖는다고 한다. 최대 주기를 가지면 생성되는 난수가 가장 균등하게 분포되므로, 좋은 LCG는 최대 주기를 갖도록 파라미터를 선택해야 한다.
78+
79+
#### 최대 주기를 위한 조건(Hull-Dobell 정리)
80+
81+
최대 주기를 얻기 위해서는 다음 조건을 모두 만족해야 한다:
82+
83+
1. $c$와 $m$은 서로소(coprime)이어야 한다
84+
2. $a - 1$은 $m$의 모든 소인수로 나누어 떨어져야 한다
85+
3. $m$이 4의 배수이면, $a - 1$도 4의 배수여야 한다
86+
87+
### 실제 사용되는 파라미터 예시
88+
89+
| 사용처 | $m$ | $a$ | $c$ |
90+
| -------------------- | ------------ | ---------- | ------- |
91+
| glibc | $2^{31}$ | 1103515245 | 12345 |
92+
| Microsoft Visual C++ | $2^{32}$ | 214013 | 2531011 |
93+
| MINSTD | $2^{31} - 1$ | 48271 | 0 |
94+
95+
더 많은 파라미터 예시는 [LCG#Parameters in common use](https://en.wikipedia.org/wiki/Linear_congruential_generator#Parameters_in_common_use) 에서 확인해볼 수 있다.
96+
97+
## ID 셔플 함수로 활용하기
98+
99+
LCG 에 대해 알아보았으니 본격적으로 오늘 다뤄보고자 한 ID 셔플링에 대해 설명해보겠다.
100+
101+
원래 LCG는 이전 결과값을 다음 입력으로 사용하여 수열을 만드는 용도이다. 하지만 LCG의 수식을 빌려와 순차적 ID를 셔플(shuffle)하는 함수로 활용할 수도 있다.
102+
103+
$$f(id) = (a \cdot id + c) \mod m$$
104+
105+
이 방식은 순차적으로 증가하는 ID($0, 1, 2, 3, \ldots$)를 입력으로 넣어 난수처럼 보이는 값으로 변환한다:
106+
107+
$$f(0), f(1), f(2), f(3), \ldots$$
108+
109+
> 참고: 이는 LCG 수열의 n번째 항을 직접 계산하는 것과는 다르다. 여기서는 단순히 LCG의 선형 변환 수식을 ID 셔플 용도로 활용하는 것이다.
110+
111+
## 구현 예시
112+
113+
### Java
114+
115+
```java
116+
public class LCG {
117+
private final long a;
118+
private final long c;
119+
private final long m;
120+
121+
public LCG(long a, long c, long m) {
122+
this.a = a;
123+
this.c = c;
124+
this.m = m;
125+
}
126+
127+
// 직접 접근 방식: ID를 입력받아 변환된 값을 반환
128+
public long transform(long id) {
129+
return (a * id + c) % m;
130+
}
131+
132+
public static void main(String[] args) {
133+
// glibc 파라미터 사용
134+
LCG lcg = new LCG(1103515245, 12345, (1L << 31));
135+
136+
// 순차 ID(0, 1, 2, ...)를 난수처럼 보이는 값으로 변환
137+
for (int i = 0; i < 10; i++) {
138+
System.out.printf("ID %d -> %d%n", i, lcg.transform(i));
139+
}
140+
}
141+
}
142+
```
143+
144+
#### 실행 결과
145+
146+
```
147+
ID 0 -> 12345
148+
ID 1 -> 1103527590
149+
ID 2 -> 59559187
150+
ID 3 -> 1163074432
151+
ID 4 -> 119106029
152+
ID 5 -> 1222621274
153+
ID 6 -> 178652871
154+
ID 7 -> 1282168116
155+
ID 8 -> 238199713
156+
ID 9 -> 1341714958
157+
```
158+
159+
## Base34 변환
160+
161+
여기서 조금 더 난수처럼 보이고 싶다면 Base34 인코딩을 활용할 수 있다. Base34는 0-9(10개)와 A-Z에서 I, O를 제외한 24개 문자를 사용하여 총 34개의 문자로 숫자를 표현하는 방식이다.
162+
163+
> Base34 에서 `I`는 숫자 `1`과, `O`는 숫자 `0`과 혼동되기 쉬워 제외한다.
164+
165+
### 변환 원리
166+
167+
10진수를 34진법으로 변환하는 과정은 다음과 같다:
168+
169+
1. 숫자를 34로 나눈 나머지를 구한다
170+
2. 나머지에 해당하는 문자를 기록한다
171+
3. 몫이 0이 될 때까지 반복한다
172+
4. 기록한 문자를 역순으로 나열한다
173+
174+
### 구현 예시
175+
176+
#### Java
177+
178+
```java
179+
public class Base34 {
180+
private static final String CHARACTERS = "0123456789ABCDEFGHJKLMNPQRSTUVWXYZ";
181+
// I, O 제외 (혼동 방지)
182+
183+
public static String encode(long number) {
184+
if (number == 0) return "0";
185+
186+
StringBuilder result = new StringBuilder();
187+
while (number > 0) {
188+
int remainder = (int) (number % 34);
189+
result.append(CHARACTERS.charAt(remainder));
190+
number /= 34;
191+
}
192+
return result.reverse().toString();
193+
}
194+
195+
public static long decode(String encoded) {
196+
long result = 0;
197+
for (char c : encoded.toCharArray()) {
198+
result = result * 34 + CHARACTERS.indexOf(c);
199+
}
200+
return result;
201+
}
202+
203+
public static void main(String[] args) {
204+
LCG lcg = new LCG(1103515245, 12345, (1L << 31));
205+
206+
// 순차 ID를 LCG로 변환 후 Base34 인코딩
207+
for (int i = 0; i < 10; i++) {
208+
long value = lcg.transform(i);
209+
String encoded = encode(value);
210+
System.out.printf("ID %d -> %d -> %s%n", i, value, encoded);
211+
}
212+
}
213+
}
214+
```
215+
216+
### 출력 예시
217+
218+
```
219+
ID 0 -> 12345 -> AP3
220+
ID 1 -> 1103527590 -> Q9SQMU
221+
ID 2 -> 59559187 -> 1AKBST
222+
ID 3 -> 1163074432 -> RLBRRJ
223+
ID 4 -> 119106029 -> 2M4CWH
224+
ID 5 -> 1222621274 -> SWWSV8
225+
ID 6 -> 178652871 -> 3XPE07
226+
ID 7 -> 1282168116 -> U7FTYY
227+
ID 8 -> 238199713 -> 588F3X
228+
ID 9 -> 1341714958 -> VJ0V2N
229+
```
230+
231+
### 활용 예시
232+
233+
Base34로 인코딩된 LCG 출력은 다음과 같은 용도로 활용할 수 있다:
234+
235+
- **단축 URL 생성**: 긴 숫자 ID를 짧은 문자열로 변환
236+
- **고유 코드 생성**: 쿠폰 코드, 초대 코드 등
237+
- **파일명 생성**: 임시 파일이나 업로드 파일의 고유 이름
238+
239+
## LCG의 장점과 단점
240+
241+
### 장점
242+
243+
- **빠른 속도**: 단순한 산술 연산만 사용하므로 매우 빠르다
244+
- **적은 메모리**: 현재 상태 값 하나만 저장하면 된다
245+
- **재현 가능성**: 동일한 시드를 사용하면 동일한 수열을 재현할 수 있다
246+
- **구현 용이**: 알고리즘이 단순하여 구현하기 쉽다
247+
248+
### 단점
249+
250+
- **예측 가능성**: 연속된 몇 개의 값만 알면 전체 수열을 예측할 수 있다
251+
- **낮은 차원에서의 상관관계**: 연속된 값들을 n차원 공간에 점으로 찍으면 초평면(hyperplane) 위에 놓이는 특성이 있다 (Marsaglia의 정리)
252+
- **제한된 주기**: 최대 주기가 $m$으로 제한된다
253+
254+
## 사용 시 주의사항
255+
256+
### 보안 목적으로 사용하면 안 된다
257+
258+
LCG는 **암호학적으로 안전하지 않다(cryptographically insecure)**. 단순히 숫자가 커졌다 작아졌다 하는 착시를 줄 뿐, 패턴 분석에 매우 취약하다.
259+
260+
LCG는 선형성이 강하기 때문에, $a$와 $c$ 값을 모르더라도 몇 개의 (입력, 출력) 쌍만 알면 간단한 연립방정식으로 파라미터를 알아낼 수 있다. **실제 보안이 필요한 곳에서는 절대 사용하면 안 된다.**
261+
262+
보안이 필요한 경우 CSPRNG(Cryptographically Secure PRNG)를 사용해야 한다. Java에서는 `SecureRandom`이 이를 제공한다.
263+
264+
## 마무리
265+
266+
LCG는 단순하면서도 효과적인 의사난수 생성 알고리즘이다. 알아두면 유용하게 사용할 수 있을 것이다.
267+
268+
## 참고
269+
270+
### 최대 주기 실제로 확인해보기
271+
272+
간단한 예시로 최대 주기가 실제로 어떻게 만들어지는지 확인해보겠다.
273+
274+
$m = 8$, $a = 5$, $c = 1$로 설정하면 위에서 설명한 Hull-Dobell 조건을 모두 만족한다:
275+
276+
- $c = 1$과 $m = 8$은 서로소 ✓
277+
- $a - 1 = 4$는 $m$의 소인수 2로 나누어 떨어짐 ✓
278+
- $m = 8$이 4의 배수이고, $a - 1 = 4$도 4의 배수 ✓
279+
280+
$X_0 = 0$부터 시작하여 수열을 생성하면 다음과 같이 나온다.
281+
282+
| n | 계산 과정 | $X_n$ |
283+
| --- | ------------------------------------- | ----- |
284+
| 0 | 초기값 | 0 |
285+
| 1 | $(5 \times 0 + 1) \mod 8 = 1$ | 1 |
286+
| 2 | $(5 \times 1 + 1) \mod 8 = 6$ | 6 |
287+
| 3 | $(5 \times 6 + 1) \mod 8 = 31 \mod 8$ | 7 |
288+
| 4 | $(5 \times 7 + 1) \mod 8 = 36 \mod 8$ | 4 |
289+
| 5 | $(5 \times 4 + 1) \mod 8 = 21 \mod 8$ | 5 |
290+
| 6 | $(5 \times 5 + 1) \mod 8 = 26 \mod 8$ | 2 |
291+
| 7 | $(5 \times 2 + 1) \mod 8 = 11 \mod 8$ | 3 |
292+
| 8 | $(5 \times 3 + 1) \mod 8 = 16 \mod 8$ | **0** |
293+
294+
8번째 단계에서 다시 0으로 돌아왔다. 생성된 수열 **0 → 1 → 6 → 7 → 4 → 5 → 2 → 3 → 0**을 보면, 0부터 7까지 모든 값이 정확히 한 번씩 나타난 후 다시 처음으로 돌아온다.
295+
296+
![lcg example](/assets/images/2026-02-21-LCG/lcg-example.png)
297+
298+
이것이 바로 최대 주기($m = 8$)를 갖는 LCG의 특성이다.
24.2 KB
Loading

0 commit comments

Comments
 (0)