float와 double은 모두 소수점을 처리하기 위한 타입이다. 즉 실수(Real Number) 타입이다.
간단한 계산에서는 사용해도 무방하지만,
💵돈💵 계산과 같이 정확한 계산이 요구될 때에는 BigDecimal사용을 해야 한다고 한다.
어떠한 이유로 그래야 할까?
float와 double의 문제점float와 double의 정의Oracle의 Java Tutorial 에서는 다음과 같이 정의한다.
java.math.BigDecimal 클래스를 대신 사용해야 한다고 한다.
float: The float data type is a single-precision 32-bit IEEE 754 floating point. Its range of values is beyond the scope of this discussion, but is specified in the Floating-Point Types, Formats, and Values section of the Java Language Specification. As with the recommendations for byte and short, use a float (instead of double) if you need to save memory in large arrays of floating point numbers. This data type should never be used for precise values, such as currency. For that, you will need to use the java.math.BigDecimal class instead. Numbers and Strings covers BigDecimal and other useful classes provided by the Java platform.double: The double data type is a double-precision 64-bit IEEE 754 floating point. Its range of values is beyond the scope of this discussion, but is specified in the Floating-Point Types, Formats, and Values section of the Java Language Specification. For decimal values, this data type is generally the default choice. As mentioned above, this data type should never be used for precise values, such as currency.
[출처] Oracle Java Tutorials - Primitive Data Types
이진법에서의 부동소수점을 표현하는 표준이며
부동소수점은 가수부(fraction: 유효 숫자)와 지수부(exponent: 소수점의 위치)를 나눠서 소수를 표현한 것이다.
그리고 음수(1)인지, 양수(0)인지에 대한 표현에 대해 부호비트(sign bit)도 포함한다.
float: 부호비트 1bit, 지수부는 8bit, 가수부는 23bit의 크기를 가지게 됩니다.double: 부호비트 1bit, 지수부는 11bit, 가수부는 52bit의 크기를 가지게 됩니다.

