-
[JavaScript] 자바스크립트 소수점 계산 오류 가볍게 이해하기프로그래밍 이야기/JavaScript 공부 2020. 6. 23. 00:55
자바스크립트로 숫자를 조금 다루다 보면, 분명히 우리가 상식적으로 생각했을 때 너무나도 당연하고 올바른 계산식을 컴퓨터에게 맡겼는데, 흔하지 않지만 나름 흔한 확률(?)로 계산 오류가 발생하는 경험을 마주하게 된다.
대부분 피연산자들이 소수점(floating point)을 가진 경우에 발생하는데, 이번 포스트에서는 자바스크립트의 부정확한 계산 오류에 대해서 조금 정리를 해보고자 한다.
특히나, 만일 자바스크립트로 계산기를 만들고자 한다면 이 부분은 명확하게 짚고 넘어가야 될 부분이라고 할 수 있겠다.
가장 대표적인 자바스크립트 소숫점 계산 오류
아마 구글에 자바스크립트 소숫점 계산 오류라고 검색하면 굉장히 높은 확률로 0.1 + 0.2를 보게 된다.
그렇다. 0.1과 0.2를 더하면 분명히 0.3이 나와야 하는데.. 실제로 코드를 작성해보면 그렇지 않은 결과를 확인해 볼 수 있다.
let a = 0.1; let b = 0.2; console.log(a + b); // 0.30000000000000004
0.30000000000000004라는 숫자를 마주하게 된다.
자바스크립트만 멍청한게 아니다!
자바스크립트만 이런 멍청한 계산을 하냐고? 절대 아니다!
PHP, Java, C, Perl, Ruby, 등등.. 누군가에게는 죽고 못 사는 Python 마저도 이런 멍청한 계산 오류를 범한다.
원인은?
그렇다면 과연 이 오류의 원인은 무엇일까?
컴퓨터는 0과 1로만 이루어진 계산기라는 말 많이 들어봤을 것이다.
사실 우리가 익숙하게 사용하는 10진법과는 다르게 컴퓨터는 2진법을 사용해 계산을 한다.
몇몇 소수의 경우에는 10진법을 2진법으로 변환할 때 무한소수가 되어버리는데, 유한한 컴퓨터의 자원을 활용하는 프로그램은 당연히 저장공간의 한계로 인해 결국 무한 소수를 유한한 수로 바꾸게 되고, 그 과정에서 나타나는 미세한 오차가 계산의 오류를 범하게 하는 것이다.
간혹, 학원이나 온라인 강의, 블로그 포스팅에서는 이 현상을 간략히 하기 위해 '10진법을 2진수로 바꾸기 때문에 계산오류가 발생한다'라고만 설명하는 경우도 봤는데 사실 단순하고 가볍게 이해하기 위함인 건 이해하지만, 개인적으로 '10진법을 2진수로 바꾸기 때문에 계산오류가 발생한다'라고만 이해하는 것은 너무 추상적인 이해라고 생각한다.
적어도 저장공간의 한계가 있기 때문인 사실도 명확하게 짚어주는 것이 좀 더 좋지 않나 라는 생각이 들었다.
근본을 따지자면 사실 숫자는 내부적으로 64비트 형식 IEEE-754으로 표현되기 때문에 숫자를 저장하려면 정확히 64비트가 필요하다. 64비트 중 52비트는 숫자를 저장하는 데 사용되고, 11비트는 소수점 위치를(정수는 0), 1비트는 부호를 저장하는 데 사용된다. 그런데 너무 큰 수는 표현하는 데 64비트를 초과하기 때문에 Infinity로 처리 된다.
이 사실을 먼저 알고서 소수점 오류를 이해해야 한다.
그런데 현실적으로 만약 컴퓨터 전공자가 아니고 이제 막 프로그래밍에 흥미를 갖고서 배우는 사람이 갑자기 64비트가 어떻고 52비트가 어떻고 11비트는 어떻고.. IEEE-754는 어떻다는데 막상 들어가 보면, 괜히 문송해야할 것만 같은 내용들이 즐비하다... 이런 이야기를 맘 편히 받아들일 수 있을까? 솔직히 컴퓨터 과학에서 값의 표현이 메모리에 끼치는 영향에 대한 이야기는 비전공자의 시선에서는 생각보다 그리 즐거운 주제는 아니라고 생각한다.
그러니, 일단은! 가볍게!
1. 컴퓨터로 숫자를 표현하는 데에는 한계가 있고
2. 10진수로 표현되는 소수를 2진수로 표현하려면 몇몇의 경우 무한소수가 발생하는데,
3. 무한수를 유한하게 표현하려다 보니 미세한 값들이 초과되거나 손실되어
4. 계산 오류가 일어난다.
라고 정리해 두자.
해결방안은?
1. toFixed() 메서드
이런 소수점 계산 오류를 해결하기 위한 방법들 중 첫 번째 방법은 toFixed(n) 메서드를 사용해 어림수를 만드는 것이다.
let a = 0.1; let b = 0.2; console.log((a + b).toFixed(1)); // '0.3'
toFixed 메서드는 파라미터로 0부터 20까지 숫자를 넘길 수 있는데 해당 값이 소수점 이후의 자릿수를 결정한다.
하지만 반환되는 값이 문자열이기 때문에 만일 숫자로 사용하고 싶다면 앞에 단항 연산자로 덧셈 연산자를 붙여주어야 한다.
let a = 0.1; let b = 0.2; console.log(+(a + b).toFixed(1)); // 0.3
2. Math 객체의 메서드 활용
Math 객체의 메서드를 이용하는 것도 하나의 방법이라고 할 수 있다. Math 객체에는 Math.floor, Math.ceil, Math.trunc, Math.round 같은 소수를 다루는 메서드들이 있는데 이를 통해 연산하고자 하는 의도에 따라 계산을 하는 방법도 있다.
let a = 0.1; let b = 0.2; console.log(Math.round((a + b) * 10) / 10); // 0.3
3. 라이브러리 활용
좀 더 편리한 방법은 자바스크립트용 수학 라이브러리를 사용하는 것이다.
Big.js, BigNumber.js, Decimal.js mathjs 등 라이브러리를 활용하면 좀 더 안전하게 수학계산을 할 수 있기도 하다만 개인적으로 이런 라이브러리를 활용하는 게 불편할 수 도 있으니.. 각자의 취향에 맡기는 것이 좋다.
(참고) 마냥 소수점에서만 계산 오류가 일어나지 않는다!?
console.log(9999999999999999); // 10000000000000000
위 코드를 보자. 분명히 9999999999999999를 출력했는데 10000000000000000이 출력되었다.
멍청한 자바스크립트.
(?)
아무튼, 계산 오류는 소수에서만 일어나는 것도 아니고, 언어를 개발하는 누군가의 실수에 의한 것도 아니다.
'프로그래밍 이야기 > JavaScript 공부' 카테고리의 다른 글
[JavaScript] 자바스크립트 DOM (Document Obejct Model) 가볍게 이해하기 - 1 (2) 2020.06.27 [JavaScript] 자바스크립트 window 객체 이해하기 (2) 2020.06.25 [JavaScript] 자바스크립트 Date 객체 이해하기 - 2 (0) 2020.06.21 [JavaScript] 자바스크립트 Date 객체 이해하기 - 1 (0) 2020.06.17 [JavaScript] 자바스크립트 this 가볍게 이해하기 (0) 2020.06.14