티스토리 뷰
- 내가 운영/개발 하는 서비스에선 사용자나, 구분이 필요한 무언가를 위해 13자리의 난수를 생성해 아이디를 부여한다. 난수 생성기를 올바르게 사용하지 못 한다면, 동일한 아이디가 생성되고, 그 아이디로 특정 데이터를 insert했을 때 DB에선 PK 에러가 발생한다. 빼도 박도 못하는 서버 에러다.
- 최근 로스트아크에서 카드 확률에 대한 논쟁이 있었었다.
영웅 카드 팩 230개를 개봉했다. 첫 개봉에서 사이카 카드 5장, 피요르긴 카드 5장이 등장했다. 190개를 개봉했지만 똑같이 사이카, 피요르긴 카드 5장씩만 나타났다.
스마일게이트의 설명에 따르면 해당 사례는 시스템 구조상 14억분의 1 확률로 발생할 수 있는 버그 현상이라고 설명했다.
출처 : 게임톡(https://www.gametoc.co.kr)
이게 말이 되는 확률인가 싶지만, 정말 발생할 수도 있는 일이다. 사실 위 같은 문제는 버그가 아닐 수도 있지만(진짜 극악의 확률로 나올 수 있으니), 스마게가 버그라 인정했으니 난수 생성기의 문제일 가능성이 가장 크다.
그렇다면 난수를 어떻게 생성하면 좋을까? 완전한 난수를 생성할 수 있을까? 란 생각에서 자바에서 난수 생성하는 방법에 대해 알아봤다.
1. Random
자바에서 가장 일반적으로 쓰이는 난수 생성 클래스는 java.util.Random 클래스다. 난수를 생성할때 생성자로 별도의 Seed값을 받으며, 생성자 없이 디폴트로 생성할때는 System.nanoTime() 을 기반으로 난수를 생성한다.
Seed 값을 받으면, pseudorandom number generator(의사 난수 생성기)를 이용해 통계적으로는 무작위이지만 실제로는 무작위가 아닌 일련의 숫자를 생성한다. Random 클래스의 의사 난수 생성기는 밀리세컨드 단위의 시간을 고려하는 비교적 간단한 알고리즘을 사용한다.
다만, 여기서 문제가 되는건 같은 seed를 입력해 Random 클래스를 초기화하면, 의사 난수 생성기도 매번 초기화되면서 이전과 동일한 매서드 호출 순서가 적용되어 같은 숫자를 생성한다. 이는 Random 클래스의 의사 난수 생성기의 특성이다.
public static void randomTest() {
Random ran = new Random(5);
for(int i = 0; i < 10; i++) {
System.out.printf("%.5f ", ran.nextDouble());
}
ran = null;
System.out.println();
}
public static void main(String[] args) {
for(int i = 0; i < 5; i++) {
randomTest();
}
}
위와 같이 코드를 작성하면, 아래와 같이 반복문이 실행 될 때마다 같은 난수를 생성한다.
위 문제는 사실 Random 클래스를 글로벌로 선언해주면 해결이 된다. 의사 난수 생성기를 초기화 시키지 않게끔 구현하면 되기 때문이다.
위와같이 단순한 의사 난수 생성기를 사용하기 때문에, 엄격한 보안이 필요한 암호화하는데는 적합하지 않다. 자바 Docs에서도 이와 관련해 언급한 내용이 있다.
Instances of java.util.Random are not cryptographically secure. Consider instead using SecureRandom to get a cryptographically secure pseudo-random number generator for use by security-sensitive applications.
요약하면 암호학적으로 더 안전한 의사난수 생성기를 사용하는 SecureRandom을 써라이다.
2. SecureRandom
위에서 언급했지만, 좀 더 강력한 의사난수 생성기를 이용해 난수를 생성하는 java.security.SecureRandom 클래스를 제공한다. 디폴트 생성자로 객체 생성이 가능하며 seed로 바이트 형식의 Array를 받는다.
Random 클래스와 다르게 SecureRandom 클래스는 아래의 코드를 실행시켰을 때, 난수가 제대로 생성되는 것을 확인할 수 있다.
public static void randomTest() {
Random ran = new SecureRandom(new byte[]{0x66, 0x67, 0x68, 0x69});
for(int i = 0; i < 10; i++) {
System.out.printf("%.5f ", ran.nextDouble());
}
ran = null;
System.out.println();
}
public static void main(String[] args) {
for(int i = 0; i < 5; i++) {
randomTest();
}
}
SecureRandom 클래스의 장점은 완전한 난수를 생성해준다는 점과, 암호학적으로도 안전하다는 점이다.
하지만 내부에서 다양한 난수 생성 과정을 거치기 때문에, Random 클래스보다 느리다는 단점이 있다. 또 일반적이진 않지만 특정 플랫폼에서 SecureRandom이 사용하는 hardware-based random number generator가 없는 경우 제대로된 암호화를 제공하지 못할 수 있다고 한다.(일반적으로는 Windows, macOS, Linux 등 JRE를 지원하는 모든 플랫폼에서 사용가능하다.)
3. 마치며
나는 Random 클래스가 시간을 기반으로 했기 때문에, 동일한 시간에 랜덤 값을 생성했을 때 동일한 값을 출력한다고 생각했었다. 하지만, 이번에 완전히 잘못된 생각이었다는 것을 알게 됐다... 그리고 세상에 마냥 안전하기만한 방법은 없다. 언제나 그만큼의 자원이 소모하는 것 같다.
참고사이트
'개발 > JAVA' 카테고리의 다른 글
[JAVA] 함수형 인터페이스 (1) | 2023.01.26 |
---|---|
[JAVA] TemporalAdjusters (0) | 2023.01.26 |
[Java] ObjectMapper (0) | 2023.01.22 |
[JAVA] Thread Safety (0) | 2023.01.21 |
[JAVA] HttpURLConnection, HttpClient, okHttp (0) | 2022.12.18 |
- Total
- Today
- Yesterday
- AWS
- 인프런
- JWT
- terraform
- cache
- 람다
- awskrug
- Log
- 스프링부트
- serverless
- lambda
- AWS EC2
- OpenAI
- elasticsearch
- springboot
- MySQL
- docker
- GIT
- java
- CloudFront
- ChatGPT
- chat GPT
- AOP
- openAI API
- OpenFeign
- S3
- EKS
- Kotlin
- Elastic cloud
- Spring
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |