// chikrii_algorithm_strategic_labprovided as internal research — read-only
root@chikrii-lab:~/chikrii-lab/softlab-archive/numeric-precision.md

수치 정밀도 오차 줄이는 방법: 부동소수점 계산을 실무에서 안전하게 다루기

왜 0.1을 세 번 더했는데 0.3과 정확히 같지 않을까요? 수치 정밀도 오차 줄이는 방법을 찾는다면 먼저 이 현상이 언어의 버그가 아니라 부동소수점 표현 방식의 한계라는 점부터 이해해야 합니다. 금융 금액, 센서 데이터, 통계 평균, 시뮬레이션, 머신러닝 학습 로그처럼 숫자를 반복해서 다루는 시스템에서는 아주 작은 오차도 비교 실패, 누적 합계 차이, 재현성 문제로 이어질 수 있습니다.

핵심은 오차를 완전히 없애는 것이 아니라, 목적에 맞는 자료형과 알고리즘을 선택하고 허용 가능한 범위 안에서 관리하는 것입니다. 아래에서는 표현 오차, 반올림 오차, 소거 오차를 구분한 뒤 실무에서 바로 적용할 수 있는 비교, 합산, 정밀도 선택, 금융 계산 전략을 정리합니다.

수치 정밀도 오차가 발생하는 부동소수점 표현 구조

수치 정밀도 오차가 생기는 이유

컴퓨터는 실수를 무한히 정확하게 저장하지 않습니다. 대부분의 현대 시스템에서 널리 쓰이는 IEEE 754 방식의 부동소수점은 부호, 지수, 가수로 실수를 근사해 저장합니다. 이 구조는 매우 빠르고 넓은 범위의 값을 다룰 수 있지만, 저장 공간이 유한하기 때문에 모든 실수를 정확히 표현할 수는 없습니다.

대표적인 예가 0.1입니다. 사람에게는 간단한 십진 소수지만, 이진 부동소수점에서는 유한한 비트로 딱 떨어지지 않는 반복값이 됩니다. Python 공식 문서도 부동소수점 산술의 한계를 설명하며 0.1의 내부 근사가 실제 1/10과 정확히 같지 않고, 0.1 + 0.1 + 0.1 == 0.3 비교가 False가 될 수 있다고 안내합니다.

표현 오차와 반올림 오차는 다릅니다

표현 오차는 값을 저장하는 순간 발생합니다. 0.1처럼 이진수로 정확히 표현되지 않는 값은 메모리에 들어갈 때 이미 가장 가까운 표현 가능한 값으로 근사됩니다. 반올림 오차는 연산 결과를 다시 제한된 정밀도 안에 맞추는 과정에서 생깁니다. 덧셈, 곱셈, 나눗셈을 할 때마다 중간 결과가 무한한 정밀도로 유지되는 것이 아니라 다시 표현 가능한 값으로 정리되기 때문입니다.

또 하나 헷갈리는 부분은 출력입니다. 화면에 0.3처럼 보인다고 해서 내부 저장값이 정확히 0.3이라는 뜻은 아닙니다. 많은 언어와 도구는 사람이 읽기 좋은 형태로 값을 표시합니다. 출력 포맷은 보기를 정돈할 뿐, 내부 오차 자체를 없애거나 이후 계산의 안정성을 보장하지 않습니다.

유효숫자 손실과 소거 오차

비슷한 크기의 두 수를 빼면 앞자리의 중요한 숫자가 사라지고 상대 오차가 커질 수 있습니다. 예를 들어 x와 y가 거의 같을 때 x² – y²를 직접 계산하면 두 큰 값이 서로 상쇄되면서 남은 작은 차이에 오차가 크게 반영될 수 있습니다. 상황에 따라 (x – y)(x + y)처럼 수식을 바꾸면 더 안정적인 결과를 얻을 수 있습니다.

다만 모든 수식 변형이 항상 안전한 것은 아닙니다. 입력 범위, 오버플로 가능성, 언더플로 가능성, 라이브러리 함수의 구현까지 함께 봐야 합니다. log(1+x)처럼 x가 매우 작을 때는 일반 log 계산보다 log1p 같은 전용 함수를 쓰는 편이 안정적인 경우가 많습니다.

수치 정밀도 오차 줄이는 방법 핵심 체크리스트

실무에서 수치 정밀도 오차 줄이는 방법은 하나의 마법 같은 함수가 아니라 여러 선택의 조합입니다. 다음 체크리스트를 기준으로 문제를 분해해 보세요.

  • 필요한 정밀도와 허용 오차를 먼저 정합니다.
  • float32 대신 float64 또는 더 높은 정밀도가 필요한지 검토합니다.
  • 부동소수점 직접 동등 비교를 피하고 허용 오차를 사용합니다.
  • 대량 합산에는 안정적인 합산 알고리즘을 적용합니다.
  • 금융·회계 계산은 이진 float 대신 Decimal, 정수 최소 단위, 고정소수점을 고려합니다.
  • 큰 수와 작은 수를 섞어 더할 때 연산 순서를 점검합니다.
  • 알고리즘 자체가 수치적으로 안정적인지 확인합니다.

