프로그래밍에서 소수점 반올림은 왜 오차가 생길까?

2025. 12. 3. 11:19·CS(Computer Science)

문제 상황: 반올림 오차

// 평균을 소수점 둘째 자리에서 반올림
double sum = 85 + 90 + 78;
double average = sum / 3;  // 84.333...

System.out.println(Math.round(average * 10) / 10.0);
// 예상: 84.3
// 실제: 84.30000000000001 또는 84.29999999999999

// 더 심각한 경우
double price = 0.1 * 3;  // 0.30000000000000004
System.out.println(Math.round(price * 100));
// 예상: 30
// 실제: 30 (운이 좋으면) 또는 31 (틀림!)

분명히 수학적으로는 정확한 계산인데, 왜 컴퓨터는 이상한 값을 내놓는 걸까? 이 문제를 제대로 이해하려면 두 가지 오차의 근원을 알아야 한다.


첫 번째 오차: 부동소수점 표현 오차

컴퓨터는 2진수를 사용한다

우리가 당연하게 쓰는 0.1이라는 숫자, 컴퓨터에게는 표현할 수 없는 숫자다.

10진수 → 2진수 변환: 0.1 = 1/10 = 1/(2×5)

2진수 변환 과정:
0.1 × 2 = 0.2 → 0
0.2 × 2 = 0.4 → 0
0.4 × 2 = 0.8 → 0
0.8 × 2 = 1.6 → 1
0.6 × 2 = 1.2 → 1
0.2 × 2 = 0.4 → 0  ← 0.2가 다시 나타남! (무한 반복)

결과: 0.0001100110011001100110011...

마치 10진수에서 1/3 = 0.333...이 무한소수인 것처럼, 2진수에서 0.1은 무한소수다.

유한한 메모리에 무한을 담으려면

IEEE 754 double (64비트):
부호(1비트) / 지수부(11비트) / 가수부(52비트)

0.1의 실제 저장값:
0.00011001100110011001100110011001100110011001100110011001

실제 메모리 값:
0.1000000000000000055511151231257827021181583404541015625
System.out.println(new BigDecimal(0.1));
// 0.1000000000000000055511151231257827021181583404541015625

System.out.println(new BigDecimal(0.2));
// 0.200000000000000011102230246251565404236316680908203125

System.out.println(new BigDecimal(0.1 + 0.2));
// 0.3000000000000000444089209850062616169452667236328125

이게 첫 번째 오차다. 소수를 저장하는 순간부터 이미 정확하지 않다.


두 번째 오차: 반올림 연산 오차

반올림 전에 이미 오차가 있다

double value = 2.5;
System.out.println(new BigDecimal(value));
// 2.5 (정확함! 2.5 = 5/2 = 5/2¹ → 2진수에서 유한소수)

double value2 = 2.15;
System.out.println(new BigDecimal(value2));
// 2.149999999999999911182158029987476766109466552734375
// (오차 있음! 2.15 = 215/100 = 215/(2²×5²) → 2진수에서 무한소수)

반올림 시점의 오차 증폭

// 케이스 1: 운이 좋은 경우
double price = 29.95;
double vat = price * 0.1;  // 2.9949999999999997 (실제)
System.out.println(Math.round(vat * 100) / 100.0);  // 2.99

// 케이스 2: 경계값에서 틀리는 경우
double price2 = 24.95;
double vat2 = price2 * 0.1;  // 2.4950000000000001 (실제)
System.out.println(Math.round(vat2 * 100) / 100.0);  // 2.5 (예상: 2.49)

핵심: 연산 결과가 이미 오차를 포함하고, 반올림 경계(0.5)에 걸릴 때 오차 방향에 따라 결과가 달라진다.


왜 특정 숫자만 문제가 될까?

2진수에서 정확한 숫자 vs 부정확한 숫자

유한소수 (정확):
0.5   = 1/2      = 1/2¹       → 0.1
0.25  = 1/4      = 1/2²       → 0.01
0.125 = 1/8      = 1/2³       → 0.001
0.75  = 3/4      = 3/2²       → 0.11