0.1을 1000번 더하는 프로그래밍을 실행하게 되면 당연히 100이 나오겠지?!
여기서 나는 실제 개발할 때 double로 소수 타입을 많이 하므로 double형으로 구현해보았다.
내가 기대한 값과, 실제로 실행한 결과는 다르게 나왔다.
double result = 0;
for (int i = 0; i < 1000; i++) {
result += 0.1;
}
System.out.println("result : " + result);
------------- 실행결과 -------------
result : 99.9999999999986
더하는 과정중에 어떤 일이 일어났는지 더할 때마다의 실행결과를 출력해보았다.
아래 실행결과를 보면 0.1씩 더한 결과들이 나와야하는데 소수점 아래의 숫자들이 너무나 많다.
몇 번을 반복 실행해 봐도 결과는 같다. 왜 이러는 거지?
double result = 0;
for (int i = 0; i < 1000; i++) {
result += 0.1;
System.out.println((i+1) + " 번째 합계 : " + result);
}
------------- 실행결과 -------------
1 번째 합계 : 0.1
2 번째 합계 : 0.2
3 번째 합계 : 0.30000000000000004
4 번째 합계 : 0.4
5 번째 합계 : 0.5
6 번째 합계 : 0.6
7 번째 합계 : 0.7
8 번째 합계 : 0.7999999999999999
9 번째 합계 : 0.8999999999999999
10 번째 합계 : 0.9999999999999999
.
.
.
991 번째 합계 : 99.09999999999864
992 번째 합계 : 99.19999999999864
993 번째 합계 : 99.29999999999863
994 번째 합계 : 99.39999999999863
995 번째 합계 : 99.49999999999862
996 번째 합계 : 99.59999999999862
997 번째 합계 : 99.69999999999861
998 번째 합계 : 99.7999999999986
999 번째 합계 : 99.8999999999986
1000 번째 합계 : 99.9999999999986
소수점 이하의 수를 다룰 때 float와 double타입은 사칙연산 시 우리가 기대한 값과 다른 값을 출력한다.
이유는 내부적으로 수를 저장할 때 이진수의 근사치를 저장하기 때문이다. 그 저장된 수를 다시 십진수로 표현하면서 문제가 발생한다.
이유를 살펴보기 전에 컴퓨터가 어떻게 실수(float, double)들을 저장하는지에 대한 것을 알아야 한다.
컴퓨터는 0과 1로 값을 저장한다. 즉 이진법으로 숫자를 저장한다.
이는 정수를 저장하는데 있어서 모든 수를 표현할 수 있지만, 소수를 저장하는데 문제가 생긴다.
(이진수) = (십진수)
...
그러면 0.3을 컴퓨터에 저장하기 위해서는?
이고,
따라서 가 될 수 있다.
하지만, 을 정확히 이진수로 정확하게 나타낼 수 없다.
이진수를 십진수로 변환할 때에는 소수 부분의 첫째자리에 를 계속해서 곱하면서 일의자리 숫자를 확인한다.
[예시1] 나누어 떨어지는 계산
0.5 x 2 = 1.0 ----------------- 일의 자리가 1이므로 해당 소수 첫째 자리는 1
0.25 x 2 = 0.5 --------------- 일의 자리가 0이므로 해당 소수 첫째 자리는 0
0.5 x 2 = 1.0 ----------------- 일의 자리가 1이므로 해당 소수 둘째 자리는 1
[예시2] 나누어 떨어지는 않는 계산 0.1
0.1 x 2 = 0.2 ---------------- 일의 자리가 0이므로 해당 소수 첫째 자리는 0
0.2 x 2 = 0.4 ---------------- 일의 자리가 0이므로 해당 소수 둘째 자리는 0
0.4 x 2 = 0.8 ---------------- 일의 자리가 0이므로 해당 소수 셋째 자리는 0
0.8 x 2 = 1.6 ---------------- 일의 자리가 1이므로 해당 소수 넷섯째 자리는 1
0.6 x 2 = 1.2 ---------------- 일의 자리가 1이므로 해당 소수 다섯째 자리는 1
0.2 x 2 = 0.4 ---------------- 일의 자리가 0이므로 해당 소수 여섯째 자리는 0 <-- 계산이 계속된다.
위의 식을 통해 0.1이나 0.3과 같은 소수들이 이진수로 변환될 때 정확하게 유한소수로 나타낼 수 없는 것을 확인하였다.
IEEE754에서는 실수의 처리에 대해 여러가지 방법을 제시한다.
그 중 JAVA스펙에서는 가까운 값으로 반올림(round to nearest)방식을 채택하였다.
float과 double의 각각 가수부에 대한 비트수가 있다.
만약 무한소수에서 가수부가 각각 비트수(32/64bit)를 넘어가게 된다면, 소수점이 잘리면서 rounding이 이뤄지는 점을 주목해야 한다.
- 십진수의 소수를 이진수로 변환하면서 어떤 소수들은 이진수의 유한소수로 나타낼 수 없어 무한소수로 나타내어진다.
- 각각 실수 자료형의 가수부의 비트 수에 따라 소수점이 잘리면서 반올림이 일어나 다시 십진수로 표현하면서 근사치인 소수를 얻게된다.
- 이러한 근사치 소수들끼리의 연산의 결과도 마찬가지로 가수부가 비트 수 만큼 소수점이 잘리면서 다시 십진수로 표현할 때 근사치인 소수를 얻게 될 수도 있을 것이다.
결론 : float나 double의 경우 내부적으로 수를 저장할 때 이진수의 근사치를 저장한다.
저장된 수를 다시 십진수로 표현하면서 계산의 결과가 정확하지 않을 수도 있다는 결과를 확인할 수 있었다.
BigDecimalBigDecimal이란?BigDecimal은 Java 언어에서 숫자를 정밀하게 저장하고 표현할 수 있는 객체이다.double이 소수점의 정밀도에 있어 한계가 있어 값이 유실될 수 있는 점을 방지한다.BigDecimal은 필수이다.BigDecimal의 단점은 느린 속도와 기본 타입보다 복잡한 사용법이다.BigDecimal 기본 용어precision : 숫자를 구성하는 전체 자리수.unscale과 동의어이다.scale : 전체 소수점 자리수.fraction과 동의어이다.BigDecimal 기본 상수// 흔히 쓰이는 값은 상수로 정의
// 0
BigDecimal.ZERO
// 1
BigDecimal.ONE
// 10
BigDecimal.TEN
BigDecimal 초기화BigDecimal 타입을 초기화하는 방법으로 가장 안전한 것은 문자열의 형태로 값을 생성자에 전달하여 초기화하는 것이다.
double 타입의 값을 그대로 전달할 경우 앞서 사칙연산 결과에서 본 것과 같이 이진수의 근사치를 가지게 되어 예상과 다른 값을 얻을 수 있다.
double result = 0.1;
BigDecimal bigDecimal = new BigDecimal(result);
System.out.println("result : " + bigDecimal);
------------- 실행결과 -------------
result : 0.1000000000000000055511151231257827021181583404541015625
그래서 문자열로 값을 초기화해야 정상 인식한다.
BigDecimal bigDecimal = new BigDecimal("0.01");
System.out.println("result : " + bigDecimal);
------------- 실행결과 -------------
result : 0.1
또는 double을 toString을 이용하여 문자열로 초기화 할 수 있다.
System.out.println("result : " + BigDecimal.valueOf(0.01));
------------- 실행결과 -------------
result : 0.1
BigDecimal 비교연산BigDecimal은 기본 타입이 아닌 오브젝트이기 때문에 동등 비교 연산을 유의해야 한다.
double 타입을 사용하던 습관대로 무의식적으로 == 기호를 사용하면 예기치 않은 연산 결과를 초래할 수 있다.
BigDecimal a = new BigDecimal("2.01");
BigDecimal b = new BigDecimal("2.01");
BigDecimal c = new BigDecimal("2.010");
BigDecimal d = new BigDecimal("2.000");
// 객체의 레퍼런스 주소에 대한 비교 연산자
// false
System.out.println("(a == b) = " + (a == b));
// 값의 비교를 위해 사용, 소수점 맨 끝의 0까지 완전히 값이 동일해야 true 반환
// true
System.out.println("a.equals(b) = " + a.equals(b));
// false
System.out.println("a.equals(c) = " + a.equals(c));
// 값의 비교를 위해 사용, 소수점 맨 끝의 0을 무시하고 값이 동일하면 0, 적으면 -1, 많으면 1을 반환
// 0
System.out.println("a.compareTo(c) = " + a.compareTo(c));
// 1
System.out.println("a.compareTo(d) = " + a.compareTo(d));
------------- 실행결과 -------------
(a == b) = false
a.equals(b) = true
a.equals(c) = false
a.compareTo(c) = 0
a.compareTo(d) = 1
BigDecimal 사칙연산Java에서 BigDecimal 타입의 사칙 연산 방법은 아래와 같다.
보다시피 double 타입에서 지원하는 연산자로는 사칙연산을 할 수 없다.
따로 메소드가 있으니 BigDecimal API문서를 확인해 본다.
BigDecimal a = new BigDecimal("10");
BigDecimal b = new BigDecimal("3");
// 더하기
// 13
a.add(b);
// 빼기
// 7
a.subtract(b);
// 곱하기
// 30
a.multiply(b);
// 나누기
// 3.333333...
// java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
a.divide(b);
// 나누기
// 3.333
a.divide(b, 3, RoundingMode.HALF_EVEN);
// 나누기 후 나머지
// 전체 자리수를 34개로 제한
// 1
a.remainder(b, MathContext.DECIMAL128);
// 절대값
// 3
new BigDecimal("-3").abs();
// 두 수 중 최소값
// 3
a.min(b);
// 두 수 중 최대값
// 10
a.max(b);
BigDecimal을 이용해야 한다.float와 double같은 경우에는 연산처리를 할 때 값이 유실되어 예상치 못한 결과값을 얻을 수 있다.BigDecimal은 정확한 계산을 보장하지만, 성능은 기본타입보다 떨어질 수 있고 사용법이 기본연산과 다르므로 API문서를 확인해야한다.[출처] 블로그 - 부동소수점 계산
[출처] Oracle Java Tutorials - Primitive Data Types
[출처] Oracle Java Tutorials - 4.2.4. Floating-Point Operations
[출처] Wiki - IEEE 754
[출처] Oracle Java API - BigDecimal
[출처] baeldung -java-bigdecimal-biginteger
[출처] 지탄로보트 블로그 - Java, BigDecimal 사용법 정리
[출처] 니코딩코딩 블로그 - BigDecimal 정리
좋은 공부가 됬습니다 감사합니다