별 개수: 50
속도: 2

JavaScript

웹 개발의 필수 언어

동적인 웹 페이지 구현을 위한 핵심 프로그래밍 언어.

Java

객체지향 프로그래밍

안정적이고 확장성 있는 백엔드 개발의 대표 언어.

HTML

웹의 기초

웹 페이지의 구조를 정의하는 마크업 언어.

React

현대적 UI 라이브러리

효율적인 사용자 인터페이스 구축을 위한 JavaScript 라이브러리.

CSS

웹 디자인의 핵심

웹 페이지의 시각적 표현을 담당하는 스타일 언어.

Spring

자바 웹 프레임워크

기업급 애플리케이션 개발을 위한 강력한 프레임워크.

JavaScript

웹 개발의 필수 언어

동적인 웹 페이지 구현을 위한 핵심 프로그래밍 언어.

Java

객체지향 프로그래밍

안정적이고 확장성 있는 백엔드 개발의 대표 언어.

HTML

웹의 기초

웹 페이지의 구조를 정의하는 마크업 언어.

React

현대적 UI 라이브러리

효율적인 사용자 인터페이스 구축을 위한 JavaScript 라이브러리.

CSS

웹 디자인의 핵심

웹 페이지의 시각적 표현을 담당하는 스타일 언어.

Spring

자바 웹 프레임워크

기업급 애플리케이션 개발을 위한 강력한 프레임워크.

JavaScript

웹 개발의 필수 언어

동적인 웹 페이지 구현을 위한 핵심 프로그래밍 언어.

Java

객체지향 프로그래밍

안정적이고 확장성 있는 백엔드 개발의 대표 언어.

HTML

웹의 기초

웹 페이지의 구조를 정의하는 마크업 언어.

React

현대적 UI 라이브러리

효율적인 사용자 인터페이스 구축을 위한 JavaScript 라이브러리.

CSS

웹 디자인의 핵심

웹 페이지의 시각적 표현을 담당하는 스타일 언어.

Spring

자바 웹 프레임워크

기업급 애플리케이션 개발을 위한 강력한 프레임워크.

프로그래밍/개념 뽀개기

부동 소수점에 대해서(무한 소수, 소수점 오차)

lamarcK 2025. 5. 21. 02:17

I. 소수(Decimal Number)란 무엇인가?

10진법에서의 소수는 정수가 아닌 분수를 0과 .(소수점) 그리고 1~9까지의 자연수를 활용해서 표현하는 방법이다. 0.1은 1/10을 표현한 것이고 0.5는 5/10를 표현한 것이다. 소수는 기본적으로 분수의 다른 표현법이며, 소수점 왼쪽은 정수부, 오른쪽은 소수부라고 한다. 소수부의 각 자리는 10의 거듭제곱분의 1을 의미한다.

그런데 여기서 문제점이 발생한다.

1. 컴퓨터에서 소수점 연산 시 오차 문제

수학적으로 예상되는 값과 실제로 나오는 값이 다르다. 이유는 프로그래밍 언어가 0과 1로 이루어진 2진법을 사용하기 때문이다.

javascript
클릭하여 코드 펼치기
// 부동 소수점 나머지 연산
let remainder3 = 3.14 % 2; // 1.14
console.log("부동 소수점 나머지:", remainder3);
클릭하여 코드 복사

3.14를 2로 나누면 나머지가 1.14로 나와야 하지만 실제로는 1.1400000000000001 같은 값이 나오게 된다.

2. 어째서 그럴까?

앞서 말했듯이 소수는 분수를 자연수를 활용해서 표현한 것이다. 10진법 수 체계에서 0.14는 14/100으로 표현이 가능하다. 가지고 있는 숫자의 레인지가 0~9까지이기 때문에 1도 표현 가능하고 4도 표현 가능하고 100도 10의 제곱으로 표현이 가능하다.

그런데 2진법에서는 0과 1이라는 자연수 밖에 활용하지 못하며, 분모는 2의 거듭제곱만을 활용해서 수치를 표현해야 한다.

 

