Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

Ruby에서 난수 생성하기: rand부터 SecureRandom까지 완벽 가이드

난수는 게임 개발, 암호화, 시뮬레이션 구축 등 다양한 분야에서 활용됩니다. 그런데 흥미롭게도 컴퓨터는 순수한 연산만으로 진정한 의미의 난수를 만들어낼 수 없습니다. 결정론적(deterministic) 장치에서 참난수(true random number)를 생성하는 것은 근본적으로 불가능하며, 우리가 기대할 수 있는 최선은 겉보기에 무작위처럼 보이는 수열, 즉 유사난수(pseudorandom number)입니다.

이 글에서는 Ruby에서 난수를 생성하는 다양한 방법을 살펴보겠습니다.

Kernel#rand로 난수 생성하기

가장 먼저 rand 메서드를 사용해 난수를 생성해 보겠습니다. 인자 없이 호출하면 0.0 이상 1.0 미만의 부동소수점 값을 반환합니다.

rand()
> 0.7308136972953823

정수를 얻고 싶다면 함수에 정수를 전달하면 됩니다. 이때 함수는 0 이상, 전달한 정수 미만의 임의의 정수를 반환합니다. 아래 코드는 실행할 때마다 0부터 7 사이의 숫자를 출력합니다.

rand(8)
> 5

특정 범위 내의 난수가 필요하다면 Range 객체를 rand에 전달하세요.

아래 예제는 포함(inclusive) 범위를 사용해 하한(1)부터 상한(10)까지, 상한을 포함한 난수를 생성합니다.

rand(1..10)
> 6

다음 예제는 배제(exclusive) 범위를 사용해 하한부터 상한 직전까지, 즉 상한을 제외한 난수를 생성합니다.

rand(1...10)
> 9

범위는 부동소수점 값 사이로 지정할 수도 있습니다.

rand(1.5..3.0)
> 1.7494305393711571

음수 범위 역시 문제없이 사용할 수 있습니다.

rand(-5..-1)
> -5

반면 음수 하나를 단독으로 전달하면 아래와 같이 의외의 결과가 나올 수 있습니다.

rand(-100)
> 94
 
rand(-0.5)
> 0.7692627344737486

그 이유는 rand에 인자 n을 전달하면 0부터 n.to_i.abs 미만 사이의 난수를 반환하기 때문입니다. 위 예제에서 (-100).to_i.abs100, (-0.5).to_i.abs0이므로 각각 해당 범위의 결과가 나옵니다.

rand(0)을 호출하는 것은 rand()를 호출하는 것과 같으며, 0.0 이상 1.0 미만의 난수를 얻게 됩니다.

Kernel#srand로 재현 가능한 시퀀스 만들기

다음 방법으로 넘어가기 전에 먼저 srand 함수를 살펴보겠습니다.

Kernel#srandKernel#rand의 시드(seed)를 설정합니다. 이를 활용하면 프로그램을 여러 번 실행하더라도 동일한 난수 시퀀스를 반복해서 생성할 수 있습니다.

이것이 무슨 의미인지 이해하려면, 난수가 어떻게 생성되는지 먼저 알아야 합니다.

시드로부터 "랜덤" 숫자 생성하기

앞서 언급했듯이 컴퓨터는 순수한 연산만으로 참난수를 만들지 못하고, 대신 무작위로 보이는 수열을 생성합니다. 이를 위해 컴퓨터는 시드(seed) 숫자에서 출발해 특정 알고리즘을 거친 뒤, 겉보기에 무작위인 출력값을 내놓습니다.

시드 숫자는 타임스탬프, 프로그램의 프로세스 ID 등 여러 요소를 조합해 생성됩니다. 난수 생성 요청마다 이러한 요소들이 달라지므로 시드 값도 항상 달라지고, 그 결과 서로 다른 수열과 난수가 만들어집니다. 반대로 같은 시드로 알고리즘을 실행하면 매번 동일한 수열을 얻게 되는데, 바로 이것이 Kernel#srand가 해주는 일입니다.

srand는 주로 테스트에서 사용됩니다. 무작위성을 다루는 앱 코드를 테스트할 때, 값은 무작위지만 충분히 예측 가능하므로 테스트에 유용하며, 버그를 고립시키거나 재현하는 데도 도움이 됩니다.

아래에서는 srand로 시드를 설정한 뒤, rand를 호출해 개별 난수 몇 개와 난수 시퀀스 두 개를 생성합니다.

srand(777)
 
rand()
> 0.152663734901322
 
rand()
> 0.3023566097075212
 
10.times.map { rand(10) }
> [7, 1, 7, 4, 7, 9, 8, 7, 2, 0]
 
