난수는 성능, 정확성, 보안을 모두 고려해야 합니다. .NET 6+에서 도입된 Random.Shared는 일반 난수 생성의 표준 선택이며, 보안이 필요한 경우에는 RandomNumberGenerator를 사용해야 합니다. 이 글에서는 Random.Shared의 올바른 사용법과 병렬, 재현성, 보안 난수 전략을 간단히 정리합니다.
1. Random.Shared란?
Random.Shared는 전역적으로 공유되는 스레드 안전한 Random 인스턴스입니다. 대부분의 애플리케이션에서 new Random()를 반복 생성하는 대신 Random.Shared를 사용하면 성능과 안전성이 개선됩니다.
using System;
class Program
{
static void Main()
{
int n = Random.Shared.Next(0, 100); // 0~99
double d = Random.Shared.NextDouble(); // [0,1)
long l = Random.Shared.NextInt64(0, 1_000_000);// .NET 6+
var buffer = new byte[16];
Random.Shared.NextBytes(buffer); // 바이트 채우기
Console.WriteLine($"{n}, {d}, {l}, {Convert.ToHexString(buffer)}");
}
}
2. 언제 Random.Shared를 쓰나요
- 대부분의 일반 난수: 게임 로직, 샘플링, 무작위 정렬 등은 Random.Shared로 충분합니다.
- ASP.NET에서 요청마다 new Random() 생성은 피하고, Random.Shared를 사용합니다.
- 루프나 이벤트마다 Random 인스턴스 생성은 같은 시드 문제(동일한 값 반복)를 유발할 수 있으므로 지양합니다.
// 나쁜 예: 짧은 시간에 여러 번 생성하면 같은 시드로 같은 수열을 만들 수 있습니다.
for (int i = 0; i < 3; i++)
{
var r = new Random();
Console.WriteLine(r.Next());
}
// 좋은 예: 공유 인스턴스 사용
for (int i = 0; i < 3; i++)
{
Console.WriteLine(Random.Shared.Next());
}
3. 병렬 코드에서의 패턴
Random.Shared는 스레드 안전하므로 Parallel.For, Task 등과 함께 직접 사용할 수 있습니다. 극단적으로 RNG 사용량이 매우 높은 경우에만 스레드별 Random을 고려합니다.
using System;
using System.Threading.Tasks;
class Program
{
static void Main()
{
long sum = 0;
object gate = new object();
Parallel.For(0, 1_000_000, i =>
{
int v = Random.Shared.Next(100); // 스레드 안전
if (v % 2 == 0)
{
lock (gate)
{
sum += v; // 공유 상태는 별도 동기화 필요
}
}
});
Console.WriteLine(sum);
}
}
아주 높은 동시성에서 RNG만 병목이 된다면, 스레드별 Random을 사용할 수 있습니다. 단, 시드 충돌을 피하도록 주의합니다.
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static void Main()
{
var tl = new ThreadLocal<Random>(() =>
new Random(unchecked(Environment.TickCount ^ Thread.CurrentThread.ManagedThreadId)));
Parallel.For(0, 1_000_000, i =>
{
int v = tl.Value!.Next(100);
// ... 계산
});
}
}
4. 재현 가능성과 시드 관리
실험, 테스트에서 같은 수열이 필요하면 Random.Shared 대신 명시적으로 시드를 준 Random 인스턴스를 사용합니다. 수열은 런타임 버전 차이에 따라 달라질 수 있으므로, 재현은 같은 .NET 버전/환경에서 수행하는 것이 안전합니다.
var r = new Random(42); // 고정 시드
for (int i = 0; i < 5; i++)
{
Console.WriteLine(r.Next());
}
5. 보안 난수 생성 전략
토큰, API 키, 암호학적 용도에는 Random.Shared를 사용하면 안 됩니다. System.Security.Cryptography.RandomNumberGenerator를 사용하세요. 범위 균등 분포, 바이트 채우기, 토큰 생성에 특화된 API가 제공됩니다.
using System;
using System.Security.Cryptography;
class Program
{
static void Main()
{
// 256비트 토큰 생성 (HEX)
string token = Convert.ToHexString(RandomNumberGenerator.GetBytes(32));
Console.WriteLine(token);
// 균등한 정수 (편향 없음) [min, max) 범위
int code = RandomNumberGenerator.GetInt32(100_000, 1_000_000); // 6자리 코드
Console.WriteLine(code);
// 버퍼 채우기
byte[] salt = new byte[16];
RandomNumberGenerator.Fill(salt);
Console.WriteLine(Convert.ToHexString(salt));
}
}
추가 팁: Guid.NewGuid()는 내부적으로 안전한 난수를 사용합니다. 간단한 식별자가 필요하면 고려할 수 있습니다.
6. 흔한 실수와 체크리스트
- 루프/요청마다 new Random() 생성하지 않습니다. Random.Shared를 우선 사용합니다.
- 보안이 필요한 경우 RandomNumberGenerator 계열을 사용합니다.
- 범위 난수에서 상한은 제외입니다: Next(min, max)와 GetInt32(min, max)는 max 미포함입니다.
- 병렬 코드에서 난수는 안전하지만 공유 상태 갱신은 별도 동기화가 필요합니다.
- 재현성 필요 시 고정 시드 Random을 별도 주입/생성합니다.
7. 요약
일반 난수는 Random.Shared로 간단하고 빠르게 처리합니다. 보안 난수는 RandomNumberGenerator로 분리해 안전하게 생성합니다. 병렬 환경에서도 Random.Shared는 기본 선택이며, 극단적 케이스에만 스레드별 Random을 고려합니다. 재현성이 필요하면 고정 시드를 사용하세요.
'C#' 카테고리의 다른 글
| C# Semaphore와 Mutex 차이 및 선택 기준 (0) | 2026.07.28 |
|---|---|
| C# PeriodicTimer로 주기적 작업 구현하기 (0) | 2026.07.28 |
| C# UTF8String 리터럴과 UTF-8 문자열 처리 최적화 (0) | 2026.07.27 |
| C# init 접근자를 활용한 불변 객체 설계 (0) | 2026.07.26 |
| C# required 키워드로 필수 초기화 강제하기 (0) | 2026.07.26 |