그런데 2진법으로 나타낼 수 있는 분수는 1/2의 n제곱 뿐이다. 따라서 0.14를 2진법으로 표현하려면

  • 1/2 = 0.5
  • 1/4 = 0.25
  • 1/8 = 0.125 (0.14보다 작음)
  • 1/16 = 0.0625 (0.14보다 큼)

이처럼 2의 거듭제곱의 역수들을 조합해서 0.14에 최대한 가깝게 근사해야 하는 상황이 된다.

3. n진법 수체계의 한계

예를 들어서

10진법에서 1/3은 0.33333... 과 같이 무한 소수로 표현된다. 하지만 3진법에서 1/3은 0.1(3)로 표현되어 유한한 자릿수의 수로 정확하게 표현할 수 있다. 숫자의 레인지가 1, 2, 3 이기 때문이다.

마찬가지로 10진법에서는 1.14를 정확하게 표현이 가능하다. 숫자의 레인지가 1, 2, 3, 4, 5... 9 이기 때문에 0.14를 표현 가능하다. 이건 10진법 분수로 14/100 = 14/(10*10)이기 때문이다.

하지만 2진법에서는 표현 가능한 숫자가 0과 1 뿐이기 때문에 0.14에 정확히 대응하는 소수를 나타내는 것이 불가능하다. 결국 해당 값에 가깝도록 여러 값들을 더해서 근삿값을 보여주는 것이다.

java
클릭하여 코드 펼치기
0.14 = 0 × (1/2)   = 0 × 0.5    = 0
     + 0 × (1/4)   = 0 × 0.25   = 0
     + 0 × (1/8)   = 0 × 0.125  = 0
     + 1 × (1/16)  = 1 × 0.0625 = 0.0625
     + 1 × (1/32)  = 1 × 0.03125 = 0.03125
     + 1 × (1/64)  = 1 × 0.015625 = 0.015625
     + 1 × (1/128) = 1 × 0.0078125 = 0.0078125
     + ...
클릭하여 코드 복사

1. 계산 과정

2진법 상으로 0.14를 분수로 표현해야 하는데 1/8과 1/16 사이의 수는 2진법으로 표현 불가. 때문에 1/8 보다 작은 수들을 더하는 방식으로 표현해야 하는데 2진법의 한계로 분모가 2의 거듭제곱인 수를 아무리 더해도 0.14에 가까워 질뿐 완벽히 동일해질 수 없어서 무한 소수가 돼버린다.

 

0.14가 되려면 1/16보다 작은 숫자를 더해서 해결해야 하는데 바로 한 단계 표현 가능한 가장 작은 숫자가 1/32이고 이것을 더해도 0.14는 되지 못하지만 그렇다고 1/32를 다시 더하자니 1/32+1/32 = 1/16을 더한 것이 되고 원래 존재하던 1/16에 1/16을 더하면 1/8로 0.14보다 커져버리는 오류가 발생한다. 결국 1/16+1/32+1/64( 1/32 다음 가장 작은 수) 식으로 계속 작은 숫자를 무한하게 더하게 돼버리는 것이다. 결과적으로 2진법 비트로 표현하면 무한히 이어지는 소수가 돼버린다.

4. IEEE 754 표준과 부동소수점

그런데 그렇다고 무한 소수를 정말 영원히 표현할 수는 없기 때문에 부동소수점을 표현하는 가장 일반적인 표준인 IEEE 754 표준을 따라서 소수점을 특정 자릿수까지 표현하게 된다. 이 경우 일반적으로 32비트나 64비트를 사용하게 되는데 비트 수에 따라 단일 정밀도와 배정밀도로 나뉜다.

부동소수점의 "부동(浮動, floating)"은 "떠다닌다"는 의미다. 즉 소수점이 고정되지 않고 움직일 수 있는 방식의 실수 표현법이라고 할 수 있다.

 

단일 정밀도(single precision)는 32비트를 사용하는 것을 말하며

배정밀도(double precision)는 64비트를 사용하는 것을 말한다.

1. 정밀도(Precision)란?

