
[기술 딥다이브] 0.1 + 0.2 ≠ 0.3인 이유: 부동소수점
한 줄 요약: 0.1 + 0.2가 0.30000000000000004가 되는 이유는 두 수가 2진법에서 무한소수라서, 유한 비트(IEEE 754)에 담는 순간 반올림 오차가 생기기 때문이다.
대부분의 프로그래밍 언어에서 0.1 + 0.2를 계산하면 0.3이 아니라 0.30000000000000004가 나와요. 이것은 버그가 아니라 부동소수점의 본질이에요. 왜 이런 일이 발생하는지 비트 구조와 실제 출력으로 풀어 설명하겠어요.
10진법의 0.1은 2진법으로 딱 떨어지지 않는다
컴퓨터는 수를 2진법으로 저장해요. 0.1을 2진 소수로 변환하면 0.0001100110011...처럼 무한 반복하는 소수가 돼요. 2진법은 1/2, 1/4, 1/8의 합으로 수를 표현하는데, 0.1(= 1/10)은 2의 거듭제곱의 합으로 정확히 표현할 수 없어요. 이것은 10진법에서 1/3 = 0.3333...이 되는 것과 같은 원리예요. 3으로 나누어 떨어지지 않는 것은 10진법의 결함이 아닌 것처럼, 10으로 나누어 떨어지지 않는 것은 2진법의 결함이 아니에요. ’어느 진법으로 쓰느냐’에 따라 유한소수가 되는 수가 달라질 뿐이에요.
IEEE 754: 유한 비트에 무한소수를 담다
컴퓨터는 0.1을 64비트(double) 부동소수점에 담아요. IEEE 754 배정도 형식은 부호 1비트 + 지수 11비트 + 가수 52비트로 구성돼요. 문제는 가수 52비트가 유한하다는 점이에요. 무한히 이어지는 꼬리를 유한한 메모 스티커에 베껴 쓰다 끊기는 것이라, 끊긴 자리에서 반올림이 일어나요. 아래 도식은 이 과정을 보여줘요.
실제로 확인해 보기: 숨은 오차를 드러내기
평소 화면에는 0.1로 ‘예쁘게’ 표시되므로 오차가 안 보여요. 소수점 20자리까지 강제로 출력하면 숨어 있던 값이 드러나요. 파이썬에서 실행한 실제 출력이에요.
>>> print(format(0.1, '.20f'))
0.10000000000000000555
>>> print(format(0.2, '.20f'))
0.20000000000000001110
>>> print(format(0.1 + 0.2, '.20f'))
0.30000000000000004441
>>> 0.1 + 0.2 == 0.3
False
0.1로 알고 있던 값은 사실 0.1000…000555이고, 0.2도 0.2000…0001110예요. 둘을 더하면 각자의 오차가 합쳐져 0.3000…0004441이 되고, 표시할 때 반올림되어 0.30000000000000004로 눈에 보이는 것이에요. 0.1 + 0.2 === 0.3이 false를 반환하는 것은 언어의 버그가 아니라 IEEE 754를 준수하는 모든 시스템의 정상적인 동작이에요.
왜 이렇게 설계했는가: 속도와 범용성
부동소수점은 매우 넓은 범위의 값(아주 작은 수부터 아주 큰 수까지)을 고정 비트로 빠르게 처리하기 위해 만든 타협이에요. 십진 정확 계산보다 ‘대략 맞게, 빠르게, 넓은 범위를’ 선택한 것이에요. 그래서 과학 계산과 그래픽에는 적합하지만, 금전 계산에는 부적합해요.
그러면 금전 계산은 어떻게: 십진 타입으로
금액처럼 십진 정확도가 중요한 경우에는 다음 방법을 사용해요: (1) 센트 단위 정수로 계산(예: 1000원 = 100000센트), (2) 십진 전용 타입(Python의 decimal.Decimal, Java의 BigDecimal, JavaScript의 Decimal 라이브러리), (3) 표시할 때만 반올림. 파이썬에서 실제로 비교하면:
>>> from decimal import Decimal
>>> Decimal('0.1') + Decimal('0.2')
Decimal('0.3') ← 딱 떨어짐
주의할 점이 하나 있어요. Decimal(0.1)처럼 float 값을 그대로 넘기면 앞서 본 오차 있는 값(0.100000…0055…)을 그대로 받아 와요. 정확한 십진 계산이 필요하면 Decimal('0.1')처럼 문자열로 넘겨야 해요. float로 절대 금전을 처리하지 않는다는 원칙과 함께 기억할 두 가지예요.
비교할 때는 ’오차 허용’으로
0.1 + 0.2 == 0.3 대신, Math.abs(a - b) < 1e-9처럼 ’충분히 가까우면 같다’로 비교해요. 이것이 부동소수점 비교의 표준 방식이며, 수치 계산 라이브러리들이 내부적으로 쓰는 방식이기도 해요.
어떻게 연결되는가
한 줄로 요약하면: 0.1과 0.2가 2진 무한소수이므로 유한 비트(IEEE 754)에 담으면 반올림 오차가 발생하고, 둘을 더했을 때 그 오차가 0.30000000000000004로 드러난다는 것이에요. 컴퓨터가 ’10진법’이 아니라 ’2진법’으로 수를 표현한다는 사실 하나에서 비롯된, 가장 유명한 ’버그 같은 정상 동작’예요. 같은 원리의 저장 방식 문제는 문자에도 있어요. 한글이 왜 3바이트가 되는지는 UTF-8 인코딩 글에서 이어져요.
출처: 위키백과: IEEE 754 · 위키백과: 부동소수점 산술 · 0.30000000000000004.com
이미지: Sss41, Wikimedia Commons (CC-BY-SA 3.0) — 원본