GITHUB
부동소수점을 제대로 알아야 하는 이유
BLOGProject
Seohyun
Develop
11 Oct 2022
부동소수점을 제대로 알아야 하는 이유

부동소수점?

2진법 체계를 사용하는 컴퓨터가 소수점을 나타낼 때 부동소수점을 사용합니다.

하나의 수를 고정 소수점 부분을 나타내는 부분(가수)과 고정 소수점 위치를 나타내는 부분(지수)으로 나누어 표현하는 표기법, 또는 그러한 수치를 연산하는 연산 조직. 고정 소수점 방식보다 넓은 영역의 숫자를 나타낼 수 있어 과학적인 응용에 많이 이용된다.
네이버 지식백과 (IT용어사전, 한국정보통신기술협회)

갑자기 부동소수점을 왜?

위의 부동소수점을 보면 컴퓨터에서 부동소수점을 저렇게 다루는구나 하며 딱히 의아한 부분이 생기지 않습니다.
하지만 현업에서 연산하는 식의 인수에 소수점 적용을 도입하면서 문제가 발생했습니다. 정수의 곱셈 연산을 하는 식에 소수점이 등장한 것입니다. 이렇게 소수점이 계산에 포함되는 경우, 결괏값에 소수점이 포함되는 실수(Real Number)가 나오기 마련입니다. 하지만 전 정수(Integer)의 결괏값을 원하므로 실수를 소수점 첫째 자리에서 반올림 해 정수로 표현해야 합니다.
문제는 이 과정에서 부동소수점의 표현은 2진법 체계의 컴퓨터에서 정확하지 않은 오차가 있는 표현이라는 점입니다. 두둥 ⚡️

부동소수점이 무슨 오차가 있나요?

// 0.30000000000000004
console.log(0.1 + 0.2);
// 0.30000000000000004

이 계산은 당연히 답이 0.3입니다.

하지만 실제 JavaScript에서 실행한 결과는 0.3이 아닙니다.

컴퓨터가 2진법 체계라는 점은 공공연한 사실이지만 여기서 오차로 발견하니 새삼 컴퓨터는 2진법 체계가 맞았구나 싶습니다.

부동소수점의 오차는 왜 있죠?

JavaScript에서 숫자는 64 bit IEEE-754 형식으로 다뤄집니다. 이 형식을 바탕으로 표현된 0.1은 정확인 0.1이 아니기 때문에 발생한 오차입니다. 우리가 사용하는 10진법과 다른 2진법으로는 0.1을 완벽히 표현할 수 없기 때문입니다.

이 문제는 단연 JavaScript만의 문제는 아닙니다. 컴퓨터가 2진법을 사용하므로 다른 언어에서도 같은 현상이 존재합니다.

무슨 말인지 예시와 함께 살펴봅시다.

1/10이라는 분수를 십진법의 소수로 표현할 경우 0.1로 표현할 수 있습니다. 이는 나누고자 하는 10이 10의 거듭제곱(10의 1승)이기 때문이죠. 이렇게 10의 거듭제곱으로 나누었기 때문에 유한소수로 표현할 수 있습니다. 반면, 1/3 이라는 분수를 십진법의 소수로 표현할 경우 0.3333⋯의 무한소수로 표현됩니다. 1을 3으로 나누었을 때 딱 나누어 떨어지지 않습니다. 이유는 나누고자 하는 숫자 3이 10의 거듭제곱이 아니기 때문에 결과가 무한소수로 나오게 됩니다. 이와 마찬가지로 2진법의 체계에서 2의 거듭제곱으로 나눈 값은 유한 소수로 표현 가능하지만 2의 거듭제곱이 아닌 값으로 나누게 되면 무한소수가 됩니다. 즉 10진법에서 1/3을 정확히 나타낼 수 없듯이 2진법을 사용해 0.1 또는 0.2를 정확하게 저장하는 방법이 없게 되는거죠.

부동소수점의 오차를 어떻게 처리하죠?

앞부분 설명에서 JavaScript에서 숫자는 64bit IEEE-754 형식으로 다룬다고 했었습니다. IEEE-754에서는 가능한한 가장 가까운 숫자로 반올림하는 방법을 사용해 10진법으로 표현된 수를 2진법으로 변환했을 때 생기는 무한 소수를 어느정도 해결해 줍니다.

