본문 바로가기
Java & Kotlin/Java

BigDecimal이 소수를 다루는 방법

by devson 2022. 12. 8.

정산 시스템과 같은 정확한 숫자 계산을 다뤄야하는 시스템에서는 Java의 숫자를 다루는 가장 기본적인 타입인 Long이나 Double을 사용하는게 아니라 BigDecimal을 사용하라고 권고한다.

 

이 포스팅에서는 왜 BigDecimal을 사용해야하는지에 대해알아보기 위해 먼저 컴퓨터에서 실수를 표현하는 방식에 대해 알아보고, BigDecimal은 어떻게 정확한 계산을 처리할 수 있는지에 대해 알아보도록 하겠다.

 

소수점 방식 - 부동 소수점 (floating point)

먼저 컴퓨터에서 2진수로 실수를 표현하는 방식에 대해 알아보도록 하겠다.

그 방식은 크게 고정 소수점(fixed point)부동 소수점(floating)으로 나눠볼 수 있는데,

프로그래밍 언어에서 일반적으로 사용되는 방식인 부동 소수점 방식을 알아보자.

 

부동 소수점 (floating point)

부동 소수점은 말 그대로 소수점(point)의 위치고정하지 않는다(floating)는 것이다.

Java의 float, double은 이 방식을 사용한다.

 

https://ko.wikipedia.org/wiki/부동소수점

 

  • 부호부 (1-bit): 0 양수, 1 음수
  • 지수부 (8-bit): 소수점의 위치
  • 가수부 (23-bit): 양의 정수

10진수 소수를 부동 소수점을 이용해 2진수로 변환하는 과정은 아래를 참고하도록 하자.

- https://ko.wikipedia.org/wiki/부동소수점

https://madplay.github.io/post/the-need-for-bigdecimal-in-java

 

 

부동 소수점 방식을 사용하면 표현할 수 있는 값의 범위가 넓어지지만 근사치를 사용하기 때문에 오차가 발생할 수 밖에 없는데,

아래의 예제를 보면 그 오차가 어떻게 발생하는지 확인할 수 있다.

float을 사용한 소수 계산

이 float 계산 예제를 보면 오차가 더해져서 0.2 + 0.1 같은 경우 0.00000000000000004 의 오차가 발생하게 된다.

이러한 오차는 정산 시스템과 같이 소수점 단위의 정확한 연산이 필요한 시스템에는 자동화된 연산 결과가 원하는 연산 결과와 맞지 않는 문제를 일으킬 수 있다.

 

BigDecimal이 소수를 다루는 방법

그러면 BigDecimal은 소수를 어떻게 다루기에 floatdouble과 다르게 정확하게 소수 계산을 할 수 있는 것일까?

 

내부를 살펴보면 BigDecimal은 실수 타입인 float이나 double를 사용하지 않고 정수를 사용하여 소수값을 다룬다.

부동 소수점의 시스템을 사용하지 않고 내부적으로 정수로써 소수를 저장하고 계산하기 때문에 정확한 계산이 가능한 것이다.

예를 들어 100.234 라는 값을 BigDecimal에 담으면 아래와 같이 데이터를 저장하는데 각 데이터는 다음과 같다.

  • intVal (BigInteger): 정수부 값이다. (자세하게는 여기 참고)
  • precision (int): 정수부, 소수부의 길이를 각각 합한 값
  • scale (int): 소수부의 길이
  • stringCache (String): 숫자를 String으로 변환한 값
    • (BigDecimal.valueOf 메서드를 살펴보면 수를 String으로 변환하는 것을 확인할 수 있다)
  • intCompact (long): 소수점을 제외한 전체 수

(단, 정수의 경우와 BigDecimal 생성자를 사용하는 경우는 동작이 다르기 때문에 BigDecimal.valueOf(소수)를 사용하는 케이스는 위와 같다고 보면된다)

 

BigDecimalmultiply(곱하기) 메서드 코드를 보면 수를 intVal, intCompact와 같은 정수를 사용하여 값을 곱하는 것을 볼 수 있다.

    public BigDecimal multiply(BigDecimal multiplicand) {
        int productScale = checkScale((long) scale + multiplicand.scale);
        if (this.intCompact != INFLATED) {
            if ((multiplicand.intCompact != INFLATED)) {
                return multiply(this.intCompact, multiplicand.intCompact, productScale);
            } else {
                return multiply(this.intCompact, multiplicand.intVal, productScale);
            }
        } else {
            if ((multiplicand.intCompact != INFLATED)) {
                return multiply(multiplicand.intCompact, this.intVal, productScale);
            } else {
                return multiply(this.intVal, multiplicand.intVal, productScale);
            }
        }
    }

 

실제로 앞서 float에서는 부정확했던 숫자값이 BigDecimal을 사용하면 정확하게 계산이 되는 것을 확인할 수 있다.

BigDecimal을 사용한 소수 계산

 

주의 BigDecimal 생성자를 사용하는건 값이 부정확할 수 있기 때문에 valueOf를 사용하여 BigDecimal 인스턴스를 생성하는 것을 권고한다.

(생성자와 valueOf를 사용하는 코드는 내부적으로 로직이 다르다)

 

댓글