정확한 계산을 위한 BigDecimal 사용하기

바인하·2024년 1월 7일
1
post-thumbnail

자바에는 BigDecimal 클래스가 존재한다.
현 프로젝트에서 주문 모듈에 속해 일을 하면서, 여러 가격 정보들을 BigDecimal 타입으로 관리하는 것을 확인할 수 있었다. 또한, 최근에 읽은 이펙티브 자바 아이템 60 [정확한 답이 필요하다면 float 와 double 은 피하라] 내용 중 일부로도, 아래와 같은 해답을 제시하고 있다.

금융계산에는 BigDecimal, int 혹은 long 을 사용하라

즉, 정확한 결과가 필요할 때는 BigDecimal 을 사용해야 한다.
이 게시물에서는 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).

  • Immutable, arbitrary-precision signed decimal numbers.
    : 불변의, 임의 정밀도와 부호를 가진 10진수
  • arbitrary precision integer unscaled value and a 32-bit integer scale
    : 임의 정밀도를 나타내는 unscaled value와 32bit의 scale로 이루어짐

여기서 임의 정밀도를 나타내는 unscaled value 와 scale 은 무엇을 뜻할까?

  • unscaled value는 정수부를 표현하고, scale은 소수점 아래 자릿수를 표현한다.
    ex) BigDecimal 3.14의 경우 unscaled value는 314이고 scale은 2가 된다.

여기서 임의 정밀도라는 개념이 나오게 되는데 조금 더 자세히 알아보자!

임의 정밀도란?

  • 정수나 소수를 정확히 표현하기 위해 제한이 없는 정밀도
  • 정밀 계산이 필요한 금융, 과학, 통계 분야에서 유용하게 사용된다.
  • 큰 숫자 = [int] + [int] + [int] ... 의 형태로, 정수를 숫자 배열로 간주하고 자릿수 단위로 쪼개서 배열형태로 표현함으로써 무제한 자릿수의 정수를 담는 방식이라고 생각하면 된다.

그렇다면, BigDecimal 을 사용할 때 주의해야 할 점은 무엇일까?

1. BigDecimal 초기화할 때, String 으로 초기화하자!

    // double 타입을 그대로 초기화하면 기대값과 다른 값을 가진다.
    // 0.01000000000000000020816681711721685132943093776702880859375
    new BigDecimal(0.01);
    
    // 문자열로 초기화하면 정상 인식
    // 0.01
    new BigDecimal("0.01");
    
    // 위와 동일한 결과, double#toString을 이용하여 문자열로 초기화
    // 0.01
    BigDecimal.valueOf(0.01);

double 타입을 그대로 초기화하면, 기댓값과 다른 결과를 낳는데 왜 double 타입을 받는 생성자가 있을까? 하는 의문이 들어 BigDecimal 클래스를 살펴보았다.

  1. 이 생성자의 결과는 예측하기 어려울 수 있습니다.
    예를 들어, Java에서 new BigDecimal(0.1)을 작성하면 0.1과 정확히 일치하는 BigDecimal이 생성되는 것으로 생각할 수 있지만, 실제로는 0.1000000000000000055511151231257827021181583404541015625와 같이 나타납니다. 이는 0.1을 double로 정확히 표현할 수 없기 때문에 발생하는 현상입니다.
  2. 반면에 String 생성자는 예측 가능합니다. new BigDecimal("0.1")을 작성하면 기대한 대로 0.1과 정확히 일치하는 BigDecimal이 생성됩니다. 따라서 일반적으로 이 생성자 대신 BigDecimal(String) 를 사용하는 것이 권장됩니다.

주석을 통해서도 확인할 수 있듯이, BigDecimal 클래스에서도 String 생성자를 사용하는 것을 권장하고 있다.

그래서 double 생성자 존재 이유에 대한 결론을 내리면,
BigDecimal 클래스의 double 타입을 받는 생성자는 부동 소수점 오차를 피하고 정확한 10진수 연산을 위해 제공하고 있다.
명시적으로 이런 생성자를 제공함으로써 double -> BigDecimal 로 변환하도록 도와주긴 하지만, 이미 double 자체가 정확한 10진수로 변환될 수 없는 부정확한 값을 가지고 있기 때문에 정확성을 보장할 수 없다고 한다.

2. BigDecimal은 기본 타입이 아닌 오브젝트이기 때문에 동등 비교 연산 시 유의하자!

  • equals() : unscaled value, scale 을 모두 비교
  • compareTo() : 소수점 맨 끝의 0을 무시하고 값만을 비교하고 싶을 때 사용
    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 출력
profile
되면 한다

0개의 댓글