TIL

  1. 정확한 실수 계산이 필요할 때, float과 double을 사용하면 안 된다.
  2. 해결책
    1. 정수형 타입으로 치환해서 사용
    2. BigDecimal 클래스 사용

 


 

정확한 실수 계산이 필요할 때, float과 double을 사용하면 안 된다

 

💡 float과 double은 실수를 표현할 때, 유효 자릿수가 존재하고 이는 정확한 실수의 값을 표현하는데 한계가 있다는 뜻이다.

 

0.1(10)을 2진수로 바꾸면 0.0001100110011...(2) 로 무한 반복 된다. 무한 반복 되는 2진수 실수는 아무리 부동소수점 방식으로 저장한다고 해도 한정된 메모리 크기에 무한대의 값을 저장할 수 없다. 결국 소수점 몇 째 자리에서 끊어 반올림을 해주어야 한다.

 

즉, 컴퓨터의 메모리는 한정적이기 때문에 실수의 소숫점을 표현하는데 제한이 있을 수 밖에 없다. 실수를 표현하는 숫자에 제한이 있다는 것은 곧 부정확한 실수의 계산값을 초래한다는 뜻이기도 하다.

 

double v1 = 12.23;
double v2 = 34.45;

System.out.println(v1 + v2); // 46.68000000000001

원래는 46.68 값이 나오기를 기대했지만 계산 결과 46.68000000000001이 나온다. 12.23과 34.45를 2진수로 변환하는 과정에서 무한 소수 현상이 나타나 해당 2진수 값을 메모리에 할당하는데 한계가 생긴 것이다. 이 한계 때문에 소수점 몇 째 자리까지 반올림으로 표현되고 이 부정확한 값을 이용하여 연산 했으니 결과값도 당연히 부정확하게 출력된다.

 

물론 이 결과 값만 봤을 땐 아주 작은 차이 밖에 안 나지만 이게 돈 관련 계산 프로그램이라면...? 이 오차가 큰 영향을 미칠 수 있기 때문에 아주 정확한 계산이 필요하다.

 

이러한 컴퓨터의 실수 연산 문제를 해결하기위해 자바는 2가지의 방법을 제공한다.

  1. int, long 정수형 타입으로 치환해서 사용
  2. BigDecimal 클래스 이용

 

 

 

정수형 타입으로 치환해서 사용

double v1 = 12.23;
double v2 = 34.45;
System.out.println(v2 - v1); // 22.220000000000002

// 정수타입으로 치환하여 실수 연산
long v3 = (long) (v1 * 100);
long v4 = (long) (v2 * 100);
double v5 = (v4 - v3) / 100.0;
System.out.println(v5); // 22.22

12.23에 100을 곱해서 1223 정수로 치환하고 두 수를 계산한 후, 다시 100을 나누어 소수 결과값을 도출한다.

 

 

 

BigDecimal 클래스 이용

자바에서 실수를 저장하는 변수 타입은 float와 double이 있다. 이 두 타입은 부동소수점을 이용해 실수를 저장하는데 정밀도가 완벽하지 않다는 특징이 있다. 그래서 사칙연산 시, 부정확한 값을 출력하는 경우가 있다. 이 오차는 돈 계산 관련된 프로그램에서 아주 큰 영향을 미칠 수 있기 때문에 float과 double 타입을 사용하면 안 된다.

 

이런 경우엔 미세한 숫자의 변동도 허용하지 않는 BigDecimal(API문서)을 사용해야 한다.

BigDecimal 클래스는 java.math 패키지 안에 포함되어 있다.
BigDecimal은 float, double과는 달리 정수를 이용해서 실수를 표현한다. 실수의 오차는 10진수의 실수를 2진수의 실수로 정확히 변환하기 힘든 경우가 있어서 오차가 없는 2진수의 정수로 변환해서 다루는 것이다.


BigDecimal은 실수를 10의 제곱정수의 곱으로 표현한다.

 

정수 X 10^-scale

 

scale은 0부터 Integer.MAX_VALUE 사이의 범위에 있는 값이다. BigDecimal은 정수를 저장하는데 BigInteger를 사용하며 불변(Immutable)이다.

BigDecimal val = new BigDecimal("123.45"); // 12345 X 10^-2
private final BigInteger intVal;    //정수(unscaled value)
private final int scale;    		//지수(scale)
private transient int precision;    //정밀도(precision): 정수의 자릿수

 

BigDecimal은 double 정밀도의 한계 때문에 사용하는 클래스이므로 double 타입으로 객체를 생성할 경우, 오차가 발생할 수 있다. 그러므로 문자열로 객체를 생성하는 것이 일반적이다.

import java.math.BigDecimal;

// 문자열로 생성
BigDecimal bigDecimal = new BigDecimal("123.45678");

// double 타입으로 생성(오차가 발생할 수 있다)
// 123.4560000000000030695446184836328029632568359375
BigDecimal bigDecimal2 = new BigDecimal(123.456);

 

 

 

📚 참고
자바의 정석 9장
https://inpa.tistory.com/entry/JAVA-%E2%98%95-BigInteger-BigDecimal-%EC%9E%90%EB%A3%8C%ED%98%95-%EC%82%AC%EC%9A%A9%EB%B2%95-%EC%B4%9D%EC%A0%95%EB%A6%AC