프로그래밍을 하다 보면 난수가 필요할 때가 많다. 특히 게임 분야에서 난수는 매우 중요하다. 난수란 정의된 범위에서 무작위로 추출된 임의의 수를 의미한다. 난수는 그 다음에 나올 값을 누구도 확신할 수 없어야 한다. 컴퓨터 프로그래밍에서 난수를 만들 때는 보통 시드(seed)
라는 시작 숫자를 이용하는데, 이 시드값으로 현재 시각을 사용하는 경우가 많다. 즉, 시시각각 변하는 현재 시작을 특정한 알고리즘에 넣어 난수를 만든다.
C
와 C++
언어에서는 이처럼 난수를 생성하는 rand
와 srand
함수가 있다.
rand
함수는 난수 생성 패턴을 한 개로 설정.srand
함수는 난수 생성 패턴을 여러 개로 설정.이 함수들이 정의된 헤더 파일을 보면 #define RAND_MAX 0x7fff
라는 매크로 상수가 선언되어 있다. 이는 난수의 범위가 0 ~ 32,767
까지라는 의미이다. 범위가 생각보다 넓지 않아서 난수가 균등하게 분포되지 않을 수 있다.
이런 문제를 해결하기 위해 C++11
부터는 고품질의 난수 생성기와 분포 클래스를 제공하여 난수의 형식, 범위, 분포와 형태 등을 세세하게 조절할 수 있도록 했다.
다음 코드는 <random>
헤더 파일에 있는 std::mt19937
을 이용하는 예이다. mt19937
은 32bit
버전이고, 64bit
버전인 mt19937_64
도 있다. 다음 코드는 64bit 버전을 이용하여 임의의 수 10개를 생성하는 예이다.
mt19937
: 32bit
mt19937_64
: 64bit
#include <iostream>
#include <random>
using namespace std;
int main()
{
mt19937_64 mt_rand;
for (int i = 0; i < 10; i++) {
cout << mt_rand() << endl;
}
return 0;
}
실행 결과
14514284786278117030
4620546740167642908
13109570281517897720
17462938647148434322
355488278567739596
7469126240319926998
4635995468481642529
418970542659199878
9604170989252516556
6358044926049913402
그런데 이 코드는 난수를 생성할 때에 시드값을 사용하지 않아 실행할 때마다 같은 값이 나올 수 있다. 이번에는 시드값으로 시스템의 현재 시각을 넣어 난수를 생성해보자.
#include <iostream>
#include <random>
#include <chrono>
using namespace std;
int main()
{
// seed 값 사용
auto curTime = chrono::system_clock::now();
auto duration = curTime.time_since_epoch();
auto millis = chrono::duration_cast<chrono::milliseconds>(duration).count();
mt19937_64 mt_rand(millis);
for (int i = 0; i < 10; i++) {
cout << mt_rand() << endl;
}
return 0;
}
실행 결과
13046483459348615973
428865184304725544
4092226827631619104
9798059489380720993
17101403504210358503
18339317656448932595
8073935047207915881
13039217037191916072
785563860698437517
806762417456524090
이처럼 시드값을 이용하면, 실행할 때마다 다른 값을 만들 수 있다. 그런데 이러한 시드값 대신 하드웨어 엔트로피
를 이용하는 방법도 있다.
하드웨어 엔트로피란?
시스템에서 발생하는 무작위성의 정도.
표준 라이브러리에서 제공하는 random_device
클래스를 이용하면 이러한 하드웨어 엔트로피를 이용해 난수를 생성할 수 있다. 하드웨어의 마우스 움직임, 커서 위치, 키보드 입력, 디스크 I/O 등 다양한 외부 요인을 활용하여 엔트로피
를 수집한다.
#include <iostream>
#include <random>
using namespace std;
int main()
{
random_device rng;
for (int i = 0; i < 10; i++) {
auto result = rng();
cout << result << endl;
}
return 0;
}
실행 결과
2599606016
3595906431
4166036206
2567559748
3970587961
3258923598
1014178902
1690990272
2048142794
3489405322
그러나 random_device
는 대체로 mt19937
엔진보다 느리다. 그래서 생성할 난수가 많을 때는 mt19337
엔진을 사용하고, random_device
는 엔진의 시드값을 생성하는 데만 사용하는 것이 좋다.
random_device는 mt19937 엔진보다 느리다!
<random>
에는 random_device
외에도 3가지 난수 엔진을 제공한다. 표준 라이브러리에서 다양한 난수 생성 엔진을 제공한다는 것만 기억해 두고 필요할 때 참고하면 된다.
linear_congruential_engine
mersenne_twister_engine
subtract_with_carry_engine
완벽한 난수 생성기는 예측이 불가능하며 어떠한 패턴도 갖지 않아야 한다. 그러나 컴퓨터에서 사용되는 대부분의 난수 생성기는 사실상 의사 난수(pseudo-random number)
이다.
의사 난수
는 특정한 알고리즘을 기초로 생성되며 초깃값이 같으면 같은 순서로 생성한다. 따라서 초깃값이나 시드값에 의존하므로 예측할 수 있다. 또한, 오랫동안 사용하면 주기적으로 반복되는 패턴이 나타날 수 있다.
이를 보완하려면 더 복잡하고 예측이 어려운 알고리즘을 사용하거나 외부 장치를 활용하여 무작위성을 높이는 방법이 있다. 외부 장치로는 물리적인 현상을 활용하는 하드웨어 난수 발생기가 있다. 이러한 발생기는 주로 양자역학
의 물리적인 현상을 활용하여 완벽한 난수를 생성한다.
정리하면, 컴퓨터에서 완벽한 난수를 만드는 것은 매우 어려우며, 현실에서는 특정한 요구 사항에 맞는 안전하고 예측이 어려운 난수 생성기를 선택하는 것이 중요하다.