Go 언어 난수 생성 완벽 가이드: math/rand부터 crypto/rand까지


Go 언어 난수 생성 완벽 가이드: math/rand부터 crypto/rand까지

시뮬레이션, 게임, 테스트 데이터 생성, 보안 프로토콜 등 프로그래밍의 수많은 시나리오에서 '난수(Random Number)'는 필수적인 요소입니다.


Go 언어에서는 math/rand 패키지를 통해 '의사 난수(pseudo-random number)'를 생성하는 기능을 제공합니다.


오늘은 이 패키지를 효과적으로 활용하는 방법과, 더 나아가 보안이 중요한 상황에서는 어떤 다른 접근 방식이 필요한지 함께 알아보겠습니다.

1. math/rand 패키지의 기본

가장 먼저, 필요한 패키지를 가져오는 것부터 시작합니다.

package main

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



정수 난수 생성하기

rand.Intn 함수는 0 이상 n 미만([0, n)) 범위의 음수가 아닌 의사 난수를 반환합니다.

func main() {
    // 0 이상 100 미만의 정수 난수 두 개를 생성합니다.
    fmt.Print(rand.Intn(100), ",")
    fmt.Print(rand.Intn(100))
    fmt.Println()
}



위 코드를 실행해 보세요.


아마 81,87 같은 결과가 나올 것입니다.


그런데 프로그램을 다시 실행해도, 또다시 81,87이라는 똑같은 결과가 나옵니다.


왜 그럴까요?


이 비밀을 풀기 전에, 부동소수점 난수도 한번 살펴보겠습니다.

부동소수점 난수 생성하기

rand.Float64 함수는 0.0 이상 1.0 미만([0.0, 1.0)) 범위의 의사 난수를 생성합니다.

// 0.0 이상 1.0 미만의 부동소수점 난수를 생성합니다.
fmt.Println(rand.Float64())



만약 특정 범위의 부동소수점 난수를 원한다면, 결과값을 적절히 조정하면 됩니다.


예를 들어, 5.0 이상 10.0 미만의 난수를 생성하고 싶다면 다음과 같이 할 수 있습니다.

// rand.Float64() * 5 => [0.0, 5.0) 범위의 난수
// (rand.Float64() * 5) + 5 => [5.0, 10.0) 범위의 난수
fmt.Print((rand.Float64()*5)+5, ",")
fmt.Print((rand.Float64() * 5) + 5)
fmt.Println()



2. '시드(Seed)'의 중요성: 예측 가능성 깨뜨리기

앞서 rand.Intn(100)이 매번 똑같은 결과를 내놓았던 이유는 math/rand가 생성하는 숫자가 진정한 의미의 '무작위'가 아닌 '의사 난수'이기 때문입니다.


의사 난수 생성기는 결정론적인 알고리즘에 의해 작동하며, 이 알고리즘의 시작점을 '시드(Seed)'라고 부릅니다.


시드 값이 같으면, 생성되는 난수열은 언제나 동일합니다.


Go의 math/rand는 기본적으로 1이라는 고정된 시드 값을 사용하기 때문에, 프로그램을 실행할 때마다 항상 같은 순서의 숫자들이 나오는 것입니다.

이러한 예측 가능성은 테스트나 디버깅 시에는 동일한 조건을 재현할 수 있어 유용하지만, 대부분의 실제 애플리케이션에서는 매번 다른 난수를 원합니다.


이를 해결하려면 프로그램이 실행될 때마다 다른 시드 값을 제공해야 합니다.


가장 일반적인 방법은 계속해서 변하는 '현재 시간'을 사용하는 것입니다.

전역 난수 생성기에 시드 설정하기 (구 방식, 비권장)

과거에는 rand.Seed() 함수를 사용하여 전역 난수 생성기의 시드를 설정했습니다.

// main 함수 시작 부분에 이 코드를 추가합니다.
rand.Seed(time.Now().UnixNano())



time.Now().UnixNano()는 현재 시간을 나노초 단위의 정수로 반환하므로, 매번 다른 시드 값을 제공할 수 있습니다.


하지만 이 방식은 여러 고루틴(goroutine)이 동시에 전역 난수 생성기에 접근할 때 경합 상태(race condition)를 유발할 수 있어 더 이상 권장되지 않습니다.

새로운 난수 생성기 인스턴스 만들기 (권장 방식)

