자바에는 BigDecimal 클래스가 존재한다.
현 프로젝트에서 주문 모듈에 속해 일을 하면서, 여러 가격 정보들을 BigDecimal 타입으로 관리하는 것을 확인할 수 있었다. 또한, 최근에 읽은 이펙티브 자바 아이템 60 [정확한 답이 필요하다면 float 와 double 은 피하라] 내용 중 일부로도, 아래와 같은 해답을 제시하고 있다.
금융계산에는 BigDecimal, int 혹은 long 을 사용하라
즉, 정확한 결과가 필요할 때는 BigDecimal 을 사용해야 한다.
이 게시물에서는 BigDecimal 의 개념과 사용 시 유의해야 할 점들 위주로 정리를 해보려고 한다.
공식문서 설명은 아래와 같이 설명하고 있는데, 이 중 핵심만 뽑아내보겠다.
Immutable, arbitrary-precision signed decimal numbers.
A BigDecimal consists of an arbitrary precision integer unscaled value and a 32-bit integer scale.
If the scale is zero or positive, the scale is the number of digits to the right of the decimal point.
If the scale is negative, the unscaled value of the number is multiplied by ten to the power of the negation of the scale. The value of the number represented by the BigDecimal is therefore (unscaledValue × 10-scale).
불변의, 임의 정밀도와 부호를 가진 10진수
unscaled value
와 32bit의 scale
로 이루어짐여기서 임의 정밀도를 나타내는 unscaled value 와 scale 은 무엇을 뜻할까?
unscaled value
는 정수부를 표현하고, scale
은 소수점 아래 자릿수를 표현한다.unscaled value
는 314이고 scale
은 2가 된다.여기서 임의 정밀도라는 개념이 나오게 되는데 조금 더 자세히 알아보자!
// double 타입을 그대로 초기화하면 기대값과 다른 값을 가진다.
// 0.01000000000000000020816681711721685132943093776702880859375
new BigDecimal(0.01);
// 문자열로 초기화하면 정상 인식
// 0.01
new BigDecimal("0.01");
// 위와 동일한 결과, double#toString을 이용하여 문자열로 초기화
// 0.01
BigDecimal.valueOf(0.01);
double 타입을 그대로 초기화하면, 기댓값과 다른 결과를 낳는데 왜 double 타입을 받는 생성자가 있을까? 하는 의문이 들어 BigDecimal 클래스를 살펴보았다.
- 이 생성자의 결과는 예측하기 어려울 수 있습니다.
예를 들어, Java에서 new BigDecimal(0.1)을 작성하면 0.1과 정확히 일치하는 BigDecimal이 생성되는 것으로 생각할 수 있지만, 실제로는 0.1000000000000000055511151231257827021181583404541015625와 같이 나타납니다. 이는 0.1을 double로 정확히 표현할 수 없기 때문에 발생하는 현상입니다.- 반면에 String 생성자는 예측 가능합니다. new BigDecimal("0.1")을 작성하면 기대한 대로 0.1과 정확히 일치하는 BigDecimal이 생성됩니다. 따라서 일반적으로 이 생성자 대신 BigDecimal(String) 를 사용하는 것이 권장됩니다.
주석을 통해서도 확인할 수 있듯이, BigDecimal 클래스에서도 String 생성자를 사용하는 것을 권장하고 있다.
그래서 double 생성자 존재 이유에 대한 결론을 내리면,
BigDecimal 클래스의 double 타입을 받는 생성자는 부동 소수점 오차를 피하고 정확한 10진수 연산을 위해 제공하고 있다.
명시적으로 이런 생성자를 제공함으로써 double -> BigDecimal 로 변환하도록 도와주긴 하지만, 이미 double 자체가 정확한 10진수로 변환될 수 없는 부정확한 값을 가지고 있기 때문에 정확성을 보장할 수 없다고 한다.
final BigDecimal b1 = new BigDecimal("7.10");
final BigDecimal b2 = new BigDecimal("7.1");
System.out.println(b1 == b2);
// false _ 주소값 비교
System.out.println(b1.equals(b2));
// false _ 7.10과 7.1은 논리적으로 같은 수일지라도, 소수점아래 자릿수가 다르므로 equals의 결과는 false
System.out.println(b1.compareTo(b2));
// 0 (true) _ 0을 무시하고 7.1 로만 비교하니까 0 출력