📌 As-is
static Random rnd = new Random();
static int random(int n){
return Math.abs(rnd.nextInt()) % n;
}
0부터 n 사이의 무작위 정수를 생성하는 흔한 코드지만 3가지 문제점을 가지고 있다.
1️⃣ n이 그리 크지 않은 2의 제곱수라면 얼마 지나지 않아 같은 수열이 반복된다.
- 'Math.abs(rnd.nextInt())'는 int 범위 내에서 난수를 생성한다.
- 이후 '% n' 연산을 통해 0부터 n-1 까지의 범위에서 값을 생성한다.
- n이 2의 제곱수가 아니라면, n-1 이 'rnd.nextInt()' 결과를 균등하게 나누어주지 못한다.
- 따라서 random() 을 여러번 호출하면 n이 크지 않은 2의 제곱수일 경우, 동일한 수열이 반복될 수도 있다.
✒️ n이 2의 거듭제곱인 것과 균등성의 관계
처음엔 이해가 안 갔는데 생각해보면 너무 당연한 이야기였다.
nextInt() 메서드가 균등한 분포의 랜덤 값을 반환한다고 가정해보자.
0, 1, 2, 3이라는 4개의 값에 대해서 n이 2인 경우와 3인 경우로 나머지를 구해보면,
3으로 나누었을 때는 1, 2보다 0을 두배 더 자주 반환한다.
반면 n이 2의 거듭제곱일 때는 이러한 현상이 일어나지 않는다. 2의 거듭제곱 중 하나는 다른 하나로 나눌 수 있기 때문이다.
더 정확히 따지자면 nextInt()의 최대값인 Integer.MAX_VALUE와 서로소 관계(공약수가 없는 관계)인 n이 선택되어야 가장 이상적이며, 2제곱수인 형태도 보다 균등한 분포를 유지할 수 있는 것이다.
2️⃣ n이 2의 제곱수가 아니라면 몇몇 숫자가 평균적으로 더 자주 반환된다.
public static void main(String[] args) {
int n = 2 * (Integer.MAX_VALUE / 3);
int low = 0;
for (int i = 0; i < 1_000_000; ++i)
if (random(n) < n/2)
low++;
System.out.println(low);
}
위 코드는 무작위 수를 뽑아 백만 개 생성하여 중간 값보다 작은 값의 개수를 출력한다.
이상적으로는 약 50만 개가 출력되어야 하지만 실제로는 66만에 가까운 수가 나온다.
즉, 애초에 random()이 반환하는 값조차 균등하지 않다.
3️⃣ 지정한 범위 '바깥'의 수가 종종 튀어나올 수 있다.
- nextInt()가 Integer.MIN_VALUE(-2,147,483,648)를 반환하여 abs()를 수행하면 음수가 나온다.
📌 To-be
💡 표준 라이브러리를 사용하면 전문가의 지식과 다른 프로그래머들의 경험을 활용할 수 있다.
- 여러 전문가들이 문제를 고민했고, Random.nextInt(int)가 이미 해결해놨다.
- 위의 결함을 해결하려면 의사난수 생성기, 정수론, 2의 보수 계산 등에 조예가 깊어야 한다.
- 하지만 알고리즘에 능통한 다른 개발자가 설계, 구현, 검증 단계를 거쳐 개발했다.
- 릴리스된 후 20여 년 가까이 수백만의 개발자들이 사용했지만 버그가 보고된 적이 없다.
- 설령 보고되었더라도 다음 릴리스에서 수정될 것이다.
- Java 7 부터는 Random을 더 이상 사용하지 않는 게 좋다.
- Random은 LCG Algorithm을 사용하는데, 결과의 패턴이 존재하여 보안 이슈가 발생할 수 있다. → java.security.SecureRandom 대체
- ThreadLocalRandom으로 대체하면 대부분 잘 작동한다. (ThreadLocalRandom.current().nextInt(int);)
- Random보다 더 고품질의 무작위 수를 생성하면서 속도도 빠르다.
- fork-join 풀이나 병렬 stream에서는 SplittableRandom을 사용하라.
- 핵심적인 일과 크게 관련 없는 문제를 해결하느라 시간을 허비하지 않아도 된다.
- 따로 노력하지 않아도 성능이 지속해서 개선된다.
- 업계 표준 벤치마크를 사용해 성능을 확인하기 때문에 표준 라이브러리 제작자들이 더 나은 방법을 꾸준히 모색할 수밖에 없다.
- 자바 플랫폼의 라이브러리 많은 부분이 수 년에 걸쳐 지속해서 다시 작성되며, 극적으로 개선되기도 한다.
- 기능이 점점 많아진다.
- 라이브러리의 부족한 점은 개발자 커뮤니티에서 논의된 후 다음 릴리스에 추가되곤 한다.
- 라이브러리를 작성한 코드는 많은 사람에게 낯익은 코드가 된다.
📌 표준 라이브러리를 주기적으로 확인하라
- 표준 라이브러리는 메이저 릴리스마다 주목할 만한 수많은 기능이 라이브러리에 추가된다.
- 자바 프로그래머라면 적어도 익숙해져야할 패키지들이 있다.
- java.lang
- java.util
- java.io
- Collection Framework
- Stream
- java.util.concurrent
- 멀티스레드 프로그래밍 작업의 고수준 편의 기능부터, 고수준 개념을 구현하도록 돕는 저수준 요소들까지도 제공한다.
- 되도록 라이브러리를 사용하려 시도해보라.
- 라이브러리가 제공하는 기능은 유한하므로 빈 구멍이 있기 마련이다. 만약, 그렇다면 다음 선택지는 고품질의 서드파티 라이브러리가 될 것이다.
- 그럼에도 찾지 못한다면 그 때는 직접 구현하자.
책의 예시에서 Linux의 curl 명령을 쉽게 해결하는 방법을 알려준다.
public static void main(String[] args) throw IOException {
try (InputStream in = new URL(args[0]).openStream()) {
in.transferTo(System.out);
}
}
InputStream의 transferTo() 메서드로 간단하게 구현할 수 있게 되었다.