10.times.map { rand(10) }
> [1, 2, 4, 5, 7, 1, 7, 2, 2, 7]

같은 시드로 srand를 다시 실행하고 앞서와 동일한 호출을 하면, 동일한 난수가 반환되는 것을 확인할 수 있습니다.

srand(777)
 
rand()
> 0.152663734901322
 
rand()
> 0.3023566097075212
 
10.times.map { rand(10) }
> [7, 1, 7, 4, 7, 9, 8, 7, 2, 0]
 
10.times.map { rand(10) }
> [1, 2, 4, 5, 7, 1, 7, 2, 2, 7]

Random 클래스로 난수 생성하기

Random 클래스를 통해서도 난수를 생성할 수 있습니다.

클래스 메서드 randKernel#rand의 기본 기능을 제공하면서 부동소수점 값을 더 안정적으로 처리합니다.

Random.rand(1...10)
> 5

Kernel#rand와 달리, Random.rand에 음수나 0을 인자로 전달하면 ArgumentError가 발생한다는 점에 유의하세요.

정규 분포 기반 난수 생성하기

실제 세계의 많은 현상은 정규 분포(Normal Distribution)를 따르는 경향이 있습니다. 어떤 값의 범위가 있을 때 모든 값이 균등하게 분포하는 경우는 드물고, 대부분의 데이터는 좁은 범위 안에 몰려 있으며 넓은 범위에는 상대적으로 적은 비율의 데이터가 속합니다. 성인 남성의 키를 예로 들어 보겠습니다. 기록된 최단 신장은 54.6cm(21.5인치), 최장 신장은 267cm(8피트 9인치)입니다. 인구 집단의 남성 키를 시뮬레이션하는 데이터를 생성한다면 이 한계값으로 rand를 사용하고 싶지 않을 겁니다. 267cm 남성이 나올 확률과 6피트(약 183cm) 남성이 나올 확률이 같아지면 안 되기 때문입니다. 후자가 훨씬 흔하니까요.

정규 분포를 따르는 다른 예로는 다음과 같은 것들이 있습니다.

  • 측정 오차
  • 혈압
  • 시험 점수
  • 성인 남녀의 체중

이런 용도에 더 적합한 난수를 생성하려면 rubystats 젬(gem)을 사용할 수 있습니다.

$ gem install rubystats
require 'rubystats'
 
adult_male_height = Rubystats::NormalDistribution.new(178, 10)
sample = 50.times.map { adult_male_height.rng.round(1) }
 
> [183.2, 169.5, 189.7, 171.9, 176.0, 179.3, 189.3, 175.3, 188.3, 190.0, 185.5, 182.8, 187.2, 191.6, 185.4, 178.4, 187.1, 183.3, 189.6, 179.7, 172.7, 174.4, 153.8, 197.4, 176.0, 174.6, 181.1, 182.0, 204.7, 185.2, 175.9, 167.7, 160.6, 170.5, 169.3, 160.6, 165.6, 166.4, 182.6, 179.7, 183.1, 171.9, 185.4, 175.4, 179.7, 176.9, 160.6, 173.8, 181.9, 190.2]

위 코드에서는 남성 평균 신장(178cm)과 표준편차 10cm를 NormalDistribution.new에 전달한 뒤, 이 정규 분포에 속하는 값 50개를 생성했습니다. 관련 수학적 원리가 궁금하다면 별도의 참고 자료를 찾아보시길 권합니다.

마무리

지금까지 Ruby에서 '무작위' 숫자를 만드는 여러 가지 방법, 즉 rand, srand, Random, Rubystats를 살펴봤습니다. 또한 '난수'가 어떻게 만들어지는지 간략히 짚어보며, 결정론적 장치가 진짜 난수를 만들 수 없는 이유도 함께 확인했습니다.

다만 소개한 메서드들이 모든 무작위성 요구 사항에 적합한 것은 아니라는 점에 유의하세요. 생성된 정수나 부동소수점 숫자는 게임의 확률 요소나 시뮬레이션에는 적합하지만, 보안이 필요한 상황, 예컨대 비밀번호 재설정 토큰을 생성할 때는 SecureRandom을 사용하는 것이 좋습니다. SecureRandom을 사용하면 단순 숫자보다 훨씬 크랙하기 어려운 16진수, base64, 바이너리, UUID 문자열을 생성할 수 있습니다.

이 글이 도움이 되었기를 바랍니다. 내용에 대한 의견이나 질문이 있다면 @AppSignal으로 연락해 주세요. 다뤄줬으면 하는 주제 요청도 언제든 환영합니다.

이 글은 2018년 8월 1일자로 SecureRandom 관련 내용을 추가하여 업데이트되었습니다.