Rust 난수 (rand 크레이트)
게임은 주사위를 굴려요. 퀴즈는 문제 순서를 섞어요. 테스트에는 샘플 데이터가 필요해요. 이 모든 일에 난수(random number)가 필요한데, Rust 표준 라이브러리에는 바로 쓸 수 있는 난수 생성기가 없어요. 대신 거의 모든 사람이 크레이트 하나를 써요. 바로 rand (영어)예요.
이 페이지에서는 최신 rand 0.10 API를 보여 줘요. 이게 중요한 이유는 rand가 0.9와 0.10 버전에서 일부 이름을 바꿨기 때문이에요. 오래된 튜토리얼에 thread_rng()나 gen_range()가 나온다면, 예전 버전을 기준으로 쓴 글이에요. 새 이름과, 예전 이름을 쓰면 나오는 에러도 함께 살펴볼게요.
프로젝트에 rand 추가하기
섹션 제목: “프로젝트에 rand 추가하기”프로젝트 폴더에서 다음을 실행하세요.
cargo add rand그러면 Cargo.toml에 이런 줄이 추가돼요.
[dependencies]rand = "0.10"크레이트와 Cargo.toml이 처음이라면 그 페이지부터 읽어 보세요. 이 페이지의 “Try it” 버튼은 Rust Playground를 여는데, Playground에는 이미 rand가 들어 있어요.
간단하게 난수 하나 만들기
섹션 제목: “간단하게 난수 하나 만들기”난수 하나만 필요하다면 자유 함수(free function)인 rand::random_range와 rand::random을 쓰세요.
fn main() { let die: u32 = rand::random_range(1..=6); let coin: bool = rand::random(); let chance: f64 = rand::random();
println!("주사위 값이 1~6 사이인가요? {}", (1..=6).contains(&die)); println!("chance가 0.0..1.0 범위인가요? {}", (0.0..1.0).contains(&chance)); println!("동전이 앞면인가요? {coin}");}여기에 실행 결과가 없는 건 일부러예요. 마지막 줄이 무작위로 true나 false가 되거든요. “Try it”을 몇 번 눌러서 바뀌는 걸 확인해 보세요.
random_range(1..=6)은 1부터 6까지, 6을 포함한 숫자를 줘요.1..6이라고 쓰면 1부터 5까지만 나와요. 범위는 for 반복문에서와 똑같이 동작해요.random()은 요청한 타입의 값을 아무거나 골라요.bool이라면 공정한 동전 던지기예요.f64라면0.0이상1.0미만(1.0은 포함하지 않아요)의 숫자예요.- 타입 표기(
: u32,: bool)가rand에게 무엇을 만들지 알려 줘요. 이게 없으면 Rust가 어떤 타입을 원하는지 모를 수도 있어요.
생성기와 RngExt 트레이트
섹션 제목: “생성기와 RngExt 트레이트”난수가 많이 필요하다면, rand::rng()로 생성기(generator)를 한 번 얻어 두고 그 메서드를 호출하세요. 조금 더 빠르기도 하고, 대부분의 코드에서 볼 수 있는 스타일이에요.
use rand::RngExt;
fn main() { let mut rng = rand::rng();
let rolls: Vec<u32> = (0..1000).map(|_| rng.random_range(1..=6)).collect(); println!("주사위를 {}번 굴렸어요", rolls.len()); println!("모두 1~6 사이인가요? {}", rolls.iter().all(|r| (1..=6).contains(r)));
let x: f64 = rng.random(); println!("x가 0.0..1.0 범위인가요? {}", (0.0..1.0).contains(&x));}실행 결과:
주사위를 1000번 굴렸어요모두 1~6 사이인가요? truex가 0.0..1.0 범위인가요? true숫자 자체 대신 확인 결과를 출력했어요. 그래서 값은 매번 달라도 출력은 항상 같아요.
자주 쓰게 될 메서드가 두 개 더 있어요.
let lucky = rng.random_bool(0.1); // 약 10%의 확률로 truelet rare = rng.random_ratio(1, 100); // 약 100번에 1번꼴로 trueuse rand::RngExt; 줄은 왜 필요할까요? random_range나 random_bool 같은 메서드는 트레이트에서 오는데, 트레이트의 메서드는 그 트레이트가 스코프 안에 있을 때만 호출할 수 있어요. rand 0.10에는 트레이트가 두 개 있어요.
Rng: “나는 무작위 비트를 만들 수 있어”라는 기본 트레이트예요. 모든 생성기가 구현해요.RngExt: 쓰기 편한 메서드들을 더해 주는 트레이트예요.random,random_range,random_bool등이요. 모든Rng에서 자동으로 동작해요.
정리하면, 메서드를 호출하려면 RngExt를 가져오고, “아무 생성기나” 받는 함수를 쓸 때는 Rng를 쓰세요. 아래의 주사위 굴리기 예제에서 둘 다 보게 될 거예요.
시드로 같은 결과 재현하기
섹션 제목: “시드로 같은 결과 재현하기”rand::rng()는 실행할 때마다 다른 숫자를 줘요. 실제 게임에서는 그게 맞아요. 하지만 테스트나 디버깅, 게임의 “오늘의 도전” 스테이지 같은 경우에는 매번 같은 “무작위” 숫자가 필요해요.
이럴 때 시드(seed)를 써요. 시드를 준 생성기는 같은 시드에 대해 항상 같은 수열을 만들어요.
use rand::rngs::StdRng;use rand::{RngExt, SeedableRng};
fn main() { let mut a = StdRng::seed_from_u64(2024); let mut b = StdRng::seed_from_u64(2024);
let first: Vec<u8> = (0..4).map(|_| a.random_range(0..100)).collect(); let second: Vec<u8> = (0..4).map(|_| b.random_range(0..100)).collect();
println!("{:?}", first); println!("{:?}", second); println!("같은가요? {}", first == second);}실행 결과:
[53, 82, 74, 51][53, 82, 74, 51]같은가요? trueseed_from_u64는 SeedableRng 트레이트에서 오기 때문에, 이것도 가져와야 해요. 2024를 다른 숫자로 바꾸면 다른 수열이 나오지만, 그 수열 역시 실행할 때마다 같아요.
솔직하게 한 가지 주의할 점이 있어요. StdRng의 알고리즘은 앞으로 나올 rand 버전에서 바뀔 수 있어요. 위의 숫자는 rand 0.10에서 나온 거예요. 테스트가 영원히 정확히 같은 숫자를 내야 한다면, rand 책에서는 ChaCha8Rng 같은, 이름이 정해진 생성기를 쓰라고 권해요(아래 “더 알아보기” 참고).
슬라이스 섞기와 고르기
섹션 제목: “슬라이스 섞기와 고르기”리스트를 다루려면 rand::seq에서 트레이트 두 개를 더 가져와요.
SliceRandom은 슬라이스를 그 자리에서 섞는shuffle을 제공해요.IndexedRandom은choose(무작위 항목 하나)와sample(서로 다른 항목 여러 개)을 제공해요.
use rand::rngs::StdRng;use rand::seq::{IndexedRandom, SliceRandom};use rand::SeedableRng;
fn main() { let mut rng = StdRng::seed_from_u64(7);
let mut cards = vec!["A", "K", "Q", "J", "10"]; cards.shuffle(&mut rng); println!("섞은 결과: {:?}", cards);
let colors = ["빨강", "초록", "파랑"]; println!("고른 색: {:?}", colors.choose(&mut rng));
let team: Vec<&str> = cards.sample(&mut rng, 2).copied().collect(); println!("팀: {:?}", team);
let empty: [i32; 0] = []; println!("빈 배열에서: {:?}", empty.choose(&mut rng));}실행 결과:
섞은 결과: ["K", "J", "10", "Q", "A"]고른 색: Some("빨강")팀: ["K", "J"]빈 배열에서: Nonechoose는 Option을 반환해요. 빈 슬라이스에서는 고를 게 없으니까요. 이 메서드들은 배열, 벡터, 슬라이스에서 똑같이 동작해요. 시드를 준 rng를 rand::rng()로 바꾸면 실행할 때마다 새롭게 섞여요.
예제: 주사위 굴리기
섹션 제목: “예제: 주사위 굴리기”주사위를 굴리는 함수를 만들어 볼게요. 핵심은 매개변수 타입이에요. &mut impl Rng는 “아무 난수 생성기나”라는 뜻이에요. 실제 게임에서는 rand::rng()를 넘기고, 테스트에서는 시드를 준 StdRng를 넘겨서 어떤 값이 나올지 정확히 알 수 있어요.
use rand::rngs::StdRng;use rand::{Rng, RngExt, SeedableRng};
/// 면이 `sides`개인 주사위를 `count`개 굴려요.fn roll(rng: &mut impl Rng, count: u32, sides: u32) -> Vec<u32> { (0..count).map(|_| rng.random_range(1..=sides)).collect()}
fn main() { let mut rng = StdRng::seed_from_u64(1);
for (count, sides) in [(2, 6), (1, 20), (3, 4)] { let rolls = roll(&mut rng, count, sides); let total: u32 = rolls.iter().sum(); println!("{count}d{sides}: {:?} = {total}", rolls); }}실행 결과:
2d6: [5, 6] = 111d20: [16] = 163d4: [3, 1, 2] = 62d6은 보드게임에서 “6면체 주사위 두 개”를 줄여 쓰는 표기예요. roll 안에서 rand::rng()를 호출하지 않고 생성기를 밖에서 넘겨받는 건 좋은 습관이에요. 함수를 테스트하기 쉬워지거든요.
예제: 무작위 비밀번호 생성기
섹션 제목: “예제: 무작위 비밀번호 생성기”허용된 문자 집합에서 16글자를 골라 볼게요. 이 집합에서는 0/O, 1/l처럼 헷갈리기 쉬운 문자를 뺐어요. 그래서 비밀번호를 입력하기가 더 쉬워요.
use rand::seq::IndexedRandom;use rand::Rng;
const CHARSET: &[u8] = b"ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%&*";
fn password(rng: &mut impl Rng, length: usize) -> String { (0..length) .map(|_| *CHARSET.choose(rng).unwrap() as char) .collect()}
fn main() { let mut rng = rand::rng(); let pw = password(&mut rng, 16);
println!("길이: {}", pw.len()); println!("허용된 문자만 있나요? {}", pw.bytes().all(|b| CHARSET.contains(&b))); // println!("{pw}"); // 주석을 풀면 비밀번호가 보여요. 실행할 때마다 달라요}실행 결과:
길이: 16허용된 문자만 있나요? trueb"..."는 바이트 문자열이에요. ASCII 문자로 된 &[u8]이죠. choose가 바이트 하나를 고르고, as char가 그것을 문자로 바꿔요. 여기서는 시드를 준 생성기가 아니라 rand::rng()를 썼다는 점에 주의하세요. 시드를 준 생성기는 절대 쓰면 안 돼요. 고정된 시드로 만든 비밀번호는 매번 같은 비밀번호가 되니, 비밀번호라고 할 수 없어요.
다시 숫자 맞히기 게임으로
섹션 제목: “다시 숫자 맞히기 게임으로”숫자 맞히기 게임 프로젝트는 표준 라이브러리만 쓰려고 시계를 이용한 꼼수로 비밀 숫자를 골라요. rand를 쓰면 secret_number 함수 전체가 한 줄이 돼요.
let secret: u32 = rand::random_range(1..=100);게임의 나머지 부분은 그대로예요. 다음은 Playground에서 실행해 볼 수 있는 버전이에요. 비밀 숫자는 시드를 준 생성기에서 나오고, 컴퓨터는 남은 범위의 한가운데를 고르는 방식으로 추측해요.
use rand::rngs::StdRng;use rand::{RngExt, SeedableRng};use std::cmp::Ordering;
fn main() { // 실제 게임에서는: let secret: u32 = rand::random_range(1..=100); let mut rng = StdRng::seed_from_u64(99); let secret: u32 = rng.random_range(1..=100);
let (mut low, mut high) = (1, 100); let mut attempts = 0; loop { let guess = (low + high) / 2; attempts += 1; match guess.cmp(&secret) { Ordering::Less => { println!("{guess}: 너무 작아요"); low = guess + 1; } Ordering::Greater => { println!("{guess}: 너무 커요"); high = guess - 1; } Ordering::Equal => { println!("{guess}: 정답이에요! {attempts}번 만에 찾았어요."); break; } } }}실행 결과:
50: 너무 작아요75: 너무 작아요88: 너무 작아요94: 너무 작아요97: 너무 작아요99: 정답이에요! 6번 만에 찾았어요.재미있는 사실: 이렇게 “범위를 반으로 줄이는” 전략을 쓰면 1부터 100까지에서는 7번 넘게 추측할 일이 절대 없어요.
rand는 비밀번호나 키에 써도 안전할까?
섹션 제목: “rand는 비밀번호나 키에 써도 안전할까?”짧게 답하면, rand::rng()는 예측하기 어렵도록 설계됐지만, 정말 중요한 비밀에는 그 일을 위해 만든 도구를 쓰세요.
rand::rng()는 암호학적으로 안전한 알고리즘(현재는 ChaCha12)을 써요.getrandom(영어) 크레이트를 통해 운영체제의 난수 소스에서 시드를 받고, 주기적으로 시드를 다시 받아요. rand 문서에서는 이것을 “빠르고, 꽤 안전한 생성기”라고 부르면서, 모든 보안 요구 사항에 맞는다고 약속할 수는 없다고 말해요.- 암호화 키, API 토큰 같은 중요한 비밀에는
getrandom(또는rand::rngs::SysRng)으로 운영체제에서 바이트를 바로 받거나, 더 좋게는 전용 암호 라이브러리가 키를 만들게 하세요. - 시드를 준 생성기(
seed_from_u64)나 빠른SmallRng는 비밀에 절대 쓰지 마세요. 시드를 알거나 추측한 사람은 누구든 똑같은 “무작위” 값을 만들어 낼 수 있어요.
게임, 시뮬레이션, 섞기, 테스트 데이터에는 rand::rng()가 딱 맞아요.
자주 하는 실수
섹션 제목: “자주 하는 실수”RngExt 대신 Rng를 가져오기. rand 0.9에서는 메서드가 Rng에 있었기 때문에, 예전 코드는 use rand::Rng;라고 써요. rand 0.10에서는 이렇게 돼요.
use rand::Rng;
fn main() { let mut rng = rand::rng(); let n = rng.random_range(1..=6); println!("{n}");}error[E0599]: no method named `random_range` found for struct `ThreadRng` in the current scope --> src/main.rs:5:17 |5 | let n = rng.random_range(1..=6); | ^^^^^^^^^^^^ | = help: items from traits can only be used if the trait is in scopehelp: trait `RngExt` which provides `random_range` is implemented but not in scope; perhaps you want to import it |1 + use rand::RngExt; |ThreadRng에서 random_range 메서드를 찾을 수 없고, 그 메서드를 제공하는 RngExt 트레이트가 스코프에 없다는 뜻이에요. 컴파일러가 해결책까지 알려 줘요. use rand::RngExt;를 추가하면 돼요.
rand 0.8용 코드 복사하기. 아주 오래된 예제는 thread_rng()와 gen_range()를 써요.
fn main() { let n = rand::thread_rng().gen_range(1..=6); println!("{n}");}error[E0425]: cannot find function `thread_rng` in crate `rand` --> src/main.rs:2:19 |2 | let n = rand::thread_rng().gen_range(1..=6); | ^^^^^^^^^^ not found in `rand`rand 크레이트에서 thread_rng 함수를 찾을 수 없다는 에러예요. 새 이름은 rand::rng()와 random_range()예요. 마찬가지로 gen()은 random()이 됐고, gen_bool()은 random_bool()이 됐어요.
범위의 끝을 하나 틀리기. random_range(1..6)은 절대 6을 돌려주지 않아요. 주사위라면 1..=6이라고 쓰세요.
cargo add rand로 크레이트를 추가하세요. Rust 표준 라이브러리에는 자체 난수 생성기가 없어요.- 간단한 값에는
rand::random_range(1..=6)과rand::random::<T>()를 쓰세요. - 값이 많이 필요하면
let mut rng = rand::rng();를 쓰고,random_range나random_bool같은 메서드를 위해use rand::RngExt;를 추가하세요. SliceRandom::shuffle은 슬라이스를 섞고,IndexedRandom::choose는 항목 하나를 골라Option으로 돌려줘요.StdRng::seed_from_u64(n)은 실행할 때마다 같은 수열을 줘요. 테스트나 반복 가능한 게임 스테이지에 좋지만, 비밀에는 절대 쓰지 마세요.- 키와 토큰에는 운영체제의 난수 소스(
getrandom)나 전용 암호 크레이트를 쓰세요.
이해도 점검
섹션 제목: “이해도 점검”이해도 점검
1. 6을 돌려줄 수 있는 호출은 무엇일까요?
2. rand 0.10에서 rng.random_range(..)가 "no method named random_range" 에러로 실패해요. 무엇이 빠졌을까요?
3. StdRng::seed_from_u64(42)는 왜 쓸까요?
4. 빈 슬라이스에 choose를 호출하면 무엇을 돌려줄까요?
점수: 0 / 4
더 알아보기
섹션 제목: “더 알아보기”- The Rust Rand Book (영어):
rand의 공식 안내서예요. 빠른 시작부터 생성기 고르기까지 다뤄요. randAPI 문서 - docs.rs (영어): 모든 함수, 트레이트, 생성기를 예제와 함께 볼 수 있어요.- Updating to 0.10 - The Rust Rand Book (영어):
Rng/RngExt이름 변경을 포함해 0.10에서 무엇이 바뀌었는지 정리했어요. 예전 코드를 만났을 때 유용해요. - Our RNGs - The Rust Rand Book (영어): 생성기들을 비교하고, 어떤 것이 보안에 적합한지 알려 줘요.
- Reproducibility - The Rust Rand Book (영어): 시드를 준 결과를 버전과 플랫폼이 달라도 똑같이 유지하는 방법을 설명해요.