JAVA&JSP

Java 소수점 연산에서 오차가 발생할때

junakim 2023. 4. 7. 17:47

한때 아메리칸 드림을 꿈꾸며 해외 사업에 잠시 몸 담았던 적이 있다

프로젝트를 진행하며 수많은 고난과 역경을 겪었지만
그중 가장 곤란했던 문제는 달러 상품 가격을 설정하는 과정에서 발생했다.

소수점 단위의 숫자들을 연산하는 과정에서 미세한 수치지만 오차가 발생했고,

해결 방법을 열심히 찾아본 결과 이 오차는 부동소수점을 사용하는 double 자료형의 문제점인 것으로 확인했다.


오차가 왜 생겼을까?

컴퓨터는 0과 1로 이루어진 2진법을 사용하여 연산을 수행한다.
그렇기 때문에 10진법으로 이루어진 수를 2진법으로 변환하는 과정에서 오차가 발생할 확률이 존재한다.

0.2 를 2진법으로 변환할 경우
0.001100110011... 과 같이 0011 이 무한으로 반복되는 수가 된다.
0.3 을 2진법으로 변환할 경우도 마찬가지로
0.01001100110011... 과 같이 0011 이 무한으로 반복되어
컴퓨터는 0.2 0.3 과 같은 숫자들을 정확한 값으로 저장하지 않고 근사치로 저장하게 된다.

이때 double을 이용하여 이 둘을 연산할 경우

double salePrice = 0.3;
double discountPrice = 0.2;
System.out.println(salePrice - discountPrice);

 

 

위와 같은 오차가 발생하게 된다.

그렇다면 우리는 이러한 오차를 감수해야할까?


해결 방법은?

오차를 제거할 수 있는 방법 2가지를 찾았다.

가장 간단한 해결 방법은 소수점 연산을 하지 않는 것이다.
소수점 자리만큼 10^n 값을 곱해주고 나눠주는 연산을 통해 간단한 문제는 해결할 수 있다.

double salePrice = 0.3;
double discountPrice = 0.2;
System.out.println( ((salePrice*10) - (discountPrice*10)) /10 );

 


그러나 좀 더 정확한 연산을 위해서는 부동소수점인 float나 double 자료형이 아닌 BigDecimal 자료형을 사용해야 한다.

 

BigDecimal

BigDecimal의 초기화 방법은 다음과 같다.

BigDecimal salePrice = new BigDecimal("0.3");

초기화 과정에서 파라미터를 String 형태로 넘겨주는 것이 중요한데,

다른 숫자 형태의 파라미터로도 BigDecimal 초기화는 가능하지만 이때는 앞선 double 형태의 연산과 마찬가지로 오차가 발생할 수 있다.

따라서, 파라미터를 String 형태로 넘겨주는 것이 중요하다.

 

BigDecimal 연산

연산하는 방법은 다음과 같다

double salePrice = 0.3;
double discountPrice = 0.2;


BigDecimal salePrice2 = new BigDecimal(String.valueOf(salePrice));
BigDecimal discountPrice2 = new BigDecimal(String.valueOf(discountPrice));

// 덧셈 연산
System.out.println(salePrice2.add(discountPrice2));
// 뺄셈 연산
System.out.println(salePrice2.subtract(discountPrice2));
// 곱셈 연산
System.out.println(salePrice2.multiply(discountPrice2));

나눗셈의 경우는 추가적으로 옵션을 설정해주어야 하는데

BigDecimal a = new BigDecimal("0.3");
BigDecimal b = new BigDecimal("0.2");

System.out.println(b.divide(a));

위와같이 나누어지지 않는 수를 나누려 할 경우에 다음과 같은 에러가 발생한다.

Non-terminating decimal expansion; no exact representable decimal result.
java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.

 

따라서 나눗셈에는 다음과 같은 옵션이 추가된다.

System.out.println(b.divide(a, 2, RoundingMode.UP));

2번째 매개변수는 표현할 소수점 자리수, 3번째 매개변수는 처리방식을 나타낸다.

상황에 따라 적절히 옵션을 이용하여 사용하면 될 것이다.

 

BigDecimal 비교

compareTo 메서드를 이용하여 BigDecimal 간의 비교를 할 수 있다.

비교하려는 수가 비교되는 수 보다 클 경우 1, 같을 경우 0, 작을 경우 -1 을 리턴한다.

BigDecimal a = new BigDecimal("2");
BigDecimal b = new BigDecimal("1");
BigDecimal c = new BigDecimal("1");

System.out.println(a.compareTo(b));
System.out.println(b.compareTo(a));
System.out.println(b.compareTo(c));


 

글을 작성하다보니 옛 생각이 새록새록 떠오릅니다.

부족한 글이지만 도움이 되길 바랍니다.

 

감사합니다.

반응형