프로그래밍에서 정확한 소수점 계산은 어떻게 할까?

Lee Anne
7 min readSep 14, 2021

--

프로그래밍을 하면서 소수점 단위의 숫자를 표현하려고 할 적이면 Java에선 double type을 선언하여 사용하는 경우가 많다.(실제로 double을 사용하기를 권장한다.) 근데 정말 double이 우리가 실제로 표현하고자 하는 범위의 숫자를 표현할 수 있을까? 이 사실이 궁금하여 여러 가지를 찾아보았다.

컴퓨터에서 소수점 숫자를 표현하는 방식

컴퓨터는 우리가 알고 있듯이 0,1 이진수로 표현한다. 정수 표현방식은 다들 잘 알고 있으리라 생각하기에 소수점 표현 방식만 다루겠다.

  1. 0.625 변환과정
  • 0.625 * 2 = 1.25 → 정수부분 1
  • 0.25 * 2 = 0.5 → 정수부분 0
  • 0.5 * 2 = 1.0 → 정수부분 1
  • 0.0 * 2 = 0 종료
  • 결과: 0.101(읽는 방향: 위 → 아래)

정수 변환에서는 2로 나누는 연산을 하며 찾아갔지만 소수점에서는 2를 곱하며 찾아가야한다.

Fixed-Point 방식과 Floating-Point 방식

해당 부분에 대해선 위키피디아 혹은 다른 기술 블로그에서 자세하게 설명하고 있기에 링크를 첨부한다.

고정소수점의 포인트는 미리 소수와 정수를 어디까지 표현할지를 정하고 계산한다는 점이다. 이 말은 즉, 16비트 체계 CPU에서는 앞에 부호비트 1비트를 제외한 나머지 지수와 가수비트를 표현하고자 하는 최대 범위의 숫자까지 표현 가능한 범위에 맞게 조절할 수 있다는 뜻이다.(7 : 8 혹은 5 : 10 이런 식으로 표현 비트를 미리 고정하는 방식)

하지만, 현실에서 표현할 수 있는 범위의 숫자가 어디까지일지를 미리 알기가 어렵기 때문에 고정 소수점 표현 방식은 컴퓨터에 잘 적용하지는 않는다.

고정 소수점과 달리 유효숫자를 표현하기 위한 소수점의 위치를 따로 고정하지 않기에 더 넓은 범위의 수를 표현할 수 있다는 장점이 있다.(물론, 연산할 적마다 소수점 위치를 계산해줘야 하기에 고정 소수점 방식보다는 연산 속도가 그만큼 느리긴 하다…)

Floating-Point 정확도

실제 Intellij에서 아래 연산을 한 결과값을 구해보았다.

double precisionTest = 0.10000000D;
System.out.println(precisionTest*precisionTest);
precisionTest = 0.2D; System.out.println(precisionTest*precisionTest);

> Task :JavaFloatDoubleTest.main()
0.010000000000000002
0.04000000000000001

혹시나싶어 다른 실수값으로 바꿔서 계산했는데 다음과 같이 우리가 예상한 결과와는 다른 값이 출력되었다. 왜 이런 결과값이 나온걸까?

계산방식을 보면 1/2, 1/4, 1/8, … 이런 식으로 1/ 2의 제곱수들은 정확한 값을 구할 수 있는데, 그 이외의 수들의 경우 위에서 설명한 연산을 무한대로 시행해야하는 경우가 있다. 그러다 표현할 수 있는 범위비트를 넘어가는 연산을 하게되면 거기서 반올림 혹은 버림을 하여 근사값을 출력하게 되는데, 이때 오차가 발생하게 된다.

7.6 Inexact 7.6.0 Unless stated otherwise, if the rounded result of an operation is inexact — that is, it differs from what would have been computed were both exponent range and precision unbounded — then the inexact exception shall be signaled. The rounded or overflowed result shall be delivered to the destination.

p.s. 근사값을 출력할 적에는 IEEE 754_2008 공식문서에 의하면 round한 값이나 overflow 결과값을 리턴한다고 한다.

그렇담 어떻게 정확한 값을 표현해줄 수 있을까?

금융 도메인 소프트웨어 개발을 진행하다보면 정확한 소수점 연산이 필요할 때가 있다.(증권의 경우는 주가 대비 수익률 계산, 은행의 경우 이자율 계산 등…)이럴 경우엔 우리가 어떻게 처리해줄 수 있을까?

Java, Python, C/C++ 언어 부동 소수점 오차 관련 기술 블로그 글들을 보면서 여러 가지 해결방법을 적용해볼 수 있을 것으로 보인다.

  1. Machine Epsilon 활용
  • 이 방법은 +, -, /, * 사칙연산을 제외한 ==, != 같은 비교연산자와 사용하기에 적합한 방법이다.
float f = 0.01f;
double d = 0.01;
if(f*f - f <= __FLT_EPSILON__) {
printf("FLT_EPSILON is working!\n");
}
if(d*d - d <= __DBL_EPSILON__) {
printf("DBL_EPSILON is working!\n");
}

FLT_EPSILON is working!
DBL_EPSILON is working!

  • Java에서는 Machine Epsilon값을 static 필드값으로 선언해주지 않았기에 프로그래밍을 하면서 대략적인 오차가 얼마정도 나올지 미리 계산하고 해당 방법을 적용해야하지 않을까 싶다…

2. BigDecimal 클래스 활용

  • 이 방법은 Java API 문서에 설명된 클래스를 활용하는 방법으로 오차 없는 사칙연산이 가능하다. 참고로, python에서는 decimal 모듈로 존재한다고 한다.

공식문서에서 보면 int[], char[] 배열을 활용하여 BigDecimal끼리 연산하는 것으로 보인다. 실제 정확도를 비교하고자 float, double, BigDecimal 세 가지를 각각 활용한 코드를 작성해보고 실행해보았다.

// 1. 0.1 * 0.1 = 0.01
float singlePrecisionTest = 0.1F;
double doublePrecisionTest = 0.1D;

System.out.println(singlePrecisionTest * singlePrecisionTest);
System.out.println(doublePrecisionTest * doublePrecisionTest);
BigDecimal bc = new BigDecimal("0.1");

System.out.println(bc.multiply(new BigDecimal("0.1")).floatValue());
System.out.println(bc.multiply(new BigDecimal("0.1")).doubleValue());

> Task :JavaFloatDoubleTest.main()
0.010000001
0.010000000000000002
0.01
0.01

여기서 중요했던 포인트가 BigDecimal 생성자 매개변수로 넣어주는 값이 String type 이어야 한다는 점이다. 실제로 double, float type으로 값을 넘겨줄 경우 계산 오차가 있었다. 만약 저런 방식이 싫다면 아래와 같이 선언하여 사용하기를 추천한다.

System.out.println(BigDecimal.valueOf(0.1).multiply(BigDecimal.valueOf(0.1)));

valuOf()라는 static 메소드를 활용하여 사용하는게 BigDecimal 객체 생성으로 인한 리소스 소모를 줄일 수 있을 것으로 보인다.

결론: BigDecimal 객체를 활용하여 소수점 계산을 수행하기를 추천. 금융계 소프트웨어에서는 많이 사용한다고 함.

--

--