진정한 랜덤수 만들기

From IT도서관

Jump to: navigation, search

목차

이 글을 쓰는 이유

안녕하세요. 포프입니다. 제가 결코 뭔가 저술할만한 슈퍼내공을 가지고 있지 않음에도 불구하고 이 글을 쓰는 이유는 예전에 denoil님이 간단한 가위바위보 게임 만들기 강좌를 쓰실 때 저와 나눈 토론을 글로 남겨두기 위해서입니다.

성격상 질질 끄는 지겨운 글은 싫어하니 간단하고도 짧게 글을 써보도록 하겠습니다. 참고로 이 글은 C++를 기준으로 하나 뭐 다른 언어에서도 비슷하겠죠. -_-

rand() 함수의 동작원리

일단 간단히 rand() 함수의 동작원리에 대해 말씀드려야 할 것 같습니다.

rand()함수는 랜덤한 수를 만들지 않습니다. 사실 rand()함수가 하는 일은 특정 알고리듬을 통해 랜덤한 것 처럼 보이는 수들을 발생하는 것 뿐인데요 따라서 srand()함수를 사용하여 기준값(seed값)을 변경하지 않는한 rand() 함수에서 나오는 수들의 순서는 완전히 일치합니다. rand()함수가 어떤 알고리듬을 쓰는지는 저희에게는 그다지 중요하지 않으니 그냥 생략합니다.

진정한 랜덤수를 만들어보자

그렇다면 진정한 랜덤수는 어떻게 만들 수 있을까요? 제가 여기서 논할 것은 두가지입니다. 물론 첫번째 방법은 이미 누구나 다 알고 있는 방법으로 그 방법만 논하려고 했다면 이 글을 쓸 생각도 안했을 것입니다. 제가 정말 말씀드리려고 하는 것은 두번째 방법입니다.

시드값을 바꾼다

현재 시스템 클럭을 시드값으로 넣으면 프로그램을 실행하는 시각에 따라 발생되는 랜덤수의 순서가 달라지게 됩니다. 이것은 denoil님의 간단한 가위바위보 게임 만들기 강좌에 이미 나와있으므로 자세한 설명을 생략하겠습니다. 코드만 간단히 보이면 다음과 같습니다.

srand( time( NULL ) )  ;

main() 함수 앞부분에 한번만 넣어주시면 됩니다. 이 함수를 쓰시려면 time.h 헤더파일을 포함하셔야 합니다.

RAND_MAX 매크로를 이용해 정규화한다

현재 주요 임플리멘테이션에서 rand()함수가 반환하는 값의 범위는 0~32767 입니다. 보통 사람들은 0~9 사이의 값을 얻으려고 할 때 다음과 같은 코드를 사용합니다.

int result = rand() % 10;

역시 왜 이것이 0~9사이의 값을 반환하는지에 대해서는 모두 잘 알고 계시리라 믿으니 생략합니다. 혹시 모르신다면 간단한 가위바위보 게임 만들기를 보시기 바랍니다.

자, 그럼 이게 정말 0~9 사이의 값을 제대로 반환할까요? 라는게 제 질문입니다. 만약 rand()가 0~32767이 아니라 0~9만을 반환하고, 각 수가 나올 확률이 10%로 동일하다면 % 10 연산의 결과가 0~9가 될 확률도 동일합니다. 즉, 각 수가 한번씩 나오겠지요.

그럼 rand()가 0~19를 반환한다면 어떨까요? 역시 각 수가 두번씩 나올테니 진정한 랜덤수가 되겠지요? 0~99여도 마찬가지죠? 이런식으로 가면 0~32759까지도 마찬가지일 것입니다. 각수가 32760번씩 나오겠지요. 자, 그렇다면 문제는 32760~32767 사이의 값입니다. 즉 rand()의 범위가 0~32767이므로 0~32759까지는 모든 수가 동일한 확률로 나오지만 32760은 0을 한번 더 만들어내고 32761은 1을, 32762는 2를 ... 32767은 7을 한번 더 만들어 냅니다. 즉 0~7이 나올 확률이 8~9가 나올 확률보다 아주아주 약간 높다는 것입니다.

뭐 사실 3276 / 32767 = 9.997% 이고 3278 / 32767 = 10.003%이니 실제 0~10 사이의 수를 구하는 데서는 큰 차이가 없다고 할 수 있지만 그보다 큰 범위내의 수를 찾는다면 이보다는 좀더 차이가 나겠지요. 뭐 아님 말고...

