최근에 유튜브를 보다 재밌는 걸 발견했다.
https://www.youtube.com/watch?v=-GsrYvZoAdA&list=LL&t=6s
코딩애플님의 영상이다.
영상에서는 자바스크립트에서 1.1 + 0.2를 계산했는데, 결과가 정확히 1.3이 아니라는 이야기가 나온다.
한번 알아보자
코드 짜보기
public class Main {
public static void main(String[] args) {
double a = 0.1;
double b = 0.2;
double sum = a + b;
double target = 0.3;
System.out.printf("0.1 + 0.2 계산값 : %.20f%n", sum);
System.out.printf(" 0.3 리터럴 : %.20f%n", target);
System.out.println(sum == target);
System.out.println(sum > target);
}
}
0.1 + 0.2 계산값 : 0.30000000000000004000
0.3 리터럴 : 0.30000000000000000000
false
true
프린트로 소수점 20자리까지 내려가서 보면,
두 값이 이미 다르다는 걸 확인할 수 있다.
0.1 + 0.2의 결과가 0.3보다 아주 조금 더 큰 값으로
그래서 == 비교는 실패하고,
sum > target은 true가 된다.
정말 내부 값이 다른가?
앞에서는 소수점 아래를 길게 출력해서 두 값이 다르다는 건 확인했다.
저장된 값 자체가 다른 건가?
Double.toHexString()을 사용하면
double 값을 IEEE 754 기준의
16진수 표현으로 볼 수 있다.
10진수 말고 16진수로 봐보자
System.out.println("0.1 + 0.2 (hex) : " + Double.toHexString(sum));
System.out.println("0.3 (hex) : " + Double.toHexString(target));
0.1 + 0.2 (hex) : 0x1.3333333333334p-2
0.3 (hex) : 0x1.3333333333333p-2
두 값은 아예 다른 값으로 저장되어 있었다.
- 0.1 + 0.2 → ...33334
- 0.3 → ...33333
즉, 이 문제는 출력 방식의 문제가 아니라,
처음부터 메모리에 저장된 값이 달랐던 것이다.
그래서 == 비교가 실패하는 건 당연한 결과였다.
그러면?
금액, 포인트, 정산처럼
조금의 오차도 허용되지 않는 영역에서는
double 비교 자체가 위험하다.
이럴 때는 BigDecimal을 사용하면 된다.
BigDecimal bd1 = new BigDecimal("0.1");
BigDecimal bd2 = new BigDecimal("0.2");
BigDecimal bdSum = bd1.add(bd2);
BigDecimal bdTarget = new BigDecimal("0.3");
System.out.println("BigDecimal 계산값: " + bdSum);
System.out.println("결과가 0.3과 같니? -> " + bdSum.equals(bdTarget));
BigDecimal 계산값: 0.3
결과가 0.3과 같니? -> true
BigDecimal은 부동소수점 방식이 아니라, 10진수를 그대로 표현한다.
즉, 처음부터 근사값이 아니라,
의도한 값 그대로 저장하고 계산하기 때문에 이런 문제 자체가 발생하지 않는다.
하지만 BigDecimal 쓸떄 주의해야할점이 있다.
정확한 계산이 필요할 때는 BigDecimal이 좋은 선택이지만
아래처럼 쓰면 안된다.
BigDecimal wrong = new BigDecimal(0.1);
BigDecimal correct = new BigDecimal("0.1");
System.out.println("new BigDecimal(0.1) : " + wrong);
System.out.println("new BigDecimal(\"0.1\") : " + correct);
System.out.println("둘이 같니? -> " + wrong.equals(correct));
new BigDecimal(0.1) : 0.1000000000000000055511151231257827021181583404541015625
new BigDecimal("0.1") : 0.1
둘이 같니? -> false
같은 0.1처럼 보이지만, 두 값은 전혀 다른 값이다.
new BigDecimal(0.1)은 0.1이라는 십진수를 BigDecimal로 바꾸는 게 아니라,
이미 오차가 포함된 double 0.1을 그대로 BigDecimal로 감싼 것이다.
반면,
new BigDecimal("0.1")은 사람이 의도한 십진수 0.1을 처음부터 정확하게 BigDecimal로 만든다.
BigDecimal은 만능 해결책이 아니다.
입력부터 정확해야 의미가 있다.
- X new BigDecimal(double)
- O new BigDecimal(String)
- O BigDecimal.valueOf(double)
0.1 + 0.2 != 0.3 같은 부동소수점의 계산과 비교는
컴퓨터가 숫자를 표현하는 방식에서 비롯된, 아주 자연스러운 결과다.
다들 알맞은 방식을 쓰고 결과를 도출해내길