Go

Go 언어의 난수, 왜 예측 가능할까요? (math/rand vs crypto/rand 깊이 파헤치기)

드리프트2 2025. 3. 24. 23:03

Go 언어의 난수, 왜 예측 가능할까요? (math/rand vs crypto/rand 깊이 파헤치기)

 

안녕하세요!

 

오늘은 컴퓨터 과학에서 정말 중요한 역할을 하는 '난수'에 대해 이야기해볼까 하는데요, 특히 Go 언어에서의 난수 생성에 초점을 맞춰보겠습니다.

1. 들어가며

난수는 컴퓨터 과학 분야에서 널리 사용되는데요, 암호화부터 시뮬레이션, 게임에 이르기까지 그 활용 범위가 정말 다양합니다.

난수는 크게 두 가지 유형으로 분류할 수 있습니다.

바로 진짜 난수(True Random Numbers)의사 난수(Pseudorandom Numbers)입니다.

2. 진짜 난수 (True Random Numbers)

진짜 난수는 물리적인 현상을 이용하여 생성되는데요.

동전 던지기, 주사위 굴리기, 회전판 돌리기, 전자적 노이즈, 핵분열 등이 그 예입니다.

이런 방법을 기반으로 하는 난수 생성기를 물리적 난수 생성기라고 부릅니다.

정말 예측 불가능한 무작위성을 제공하는 방식입니다.

3. 의사 난수 (Pseudorandom Numbers)

의사 난수(Pseudorandomness)는 겉보기에는 무작위처럼 보이지만 실제로는 그렇지 않은 과정을 의미합니다.

예를 들어, 의사 난수는 결정론적 알고리즘을 사용하여 계산되는데요, 그 결과로 생성되는 시퀀스가 마치 무작위처럼 보이게 됩니다.

의사 난수를 계산하는 데 사용되는 함수를 난수 함수라고 부르며, 난수 함수를 사용하여 난수를 생성하는 알고리즘을 난수 생성기라고 합니다.

일부 난수 함수는 주기성을 가지는데요.

비주기적 함수가 일반적으로 더 좋은 난수 품질을 제공하지만, 주기적 난수 함수가 성능 면에서는 더 빠른 경우가 많습니다.

어떤 주기 함수는 계수를 조정할 수 있어서 매우 긴 주기를 갖도록 만들 수 있는데요, 이렇게 하면 거의 비주기적 함수만큼이나 효과적으로 사용할 수 있습니다.

이제 Go 언어에서의 의사 난수 구현을 한번 자세히 살펴볼까요?

4. 초기 Go 버전에서의 난수

// Go 1.18 버전 기준 코드입니다.
import (
    "fmt"
    "math/rand"
)

func main() {
    fmt.Println(rand.Intn(100))
}

 

이 코드를 실행하면, 몇 번을 실행하든 결과가 항상 똑같이 나오는데요.

왜 그럴까요?

당황하지 마시고, 구현 내용을 함께 살펴보겠습니다.

소스 코드를 들여다보면, Go의 math/rand 라이브러리는 시드(seed) 값으로 1을 사용하는 기본 소스(source)를 통해 난수를 생성하는 것을 알 수 있습니다.

앞서 언급했듯이, 의사 난수는 결정론적 알고리즘으로 생성되기 때문에 진정으로 무작위적이지 않습니다.

만약 NewSource 함수에서 사용되는 시드 값이 변경되지 않으면, 프로그램을 다시 시작할 때마다 생성되는 난수 시퀀스는 항상 동일하게 됩니다.

아래는 Go 1.18 버전의 rand.go 소스 코드 일부입니다.

// rand.go (Go 1.18) 소스 코드 일부입니다.
// ...

/*
 * 최상위 편의 함수들입니다.
 */

// globalRand는 잠긴 소스(lockedSource)를 사용하는 새로운 난수 생성기 인스턴스입니다.
// 이 소스는 시드 값 1로 초기화된 rngSource를 사용합니다.
var globalRand = New(&lockedSource{src: NewSource(1).(*rngSource)})

// ...

 

매번 실행할 때마다 다른 시드 값을 갖도록 하려면, 타임스탬프를 시드로 사용할 수 있습니다.

이렇게 하면 프로그램을 실행할 때마다 난수 시퀀스가 달라지게 됩니다.

// Go 1.18 버전 기준 코드입니다.
package main

import (
    "fmt"
    "math/rand"
    "time"
)

