Menu

C# 랜덤 난수: Random.Next, 범위, 시드, 셔플, 보안 난수

C#에서 Random 클래스로 난수를 만드는 방법: Next와 포함되지 않는 상한, 범위 안의 랜덤 double, 재현 가능한 결과를 위한 시드, 랜덤 요소 고르기, 리스트 셔플, 루프 안의 new Random 함정, 암호학적으로 안전한 난수를 알아봅니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

System.Random 클래스는 의사 난수를 만듭니다. 시드에서 출발하는, 무작위처럼 보이는 결정적 시퀀스입니다. 인스턴스를 하나 만들고 메서드를 호출하세요:

출력 예:

1294270287
83
You rolled a 5
0.6060

다시 실행하면 숫자가 바뀝니다. 상한은 포함되지 않습니다. Next(1, 7)은 1, 2, 3, 4, 5, 6을 반환할 수 있고 7은 반환하지 않습니다. 1부터 10까지(10 포함)의 수를 원하면 Next(1, 11)이라고 쓰세요. min보다 작은 max를 넘기면 ArgumentOutOfRangeException이 발생합니다.

범위 안의 난수

정수는 Next(min, max)로 해결됩니다. 범위 안의 double은 NextDouble()을 비율에 맞게 늘리고, 가격 같은 decimal은 원하는 정밀도가 나오도록 정수로부터 만드세요:

출력 예:

23.6 C
72.42
heads
N
-3

rng.Next(100, 10000) / 100m은 센트 단위의 정수를 고르고 decimal로 나누므로, 가격은 항상 소수 둘째 자리까지 정확히 나옵니다. double을 늘리면 72.4183 같은 값이 나옵니다.

시드: 재현 가능한 시퀀스

정수 시드를 넘기면 시퀀스를 반복할 수 있습니다. 같은 시드를 가진 두 Random 객체는 정확히 같은 숫자를 같은 순서로 만듭니다:

출력:

16 29 91 99 91
16 29 91 99 91
38

시드는 테스트, 다시 돌려야 하는 시뮬레이션, 게임의 리플레이, 절차적으로 생성하는 레벨("월드 시드 2026")에 필요한 것입니다. 사용한 시드를 기록해 두면 놀라운 결과를 나중에 재현할 수 있습니다.

Microsoft는 시드가 모든 .NET 버전에서 같은 시퀀스를 만든다고 약속하지 않습니다. 시드로 만든 결과를 데이터로 저장하지 마세요(예를 들어 고정 시드로 고객 ID를 만들고 업그레이드 후에 다시 만들 수 있다고 기대하는 것).

랜덤 요소 고르기

랜덤 인덱스는 Next(0, count)이며, 상한이 포함되지 않으므로 항상 올바른 인덱스입니다:

출력:

Winner 1 gets a sticker pack
Winner 2 gets a mug
Winner 3 gets a sticker pack
common     695
rare       260
legendary  45

가중치 뽑기는 0부터 99까지의 수를 가중치 크기만큼의 구간에 대응시킵니다. 0~69는 common, 70~94는 rare, 95~99는 legendary입니다. 1,000번 뽑으면 개수가 700, 250, 50 근처에 모입니다(여기서는 695, 260, 45).

리스트 셔플: Fisher-Yates

리스트를 무작위 순서로 만들려면 끝에서부터 걸어가며 각 요소를 그 위치 이전(자기 자신 포함)의 랜덤 요소와 바꿉니다. 이것이 Fisher-Yates 셔플이며, 빠르고 모든 순서가 같은 확률로 나옵니다. 제네릭 메서드로 작성하면 어떤 요소 타입의 배열과 리스트에도 동작합니다:

출력:

5 A 8 6 4 7 2 3
4 2 1 3 5

인터넷에서 보이는 두 가지 지름길은 더 나쁩니다. list.OrderBy(x => rng.Next())는 동작하지만 정렬을 하므로 더 느리고, 랜덤 키가 겹칠 수 있습니다. rng.Next(i + 1) 대신 rng.Next(items.Count)로 바꾸는 방식은 비슷해 보이지만 일부 순서가 다른 순서보다 더 자주 나옵니다. .NET 8 이상에서는 Random.Shared.Shuffle(array)가 올바른 셔플을 제자리에서 하고, rng.GetItems(choices, 5)는 랜덤 항목 다섯 개를 (중복을 허용해) 고릅니다.

루프 안의 new Random() 함정

숫자가 필요할 때마다 새 Random을 만드는 것은 전형적인 버그입니다:

// Don't do this
int RollDie()
{
    var rng = new Random();   // new instance on every call
    return rng.Next(1, 7);
}

.NET Framework에서 시드 없는 new Random()은 10~16밀리초마다 한 번씩만 바뀌는 시스템 시계(Environment.TickCount)를 씁니다. 빠른 루프에서 호출하면 같은 시드의 인스턴스가 만들어져 같은 숫자를 반환하므로, 주사위 굴리기 루프가 4 4 4 4 4를 출력합니다. .NET Core와 .NET 5 이상은 공유 랜덤 소스로 인스턴스마다 시드를 정해 증상을 가리지만, 숫자마다 객체를 만드는 것은 여전히 낭비이고, 이 코드를 예전 프로젝트나 Unity에서 재사용하면 깨집니다.