정수로 변환

소수점이 있는 수에 10의 거듭제곱 수를 곱한 후, 연산 후 처음 곱한 수를 다시 나누면 됩니다. 연산 과정에서의 소수점을 없애 주어 조금더 정확한 연산이 가능한 방법입니다.(연산 과정에서 0.1 + 0.2가 아닌 1 + 2를 하게 되기 때문입니다.)

console.log((0.1 * 10 + 0.2 * 10) / 10); // 0.3

toFixed()

toFixed() 메서드를 이용하는 방법입니다. toFixed(n)은 소수점 n번째 수까지 어림수를 구하는 방법입니다. toFixed()의 default는 toFixed(0)으로 소수점을 하나도 표현하지 않는, 정수로 표현하는 것이 기본값입니다.

console.log((0.1 + 0.2).toFixed(1)); // 0.3
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(typeof (0.1 + 0.2).toFixed(1)); // string
console.log((0.1 + 0.2).toFixed(1)); // 0.3
console.log(Number((0.1 + 0.2).toFixed(1)));
console.log(typeof (0.1 + 0.2).toFixed(1)); // string
console.log(Number((0.1 + 0.2).toFixed(1)));

위에서 미세한 오차가 있었던 계산입니다.

toFixed(1)이므로 소수점 첫번째 자리까지 어림수를 구하는 것이므로 0.3이라는 결과가 나왔습니다.

toFixed(n) 메서드는 반환값이 string입니다. string 값이므로 number type이 필요한 경우 형변환이 필요합니다.

이 경우 Number type의 0.3이 결과가 됩니다.

완벽한 해결방법?

사실 2진법 체계를 사용하는 컴퓨터 연산에 있어서 완벽한 해결방법은 없습니다. 왜 그럴까요? 앞선 방법으로 분명 해결했는데 말이죠. 그 이유는 다음과 같습니다.

정밀도 손실

console.log((0.1).toFixed(20)); // 0.10000000000000000555

toFixed() 메서드를 통해 무한 소수를 없애는 데에 성공했다고 생각할 수 있습니다. 하지만 소수점 20번째 자리까지 출력해보니 오차가 아직도 있습니다. 무한 소수를 방지하는 방법은 사실 없다고 할 수 있습니다. 이는 단지 우리가 보여주고 싶은 자리수만큼 소수점 자리수를 잘라 어림수를 만드는 방법으로 해결하고 있는 것입니다.

그래서 왜 부동소수점을 제대로 알아야 하죠?

2진법 체계의 컴퓨터가 부동소수점을 저장할 때에 이러한 정밀도 손실이라는 개념이 있는지를 알아야 제대로 숫자를 처리할 수 있겠죠. 무엇보다 소수점까지 표기하며 숫자를 사용할 때에는 정확도를 염두에 두고 사용하는 것일겁니다. 이러한 정밀도 손실 오류가 있다는 것을 인지하지 않았다면, 우린 JavaScript의 계산 결과에 대해 의심없이 지나칠 것이고, 이러한 작은 오류가 커다란 오류로 돌아올 지 모르기 때문입니다. 물론 금방 알아차렸거나, 이 사실에 대해 인지한 상태라면 다르지만요. 이러한 이유로 부동소수점을 제대로 아는 것이 중요합니다. ✨

💡TL;DR

10진법의 체계와 컴퓨터의 2진법 체계가 달라 소수점 표현 시 정밀도 손실이 있다.

  • 정밀도 손실을 최대한 보정하기 위해서 아래와 같은 방법을 사용한다.

    1. 10의 거듭제곱을 곱한 후 연산 후 다시 나누어줌
    2. toFixed() 메서드 사용
  • 하지만 반올림 규칙을 적용하면 발생하는 정밀도 손실은 우리가 볼 수는 없지만 실제 손실은 발생한다.

이렇게 끝난 줄로만 알았던 부동소수점 정밀도 손실은 점점 스케일이 커져 돌아옵니다…

© 2024 Park Seohyun