ludlet

컴퓨터가 뽑는 랜덤은 정말 랜덤일까

Is a computer's random really random?

룰렛 앱이 같은 사람을 세 번 연달아 고르면 "이거 조작된 거 아니야?"라는 말이 꼭 나와요. 컴퓨터는 정해진 명령만 그대로 실행하는 기계인데, 그런 기계가 어떻게 예측할 수 없는 값을 만들어 낼까요? 답부터 말하면, 대부분의 컴퓨터 난수는 진짜 우연이 아니라 아주 정교한 계산의 결과예요. 그래도 제대로 만들고 제대로 쓰면 사람이 뽑는 제비보다 공정할 수 있어요.

계산으로 만드는 난수, 의사난수

컴퓨터가 흔히 쓰는 난수 생성기는 의사난수 생성기(PRNG)예요. 처음 값 하나(시드)를 넣으면 정해진 수식에 따라 다음 값, 그다음 값을 줄줄이 계산해 내요. 결과는 겉보기에 무작위처럼 고르게 흩어지지만, 시드와 수식을 알면 다음에 나올 값을 똑같이 다시 계산할 수 있어요. 그래서 "의사(가짜)"라는 말이 붙어요.

자바스크립트의 Math.random()이 대표적인 예예요. MDN 문서에 따르면 0 이상 1 미만의 값을 대략 고르게 돌려주고, 시드는 브라우저가 정하며 사용자가 고르거나 되돌릴 수 없어요. 다만 같은 문서에 Math.random()은 암호학적으로 안전한 난수를 주지 않으니 보안과 관련된 곳에는 쓰지 말라고 적혀 있어요. 게임 속 장식이나 그래픽 효과에는 충분하지만, 결과를 누가 미리 맞히면 곤란한 곳에는 맞지 않는다는 뜻이에요.

예측하기 어렵게 만든 난수, CSPRNG

비밀번호, 암호 키, 추첨처럼 결과를 예측당하면 안 되는 곳에는 암호학적으로 안전한 의사난수 생성기(CSPRNG)를 써요. 역시 계산으로 만들지만, 운영체제가 모은 예측하기 어려운 값(엔트로피)으로 시드를 정하고, 지금까지 나온 값을 아무리 많이 봐도 다음 값을 알아내기 어렵도록 설계돼요.

브라우저에서는 Web Crypto API의 crypto.getRandomValues()가 이 역할을 해요. MDN은 성능 때문에 진짜 하드웨어 난수를 바로 쓰지는 않지만, 충분한 엔트로피로 시드를 정한 생성기를 쓰고 그 알고리즘은 암호 용도에 알맞다고 설명해요. W3C 표준도 브라우저가 가능한 한 좋은 엔트로피를 쓰도록 권하고 있어요.

구분Math.random()crypto.getRandomValues()
종류의사난수(PRNG)암호학적 의사난수(CSPRNG)
결과 모양0 이상 1 미만의 소수정수 배열(예: 32비트 정수)
다음 값 예측보안용으로는 보장하지 않음예측하기 어렵도록 설계
알맞은 곳그래픽, 장식, 연습용 게임암호, 추첨, 공정한 뽑기
두 함수 모두 계산으로 만든 난수예요. 차이는 "얼마나 예측하기 어렵게 만들었는가"에 있어요.

좋은 난수도 잘못 쓰면 치우쳐요: 모듈로 편향

생성기가 아무리 좋아도, 받은 값을 원하는 범위로 바꾸는 과정에서 공정함이 깨질 수 있어요. 가장 흔한 실수가 나머지 연산(%)이에요.

아주 작은 예로 생각해 볼게요. 0부터 7까지 여덟 가지 값이 똑같은 확률로 나오는 생성기가 있고, 여기서 세 사람 중 한 명(0, 1, 2)을 고르려고 해요. 값을 3으로 나눈 나머지를 쓰면 이렇게 돼요.

나머지이 결과가 되는 값확률
00, 3, 63/8 (37.5%)
11, 4, 73/8 (37.5%)
22, 52/8 (25%)
8은 3으로 나누어떨어지지 않아서, 남는 두 값(6, 7)이 0과 1 쪽에 붙어요. 이것이 모듈로 편향이에요.

