부정확한 계산
숫자는 내부적으로 64비트 형식으로 표현되기 때문에 숫자를 저장하려면 정확히 64비트가 필요하다. 52비트는 숫자를 저장하고 11비트는 소수점 위치를(정수는 0), 나머지 1비트는 부호를 저장하는데 사용된다.
근데 숫자가 너무 커지면 64비트 공간이 넘쳐서 Infinity로 처리된다.
alert( 1e500 ); // Infinity
이것 말고도 꽤 자주 발생하는 현상인 정밀도 손실도 있다.
alert(0.1 + 0.2 == 0.3) // false
alert( 0.1 + 0.2 ); // 0.30000000000000004
원인
숫자는 0과 1로 이루어진 이진수로 변환되어 연속된 메모리 공간에 저장되는데 10진법을 사용하면 쉽게 표현할 수 있는 0.1, 0.2 같은 분수는 이진법으로 표현하면 무한 소수가 된다.
0.1은 1을 10으로 나눈 수인 1/10 이다. 10진법을 사용하면 이러한 숫자를 쉽게 표현할 수 있다. 1/3은 무한소수 0.33333(3)이 된다.
이렇게 10의 거듭제곱으로 나눈 값은 10진법에서 잘 동작하지만 3으로 나누게 되면 10진법에서 제대로 동작하지 않는다. 같은 이유로 2진법 세계에서 2의 거듭제곱으로 나눈 값은 잘 동작하지만 1/10같이 2의 거듭제곱이 아닌 값으로 나누게 되면 무한소수가 되어버린다.
즉 10진법에서 1/3을 정확히 나타낼 수 없듯이, 2진법을 사용해 0.1 혹은 0.2를 정확하게 저장하는 법은 없다.
IEEE-754 에서는 가장 가까운 숫자로 반올림 하는 방법을 사용해 이런 문제를 해결하고 있다. 하지만 이렇게 반올림을 하더라도 우리가 볼 수 없는 작은 정밀도 손실은 발생한다.
alert( 0.1.toFixed(20) ); // 0.10000000000000000555
0.1은 사실 이런 숫자이기 때문에 0.1 + 0.2 == 0.3은 false이다. 사실 이 문제는 java, PHP 등에서도 똑같이 일어나는 문제이다.
해결
toFixed(n) 메서드를 사용해서 어림수 만드는 방법이 있다.
let sum = 0.1 + 0.2
sum.toFixed(2) // 0.30
toFixed() 는 항상 문자열을 반환하기 때문에 소수점 다음에 오는 숫자가 항상 2개가 될 수 있다. 이걸 다시 숫자형으로 변환하려면 + 기호를 붙이면 된다
let sum = 0.1 + 0.2;
alert( +sum.toFixed(2) ); // 0.3
다른 방법으로 숫자에 임시로 100(혹은 더 큰 숫자)를 곱하여 정수로 바꾸고 원하는 연산을 다시 한 후 다시 100으로 나누는 방법도 있다. 하지만 어쨋든 마지막에 나누기가 들어가기 때문에 소수가 재등장 할 수 있긴 하다
alert( (0.1 * 10 + 0.2 * 10) / 10 ); // 0.3
alert( (0.28 * 100 + 0.14 * 100) / 100); // 0.4200000000000001
'IT 연구소' 카테고리의 다른 글
입사자 과제 회고 (0) | 2024.04.22 |
---|---|
단위테스트 프레임워크(Junit5, Spock) (0) | 2024.03.05 |
단위테스트 (0) | 2024.03.04 |
유용한 Git 명령어 (0) | 2024.02.16 |
다시 정리하는 Git의 개념 (0) | 2024.02.16 |