func main() {
    // 현재 시간을 나노초 단위로 변환하여 시드로 사용합니다.
    rand.Seed(time.Now().UnixNano())
    fmt.Println(rand.Intn(100)) // 이제 프로그램을 실행할 때마다 다른 결과가 나옵니다.
}

 

다행히도, Google은 결국 Go 1.20 버전에서 이 문제를 개선했습니다.

Go 1.20 버전부터는 전역 난수 생성기의 시드가 1로 고정되는 대신 자동으로 초기화됩니다.

GODEBUG=randautoseed=0 환경 변수를 설정하여 이 동작을 명시적으로 비활성화하지 않는 한, 프로그램을 실행할 때마다 난수 시퀀스가 달라집니다.

이제 개발자들이 시드 설정에 대해 덜 신경 써도 되게 개선된 것입니다.

5. 암호학적으로 안전한 난수

Go 언어에는 crypto/rand라는 더 안전한 난수 생성기를 구현하는 라이브러리도 있습니다.

코드 주석에 따르면, Linux 기반 플랫폼에서는 getrandom(2) 시스템 콜을 우선적으로 사용합니다.

만약 getrandom(2)를 사용할 수 없는 경우에는 /dev/urandom을 대신 사용합니다.

getrandom/dev/urandom 문자 장치 파일에서 데이터를 읽는 작업을 캡슐화하여 고품질의 난수를 얻는 시스템 콜인데요.

/dev/urandom/dev/random을 시드 참조로 사용합니다.

그리고 /dev/random은 키보드 입력 간격, 마우스 움직임, 디스크 I/O 타이밍 등 하드웨어에서 발생하는 예측 불가능한 노이즈로부터 값을 얻기 때문에 매우 높은 수준의 무작위성 품질을 가집니다.

아래는 crypto/rand 패키지의 일부 코드와 주석입니다.

package rand

import "io"

// Reader는 암호학적으로 안전한 난수 생성기의 전역 공유 인스턴스입니다.
//
// Linux, FreeBSD, Dragonfly, Solaris에서는 사용 가능하다면 getrandom(2)를 사용하고,
// 그렇지 않으면 /dev/urandom을 사용합니다.
// OpenBSD와 macOS에서는 getentropy(2)를 사용합니다.
// 다른 Unix 계열 시스템에서는 /dev/urandom에서 읽어옵니다.
// Windows 시스템에서는 RtlGenRandom API를 사용합니다.
// Wasm에서는 Web Crypto API를 사용합니다.
var Reader io.Reader

// Read는 io.ReadFull을 사용하여 Reader.Read를 호출하는 헬퍼 함수입니다.
// 반환 시, err == nil인 경우에만 n == len(b)가 됩니다.
func Read(b []byte) (n int, err error) {
    return io.ReadFull(Reader, b)
}

 

6. 요약

오늘 내용을 간단히 정리해보겠습니다.

  • 일반적으로 프로그램에서 생성되는 난수는 진짜 난수가 아닌 의사 난수입니다.
  • Go 1.20 이전 버전에서는 math/rand 패키지가 기본적으로 고정된 시드 값(seed=1)을 사용했기 때문에, 프로그램을 실행할 때마다 항상 동일한 난수 시퀀스가 생성되었습니다.
  • Go 1.20 버전부터는 GODEBUG=randautoseed=0 환경 변수를 설정하지 않는 한, 시드가 자동으로 초기화되어 실행할 때마다 다른 난수 시퀀스를 생성합니다.
  • crypto/rand 패키지는 암호학적으로 안전한 난수 생성기를 제공합니다. 이는 운영체제가 제공하는 강력한 무작위성 소스를 활용합니다.
  • crypto/randmath/rand보다 훨씬 안전하지만, 그만큼 성능 비용이 따릅니다. 따라서 사용 목적에 맞게 적절한 라이브러리를 선택하는 것이 중요합니다. 예를 들어, 단순히 게임에서 몬스터의 위치를 무작위로 정하는 것과 같은 경우에는 math/rand로 충분할 수 있지만, 암호화 키 생성이나 보안 토큰 생성 등 보안이 중요한 작업에는 반드시 crypto/rand 패키지를 사용해야 합니다.
  • 참고로, JavaScript 환경에서는 Math.random 함수보다 crypto.getRandomValues 함수가 암호학적으로 안전한 난수를 제공하므로 더 안전한 선택지입니다.

Go 언어에서 난수를 다룰 때 이러한 차이점을 이해하고 상황에 맞는 올바른 도구를 사용하는 것이 중요합니다.