세 번째 사람만 덜 뽑히는 셈이에요. 해결 방법은 간단해요. 나누어떨어지는 범위(0~5)를 넘는 값, 즉 6과 7이 나오면 버리고 다시 뽑는 거예요. 이것을 거부 샘플링이라고 해요. 그러면 0~5 여섯 값만 남고, 각 결과가 두 값씩 똑같이 나눠 가져요.

실제 생성기는 32비트 정수처럼 범위가 훨씬 넓어서, 6명 중 한 명을 고를 때의 편향은 아주 작아요. 하지만 고를 범위가 커질수록 편향도 커져요. 골라줘의 테스트에는 범위가 2³²에 가까울 때를 일부러 확인하는 항목이 있는데, 그 경우 나머지만 쓰면 앞쪽 1/3 구간이 두 배로 자주 나온다고 적혀 있어요. 작은 차이라도 수백만 번 반복하는 뽑기에서는 분명한 차이가 돼요.

섞기도 방법이 중요해요

목록을 무작위로 섞을 때도 비슷한 함정이 있어요. 아무렇게나 자리를 바꾸면 특정 순서가 더 자주 나올 수 있어요. 널리 쓰는 정답은 피셔–예이츠 셔플이에요. 맨 뒤 자리부터 시작해서, 아직 정해지지 않은 자리 중 하나를 고르게 골라 바꾸는 일을 앞으로 한 칸씩 반복해요. 이렇게 하면 n개를 섞을 때 나올 수 있는 n!가지 순서가 모두 같은 확률로 나와요. 단, 자리를 고를 때 쓰는 난수 자체에 모듈로 편향이 없어야 해요.

골라줘는 이렇게 뽑아요

ludlet의 뽑기 도구 골라줘는 위의 원칙을 그대로 따라요. 코드는 페이지와 함께 내려받는 core.js에 모두 들어 있어요.

  • 난수는 전부 crypto.getRandomValues: Math.random은 쓰지 않아요. 매번 부르지 않도록 32비트 정수 256개를 한 번에 받아 두고 하나씩 써요.
  • 범위 바꾸기는 거부 샘플링: 2³²을 n으로 나눈 나머지만큼의 끝값이 나오면 버리고 다시 뽑아서, 0부터 n−1까지가 정확히 같은 확률이 돼요.
  • 섞기와 중복 없는 뽑기는 피셔–예이츠: 이름 섞기, 팀 나누기, 로또 번호처럼 중복 없이 여러 개를 고를 때 모두 이 방식을 써요.
  • 테스트로 확인: 각 기능을 수십만~백만 번 돌려 카이제곱 검정으로 고르게 나오는지 보고, 사이트 스크립트에 Math.random이 없는지도 검사해요.

그래도 같은 사람이 연달아 걸릴 수 있어요. 매번 새로 뽑기 때문에 앞의 결과가 다음 결과에 영향을 주지 않거든요. 다섯 명 중 한 명을 뽑는다면, 방금 걸린 사람이 다음 판에 또 걸릴 확률은 5분의 1이에요. 여러 번 하다 보면 연달아 걸리는 일이 생각보다 자주 생겨요. 연달아 나오는 것 자체는 조작의 증거가 아니라 무작위의 자연스러운 모습이에요.

정리

컴퓨터 난수는 대부분 계산으로 만든 의사난수예요. 공정한 뽑기에는 예측하기 어려운 CSPRNG를 쓰고, 범위를 바꿀 때 모듈로 편향을 없애고, 섞을 때는 피셔–예이츠를 쓰면 돼요. 이 세 가지를 지키면 결과를 누구도 미리 알 수 없고, 모든 결과가 똑같은 기회를 가져요.

참고 자료

골라줘에서 공정하게 뽑기

숫자 뽑기 바로 가기

When a roulette app picks the same person three times in a row, someone always asks whether it is rigged. A computer only follows instructions, so how can it produce something unpredictable? The short answer: most computer random numbers are not true chance but the output of very careful calculation. Built and used properly, though, they can be fairer than drawing lots by hand.

Random numbers by calculation: pseudo-random

Most random number generators in computers are pseudo-random number generators (PRNGs). You give them a starting value, the seed, and a fixed formula produces the next value, then the next. The output looks evenly scattered, but anyone who knows the seed and the formula can compute the same sequence again. That is why it is called pseudo-random.

