CodeGym /행동 /JAVA 25 SELF /정밀도 문제와 특수 값

정밀도 문제와 특수 값

JAVA 25 SELF
레벨 6 , 레슨 3
사용 가능

1. 소개

컴퓨터가 “똑똑하다면” 0.1 + 0.2는 그냥 0.3이어야 할 것 같죠. 하지만 꼭 그렇진 않습니다. 간단한 예로 살펴보겠습니다.


double x = 0.1;
double y = 0.2;
double sum = x + y;
System.out.println(sum); // 프로그램은 무엇을 출력할까요?
실수 덧셈: 기대는 0.3, 실제는?

이제 0.3과 비교해 보세요:

System.out.println(sum == 0.3); // 여기서는 true일까요, false일까요?

false가 보이더라도 놀라지 마세요!

원인은 메모리에서의 수 표현 방식에 있습니다

컴퓨터는 이진수로 숫자를 다룹니다. 하지만 십진 소수 중 상당수는 유한한 이진 소수로 표현할 수 없습니다. 마치 1/3을 십진 소수(0.333...)로 정확히 쓸 수 없는 것과 같습니다. 예를 들어 0.1은 이진수로 무한 소수이며, 저장을 위해 “반올림”이 필요합니다.

쉽게 말해 double은 때때로 여러분의 숫자를 정확히 저장하는 척하지만, 실제로는 매우 가까운 근사값만 저장합니다.

2. double 연산에서 어떤 ‘이상한 현상’이 생길까요?

실용적인 예로 살펴봅시다.

예제 1. 고전적인 ‘0.1 + 0.2 마법’

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

컴퓨터는 0.3이 아니라 0.30000000000000004를 출력합니다. 차이는 작지만, 예를 들어 금융 같은 분야에서는 이게 치명적일 수 있습니다.

예제 2. 반복 덧셈

double result = 0;
for (int i = 0; i < 10; i++)
{
    result += 0.1;
}
System.out.println(result); // 0.9999999999999999

1.0을 원했지만, 조금 모자라게 나왔습니다. 역시 double 내부의 반올림 때문입니다.

현실 문제에서 왜 중요한가

많은 사람이 “무슨 상관이야, 작은 오차쯤이야!”라고 생각합니다. 결제 분야의 예를 들어 보죠.

여러분의 인터넷 뱅킹이 100건의 0.1유로 거래를 합산한다고 합시다. 프로그램이 루프마다 유로의 십만 분의 일을 “잃는다”면, 은행 규모에서는 이미 실제 돈을 “잃게” 됩니다. 이쯤 되면 회계 담당자가 찾아와서 이렇게 묻겠죠: “우리 돈은 어디 갔죠?!”

3. 실수를 올바르게 비교하는 방법

double이 종종 여러분이 기대한 정확한 값을 담지 못하기 때문에, ==로의 직접 비교는 실패할 수 있습니다. 대신 두 수의 차이의 절댓값을 어떤 아주 작은 수(epsilon)와 비교하는 것이 일반적입니다.

허용 오차를 둔 비교 예

double a = 0.1 + 0.2;
double b = 0.3;
double epsilon = 0.000001;

if (Math.abs(a - b) < epsilon)
{
    System.out.println("거의 같습니다!"); // 이렇게 비교하는 편이 더 안전합니다
}

여기서 우리는 “두 수의 차이가 백만 분의 1보다 작으면 두 수가 같다”고 간주합니다.
참고: 함수 Math.abs(value)는 전달된 숫자의 절댓값을 반환합니다.

4. double의 특수 값: Infinity, NaN, -Infinity

double은 일반 숫자뿐 아니라 특수 값도 담습니다. 수학에서 “하면 안 된다”고 배우는 상황에서 이런 값이 생깁니다.

무한대 (Infinity)

10으로 나누면 어떻게 될까요?

double result = 1.0 / 0.0;
System.out.println(result); // Infinity

Java(및 많은 언어)에서 double에 대한 0으로 나누기는 예외를 던지지 않습니다! 대신 결과는 “양의 무한대”라는 특수 값이 됩니다.

음의 무한대 (-Infinity)

음수를 0으로 나누면 음의 무한대를 얻게 됩니다:

double result = -1.0 / 0.0;
System.out.println(result); // -Infinity

“숫자가 아님” (NaN — Not a Number)

아주 이상한 일을 하면, 예를 들어 음수의 제곱근을 구하려 하면:

double result = Math.sqrt(-1);
System.out.println(result); // NaN

또는 0.0 / 0.0의 결과:

double result = 0.0 / 0.0;
System.out.println(result); // NaN

NaN은 “현실 세계에서 수라고 할 수 없는 모든 것”입니다.

특수 값 확인

Java에는 특수 값을 확인하는 함수가 있습니다:

System.out.println(Double.isInfinite(result));    // 무한대이면 true
System.out.println(Double.isNaN(result));         // NaN이면 true

표: double이 특이한 연산에 어떻게 반응하는가

연산 결과 double에 저장되는 것
1.0 / 0.0
Infinity +∞
-1.0 / 0.0
-Infinity -∞
0.0 / 0.0
NaN 숫자가 아님
Math.sqrt(-1)
NaN 숫자가 아님

5. 실수 처리에서 흔한 실수들

오류 №1: ==로 부동 소수점 수를 비교
가장 흔한 함정은 계산된 부동 소수점 두 수가 같은지 일반 비교로 확인하려는 것입니다. 반올림 오차가 누적되기 때문에 예상과 다른 결과를 얻기 쉽습니다. 항상 허용 오차(epsilon)를 둔 비교를 사용하세요.

오류 №2: 계산 중 나타나는 예상치 못한 NaNInfinity
0으로 나누기나 음수에 대한 제곱근을 제어하지 않으면, 프로그램에 NaN이나 Infinity가 나타나 이후 계산을 모두 “오염”시킬 수 있습니다. Double.isNaN()Double.isInfinite()로 의심스러운 값을 꼭 점검하세요. 불쾌한 놀라움을 피할 수 있습니다.

오류 №3: NaN을 “마커”로 사용
일부 초보자는 “찾지 못했으면 NaN을 반환하자” 같은 용도로 NaN을 “특별한” 값으로 쓰곤 합니다. 하지만 기억하세요: NaN은 까다롭고, == 비교는 동작하지 않습니다! 반드시 전용 메서드로만 검사하세요.

오류 №4: 0.0으로 나눌 때 예외가 발생한다고 기대함
정수 나눗셈과 달리, double에 대한 0.0으로의 나눗셈은 오류를 발생시키지 않고 Infinity 또는 NaN을 반환합니다. 결과는 존재하지만 기대와 다르기 때문에 조용하고 잡기 어려운 버그로 이어질 수 있습니다.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION