글 목록

최신 글과 검색 결과
DEVELOPMENT

컴퓨터의 계산 실수? 부동 소수점 연산 오류의 비밀과 해결법

간지뽕빨리턴님

이 글의 목차

    반응형

    컴퓨터가 항상 우리가 원하는 계산 결과를 주는 것은 아닙니다.

    0.1 + 0.2 = 0.30000000000000004? 부동소수점 오차를 해결해 보자

    프로그래밍을 하다 보면 의외의 계산 결과가 나오는 경우가 있습니다. 대표적인 예가 0.1 + 0.20.3이 아닌 0.30000000000000004로 출력되는 현상입니다. 처음 마주치면 버그처럼 보이지만, 사실 이것은 거의 모든 프로그래밍 언어에서 동일하게 나타나는 정상적인 동작입니다. 원인은 컴퓨터가 소수를 표현하는 방식인 부동소수점(floating point)에 있습니다. 이 글에서는 부동소수점 오차가 왜 생기는지 원리부터, 언어별로 어떻게 나타나는지, 그리고 실무에서 이를 안전하게 다루는 방법까지 정리하겠습니다.

    목차

      왜 0.1 + 0.2 = 0.3이 아닐까

      컴퓨터는 소수를 2진수로 저장한다

      컴퓨터는 모든 숫자를 2진수(0과 1)로 변환해 메모리에 저장합니다. 정수는 2진수로 정확히 표현되지만, 소수는 사정이 다릅니다. 우리가 10진수에서 1/30.3333…처럼 무한히 반복되는 형태로밖에 적지 못하는 것과 똑같이, 2진수에서는 0.1이나 0.2 같은 값이 무한히 반복되는 소수가 됩니다. 즉 0.1은 2진수로 정확히 떨어지지 않습니다.

      메모리 공간은 유한하기 때문에, 컴퓨터는 이 무한한 2진 소수를 어느 지점에서 잘라 가장 가까운 값으로 반올림해 저장합니다. 이때 생긴 미세한 차이가 바로 부동소수점 오차입니다. 0.10.2 각각에 이미 작은 오차가 들어 있고, 둘을 더하면 그 오차가 함께 더해져 0.30000000000000004라는 결과로 드러나는 것입니다.

      IEEE 754 표준

      대부분의 언어가 사용하는 소수 표현 방식은 IEEE 754라는 국제 표준을 따릅니다. 우리가 흔히 쓰는 64비트 실수(double)는 숫자를 부호(1비트), 지수(11비트), 가수(52비트)로 나누어 저장합니다. 마치 과학적 표기법(예: 1.23 × 10⁵)처럼 '유효숫자 × 2의 거듭제곱' 형태로 표현하는 방식입니다. 이 구조 덕분에 아주 크거나 아주 작은 수까지 폭넓게 다룰 수 있지만, 가수의 비트 수가 유한하므로 정밀도에는 한계가 생깁니다. 부동소수점 오차는 특정 언어의 버그가 아니라, 이 표준을 따르는 모든 환경에서 공통으로 나타나는 현상입니다.

      언어별로 확인해 보기

      같은 연산이 여러 언어에서 동일하게 재현됩니다. Python, JavaScript, Java 모두 같은 IEEE 754 double을 쓰기 때문입니다.

      # Python
      a = 0.1 + 0.2
      print(a)  # 0.30000000000000004
      print(a == 0.3)  # False
      // JavaScript
      console.log(0.1 + 0.2);         // 0.30000000000000004
      console.log(0.1 + 0.2 === 0.3); // false
      // Java
      System.out.println(0.1 + 0.2);  // 0.30000000000000004

      해결 방법

      1. Decimal / 고정소수점 타입 사용

      정확한 10진 소수 계산이 필요할 때는 부동소수점 대신 Decimal 계열의 타입을 사용합니다. Python의 Decimal, Java의 BigDecimal이 대표적입니다. 특히 금액 계산처럼 오차가 절대 허용되지 않는 영역에서는 필수입니다.

      from decimal import Decimal
      
      # 주의: 문자열로 생성해야 정확하다. Decimal(0.1)은 이미 오차를 가진 float를 받는다.
      a = Decimal('0.1') + Decimal('0.2')
      print(a)          # 0.3
      print(a == Decimal('0.3'))  # True

      한 가지 주의할 점은, Decimal(0.1)처럼 숫자 리터럴을 넣으면 이미 오차가 섞인 float가 전달되므로 의미가 없습니다. 반드시 Decimal('0.1')처럼 문자열로 생성해야 합니다.

      2. 비교는 '오차 허용 범위(epsilon)'로

      부동소수점 값은 ==로 직접 비교하면 안 됩니다. 두 값의 차이가 아주 작은 허용 오차(epsilon)보다 작은지를 검사하는 방식으로 비교해야 합니다.

      # 나쁜 예
      print(0.1 + 0.2 == 0.3)  # False
      
      # 좋은 예 1: 직접 epsilon 비교
      print(abs((0.1 + 0.2) - 0.3) < 1e-9)  # True
      
      # 좋은 예 2: 표준 함수 사용 (Python 3.5+)
      import math
      print(math.isclose(0.1 + 0.2, 0.3))  # True

      3. 돈은 '정수(최소 단위)'로 다룬다

      실무에서 가장 널리 쓰이는 패턴입니다. 금액을 실수(예: 1500.50원)로 저장하지 말고, 최소 화폐 단위의 정수로 저장합니다. 원화는 '원' 단위 정수로, 달러는 '센트' 단위 정수로 다루면 부동소수점 오차 자체가 발생하지 않습니다. 화면에 표시할 때만 단위를 나누어 보여주면 됩니다.

      # 달러를 센트(정수)로 저장
      price_cents = 1050      # $10.50
      tax_cents   = 84        # $0.84
      total_cents = price_cents + tax_cents  # 1134, 오차 없음
      
      # 표시할 때만 변환
      print(f"${total_cents / 100:.2f}")  # $11.34

      방법 비교 요약

      방법 언제 쓰나 특징
      Decimal / BigDecimal 정확한 10진 계산이 필요할 때 정확하지만 float보다 느림
      epsilon 비교 두 실수가 같은지 판단할 때 == 대신 isclose 사용
      정수(최소 단위) 금액 등 단위가 고정된 값 오차 원천 차단, 가장 권장
      round() 표시용 자릿수 정리 근본 해결책은 아님

      실무에서의 주의점

      실무에서는 정확한 계산이 필수적인 영역(특히 금융, 회계, 정산)이 많으므로, 이런 곳에서는 부동소수점을 직접 쓰지 말고 Decimal이나 정수 방식을 택하는 것이 안전합니다. 반대로 그래픽, 머신러닝, 과학 계산처럼 약간의 오차가 문제되지 않고 속도가 중요한 영역에서는 float가 더 적합합니다. 핵심은 '내가 다루는 값이 오차를 허용하는가'를 먼저 판단하고, 그에 맞는 타입을 고르는 것입니다.

      마무리

      부동소수점 연산 오차는 버그가 아니라, 유한한 메모리로 무한한 소수를 표현해야 하는 컴퓨터의 구조적 한계에서 비롯되는 필연적 현상입니다. 원리를 이해하고 나면 두려워할 일이 아니라, 상황에 맞는 도구를 고르는 문제가 됩니다. 정확성이 중요하면 Decimal이나 정수로, 비교가 필요하면 epsilon으로, 속도가 중요하면 float로. 값의 성격에 맞는 연산 방식을 선택하는 습관이 신뢰할 수 있는 코드를 만듭니다.