JavaScript's Math.random() is a typical example. According to MDN it returns a value from 0 up to but not including 1 with an approximately uniform distribution, and the browser picks the seed, which the user cannot choose or reset. The same page warns that Math.random() does not provide cryptographically secure random numbers and should not be used for anything related to security. It is fine for visual effects or decoration, but not where someone predicting the result would be a problem.

Built to be unpredictable: CSPRNGs

Passwords, encryption keys and draws need results nobody can predict, so they use a cryptographically secure pseudo-random number generator (CSPRNG). It is still a calculation, but it is seeded with hard-to-predict values (entropy) gathered by the operating system, and it is designed so that seeing any number of past outputs does not reveal the next one.

In the browser, the Web Crypto API's crypto.getRandomValues() plays this role. MDN explains that for performance it does not use a true hardware random source directly, but a generator seeded with enough entropy whose algorithm is suitable for cryptographic purposes. The W3C specification urges browsers to provide the best entropy they can.

Math.random()crypto.getRandomValues()
KindPseudo-random (PRNG)Cryptographically secure (CSPRNG)
OutputA decimal from 0 to just under 1An array of integers (e.g. 32-bit)
Predicting the next valueNot guaranteed hardDesigned to be hard
Good forGraphics, decoration, practice gamesCryptography, draws, fair picks
Both are calculated. The difference is how hard the next value is to predict.

Good randomness used badly: modulo bias

Even with a great generator, fairness can break when you squeeze its output into the range you want. The most common mistake is the remainder operator (%).

Take a tiny example. A generator gives eight values, 0 to 7, each equally likely, and you want to pick one of three people (0, 1, 2). Taking the remainder after dividing by 3 gives this:

RemainderValues that give itProbability
00, 3, 63/8 (37.5%)
11, 4, 73/8 (37.5%)
22, 52/8 (25%)
8 is not divisible by 3, so the two leftover values (6 and 7) land on 0 and 1. That is modulo bias.

The third person is picked less often. The fix is simple: if you get a value above the largest evenly divisible range (here 6 or 7), throw it away and draw again. This is called rejection sampling. Only 0 to 5 remain, and each result gets exactly two of them.

Real generators have a much wider range, such as 32-bit integers, so the bias when picking one of six people is tiny. But it grows as the range you pick from grows. The Pick tool's tests deliberately check a range close to 2³², and note that with a plain remainder the first third of that range would come up twice as often. Small differences become clear ones over millions of draws.

Shuffling needs the right method too

Shuffling a list has a similar trap: swapping positions carelessly can make some orders more likely than others. The standard answer is the Fisher–Yates shuffle. Starting from the last position, pick one of the positions not yet fixed uniformly at random and swap, then move one step forward and repeat. All n! possible orders of n items come out equally likely, as long as the random index itself has no modulo bias.

How the Pick tool does it

ludlet's picking tool, Pick (골라줘), follows these rules. All of its code is in core.js, which is downloaded with the page.

  • All randomness from crypto.getRandomValues: it never uses Math.random. It fetches 256 32-bit integers at a time and uses them one by one.
  • Range conversion by rejection sampling: values in the leftover tail (2³² mod n of them) are discarded and redrawn, so 0 to n−1 are exactly equally likely.
  • Fisher–Yates for shuffling and picking without repeats: shuffling names, splitting teams and lotto numbers all use it.
  • Checked by tests: each feature is run hundreds of thousands to a million times and checked with a chi-squared test, and a test confirms the site scripts contain no Math.random.

The same person can still come up twice in a row. Every draw is fresh, so earlier results do not affect the next one. When picking one of five people, the person just picked has a 1 in 5 chance of being picked again next round, so over many rounds streaks happen more often than you might expect. A streak is not evidence of rigging; it is what randomness looks like.

Summary

Most computer random numbers are calculated pseudo-random numbers. For fair picking, use a hard-to-predict CSPRNG, remove modulo bias when converting ranges, and shuffle with Fisher–Yates. Do those three things and nobody can know the result in advance, and every outcome gets the same chance.

Sources

Pick fairly with Pick

Go to the number picker