백엔드 멘토링

[부동소수점] 0.1 + 1.1 == 1.1 은 False??

piedra_de_flor 2025. 8. 30. 00:27

 

 


 

🎯 글을 쓰게 된 이유

 

코딩을 하다가 문득 이상한 현상을 발견했다.

double a = 0.1;
double b = 0.2;
System.out.println(a + b == 0.3); // false???

 

분명히 초등학교 수학으로는 0.1 + 0.2 = 0.3인데, Java에서는 false가 나온다.

계산기로 해봐도 0.3이 나오는데, 컴퓨터는 왜 이 간단한 계산을 틀릴까?

혹시 Java만의 문제인가 싶어서 다른 언어들도 테스트해봤다.

  • JavaScript: 0.1 + 0.2 === 0.3 → false
  • Python: 0.1 + 0.2 == 0.3 → False
  • C++: 동일한 현상 발생

도대체 왜 모든 언어에서 이런 일이 일어나는 걸까?

그래서 오늘은 부동소수점 연산의 함정과 Java에서의 해결책에 대해 파헤쳐보기로 했다.

 


 

🔍 실제로 어떤 값이 나오는지 확인해보자

 
public class FloatingPointTest {
    public static void main(String[] args) {
        double a = 0.1;
        double b = 0.2;
        double result = a + b;
        
        System.out.println("0.1 + 0.2 = " + result);
        System.out.println("0.1 + 0.2 == 0.3: " + (result == 0.3));
        System.out.println("실제 저장된 값들:");
        System.out.printf("0.1 = %.17f%n", a);
        System.out.printf("0.2 = %.17f%n", b);
        System.out.printf("0.3 = %.17f%n", 0.3);
        System.out.printf("0.1 + 0.2 = %.17f%n", result);
    }
}

 

 

출력 결과:

0.1 + 0.2 = 0.30000000000000004
0.1 + 0.2 == 0.3: false
실제 저장된 값들:
0.1 = 0.10000000000000001
0.2 = 0.20000000000000001
0.3 = 0.29999999999999999
0.1 + 0.2 = 0.30000000000000004

 

뭔가 이상하다. 0.1이 정확히 0.1이 아니라 0.10000000000000001로 저장되어 있다!

 


💾 컴퓨터가 소수를 저장하는 방식: IEEE 754

 

컴퓨터는 모든 것을 이진법(0과 1)으로 처리한다. 그런데 십진법의 소수를 이진법으로 변환할 때 문제가 생긴다.

 

십진법 0.1을 이진법으로 변환해보자

 

십진법 0.1을 이진법으로 변환하면

0.1 (십진법) = 0.000110011001100110011... (이진법, 무한반복)

 

마치 십진법에서 1/3 = 0.333333...처럼 무한반복되는 것과 같다.

 

IEEE 754 표준의 구조

 

IEEE 754 는 컴퓨터에서 부동소수점을 표현하는 데 가장 널리 쓰이는 표준이다.

 

IEEE 754의 부동 소수점 표현은 크게 세 부분으로 구성되는데,

최상위 비트는 부호를 표시하는 데 사용되며, 지수 부분(exponent)가수 부분(fraction/mantissa)이 있다.

 

부호 비트는 1비트이며 실수를 32비트(float)로 표현할 때는 exp가 8비트, frac이 23비트이며

64비트(double)로 표현할 때는 exp가 11비트, frac이 52비트가 된다.

 

  • 부호 비트 (sign bit): 0이면 양수, 이면 음수
  • 지수부 (exponent): 2의 e승일 때 e+bias. bias는 거듭제곱의 범위가 음수와 양수에 걸쳐 고르게 나타날 수 있도록 정해놓은 오프셋으로, 32비트 자료형에서는 (2^8-1)-1 = 127의 값을 가진다.
  • 가수부 (fraction): 의 형태로 표기한 가수. 왼쪽부터 차례대로 채운다.

 

Java의 double 타입은 64비트로 구성된다

 

 

출처: 위키피디아

 

 

예시 사진을 보고 이해해보자

