public class Decimal {
public static void main(String[] args) {
double a = 1.1;
double b = 0.1;
System.out.println(a+b==1.2); // false
}
}
double형의 a(1.1)와 b(0.1)를 더하면 1.2가 나오지 않는 상황을 마주하게 된다.
먼저 컴퓨터의 소수 처리를 알아보자.
컴퓨터는 모든 수를 이진법으로 처리하는데, 소수(Decimal)는 어떻게 처리할까??
- 정수 부분을 2진법으로 나타낸다.
- 소수 부분에 2를 곱한다.
- 곱한 결과의 정수 부분을 취한다.
- 정수 부분을 떼고 남은 소수 부분이 0이 아니라면 2로 복귀한다.
ex) 15.125 > ????
1. 15 > 1111
2. 0.125 * 2 = '0'.25
3. 1111.0xxxx
4. 0.25 != 0
2. 0.25 * 2 = '0'.5
3. 1111.00xxx
4. 0.5 != 0
2. 0.5 * 2 = '1'.0
3. 1111.001xxx
4. 0 == 0
result : 15.125 > 1111.001
소수를 2진법으로 바꾸는 방식을 통해 1.1과 0.1을 2진법으로 표현해보자
1. 1 > 1
2. 0.1 * 2 = '0'.2
3. 1.0xxxx
4. 0.2 != 0
2. 0.2 * 2 = '0'.4
3. 1.00xxxx
4. 0.4 != 0
2. 0.4 * 2 = '0'.8
3. 1.000xxx
4. 0.8 != 0
2. 0.8 * 2 = '1'.6
3. 1.0001xxx
4. 0.6 != 0
2. 0.6 * 2 = '1'.2
3. 1.00011xxxxx
4. 0.2 != 0 ---------------------소숫점이 2,4,8,6,2,~~~ 무한히 순환하게 된다.
1.1을 2진법으로 변환하게 되면 1.0001100011000110001100011~~~, 소숫점이 '00011'이 순환하는 형태가 된다.
0.1을 2진법으로 변환하게 되면 0.0001100011000110001100011~~~, 소숫점이 '00011'이 순환하는 형태가 된다.
그렇다면 1.1 + 0.1 != 1.2가 발생하는 원인은 무엇일까.
Java에는 실수를 저장할 수 있는 primitive type으로 float과 double이 존재한다.
float , 4byte -> 32 bit -> 1bit (Sign, 부호) + 8bit(Exponent, 지수) + 23bit(Mantissa, 가수)
double , 8byte -> 64 bit -> 1bit (S) + 11bit(E) + 52bit(M)
Java에서 double형으로 1.1과 0.1을 표현했다고 가정하자. 1.1과 0.1의 정수 부분은 손실없이 비트에 저장할 수 있지만, 무한히 순환하는 소수 부분은 52개의 비트에 밖에 저장할 수 없기에 실제로 저장된 값들은 1.1과 0.1과는 다를 수 밖에 없는 것이다.
따라서 소수 부분이 1을 2^n으로 나눈 결과가 아니라면 해당 소수를 2진법으로 나타냈을 때 소수 부분이 순환하게 되고, 이는 잘못된 연산을 이끌 수 있다.
실제 로직을 다룰 때 소숫점을 다루는 연산은 지양하는 것이 좋을 것 같다. 그 예로 1.4m 대신 140cm로 소숫점을 다루지 않는 것으로 하고, 소숫점을 다뤄야한다면, 위처럼 비트를 잘 생각하며 다뤄야겠다.