앞 글에서 보았듯, 현실의 흥미로운 시뮬레이션은 대부분 확률적(stochastic)이다. 도착 간격, 서비스 시간, 고장 발생 — 이 무작위 입력들이 모두 어딘가에서 “무작위 숫자”로 만들어져야 한다. 그런데 컴퓨터는 본질적으로 결정론적인 기계다. 같은 명령에 항상 같은 결과를 내는 기계가 어떻게 “무작위”를 만들어 낼까?
이 글은 모든 확률적 시뮬레이션의 가장 밑바닥, 난수 생성을 다룬다. 정확히는 구간의 균일난수(uniform random number)를 만드는 문제다. 이것만 잘 만들면, 다음 글에서 보듯 어떤 분포의 난수든 여기서 변환해 낼 수 있다.
의사난수 (pseudo-random)
컴퓨터가 만드는 난수는 사실 진짜 무작위가 아니라 의사난수(pseudo-random number)다. 어떤 초기값(seed)에서 출발해, 정해진 점화식으로 다음 수를 계산해 나간다. 즉 수열은 완전히 결정론적이지만, 겉보기에 무작위처럼 보이도록(통계적으로 무작위와 구분되지 않도록) 설계한다.
이것은 단점이 아니라 오히려 장점이다. 같은 seed를 주면 항상 같은 수열이 나오므로 — 즉 재현 가능(reproducible) 하므로 — 디버깅과 실험 비교가 가능해진다. 진짜 물리적 난수였다면 “어제 그 버그”를 다시 만들어 낼 수 없다.
좋은 의사난수 생성기(pseudo-random number generator, PRNG)가 갖춰야 할 성질은 다음과 같다.
- 균일성(uniformity): 생성된 수들이 에 고르게 퍼져야 한다. 특정 구간에 몰리면 안 된다.
- 독립성(independence): 앞 수로 다음 수를 예측할 수 없어야 한다. 자기상관이 없어야 한다.
- 긴 주기(long period): 점화식은 유한 상태를 가지므로 언젠가 수열이 반복된다. 이 주기가 시뮬레이션에서 뽑을 난수 개수보다 압도적으로 길어야 한다.
- 재현성·이식성·속도: 같은 seed로 같은 결과, 환경이 달라도 동일, 그리고 빨라야 한다.
선형합동법 (LCG): 가장 단순한 예
가장 고전적이고 이해하기 쉬운 PRNG가 선형합동법(Linear Congruential Generator, LCG)이다. 다음 점화식으로 정수열을 만든다.
여기서 (multiplier), (increment), (modulus)은 정해진 상수이고 이 seed다. 이렇게 얻은 정수 을 으로 나누면 의 균일난수 이 된다.
예를 들어 이면 수열은 처럼 흘러간다. 매우 빠르고 구현이 단순하다는 게 장점이다.
문제는 품질이다. LCG는 이 곧 최대 주기라 주기가 짧아지기 쉽고, 무엇보다 생성된 점들을 고차원 공간에 찍어 보면 평행한 초평면(격자) 위에만 놓이는 구조적 결함이 있다(이를 발견자 이름을 따 Marsaglia의 정리라 한다). 이 규칙성 때문에 LCG는 진지한 시뮬레이션에는 부적합하다. 교육용·예시용으로만 생각하는 것이 좋다.
현대적 생성기
오늘날 실무에서는 LCG보다 훨씬 정교한 생성기를 쓴다.
- Mersenne Twister (MT19937): 주기가 로 천문학적이고 고차원 균일성이 좋아 오랫동안 사실상 표준이었다. (단, 암호학적으로 안전하지는 않다.)
- PCG, xorshift 계열: 더 작은 상태로 비슷하거나 더 나은 통계적 품질과 속도를 낸다. NumPy는 1.17부터 기본 생성기를 Mersenne Twister에서 PCG64로 바꿨다.
핵심 메시지는, 직접 PRNG를 구현하려 하지 말고 검증된 라이브러리를 쓰라는 것이다. NumPy의 numpy.random.default_rng()가 좋은 기본값이다. 우리가 원리를 아는 이유는 구현하기 위해서가 아니라, 언제 무엇이 잘못될 수 있는지를 알고 seed를 올바르게 다루기 위해서다.
난수의 품질을 어떻게 검정하나
“이 생성기가 정말 무작위처럼 보이는가”는 통계적 가설검정으로 따진다. 두 종류의 성질을 본다.
- 균일성 검정: 수들이 에 고르게 퍼졌는지. 구간을 나눠 관측 빈도와 기대 빈도를 비교하는 카이제곱 검정, 누적분포를 비교하는 Kolmogorov–Smirnov(KS) 검정 등을 쓴다.
- 독립성 검정: 연속한 수들 사이에 패턴이 없는지. 자기상관(autocorrelation), 증가·감소 구간의 길이를 보는 runs test, 포커 검정 등이 있다.
실무에서는 이런 개별 검정을 묶은 검정 배터리 — Diehard, 그리고 더 엄격한 TestU01(L’Ecuyer & Simard) — 를 통과하는지로 생성기의 품질을 평가한다.
seed와 재현성
마지막으로 강조할 실무 포인트는 seed 관리다. 시뮬레이션을 시작할 때 seed를 명시적으로 고정하면, 같은 결과를 언제든 재현할 수 있다. 이것은 세 가지 이유로 중요하다.
- 디버깅: 문제가 된 실행을 그대로 다시 돌려볼 수 있다.
- 공정한 비교: 두 시나리오(예: 서버 2대 vs 3대)를 같은 난수열로 돌리면, 결과 차이가 무작위 잡음이 아니라 시나리오 차이에서 왔다고 더 자신 있게 말할 수 있다. 이 아이디어를 체계화한 것이 분산 감소 기법인 common random numbers로, 시리즈 4부에서 다룬다.
- 여러 독립 실행: 반대로 output analysis에서 신뢰구간을 얻으려면 서로 다른 seed로 독립 반복을 돌려야 한다. 이때 수열들이 서로 겹치지 않도록 seed를 잘 분리하는 것이 중요하다.
주의: 시뮬레이션용 PRNG와 암호학적 난수(cryptographically secure RNG)는 목적이 다르다. 전자는 통계적 품질과 속도가, 후자는 예측 불가능성(다음 수를 추측할 수 없음)이 핵심이다. 보안이 필요한 곳에 Mersenne Twister를 쓰면 안 된다.
정리
- 컴퓨터의 난수는 seed에서 점화식으로 생성되는 의사난수다. 결정론적이지만 통계적으로 무작위처럼 보이도록 설계하며, 덕분에 재현 가능하다.
- 좋은 PRNG는 균일성·독립성·긴 주기·속도·재현성을 갖춘다. LCG는 이해하기 쉽지만 격자 구조 결함으로 실무 부적합이고, 오늘날은 Mersenne Twister·PCG 등을 쓴다.
- 품질은 카이제곱·KS·독립성 검정과 TestU01 같은 배터리로 검증한다. 직접 구현하기보다 검증된 라이브러리를 쓰는 것이 옳다.
- seed 관리는 재현성·공정한 비교·독립 반복의 토대다.
다음 글에서는 이렇게 얻은 균일난수를 exponential·normal 같은 원하는 확률분포로 변환하는 방법을 다룬다.