1. 개요
2. 상세
생각 외로 컴퓨터는 난수를 간단히 만들 수 없다. 컴퓨터는 기본적으로 정해진 입력에 따라 정해진 값을 낼 뿐이다. 사람처럼 무의식적인 선택, 혹은 우연에 의한 선택을 할 수 없기 때문이다. 전문용어로는 결정적 유한 오토마타(Deterministic Finite Automata). 흔히 보는 랜덤은 정말로 임의의 값이 아니고 특정한 방법으로 계산하거나 몇 밀리초(ms) 단위로 시시각각 변하는 값을 초기값으로 잡은 다음 여러 계산 과정을 거쳐 사람이 볼 때에는 마치 임의의 값인 것처럼 보이게 하는 것이다. 의사난수(Pseudo Random)라고 한다.
흔히 난수표를 쓰는데 난수표가 정해진 이상 결국 같은 순서로 같은 숫자가 나오게 된다. 패미컴 버전 봄버맨에서 맨 처음 플레이할 때 1탄의 벽돌들이 대체로 일정하게 배열된다는 점을 떠올리면 어느 정도 이해가 갈 것이다. 비슷한 예로 DOS 버전 테트리스는 나오는 블럭의 순서가 모두 똑같았으며, 크레이지 아케이드 테트리스도 난수가 제대로 생성되지 않았던 탓에 테트리스 계의 똥겜으로 전락할 뻔했다.
이를 해결하기 위한 방법은 난수표를 여러개 만들어 놓고 매번 다른 난수표를 읽도록 만드는 것이다. 이 난수표를 선택하는 것을 시드라고 한다. 그런데 시드값이 똑같으면 선택되는 난수표도 똑같기 때문에 시드값도 난수여야 한다. 즉 난수를 만들려면 난수가 필요하다는 문제가 발생하게 되는 것. 그래서 보통은 시드값으로 현재 시간을 넣어서 해결한다.[1] 일반적으론 밀리초, 즉 1/1000초 단위의 값이니 인간이 의도적으로 같은 값을 내게 하는건 불가능에 가깝다. Windows에선 시간 이외에도 컴퓨터가 켜져있는 시간 (ns단위), CPU 메모리의 클럭/온도, 프로세스 ID나 쓰레드 ID (매 부팅마다 방 온도나 발전소 전기 품질, 컴퓨터 내부 부품 등에 따라 어느정도씩 전부 달라질 수 있는 것 들이다.) 등도 사용한다. 마지막으로 생성한 난수값을 별도로 저장해 뒀다가 그것을 시드로 하기도 하고 사용자의 입력 행동 자체를 시드로 하기도 한다. 사용자의 마우스 움직임[2]이나 키보드의 키를 누른 시간 간격 등을 샘플링해서 그것을 시드값으로 삼기도 한다. 이와 같이 컴퓨터가 진짜 랜덤값을 얻을 수 있는 방법이 수도 없이 많다. 컴퓨터에서 쓰는 시드값은 진짜 랜덤이라 보아도 무방하고, C언어 `rand()` 함수를 정말 시간 값만으로 초기화하는, 수십 년도 더 된 알고리즘을 쓰는 프로그램들만 문제가 된다 보아도 무방하다.
여러 개선책이 있지만 근본적으로 시드를 기반으로 한 랜덤함수는 완전한 무작위라고 보기가 힘들다. 특정 패턴의 경우 영원히 안 나올 수도 있다. 암호화에도 문제가 되는데, 시드 기반 난수로 암호화 키를 생성하는 경우 키 값의 일부를 알고 있다면 나머지 키 값도 유추하는게 가능해질 수 있다. 일반적인 게임에서는 어느정도의 무작위성으로도 충분하지만, 현찰이 오가는 도박 사이트처럼 정말 랜덤이 필요한 경우 시드 기반의 의사랜덤은 쓰지 않는다.
1997년 개발된 MT19937(후술할 메르센 트위스터의 일종이다.)를 예로 들자면, 이 알고리즘으로 생성된 난수는 623개까지 연속된 난수를 뽑아볼 때 각 난수의 처음 32비트는 동일분포가 보장된다(32-bit accuracy). 다만 623에서 단 하나 늘어난 624개의 연속된 난수만 어떻게 확보한다면 시드값을 털어버릴 수 있는 치명적인 문제가 있어서 보안이 필요한 곳에서 쓰려면 좀 꼼수가 필요하다.[3] 결과값을 공개하기전에 해쉬함수로 원본을 알아내지 못하게 한다던가 난수를 한번에 생성한후 순서를 뒤섞어서 공개한다던가. 이런 좋은 MT19937이란 알고리즘은 Python, PHP 등 대부분의 프로그래밍 언어에서도 이미 기본으로 쓰고 있는 알고리즘이고, C/C++에서 쓰고 있는 rand() 함수가 언어 역사만큼 오래 된 안 좋은 랜덤 알고리즘을 쓰고 있을 뿐이다.
현실을 재현하는 것이 아닌 실용 프로그래밍, 특히 단방향 암호화를 포함한 보안 계열을 제외한 실제 응용 프로그램 설계에선 완전 난수보다 의사 난수가 더 많은 분야에 유용하다. 의사난수의 '같은 시드는 같은 결과를 부른다'를 장점으로 활용해, 네트워크 게임의 플레이어 간 동기화에서부터 리플레이 모드 구현까지(스타크래프트의 리플레이 모드를 생각해 보면 된다. 게임의 시드만 저장하면 AI의 모든 판단을 다 저장할 필요가 없어진다!) 오만 곳에 써먹을 수 있다. 마찬가지로 도박에서 원하는 결과가 나오게 악용할 수 있다. 이런 걸 난수조절이라고 한다.
흔히 난수표를 쓰는데 난수표가 정해진 이상 결국 같은 순서로 같은 숫자가 나오게 된다. 패미컴 버전 봄버맨에서 맨 처음 플레이할 때 1탄의 벽돌들이 대체로 일정하게 배열된다는 점을 떠올리면 어느 정도 이해가 갈 것이다. 비슷한 예로 DOS 버전 테트리스는 나오는 블럭의 순서가 모두 똑같았으며, 크레이지 아케이드 테트리스도 난수가 제대로 생성되지 않았던 탓에 테트리스 계의 똥겜으로 전락할 뻔했다.
이를 해결하기 위한 방법은 난수표를 여러개 만들어 놓고 매번 다른 난수표를 읽도록 만드는 것이다. 이 난수표를 선택하는 것을 시드라고 한다. 그런데 시드값이 똑같으면 선택되는 난수표도 똑같기 때문에 시드값도 난수여야 한다. 즉 난수를 만들려면 난수가 필요하다는 문제가 발생하게 되는 것. 그래서 보통은 시드값으로 현재 시간을 넣어서 해결한다.[1] 일반적으론 밀리초, 즉 1/1000초 단위의 값이니 인간이 의도적으로 같은 값을 내게 하는건 불가능에 가깝다. Windows에선 시간 이외에도 컴퓨터가 켜져있는 시간 (ns단위), CPU 메모리의 클럭/온도, 프로세스 ID나 쓰레드 ID (매 부팅마다 방 온도나 발전소 전기 품질, 컴퓨터 내부 부품 등에 따라 어느정도씩 전부 달라질 수 있는 것 들이다.) 등도 사용한다. 마지막으로 생성한 난수값을 별도로 저장해 뒀다가 그것을 시드로 하기도 하고 사용자의 입력 행동 자체를 시드로 하기도 한다. 사용자의 마우스 움직임[2]이나 키보드의 키를 누른 시간 간격 등을 샘플링해서 그것을 시드값으로 삼기도 한다. 이와 같이 컴퓨터가 진짜 랜덤값을 얻을 수 있는 방법이 수도 없이 많다. 컴퓨터에서 쓰는 시드값은 진짜 랜덤이라 보아도 무방하고, C언어 `rand()` 함수를 정말 시간 값만으로 초기화하는, 수십 년도 더 된 알고리즘을 쓰는 프로그램들만 문제가 된다 보아도 무방하다.
여러 개선책이 있지만 근본적으로 시드를 기반으로 한 랜덤함수는 완전한 무작위라고 보기가 힘들다. 특정 패턴의 경우 영원히 안 나올 수도 있다. 암호화에도 문제가 되는데, 시드 기반 난수로 암호화 키를 생성하는 경우 키 값의 일부를 알고 있다면 나머지 키 값도 유추하는게 가능해질 수 있다. 일반적인 게임에서는 어느정도의 무작위성으로도 충분하지만, 현찰이 오가는 도박 사이트처럼 정말 랜덤이 필요한 경우 시드 기반의 의사랜덤은 쓰지 않는다.
1997년 개발된 MT19937(후술할 메르센 트위스터의 일종이다.)를 예로 들자면, 이 알고리즘으로 생성된 난수는 623개까지 연속된 난수를 뽑아볼 때 각 난수의 처음 32비트는 동일분포가 보장된다(32-bit accuracy). 다만 623에서 단 하나 늘어난 624개의 연속된 난수만 어떻게 확보한다면 시드값을 털어버릴 수 있는 치명적인 문제가 있어서 보안이 필요한 곳에서 쓰려면 좀 꼼수가 필요하다.[3] 결과값을 공개하기전에 해쉬함수로 원본을 알아내지 못하게 한다던가 난수를 한번에 생성한후 순서를 뒤섞어서 공개한다던가. 이런 좋은 MT19937이란 알고리즘은 Python, PHP 등 대부분의 프로그래밍 언어에서도 이미 기본으로 쓰고 있는 알고리즘이고, C/C++에서 쓰고 있는 rand() 함수가 언어 역사만큼 오래 된 안 좋은 랜덤 알고리즘을 쓰고 있을 뿐이다.
현실을 재현하는 것이 아닌 실용 프로그래밍, 특히 단방향 암호화를 포함한 보안 계열을 제외한 실제 응용 프로그램 설계에선 완전 난수보다 의사 난수가 더 많은 분야에 유용하다. 의사난수의 '같은 시드는 같은 결과를 부른다'를 장점으로 활용해, 네트워크 게임의 플레이어 간 동기화에서부터 리플레이 모드 구현까지(스타크래프트의 리플레이 모드를 생각해 보면 된다. 게임의 시드만 저장하면 AI의 모든 판단을 다 저장할 필요가 없어진다!) 오만 곳에 써먹을 수 있다. 마찬가지로 도박에서 원하는 결과가 나오게 악용할 수 있다. 이런 걸 난수조절이라고 한다.
3. 난수 발생 방식
3.1. 중앙제곱법
<math>X_{n+1} = (X_n)^2</math>의 가운데 a자리
X는 난수 수열, a는 원하는 자릿수(10진법으로), 시드(X0)는 임의의 a자리인 수[4].
Middle-square method. 폰 노이만이 1949년에 고안한 의사난수법. 생성된 품질이 좋지 않기 때문에 그 당시의 컴퓨터면 몰라도 그 이후 시대에는 웬만하면 아래의 선형합동법을 사용한다.[5]
초기 값이 2345이고, 가운데에서 4자리를 선택하여 5개의 난수를 만드는 경우에는 다음과 같다.
X는 난수 수열, a는 원하는 자릿수(10진법으로), 시드(X0)는 임의의 a자리인 수[4].
Middle-square method. 폰 노이만이 1949년에 고안한 의사난수법. 생성된 품질이 좋지 않기 때문에 그 당시의 컴퓨터면 몰라도 그 이후 시대에는 웬만하면 아래의 선형합동법을 사용한다.[5]
초기 값이 2345이고, 가운데에서 4자리를 선택하여 5개의 난수를 만드는 경우에는 다음과 같다.
#include <stdio.h>
#include <math.h>
int main(void)
{
int i;
long num1 = 1000000, num2 = 100, temp;
double k = 2345, temp1, temp2;
for(i=1; i<=5; i++)
{
temp1 = pow(k, 2);
temp = temp1 / num1;
temp2 = temp1 - temp * num1;
temp = temp2 / num2;
printf("%5d\n", temp);
k = temp;
}
return 0;
}
3.2. 선형합동법
<math>X_{n+1} = (a X_n + c) text{mod} m</math>
static UINT32 next = 1;
int __cdecl rand(void)
{
next = next * 1103515245 + 12345;
return (UINT32)(next>>16) & RAND_MAX;
}
void __cdecl srand(unsigned int seed)
{
next = seed;
}
X는 난수 수열. ANSI C 표준은 m = 231, a = 1103515245, c = 12345. 시드는 이 수열에서 X0의 값을 말한다.
__cdecl 은 함수 호출 규약인데, 생략해도 상관없다. 함수에 진입할 때, 나올 때 어떻게 매개변수를 전달하고 처리하는지를 지정하는 컴파일러 키워드이다.
Linear Congruential. 가장 널리 쓰이는 유사난수법.
계산이 매우 빠르기 때문에 초창기부터 컴퓨터에 널리 사용되었으며, 흔히 쓰이는 rand() 함수는 바로 이것이다. 난수분포가 치우쳐 있고, 주기성이 있다는 결점이 있지만 계산속도가 빠르기 때문에 지금도 난수의 품질을 크게 신경쓰지 않는 경우에는 널리 사용된다. 그런데 시드가 같으면 의미도 없기에 보통은 srand(time(0))으로 난수를 초기화한다. 요즘에는 이 과정을 객체 차원에서 수행하는 모듈도 많이 나와 있다.
3.3. 메르센 트위스터
Mersenne-twister. 흔히 mt_rand()라는 이름을 가진다. 메르센 소수라는 것을 이용하며 선형 합동법보다 우수한 난수의 품질과 속도를 인정받아 C++11 부터는 mt19937 이 표준으로 채택되면서 C++11 부터는 아래와 같은 코드로 메르센 트위스터를 이용한 난수생성이 가능하다.
참고로 PHP는 7.1 버전 이전에는 구현이 잘못되어[6] 엉뚱한 난수값을 뱉어냈었다(...).
#include <iostream>
#include <random>
int main(void)
{
std::random_device random; //하드웨어 리소스를 기반으로 난수를 생성
std::mt19937 engine(random()); //메르센 트위스터 방식으로 난수를 생성하겠다는 선언
std::uniform_int_distribution<int> distribution(0, 100); //난수의 범위와 자료형 정의. rand() % 100과는 다르게 100도 나올 수 있음
auto generated = distribution(engine);
std::cout << generated << std::endl;
}
참고로 PHP는 7.1 버전 이전에는 구현이 잘못되어[6] 엉뚱한 난수값을 뱉어냈었다(...).
3.4. XOR 시프트
uint32_t x, y, z, w;
uint32_t xorshift128(void) {
uint32_t t = x;
t ^= t << 11;
t ^= t >> 8;
x = y; y = z; z = w;
w ^= w >> 19;
w ^= t;
return w;
}
Xorshift. XOR과 비트시프트 연산을 사용하는 난수 발생법. 구조상 현대의 컴퓨터에서 연산 속도가 매우 빠르고 품질도 선형합동법보다 좋다. 그렇지만 몇몇 난수 품질 테스트를 통과하지 못하는 경우가 있어 여러 변종이 나와 있다. 아래는 이를 다소 해결한 변종 중 하나인 Xorshift+로 2128 - 1의 주기를 가진다.
#include <stdint.h>
/* The state must be seeded so that it is not everywhere zero. */
uint64_t s[2];
uint64_t xorshift128plus(void) {
uint64_t x = s[0];
uint64_t const y = s[1];
s[0] = y;
x ^= x << 23; // a
s[1] = x ^ y ^ (x >> 17) ^ (y >> 26); // b, c
return s[1] + y;
}
3.5. 진짜 난수
앞선 방법은 좋은 아이디어로 그럴듯한 난수를 만들어냈지만 이들이 만들어내는 난수는 결국 어디까지나 의사난수(Pseudo Random)이다. 그렇지만 컴퓨터가 진짜 난수를 만들 방법이 없는건 아니다. 이곳에서는 True Random Generater라는 특수 하드웨어로 만든 진짜 난수를 얻을 수 있다.[7] 물론, 일반적인 상황에서 그렇다. 아님, 호주국립대학교에서 양자난수를 얻을수 있다. 이 비슷한 것이 가이거 계수기인데, 가이거 계수기를 때리는 우주선 등으로 인해 진짜 난수를 손 쉽게 생성할 수 있다.
Cloudflare는 라바 램프가 나열된 벽을 찍은 화상에서 노이즈를 추출하여 난수를 생성한다.
Cloudflare는 라바 램프가 나열된 벽을 찍은 화상에서 노이즈를 추출하여 난수를 생성한다.
4. 관련 문서
[1] C언어에서도 마찬가지로 함수 rand는 초기 값에 따라 매 실행마다 같은 난수를 생성할 수도 있으므로 현재 시간을 이용하기 위해 함수 time을 사용한다.
time(0)로 사용한다. 참고로 GCC 기준 time(0)은 유닉스 시간을 반환한다. 실행할 때마다 시간이 변하므로 매 실행마다 다른 난수가 생성된다. 비주얼 베이직이나 베이직에서는 randomize timer를 사용하며, 파이썬에서는 필요없다.[2] 베라크립트가 한 예시.[3] 의사난수인 이상 시드값을 알면 모든 결과를 미리 계산할 수 있다.[4] a=6이면 100000 <= X0 <= 999999 의 범위 중 하나를 쓰라는 말[5] 결정적으로, <math>X_k</math>의 값이 0일 때 <math>n > k</math>인 <math>X_n</math>의 모든 값이 0이다(...). 회차마다 난수에 상수를 더해봐도 계속 가다 보면 어느 순간 반복되는 일정한 패턴이 나타난다.[6] 더 경악할 만한 사실은 해당 링크에서 나온 소스 코드 부분은 이 그릇된 구현을 바로잡았었는데 PHP 주 제작진에 의해 이전으로 롤백된 상황(...).[7] 해당 서버에서 사용하는 하드웨어는 주변 노이즈를 기반으로 난수를 생성한다.