-118.625를 float형 부동소수점으로 표현해보자.

 

  • 음수이므로, 부호비트는 1 이다.
  • 절대값을 이진법으로 나타내면 1110110.101 이다.
  • 정규화 과정 (2진수를 1.xxx * 2^n 꼴로 변환한다) : 소수점을 왼쪽으로 이동시켜, 왼쪽에는 1 만 남겨둔다. 1.110110101×2⁶
  • 앞의 정수부분 1은 정규화과정을 거치면 모두 1 이므로 무시한다. 
  • 가수부는 소수점의 오른쪽 부분을 그대로 쓰고 부족한 뒤의 비트는 0으로 채운다. frac : 11011010100000000000000
  • 지수는 6 이므로 32비트 형식의 Bias인 127을 더해 6+127 = 133이된다. 이진법으로 변환한 exp : 10000101
  • 결과 : 1 1000 0101 1101 1010 1000 0000 0000 000

 

문제는 여기서 발생한다:

  • 52비트로는 무한반복하는 이진 소수를 정확히 표현할 수 없다
  • 어딘가에서 잘라내야 하므로 반올림 오차가 발생한다

🌍 다른 언어들은??

 

IEEE 754 표준을 따르는 모든 언어에서 동일한 현상이 발생한다:

javascript
// JavaScript
console.log(0.1 + 0.2 === 0.3); // false
console.log(0.1 + 0.2);          // 0.30000000000000004
 
 
python
# Python
print(0.1 + 0.2 == 0.3)  # False
print(0.1 + 0.2)         # 0.30000000000000004

 

 

 

결론: Java의 문제가 아니라 IEEE 754 표준의 특성이다.

 

 


⚠️ 실무에서 이런 문제가 언제 발생할까?

 

1. 금융 계산에서의 치명적 오류

public class MoneyCalculation {
    public static void main(String[] args) {
        double price = 0.10;    // 10센트 상품
        double tax = 0.05;      // 5센트 세금
        double total = price + tax;  // 15센트여야 함
        
        if (total == 0.15) {
            System.out.println("정확한 계산입니다.");
        } else {
            System.out.println("계산 오류 발생!"); // 이게 실행됨
            System.out.printf("예상: 0.15, 실제: %.17f%n", total);
        }
    }
}

 

2. 반복 계산에서의 오차 누적

public class AccumulationError {
    public static void main(String[] args) {
        double sum = 0.0;
        for (int i = 0; i < 10; i++) {
            sum += 0.1;  // 0.1을 10번 더함
        }
        
        System.out.println("sum = " + sum);           // 0.9999999999999999
        System.out.println("sum == 1.0: " + (sum == 1.0));  // false!
        
        // 오차가 계속 누적됨
        System.out.printf("정확한 값: %.17f%n", sum);
    }
}

 

3. 게임에서의 좌표 계산 오류

public class GameCoordinate {
    public static void main(String[] args) {
        double playerX = 0.0;
        
        // 플레이어가 0.1씩 10번 이동
        for (int i = 0; i < 10; i++) {
            playerX += 0.1;
        }
        
        // 목표 지점 도달 확인
        if (playerX == 1.0) {
            System.out.println("목표 지점 도달!");
        } else {
            System.out.println("목표 지점 못 도달..."); // 이게 실행됨
            System.out.printf("현재 위치: %.17f%n", playerX);
        }
    }
}

 

 


💡 Java에서의 해결책들

 

1. BigDecimal 사용 

import java.math.BigDecimal;

public class BigDecimalSolution {
    public static void main(String[] args) {
        // 문자열로 생성해야 정확함
        BigDecimal a = new BigDecimal("0.1");
        BigDecimal b = new BigDecimal("0.2");
        BigDecimal result = a.add(b);
        BigDecimal expected = new BigDecimal("0.3");
        
        System.out.println("BigDecimal 결과: " + result);
        System.out.println("0.3과 같은가? " + result.equals(expected)); // true
        
        // 금융 계산 예시
        BigDecimal price = new BigDecimal("19.99");
        BigDecimal tax = new BigDecimal("1.60");
        BigDecimal total = price.add(tax);
        
        System.out.println("총액: $" + total); // 정확히 21.59
    }
}

 

 

