자바스크립트의 실수 계산 오류

김진석
6 min readOct 20, 2018

--

0.1과 0.2를 더하면 과연 0.3일까?

자바스크립트에서 0.1과 0.2를 더하면 0.3이 아닌 0.30000000000000004가 나온다. 도대체 왜 이럴까?

원인은 바로 자바스크립트가 숫자를 저장하는 방식에 있다. MDN 웹 문서에 따르면 자바스크립트는 숫자를 부동소수점(double-precision 64-bit binary format IEEE 754)으로 처리를 한다.

그러면 우선 10진수 값을 2진수로 변환하게 되는데, 0.1이나 0.2 같은 경우는 무한 소수가 발생한다. 위의 같은 경우에는 부호비트가 1bit, 지수부 11bit, 가수부가 52bit임으로 이 이상 넘어가는 부분은 반올림 처리 하여 잘라내게 된다. 그래서 해당 값은 정확한 값이 아닌 근사 값으로 저장이 된다.

double-precision 64-bit binary format IEEE 754

이제 0.1과 0.2를 더했을 때 왜 저렇게 나오는지는 이해가 갔지만, 내부적으로 저 값이 도출되는 과정이 궁금해져서 직접 계산도 해보았다.

우선 0.1을 2진수로 변환해 보면 0.000110011... 으로 순환소수가 되는데, 이를 위의 부동소수점으로 변환할려면, 우선 소수점을 옮겨야 한다. 1이 나올때 까지 소수점을 왼쪽 또는 오른쪽으로 옮기면 되는데, 위 같은 경우는 오른쪽으로 총 4칸을 옮기면 된다. 따라서 1.100110011… * 2^-4(1/2⁴)가 된다.

여기서 부호비트는 양수는 0, 음수는 1인데 0.1은 양수라서 0이고, 지수부는 2^(n-1)-1+m이라는 공식을 사용하면 된다. 여기서 n은 지수부의 총 bit이고, m은 지수인데 따라서 2^(11-1)-1+(-4)라는 식이 나오고 답은 1019가 나온다. 여기서 이 값을 2진수로 바꾸면 1111111011인데 지수부는 총 11bit임으로 앞에 0을 붙여서 01111111011이 들어가게 된다. 마지막으로 가수부는 간단하다. 1.100110011…에서 소수점 뒷자리에 있는 값을 넣으면 된다. 따라서 100110011…으로 들어가는데 단 0.1은 순환소수라 52번째 이후 값은 반올림을 해야한다. 50번째 부터 53번째의 값을 보면 0011인데, 여기서 53번째 값을 반올림하면 010이 된다.

0.1을 부동소수점으로 나타낸 그림이다.

이 값을 다시 2진수로 바꿔보면 자바스크립트에서 toString(2)를 이용해서 나온 값과 일치한 걸 볼 수 있다.

2진수로 다시 변환한 값
0.00011001100110011001100110011001100110011001100110011010
자바스크립트에서 나온 값
(0.1).toString(2);
"0.0001100110011001100110011001100110011001100110011001101"

위와 같이 0.2도 변환을 해 보면 0.00110011... 으로 순환소수가 되며, 부동소수점으로 변환하면 1.100110011… * 2^-3(1/2³)이 되고, 부호비트는 0, 지수부는 2^(11–1)-1+(-3)으로 1020이 나오고 2진수로 변환하면 01111111100이 된다. 가수부는 0.1의 값과 동일하다.

0.2를 부동소수점으로 나타낸 그림이다.

이 값 또한 다시 2진수로 바꿔보면 자바스크립트에서 toString(2)를 이용해서 나온 값과 일치한 걸 볼 수 있다.

2진수로 다시 변환한 값
0.0011001100110011001100110011001100110011001100110011010
자바스크립트에서 나온 값
(0.2).toString(2);
"0.001100110011001100110011001100110011001100110011001101"

자 이제 0.1과 0.2를 더하는 일만 남았다.

0.0001100110011001100110011001100110011001100110011001101
0.001100110011001100110011001100110011001100110011001101
---------------------------------------------------------
0.0100110011001100110011001100110011001100110011001100111

이렇게 해서 나온 값을 이제 10진수로 바꾸면 되는데 이 수를 일일이 계산하기에는 너무 힘들어서 자바스크립트의 함수를 이용해서 계산을 했다.

parseInt('0100110011001100110011001100110011001100110011001100111', 2) * Math.pow(2, -55);
0.30000000000000004

그렇다. 원하던 값이 나왔다 ㅎㅎ… 이렇게 값이 정확하지 않고 근사 값으로 계산을 하기 때문에 오차가 나오는 수 밖게 없는 상황이 된다. 참고로 0.3을 2진수로 바꿔보면 아래와 같이 나온다.

0.3을 부동소수점으로 나타낸 그림이다.

0.1과 0.2를 더해서 나온 값과 0.3의 값을 비교해 보면 마지막 자리가 다르다는 걸 볼 수 있다.

0.1 + 0.2
0.0100110011001100110011001100110011001100110011001100111
parseInt('0100110011001100110011001100110011001100110011001100111', 2) * Math.pow(2, -55);
0.30000000000000004
0.3
0.010011001100110011001100110011001100110011001100110011
parseInt('010011001100110011001100110011001100110011001100110011', 2) * Math.pow(2, -54);
0.3

마지막으로 이 같은 상황은 자바스크립트만의 문제는 아니다. 숫자를 부동소수점(double-precision 64-bit binary format IEEE 754)으로 처리하는 언어에서 동일한 문제가 나타난다. 또한 이를 해결할 방법은 수학관련 라이브러리를 사용하던가 아니면 10^n으로 곱하여 정수로 바꿔서 계산하는 방식등이 있다.

--

--