C++ random_device | '하드웨어 난수' 가이드
이 글의 핵심
C++ random_device의 C++, random_device, "하드웨어, 1.
들어가며
std::random_device는 C++11에서 도입된 하드웨어 기반 난수 생성기입니다. 시스템의 엔트로피 소스(예: /dev/urandom)를 사용하여 비결정적 난수를 생성하며, 주로 난수 엔진의 시드 생성이나 암호학적 용도로 사용됩니다.
1. random_device 기본
std::random_device는 함수 호출 연산자 하나만 정의된 아주 단순한 인터페이스를 가지고 있지만, 그 내부는 운영체제의 엔트로피 소스에 직접 접근해 예측 불가능한 값을 만들어낸다는 점에서 다른 난수 생성기들과 근본적으로 다릅니다. 가장 기본적인 사용법은 아래처럼 rd()를 그냥 호출해 32비트 부호 없는 정수를 얻는 것이지만, 이 값 자체를 반복적으로 대량 생성하는 것은 다음 절에서 설명할 이유로 권장되지 않습니다.
#include <iostream>
#include <random>
int main() {
std::random_device rd;
// 난수 생성 (32비트 unsigned int)
for (int i = 0; i < 5; ++i) {
std::cout << rd() << std::endl;
}
return 0;
}
이 프로그램을 실행할 때마다 아래처럼 매번 다른 값이 나오는데, 이는 random_device가 시스템의 진짜 엔트로피(하드웨어 잡음, 인터럽트 타이밍 등)를 사용하기 때문입니다.
3847291038
1029384756
2938475610
4756102938
1847562903
시드 생성
random_device의 실질적인 용도는 난수를 직접 뽑아 쓰는 것이 아니라, mt19937 같은 빠른 의사 난수 엔진의 시드를 안전하게 초기화하는 것입니다. 아래 예제처럼 std::mt19937 gen{rd()};로 딱 한 번만 하드웨어 엔트로피를 사용해 엔진을 초기화한 뒤에는, 그 엔진에서 반복적으로 값을 뽑아 쓰는 것이 훨씬 빠르고 실용적입니다. std::uniform_int_distribution을 함께 사용하면 엔진이 생성하는 원시 비트열을 원하는 범위(예: 1부터 100까지)의 정수로 매끄럽게 변환할 수 있습니다.
#include <random>
#include <iostream>
int main() {
// random_device로 시드 생성
std::random_device rd;
// mt19937 엔진 초기화
std::mt19937 gen{rd()};
// 균등 분포
std::uniform_int_distribution<> dist{1, 100};
// 난수 생성 (빠름)
for (int i = 0; i < 10; ++i) {
std::cout << dist(gen) << " ";
}
std::cout << std::endl;
return 0;
}
출력:
42 17 89 3 56 91 28 64 11 73
2. 엔트로피
random_device가 정말로 예측 불가능한 하드웨어 난수를 제공하는지, 아니면 내부적으로 의사 난수 알고리즘으로 대체되어 있는지는 플랫폼마다 다를 수 있습니다. 표준은 이를 확인할 수 있는 entropy()라는 메서드를 제공하지만, 이 값의 신뢰도 자체도 구현에 따라 다르다는 점을 감안해서 사용해야 합니다.
entropy() 메서드
rd.entropy()는 해당 random_device가 제공하는 엔트로피의 양을 비트 단위로 추정해 반환합니다. 이 값이 0이면 표준 구현이 실제 하드웨어 엔트로피 소스를 찾지 못해 결정적인 의사 난수로 대체하고 있다는 신호이고, 0보다 크면(보통 32) 실제로 비결정적인 하드웨어 소스를 사용하고 있다는 뜻입니다. 다만 이 값은 어디까지나 힌트일 뿐 표준이 정확한 값을 강제하지 않으므로, 보안이 중요한 코드에서는 entropy()의 값만 맹신하지 말고 플랫폼별 문서를 함께 확인하는 것이 안전합니다.
#include <iostream>
#include <random>
int main() {
std::random_device rd;
// 엔트로피 확인 (비트)
double entropy = rd.entropy();
std::cout << "엔트로피: " << entropy << " bits" << std::endl;
if (entropy == 0.0) {
std::cout << "의사 난수 (결정적)" << std::endl;
} else {
std::cout << "하드웨어 난수 (비결정적)" << std::endl;
}
return 0;
}
출력 (Linux):
엔트로피: 32 bits
하드웨어 난수 (비결정적)
플랫폼별 구현
주요 운영체제는 대부분 실제 하드웨어 기반 엔트로피 소스를 제공하지만, 일부 임베디드 환경에서는 이런 소스가 없어 표준 라이브러리가 의사 난수로 대체하기도 합니다. 아래 표는 주요 플랫폼에서 random_device가 어떤 소스를 사용하는지 정리한 것으로, 새로운 타겟 플랫폼에 코드를 이식할 때는 entropy() 값을 실제로 확인해보는 것이 안전합니다.
| 플랫폼 | 엔트로피 소스 | entropy() |
|---|---|---|
| Linux | /dev/urandom | 32 bits |
| Windows | CryptGenRandom | 32 bits |
| macOS | /dev/urandom | 32 bits |
| 일부 임베디드 | 의사 난수 | 0 bits |
3. 시드 시퀀스
앞서 본 std::mt19937 gen{rd()};처럼 단일 32비트 값만으로 시드를 주는 방식은 간편하지만, mt19937이 가진 방대한 내부 상태를 완전히 초기화하기에는 부족할 수 있습니다. std::seed_seq는 여러 개의 시드 값을 받아 이를 골고루 섞어 엔진의 전체 상태를 더 고르게 채워주는 도구입니다.
단일 시드 vs 여러 시드
mt19937은 내부적으로 624개의 32비트 정수(약 19,968비트)로 이루어진 거대한 상태를 가지고 있는데, 단일 시드로 초기화하면 그중 32비트만 실제 무작위 정보이고 나머지는 알고리즘이 결정론적으로 채워 넣습니다. 아래 예제의 gen2처럼 mt19937::state_size개의 시드 값을 random_device로 채운 뒤 std::seed_seq로 감싸서 초기화하면, 훨씬 많은 실제 엔트로피가 내부 상태에 반영되어 특히 암호학적으로 민감하거나 통계적 품질이 중요한 시뮬레이션에서 더 신뢰할 수 있는 초기화가 됩니다.
#include <random>
#include <array>
#include <algorithm>
int main() {
std::random_device rd;
// ❌ 단일 시드 (32비트만 사용)
std::mt19937 gen1{rd()};
// ✅ 여러 시드 (더 좋은 초기화)
std::array<unsigned int, std::mt19937::state_size> seedData;
std::generate(seedData.begin(), seedData.end(), std::ref(rd));
std::seed_seq seq(seedData.begin(), seedData.end());
std::mt19937 gen2{seq};
return 0;
}
설명:
mt19937의 상태 크기는 624개의 32비트 정수 (19,968비트)- 단일 시드는 32비트만 사용 (나머지는 알고리즘으로 채움)
- 여러 시드는 더 많은 엔트로피 제공
4. 실전 예제
random_device가 실무에서 가장 많이 쓰이는 세 가지 시나리오, 즉 암호 키 생성, UUID 생성, 무작위 토큰 생성을 코드로 살펴보겠습니다. 세 예제 모두 예측 불가능성이 핵심 요구사항이라는 공통점이 있습니다.
예제 1: 암호학적 난수
암호화 키처럼 절대 예측되어서는 안 되는 값을 만들 때는 mt19937 같은 의사 난수 엔진보다 random_device를 직접 사용하는 편이 더 안전합니다. 아래 generateKey는 요청받은 바이트 수만큼 rd() % 256으로 각 바이트를 채워 원시 바이트 배열을 만드는데, 실제 프로덕션 암호화 코드에서는 이보다 더 엄격한 암호학적 안전성 검증을 거친 라이브러리(OpenSSL 등)를 사용하는 것이 일반적이지만, random_device가 왜 암호학적 용도에 적합한 재료가 되는지를 보여주는 예로는 충분합니다.
#include <random>
#include <vector>
#include <iostream>
#include <iomanip>
std::vector<unsigned char> generateKey(size_t length) {
std::random_device rd;
std::vector<unsigned char> key(length);
for (auto& byte : key) {
byte = static_cast<unsigned char>(rd() % 256);
}
return key;
}
int main() {
// 16바이트 키 생성
auto key = generateKey(16);
std::cout << "암호 키: ";
for (auto byte : key) {
std::cout << std::hex << std::setw(2) << std::setfill('0')
<< static_cast<int>(byte);
}
std::cout << std::endl;
return 0;
}
출력:
암호 키: 3f7a9b2c8d1e4f6a5b3c9d2e7f1a4b8c
예제 2: UUID 생성
버전 4 UUID는 대부분의 비트가 무작위 값으로 채워지는 표준 식별자 형식으로, 데이터베이스 기본 키나 분산 시스템의 고유 ID로 흔히 쓰입니다. 아래 generateUUID는 random_device로 시드를 얻은 mt19937과 uniform_int_distribution을 이용해 8-4-4-4-12 형식의 16진수 문자열을 조립하는데, 버전을 나타내는 “4”와 변형(variant)을 나타내는 dist2{8, 11}의 결과를 정해진 위치에 고정으로 삽입해 RFC 4122 형식을 정확히 따르도록 합니다. 실무에서는 이런 저수준 조립보다 검증된 UUID 라이브러리를 쓰는 것이 안전하지만, UUID의 각 자리가 어떻게 채워지는지 이해하는 데는 이 예제가 좋은 출발점이 됩니다.
#include <random>
#include <sstream>
#include <iomanip>
#include <iostream>
std::string generateUUID() {
std::random_device rd;
std::mt19937 gen{rd()};
std::uniform_int_distribution<> dist{0, 15};
std::uniform_int_distribution<> dist2{8, 11};
std::ostringstream oss;
oss << std::hex;
// 8-4-4-4-12 형식
for (int i = 0; i < 8; ++i) oss << dist(gen);
oss << "-";
for (int i = 0; i < 4; ++i) oss << dist(gen);
oss << "-4"; // 버전 4
for (int i = 0; i < 3; ++i) oss << dist(gen);
oss << "-";
oss << dist2(gen); // 변형 (8, 9, a, b)
for (int i = 0; i < 3; ++i) oss << dist(gen);
oss << "-";
for (int i = 0; i < 12; ++i) oss << dist(gen);
return oss.str();
}
int main() {
for (int i = 0; i < 3; ++i) {
std::cout << generateUUID() << std::endl;
}
return 0;
}
출력:
550e8400-e29b-41d4-a716-446655440000
6ba7b810-9dad-11d1-80b4-00c04fd430c8
3d813cbb-47fb-32ba-91df-831e1593ac29
예제 3: 토큰 생성
API 인증 토큰이나 임시 비밀번호처럼 사람이 다루기 쉬운 문자열 형태의 난수가 필요할 때는, 숫자와 영문자로 구성된 문자 집합에서 무작위로 골라 조합하는 방식이 흔히 쓰입니다. 아래 generateToken은 숫자·대문자·소문자를 합친 62개 문자 집합에서 uniform_int_distribution으로 매번 하나씩 골라 원하는 길이의 토큰을 만드는데, token.reserve(length)로 미리 메모리를 확보해두어 문자를 하나씩 추가할 때마다 발생할 수 있는 재할당을 피하는 세심한 최적화도 함께 담겨 있습니다.
#include <random>
#include <string>
#include <iostream>
std::string generateToken(size_t length) {
std::random_device rd;
std::mt19937 gen{rd()};
const std::string chars =
"0123456789"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz";
std::uniform_int_distribution<> dist{0, static_cast<int>(chars.size() - 1)};
std::string token;
token.reserve(length);
for (size_t i = 0; i < length; ++i) {
token += chars[dist(gen)];
}
return token;
}
int main() {
// 32자 토큰 생성
std::cout << "토큰: " << generateToken(32) << std::endl;
return 0;
}
출력:
토큰: 7aB3xK9mP2qW5nL8vC1dF4jH6rT0yU3z
5. 자주 발생하는 문제
random_device는 강력한 도구지만, 그 강력함이 나오는 원천(진짜 하드웨어 엔트로피)이 곧 성능·이식성 측면의 제약으로 이어지기도 합니다. 성능, 플랫폼 의존성, 시드 품질, 재현성이라는 네 가지 실무 이슈를 살펴봅니다.
문제 1: 성능
random_device가 값을 하나 생성할 때마다 운영체제 커널의 엔트로피 소스에 접근하는 시스템 호출이 발생할 수 있어, 순수 메모리 연산으로 동작하는 mt19937 같은 엔진보다 수십에서 수백 배 느립니다. 아래 벤치마크에서 100만 번 호출 기준 random_device가 약 5초, mt19937이 약 50밀리초로 100배 가까이 차이 나는 것을 확인할 수 있는데, 이 격차 때문에 대량의 난수가 필요한 시뮬레이션이나 게임 로직에서는 random_device를 시드 생성 한 번에만 쓰고 실제 반복 호출은 반드시 엔진에 맡겨야 합니다.
#include <random>
#include <chrono>
#include <iostream>
void benchmarkRandomDevice() {
std::random_device rd;
auto start = std::chrono::high_resolution_clock::now();
// ❌ random_device 직접 사용 (매우 느림)
for (int i = 0; i < 1000000; ++i) {
volatile unsigned int r = rd();
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
std::cout << "random_device: " << duration.count() << "ms" << std::endl;
}
void benchmarkMT19937() {
std::random_device rd;
std::mt19937 gen{rd()};
auto start = std::chrono::high_resolution_clock::now();
// ✅ mt19937 사용 (빠름)
for (int i = 0; i < 1000000; ++i) {
volatile unsigned int r = gen();
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
std::cout << "mt19937: " << duration.count() << "ms" << std::endl;
}
int main() {
benchmarkRandomDevice(); // ~5000ms
benchmarkMT19937(); // ~50ms
return 0;
}
출력:
random_device: 5234ms
mt19937: 47ms
문제 2: 플랫폼 의존성
C++ 표준은 random_device가 반드시 진짜 비결정적 하드웨어 난수를 제공해야 한다고 강제하지 않기 때문에, 하드웨어 엔트로피 소스가 없는 일부 임베디드 환경이나 오래된 컴파일러 구현에서는 결정적인 의사 난수로 조용히 대체될 수 있습니다. 아래 예제처럼 entropy()가 0을 반환하면 이런 상황을 감지해 경고를 남기고, 필요하다면 고해상도 타이머 값을 대안 시드로 사용하는 방어적 코드를 작성해둘 수 있습니다. 다만 이런 대안은 하드웨어 엔트로피만큼 안전하지 않으므로, 보안이 중요한 코드라면 대상 플랫폼에서 entropy()가 실제로 어떤 값을 반환하는지 배포 전에 반드시 확인해야 합니다.
#include <random>
#include <iostream>
int main() {
std::random_device rd;
// 엔트로피 확인
double entropy = rd.entropy();
if (entropy == 0.0) {
std::cerr << "경고: random_device가 의사 난수를 사용합니다." << std::endl;
std::cerr << "플랫폼: 하드웨어 난수 미지원" << std::endl;
// 대안: 시간 기반 시드
auto now = std::chrono::high_resolution_clock::now();
auto seed = now.time_since_epoch().count();
std::mt19937 gen{static_cast<unsigned int>(seed)};
} else {
std::cout << "하드웨어 난수 사용 (엔트로피: " << entropy << " bits)" << std::endl;
}
return 0;
}
문제 3: 시드 품질
3절에서 다룬 단일 시드와 여러 시드의 차이를 다시 세 가지 코드 형태로 비교하는 예제입니다. poorSeeding처럼 32비트 시드 하나만 주는 방식은 간편하지만 상태 공간을 충분히 채우지 못하고, goodSeeding처럼 state_size만큼 정확히 시드를 채우면 이론적으로 가장 철저한 초기화가 되지만 코드가 다소 장황해지며, simpleGoodSeeding처럼 8개(256비트) 정도의 시드만 사용하는 절충안도 실무에서 널리 쓰이는 합리적인 타협점입니다. 통계적 시뮬레이션처럼 초기화 품질이 결과에 큰 영향을 주는 상황이 아니라면, 세 번째 방식 정도로도 대부분의 실무 요구를 충분히 만족시킵니다.
#include <random>
#include <array>
#include <algorithm>
// ❌ 단일 시드 (품질 낮음)
void poorSeeding() {
std::random_device rd;
std::mt19937 gen{rd()}; // 32비트만 사용
}
// ✅ 여러 시드 (품질 높음)
void goodSeeding() {
std::random_device rd;
// mt19937 상태 크기만큼 시드 생성
std::array<unsigned int, std::mt19937::state_size> seedData;
std::generate(seedData.begin(), seedData.end(), std::ref(rd));
std::seed_seq seq(seedData.begin(), seedData.end());
std::mt19937 gen{seq};
}
// ✅ 간단한 여러 시드
void simpleGoodSeeding() {
std::random_device rd;
// 8개 시드 (256비트)
std::array<unsigned int, 8> seedData;
for (auto& seed : seedData) {
seed = rd();
}
std::seed_seq seq(seedData.begin(), seedData.end());
std::mt19937 gen{seq};
}
문제 4: 재현성
random_device로 매번 다른 시드를 사용하면 프로덕션에서는 바람직하지만, 버그를 재현하고 디버깅해야 할 때는 오히려 방해가 됩니다. 아래 nonReproducible은 실행할 때마다 다른 결과를 내어 특정 입력에서만 발생하는 버그를 다시 재현하기 어렵게 만드는 반면, reproducible 함수는 debug 플래그에 따라 고정된 시드(42)를 쓸지 random_device 기반의 진짜 난수를 쓸지 선택할 수 있게 설계되어 있습니다. 이런 식으로 디버그 모드와 프로덕션 모드의 시드 전략을 분리해두면, 문제가 재현되는 특정 시드를 찾아 고정해두고 반복적으로 디버깅할 수 있어 개발 생산성이 크게 올라갑니다.
#include <random>
#include <iostream>
// ❌ 재현 불가 (디버깅 어려움)
void nonReproducible() {
std::random_device rd;
std::mt19937 gen{rd()}; // 매번 다른 시드
std::uniform_int_distribution<> dist{1, 100};
std::cout << dist(gen) << std::endl; // 매번 다른 결과
}
// ✅ 재현 가능 (디버깅 용이)
void reproducible(bool debug = false) {
std::mt19937 gen;
if (debug) {
gen.seed(42); // 고정 시드
} else {
std::random_device rd;
gen.seed(rd()); // 랜덤 시드
}
std::uniform_int_distribution<> dist{1, 100};
std::cout << dist(gen) << std::endl;
}
int main() {
std::cout << "디버그 모드:" << std::endl;
reproducible(true); // 항상 같은 결과
reproducible(true); // 항상 같은 결과
std::cout << "\n프로덕션 모드:" << std::endl;
reproducible(false); // 매번 다른 결과
reproducible(false); // 매번 다른 결과
return 0;
}
6. 실전 예제: 난수 유틸리티
지금까지 다룬 시드 초기화, 분포, 문자열·바이트 생성을 하나의 재사용 가능한 클래스로 묶은 종합 예제입니다. RandomUtils는 생성자에서 8개의 시드로 seed_seq를 구성해 mt19937 엔진을 한 번만 고품질로 초기화해두고, 이후 randomInt, randomDouble, randomString, randomBytes, shuffle이라는 다섯 가지 메서드가 모두 이 하나의 엔진을 공유해서 사용합니다. 이렇게 엔진 초기화 로직을 클래스 안에 캡슐화해두면, 사용하는 쪽에서는 매번 random_device와 시드 생성 코드를 반복해서 작성할 필요 없이 RandomUtils 인스턴스 하나로 프로젝트 전반의 난수 요구를 일관되게 처리할 수 있습니다.
#include <random>
#include <string>
#include <vector>
#include <array>
#include <algorithm>
class RandomUtils {
std::mt19937 gen;
public:
// 생성자: 고품질 시드
RandomUtils() {
std::random_device rd;
std::array<unsigned int, 8> seedData;
std::generate(seedData.begin(), seedData.end(), std::ref(rd));
std::seed_seq seq(seedData.begin(), seedData.end());
gen.seed(seq);
}
// 범위 내 정수
int randomInt(int min, int max) {
std::uniform_int_distribution<> dist{min, max};
return dist(gen);
}
// 범위 내 실수
double randomDouble(double min, double max) {
std::uniform_real_distribution<> dist{min, max};
return dist(gen);
}
// 랜덤 문자열
std::string randomString(size_t length) {
const std::string chars =
"0123456789"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz";
std::uniform_int_distribution<> dist{0, static_cast<int>(chars.size() - 1)};
std::string result;
result.reserve(length);
for (size_t i = 0; i < length; ++i) {
result += chars[dist(gen)];
}
return result;
}
// 랜덤 바이트
std::vector<unsigned char> randomBytes(size_t length) {
std::uniform_int_distribution<> dist{0, 255};
std::vector<unsigned char> bytes(length);
for (auto& byte : bytes) {
byte = static_cast<unsigned char>(dist(gen));
}
return bytes;
}
// 배열 셔플
template<typename T>
void shuffle(std::vector<T>& vec) {
std::shuffle(vec.begin(), vec.end(), gen);
}
};
int main() {
RandomUtils rng;
// 정수
std::cout << "랜덤 정수 (1-100): " << rng.randomInt(1, 100) << std::endl;
// 실수
std::cout << "랜덤 실수 (0-1): " << rng.randomDouble(0.0, 1.0) << std::endl;
// 문자열
std::cout << "랜덤 문자열: " << rng.randomString(16) << std::endl;
// 바이트
auto bytes = rng.randomBytes(8);
std::cout << "랜덤 바이트: ";
for (auto byte : bytes) {
std::cout << std::hex << std::setw(2) << std::setfill('0')
<< static_cast<int>(byte) << " ";
}
std::cout << std::endl;
// 셔플
std::vector<int> vec = {1, 2, 3, 4, 5};
rng.shuffle(vec);
std::cout << "셔플: ";
for (int x : vec) std::cout << x << " ";
std::cout << std::endl;
return 0;
}
위 main 함수를 실행하면 정수, 실수, 문자열, 바이트, 셔플 결과가 다음처럼 출력됩니다(값은 실행마다 달라집니다).
랜덤 정수 (1-100): 73
랜덤 실수 (0-1): 0.642857
랜덤 문자열: aB7xK3mP9qW2nL5v
랜덤 바이트: 3f 7a 9b 2c 8d 1e 4f 6a
셔플: 3 1 5 2 4
정리
핵심 요약
- random_device: 하드웨어 기반 비결정적 난수
- 용도: 시드 생성, 암호학적 난수
- 성능: 느림 (시드로만 사용)
- entropy(): 비결정성 측정 (0이면 의사 난수)
- 시드 품질: 여러 시드로 초기화
- 재현성: 디버깅 시 고정 시드 사용
random_device vs mt19937
| 특징 | random_device | mt19937 |
|---|---|---|
| 속도 | 매우 느림 | 빠름 |
| 품질 | 하드웨어 난수 | 의사 난수 |
| 용도 | 시드 생성 | 일반 난수 |
| 재현성 | 불가 | 가능 (시드 고정) |
| 플랫폼 | 의존적 | 독립적 |
실전 팁
사용 원칙:
random_device는 시드 생성에만 사용- 실제 난수는
mt19937같은 엔진 사용 - 암호학적 용도는
random_device직접 사용 고려 - 여러 시드로 엔진 초기화 (품질 향상)
성능:
random_device는 시스템 콜 사용 (느림)- 엔진은 메모리 기반 (빠름)
- 대량 난수는 반드시 엔진 사용
디버깅:
- 디버깅 시 고정 시드 사용
- 프로덕션에서는
random_device시드 entropy()확인으로 플랫폼 검증
다음 단계
관련 글
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Everything about C++ random_device : from basic concepts to practical applications.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
이 글에서 다루는 키워드 (관련 검색어)
C++, random, device, seed, C++11 등으로 검색하시면 이 글이 도움이 됩니다.