해결책은 클래스에 인스턴스 하나를 필드로 두는 것입니다:

출력:

3 6 6 5 6 1 2 1

여기서 시드는 예제가 매번 같은 결과를 출력하게 하려고 넣은 것일 뿐이며, 실제 게임이라면 new Random()이라고 씁니다.

스레드와 Random.Shared

Random은 스레드로부터 안전하지 않습니다. 두 스레드가 같은 인스턴스의 Next를 동시에 호출하면 내부 상태가 손상될 수 있으며, 잘 알려진 증상은 그 뒤로 계속 0을 반환하는 인스턴스입니다. 간단한 것부터 선택지를 보면:

static class Dice
{
    // .NET 6 and later: a thread-safe shared instance
    public static int RollShared() => Random.Shared.Next(1, 7);

    // Any version: one instance per thread
    [ThreadStatic] private static Random _local;
    private static Random Local => _local ?? (_local = new Random(Guid.NewGuid().GetHashCode()));
    public static int RollPerThread() => Local.Next(1, 7);

    // Any version: a lock around one shared instance
    private static readonly Random _rng = new Random();
    private static readonly object _sync = new object();
    public static int RollLocked()
    {
        lock (_sync) { return _rng.Next(1, 7); }
    }
}

새 코드에서는 Random.Shared가 올바른 기본값입니다. 스레드로부터 안전하고, 필드가 필요 없고, 무작위로 시드가 정해집니다. 시드를 정할 수는 없으므로 재현성이 필요하면 직접 new Random(seed)를 쓰세요.

암호학적으로 안전한 난수

System.Random은 설계상 예측 가능합니다. 시드를 알거나 출력을 충분히 본 사람은 나머지를 계산할 수 있습니다. 게임과 샘플링에는 괜찮지만, 비밀번호, 재설정 토큰, 세션 ID, 키, 추첨에는 틀린 선택입니다. 이런 경우에는 운영체제의 안전한 생성기에서 읽는 System.Security.Cryptography의 RandomNumberGenerator를 쓰세요:

출력 예:

Verification code: 276062
Reset token: 391e40ab5fa205e6457e48661586d10a
Password: gyv8LWyajJcF

RandomNumberGenerator.GetInt32(.NET Core 3.0 이상)는 범위 안의 치우침 없는 정수를 반환합니다. bytes[0] % 10으로 보안 코드를 만들지 마세요. 나머지 연산 때문에 일부 숫자가 다른 숫자보다 더 자주 나옵니다. .NET 6 이상에서는 RandomNumberGenerator.GetBytes(16)이 새 배열을 바로 반환하고, Convert.ToHexString(bytes)가 이를 서식화합니다. 비밀일 필요가 없는 고유 식별자에는 Guid.NewGuid()가 더 간단합니다.

자주 묻는 질문

C#에서 난수는 어떻게 만드나요?

Random 객체를 하나 만들고 Next를 호출합니다: var rng = new Random(); int roll = rng.Next(1, 7);은 1부터 6까지를 줍니다. Next(max)는 0부터 max - 1까지, NextDouble()은 0.0 이상 1.0 미만의 double을 줍니다. .NET 6 이상에서는 객체 없이 Random.Shared.Next(1, 7)을 쓸 수 있습니다.

Random.Next의 상한은 포함되나요?

아니요. Next(min, max)는 min 이상이고 max보다 엄격히 작은 수를 반환합니다. 1부터 10까지(10 포함)의 수를 원하면 Next(1, 11)이라고 쓰세요. 이 덕분에 Next(0, list.Count)는 리스트의 올바른 인덱스가 됩니다.

new Random()이 같은 숫자를 내는 이유는 무엇인가요?

.NET Framework에서 new Random()은 몇 밀리초마다 한 번씩만 바뀌는 시스템 시계로 시드를 정하므로, 빠른 루프에서 만든 여러 인스턴스가 같은 시드와 같은 시퀀스를 얻습니다. .NET Core와 .NET 5 이상은 인스턴스마다 다르게 시드를 정하지만, 해결책은 어디서나 같습니다. Random을 하나 만들어 재사용하세요.

C#에서 매번 같은 난수를 얻으려면 어떻게 하나요?

생성자에 시드를 넘기세요: new Random(42). 같은 시드를 가진 두 인스턴스는 같은 시퀀스를 만들므로 테스트, 시뮬레이션, 절차적 생성을 재현할 수 있습니다. 시드에 대한 시퀀스는 .NET 버전마다 다를 수 있으므로, 시드로 만든 결과를 영구 데이터로 저장하지 마세요.

System.Random은 비밀번호나 토큰에 써도 안전한가요?

아니요. Random은 예측 가능합니다. 상태나 시드를 알게 된 사람은 숫자를 재현할 수 있습니다. 비밀번호, 토큰, 키 등 보안과 관련된 모든 것에는 System.Security.Cryptography.RandomNumberGenerator를 쓰세요. 예를 들어 RandomNumberGenerator.GetInt32(0, 10)이나 RandomNumberGenerator.GetBytes(32)입니다.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기