더 안전하고 현대적인 방법은 자신만의 난수 생성기 인스턴스를 만드는 것입니다.

// 1. 현재 시간을 기반으로 새로운 난수 소스를 생성합니다.
s1 := rand.NewSource(time.Now().UnixNano())
// 2. 소스를 사용하여 새로운 난수 생성기 인스턴스를 만듭니다.
r1 := rand.New(s1)

// 이제 r1을 사용하여 난수를 생성합니다.
fmt.Print(r1.Intn(100), ",")
fmt.Print(r1.Intn(100))
fmt.Println()



이제 프로그램을 실행할 때마다 완전히 다른 난수 시퀀스를 얻을 수 있습니다.


반대로, 테스트 목적으로 항상 동일한 순서의 난수를 재현하고 싶다면 고정된 시드 값을 사용하면 됩니다.

s2 := rand.NewSource(42) // 시드 값을 42로 고정
r2 := rand.New(s2)
fmt.Print(r2.Intn(100), ",")
fmt.Print(r2.Intn(100)) // 언제 실행해도 항상 81, 87이 나옵니다.
fmt.Println()



3. Go 1.22의 새로운 강자: math/rand/v2

Go 1.22 버전부터는 더 개선된 math/rand/v2 패키지가 도입되었습니다.


이 새로운 패키지는 몇 가지 중요한 이점을 가집니다.

  • 자동 시드 설정: 더 이상 time.Now().UnixNano()를 사용한 수동 시드 설정이 필요 없습니다.


    패키지가 알아서 안전하게 시드를 초기화해 줍니다.

  • 더 나은 API: rand.IntN(n) 처럼 더 직관적인 이름의 함수들을 제공합니다.

v2 패키지를 사용하면 코드가 훨씬 더 간결해집니다.

import "math/rand/v2" // v2 패키지를 임포트합니다.

func main() {
    // 시드 설정 없이 바로 사용해도 매번 다른 값이 나옵니다.
    fmt.Println(rand.IntN(100))
    fmt.Println(rand.Float64())
}



만약 Go 1.22 이상을 사용하고 있다면, 특별한 이유가 없는 한 math/rand/v2 패키지를 사용하는 것이 좋습니다.

4. 보안이 중요하다면: crypto/rand

지금까지 우리가 다룬 math/rand 패키지는 시뮬레이션이나 게임처럼 예측 가능성이 크게 문제 되지 않는 일반적인 목적에 적합합니다.


하지만 절대로 암호학적인 용도로 사용해서는 안 됩니다.


의사 난수는 알고리즘에 의해 생성되므로, 시드 값이나 이전 난수 값을 알면 다음 난수를 예측할 수 있기 때문입니다.


만약 암호 키, 보안 토큰, 세션 ID 등 보안과 관련된 난수를 생성해야 한다면, 반드시 crypto/rand 패키지를 사용해야 합니다.

crypto/rand는 운영체제가 제공하는 암호학적으로 안전한 난수 생성기를 사용합니다.


이 난수는 예측이 불가능하여 보안 애플리케이션에 적합합니다.

import (
    "crypto/rand"
    "fmt"
    "math/big"
)

func main() {
    // 0 이상 100 미만의 암호학적으로 안전한 난수를 생성합니다.
    n, err := rand.Int(rand.Reader, big.NewInt(100))
    if err != nil {
        panic(err)
    }
    fmt.Println(n)
}



math/rand에 비해 사용법이 조금 더 복잡하고 속도도 느리지만, 보안이 최우선인 상황에서는 이 차이가 여러분의 시스템을 안전하게 지켜줄 것입니다.

결론

Go 언어에서 난수를 생성하는 것은 간단하지만, 그 목적에 맞는 올바른 도구를 선택하는 것이 중요합니다.

  • '일반적인 용도': math/rand/v2를 사용하여 간결하고 안전하게 난수를 생성하세요.


    (Go 1.22 미만이라면 rand.New()NewSource를 사용하세요.

    )
  • '테스트 및 재현': math/rand와 고정된 시드 값을 사용하여 예측 가능한 난수열을 만드세요.

  • '보안 및 암호학': 반드시 crypto/rand 패키지를 사용하여 예측 불가능한 난수를 생성하세요.

이 세 가지 원칙만 기억한다면, 여러분은 어떤 상황에서든 올바르고 안전하게 난수를 활용할 수 있을 것입니다.