← 홈으로

CS · 2025/08

컴퓨터는 0.1을 모른다

0.1 + 0.2 = 0.30000000000000004

JavaScript 콘솔에서 0.1 + 0.2를 입력하면 0.3이 아니라 0.30000000000000004가 나온다.

버그가 아니라 컴퓨터의 정상적인 동작이다.

계산기 앱에서는 0.3이 나온다. 사용자 편의를 위해 반올림하기 때문이다. 하지만 내부적으로는 여전히 부정확한 값으로 저장되어 있다.

컴퓨터에게 0.1이란?

10진법에서 1/3이 0.333...이 되는 것처럼, 2진법에서 0.1은 0.000110011001100110011...이 된다.

끝없이 0011이 반복되는 순환소수다.

0.1을 2진법으로 변환

0.1 × 2 = 0.2 → 정수부: 0
0.2 × 2 = 0.4 → 정수부: 0  
0.4 × 2 = 0.8 → 정수부: 0
0.8 × 2 = 1.6 → 정수부: 1
0.6 × 2 = 1.2 → 정수부: 1
0.2 × 2 = 0.4 → 정수부: 0 (반복 시작)
0.4 × 2 = 0.8 → 정수부: 0
0.8 × 2 = 1.6 → 정수부: 1
0.6 × 2 = 1.2 → 정수부: 1
...

결과: 0.000110011001100110011...

이 패턴이 영원히 반복된다.

다른 소수들은 어떨까?

10진법2진법정확히 표현 가능한가?
0.50.1✅
0.250.01✅
0.1250.001✅
0.10.000110011...❌
0.20.001100110011...❌
0.30.010011001100...❌

분모가 2의 거듭제곱인 분수만 2진법에서 정확히 표현된다.

  • 1/2 = 0.5 → ✅
  • 1/4 = 0.25 → ✅
  • 1/5 = 0.2 → ❌
  • 1/10 = 0.1 → ❌

완벽을 추구했던 인류, 타협해야 했던 컴퓨터

기원전 5세기, 피타고라스 학파는 √2가 끝나지 않는 무리수라는 사실을 발견했다.

한 변이 1인 정사각형의 대각선 길이를 구하면:

1² + 1² = c²
2 = c²
c = √2

√2 = 1.414213562373095... 처럼 끝없이 이어진다. 컴퓨터가 등장하면서 이 문제는 공학적 현실로 돌아왔다. 무한한 숫자를 유한한 비트에 저장해야 하는 문제다.

우리도 이미 타협하고 있다

π = 3.14159265...를 계산할 때 3.14로 쓰고, √2 = 1.41421356... 대신 1.41을 쓴다.

  • 화폐: 1원 단위 (0.1원은 없음)
  • 시간: 스톱워치의 최소 측정 단위는 0.01초
  • 길이: 자의 최소 눈금은 1mm
  • 무게: 일반적인 저울도 0.1g, 0.01g까지만 측정

무한한 숫자를 유한하게 타협해서 사용하는 건 우리도 마찬가지다.

컴퓨터도 마찬가지다

무한한 숫자를 유한한 비트로 저장해야 하기 때문에, 컴퓨터도 딱 떨어지지 않는 수를 근사해서 표현한다.

한 가지 더 큰 제약이 있다.

오직 2진법만 사용한다는 점이다

0.1은 10진법에서 깔끔한 소수지만, 2진법으로 바꾸는 순간 0.000110011001100...처럼 끝없이 반복되는 순환소수가 된다. (10진법에서 1/3 = 0.333…과 같은 원리다.)

결국 컴퓨터는 0.1을 정확히 표현하지 못하고 가까운 수치로 근사한다.


IEEE 754, 전 세계가 합의한 '불완전한' 표준

IEEE 754는 1985년 미국 전기전자학회(IEEE)에서 제정한 부동소수점 수의 표현 및 연산 표준이다.

"완벽한 계산은 포기하고, 대신 모든 컴퓨터가 같은 방식으로 근사하자."

"부동소수점의 아버지" 윌리엄 카한(William Kahan)을 비롯한 엔지니어들이 참여했으며, 현재 거의 모든 컴퓨터 시스템에서 실수를 표현하는 사실상의 표준이 되었다.

과학적 표기법의 2진법 버전

과학적 표기법으로 123,000,000 = 1.23 × 10⁸처럼 나타내듯, IEEE 754도 비슷한 방식이다. 다만 2진법을 사용한다.

1.101 × 2³ (2진법 버전)

32비트 부동소수점 구조

32비트 부동소수점은 다음과 같이 구성된다.

각 필드의 역할

1. 부호 비트 (Sign bit)

  • 0 : 양수
  • 1 : 음수

2. 지수 부분 (Exponent, 8비트)

  • 실제 지수에 127을 더한 값 (바이어스된 지수)
  • 범위 : 0~255
  • 실제 지수 범위 : -126~127

3. 분수 부분 (Fraction, 23비트)

  • 소수점 이하 부분만 저장
  • 정규화된 수에서는 암묵적으로 1.이 앞에 붙음

0.1을 IEEE 754로 저장하기

1. 2진법으로 변환

0.1 = 0.000110011001100110011... (2진법)

이진수로 변환하면 무한히 반복되는 소수가 된다.

2. 정규화