뭐 이래 저래서 나온게 바로 다음의 공식입니다.

int result = 범위의 하한 + (int)(범위 * rand() / (RAND_MAX + 1.0));

즉, 저희의 예에서는 범위의 하한이 0이고 범위 = 상한 - 하한 + 1 이니(산수책 보세요 -_-) 범위 = 9 - 0 + 1 = 10이 되겠지요. 따라서 저희의 경우 아래와 같이 코드를 쓸 수 있습니다.

int result = 0 + (int)(10 * rand() / (RAND_MAX + 1.0));

이 코드의 개념은 간단합니다.

  1. rand()로 0 - 32767 사이의 수를 만든다.
  2. 그 수를 [0.0, 1.0)으로 정규화(normalization) 한다.
  3. 정규화된 값을 우리가 구하려는 범위값과 곱한다.
  4. 소수점을 버린다.

일단 여기서 주의하셔야 할 것은 1.0은 포함이 안된다는 것입니다. 따라서 10과 최고큰 랜덤값을 곱하더라도 10보다 약간은 작은 값이 나오겠죠? 그것의 소수점을 버리면 결국 9가 됩니다. 따라서 10 * rand()는 0부터 10보다 약간 작은 값을 만들어 냅니다. 정규화의 개념은 게임 그래픽쪽을 조금이나마 건드려보신 분이라면 잘 알고 계실테니 굳이 자세히 설명할 필요는 없겠죠? 뭐 잘 이해 안되시더라도 그냥 저 위의 코드를 가져다 쓰시기만 하면 됩니다. 언젠가 이해될 날이 있습니다.

마지막으로 32767이라는 수를 직접 써넣지 않고 RAND_MAX를 사용하는 이유를 설명드리겠습니다. 일단 C/C++에서 short이나 char, 또는 long등의 스펙은 정해져있습니다. char는 8비트, short은 16비트 long은 32비트 등입니다. 하지만 유독 int만은 스펙이 정해져 있지 않습니다. 즉, 컴파일러에 따라 또는 플랫폼에 따라 int가 8비트가 될수도 있고 64비트가 될수도 있다는 것입니다. 보통은 시스템의 아키텍처에서 사용하는 레지스트리의 크기가 int의 크기가 되는것이 보통입니다. 현재 저희가 주로 사용하는 PC는 32비트 기계이므로 int도 보통 32비트입니다.

아까 rand값의 반환값이 int라고 말했던가요? 뭐 코드에 int가 반환된다고 보이는군요. 하지만 rand()는 음수는 반환하지 않으니 결국 0부터 int의 최대값까지를 반환하는 것입니다. 현재 64비트 컴퓨터도 나와있고 임베디드 시스템들도 널리 쓰이는 세상이니 int의 값이 어떤것 하나다! 라고 말하기 힘든것이지요. 그래서 RAND_MAX가 있는 겁니다. RAND_MAX는 아마도 stdlib.h 안에 정의되어 있을것이며, 컴파일러 제조사들이 미리 잘 알아서 만들어 놓은 값입니다. 즉, int가 8비트인 시스템을 겨냥해 나온 컴파일러는 RAND_MAX값으로 127을 가지고 있을 것이고, 32비트용은 32767, 64비트용은 엄청큰 수 -_-... 를 가지고 있을 것입니다. 따라서 어느 플랫폼에서도 돌아가는 rand코드를 작성하려면 32767을 직접 쳐넣는 것보다 RAND_MAX를 쓰는 것이 좋을 것입니다.

마치며

역시 생각보다 글이 조금 길어졌습니다. 대충 이런 저런 소리를 하다보니 그렇게 되는군요. 뭐든간에 여러분이 기억해야 할 것은 다음의 코드 두줄 뿐입니다.

srand( time( NULL ) )  ; int result = 범위의 하한 + (int)(범위 * rand() / (RAND_MAX + 1.0));

그 쉬운걸 가지고 괜히 길게만 썼네요. 뭐 앞으로 또 글 쓸일이 있을진 모르겠지만 그렇게 된다면 그 때 뵙죠. 포프였습니다.

저술정보

  • 저자: Pope Kim
  • 저술일: 2005년 8월 27일