⚠️ BigDecimal 주의사항

// 잘못된 방법 - 이미 부정확한 double을 사용
BigDecimal wrong = new BigDecimal(0.1); 
System.out.println(wrong); // 0.1000000000000000055511151231257827021181583404541015625

// 올바른 방법 - 문자열로 생성
BigDecimal correct = new BigDecimal("0.1");
System.out.println(correct); // 0.1

 

2. 허용 오차(Epsilon) 비교

public class EpsilonComparison {
    private static final double EPSILON = 1e-9; // 0.000000001
    
    public static boolean equals(double a, double b) {
        return Math.abs(a - b) < EPSILON;
    }
    
    public static void main(String[] args) {
        double result = 0.1 + 0.2;
        
        // 직접 비교는 실패
        System.out.println("직접 비교: " + (result == 0.3)); // false
        
        // 오차 허용 비교는 성공
        System.out.println("오차 허용 비교: " + equals(result, 0.3)); // true
        
        // 게임 좌표 예시
        double playerX = 1.0000000000000002;
        double targetX = 1.0;
        
        if (equals(playerX, targetX)) {
            System.out.println("목표 지점 도달!"); // 이제 성공
        }
    }
}

 

 

3. 정수 연산으로 변환

public class IntegerArithmetic {
    public static void main(String[] args) {
        // 센트 단위로 계산 (소수점 2자리)
        int cents1 = 10;  // 0.1달러 = 10센트
        int cents2 = 20;  // 0.2달러 = 20센트
        int totalCents = cents1 + cents2;
        
        double totalDollars = totalCents / 100.0;
        System.out.println("총합: $" + totalDollars); // 정확히 0.3
        
        // 게임 점수 계산 (소수점 1자리)
        int score1 = 15;  // 1.5점 * 10
        int score2 = 23;  // 2.3점 * 10
        int totalScore = score1 + score2;
        
        double finalScore = totalScore / 10.0;
        System.out.println("최종 점수: " + finalScore); // 정확히 3.8
    }
}

 

4. Math.round() 활용

public class RoundingSolution {
    public static void main(String[] args) {
        double result = 0.1 + 0.2;
        
        // 소수점 둘째 자리에서 반올림
        double rounded = Math.round(result * 100.0) / 100.0;
        System.out.println("반올림 결과: " + rounded); // 0.3
        
        // 더 정교한 반올림 함수
        System.out.println("정교한 반올림: " + round(result, 1)); // 0.3
    }
    
    public static double round(double value, int places) {
        if (places < 0) throw new IllegalArgumentException();
        
        long factor = (long) Math.pow(10, places);
        value = value * factor;
        long tmp = Math.round(value);
        return (double) tmp / factor;
    }
}

 

 
 

🚫 절대 하지 말아야 할 것들

// 1. 부동소수점 직접 비교 금지
if (result == 0.3) { } 

// 2. BigDecimal을 double로 생성 금지
BigDecimal wrong = new BigDecimal(0.1);

// 3. 금융 계산에 double 사용 금지
double money = 19.99 + 1.01;

// 4. 반복문에서 부동소수점 증가 금지
for (double i = 0; i != 1.0; i += 0.1) { } // 무한루프

 


🔍 결론

 

부동소수점 연산의 부정확성은 컴퓨터가 소수를 저장하는 방식인 IEEE 754 방식의 문제였다.

즉, Java의 버그가 아니라, 이진법 체계에서 십진법 소수를 표현할 때 발생하는 수학적 한계다.

 

핵심 포인트들:

  1. 문제 인식: 0.1 + 0.2 ≠ 0.3임을 항상 기억
  2. 상황별 대응: 정확성이 중요하면 BigDecimal, 성능이 중요하면 epsilon 비교
  3. 직접 비교 금지: == 대신 적절한 비교 방법 사용
  4. 도메인 고려: 금융은 BigDecimal, 게임은 epsilon, 과학은 상황에 따라

 

이제 더 이상 0.1 + 0.2가 0.3이 아닌 것에 당황하지 말고, 적절한 해결책을 선택해보자.