이진수 부동소수점은 항상 1.xxxx × 2^n 형태로 표현된다.

0.000110011001100110011... 

첫 번째 1이 소수점 뒤 4번째 자리(2⁻⁴ 자리)에 있으므로, 소수점을 오른쪽으로 4자리 이동시킨다.

0.000110011001100110011... 
= 1.10011001100110011... × 2⁻⁴

정규화란? 항상 1.xxxx × 2^n 형태로 만드는 것이다.

  • 오른쪽으로 4자리 이동 → 2⁻⁴를 곱해줌
  • 왼쪽으로 3자리 이동 → 2³을 곱해줌

3. 각 필드 계산

부호: 양수이므로 0

지수: -4 + 127(바이어스) = 123

123 = 01111011 (2진법)

왜 Bias를 더하나?
지수는 양/음수 모두 가능하지만, 저장은 부호 없는 정수(Unsigned Integer) 로 해야 한다.
32비트에서는 127을 더해 -126~127 → 0~255로 표현한다.
127은 8비트 지수의 중간값 (2⁷ - 1) 이기도 하다.

분수 부분: 1. 뒤의 23비트

10011001100110011001100

4. 결과

0 01111011 10011001100110011001100

23비트로는 무한 반복되는 패턴을 완전히 표현할 수 없으므로, 실제로 저장되는 값은 근사값이다.

64비트 부동소수점 (Double)

비트 수가 늘어나면 더 많은 숫자를 더 정밀하게 표현할 수 있다.

  • 더 높은 정밀도
  • 지수 바이어스: 1023
  • 지수 범위: -1022~1023

0.1처럼 32비트로는 정확하게 표현하기 어려운 수들도 64비트에선 근사치 오차를 훨씬 줄여서 저장할 수 있다.


부동소수점 오차로 인한 실제 사례들

1991년, 패트리어트 미사일의 실패

걸프 전쟁 당시, 패트리어트 미사일 시스템이 부동소수점 오차로 인해 실패했다.

문제의 원인

  • 시스템 가동 시간을 1/10초 단위로 측정
  • 1/10 = 0.1을 24비트 고정소수점으로 표현
  • 0.1의 실제 저장값: 0.09999999999534
  • 100시간 후 누적 오차: 약 0.34초

결과
날아오는 스커드 미사일의 예상 위치가 실제보다 687미터 벗어나 요격 실패. 미군 28명이 사망했다.

1996년, 아리안 5호 로켓의 폭발

유럽우주기구(ESA)의 아리안 5호 로켓이 발사 37초 만에 공중 폭발했다.

문제의 원인

  • 64비트 부동소수점 값을 16비트 정수로 강제 변환
  • 수평 속도 값이 16비트 범위 초과
  • 소프트웨어 예외 발생 → 시스템 종료 → 비행 제어 상실

손실
개발비 5억 달러 증발

2012년, 나이트 캐피털의 알고리즘 거래 실패

미국의 대형 증권사 나이트 캐피털은 자동 매매 알고리즘의 반올림 오차로 인해 45분 만에 4억 4천만 달러의 손실을 입었다.

문제의 원인

  • 부동소수점 연산 중 잘못된 반올림 처리
  • 주문량 계산에 오류 발생
  • 정상 가격보다 수백 배 비싼 가격에 매수 주문 발생

결과
기업 파산 직전까지 몰리며 타사에 인수됐다.


그래서 어떻게 해결하나?

일상적인 계산에서는 반올림으로 충분한 경우가 많다.

console.log((0.1 + 0.2).toFixed(1));      // "0.3"

toFixed는 문자열 포맷일 뿐, 내부적으로 오차 자체는 여전히 존재한다. 사람이 눈으로 확인하기 좋은 숫자로 표현하는 것이다.

더 정확한 계산이 필요한 경우:

  • 금융 시스템 — 수백억 원이 오가는 계좌 간 이체
  • 항공우주 — 위성 궤도 계산에서의 미세한 시간 차이
  • 과학 계산 — 시뮬레이션에서의 에너지 보존
  • 실시간 제어 — 자동차 ABS 제어처럼 밀리초 단위가 중요한 경우

이런 경우에는 센트 단위로 정수 계산을 하거나, 전용 라이브러리를 사용한다.


부동소수점의 특수값

IEEE 754는 숫자 외에도 특수한 값들을 정의한다.

Infinity (무한대)

0으로 나누거나, 표현할 수 있는 수의 범위를 초과하면 Infinity가 발생한다.

console.log(1 / 0);        // Infinity
console.log(-1 / 0);       // -Infinity

NaN (Not a Number)

정의되지 않은 연산의 결과다.

console.log(0 / 0);        // NaN
console.log(NaN === NaN);  // false

NaN은 자기 자신과도 같지 않다고 정의되어 있어서 false가 나온다.


0.1 + 0.2 = 0.30000000000000004.

버그가 아니다. 무한한 수를 유한한 비트에 구겨 넣으려다 생긴 일이다. 피타고라스도 √2 앞에서 멈췄고, 우리도 π를 3.14로 쓰고, 컴퓨터는 0.1을 근사한다.

완벽한 표현이 불가능하다는 걸 알면서도, 전 세계가 같은 방식으로 타협하기로 합의했다. 그 합의가 IEEE 754다.