float 비교는 == 대신 허용 오차를 사용합니다

부동소수점 결과를 비교할 때 가장 흔한 실수는 ==로 정확히 같은지 확인하는 것입니다. 계산 과정에서 생기는 미세한 차이 때문에 논리적으로 같은 결과도 직접 비교에서는 실패할 수 있습니다. Python에서는 math.isclose()처럼 상대 오차와 절대 오차를 함께 고려하는 비교 방식을 사용할 수 있습니다.

import math

x = 0.1 + 0.1 + 0.1
print(x == 0.3)                 # False가 될 수 있음
print(math.isclose(x, 0.3))      # 일반적으로 True

절대 오차는 0 근처의 값에서 유용합니다. 예를 들어 계산 결과가 0에 가까워야 하는지 확인할 때 기준값 자체가 작기 때문에 상대 오차만으로는 판단이 애매할 수 있습니다. 반대로 값의 규모가 매우 클 때는 상대 오차가 더 자연스럽습니다. 실무에서는 두 기준을 함께 두고, 도메인 요구사항에 맞게 tolerance를 명시하는 방식이 안전합니다.

절대 오차와 상대 오차 차이를 설명하는 그래프

누적 합산은 순서와 알고리즘을 바꿉니다

단순 반복 합산은 매 단계에서 반올림을 반복하므로 데이터가 많을수록 오차가 누적될 수 있습니다. 특히 아주 큰 값과 아주 작은 값이 섞여 있거나, 큰 양수와 큰 음수가 상쇄되는 데이터에서는 합산 순서에 따라 마지막 자릿수가 달라질 수 있습니다.

가능한 대응은 여러 가지입니다. 작은 값부터 더하도록 정렬하거나, 배열을 둘로 나누어 더하는 pairwise summation을 사용하거나, Kahan summation처럼 손실된 보정값을 추적하는 기법을 적용할 수 있습니다. Python에서는 일반 sum()보다 math.fsum()이 더 정확한 결과를 주는 경우가 있습니다. 다만 더 안정적인 방법은 보통 계산 비용이 더 들 수 있으므로, 데이터 크기와 정확도 요구사항을 함께 봐야 합니다.

NumPy를 쓴다면 np.sum의 dtype 인자를 확인하세요. float32 배열을 대량 합산할 때 누산 타입을 float64로 지정하면 오차를 줄이는 데 도움이 될 수 있습니다. NumPy 문서도 numpy.sum의 부동소수점 정밀도와 dtype에 대해 설명하며, 상황에 따라 더 높은 정밀도의 accumulator를 사용할 수 있음을 안내합니다.

import numpy as np

arr = np.array([0.1] * 1_000_000, dtype=np.float32)
print(np.sum(arr))
print(np.sum(arr, dtype=np.float64))

정밀도 타입을 올리되 비용도 함께 봅니다

float32에서 float64로 올리면 표현 가능한 유효 자릿수가 늘어나 오차가 줄어드는 경우가 많습니다. 하지만 정밀도를 올린다고 오차가 사라지는 것은 아닙니다. 또한 메모리 사용량, 캐시 효율, GPU 처리량, 네트워크 전송량에 영향을 줄 수 있습니다. 머신러닝에서는 float32, float16, bfloat16 같은 낮은 정밀도를 성능 때문에 쓰기도 하고, 통계 검증이나 최종 집계에서는 float64를 쓰기도 합니다.

CPU와 GPU 결과가 조금 다르다고 해서 반드시 한쪽이 틀린 것은 아닙니다. 병렬 reduction의 순서, FMA 사용 여부, 컴파일러 최적화, 하드웨어 구현 차이 때문에 합법적인 범위의 차이가 생길 수 있습니다. 재현성이 중요한 프로젝트라면 하드웨어, 라이브러리 버전, 난수 시드, 병렬 처리 설정, 허용 오차 기준을 함께 기록해야 합니다.

금융·회계 계산은 Decimal 또는 정수 단위로 다룹니다

금액 계산에서는 0.1, 0.01 같은 십진 소수의 의미가 중요합니다. 이진 float로 금액을 계속 누적하면 화면에는 작은 차이처럼 보이더라도 정산, 세금, 반올림 규칙에서 문제가 될 수 있습니다. 따라서 원, 센트 같은 최소 화폐 단위를 정수로 저장하거나 Decimal, 고정소수점 방식을 사용하는 것이 일반적으로 더 안전합니다.

Python에서 Decimal을 사용할 때는 Decimal(0.1)보다 Decimal(‘0.1’)처럼 문자열에서 생성하는 편이 의도한 십진값을 보존하기 쉽습니다. Decimal(0.1)은 이미 이진 float로 근사된 값을 Decimal로 옮기는 것이므로 기대한 0.1과 다르게 보일 수 있습니다. 또한 Decimal은 십진 정확성이 필요한 업무에 유리하지만, 과학 계산 전체를 대체하는 만능 도구는 아닙니다. 성능, 메모리, 라이브러리 호환성, 반올림 정책을 함께 검토해야 합니다.