숫자를 얼마나 정확하게 표현할 수 있는지를 나타내는 지표다. 간단하게 말하면 "크레파스의 색이 몇가지냐"라고 할 수 있다. 32색 크레파스보다 64색 크레파스가 더 다양한 색상을 표현할 수 있듯이 64비트도 32비트보다 더 많은 수를 표현할 수 있다.

  • float (32비트)
    • 부호 1비트
    • 지수부 8비트
    • 가수부 23비트
    • 십진수로 약 7자리까지 정확도 보장
    • 예: 3.1415927 (7자리)
  •  double (64비트)
    • 부호 1비트
    • 지수부 11비트
    • 가수부 52비트
    • 십진수로 약 15-17자리까지 정확도 보장
    • 예: 3.141592653589793 (15자리)

2. 구성 요소

  • 부호(Sign bit) : 말 그대로 +와 - 부호를 나타내는 비트다. 
    • 0: 양수(Positive)
    • 1: 음수(Negative)
  • 지수부(Exponent) : 실수의 크기, 즉 소수점의 위치를 나타내는 부분이다.
  • 가수부(Significand) : 실수의 유효 숫자 (Significant Digits) 를 나타내는 부분이다. 이는 숫자의 정밀도를 결정한다.

그런데 이러한 표현법도 결국 자릿수의 한계가 있기 때문에 특정 자릿수에서 반올림을 수행하고 그 과정에서 1이 남게 된다. 정말로 무한하게 표현을 할 수도 있겠지만 "수를 표현한다"라는 말은 결과적으로 "메모리 칸수를 차지한다"라는 것을 알아야 한다. 무한대를 명시적으로 표현한다는 것은 결국 메모리를 무한하게 사용한다는 뜻이다. 소수점 계산기를 사용해서 1 + 0.14를 계산했는데 계산이 무한히 이어져서 무한 로딩에 걸리게 되고 오랜 시간 기다렸는데 결과적으로 컴퓨터가 사용할 수 있는 메모리의 용량을 초과해서 계산 오류가 나버리면 얼마나 웃긴 상황이겠는가? 괜히 슈퍼 컴퓨터들이 무한소수인 파이(π)를 구해보는 것이 아니다.

3. 결과적으로

최적화 과정에서 무한 소수는 특정 자릿수까지만 표현되지만 완벽한 소수점 표현이 불가능하니 0.0000... 1 같은 자투리 소수가 남게 된다.

java
클릭하여 코드 펼치기
double A = 0.1 + 0.2;
double  B = 0.3;
boolean  result = (A == B);
클릭하여 코드 복사

이처럼 0.1과 0.2를 더했는데도 0.3이 아닌 것을 알 수 있다.

 

이러한 숫자들이 계산에 포함될수록 조금씩 오차가 커지기 때문에 정확한 계산을 위해서는 가능한 모든 계산을 정수로 변환하여 수행해야 하는 것이 좋다.

 

금융 계산의 경우

javascript
클릭하여 코드 펼치기
// 문제가 되는 상황
let price = 0.1 + 0.2;  // 0.30000000000000004
let quantity = 3;
let total = price * quantity;  // 0.90000000000000012

// 이런 오차는 금융 거래에서 심각한 문제를 일으킬 수 있음
// 수백만 건의 거래에서 누적된 오차는 큰 금액 차이를 만듦

let priceInCents = 10 + 20;  // $0.10 + $0.20 = 30cents
let quantity = 3;
let totalInCents = priceInCents * quantity;  // 90cents
let totalInDollars = totalInCents / 100;  // $0.90 정확히 계산됨
클릭하여 코드 복사

해결책: 센트 단위로 변환하여 정수 계산

 

로켓 공학의 경우

java
클릭하여 코드 펼치기
// 문제가 되는 상황
double velocity = 11.2;  // km/s
// 수천 번의 계산 후 오차 누적
// 1mm의 오차도 치명적인 결과를 초래할 수 있음

// 해결책: 밀리미터 단위로 변환
int velocityInMM = 11200000;  // mm/s
// 정수 계산으로 오차 없이 정확한 결과
클릭하여 코드 복사

이동