Java에서 Random 클래스를 사용하지 말자
SonarLint에서 수상쩍은 경고가 떴다
SonarLint 혹은 SonarQube를 쓰는 경우, Random
객체를 사용했을 때 다음과 같은 이슈를 본 사람이 있을지도 모른다.
"Random" objects should be reused
위의 경고는 Random rand = new Random();
같이 코드를 작성했을 때 발생한다. 이런 경고가 발생하는 이유는 난수를 발생할 때 사용할 시드를 그때마다 생성시키는 비효율성 문제와 의사난수의 문제 때문에 발생한다.
Random
은 기본적으로 new
를 통해 새로운 인스턴스가 생성될 때마다 새로운 시드가 생성되고 객체에 저장된다. 따라서 한번 생성한 Random
인스턴스는 객체 내에서 재활용을 하는것이 좋다. 또 다른 문제로는 Random
이 가지는 근본적인 문제인데, Random
은 무작위 난수를 생성하지 못한다는 점이다.
Java의 Random
은 무작위 난수가 아닌 의사 난수를 생성하는 객체임을 명확히 알아야한다. 여기서 의사(擬似)란 '실제와 비슷한' 내지 '가짜'라는 의미를 내포하고 있는 단어다. 즉, 의사 난수란 난수처럼 보이는 값이라고 볼 수 있다.
입력이 있어야만 출력이 발생하는 컴퓨터는, 사람과 같이 외부 요인을 배제한 완전한 무작위의 행위를 실행 할 수 없다. 따라서 컴퓨터는 기본적으로 무작위 난수를 생성할 방법이 없다. 이런 이유 때문에 컴퓨터가 자체적으로 난수를 사용해야할 경우에는, 난수처럼 보이는 값들을 생성해서 사용하는게 일반적이다. 그러나 의사 난수는 어디까지나 '난수처럼 보이는 값'일 뿐, 실제로는 규칙성이 있는 값이다. 따라서 이런 값은 보안과 관련된 곳에 사용하면 안된다. 그렇다면 실제로 이 위험성을 알아보기 위해 Random
객체를 이용해 난수 값을 생성하고 확인해 보자.
위의 결과가 보이는가? Seed
값만 알아낼 수 있다면, 난수 값을 유추하는 게 어려운 일이 아니라는 것을 확실히 알 수 있을 것이다. 그렇다면 다음은 SecureRandom
객체를 통해 난수 값을 생성해 보자.
Random
객체를 이용해 난수를 생성한 결과 값과의 차이는 명백해 보인다. 둘 다 10번의 작업을 시행하는 동안, 동일한 Seed
값을 사용했고 (SecureRandom
객체는 byte[]
값을 seed
로 쓰기에 부득이 하게 변경하긴 했지만, 10번의 작업 내에서는 항상 동일한 Seed
값을 사용했다.) 그 결과 SecureRandom
객체를 사용했을 때, 더 난수에 가까운 값을 생성해 냈다는 사실을 확인할 수 있다.
그렇다면 해결법은?
가장 간단한 방법은 Random
클래스를 상속받아 구현한 SecureRandom
클래스를 사용하는 것이다. 시스템 시간을 기본 시드로 사용하는 Random
클래스는, 인스턴스가 생성된 시간만 알아낼 수 있다면 난수를 예측하는 것은 비교적 간단한 편이다. 반면 SecureRandom
클래스는 운영체제의 무작위 데이터를 가져와 시드로 사용하기 때문에 시드 예측이 어렵다는 장점이 있다. 또한 48bit의 결과 값을 생성하는 Random
과 달리, SecureRandom
은 128bit의 결과 값을 생성하므로 생성할 수 있는 결과 수가 훨씬 많다. 이 때문에 SecureRandom
은 Random
에 비해 난수 값을 유추해 내는 것이 더욱 어렵다. 또한, 하나의 클래스 내부에서는 하나의 SecureRandom
객체만 생성해서 사용하는 것도 잊지 말자.
정리하며
이번에 암호화 라이브러리를 개발하면서 Random
클래스에서 발생한 SonarLint의 오류가 무엇인지 궁금했었는데, 이번 기회에 SecureRandom
의 존재도 알게 되고 보안과 관련된 정보를 배우는데 큰 도움이 되었었다.