상황별로 선택이 달라집니다

데이터 분석과 통계 계산

대량 데이터의 평균, 분산, 표준편차를 구할 때는 단순 합산보다 누적 오차와 소거 오차를 신경 써야 합니다. 분산을 E[x²] – E[x]²처럼 계산하면 두 값이 비슷할 때 소거 오차가 커질 수 있습니다. 라이브러리가 제공하는 안정적인 통계 함수를 우선 사용하고, float32 원본 데이터라도 집계 단계에서는 float64 accumulator를 쓰는 선택지를 검토하세요.

과학 계산과 시뮬레이션

시뮬레이션에서는 시간 스텝마다 오차가 누적됩니다. 한 단계의 차이는 작아도 수천, 수백만 번 반복되면 결과에 영향을 줄 수 있습니다. 따라서 기준해 또는 더 높은 정밀도의 reference result를 두고, 단위 테스트에는 정확한 일치가 아니라 허용 오차를 설계하는 것이 좋습니다. 중요한 모델이라면 입력 스케일링, 알고리즘 안정성, 경계 조건에서의 오버플로와 언더플로도 점검해야 합니다.

금융·정산 시스템

금융·정산 시스템에서는 계산 정밀도뿐 아니라 규칙의 일관성이 중요합니다. 중간 계산에서 언제 반올림할지, 최종 표시에서만 반올림할지, 반올림 방식은 half-up인지 half-even인지 사전에 정해야 합니다. 법적·회계적 요구사항이 있는 영역에서는 개발 편의성보다 명세, 감사 가능성, 전문가 검토가 우선입니다.

부동소수점 합산 순서에 따른 오차 누적 비교

흔한 실수와 올바른 대응

출력만 반올림하면 내부 오차도 사라진다고 생각하는 경우

format이나 printf, f-string으로 소수점 자릿수를 제한하면 보기에는 깔끔해집니다. 그러나 내부 저장값과 이후 계산에 쓰이는 값이 바뀌는 것은 아닙니다. 보고서 출력에는 표시 반올림을 쓰되, 계산 안정성 문제는 자료형과 알고리즘으로 해결해야 합니다.

중간마다 round()를 적용하는 경우

오차가 보인다고 매 연산마다 round()를 넣으면 오히려 편향이 생기거나 정보가 일찍 손실될 수 있습니다. 반올림은 필요한 규칙이 있을 때 명시적으로 수행해야 합니다. 특히 금융 계산에서는 중간 반올림과 최종 반올림을 구분하고, 데이터 분석에서는 최종 출력 직전까지 충분한 정밀도를 유지하는 편이 낫습니다.

모든 문제를 Decimal로 해결하려는 경우

Decimal은 십진 표현이 중요한 문제에 강합니다. 하지만 대규모 배열 연산, 선형대수, GPU 가속, 과학 시뮬레이션에서는 float64와 안정적인 알고리즘이 더 적합할 때가 많습니다. 문제의 본질이 십진 금액인지, 물리량 근사 계산인지, 통계적 추정인지에 따라 선택이 달라집니다.

실무 적용 예시로 정리하기

부동소수점 값을 비교해야 한다면 == 대신 허용 오차 비교를 기본값으로 생각하세요. 합계를 정확히 구해야 한다면 단순 루프보다 math.fsum(), pairwise summation, dtype 지정, 보정 합산을 검토하세요. 금액처럼 십진 정확성이 중요한 값은 Decimal 문자열 생성 또는 최소 단위 정수 저장을 우선 고려하세요.

from decimal import Decimal

price = Decimal('0.1') + Decimal('0.2')
print(price == Decimal('0.3'))   # True

반대로 센서 데이터, 이미지 처리, 머신러닝 텐서처럼 근사 계산이 자연스러운 영역에서는 float를 피할 이유가 없습니다. 대신 입력 스케일을 조정하고, 누적 연산의 dtype을 확인하고, 테스트 기준을 tolerance 기반으로 설계하는 것이 현실적입니다.

오차를 없애기보다 관리하는 것이 핵심입니다

수치 정밀도 오차는 대부분의 계산 환경에서 자연스럽게 발생합니다. 중요한 것은 표현 오차, 반올림 오차, 누적 오차, 소거 오차를 구분하고 문제에 맞는 대응을 고르는 것입니다. float64를 쓰면 오차가 줄 수 있지만 사라지지는 않고, Decimal을 쓰면 십진 금액에는 유리하지만 모든 수치 계산의 정답은 아닙니다.

실무적인 수치 정밀도 오차 줄이는 방법은 다섯 가지로 요약할 수 있습니다. 자료형을 목적에 맞게 선택하고, 직접 동등 비교 대신 허용 오차를 쓰며, 합산 순서와 알고리즘을 개선하고, 불안정한 수식은 더 안정적인 형태로 바꾸며, 테스트와 검증 기준을 명시하는 것입니다. 이 조합을 갖추면 0.1 + 0.2의 놀라움에서 벗어나 실제 시스템에서 예측 가능한 숫자 계산을 만들 수 있습니다.