[JAVA]실수형 비교하기(epsilon, BigDecimal)

LTEN·2022년 12월 31일
1

JAVA

목록 보기
1/1

코딩하다보면 실수형을 다룰 일이 많은데, 비교 등을 수행할 때 맞게 하고 있는지 항상 찝찝했어서 이번에 한번 정리해보려고 합니다.

1. 실수를 다룰 때 주의할 점
2. epsilon
3. compare 메서드
4. BigDecimal
5. 결론

1. 실수를 다룰 때 주의할 점

컴퓨터가 수를 다루는 방법은 사람과 다릅니다.
기본적으로 2진수를 이용하며, 특히 실수의 경우에는 부호부, 지수부, 가수부로 구분하는 부동소수점 구조로 다루게 되죠.
따라서 실수를 무심코 다루다가는 예상과 다른 결과가 발생할 수 있습니다. 대표적인 예시가 다음 질문이죠.

0.1 + 0.2 == 0.3

아마 관련된 내용을 경험해보셨다면, 위 식은 한번쯤 보셨을겁니다.
처음보신다면 당연히 true가 아닐까 싶겠지만, 실제로 실행해보면 결과는 false입니다.

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

위 코드를 보면, 0.1 + 0.2 값이 0.3이 아닌 0.30000000000000004로 계산된 것을 확인할 수 있습니다.
따라서 a == b 도 false가 되는 것이죠.
컴퓨터가 실수를 다루는 방식때문에 오차가 발생하는 문제인데, 이에 대해 다루는 글은 많기 때문에 이 글에서는 자세히 다루지 않겠습니다.
어쨌든 실수를 부주의하게 다루다가는 전혀 예상치 못한 결과가 발생한다는 것을 확인할 수 있습니다. 이런 코드가 주요 로직에 포함되었다면 큰 문제가 발생하게 될겁니다.

그렇다면 어떻게 다뤄야 오차가 안생길까요?

2. epsilon

첫번째 방법은 epsilon을 사용하는 방법입니다. 어느 정도의 오차를 감안하는 방법이라고 생각하시면 될 것 같습니다.
코드를 통해 확인해보겠습니다.

	double a = 0.1 + 0.2;
    double b = 0.3;
    Double epsilon =1E-5;
        
    System.out.println(Math.abs(a - b) < epsilon);  // true

0.1 + 0.2와 0.3의 차이가 정의한 epsilon보다 작으면 같다고 판단할 수 있습니다.
다만 개인적으로 이 방법은 추천하지 않습니다. 이어서 소개되는 4. BigDecimal을 사용하는 방법이 좋습니다.
이유는 다음과 같습니다.

  • Epsilon 크기에 대한 표준이 없다.
  • 연산 자체의 오차를 없앨 수 없다.

필요에따라 이 방법은 간단한 비교에서만 사용하는 것이 좋습니다.

3. compare 메서드

'=='과 같은 비교 연산자가 아니라 Double.compare 메서드를 사용하면 어떨까요?
결론적으로 말하자면 compare 메서드는 이 문제에 대한 해결책이 아닙니다.

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

	System.out.println(Double.compare(a, b) == 0);   // false

어떻게 보면 당연한 결과입니다. 앞서 확인했듯이 a에는 실제로 0.3과 다른 값이 저장되기 때문이죠.
사실 compare 메서드 내부적으로 epsilon 등을 적용하는 처리가 있지 않을까 기대했지만, 확인해본 결과 그렇지 않습니다.

4. BigDecimal

이번에도 결론적으로 말하자면, 실수를 다룰 때는 BigDecimal을 사용하면 됩니다.
코드를 통해 BigDecimal의 사용법을 먼저 보겠습니다.

BigDecimal a = new BigDecimal("0.1").add(newBigDecimal("0.2"));
BigDecimal b = new BigDecimal("0.3");

System.out.println(a.equals(b));  // true
System.out.println(a);            // 0.3

이제야 0.1 + 0.2가 0.3으로 계산되었습니다.
new 연산자를 사용한 방법 외에 BigDecimal.valueOf를 통해 초기화 하는 방법도 존재합니다.

BigDecimal a = BigDecimal.valueOf(0.1).add(BigDecimal.valueOf(0.2));
BigDecimal b = BigDecimal.valueOf(0.3);
System.out.println(a.equals(b));  // true

이번에도 0.1 + 0.2가 정상적으로 계산되는 것을 확인할 수 있습니다.

둘의 동작은 비슷한듯 다르기 때문에 주의해야 합니다.
성능까지 자세히 들어가면 캐싱 등 고려할 점이 존재하지만 이 글에서는 초기화 관점에서만 다루겠습니다.

new 연산자

new 연산자를 사용할 때는 가능한 문자열의 형태로 생성자에 인자를 전달해야 합니다.
new 연산자는 주어진 값을 최대한 정확하게 나타내려고 하기때문에, 그 값이 바뀔 수 있습니다. 다음 코드가 그 예시입니다.


System.out.println(new BigDecimal(0.01));   // 0.01000000000000000020816681711721685132943093776702880859375
System.out.println(new BigDecimal("0.01"));   // 0.01

Double 값을 그대로 전달하면 0.01이 아니라 0.010000... 으로 초기화 된 것을 확인할 수 있습니다.
반면 문자열의 형태로 초기화한 경우에는 문제없이 초기화 됩니다.

BigDecimal.valueOf
BigDecimal.valueOf의 동작이 조금 더 직관적입니다. 간단하게 생각해서, System.out.println() 으로 출력할 때 나타나는 값 그대로 초기화된다고 생각하면 됩니다.
주의할 점은 이번엔 문자열이 아니라 double 값을 그대로 전달해야 합니다.

System.out.println(BigDecimal.valueOf(0.01));   // 0.01
//System.out.println(BigDecimal.valueOf("0.01")); // 문자열을 전달하면 컴파일 에러

상황에 따라 필요한 방법을 쓰면 되겠지만 다음과 같은 코드는 BigDecimal을 잘못사용한 경우라는 것만 알아두면 될 것 같습니다.

ex)

BigDecimal a = BigDecimal.valueOf(0.1+0.2); // (X)

a에 0.3이 저장될 것이라고 기대하면 잘못이해한 것입니다.
초기화는 valueOf나 new 연산자를 사용하고, 연산은 eqauls나 add와 같은 메서드를 이용해야 합니다.

결론

  • 실수를 다뤄야한다면 BigDecimal을 이용
  • 초기화는 valueOf나 new 연산자를 이용
  • 연산은 eqauls나 add 등 메서드 이용
profile
백엔드

1개의 댓글

comment-user-thumbnail
2024년 4월 20일

안녕하세요!
내용 정리를 잘 해주셔서 혹시 내용을 제 블로그에 퍼가도 괜찮을까요?

답글 달기