무한소수 (오차):
0.1   = 1/10     = 1/(2×5)    → 0.0001100110011...
0.2   = 1/5      = 1/5        → 0.001100110011...
0.3   = 3/10     = 3/(2×5)    → 0.0100110011...

규칙: 분모가 2의 거듭제곱이면 정확, 5가 포함되면 무한소수

// 정확한 계산
double a = 0.5 + 0.25;  // 0.75 (정확!)
System.out.println(a == 0.75);  // true

// 오차 발생
double b = 0.1 + 0.2;  // 0.30000000000000004
System.out.println(b == 0.3);  // false

어떻게 해결할까?

1. 정수로 변환

핵심 아이디어: 소수점이 문제라면 소수점을 없애버리자. 정수는 2진수에서 항상 정확하게 표현된다.

// double로 계산
double price = 19.95;
double tax = price * 0.1;  // 1.9949999999999997
double rounded = Math.round(tax * 100) / 100.0;  // 불안정

// 정수로 변환
long priceInCents = 1995;  // 19.95 → 1995센트
long taxInCents = (priceInCents * 10 + 50) / 100;  // 200센트
double result = taxInCents / 100.0;  // 2.00 (정확!)

공식: 소수점 n자리 반올림 = (값 × 10^n + 5) / 10^n

장점: 빠르고 정확, 코드 간단
단점: 범위 제한, 스케일 미리 정해야 함
적합: 알고리즘 문제, 금액 계산

2. BigDecimal 사용

핵심 아이디어: 10진수를 10진수 그대로 저장하는 자료형을 사용하자.

// double 생성자 금지 (이미 오차 포함)
BigDecimal wrong = new BigDecimal(0.1);

// 문자열 생성자 사용
BigDecimal price = new BigDecimal("19.95");
BigDecimal tax = price.multiply(new BigDecimal("0.1"));
BigDecimal rounded = tax.setScale(2, RoundingMode.HALF_UP);
System.out.println(rounded);  // 2.00 (정확!)

장점: 완벽한 정확성, 임의 정밀도
단점: 느림, 코드 장황함
적합: 금융 시스템, 법적 정확성 필요한 경우

3. 앱실론 비교

핵심 아이디어: 완벽한 일치 대신 허용 오차 범위 내에서 같다고 판단하자.

final double EPSILON = 1e-9;
double result = 0.1 + 0.2;

// 절대 금지
if (result == 0.3) { ... }

// 오차 범위 내 비교
if (Math.abs(result - 0.3) < EPSILON) {
    // 0.3으로 간주
}

장점: 빠름, 근사값 허용
단점: 엡실론 값 선택 어려움
적합: 과학 계산, 근사 허용되는 경우


정리

오차가 생기는 이유

  1. 표현 오차: 2진수에서 분모에 5가 있으면 무한소수
  2. 연산 오차: 오차값들의 계산 → 오차 증폭 → 반올림 경계에서 틀림

황금 규칙

  1. double == double은 절대 금지
  2. 정수로 바꿀 수 있으면 바꿔라
  3. BigDecimal은 문자열 생성자로
  4. 반올림은 최종 출력 직전에만

🔗 GitHub 저장소

  • 전체 프로젝트: blog-code-examples
  • 이 글의 예제: floatingpoint 패키지

'CS(Computer Science)' 카테고리의 다른 글

응답 지연 시간: 실제로 측정해보기  (0) 2026.01.01
'CS(Computer Science)' 카테고리의 다른 글
  • 응답 지연 시간: 실제로 측정해보기
zeromok
zeromok
  • zeromok
    지극히개발적인
    zeromok
  • 전체
    오늘
    어제
    • 전체글🦖
      • Java & Spring
      • CS(Computer Science)
      • Data Structure(자료구조)
      • 알고리즘
      • 독서
        • 가상 면접 사례로 배우는 대규모 시스템 설계 기초
      • 동시성 주문&재고 프로젝트
  • 블로그 메뉴

    • 홈
    • 방명록
    • GitHub
  • 링크

  • 인기 글

  • 태그

    java
    플로이드워셜
    알고리즘
    백준
    JMH
    투포인터
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
zeromok
프로그래밍에서 소수점 반올림은 왜 오차가 생길까?
상단으로

티스토리툴바