부동소수점의 함정
0.1 + 0.2가 정확히 0.3이 아닌 이유를 이진 부동소수점 표현으로 이해하고, math.isclose와 Decimal로 상황에 맞게 다루는 법을 정리합니다.
지난 글에서 클로저의 시점 함정을 봤다면, 이번 글에서는 이 묶음의 마지막으로 파이썬을 넘어 거의 모든 언어에 공통된 함정 — 부동소수점 연산을 다룬다. 0.1 + 0.2 == 0.3이 False가 되는 그 유명한 현상이다. 파이썬의 버그가 아니라, 컴퓨터가 실수를 저장하는 방식 자체에서 비롯되는 일이라 원리를 알아 둘 가치가 있다.
문제의 현상
직접 실행해 보면 누구나 한 번쯤 당황하는 결과가 나온다.
>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False
분명히 초등학교 산수로는 0.3인데, 컴퓨터는 미세하게 다른 값을 내놓는다. 그리고 그 미세한 차이 때문에 == 비교가 어긋난다.
왜 이런 일이 — 이진수로는 0.1을 정확히 못 적는다
원인은 컴퓨터가 실수를 이진(2진) 부동소수점으로 저장하기 때문이다. 우리가 10진수로 1/3을 적으면 0.3333...으로 무한히 반복되어 정확히 못 적는 것처럼, 2진수로 0.1(10진수)을 적으면 0.0001100110011...이 무한히 반복된다.
컴퓨터의 저장 공간은 유한(보통 64비트)하므로, 이 무한 소수를 어딘가에서 잘라 가장 가까운 값으로 반올림해 저장한다. 그래서 0.1로 저장된 값은 사실 진짜 0.1이 아니라 그에 아주 가까운 다른 값이다. 0.2도, 0.3도 마찬가지다. 이렇게 미세하게 어긋난 값들을 더하니, 결과 역시 0.3의 저장값과 미세하게 달라진다.
이건 파이썬만의 문제가 아니다. 같은 IEEE 754 표준을 쓰는 자바, C, 자바스크립트 등 거의 모든 언어에서 똑같이 일어난다.
해결 1: 실수는 == 대신 근사 비교
실수 두 개가 “같은지” 판단할 때는 ==를 쓰지 않는다. 대신 “충분히 가까운가”를 본다. 파이썬 3.5+에는 이를 위한 math.isclose가 표준으로 들어 있다.
import math
math.isclose(0.1 + 0.2, 0.3) # True
math.isclose(0.1 + 0.2, 0.3,
rel_tol=1e-9) # 허용 오차도 지정 가능
rel_tol은 상대 오차, abs_tol은 절대 오차다. 0에 가까운 값을 비교할 때는 abs_tol도 함께 지정해야 정확하다. 직접 abs(a - b) < 1e-9 식으로 비교해도 되지만, isclose가 상대·절대 오차를 함께 다뤄 주므로 더 안전하다.
해결 2: 정확성이 필요하면 Decimal
돈 계산처럼 단 1원의 오차도 허용되지 않는 경우에는, 애초에 이진 부동소수점을 쓰지 않는다. decimal.Decimal은 10진수를 정확하게 표현하고 연산한다.
from decimal import Decimal
Decimal("0.1") + Decimal("0.2") # Decimal('0.3') — 정확!
Decimal("0.1") + Decimal("0.2") == Decimal("0.3") # True
중요한 점은 Decimal에 문자열로 값을 넘기는 것이다. Decimal(0.1)처럼 실수를 넘기면 이미 어긋난 부동소수점 값이 그대로 들어가 의미가 없다. 반드시 Decimal("0.1")로 적어야 정확한 10진수가 된다. 금액 계산, 회계, 세금처럼 정확성이 핵심인 도메인에서는 Decimal(또는 정수로 환산해 다루는 방식)이 정석이다.
해결 3: 표시할 때만 반올림
화면에 보여 줄 때 긴 꼬리를 숨기고 싶다면 round나 포맷팅을 쓴다. 단, 이건 표시용일 뿐 연산의 정확성을 보장하지 않는다는 점을 기억해야 한다.
value = 0.1 + 0.2
print(round(value, 2)) # 0.3
print(f"{value:.2f}") # '0.30'
round로 중간 계산값을 자르며 누적하면 오히려 오차가 쌓일 수 있으니, 반올림은 마지막 출력 단계에서만 하는 것이 좋다.
정리
부동소수점의 함정은 “이진수로는 0.1 같은 10진 소수를 정확히 표현할 수 없다”는 근본적인 사실에서 나온다. 실수의 동등 비교에는 == 대신 math.isclose를, 돈처럼 정확성이 필수인 곳에는 Decimal을, 그리고 반올림은 표시 단계에서만 쓰자. 컴퓨터가 숫자를 어떻게 저장하는지 한 번 이해해 두면, 이 현상은 더 이상 당황스러운 버그가 아니라 예측 가능한 동작이 된다.
이것으로 파이썬의 보안 함정과 흔한 버그들을 짚어 본 묶음을 마친다. 안전한 난수와 비밀번호 저장에서 시작해, 직렬화와 코드 실행의 위험, 그리고 임포트·인코딩·기본 인자·클로저·부동소수점까지 — 실무에서 자주 발을 헛디디는 지점들을 한 바퀴 돌았다. 각 함정의 공통점은 “파이썬이 무엇을, 언제, 어떻게 처리하는가”를 정확히 이해하면 대부분 자연스럽게 풀린다는 것이다.
지난 글: 클로저의 늦은 바인딩 함정
읽어주셔서 감사합니다. 😊